Browse Source

1. 优化ImageBorderRemover
2. 新增ColorDeltaE色差比较类

懒得勤快 4 months ago
parent
commit
79e617b3e0

+ 1 - 1
Directory.Build.props

@@ -1,6 +1,6 @@
 <Project>
  <PropertyGroup>
-   <Version>2025.3.1</Version>
+   <Version>2025.4</Version>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
 </Project>

+ 78 - 0
Masuit.Tools.Abstractions/Media/BorderDetectionResult.cs

@@ -0,0 +1,78 @@
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace Masuit.Tools.Media;
+
+/// <summary>
+/// 边框检测结果(包含多层边框信息)
+/// </summary>
+public struct BorderDetectionResult
+{
+    private int CroppedBorderCount { get; set; }
+
+    public BorderDetectionResult(int croppedBorderCount)
+    {
+        CroppedBorderCount = croppedBorderCount;
+    }
+
+    /// <summary>原始图片宽度</summary>
+    public int ImageWidth { get; set; }
+
+    /// <summary>原始图片高度</summary>
+    public int ImageHeight { get; set; }
+
+    /// <summary>内容上边界位置</summary>
+    public int ContentTop { get; set; }
+
+    /// <summary>内容下边界位置</summary>
+    public int ContentBottom { get; set; }
+
+    /// <summary>内容左边界位置</summary>
+    public int ContentLeft { get; set; }
+
+    /// <summary>内容右边界位置</summary>
+    public int ContentRight { get; set; }
+
+    /// <summary>检测到的边框层数</summary>
+    public int BorderLayers { get; set; }
+
+    /// <summary>边框颜色层次(从外到内)</summary>
+    public List<Rgba32> BorderColors { get; set; }
+
+    /// <summary>顶部边框总宽度(像素)</summary>
+    public int TopBorderWidth => ContentTop;
+
+    /// <summary>底部边框总宽度(像素)</summary>
+    public int BottomBorderWidth => ImageHeight - 1 - ContentBottom;
+
+    /// <summary>左侧边框总宽度(像素)</summary>
+    public int LeftBorderWidth => ContentLeft;
+
+    /// <summary>右侧边框总宽度(像素)</summary>
+    public int RightBorderWidth => ImageWidth - 1 - ContentRight;
+
+    /// <summary>是否有顶部边框</summary>
+    public bool HasTopBorder => TopBorderWidth > 0;
+
+    /// <summary>是否有底部边框</summary>
+    public bool HasBottomBorder => BottomBorderWidth > 0;
+
+    /// <summary>是否有左侧边框</summary>
+    public bool HasLeftBorder => LeftBorderWidth > 0;
+
+    /// <summary>是否有右侧边框</summary>
+    public bool HasRightBorder => RightBorderWidth > 0;
+
+    /// <summary>是否有任意边框</summary>
+    public bool HasAnyBorder => BorderCount > 0;
+
+    /// <summary>是否满足裁剪条件(至少两个边)</summary>
+    public bool CanBeCropped => BorderCount >= CroppedBorderCount;
+
+    public int BorderCount => (HasTopBorder ? 1 : 0) + (HasBottomBorder ? 1 : 0) + (HasLeftBorder ? 1 : 0) + (HasRightBorder ? 1 : 0);
+
+    /// <summary>内容区域宽度</summary>
+    public int ContentWidth => ContentRight - ContentLeft + 1;
+
+    /// <summary>内容区域高度</summary>
+    public int ContentHeight => ContentBottom - ContentTop + 1;
+}

+ 15 - 0
Masuit.Tools.Abstractions/Media/CMYColor.cs

@@ -0,0 +1,15 @@
+namespace Masuit.Tools.Media;
+
+public struct CMYColor
+{
+    public double C { get; }
+    public double M { get; }
+    public double Y { get; }
+
+    public CMYColor(double c, double m, double y)
+    {
+        C = c;
+        M = m;
+        Y = y;
+    }
+}

+ 17 - 0
Masuit.Tools.Abstractions/Media/CMYKColor.cs

@@ -0,0 +1,17 @@
+namespace Masuit.Tools.Media;
+
+public struct CMYKColor
+{
+    public double C { get; }
+    public double M { get; }
+    public double Y { get; }
+    public double K { get; }
+
+    public CMYKColor(double c, double m, double y, double k)
+    {
+        C = c;
+        M = m;
+        Y = y;
+        K = k;
+    }
+}

+ 158 - 0
Masuit.Tools.Abstractions/Media/ColorConverter.cs

