696 lines
27 KiB
C#
696 lines
27 KiB
C#
using CrazyStudio.Core.Common.Tools;
|
||
using HPPH;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Drawing;
|
||
using System.Drawing.Imaging;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using TouchSocket.Core;
|
||
|
||
//public enum QuantizationAlgorithm { MedianCut, AdaptiveSpatial, NeuQuant }
|
||
//public enum QuantizationAlgorithm { MedianCut, AdaptiveSpatial }
|
||
public enum QuantizationAlgorithm { MedianCut }
|
||
public enum DitheringType { None, FloydSteinberg, Riemersma }
|
||
|
||
public class ColorVectorConverter
|
||
{
|
||
private string _potraceExePath = "potrace.exe";
|
||
private string _pngquantPath = "pngquant.exe"; // 需安装pngquant
|
||
private string _pngnqPath = "pngnq.exe"; // 需安装pngnq
|
||
private string _convertPath = "magick convert"; // ImageMagick的convert工具
|
||
private bool _verbose = false;
|
||
private int _colorCount = 16; // 目标颜色数量
|
||
private QuantizationAlgorithm _quantAlgorithm = QuantizationAlgorithm.MedianCut;
|
||
private DitheringType _dithering = DitheringType.None;
|
||
private bool _stackTracing = true; // 堆栈描摹
|
||
private float _prescale = 1.0f; // 预缩放比例(提升细节)
|
||
private int maxHandleSize = 3072;
|
||
public ColorVectorConverter(string potraceExePath, string pngquantPath, bool verbose = false)
|
||
{
|
||
_potraceExePath = potraceExePath;
|
||
_pngquantPath = pngquantPath;
|
||
_verbose = verbose;
|
||
}
|
||
|
||
public async Task ConvertToSvg(string inputFile, string outputFile, int colorCount = 16, float prescale = 1.0f,
|
||
QuantizationAlgorithm quantAlgorithm = QuantizationAlgorithm.MedianCut,
|
||
DitheringType dithering = DitheringType.None, bool stackTracing = false)
|
||
{
|
||
_colorCount = Math.Clamp(colorCount, 2, 256);
|
||
//限制倍数到0.5-2倍
|
||
_prescale = Math.Clamp(prescale, 0.5f, 2);
|
||
_quantAlgorithm = quantAlgorithm;
|
||
_dithering = dithering;
|
||
_stackTracing = stackTracing;
|
||
try
|
||
{
|
||
Image? originalImage = null;
|
||
if (inputFile.StartsWith("https:") || inputFile.StartsWith("http:"))
|
||
{
|
||
originalImage = await HttpTool.GetImageFromUrlAsync(inputFile);
|
||
}
|
||
else
|
||
{
|
||
#pragma warning disable CA1416 // 验证平台兼容性
|
||
originalImage = Image.FromFile(inputFile);
|
||
#pragma warning restore CA1416 // 验证平台兼容性
|
||
}
|
||
using (originalImage)
|
||
{
|
||
Bitmap? scaledImage = null;
|
||
int maxSize = Math.Max(originalImage.Width, originalImage.Height);
|
||
if (maxSize * _prescale > maxHandleSize)
|
||
{
|
||
int newWidth = 0, newHeight = 0;
|
||
if (originalImage.Width > originalImage.Height)
|
||
{
|
||
newWidth = maxHandleSize;
|
||
newHeight = (int)(originalImage.Height * (newWidth / (float)originalImage.Width));
|
||
}
|
||
else
|
||
{
|
||
newHeight = maxHandleSize;
|
||
newWidth = (int)(originalImage.Width * (newHeight / (float)originalImage.Height));
|
||
}
|
||
scaledImage = ResizeAndPreserveAlpha(originalImage, newWidth, newHeight);
|
||
_prescale = (float)(Math.Max(newWidth, newHeight)) / Math.Max(originalImage.Width, originalImage.Height);
|
||
}
|
||
else
|
||
{
|
||
// 步骤1:预缩放并确保包含Alpha通道(关键:保留透明信息)
|
||
scaledImage = PrescaleAndPreserveAlpha(originalImage);
|
||
}
|
||
|
||
// 步骤2:颜色量化(保留透明通道)
|
||
var quantizedImagePath = QuantizeImage(scaledImage!);
|
||
#pragma warning disable CA1416 // 验证平台兼容性
|
||
var quantizedImage = new Bitmap(quantizedImagePath);
|
||
#pragma warning restore CA1416 // 验证平台兼容性
|
||
// 步骤3:提取量化后的颜色表(排除透明色)
|
||
var colorTable = ExtractColorTable(quantizedImage);
|
||
|
||
if (_verbose)
|
||
Console.WriteLine($"量化完成,保留 {colorTable.Count} 种颜色(已排除透明)");
|
||
|
||
// 步骤4:基于量化颜色表生成颜色段
|
||
var colorSegments = CreateSegmentsFromColorTable(colorTable);
|
||
// 步骤5:生成每个颜色段的掩码并矢量化(保留透明区域)
|
||
var svgLayers = new List<string>();
|
||
for (int i = 0; i < colorSegments.Count; i++)
|
||
{
|
||
var segment = colorSegments[i];
|
||
var mask = CreateSegmentMask(quantizedImage, segment.Colors, colorSegments, i);
|
||
var svgLayer = VectorizeMask(mask, segment.AverageColor);
|
||
if (!string.IsNullOrEmpty(svgLayer))
|
||
svgLayers.Add(svgLayer);
|
||
}
|
||
|
||
quantizedImage.Dispose();
|
||
if (File.Exists(quantizedImagePath)) File.Delete(quantizedImagePath);
|
||
// 步骤6:合并图层并保存(确保SVG背景透明)
|
||
|
||
float dpiX = scaledImage.HorizontalResolution;
|
||
float dpiY = scaledImage.HorizontalResolution;
|
||
|
||
SaveCombinedSvg(svgLayers, outputFile, scaledImage.Width, scaledImage.Height, dpiX, dpiY);
|
||
scaledImage.Dispose();
|
||
//Console.WriteLine($"SVG生成完成:{outputFile}");
|
||
}
|
||
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"转换失败:{ex.Message}");
|
||
if (_verbose)
|
||
Console.WriteLine(ex.StackTrace);
|
||
}
|
||
}
|
||
|
||
#region 核心优化:保留透明通道
|
||
|
||
/// <summary>
|
||
/// 预缩放并确保Alpha通道存在(关键:保留原始透明信息)
|
||
/// </summary>
|
||
private Bitmap PrescaleAndPreserveAlpha(Image image)
|
||
{
|
||
int newWidth = (int)(image.Width * _prescale);
|
||
int newHeight = (int)(image.Height * _prescale);
|
||
// 强制使用带Alpha通道的格式
|
||
var scaled = new Bitmap(newWidth, newHeight, PixelFormat.Format32bppArgb);
|
||
using (var g = Graphics.FromImage(scaled))
|
||
{
|
||
// 初始化透明背景
|
||
g.Clear(Color.Transparent);
|
||
// 高质量缩放并保留透明度
|
||
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||
g.DrawImage(image, 0, 0, newWidth, newHeight);
|
||
}
|
||
return scaled;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 预缩放并确保Alpha通道存在(关键:保留原始透明信息)
|
||
/// </summary>
|
||
private Bitmap ResizeAndPreserveAlpha(Image image, int newWidth, int newHeight)
|
||
{
|
||
// 强制使用带Alpha通道的格式
|
||
var scaled = new Bitmap(newWidth, newHeight, PixelFormat.Format32bppArgb);
|
||
using (var g = Graphics.FromImage(scaled))
|
||
{
|
||
// 初始化透明背景
|
||
g.Clear(Color.Transparent);
|
||
// 高质量缩放并保留透明度
|
||
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||
g.DrawImage(image, 0, 0, newWidth, newHeight);
|
||
}
|
||
return scaled;
|
||
}
|
||
/// <summary>
|
||
/// 颜色量化(保留透明通道,不量化透明像素)
|
||
/// </summary>
|
||
private string QuantizeImage(Bitmap image)
|
||
{
|
||
string tempInput = Path.GetTempFileName() + ".png";
|
||
string tempOutput = Path.GetTempFileName() + ".png";
|
||
// 保存时确保保留Alpha通道
|
||
image.Save(tempInput, ImageFormat.Png);
|
||
|
||
try
|
||
{
|
||
switch (_quantAlgorithm)
|
||
{
|
||
case QuantizationAlgorithm.MedianCut:
|
||
// pngquant默认保留透明通道
|
||
string ditherArg = _dithering == DitheringType.FloydSteinberg ? "" : "--nofs";
|
||
RunCommand($"{_pngquantPath} --force {ditherArg} {_colorCount} --output {tempOutput} {tempInput}");
|
||
break;
|
||
//case QuantizationAlgorithm.NeuQuant:
|
||
// // pngnq需要特殊参数保留透明
|
||
// string ditherNq = _dithering == DitheringType.FloydSteinberg ? "-Q f" : "";
|
||
// RunCommand($"{_pngnqPath} -f {ditherNq} -n {_colorCount} -e .png -k transparent {tempInput}");
|
||
// string nqOutput = Path.ChangeExtension(tempInput, "~quant.png");
|
||
// if (File.Exists(nqOutput)) File.Move(nqOutput, tempOutput);
|
||
// break;
|
||
//case QuantizationAlgorithm.AdaptiveSpatial:
|
||
// // ImageMagick明确保留透明
|
||
// string ditherIm = _dithering switch
|
||
// {
|
||
// DitheringType.FloydSteinberg => "-dither FloydSteinberg",
|
||
// DitheringType.Riemersma => "-dither Riemersma",
|
||
// _ => "-dither None"
|
||
// };
|
||
// RunCommand($"{_convertPath} {tempInput} {ditherIm} -colors {_colorCount} -channel RGBA -separate -combine {tempOutput}");
|
||
// break;
|
||
}
|
||
return tempOutput;
|
||
}
|
||
finally
|
||
{
|
||
File.Delete(tempInput);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 提取颜色表(排除透明色)
|
||
/// </summary>
|
||
private List<Color> ExtractColorTable(Bitmap image)
|
||
{
|
||
// 存储唯一颜色(排除透明)
|
||
var uniqueColors = new HashSet<int>();
|
||
Rectangle rect = new Rectangle(0, 0, image.Width, image.Height);
|
||
|
||
// 锁定像素数据以提高访问效率
|
||
BitmapData data = image.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
||
try
|
||
{
|
||
IntPtr ptr = data.Scan0;
|
||
int bytes = Math.Abs(data.Stride) * image.Height;
|
||
byte[] argbValues = new byte[bytes];
|
||
Marshal.Copy(ptr, argbValues, 0, bytes);
|
||
|
||
// 遍历所有像素
|
||
for (int i = 0; i < argbValues.Length; i += 4)
|
||
{
|
||
// 从ARGB数组中解析通道(注意:存储顺序是BGR而非RGB)
|
||
byte blue = argbValues[i];
|
||
byte green = argbValues[i + 1];
|
||
byte red = argbValues[i + 2];
|
||
byte alpha = argbValues[i + 3];
|
||
|
||
// 跳过透明或接近透明的像素(Alpha <= 10)
|
||
if (alpha <= 10)
|
||
{
|
||
continue;
|
||
}
|
||
// 将颜色转换为ARGB整数并添加到哈希集(自动去重)
|
||
int colorArgb = Color.FromArgb(alpha, red, green, blue).ToArgb();
|
||
uniqueColors.Add(colorArgb);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
// 解锁像素数据
|
||
image.UnlockBits(data);
|
||
}
|
||
|
||
// 将哈希集中的ARGB值转换为Color对象并返回
|
||
return uniqueColors.Distinct().Select(argb => Color.FromArgb(argb)).ToList();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 掩码生成与透明处理优化
|
||
|
||
/// <summary>
|
||
/// 创建颜色段掩码(严格保留透明区域)
|
||
/// </summary>
|
||
private Bitmap CreateSegmentMask(Bitmap image, List<Color> segmentColors, List<ColorSegment> allSegments, int currentIndex)
|
||
{
|
||
int width = image.Width;
|
||
int height = image.Height;
|
||
var mask = new Bitmap(width, height, PixelFormat.Format32bppArgb);
|
||
|
||
Rectangle rect = new Rectangle(0, 0, width, height);
|
||
BitmapData srcData = image.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
||
BitmapData destData = mask.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
|
||
|
||
try
|
||
{
|
||
IntPtr srcPtr = srcData.Scan0;
|
||
IntPtr destPtr = destData.Scan0;
|
||
int stride = srcData.Stride;
|
||
int bytesPerPixel = 4;
|
||
|
||
// 当前颜色段的颜色集
|
||
HashSet<int> currentColors = new HashSet<int>(segmentColors.Select(c => c.ToArgb()));
|
||
// 堆栈描摹颜色集
|
||
HashSet<int> stackColors = new HashSet<int>();
|
||
if (_stackTracing && currentIndex < allSegments.Count - 1)
|
||
{
|
||
for (int i = currentIndex + 1; i < allSegments.Count; i++)
|
||
foreach (var c in allSegments[i].Colors)
|
||
stackColors.Add(c.ToArgb());
|
||
}
|
||
|
||
Parallel.For(0, height, y =>
|
||
{
|
||
unsafe
|
||
{
|
||
byte* srcRow = (byte*)srcPtr + (y * stride);
|
||
byte* destRow = (byte*)destPtr + (y * stride);
|
||
|
||
for (int x = 0; x < width; x++)
|
||
{
|
||
// 读取原始像素的Alpha值(关键:判断透明)
|
||
byte a = srcRow[x * bytesPerPixel + 3];
|
||
byte b = srcRow[x * bytesPerPixel];
|
||
byte g = srcRow[x * bytesPerPixel + 1];
|
||
byte r = srcRow[x * bytesPerPixel + 2];
|
||
int argb = Color.FromArgb(a, r, g, b).ToArgb();
|
||
|
||
// 完全透明的像素(Alpha=0)在掩码中也保持透明
|
||
if (a <= 10) // 允许10的容差(接近透明)
|
||
{
|
||
//SetPixel(destRow, x, 0, 0, 0, 0); // 透明
|
||
SetPixel(destRow, x, 255, 255, 255, 255); // 背景(白)
|
||
}
|
||
// 非透明像素按颜色段处理
|
||
else if (currentColors.Contains(argb) || (_stackTracing && stackColors.Contains(argb)))
|
||
{
|
||
SetPixel(destRow, x, 0, 0, 0, 255); // 前景(黑)
|
||
}
|
||
else
|
||
{
|
||
SetPixel(destRow, x, 255, 255, 255, 255); // 背景(白)
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
finally
|
||
{
|
||
image.UnlockBits(srcData);
|
||
mask.UnlockBits(destData);
|
||
}
|
||
|
||
return SmoothMask(mask);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 矢量化与SVG生成(确保透明背景)
|
||
|
||
/// <summary>
|
||
/// 矢量化(优化Potrace参数,保留细节)
|
||
/// </summary>
|
||
private string VectorizeMask(Bitmap mask, Color color)
|
||
{
|
||
if (!HasValidContent(mask)) return null;
|
||
|
||
using (var stream = new MemoryStream())
|
||
{
|
||
// 临时文件存储掩码(避免内存流格式问题,Potrace对标准输入的BMP格式可能有严格要求)
|
||
//string tempBmpPath = Path.GetTempFileName() + ".bmp";
|
||
|
||
//mask.Save(@"F:\111.bmp", ImageFormat.Bmp);
|
||
//mask.Save(@"F:\111.png", ImageFormat.Png);
|
||
mask.Save(stream, ImageFormat.Bmp);
|
||
stream.Position = 0;
|
||
|
||
float dpiX = mask.HorizontalResolution;
|
||
float dpiY = mask.VerticalResolution;
|
||
|
||
// 生成Potrace命令(添加平滑参数,保留区域形态)
|
||
string colorHex = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
|
||
var args = new List<string>
|
||
{
|
||
"--svg",
|
||
$"-C {colorHex}", // 应用颜色段的平均颜色
|
||
"-t 1", // 轻微去噪,保留细节
|
||
"-a 1.0", // 平滑转角
|
||
"-O 0.5", // 适度优化路径
|
||
"-s",
|
||
$"-r {dpiX}",
|
||
$"--width {mask.Width /_prescale / dpiX}",//单位英寸,1英寸=像素数量/dpi
|
||
$"--height {mask.Height / _prescale / dpiY}",
|
||
"-"
|
||
};
|
||
|
||
mask.Dispose();
|
||
// 调用Potrace矢量化
|
||
using (var process = new Process())
|
||
{
|
||
process.StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = _potraceExePath,
|
||
Arguments = string.Join(" ", args),
|
||
RedirectStandardInput = true,
|
||
RedirectStandardOutput = true,
|
||
RedirectStandardError = true,
|
||
UseShellExecute = false,
|
||
CreateNoWindow = true,
|
||
StandardOutputEncoding = Encoding.UTF8
|
||
};
|
||
|
||
process.Start();
|
||
stream.CopyTo(process.StandardInput.BaseStream);
|
||
process.StandardInput.Close();
|
||
|
||
string svgOutput = process.StandardOutput.ReadToEnd();
|
||
string error = process.StandardError.ReadToEnd();
|
||
process.WaitForExit();
|
||
|
||
if (process.ExitCode != 0)
|
||
{
|
||
if (_verbose)
|
||
Console.WriteLine($"矢量化错误:{error}");
|
||
return string.Empty;
|
||
}
|
||
|
||
|
||
//File.WriteAllText(@$"F:\{Guid.NewGuid()}.svg", svgOutput);
|
||
return ExtractSvgPath(svgOutput, color.A);
|
||
}
|
||
}
|
||
}
|
||
|
||
private string ExtractSvgPath(string rawSvg, byte alpha)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(rawSvg)) return string.Empty;
|
||
|
||
// 匹配原始<g>标签及其属性(非贪婪匹配)
|
||
var gTagRegex = new System.Text.RegularExpressions.Regex(
|
||
@"<g\s+([^>]*?)>",
|
||
System.Text.RegularExpressions.RegexOptions.IgnoreCase
|
||
);
|
||
var gTagMatch = gTagRegex.Match(rawSvg);
|
||
|
||
// 提取原始<g>标签内的所有属性
|
||
string originalGAttributes = gTagMatch.Success ? gTagMatch.Groups[1].Value : "";
|
||
|
||
// 提取所有path元素
|
||
var pathRegex = new System.Text.RegularExpressions.Regex(
|
||
@"<path\s+[^>]*?/>",
|
||
System.Text.RegularExpressions.RegexOptions.IgnoreCase
|
||
);
|
||
var matches = pathRegex.Matches(rawSvg);
|
||
if (matches.Count == 0) return string.Empty;
|
||
|
||
// 计算透明度属性(如果需要)
|
||
string opacityAttribute = alpha < 255 ? $"opacity=\"{alpha / 255.0:0.00}\"" : "";
|
||
|
||
// 合并原始属性和新透明度属性(去重处理)
|
||
var attributes = new List<string>();
|
||
if (!string.IsNullOrWhiteSpace(originalGAttributes))
|
||
{
|
||
attributes.AddRange(originalGAttributes.Split(
|
||
new[] { ' ' },
|
||
StringSplitOptions.RemoveEmptyEntries
|
||
));
|
||
}
|
||
if (!string.IsNullOrWhiteSpace(opacityAttribute))
|
||
{
|
||
// 移除可能存在的旧opacity属性
|
||
attributes.RemoveAll(a => a.StartsWith("opacity=", StringComparison.OrdinalIgnoreCase));
|
||
attributes.Add(opacityAttribute);
|
||
}
|
||
|
||
// 构建新的<g>标签
|
||
var result = new StringBuilder();
|
||
result.Append($"<g {string.Join(" ", attributes)}>");
|
||
|
||
// 添加所有path元素
|
||
foreach (var match in matches)
|
||
result.AppendLine(match.ToString());
|
||
|
||
result.Append("</g>");
|
||
return result.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存合并的SVG(明确设置透明背景)
|
||
/// </summary>
|
||
private void SaveCombinedSvg(List<string> layers, string outputPath, int width, int height, float dpiX, float dpiY)
|
||
{
|
||
//width = (int)(width / _prescale);
|
||
//height = (int)(height / _prescale);
|
||
int widthPx = (int)((width / _prescale) * (72 / dpiX));// $"{width / _prescale * 72 / dpiX}pt";
|
||
int heightPx = (int)((height / _prescale) * (72 / dpiX));//$"{height / _prescale * 72 / dpiY}pt";
|
||
|
||
var svg = new StringBuilder();
|
||
svg.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||
// 关键:SVG根元素不设置背景色,默认透明
|
||
svg.AppendLine($"<svg xmlns=\"http://www.w3.org/2000/svg\" " +
|
||
$"width=\"{widthPx}\" height=\"{heightPx}\" " +
|
||
$"viewBox=\"0 0 {widthPx} {heightPx}\" " +
|
||
//$"style=\"transform: scale({1 / _prescale})\" " +
|
||
$"preserveAspectRatio=\"xMidYMid meet\">");
|
||
|
||
// 添加透明背景提示(不影响实际显示,但明确意图)
|
||
svg.AppendLine($" <!-- 透明背景 -->");
|
||
|
||
// 按亮度排序图层
|
||
foreach (var layer in layers.OrderBy(l => GetLayerBrightness(l)))
|
||
svg.AppendLine(layer);
|
||
|
||
svg.AppendLine("</svg>");
|
||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||
File.WriteAllText(outputPath, svg.ToString(), new UTF8Encoding(false));
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 其他工具方法(保持不变)
|
||
|
||
private string RunCommand(string command, bool returnOutput = false)
|
||
{
|
||
using (var process = new Process())
|
||
{
|
||
process.StartInfo = new ProcessStartInfo
|
||
{
|
||
FileName = "cmd.exe",
|
||
Arguments = $"/c {command}",
|
||
RedirectStandardOutput = returnOutput,
|
||
RedirectStandardError = true,
|
||
UseShellExecute = false,
|
||
CreateNoWindow = true,
|
||
StandardOutputEncoding = returnOutput ? Encoding.UTF8 : null
|
||
};
|
||
|
||
process.Start();
|
||
string error = process.StandardError.ReadToEnd();
|
||
process.WaitForExit();
|
||
|
||
if (process.ExitCode != 0)
|
||
throw new Exception($"命令执行失败:{command}\n错误:{error}");
|
||
|
||
if (returnOutput)
|
||
{
|
||
string lastCommandOutput = process.StandardOutput.ReadToEnd();
|
||
return lastCommandOutput;
|
||
}
|
||
}
|
||
return string.Empty;
|
||
}
|
||
|
||
private unsafe bool IsWhite(byte* row, int x, int bytesPerPixel)
|
||
{
|
||
int idx = x * bytesPerPixel;
|
||
return row[idx] == 255 && row[idx + 1] == 255 && row[idx + 2] == 255 && row[idx + 3] == 255;
|
||
}
|
||
|
||
private int CountBlackNeighbors(IntPtr srcPtr, int stride, int x, int y, int bytesPerPixel)
|
||
{
|
||
int count = 0;
|
||
unsafe
|
||
{
|
||
for (int dy = -1; dy <= 1; dy++)
|
||
{
|
||
byte* neighborRow = (byte*)srcPtr + ((y + dy) * stride);
|
||
for (int dx = -1; dx <= 1; dx++)
|
||
{
|
||
if (dx == 0 && dy == 0) continue;
|
||
int nx = x + dx;
|
||
int idx = nx * bytesPerPixel;
|
||
if (neighborRow[idx] == 0 && neighborRow[idx + 1] == 0 &&
|
||
neighborRow[idx + 2] == 0 && neighborRow[idx + 3] == 255)
|
||
count++;
|
||
}
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
private unsafe void SetPixel(byte* row, int x, byte b, byte g, byte r, byte a)
|
||
{
|
||
int idx = x * 4;
|
||
row[idx] = b;
|
||
row[idx + 1] = g;
|
||
row[idx + 2] = r;
|
||
row[idx + 3] = a;
|
||
}
|
||
|
||
private bool HasValidContent(Bitmap mask)
|
||
{
|
||
Rectangle rect = new Rectangle(0, 0, mask.Width, mask.Height);
|
||
BitmapData data = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
||
try
|
||
{
|
||
IntPtr ptr = data.Scan0;
|
||
int bytes = Math.Abs(data.Stride) * mask.Height;
|
||
byte[] argb = new byte[bytes];
|
||
Marshal.Copy(ptr, argb, 0, bytes);
|
||
|
||
for (int i = 0; i < argb.Length; i += 4)
|
||
if (argb[i] == 0 && argb[i + 1] == 0 && argb[i + 2] == 0 && argb[i + 3] == 255)
|
||
return true;
|
||
return false;
|
||
}
|
||
finally { mask.UnlockBits(data); }
|
||
}
|
||
|
||
private double GetLayerBrightness(string svgLayer)
|
||
{
|
||
var match = System.Text.RegularExpressions.Regex.Match(svgLayer, @"#([0-9A-Fa-f]{6})");
|
||
if (match.Success)
|
||
{
|
||
var color = ColorTranslator.FromHtml($"#{match.Groups[1].Value}");
|
||
return (0.299 * color.R + 0.587 * color.G + 0.114 * color.B) / 255;
|
||
}
|
||
return 0.5;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 轻度平滑(仅修复微小空洞,保留边缘)
|
||
/// </summary>
|
||
private Bitmap SmoothMask(Bitmap mask)
|
||
{
|
||
int width = mask.Width;
|
||
int height = mask.Height;
|
||
var smoothed = new Bitmap(mask);
|
||
|
||
Rectangle rect = new Rectangle(0, 0, width, height);
|
||
BitmapData srcData = mask.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
||
BitmapData destData = smoothed.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
|
||
|
||
try
|
||
{
|
||
IntPtr srcPtr = srcData.Scan0;
|
||
IntPtr destPtr = destData.Scan0;
|
||
int stride = srcData.Stride;
|
||
int bytesPerPixel = 4;
|
||
|
||
Parallel.For(1, height - 1, y =>
|
||
{
|
||
unsafe
|
||
{
|
||
byte* srcRow = (byte*)srcPtr + (y * stride);
|
||
byte* destRow = (byte*)destPtr + (y * stride);
|
||
|
||
for (int x = 1; x < width - 1; x++)
|
||
{
|
||
// 仅修复完全孤立的1x1空洞(避免模糊边缘)
|
||
if (IsWhite(srcRow, x, bytesPerPixel))
|
||
{
|
||
int blackCount = CountBlackNeighbors(srcPtr, stride, x, y, bytesPerPixel);
|
||
if (blackCount == 8) // 被8个黑像素包围的白像素(空洞)
|
||
SetPixel(destRow, x, 0, 0, 0, 255);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
finally
|
||
{
|
||
mask.UnlockBits(srcData);
|
||
smoothed.UnlockBits(destData);
|
||
}
|
||
|
||
return smoothed;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 基于量化颜色表创建颜色段(确保每个颜色对应一个段)
|
||
/// </summary>
|
||
private List<ColorSegment> CreateSegmentsFromColorTable(List<Color> colorTable)
|
||
{
|
||
// 按亮度排序(避免图层覆盖错误)
|
||
var sortedColors = colorTable.OrderBy(c => (0.299 * c.R + 0.587 * c.G + 0.114 * c.B)).ToList();
|
||
return sortedColors.Select((color, idx) =>
|
||
new ColorSegment(idx, new List<Color> { color })).ToList();
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
|
||
public class ColorSegment
|
||
{
|
||
public int Id { get; }
|
||
public List<Color> Colors { get; }
|
||
public Color AverageColor { get; }
|
||
|
||
public ColorSegment(int id, List<Color> colors)
|
||
{
|
||
Id = id;
|
||
Colors = colors;
|
||
AverageColor = colors.Count == 1 ? colors[0] : CalculateAverageColor(colors);
|
||
}
|
||
|
||
private Color CalculateAverageColor(List<Color> colors)
|
||
{
|
||
int r = 0, g = 0, b = 0, a = 0;
|
||
foreach (var c in colors)
|
||
{
|
||
r += c.R;
|
||
g += c.G;
|
||
b += c.B;
|
||
a += c.A;
|
||
}
|
||
return Color.FromArgb(a / colors.Count, r / colors.Count, g / colors.Count, b / colors.Count);
|
||
}
|
||
} |