I have worked on my own solution, so for anyone who would like a simple Despeckle function that works without dependencies.
The downside however is the pixels that are connected to the letters are not removed, and some of the smaller letters are seen as spots.
The result I get with "maxSpotSize = 10" is this

The way it works is by using a fill on every pixel and measuring its area size (to the maxSpotSize limit you put it on), for speed reasons it only works with black/white images, but you could modify it to support a more complex fill with a tolerance.
LockBitmap
First of all, (mentioned by Reza Aghaei) make use of a LockBitmap class in your project to get access to the pixels in an efficient way.
Put this code inside a new class and change the namespace to your project's namespace:
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace //<YOUR NAMESPACE>//
{
public class LockBitmap : IDisposable
{
public void Dispose()
{
UnlockBits();
}
public Bitmap source = null;
IntPtr Iptr = IntPtr.Zero;
BitmapData bitmapData = null;
public byte[] Pixels { get; set; }
public int Depth { get; private set; }
public int Width { get; private set; }
public int Height { get; private set; }
public LockBitmap(Bitmap source)
{
this.source = source;
LockBits();
}
/// <summary>
/// Lock bitmap data
/// </summary>
private void LockBits()
{
try
{
// Get width and height of bitmap
Width = source.Width;
Height = source.Height;
// get total locked pixels count
int PixelCount = Width * Height;
// Create rectangle to lock
Rectangle rect = new Rectangle(0, 0, Width, Height);
// get source bitmap pixel format size
Depth = System.Drawing.Bitmap.GetPixelFormatSize(source.PixelFormat);
// Check if bpp (Bits Per Pixel) is 8, 24, or 32
if (Depth != 8 && Depth != 24 && Depth != 32)
{
throw new ArgumentException("Only 8, 24 and 32 bpp images are supported.");
}
// Lock bitmap and return bitmap data
bitmapData = source.LockBits(rect, ImageLockMode.ReadWrite,
source.PixelFormat);
// create byte array to copy pixel values
int step = Depth / 8;
Pixels = new byte[PixelCount * step];
Iptr = bitmapData.Scan0;
// Copy data from pointer to array
Marshal.Copy(Iptr, Pixels, 0, Pixels.Length);
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// Unlock bitmap data
/// </summary>
private void UnlockBits()
{
try
{
// Copy data from byte array to pointer
Marshal.Copy(Pixels, 0, Iptr, Pixels.Length);
// Unlock bitmap data
source.UnlockBits(bitmapData);
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// Get the color of the specified pixel
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public Color GetPixel(int x, int y)
{
// Get color components count
int cCount = Depth / 8;
// Get start index of the specified pixel
int i = ((y * Width) + x) * cCount;
if (i > Pixels.Length - cCount)
throw new IndexOutOfRangeException();
if (Depth == 32) // For 32 bpp get Red, Green, Blue and Alpha
{
byte b = Pixels[i];
byte g = Pixels[i + 1];
byte r = Pixels[i + 2];
byte a = Pixels[i + 3]; // a
return Color.FromArgb(a, r, g, b);
}
if (Depth == 24) // For 24 bpp get Red, Green and Blue
{
byte b = Pixels[i];
byte g = Pixels[i + 1];
byte r = Pixels[i + 2];
return Color.FromArgb(r, g, b);
}
if (Depth == 8)
// For 8 bpp get color value (Red, Green and Blue values are the same)
{
byte c = Pixels[i];
return Color.FromArgb(c, c, c);
}
return Color.Empty;
}
/// <summary>
/// Get the red color of the specified pixel
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
public int GetRedColor(int x, int y)
{
// Get color components count
int cCount = Depth / 8;
// Get start index of the specified pixel
int i = ((y * Width) + x) * cCount;
if (i > Pixels.Length - cCount)
throw new IndexOutOfRangeException();
if (Depth == 32 || Depth == 24) // For 32 bpp get Red, Green, Blue and Alpha
{
byte r = Pixels[i + 2];
return r;
}
if (Depth == 8)
// For 8 bpp get color value (Red, Green and Blue values are the same)
{
byte c = Pixels[i];
return c;
}
return 0;
}
/// <summary>
/// Set the color of the specified pixel
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="color"></param>
public void SetPixel(int x, int y, Color color)
{
// Get color components count
int cCount = Depth / 8;
// Get start index of the specified pixel
int i = ((y * Width) + x) * cCount;
if (Depth == 32) // For 32 bpp set Red, Green, Blue and Alpha
{
Pixels[i] = color.B;
Pixels[i + 1] = color.G;
Pixels[i + 2] = color.R;
Pixels[i + 3] = color.A;
}
if (Depth == 24) // For 24 bpp set Red, Green and Blue
{
Pixels[i] = color.B;
Pixels[i + 1] = color.G;
Pixels[i + 2] = color.R;
}
if (Depth == 8)
// For 8 bpp set color value (Red, Green and Blue values are the same)
{
Pixels[i] = color.B;
}
}
}
}
Despeckle code
Now you can put this code somewhere in a static class, so it works like a extension method on a image or bitmap.
public static Image Despeckle(this Image image, int maxSpotSize)
{
return ((Bitmap)image).Despeckle(maxSpotSize);
}
public static Bitmap Despeckle(this Bitmap bitmap, int maxSpotSize)
{
if (maxSpotSize == 0) { return bitmap; }
Bitmap outputBitmap = new Bitmap(bitmap);
using (LockBitmap outputLockBitmap = new LockBitmap(outputBitmap))
{
int width = outputBitmap.Width;
int height = outputBitmap.Height;
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
// Check if the pixel is white
int pixelRed = outputLockBitmap.GetRedColor(x, y);
if (pixelRed == 255)
{
//You can turn this on if you also like to fill white spots in black areas (this will take more cpu time)
//outputLockBitmap.SpotFill(x, y, Color.Black, maxSpotSize);
}
else if (pixelRed == 0)
{
outputLockBitmap.SpotFill(x, y, Color.White, maxSpotSize);
}
}
}
}
return outputBitmap;
}
public static int SpotFill(this LockBitmap bitmap, int startX, int startY, Color fillColor, int maxSpotSize)
{
int targetColor = bitmap.GetRedColor(startX, startY);
int pixelCount = 0;
int width = bitmap.Width;
int height = bitmap.Height;
// Search for connected pixels, and save them
int startPixel = startX * width + startY;
Queue<int> queue = new Queue<int>(maxSpotSize);
queue.Enqueue(startPixel);
HashSet<int> changedPixels = new HashSet<int>();
while (queue.Count > 0)
{
int pixel = queue.Dequeue();
int x = pixel / width;
int y = pixel % width;
if (x < 0 || x >= width || y < 0 || y >= height)
continue;
if (bitmap.GetRedColor(x, y) != targetColor)
continue;
if (changedPixels.Contains(pixel))
continue;
changedPixels.Add(pixel);
pixelCount++;
if (pixelCount > maxSpotSize)
return 0;
queue.Enqueue((x + 1) * width + y);
queue.Enqueue((x - 1) * width + y);
queue.Enqueue(x * width + (y + 1));
queue.Enqueue(x * width + (y - 1));
}
// Apply the fill
if (pixelCount < maxSpotSize)
{
foreach (int pixel in changedPixels)
{
int x = pixel / width;
int y = pixel % width;
bitmap.SetPixel(x, y, fillColor);
}
return pixelCount;
}
return 0;
}
Using the function
So you could do something like this:
Image image = Image.FromFile("C:\\Input.png");
image = image.Despeckle(maxSpotSize: 10);
image.Save("C:\\Output.png");