using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; namespace SauvolaTest { public static class SauvolaBinarization { /// /// Sauvola 二值化算法实现 /// public static class SauvolaBinarizer { /// /// 应用 Sauvola 二值化 /// /// 8bpp 灰度源图像 /// 局部窗口大小 (奇数) /// 修正系数 (0.2-0.5) /// 动态范围 (通常 128) public static Bitmap Apply(Bitmap source, int windowSize = 25, double k = 0.2, double r = 128.0) { if (source == null) throw new ArgumentNullException(nameof(source)); if (source.PixelFormat != PixelFormat.Format8bppIndexed) { throw new ArgumentException("输入图像必须是 8bpp 灰度格式。请先调用 ConvertToGrayscaleFast。", nameof(source)); } int width = source.Width; int height = source.Height; Bitmap result = new Bitmap(width, height, PixelFormat.Format8bppIndexed); // 复制调色板 result.Palette = source.Palette; BitmapData srcData = source.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed); BitmapData resData = result.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); try { unsafe { byte* srcPtr = (byte*)srcData.Scan0; byte* resPtr = (byte*)resData.Scan0; int stride = srcData.Stride; // 1. 构建积分图 (Integral Image) 和 平方积分图 // 使用 long 防止溢出 (最大像素和: 255 * 10^6 约等于 2.5*10^8, int 足够,但平方和需要 long) // 尺寸宽+1, 高+1 以便处理边界 long[] integral = new long[(width + 1) * (height + 1)]; long[] squaredIntegral = new long[(width + 1) * (height + 1)]; Stopwatch sw = Stopwatch.StartNew(); for (int y = 0; y < height; y++) { long rowSum = 0; long rowSqSum = 0; for (int x = 0; x < width; x++) { byte pixel = srcPtr[y * stride + x]; rowSum += pixel; rowSqSum += (long)pixel * pixel; // I(y+1, x+1) = I(y, x+1) + RowSum int currentIndex = (y + 1) * (width + 1) + (x + 1); int topIndex = y * (width + 1) + (x + 1); integral[currentIndex] = integral[topIndex] + rowSum; squaredIntegral[currentIndex] = squaredIntegral[topIndex] + rowSqSum; } } sw.Stop(); Console.WriteLine("积分图耗时:{0}", sw.ElapsedMilliseconds); int halfWin = windowSize / 2; Stopwatch Sauvolastopwatch = new Stopwatch(); // 2. 计算阈值并二值化 for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // 窗口边界 int x1 = Math.Max(0, x - halfWin); int y1 = Math.Max(0, y - halfWin); int x2 = Math.Min(width - 1, x + halfWin); int y2 = Math.Min(height - 1, y + halfWin); // 窗口内像素数 int count = (x2 - x1 + 1) * (y2 - y1 + 1); // 从积分图获取区域和 long sum = GetRegionSum(integral, x1, y1, x2, y2, width); long sqSum = GetRegionSum(squaredIntegral, x1, y1, x2, y2, width); // 均值 double mean = (double)sum / count; // 方差 = E[X^2] - (E[X])^2 double variance = ((double)sqSum / count) - (mean * mean); if (variance < 0) variance = 0; // 防止浮点误差 double stdDev = Math.Sqrt(variance); // Sauvola 公式 double threshold = mean * (1 + k * ((stdDev / r) - 1)); // 比较 byte currentPixel = srcPtr[y * stride + x]; resPtr[y * stride + x] = (byte)(currentPixel > threshold ? 255 : 0); } } Sauvolastopwatch.Stop(); Console.WriteLine("二值化耗时:{0}", Sauvolastopwatch.ElapsedMilliseconds); } } finally { source.UnlockBits(srcData); result.UnlockBits(resData); } return result; } private static long GetRegionSum(long[] integralImg, int x1, int y1, int x2, int y2, int width) { // 积分图坐标需要 +1 int w = width + 1; // A - B - C + D // A: (y2+1, x2+1) // B: (y1, x2+1) // C: (y2+1, x1) // D: (y1, x1) long a = integralImg[(y2 + 1) * w + (x2 + 1)]; long b = integralImg[y1 * w + (x2 + 1)]; long c = integralImg[(y2 + 1) * w + x1]; long d = integralImg[y1 * w + x1]; return a - b - c + d; } /// /// 高效串行版 Sauvola 二值化 (使用 fixed 指针,无 Parallel 限制) /// public static Bitmap ApplyFast(Bitmap source, int windowSize = 25, double k = 0.2, double r = 128.0) { if (source == null) throw new ArgumentNullException(nameof(source)); if (source.PixelFormat != PixelFormat.Format8bppIndexed) { throw new ArgumentException("输入图像必须是 8bpp 灰度格式。", nameof(source)); } int width = source.Width; int height = source.Height; Bitmap result = new Bitmap(width, height, PixelFormat.Format8bppIndexed); result.Palette = source.Palette; BitmapData srcData = source.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed); BitmapData resData = result.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); try { unsafe { byte* srcPtr = (byte*)srcData.Scan0; byte* resPtr = (byte*)resData.Scan0; int stride = srcData.Stride; // 预计算常数 double kDivR = k / r; int halfWin = windowSize / 2; int wPlus1 = width + 1; // 1. 构建积分图 int[] integral = new int[(width + 1) * (height + 1)]; long[] squaredIntegral = new long[(width + 1) * (height + 1)]; // 固定数组以使用指针加速构建 fixed (int* pInt = integral) fixed (long* pSq = squaredIntegral) { for (int y = 0; y < height; y++) { int rowSum = 0; long rowSqSum = 0; // 指针定位到当前行和上一行的起始位置 // 积分图每行有 width + 1 个元素 int* prevRowInt = pInt + y * wPlus1; int* currRowInt = pInt + (y + 1) * wPlus1; long* prevRowSq = pSq + y * wPlus1; long* currRowSq = pSq + (y + 1) * wPlus1; byte* rowSrc = srcPtr + y * stride; for (int x = 0; x < width; x++) { byte pixel = rowSrc[x]; rowSum += pixel; rowSqSum += (long)pixel * pixel; // 指针偏移 x+1 currRowInt[x + 1] = prevRowInt[x + 1] + rowSum; currRowSq[x + 1] = prevRowSq[x + 1] + rowSqSum; } } } // 2. 串行二值化 (使用 fixed 指针加速读取) // 对于大多数中等分辨率图像,串行+指针比 Parallel+数组索引更快或相当 fixed (int* pInt = integral) fixed (long* pSq = squaredIntegral) { for (int y = 0; y < height; y++) { byte* rowSrc = srcPtr + y * stride; byte* rowRes = resPtr + y * stride; for (int x = 0; x < width; x++) { // 窗口边界 int x1 = x - halfWin; int y1 = y - halfWin; int x2 = x + halfWin; int y2 = y + halfWin; // 边界裁剪 if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; if (x2 >= width) x2 = width - 1; if (y2 >= height) y2 = height - 1; // 计算窗口内像素数 int w = x2 - x1 + 1; int h = y2 - y1 + 1; int count = w * h; // 从积分图获取区域和 (指针访问) // 索引: (y * wPlus1) + x int idxA = (y2 + 1) * wPlus1 + (x2 + 1); int idxB = y1 * wPlus1 + (x2 + 1); int idxC = (y2 + 1) * wPlus1 + x1; int idxD = y1 * wPlus1 + x1; int sum = pInt[idxA] - pInt[idxB] - pInt[idxC] + pInt[idxD]; long sqSum = pSq[idxA] - pSq[idxB] - pSq[idxC] + pSq[idxD]; // 计算均值和方差 double mean = (double)sum / count; double variance = ((double)sqSum / count) - (mean * mean); if (variance < 0) variance = 0; double stdDev = Math.Sqrt(variance); // Sauvola 阈值公式 double threshold = mean * (1.0 + kDivR * stdDev - k); // 二值化 rowRes[x] = (byte)(rowSrc[x] > threshold ? 255 : 0); } } } } } finally { source.UnlockBits(srcData); result.UnlockBits(resData); } return result; } /// /// 高效串行版 Sauvola 二值化 (使用 fixed 指针,无 Parallel 限制) /// public static Bitmap ApplyParallelGCHandle(Bitmap source, int windowSize = 25, double k = 0.2, double r = 128.0) { if (source == null) throw new ArgumentNullException(nameof(source)); if (source.PixelFormat != PixelFormat.Format8bppIndexed) { throw new ArgumentException("输入图像必须是 8bpp 灰度格式。", nameof(source)); } int width = source.Width; int height = source.Height; Bitmap result = new Bitmap(width, height, PixelFormat.Format8bppIndexed); result.Palette = source.Palette; BitmapData srcData = source.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed); BitmapData resData = result.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); try { unsafe { byte* srcPtr = (byte*)srcData.Scan0; byte* resPtr = (byte*)resData.Scan0; int stride = srcData.Stride; // 预计算常数 double kDivR = k / r; int halfWin = windowSize / 2; int wPlus1 = width + 1; // 1. 构建积分图 int[] integral = new int[(width + 1) * (height + 1)]; long[] squaredIntegral = new long[(width + 1) * (height + 1)]; // 固定数组以使用指针加速构建 fixed (int* pInt = integral) fixed (long* pSq = squaredIntegral) { for (int y = 0; y < height; y++) { int rowSum = 0; long rowSqSum = 0; // 指针定位到当前行和上一行的起始位置 // 积分图每行有 width + 1 个元素 int* prevRowInt = pInt + y * wPlus1; int* currRowInt = pInt + (y + 1) * wPlus1; long* prevRowSq = pSq + y * wPlus1; long* currRowSq = pSq + (y + 1) * wPlus1; byte* rowSrc = srcPtr + y * stride; for (int x = 0; x < width; x++) { byte pixel = rowSrc[x]; rowSum += pixel; rowSqSum += (long)pixel * pixel; // 指针偏移 x+1 currRowInt[x + 1] = prevRowInt[x + 1] + rowSum; currRowSq[x + 1] = prevRowSq[x + 1] + rowSqSum; } } } // 2. 串行二值化 (使用 fixed 指针加速读取) // 对于大多数中等分辨率图像,串行+指针比 Parallel+数组索引更快或相当 fixed (int* pInt = integral) fixed (long* pSq = squaredIntegral) { for (int y = 0; y < height; y++) { byte* rowSrc = srcPtr + y * stride; byte* rowRes = resPtr + y * stride; for (int x = 0; x < width; x++) { // 窗口边界 int x1 = x - halfWin; int y1 = y - halfWin; int x2 = x + halfWin; int y2 = y + halfWin; // 边界裁剪 if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; if (x2 >= width) x2 = width - 1; if (y2 >= height) y2 = height - 1; // 计算窗口内像素数 int w = x2 - x1 + 1; int h = y2 - y1 + 1; int count = w * h; // 从积分图获取区域和 (指针访问) // 索引: (y * wPlus1) + x int idxA = (y2 + 1) * wPlus1 + (x2 + 1); int idxB = y1 * wPlus1 + (x2 + 1); int idxC = (y2 + 1) * wPlus1 + x1; int idxD = y1 * wPlus1 + x1; int sum = pInt[idxA] - pInt[idxB] - pInt[idxC] + pInt[idxD]; long sqSum = pSq[idxA] - pSq[idxB] - pSq[idxC] + pSq[idxD]; // 计算均值和方差 double mean = (double)sum / count; double variance = ((double)sqSum / count) - (mean * mean); if (variance < 0) variance = 0; double stdDev = Math.Sqrt(variance); // Sauvola 阈值公式 double threshold = mean * (1.0 + kDivR * stdDev - k); // 二值化 rowRes[x] = (byte)(rowSrc[x] > threshold ? 255 : 0); } } } } } finally { source.UnlockBits(srcData); result.UnlockBits(resData); } return result; } /// /// 并行版 Sauvola 二值化 (使用 GCHandle 解决 fixed 指针在 Lambda 中的限制) /// /// 8bpp 灰度源图像 /// 局部窗口大小 (奇数) /// 修正系数 (0.2-0.5) /// 动态范围 (通常 128) public static Bitmap ApplyParallel(Bitmap source, int windowSize = 25, double k = 0.2, double r = 128.0) { if (source == null) throw new ArgumentNullException(nameof(source)); if (source.PixelFormat != PixelFormat.Format8bppIndexed) { throw new ArgumentException("输入图像必须是 8bpp 灰度格式。请先调用 ConvertToGrayscaleFast。", nameof(source)); } int width = source.Width; int height = source.Height; Bitmap result = new Bitmap(width, height, PixelFormat.Format8bppIndexed); // 复制调色板 result.Palette = source.Palette; BitmapData srcData = source.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed); BitmapData resData = result.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); // 预计算常数 double kDivR = k / r; int halfWin = windowSize / 2; int wPlus1 = width + 1; // 积分图宽度 // 分配积分图数组 // 使用 int 存储普通积分图以优化缓存 (假设图像总和不超过 int.MaxValue) int[] integral = new int[wPlus1 * (height + 1)]; // 平方积分图必须使用 long long[] squaredIntegral = new long[wPlus1 * (height + 1)]; try { unsafe { byte* srcPtr = (byte*)srcData.Scan0; byte* resPtr = (byte*)resData.Scan0; int stride = srcData.Stride; // --------------------------------------------------------- // 1. 构建积分图 (串行) // --------------------------------------------------------- // 这里可以使用 fixed,因为不在 Parallel 内部 fixed (int* pInt = integral) fixed (long* pSq = squaredIntegral) { for (int y = 0; y < height; y++) { int rowSum = 0; long rowSqSum = 0; // 指针定位到当前行和上一行 int* prevRowInt = pInt + y * wPlus1; int* currRowInt = pInt + (y + 1) * wPlus1; long* prevRowSq = pSq + y * wPlus1; long* currRowSq = pSq + (y + 1) * wPlus1; byte* rowSrc = srcPtr + y * stride; for (int x = 0; x < width; x++) { byte pixel = rowSrc[x]; rowSum += pixel; rowSqSum += (long)pixel * pixel; // 积分图递推公式 currRowInt[x + 1] = prevRowInt[x + 1] + rowSum; currRowSq[x + 1] = prevRowSq[x + 1] + rowSqSum; } } } // --------------------------------------------------------- // 2. 并行二值化 (使用 GCHandle) // --------------------------------------------------------- // 使用 GCHandle 固定数组内存,防止 GC 移动它们 // 这样获取的指针可以在 Parallel.For 的 Lambda 中安全使用 GCHandle hIntegral = GCHandle.Alloc(integral, GCHandleType.Pinned); GCHandle hSqIntegral = GCHandle.Alloc(squaredIntegral, GCHandleType.Pinned); try { // 获取固定内存的地址指针 int* pIntFixed = (int*)hIntegral.AddrOfPinnedObject(); long* pSqFixed = (long*)hSqIntegral.AddrOfPinnedObject(); // 并行处理每一行 Parallel.For(0, height, y => { byte* rowSrc = srcPtr + y * stride; byte* rowRes = resPtr + y * stride; for (int x = 0; x < width; x++) { // 计算窗口边界 int x1 = x - halfWin; int y1 = y - halfWin; int x2 = x + halfWin; int y2 = y + halfWin; // 边界裁剪 if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; if (x2 >= width) x2 = width - 1; if (y2 >= height) y2 = height - 1; // 窗口像素数量 int w = x2 - x1 + 1; int h = y2 - y1 + 1; int count = w * h; // 计算积分图索引 // 公式: Index = y * wPlus1 + x int idxA = (y2 + 1) * wPlus1 + (x2 + 1); int idxB = y1 * wPlus1 + (x2 + 1); int idxC = (y2 + 1) * wPlus1 + x1; int idxD = y1 * wPlus1 + x1; // 获取区域和 (指针访问,无边界检查开销) int sum = pIntFixed[idxA] - pIntFixed[idxB] - pIntFixed[idxC] + pIntFixed[idxD]; long sqSum = pSqFixed[idxA] - pSqFixed[idxB] - pSqFixed[idxC] + pSqFixed[idxD]; // 计算均值 double mean = (double)sum / count; // 计算方差: E[X^2] - (E[X])^2 double variance = ((double)sqSum / count) - (mean * mean); // 防止浮点误差导致负数 if (variance < 0) variance = 0; // 标准差 double stdDev = Math.Sqrt(variance); // Sauvola 阈值公式: T = m * (1 + k * (s/r - 1)) // 优化为: T = m * (1 + (k/r)*s - k) double threshold = mean * (1.0 + kDivR * stdDev - k); // 二值化判定 rowRes[x] = (byte)(rowSrc[x] > threshold ? 255 : 0); } }); } finally { // 必须释放 GCHandle,否则会导致内存泄漏 if (hIntegral.IsAllocated) hIntegral.Free(); if (hSqIntegral.IsAllocated) hSqIntegral.Free(); } } } finally { source.UnlockBits(srcData); result.UnlockBits(resData); } return result; } // 可选:快速近似平方根 (如果 Math.Sqrt 仍太慢) private static double ApproxSqrt(double x) { if (x <= 0) return 0; // 牛顿迭代法一次迭代,或者使用位操作黑客技巧 // 这里简单返回 Math.Sqrt,因为在 .NET Core/.NET 5+ 中 Math.Sqrt 已经非常优化 // 如果在 .NET Framework 且极追求性能,可替换为特定算法 return Math.Sqrt(x); } } } }