@@ -0,0 +1,158 @@
+using System.Drawing;
+
+namespace Masuit.Tools.Media;
+
+internal static class ColorConverter
+{
+    // RGB转换器
+    public static LabColor ToLab(this Color color)
+    {
+        // 第一步:将RGB转换为0-1范围
+        double rLinear = color.R / 255.0;
+        double gLinear = color.G / 255.0;
+        double bLinear = color.B / 255.0;
+
+        // 第二步:应用逆伽马校正
+        rLinear = (rLinear <= 0.04045) ? rLinear / 12.92 : Math.Pow((rLinear + 0.055) / 1.055, 2.4);
+        gLinear = (gLinear <= 0.04045) ? gLinear / 12.92 : Math.Pow((gLinear + 0.055) / 1.055, 2.4);
+        bLinear = (bLinear <= 0.04045) ? bLinear / 12.92 : Math.Pow((bLinear + 0.055) / 1.055, 2.4);
+
+        // 第三步:转换为XYZ(D65白点)
+        double x = rLinear * 0.4124564 + gLinear * 0.3575761 + bLinear * 0.1804375;
+        double y = rLinear * 0.2126729 + gLinear * 0.7151522 + bLinear * 0.0721750;
+        double z = rLinear * 0.0193339 + gLinear * 0.1191920 + bLinear * 0.9503041;
+
+        // 第四步:XYZ转Lab(使用D65参考白)
+        const double xn = 0.95047;
+        const double yn = 1.00000;
+        const double zn = 1.08883;
+
+        double xRatio = x / xn;
+        double yRatio = y / yn;
+        double zRatio = z / zn;
+
+        double fx = (xRatio > 0.008856) ? Math.Pow(xRatio, 1.0 / 3.0) : (903.3 * xRatio + 16) / 116.0;
+        double fy = (yRatio > 0.008856) ? Math.Pow(yRatio, 1.0 / 3.0) : (903.3 * yRatio + 16) / 116.0;
+        double fz = (zRatio > 0.008856) ? Math.Pow(zRatio, 1.0 / 3.0) : (903.3 * zRatio + 16) / 116.0;
+
+        double l = (yRatio > 0.008856) ? (116 * fy - 16) : (903.3 * yRatio);
+        double a = 500 * (fx - fy);
+        double bVal = 200 * (fy - fz);
+
+        return new LabColor(l, a, bVal);
+    }
+
+    // CMY转换器
+    public static LabColor ToLab(this CMYColor cmy)
+    {
+        // CMY转RGB (0-1范围)
+        double r = 1 - cmy.C;
+        double g = 1 - cmy.M;
+        double b = 1 - cmy.Y;
+
+        // RGB转Lab
+        return ToLab(Color.FromArgb((int)(r * 255), (int)(g * 255), (int)(b * 255)));
+    }
+
+    // CMYK转换器
+    public static LabColor ToLab(this CMYKColor cmyk)
+    {
+        // CMYK转RGB
+        double r = (1 - cmyk.C) * (1 - cmyk.K);
+        double g = (1 - cmyk.M) * (1 - cmyk.K);
+        double b = (1 - cmyk.Y) * (1 - cmyk.K);
+
+        // RGB转Lab
+        return ToLab(Color.FromArgb((int)(r * 255), (int)(g * 255), (int)(b * 255)));
+    }
+
+    // HSL转换器
+    public static LabColor ToLab(this HSLColor hsl)
+    {
+        // HSL转RGB
+        Color rgb = ToRgb(hsl);
+        return ToLab(rgb);
+    }
+
+    private static Color ToRgb(HSLColor hsl)
+    {
+        double h = hsl.H / 360.0;
+        double s = hsl.S;
+        double l = hsl.L;
+        double r, g, b;
+        if (s == 0)
+        {
+            r = g = b = l;
+        }
+        else
+        {
+            double q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+            double p = 2 * l - q;
+
+            r = HueToRgb(p, q, h + 1.0 / 3);
+            g = HueToRgb(p, q, h);
+            b = HueToRgb(p, q, h - 1.0 / 3);
+        }
+
+        return Color.FromArgb((int)(r * 255), (int)(g * 255), (int)(b * 255));
+    }
+
+    private static double HueToRgb(double p, double q, double t)
+    {
+        if (t < 0) t += 1;
+        if (t > 1) t -= 1;
+
+        if (t < 1.0 / 6) return p + (q - p) * 6 * t;
+        if (t < 1.0 / 2) return q;
+        if (t < 2.0 / 3) return p + (q - p) * (2.0 / 3 - t) * 6;
+
+        return p;
+    }
+
+    // LCH转换器
+    public static LabColor ToLab(this LCHColor lch)
+    {
+        // LCH转Lab
+        double rad = lch.H * Math.PI / 180.0;
+        double a = lch.C * Math.Cos(rad);
+        double b = lch.C * Math.Sin(rad);
+        return new LabColor(lch.L, a, b);
+    }
+
+    // XYZ转换器
+    public static LabColor ToLab(this XYZColor xyz)
+    {
+        // 使用D65参考白点
+        const double xn = 0.95047;
+        const double yn = 1.00000;
+        const double zn = 1.08883;
+
+        double xRatio = xyz.X / xn;
+        double yRatio = xyz.Y / yn;
+        double zRatio = xyz.Z / zn;
+
+        double fx = Fxyz(xRatio);
+        double fy = Fxyz(yRatio);
+        double fz = Fxyz(zRatio);
+
+        double L = 116 * fy - 16;
+        double a = 500 * (fx - fy);
+        double b = 200 * (fy - fz);
+
+        return new LabColor(L, a, b);
+    }
+
+    private static double Fxyz(double t)
+    {
+        const double delta = 6.0 / 29.0;
+        const double delta3 = delta * delta * delta;
+        return (t > delta3) ? Math.Pow(t, 1.0 / 3.0) : t / (3 * delta * delta) + 4.0 / 29.0;
+    }
+
+    // YXZ转换器 (假设YXZ是Y,X,Z顺序)
+    public static LabColor YxzToLab(this YXZColor yxz)
+    {
+        // 转换为标准XYZ顺序
+        return ToLab(new XYZColor(yxz.X, yxz.Y, yxz.Z));
+    }
+}

+ 219 - 0
Masuit.Tools.Abstractions/Media/ColorDeltaE.cs

