This commit is contained in:
2025-11-17 10:32:51 +08:00
parent 727d52416d
commit 503b4a59f0
6 changed files with 619 additions and 61 deletions
@@ -0,0 +1,29 @@
using OpenCvSharp;
public static class ImageUtils
{
public static bool TryRead(string path, out Mat mat)
{
mat = Cv2.ImRead(path, ImreadModes.Color);
return !mat.Empty();
}
public static void Save(Mat mat, string path)
{
mat.ImWrite(path);
}
// Apply mask to image: where mask==0 set to black
public static Mat ApplyMask(Mat src, Mat mask)
{
Mat dst = new Mat();
if (mask.Type() != MatType.CV_8U)
{
Mat tmp = new();
mask.ConvertTo(tmp, MatType.CV_8U);
mask = tmp;
}
Cv2.BitwiseAnd(src, src, dst, mask);
return dst;
}
}
+23 -30
View File
@@ -1,40 +1,33 @@
using OpenCvSharp;
using System.Xml.Serialization;
//string filepath = (@"C:\Users\Surface\Downloads\" + @"1682575881230001.png");
string[] testFiles = [
@"C:\Users\Surface\Downloads\1682575881230001.png", // 风景图
@"C:\Users\Surface\Downloads\Sprite-0001.png", // 二值图
@"C:\Users\Surface\Downloads\R-C.jfif", // 街道图
@"C:\Users\Surface\Downloads\OIP.webp" // 指纹图
];
string filepath = testFiles[3];
int year = 2025;
bool isLeap1 = DateTime.IsLeapYear(year);
bool isLeap2 = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
bool isLeap3 = year switch
if (!ImageUtils.TryRead(filepath, out Mat image))
{
_ when year % 400 == 0 => true,
_ when year % 100 == 0 => false,
_ when year % 4 == 0 => true,
_ => false,
};
bool isLeap4 = year % 4 == 0 ? (year % 100 == 0 ? (year % 400 == 0 ? true : false) : true) : false;
bool isLeap5 = year % 4 != 0 ? false : year % 100 != 0 ? true : year % 400 == 0 ? true : false;
Console.WriteLine("Failed to read image: " + filepath);
return;
}
Console.WriteLine(isLeap1);
Console.WriteLine(isLeap2);
Console.WriteLine(isLeap3);
Console.WriteLine(isLeap4);
Console.WriteLine(isLeap5);
string filepath = (@"C:\Users\Surface\Downloads\" + @"sprite-0001.png");
Mat image = Cv2.ImRead(filepath);
InputArray kernel = InputArray.Create<int>(new int[,] {
{ 0, 1, 0 },
{ 1, 1, 1 },
{ 0, 1, 0 }
}, MatType.CV_8U);
Mat dst = new();
//Cv2.Filter2D(image, dst, image.Depth(), kernel);
int thresholdValue = 128;
int thresholdDelta = 30;
Cv2.Dilate(image, dst, kernel);
Mat gray = new();
Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
dst = dst - image;
Mat img1 = new(), img2 = new();
float delta = float.MaxValue;
int loopCount = 0;
dst.ImWrite(@"C:\Users\Surface\Downloads\" + @"filtered_opencv.png");
while(delta > thresholdValue)
{
//Cv2.Threshold(gray, img1, thresholdValue, 255, ThresholdTypes.Binary);
}
@@ -0,0 +1,444 @@
using OpenCvSharp;
public static class Segmentation
{
// Otsu thresholding workflow: convert to gray, blur, Otsu threshold
public static Mat OtsuThreshold(Mat src)
{
Mat gray = new();
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
Mat blur = new();
Cv2.GaussianBlur(gray, blur, new Size(5, 5), 0);
Mat mask = new();
Cv2.Threshold(blur, mask, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
return mask;
}
// K-means color segmentation
public static Mat KMeansSegmentation(Mat src, int k = 2, int attempts = 5)
{
// Reshape to a 2D samples matrix where each row is a pixel and columns are channels
Mat samples = src.Reshape(1, src.Rows * src.Cols);
Mat samples32f = new();
samples.ConvertTo(samples32f, MatType.CV_32F);
Mat labels = new();
TermCriteria criteria = new TermCriteria(CriteriaTypes.Eps | CriteriaTypes.MaxIter, 10, 1.0);
Mat centers = new();
Cv2.Kmeans(samples32f, k, labels, criteria, attempts, KMeansFlags.PpCenters, centers);
// Build segmented image by mapping each pixel to its cluster center color
Mat result = new Mat(src.Rows, src.Cols, src.Type());
int cols = src.Cols;
for (int r = 0; r < src.Rows; r++)
{
for (int c = 0; c < cols; c++)
{
int idx = r * cols + c;
int clusterIdx = labels.At<int>(idx, 0);
byte b = (byte)Clamp(centers.At<float>(clusterIdx, 0));
byte g = (byte)Clamp(centers.At<float>(clusterIdx, 1));
byte rcol = (byte)Clamp(centers.At<float>(clusterIdx, 2));
result.Set(r, c, new Vec3b(b, g, rcol));
}
}
return result;
}
// Simple morphological cleaning (opening then closing)
public static Mat MorphologicalClean(Mat mask, int kernelSize = 3)
{
Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(kernelSize, kernelSize));
Mat opened = new();
Cv2.MorphologyEx(mask, opened, MorphTypes.Open, kernel);
Mat cleaned = new();
Cv2.MorphologyEx(opened, cleaned, MorphTypes.Close, kernel);
return cleaned;
}
// Create a binary mask by HSV range. lower and upper are HSV in OpenCV order: H:0-180, S:0-255, V:0-255
public static Mat ColorRangeHSV(Mat src, Scalar lowerHsv, Scalar upperHsv)
{
Mat hsv = new();
Cv2.CvtColor(src, hsv, ColorConversionCodes.BGR2HSV);
Mat mask = new();
Cv2.InRange(hsv, lowerHsv, upperHsv, mask);
return mask;
}
// Create a binary mask by Euclidean distance in color space to a target color.
// If useLab is true, convert BGR->Lab before distance to get perceptual distance.
// targetColor is Scalar(b, g, r). thresh is distance threshold.
public static Mat ColorDistanceMask(Mat src, Scalar targetColor, double thresh, bool useLab = false)
{
Mat working = new();
if (useLab)
{
Cv2.CvtColor(src, working, ColorConversionCodes.BGR2Lab);
}
else
{
src.CopyTo(working);
}
Mat f32 = new();
working.ConvertTo(f32, MatType.CV_32F);
// Prepare a Mat filled with the target color (in same color space)
Mat target = new Mat(f32.Size(), MatType.CV_32FC3, new Scalar(targetColor.Val0, targetColor.Val1, targetColor.Val2));
Mat diff = new();
Cv2.Subtract(f32, target, diff);
Mat sq = new();
Cv2.Multiply(diff, diff, sq);
// sum channels
Mat[] ch = Cv2.Split(sq);
Mat sum = new();
Cv2.Add(ch[0], ch[1], sum);
Cv2.Add(sum, ch[2], sum);
Mat dist = new();
Cv2.Sqrt(sum, dist);
Mat mask = new();
// pixels with distance <= thresh -> 255 in mask
Cv2.Threshold(dist, mask, thresh, 255, ThresholdTypes.BinaryInv);
// ensure mask is CV_8U
if (mask.Type() != MatType.CV_8U)
{
Mat tmp = new();
mask.ConvertTo(tmp, MatType.CV_8U);
return tmp;
}
return mask;
}
// Canny edge based segmentation.
// Returns a binary edge mask (0/255). Parameters threshold1 and threshold2 are the Canny low/high thresholds.
// If makeRegions is true, the function will invert edges and apply closing to produce coarse region masks.
public static Mat CannySegmentation(Mat src, double threshold1 = 100.0, double threshold2 = 200.0,
bool blur = true, int blurKernel = 5, int apertureSize = 3, bool L2gradient = false,
bool makeRegions = false, int morphKernel = 3)
{
Mat gray = new();
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
if (blur)
{
Cv2.GaussianBlur(gray, gray, new Size(blurKernel, blurKernel), 0);
}
Mat edges = new();
Cv2.Canny(gray, edges, threshold1, threshold2, apertureSize, L2gradient);
if (!makeRegions)
{
return edges;
}
// Convert edge map to coarse regions by inverting edges and closing gaps
Mat inv = new();
Cv2.BitwiseNot(edges, inv);
Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(morphKernel, morphKernel));
Mat closed = new();
Cv2.MorphologyEx(inv, closed, MorphTypes.Close, kernel);
// Optional: threshold to ensure binary
Mat mask = new();
Cv2.Threshold(closed, mask, 128, 255, ThresholdTypes.Binary);
return mask;
}
// Sobel gradient magnitude thresholding segmentation.
// thresh is the threshold applied to gradient magnitude (0-255 after normalization).
public static Mat SobelMagnitudeSegmentation(Mat src, double thresh = 50.0, bool blur = true, int blurKernel = 3)
{
Mat gray = new();
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
if (blur)
{
Cv2.GaussianBlur(gray, gray, new Size(blurKernel, blurKernel), 0);
}
Mat gx = new();
Mat gy = new();
Cv2.Sobel(gray, gx, MatType.CV_32F, 1, 0, ksize: 3);
Cv2.Sobel(gray, gy, MatType.CV_32F, 0, 1, ksize: 3);
Mat mag = new();
Cv2.Magnitude(gx, gy, mag);
// Normalize magnitude to 0-255
Mat mag8u = new();
Cv2.Normalize(mag, mag, 0, 255, NormTypes.MinMax);
mag.ConvertTo(mag8u, MatType.CV_8U);
Mat mask = new();
Cv2.Threshold(mag8u, mask, thresh, 255, ThresholdTypes.Binary);
return mask;
}
// --- Second-derivative (Laplacian) based methods ---
// Simple Laplacian magnitude thresholding.
// ksize: aperture size for the Laplacian operator (1,3,5...). thresh: threshold on normalized magnitude (0-255).
public static Mat LaplacianSegmentation(Mat src, int ksize = 3, double thresh = 30.0, bool blur = true, int blurKernel = 3)
{
Mat gray = new();
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
if (blur)
{
Cv2.GaussianBlur(gray, gray, new Size(blurKernel, blurKernel), 0);
}
Mat lap = new();
Cv2.Laplacian(gray, lap, MatType.CV_32F, ksize: ksize);
Mat absLap = new();
absLap = Cv2.Abs(lap);
Mat norm = new();
Cv2.Normalize(absLap, norm, 0, 255, NormTypes.MinMax);
Mat mask = new();
norm.ConvertTo(norm, MatType.CV_8U);
Cv2.Threshold(norm, mask, thresh, 255, ThresholdTypes.Binary);
return mask;
}
// Laplacian of Gaussian: blur with Gaussian (sigma), then Laplacian, then detect zero-crossings.
// zeroCrossThreshold is applied to absolute Laplacian magnitude to avoid weak crossings caused by noise.
public static Mat LoGSegmentation(Mat src, double sigma = 1.4, double zeroCrossThreshold = 5.0, bool useKernelSize = false, int ksize = 0)
{
// choose kernel size from sigma if requested
if (!useKernelSize && ksize == 0)
{
ksize = (int)(sigma * 6) | 1; // ensure odd
}
Mat gray = new();
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
Mat blurred = new();
Cv2.GaussianBlur(gray, blurred, new Size(ksize, ksize), sigma);
Mat lap = new();
Cv2.Laplacian(blurred, lap, MatType.CV_32F, ksize: 3);
Mat absLap = new();
absLap = Cv2.Abs(lap);
Mat mask = ZeroCrossingMask(absLap, zeroCrossThreshold);
return mask;
}
// Difference of Gaussians as approximation of LoG.
// sigma1 < sigma2. thresh is applied to absolute DoG response after normalization.
public static Mat DoGSegmentation(Mat src, double sigma1 = 1.0, double sigma2 = 2.0, double thresh = 10.0, bool normalize = true)
{
Mat gray = new();
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
int k1 = (int)(sigma1 * 6) | 1;
int k2 = (int)(sigma2 * 6) | 1;
Mat g1 = new();
Mat g2 = new();
Cv2.GaussianBlur(gray, g1, new Size(k1, k1), sigma1);
Cv2.GaussianBlur(gray, g2, new Size(k2, k2), sigma2);
Mat dog = new();
Cv2.Subtract(g1, g2, dog);
Mat absDog = new();
absDog = Cv2.Abs(dog);
if (normalize)
{
Cv2.Normalize(absDog, absDog, 0, 255, NormTypes.MinMax);
}
Mat mask = new();
absDog.ConvertTo(absDog, MatType.CV_8U);
Cv2.Threshold(absDog, mask, thresh, 255, ThresholdTypes.Binary);
return mask;
}
// Compute Otsu variance profiles for thresholds 0..254 (we evaluate t as threshold separating [0..t] and [t+1..255]).
// Returns best thresholds (max between-class variance and min within-class variance) and the per-threshold arrays.
public static (int bestByBetween, int bestByWithin, double[] betweenVars, double[] withinVars) ComputeOtsuVarianceProfiles(Mat src)
{
// ensure grayscale CV_8U
Mat gray = new();
if (src.Type() == MatType.CV_8U && src.Channels() == 1)
{
src.CopyTo(gray);
}
else
{
Cv2.CvtColor(src, gray, ColorConversionCodes.BGR2GRAY);
}
int rows = gray.Rows;
int cols = gray.Cols;
int total = rows * cols;
// histogram
double[] hist = new double[256];
for (int y = 0; y < rows; y++)
{
for (int x = 0; x < cols; x++)
{
byte v = gray.At<byte>(y, x);
hist[v] += 1.0;
}
}
// convert to probabilities
for (int i = 0; i < 256; i++) hist[i] /= total;
// cumulative arrays
double[] P = new double[256]; // cumulative probability
double[] S = new double[256]; // cumulative first moment (i * p(i))
double[] M2 = new double[256]; // cumulative second moment (i^2 * p(i))
double pAcc = 0, sAcc = 0, m2Acc = 0;
for (int i = 0; i < 256; i++)
{
pAcc += hist[i];
sAcc += i * hist[i];
m2Acc += i * i * hist[i];
P[i] = pAcc;
S[i] = sAcc;
M2[i] = m2Acc;
}
double muT = S[255];
double m2T = M2[255];
// total variance (scalar)
double totalVar = m2T - muT * muT;
int maxIndexBetween = 0;
int minIndexWithin = 0;
double maxBetween = double.MinValue;
double minWithin = double.MaxValue;
// arrays to hold per-threshold values (we'll fill 256 but only meaningful up to 254)
double[] betweenVars = new double[256];
double[] withinVars = new double[256];
for (int t = 0; t < 255; t++)
{
double w0 = P[t];
double w1 = 1.0 - w0;
if (w0 <= 0.0 || w1 <= 0.0)
{
betweenVars[t] = 0.0;
withinVars[t] = double.PositiveInfinity;
continue;
}
double s0 = S[t];
double s1 = S[255] - s0;
double m20 = M2[t];
double m21 = m2T - m20;
double mu0 = s0 / w0;
double mu1 = s1 / w1;
// between-class variance
double sigmaB = w0 * w1 * (mu0 - mu1) * (mu0 - mu1);
betweenVars[t] = sigmaB;
// within-class variance via second moments: var0 = M2_0/w0 - mu0^2
double var0 = m20 / w0 - mu0 * mu0;
double var1 = m21 / w1 - mu1 * mu1;
double sigmaW = w0 * var0 + w1 * var1; // weighted within-class variance
withinVars[t] = sigmaW;
if (sigmaB > maxBetween)
{
maxBetween = sigmaB;
maxIndexBetween = t;
}
if (sigmaW < minWithin)
{
minWithin = sigmaW;
minIndexWithin = t;
}
}
return (maxIndexBetween, minIndexWithin, betweenVars, withinVars);
}
// Helper: detect zero-crossings in a floating-point Laplacian image.
// A pixel is considered edge if any 4-neighbor has opposite sign and the absolute difference exceeds 'threshold'.
private static Mat ZeroCrossingMask(Mat lapFloat, double threshold)
{
// ensure lapFloat is CV_32F
Mat lap = new();
if (lapFloat.Type() != MatType.CV_32F)
{
lapFloat.ConvertTo(lap, MatType.CV_32F);
}
else
{
lap = lapFloat.Clone();
}
int rows = lap.Rows;
int cols = lap.Cols;
Mat mask = new Mat(rows, cols, MatType.CV_8U, Scalar.All(0));
for (int y = 1; y < rows - 1; y++)
{
for (int x = 1; x < cols - 1; x++)
{
float v = lap.At<float>(y, x);
// check 8 neighbors
bool isZeroCross = false;
float maxDiff = 0f;
for (int ny = -1; ny <= 1; ny++)
{
for (int nx = -1; nx <= 1; nx++)
{
if (ny == 0 && nx == 0) continue;
float vn = lap.At<float>(y + ny, x + nx);
if ((v > 0 && vn < 0) || (v < 0 && vn > 0))
{
float diff = MathF.Abs(v - vn);
if (diff > maxDiff) maxDiff = diff;
isZeroCross = true;
}
}
}
if (isZeroCross && maxDiff >= threshold)
{
mask.Set(y, x, (byte)255);
}
}
}
return mask;
}
private static float Clamp(float v)
{
if (v < 0) return 0;
if (v > 255) return 255;
return v;
}
}