diff --git a/nQuant.Master/Otsu.cs b/nQuant.Master/Otsu.cs index 8c05ffb..5779a9b 100644 --- a/nQuant.Master/Otsu.cs +++ b/nQuant.Master/Otsu.cs @@ -1,6 +1,6 @@ /* Otsu's Image Segmentation Method Copyright (C) 2009 Tolga Birdal - Copyright (c) 2018-2021 Miller Cy Chan + Copyright (c) 2018-2024 Miller Cy Chan */ using nQuant.Master; @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; +using System.Linq; namespace OtsuThreshold { @@ -94,7 +95,7 @@ private short GetOtsuThreshold(int[] pixels) return FindMax(vet, 256); } - private void Threshold(int[] pixels, short thresh, float weight = 1f) + private void Threshold(int[] pixels, int[] dest, short thresh, float weight = 1f) { var maxThresh = (byte)thresh; if (thresh >= 200) @@ -104,17 +105,158 @@ private void Threshold(int[] pixels, short thresh, float weight = 1f) thresh = 200; } - var minThresh = (byte)(thresh * weight); + var minThresh = (byte)(thresh * (m_transparentPixelIndex >= 0 ? .9f : weight)); + var shadow = m_transparentPixelIndex >= 0 ? 3.5 : 3; for (int i = 0; i < pixels.Length; ++i) { var c = Color.FromArgb(pixels[i]); - if (c.R + c.G + c.B > maxThresh * 3) - pixels[i] = Color.FromArgb(c.A, Byte.MaxValue, Byte.MaxValue, Byte.MaxValue).ToArgb(); - else if (m_transparentPixelIndex >= 0 || c.R + c.G + c.B < minThresh * 3) - pixels[i] = Color.FromArgb(c.A, 0, 0, 0).ToArgb(); + if (c.A < alphaThreshold && c.R + c.G + c.B > maxThresh * 3) + dest[i] = Color.FromArgb(c.A, Byte.MaxValue, Byte.MaxValue, Byte.MaxValue).ToArgb(); + else if (c.R + c.G + c.B < minThresh * shadow) + dest[i] = Color.FromArgb(c.A, 0, 0, 0).ToArgb(); } } + private int[] CannyFilter(int width, int[] pixelsGray, double lowerThreshold, double higherThreshold) { + int height = pixelsGray.Length / width; + int area = width * height; + + var pixelsCanny = Enumerable.Repeat(Color.White.ToArgb(), area).ToArray(); + + var gx = new int[3, 3]{{-1, 0, 1}, {-2, 0, 2}, {-1, 0, 1}}; + var gy = new int[3, 3]{{-1, -2, -1}, {0, 0, 0}, {1, 2, 1}}; + var G = new double[area]; + var theta = new int[area]; + var largestG = 0.0; + + // perform canny edge detection on everything but the edges + for (int i = 1; i < height - 1; ++i) { + for (int j = 1; j < width - 1; ++j) { + // find gx and gy for each pixel + var gxValue = 0.0; + var gyValue = 0.0; + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; ++y) { + var c = Color.FromArgb(pixelsGray[(i + x) * width + j + y]); + gxValue += gx[1 - x, 1 - y] * c.G; + gyValue += gy[1 - x, 1 - y] * c.G; + } + } + + int center = i * width + j; + // calculate G and theta + G[center] = Math.Sqrt(Math.Pow(gxValue, 2) + Math.Pow(gyValue, 2)); + var atanResult = Math.Atan2(gyValue, gxValue) * 180.0 / Math.PI; + theta[center] = (int)(180.0 + atanResult); + + if (G[center] > largestG) + largestG = G[center]; + + // setting the edges + if (i == 1) { + G[center - 1] = G[center]; + theta[center - 1] = theta[center]; + } + else if (j == 1) { + G[center - width] = G[center]; + theta[center - width] = theta[center]; + } + else if (i == height - 1) { + G[center + 1] = G[center]; + theta[center + 1] = theta[center]; + } + else if (j == width - 1) { + G[center + width] = G[center]; + theta[center + width] = theta[center]; + } + + // setting the corners + if (i == 1 && j == 1) { + G[center - width - 1] = G[center]; + theta[center - width - 1] = theta[center]; + } + else if (i == 1 && j == width - 1) { + G[center - width + 1] = G[center]; + theta[center - width + 1] = theta[center]; + } + else if (i == height - 1 && j == 1) { + G[center + width - 1] = G[center]; + theta[center + width - 1] = theta[center]; + } + else if (i == height - 1 && j == width - 1) { + G[center + width + 1] = G[center]; + theta[center + width + 1] = theta[center]; + } + + // to the nearest 45 degrees + theta[center] = (int) Math.Round(theta[center] / 45.0) * 45; + } + } + + largestG *= .5; + + // non-maximum suppression + for (int i = 1; i < height - 1; ++i) { + for (int j = 1; j < width - 1; ++j) { + int center = i * width + j; + if (theta[center] == 0 || theta[center] == 180) { + if (G[center] < G[center - 1] || G[center] < G[center + 1]) + G[center] = 0; + } + else if (theta[center] == 45 || theta[center] == 225) { + if (G[center] < G[center + width + 1] || G[center] < G[center - width - 1]) + G[center] = 0; + } + else if (theta[center] == 90 || theta[center] == 270) { + if (G[center] < G[center + width] || G[center] < G[center - width]) + G[center] = 0; + } + else { + if (G[center] < G[center + width - 1] || G[center] < G[center - width + 1]) + G[center] = 0; + } + + var grey = Byte.MaxValue - (byte)(G[center] * (255.0 / largestG)); + var c = Color.FromArgb(pixelsGray[center]); + pixelsCanny[center] = Color.FromArgb(c.A, grey, grey, grey).ToArgb(); + } + } + + int k = 0; + var minThreshold = lowerThreshold * largestG; + var maxThreshold = higherThreshold * largestG; + do { + for (int i = 1; i < height - 1; ++i) { + for (int j = 1; j < width - 1; ++j) { + int center = i * width + j; + if (G[center] < minThreshold) + G[center] = 0; + else if (G[center] >= maxThreshold) + continue; + else if (G[center] < maxThreshold) { + G[center] = 0; + for (int x = -1; x <= 1; ++x) { + for (int y = -1; y <= 1; y++) { + if (x == 0 && y == 0) + continue; + if (G[center + x * width + y] >= maxThreshold) { + G[center] = higherThreshold * largestG; + k = 0; + x = 2; + break; + } + } + } + } + + var grey = Byte.MaxValue - (byte)(G[center] * 255.0 / largestG); + var c = Color.FromArgb(pixelsGray[center]); + pixelsCanny[center] = Color.FromArgb(c.A, grey, grey, grey).ToArgb(); + } + } + } while (k++ < 100); + return pixelsCanny; + } public ushort DitherColorIndex(Color[] palette, int pixel, int pos) { @@ -216,7 +358,7 @@ public Bitmap ConvertToGrayScale(Bitmap srcimg) return sourceImg; } - private void ConvertToGrayScale(int[] pixels) + private void ConvertToGrayScale(int[] pixels, int[] dest) { float min1 = Byte.MaxValue; float max1 = .0f; @@ -243,7 +385,7 @@ private void ConvertToGrayScale(int[] pixels) int green = (pixels[i] >> 8) & 0xff; var grey = (int)((green - min1) * (Byte.MaxValue / (max1 - min1))); - pixels[i] = Color.FromArgb(alfa, grey, grey, grey).ToArgb(); + dest[i] = Color.FromArgb(alfa, grey, grey, grey).ToArgb(); } } @@ -259,11 +401,14 @@ public Bitmap ConvertGrayScaleToBinary(Bitmap srcimg, bool isGrayscale = false) return srcimg; hasSemiTransparency = semiTransCount > 0; + var pixelsGray = (int[]) pixels.Clone(); if (!isGrayscale) - ConvertToGrayScale(pixels); + ConvertToGrayScale(pixels, pixelsGray); - var otsuThreshold = GetOtsuThreshold(pixels); - Threshold(pixels, otsuThreshold); + var otsuThreshold = GetOtsuThreshold(pixelsGray); + double lowerThreshold = 0.03, higherThreshold = 0.1; + pixels = CannyFilter(bitmapWidth, pixelsGray, lowerThreshold, higherThreshold); + Threshold(pixelsGray, pixels, otsuThreshold); var dest = new Bitmap(bitmapWidth, bitmapHeight, PixelFormat.Format1bppIndexed); var palettes = dest.Palette.Entries; @@ -290,5 +435,5 @@ public Bitmap ConvertGrayScaleToBinary(Bitmap srcimg, bool isGrayscale = false) return BitmapUtilities.ProcessImagePixels(dest, palettes, qPixels, m_transparentPixelIndex >= 0); } - } + } }