@@ -0,0 +1,219 @@
+using System.Drawing;
+
+namespace Masuit.Tools.Media;
+
+public static class ColorDeltaE
+{
+    // CIE1976 (ΔE*ab)
+    public static double CIE1976(Color color1, Color color2) => CalculateDeltaE1976(color1.ToLab(), color2.ToLab());
+
+    public static double CIE1976(CMYColor color1, CMYColor color2) => CalculateDeltaE1976(color1.ToLab(), color2.ToLab());
+
+    public static double CIE1976(CMYKColor color1, CMYKColor color2) => CalculateDeltaE1976(color1.ToLab(), color2.ToLab());
+
+    public static double CIE1976(HSLColor color1, HSLColor color2) => CalculateDeltaE1976(color1.ToLab(), color2.ToLab());
+
+    public static double CIE1976(LabColor color1, LabColor color2) => CalculateDeltaE1976(color1, color2);
+
+    public static double CIE1976(LCHColor color1, LCHColor color2) => CalculateDeltaE1976(color1.ToLab(), color2.ToLab());
+
+    public static double CIE1976(XYZColor color1, XYZColor color2) => CalculateDeltaE1976(color1.ToLab(), color2.ToLab());
+
+    public static double CIE1976(YXZColor color1, YXZColor color2) => CalculateDeltaE1976(color1.YxzToLab(), color2.YxzToLab());
+
+    // CIE1994 (ΔE94)
+    public static double CIE1994(Color color1, Color color2, bool textile = false) => CalculateDeltaE1994(color1.ToLab(), color2.ToLab(), textile);
+
+    public static double CIE1994(CMYColor color1, CMYColor color2, bool textile = false) => CalculateDeltaE1994(color1.ToLab(), color2.ToLab(), textile);
+
+    public static double CIE1994(CMYKColor color1, CMYKColor color2, bool textile = false) => CalculateDeltaE1994(color1.ToLab(), color2.ToLab(), textile);
+
+    public static double CIE1994(HSLColor color1, HSLColor color2, bool textile = false) => CalculateDeltaE1994(color1.ToLab(), color2.ToLab(), textile);
+
+    public static double CIE1994(LabColor color1, LabColor color2, bool textile = false) => CalculateDeltaE1994(color1, color2, textile);
+
+    public static double CIE1994(LCHColor color1, LCHColor color2, bool textile = false) => CalculateDeltaE1994(color1.ToLab(), color2.ToLab(), textile);
+
+    public static double CIE1994(XYZColor color1, XYZColor color2, bool textile = false) => CalculateDeltaE1994(color1.ToLab(), color2.ToLab(), textile);
+
+    public static double CIE1994(YXZColor color1, YXZColor color2, bool textile = false) => CalculateDeltaE1994(color1.YxzToLab(), color2.YxzToLab(), textile);
+
+    // CIE2000 (ΔE00)
+    public static double CIE2000(Color color1, Color color2) => CalculateDeltaE2000(color1.ToLab(), color2.ToLab());
+
+    public static double CIE2000(CMYColor color1, CMYColor color2) => CalculateDeltaE2000(color1.ToLab(), color2.ToLab());
+
+    public static double CIE2000(CMYKColor color1, CMYKColor color2) => CalculateDeltaE2000(color1.ToLab(), color2.ToLab());
+
+    public static double CIE2000(HSLColor color1, HSLColor color2) => CalculateDeltaE2000(color1.ToLab(), color2.ToLab());
+
+    public static double CIE2000(LabColor color1, LabColor color2) => CalculateDeltaE2000(color1, color2);
+
+    public static double CIE2000(LCHColor color1, LCHColor color2) => CalculateDeltaE2000(color1.ToLab(), color2.ToLab());
+
+    public static double CIE2000(XYZColor color1, XYZColor color2) => CalculateDeltaE2000(color1.ToLab(), color2.ToLab());
+
+    public static double CIE2000(YXZColor color1, YXZColor color2) => CalculateDeltaE2000(color1.YxzToLab(), color2.YxzToLab());
+
+    // CMC (ΔEcmc)
+    public static double CMC(Color color1, Color color2, double l = 2.0, double c = 1.0) => CalculateDeltaECMC(color1.ToLab(), color2.ToLab(), l, c);
+
+    public static double CMC(CMYColor color1, CMYColor color2, double l = 2.0, double c = 1.0) => CalculateDeltaECMC(color1.ToLab(), color2.ToLab(), l, c);
+
+    public static double CMC(CMYKColor color1, CMYKColor color2, double l = 2.0, double c = 1.0) => CalculateDeltaECMC(color1.ToLab(), color2.ToLab(), l, c);
+
+    public static double CMC(HSLColor color1, HSLColor color2, double l = 2.0, double c = 1.0) => CalculateDeltaECMC(color1.ToLab(), color2.ToLab(), l, c);
+
+    public static double CMC(LabColor color1, LabColor color2, double l = 2.0, double c = 1.0) => CalculateDeltaECMC(color1, color2, l, c);
+
+    public static double CMC(LCHColor color1, LCHColor color2, double l = 2.0, double c = 1.0) => CalculateDeltaECMC(color1.ToLab(), color2.ToLab(), l, c);
+
+    public static double CMC(XYZColor color1, XYZColor color2, double l = 2.0, double c = 1.0) => CalculateDeltaECMC(color1.ToLab(), color2.ToLab(), l, c);
+
+    public static double CMC(YXZColor color1, YXZColor color2, double l = 2.0, double c = 1.0) => CalculateDeltaECMC(color1.YxzToLab(), color2.YxzToLab(), l, c);
+
+    #region Core Calculation Methods
+
+    private static double CalculateDeltaE1976(LabColor lab1, LabColor lab2)
+    {
+        double deltaL = lab1.L - lab2.L;
+        double deltaA = lab1.a - lab2.a;
+        double deltaB = lab1.b - lab2.b;
+        return Math.Sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB);
+    }
+
+    private static double CalculateDeltaE1994(LabColor lab1, LabColor lab2, bool textile)
+    {
+        double kL = textile ? 2.0 : 1.0;
+        double kC = 1.0;
+        double kH = 1.0;
+        double k1 = textile ? 0.048 : 0.045;
+        double k2 = textile ? 0.014 : 0.015;
+
+        double deltaL = lab1.L - lab2.L;
+        double c1 = Math.Sqrt(lab1.a * lab1.a + lab1.b * lab1.b);
+        double c2 = Math.Sqrt(lab2.a * lab2.a + lab2.b * lab2.b);
+        double deltaC = c1 - c2;
+
+        double deltaA = lab1.a - lab2.a;
+        double deltaB = lab1.b - lab2.b;
+        double deltaH = Math.Sqrt(Math.Max(0, deltaA * deltaA + deltaB * deltaB - deltaC * deltaC));
+
+        double sl = 1.0;
+        double sc = 1.0 + k1 * c1;
+        double sh = 1.0 + k2 * c1;
+
+        double termL = deltaL / (kL * sl);
+        double termC = deltaC / (kC * sc);
+        double termH = deltaH / (kH * sh);
+
+        return Math.Sqrt(termL * termL + termC * termC + termH * termH);
+    }
+
+    private static double CalculateDeltaE2000(LabColor lab1, LabColor lab2)
+    {
+        const double kL = 1.0;
+        const double kC = 1.0;
+        const double kH = 1.0;
+
+        double l1 = lab1.L, a1 = lab1.a, b1 = lab1.b;
+        double l2 = lab2.L, a2 = lab2.a, b2 = lab2.b;
+
+        double c1 = Math.Sqrt(a1 * a1 + b1 * b1);
+        double c2 = Math.Sqrt(a2 * a2 + b2 * b2);
+        double meanC = (c1 + c2) / 2.0;
+
+        double meanC7 = Math.Pow(meanC, 7);
+        double g = 0.5 * (1 - Math.Sqrt(meanC7 / (meanC7 + 6103515625.0)));
+
+        double a1Prime = a1 * (1 + g);
+        double a2Prime = a2 * (1 + g);
+
+        double c1Prime = Math.Sqrt(a1Prime * a1Prime + b1 * b1);
+        double c2Prime = Math.Sqrt(a2Prime * a2Prime + b2 * b2);
+
+        double h1Prime = (Math.Atan2(b1, a1Prime) * 180.0 / Math.PI + 360) % 360;
+        double h2Prime = (Math.Atan2(b2, a2Prime) * 180.0 / Math.PI + 360) % 360;
+
+        double deltaLPrime = l2 - l1;
+        double deltaCPrime = c2Prime - c1Prime;
+
+        double deltaHPrime;
+        if (Math.Abs(h2Prime - h1Prime) <= 180)
+        {
+            deltaHPrime = h2Prime - h1Prime;
+        }
+        else if (h2Prime <= h1Prime)
+        {
+            deltaHPrime = h2Prime - h1Prime + 360;
+        }
+        else
+        {
+            deltaHPrime = h2Prime - h1Prime - 360;
+        }
+
+        double deltaH = 2 * Math.Sqrt(c1Prime * c2Prime) * Math.Sin(deltaHPrime * Math.PI / 360.0);
+        double meanLPrime = (l1 + l2) / 2.0;
+        double meanCPrime = (c1Prime + c2Prime) / 2.0;
+        double meanHPrime;
+        if (Math.Abs(h1Prime - h2Prime) > 180)
+        {
+            meanHPrime = (h1Prime + h2Prime + 360) / 2.0;
+        }
+        else
+        {
+            meanHPrime = (h1Prime + h2Prime) / 2.0;
+        }
+
+        meanHPrime %= 360;
+
+        double t = 1 - 0.17 * Math.Cos((meanHPrime - 30) * Math.PI / 180) + 0.24 * Math.Cos(2 * meanHPrime * Math.PI / 180) + 0.32 * Math.Cos((3 * meanHPrime + 6) * Math.PI / 180) - 0.20 * Math.Cos((4 * meanHPrime - 63) * Math.PI / 180);
+
+        double sl = 1 + (0.015 * Math.Pow(meanLPrime - 50, 2)) / Math.Sqrt(20 + Math.Pow(meanLPrime - 50, 2));
+        double sc = 1 + 0.045 * meanCPrime;
+        double sh = 1 + 0.015 * meanCPrime * t;
+
+        double meanCPrime7 = Math.Pow(meanCPrime, 7);
+        double rc = 2 * Math.Sqrt(meanCPrime7 / (meanCPrime7 + 6103515625.0));
+        double deltaTheta = 30 * Math.Exp(-Math.Pow((meanHPrime - 275) / 25, 2));
+        double rt = -Math.Sin(2 * deltaTheta * Math.PI / 180) * rc;
+
+        double term1 = deltaLPrime / (kL * sl);
+        double term2 = deltaCPrime / (kC * sc);
+        double term3 = deltaH / (kH * sh);
+
+        return Math.Sqrt(term1 * term1 + term2 * term2 + term3 * term3 + rt * term2 * term3);
+    }
+
+    private static double CalculateDeltaECMC(LabColor lab1, LabColor lab2, double l, double c)
+    {
+        double deltaL = lab1.L - lab2.L;
+        double c1 = Math.Sqrt(lab1.a * lab1.a + lab1.b * lab1.b);
+        double c2 = Math.Sqrt(lab2.a * lab2.a + lab2.b * lab2.b);
+        double deltaC = c1 - c2;
+        double deltaA = lab1.a - lab2.a;
+        double deltaB = lab1.b - lab2.b;
+        double deltaH = Math.Sqrt(Math.Max(0, deltaA * deltaA + deltaB * deltaB - deltaC * deltaC));
+        double h1 = (Math.Atan2(lab1.b, lab1.a) * 180.0 / Math.PI + 360) % 360;
+        double f = Math.Sqrt(Math.Pow(c1, 4) / (Math.Pow(c1, 4) + 1900));
+        double t;
+        if (h1 is >= 164 and <= 345)
+        {
+            t = 0.56 + Math.Abs(0.2 * Math.Cos((h1 + 168) * Math.PI / 180));
+        }
+        else
+        {
+            t = 0.36 + Math.Abs(0.4 * Math.Cos((h1 + 35) * Math.PI / 180));
+        }
+
+        double sl = (lab1.L < 16) ? 0.511 : (0.040975 * lab1.L) / (1 + 0.01765 * lab1.L);
+        double sc = (0.0638 * c1) / (1 + 0.0131 * c1) + 0.638;
+        double sh = sc * (t * f + 1 - f);
+        double termL = deltaL / (l * sl);
+        double termC = deltaC / (c * sc);
+        double termH = deltaH / sh;
+        return Math.Sqrt(termL * termL + termC * termC + termH * termH);
+    }
+
+    #endregion Core Calculation Methods
+}

+ 15 - 0
Masuit.Tools.Abstractions/Media/HSLColor.cs

@@ -0,0 +1,15 @@
+namespace Masuit.Tools.Media;
+
+public struct HSLColor
+{
+    public double H { get; }
+    public double S { get; }
+    public double L { get; }
+
+    public HSLColor(double h, double s, double l)
+    {
+        H = h;
+        S = s;
+        L = l;
+    }
+}

+ 37 - 89
Masuit.Tools.Abstractions/Media/ImageBorderRemover.cs

@@ -12,35 +12,51 @@ namespace Masuit.Tools.Media;
 /// </summary>
 public class ImageBorderRemover
 {
+    /// <summary>
+    /// 容差模式
+    /// </summary>
+    private ToleranceMode ToleranceMode { get; set; }
+
+    private int CroppedBorderCount { get; set; }
+
+    /// <summary>
+    ///
+    /// </summary>
+    /// <param name="mode">容差模式</param>
+    /// <param name="croppedBorderCount">达到边框个数则裁剪</param>
+    public ImageBorderRemover(ToleranceMode mode, int croppedBorderCount = 2)
+    {
+        ToleranceMode = mode;
+        CroppedBorderCount = croppedBorderCount;
+    }
+
     /// <summary>
     /// 检测图片边框信息(支持多色边框)
     /// </summary>
     /// <param name="imagePath">图片路径</param>
-    /// <param name="tolerance">颜色容差(0-100),默认10</param>
+    /// <param name="tolerance">颜色容差(0-100),通道模式建议10,ΔE模式建议1</param>
     /// <param name="maxLayers">最大检测边框层数,默认3</param>
-    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
+    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认false,开启可能会导致图片过多裁剪</param>
     /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
     /// <returns>边框检测结果</returns>
-    public static BorderDetectionResult DetectBorders(string imagePath, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
+    public BorderDetectionResult DetectBorders(string imagePath, int tolerance, int maxLayers = 3, bool useDownscaling = false, int downscaleFactor = 4)
     {
-        using (Image<Rgba32> image = Image.Load<Rgba32>(imagePath))
-        {
-            return DetectBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
-        }
+        using var image = Image.Load<Rgba32>(imagePath);
+        return DetectBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
     }
 
     /// <summary>
     /// 检测图片边框信息(从已加载的图像)
     /// </summary>
     /// <param name="image">已加载的图像</param>
-    /// <param name="tolerance">颜色容差(0-100),默认10</param>
+    /// <param name="tolerance">颜色容差(0-100),通道模式建议10,ΔE模式建议1</param>
     /// <param name="maxLayers">最大检测边框层数,默认3</param>
-    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
+    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认false,开启可能会导致图片过多裁剪</param>
     /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
     /// <returns>边框检测结果</returns>
-    public static BorderDetectionResult DetectBorders(Image<Rgba32> image, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
+    public BorderDetectionResult DetectBorders(Image<Rgba32> image, int tolerance, int maxLayers = 3, bool useDownscaling = false, int downscaleFactor = 4)
     {
-        var result = new BorderDetectionResult
+        var result = new BorderDetectionResult(CroppedBorderCount)
         {
             ImageWidth = image.Width,
             ImageHeight = image.Height,
@@ -68,12 +84,12 @@ public class ImageBorderRemover
     /// 自动移除图片的多层边框(仅当至少有两边存在边框时才裁剪)
     /// </summary>
     /// <param name="inputPath">输入图片路径</param>
-    /// <param name="tolerance">颜色容差(0-100),默认10</param>
+    /// <param name="tolerance">颜色容差(0-100),通道模式建议10,ΔE模式建议1</param>
     /// <param name="maxLayers">最大检测边框层数,默认3</param>
-    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
+    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认false,开启可能会导致图片过多裁剪</param>
     /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
     /// <returns>是否执行了裁剪操作</returns>
-    public static void RemoveBorders(string inputPath, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
+    public void RemoveBorders(string inputPath, int tolerance, int maxLayers = 3, bool useDownscaling = false, int downscaleFactor = 4)
     {
         RemoveBorders(inputPath, inputPath, tolerance, maxLayers, useDownscaling, downscaleFactor);
     }
@@ -83,12 +99,12 @@ public class ImageBorderRemover
     /// </summary>
     /// <param name="inputPath">输入图片路径</param>
     /// <param name="outputPath">输出图片路径</param>
-    /// <param name="tolerance">颜色容差(0-100),默认10</param>
+    /// <param name="tolerance">颜色容差(0-100),通道模式建议10,ΔE模式建议1</param>
     /// <param name="maxLayers">最大检测边框层数,默认3</param>
-    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
+    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认false,开启可能会导致图片过多裁剪</param>
     /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
     /// <returns>是否执行了裁剪操作</returns>
-    public static void RemoveBorders(string inputPath, string outputPath, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
+    public void RemoveBorders(string inputPath, string outputPath, int tolerance, int maxLayers = 3, bool useDownscaling = false, int downscaleFactor = 4)
     {
         using Image<Rgba32> image = Image.Load<Rgba32>(inputPath);
         var hasCropped = RemoveBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
@@ -104,12 +120,12 @@ public class ImageBorderRemover
     /// 自动移除图片的多层边框(仅当至少有两边存在边框时才裁剪)
     /// </summary>
     /// <param name="input">输入图片路径</param>
-    /// <param name="tolerance">颜色容差(0-100),默认10</param>
+    /// <param name="tolerance">颜色容差(0-100),通道模式建议10,ΔE模式建议1</param>
     /// <param name="maxLayers">最大检测边框层数,默认3</param>
-    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
+    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认false,开启可能会导致图片过多裁剪</param>
     /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
     /// <returns>是否执行了裁剪操作</returns>
-    public static PooledMemoryStream RemoveBorders(Stream input, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
+    public PooledMemoryStream RemoveBorders(Stream input, int tolerance, int maxLayers = 3, bool useDownscaling = false, int downscaleFactor = 4)
     {
         var format = Image.DetectFormat(input);
         input.Seek(0, SeekOrigin.Begin);
@@ -120,7 +136,7 @@ public class ImageBorderRemover
         return stream;
     }
 
-    private static bool RemoveBorders(Image<Rgba32> image, int tolerance, int maxLayers, bool useDownscaling, int downscaleFactor)
+    private bool RemoveBorders(Image<Rgba32> image, int tolerance, int maxLayers, bool useDownscaling, int downscaleFactor)
     {
         // 保存原始尺寸用于比较
         int originalWidth = image.Width;
@@ -564,72 +580,4 @@ public class ImageBorderRemover
         // 精确比较
         return diffR <= tolerance && diffG <= tolerance && diffB <= tolerance;
     }
-}
-
-/// <summary>
-/// 边框检测结果(包含多层边框信息)
-/// </summary>
-public struct BorderDetectionResult
-{
-    /// <summary>原始图片宽度</summary>
-    public int ImageWidth { get; set; }
-
-    /// <summary>原始图片高度</summary>
-    public int ImageHeight { get; set; }
-
-    /// <summary>内容上边界位置</summary>
-    public int ContentTop { get; set; }
-
-    /// <summary>内容下边界位置</summary>
-    public int ContentBottom { get; set; }
-
-    /// <summary>内容左边界位置</summary>
-    public int ContentLeft { get; set; }
-
-    /// <summary>内容右边界位置</summary>
-    public int ContentRight { get; set; }
-
-    /// <summary>检测到的边框层数</summary>
-    public int BorderLayers { get; set; }
-
-    /// <summary>边框颜色层次(从外到内)</summary>
-    public List<Rgba32> BorderColors { get; set; }
-
-    /// <summary>顶部边框总宽度(像素)</summary>
-    public int TopBorderWidth => ContentTop;
-
-    /// <summary>底部边框总宽度(像素)</summary>
-    public int BottomBorderWidth => ImageHeight - 1 - ContentBottom;
-
-    /// <summary>左侧边框总宽度(像素)</summary>
-    public int LeftBorderWidth => ContentLeft;
-
-    /// <summary>右侧边框总宽度(像素)</summary>
-    public int RightBorderWidth => ImageWidth - 1 - ContentRight;
-
-    /// <summary>是否有顶部边框</summary>
-    public bool HasTopBorder => TopBorderWidth > 0;
-
-    /// <summary>是否有底部边框</summary>
-    public bool HasBottomBorder => BottomBorderWidth > 0;
-
-    /// <summary>是否有左侧边框</summary>
-    public bool HasLeftBorder => LeftBorderWidth > 0;
-
-    /// <summary>是否有右侧边框</summary>
-    public bool HasRightBorder => RightBorderWidth > 0;
-
-    /// <summary>是否有任意边框</summary>
-    public bool HasAnyBorder => BorderCount > 0;
-
-    /// <summary>是否满足裁剪条件(至少两个边)</summary>
-    public bool CanBeCropped => BorderCount >= 2;
-
-    public int BorderCount => (HasTopBorder ? 1 : 0) + (HasBottomBorder ? 1 : 0) + (HasLeftBorder ? 1 : 0) + (HasRightBorder ? 1 : 0);
-
-    /// <summary>内容区域宽度</summary>
-    public int ContentWidth => ContentRight - ContentLeft + 1;
-
-    /// <summary>内容区域高度</summary>
-    public int ContentHeight => ContentBottom - ContentTop + 1;
 }

+ 15 - 0
Masuit.Tools.Abstractions/Media/LCHColor.cs

@@ -0,0 +1,15 @@
+namespace Masuit.Tools.Media;
+
+public struct LCHColor
+{
+    public double L { get; }
+    public double C { get; }
+    public double H { get; }
+
+    public LCHColor(double l, double c, double h)
+    {
+        L = l;
+        C = c;
+        H = h;
+    }
+}

+ 15 - 0
Masuit.Tools.Abstractions/Media/LabColor.cs

@@ -0,0 +1,15 @@
+namespace Masuit.Tools.Media;
+
+public struct LabColor
+{
+    public double L { get; }
+    public double a { get; }
+    public double b { get; }
+
+    public LabColor(double l, double a, double b)
+    {
+        L = l;
+        this.a = a;
+        this.b = b;
+    }
+}

+ 14 - 0
Masuit.Tools.Abstractions/Media/ToleranceMode.cs

@@ -0,0 +1,14 @@
+namespace Masuit.Tools.Media;
+
+public enum ToleranceMode
+{
+    /// <summary>
+    /// 通道
+    /// </summary>
+    Channel,
+
+    /// <summary>
+    /// ΔE
+    /// </summary>
+    DeltaE
+}

+ 15 - 0
Masuit.Tools.Abstractions/Media/XYZColor.cs

@@ -0,0 +1,15 @@
+namespace Masuit.Tools.Media;
+
+public struct XYZColor
+{
+    public double X { get; }
+    public double Y { get; }
+    public double Z { get; }
+
+    public XYZColor(double x, double y, double z)
+    {
+        X = x;
+        Y = y;
+        Z = z;
+    }
+}

+ 15 - 0
Masuit.Tools.Abstractions/Media/YXZColor.cs

@@ -0,0 +1,15 @@
+namespace Masuit.Tools.Media;
+
+public struct YXZColor
+{
+    public double Y { get; }
+    public double X { get; }
+    public double Z { get; }
+
+    public YXZColor(double y, double x, double z)
+    {
+        Y = y;
+        X = x;
+        Z = z;
+    }
+}

+ 1 - 1
Masuit.Tools.Excel/Masuit.Tools.Excel.csproj

@@ -37,7 +37,7 @@
       </None>
     </ItemGroup>
     <ItemGroup>
-        <PackageReference Include="EPPlus" Version="8.0.5" />
+        <PackageReference Include="EPPlus" Version="8.0.6" />
         <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
     </ItemGroup>
     <ItemGroup>

+ 11 - 2
README.md

@@ -745,9 +745,18 @@ gif.GetFrames(@"D:\frames\"); // 解压gif每帧图片
 var marker=ImageWatermarker(stream);
 stream=maker.AddWatermark("水印文字","字体文件",字体大小,color,水印位置,边距); // 给图片添加水印
 stream=maker.AddWatermark(水印图片,水印位置,边距,字体大小,字体); // 给图片添加水印
+```
 
-var borderInfo=ImageBorderRemover.DetectBorders(原始图片); // 检测图片是否包含纯色边框
-ImageBorderRemover.RemoveBorders(原始图片,保存图片); // 移除图片的纯色边框并另存为
+```csharp
+var borderInfo=new ImageBorderRemover(ToleranceMode.Channel).DetectBorders(原始图片); // 检测图片是否包含纯色边框
+new ImageBorderRemover(ToleranceMode.Channel).RemoveBorders(原始图片,保存图片); // 移除图片的纯色边框并另存为
+```
+
+```csharp
+var delta = ColorDeltaE.CIE1976(color1,color2); // 计算两个颜色的CIE1976色差
+var delta = ColorDeltaE.CIE1994(color1,color2); // 计算两个颜色的CIE1994色差
+var delta = ColorDeltaE.CIE2000(color1,color2); // 计算两个颜色的CIE2000色差
+var delta = ColorDeltaE.CMC(color1,color2); // 计算两个颜色的CMC(l:c)色差
 ```
 ```csharp
 // 图像相似度对比

+ 0 - 1
Test/Masuit.Tools.Abstractions.Test/Masuit.Tools.Abstractions.Test.csproj

@@ -35,7 +35,6 @@
     <Folder Include="Files\" />
     <Folder Include="Html\" />
     <Folder Include="Linq\" />
-    <Folder Include="Media\" />
     <Folder Include="TextDiff\" />
   </ItemGroup>
 

+ 110 - 0
Test/Masuit.Tools.Abstractions.Test/Media/ColorDeltaETests.cs

@@ -0,0 +1,110 @@
+using Masuit.Tools.Media;
+using Xunit;
+
+namespace Masuit.Tools.Abstractions.Test.Media;
+
+public class ColorDeltaETests
+{
+    [Fact]
+    public void CIE1976_LabColor_SameColor_ReturnsZero()
+    {
+        var lab1 = new LabColor(50, 20, 30);
+        var lab2 = new LabColor(50, 20, 30);
+
+        double deltaE = ColorDeltaE.CIE1976(lab1, lab2);
+
+        Assert.Equal(0, deltaE, 6);
+    }
+
+    [Fact]
+    public void CIE1976_LabColor_DifferentColor_ReturnsPositive()
+    {
+        var lab1 = new LabColor(50, 20, 30);
+        var lab2 = new LabColor(60, 25, 35);
+
+        double deltaE = ColorDeltaE.CIE1976(lab1, lab2);
+
+        Assert.True(deltaE > 0);
+    }
+
+    [Fact]
+    public void CIE1994_LabColor_SameColor_ReturnsZero()
+    {
+        var lab1 = new LabColor(50, 20, 30);
+        var lab2 = new LabColor(50, 20, 30);
+
+        double deltaE = ColorDeltaE.CIE1994(lab1, lab2);
+
+        Assert.Equal(0, deltaE, 6);
+    }
+
+    [Fact]
+    public void CIE1994_LabColor_DifferentColor_ReturnsPositive()
+    {
+        var lab1 = new LabColor(50, 20, 30);
+        var lab2 = new LabColor(60, 25, 35);
+
+        double deltaE = ColorDeltaE.CIE1994(lab1, lab2);
+
+        Assert.True(deltaE > 0);
+    }
+
+    [Fact]
+    public void CIE1994_LabColor_TextileParameter()
+    {
+        var lab1 = new LabColor(50, 20, 30);
+        var lab2 = new LabColor(60, 25, 35);
+
+        double deltaE1 = ColorDeltaE.CIE1994(lab1, lab2, textile: false);
+        double deltaE2 = ColorDeltaE.CIE1994(lab1, lab2, textile: true);
+
+        Assert.NotEqual(deltaE1, deltaE2);
+    }
+
+    [Fact]
+    public void CIE2000_LabColor_SameColor_ReturnsZero()
+    {
+        var lab1 = new LabColor(50, 20, 30);
+        var lab2 = new LabColor(50, 20, 30);
+
+        double deltaE = ColorDeltaE.CIE2000(lab1, lab2);
+
+        Assert.Equal(0, deltaE, 6);
+    }
+
+    [Fact]
+    public void CIE2000_LabColor_DifferentColor_ReturnsPositive()
+    {
+        var lab1 = new LabColor(50, 20, 30);
+        var lab2 = new LabColor(60, 25, 35);
+
+        double deltaE = ColorDeltaE.CIE2000(lab1, lab2);
+
+        Assert.True(deltaE > 0);
+    }
+
+    [Fact]
+    public void CMC_LabColor_SameColor_ReturnsZero()
+    {
+        var lab1 = new LabColor(50, 20, 30);
+        var lab2 = new LabColor(50, 20, 30);
+
+        double deltaE = ColorDeltaE.CMC(lab1, lab2);
+
+        Assert.Equal(0, deltaE, 6);
+    }
+
+    [Fact]
+    public void CMC_LabColor_DifferentColor_ReturnsPositive()
+    {
+        var lab1 = new LabColor(50, 20, 30);
+        var lab2 = new LabColor(60, 25, 35);
+
+        double deltaE = ColorDeltaE.CMC(lab1, lab2);
+
+        Assert.True(deltaE > 0);
+    }
+
+    // 可选:为其它颜色空间(如CMYColor、CMYKColor、HSLColor等)补充类似测试
+    // 这里只演示LabColor,因其它重载最终都转为LabColor处理
+}

+ 54 - 0
Test/Masuit.Tools.Abstractions.Test/Media/ImageBorderRemoverTests.cs

@@ -0,0 +1,54 @@
+using System;
+using System.IO;
+using Masuit.Tools.Media;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using Xunit;
+
+namespace Masuit.Tools.Abstractions.Test.Media;
+
+public class ImageBorderRemoverTests
+{
+    private Image<Rgba32> CreateTestImage(int width, int height, Rgba32 borderColor, int borderSize = 5)
+    {
+        var image = new Image<Rgba32>(width, height);
+        // 填充边框
+        for (int y = 0; y < height; y++)
+            for (int x = 0; x < width; x++)
+            {
+                if (x < borderSize || x >= width - borderSize || y < borderSize || y >= height - borderSize)
+                    image[x, y] = borderColor;
+                else
+                    image[x, y] = new Rgba32(Random.Shared.Next(255), Random.Shared.Next(255), Random.Shared.Next(255));
+            }
+        return image;
+    }
+
+    [Fact]
+    public void DetectBorders_ImageObject_ShouldDetectBorder()
+    {
+        var remover = new ImageBorderRemover(ToleranceMode.Channel);
+        using var image = CreateTestImage(30, 30, new Rgba32(255, 0, 0), 3);
+
+        var result = remover.DetectBorders(image, 10);
+
+        Assert.True(result.HasAnyBorder);
+    }
+
+    [Fact]
+    public void RemoveBorders_Stream_ShouldReturnCroppedStream()
+    {
+        var remover = new ImageBorderRemover(ToleranceMode.Channel);
+        using var image = CreateTestImage(60, 60, new Rgba32(255, 0, 0), 6);
+        using var ms = new MemoryStream();
+        image.SaveAsPng(ms);
+        ms.Position = 0;
+
+        using var resultStream = remover.RemoveBorders(ms, 0);
+        resultStream.Position = 0;
+        using var cropped = Image.Load<Rgba32>(resultStream);
+
+        Assert.True(48 >= cropped.Width);
+        Assert.True(48 >= cropped.Height);
+    }
+}