472 lines
18 KiB
C#
472 lines
18 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Drawing;
|
||
using System.Drawing.Imaging;
|
||
using System.Linq;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace XPrint.Image.Tools
|
||
{
|
||
public class VectorizationTool
|
||
{
|
||
private const byte ALPHA_OPAQUE_THRESHOLD = 250; // 不透明Alpha阈值
|
||
private readonly float _prescale;
|
||
private readonly bool _verbose;
|
||
|
||
public VectorizationTool(float prescale = 1.0f, bool verbose = false)
|
||
{
|
||
_prescale = prescale;
|
||
_verbose = verbose;
|
||
}
|
||
|
||
public static bool HasValidContent(Bitmap mask)
|
||
{
|
||
if (mask == null) return false;
|
||
|
||
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);
|
||
|
||
// 检查是否有不透明像素(仅看Alpha通道)
|
||
for (int i = 0; i < argb.Length; i += 4)
|
||
{
|
||
if (argb[i + 3] >= ALPHA_OPAQUE_THRESHOLD)
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
finally
|
||
{
|
||
mask.UnlockBits(data);
|
||
}
|
||
}
|
||
|
||
#region 矢量化与SVG生成(确保透明背景)
|
||
|
||
/// <summary>
|
||
/// 矢量化(纯C#实现,不依赖外部exe)
|
||
/// </summary>
|
||
public string? VectorizeMask(Bitmap mask, Color color)
|
||
{
|
||
if (!HasValidContent(mask)) return null;
|
||
|
||
// 步骤1: 二值化图像(基于Alpha通道)
|
||
//using var binaryImage = BinarizeImage(mask);
|
||
|
||
// 步骤2: 提取轮廓
|
||
var contours = ExtractContours(mask);
|
||
|
||
// 步骤3: 简化轮廓
|
||
var simplifiedContours = SimplifyContours(contours);
|
||
|
||
// 步骤4: 转换为SVG
|
||
string svgOutput = ConvertContoursToSvg(simplifiedContours, color);
|
||
|
||
if(string.IsNullOrEmpty(svgOutput)) return null;
|
||
|
||
return ExtractSvgPath(svgOutput, color.A);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将图像二值化(不透明区域为黑,透明区域为白)
|
||
/// </summary>
|
||
private Bitmap BinarizeImage(Bitmap source)
|
||
{
|
||
int width = source.Width;
|
||
int height = source.Height;
|
||
var result = new Bitmap(width, height, PixelFormat.Format32bppArgb);
|
||
|
||
Rectangle rect = new Rectangle(0, 0, width, height);
|
||
BitmapData sourceData = source.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
||
BitmapData resultData = result.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
|
||
|
||
try
|
||
{
|
||
IntPtr sourcePtr = sourceData.Scan0;
|
||
IntPtr resultPtr = resultData.Scan0;
|
||
int stride = sourceData.Stride;
|
||
int pixelSize = 4;
|
||
|
||
// 分配托管数组
|
||
byte[] sourceArgb = new byte[Math.Abs(stride) * height];
|
||
Marshal.Copy(sourcePtr, sourceArgb, 0, sourceArgb.Length);
|
||
|
||
// 处理每个像素
|
||
for (int y = 0; y < height; y++)
|
||
{
|
||
for (int x = 0; x < width; x++)
|
||
{
|
||
int index = y * Math.Abs(stride) + x * pixelSize;
|
||
byte alpha = sourceArgb[index + 3];
|
||
|
||
// 计算结果图像中的索引
|
||
int resultIndex = y * Math.Abs(stride) + x * pixelSize;
|
||
|
||
if (alpha >= ALPHA_OPAQUE_THRESHOLD)
|
||
{
|
||
// 不透明区域 - 黑色
|
||
sourceArgb[resultIndex] = 0; // B
|
||
sourceArgb[resultIndex + 1] = 0; // G
|
||
sourceArgb[resultIndex + 2] = 0; // R
|
||
sourceArgb[resultIndex + 3] = 255;// A
|
||
}
|
||
else
|
||
{
|
||
// 透明区域 - 白色
|
||
sourceArgb[resultIndex] = 255; // B
|
||
sourceArgb[resultIndex + 1] = 255; // G
|
||
sourceArgb[resultIndex + 2] = 255; // R
|
||
sourceArgb[resultIndex + 3] = 255; // A
|
||
}
|
||
}
|
||
}
|
||
|
||
// 将处理后的数据复制回结果图像
|
||
Marshal.Copy(sourceArgb, 0, resultPtr, sourceArgb.Length);
|
||
}
|
||
finally
|
||
{
|
||
source.UnlockBits(sourceData);
|
||
result.UnlockBits(resultData);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 提取图像轮廓
|
||
/// </summary>
|
||
private List<List<Point>> ExtractContours(Bitmap binaryImage)
|
||
{
|
||
int width = binaryImage.Width;
|
||
int height = binaryImage.Height;
|
||
bool[,] visited = new bool[width, height];
|
||
var contours = new List<List<Point>>();
|
||
|
||
// 8个方向的搜索(上、下、左、右和四个对角线)
|
||
int[] dx = { -1, -1, -1, 0, 0, 1, 1, 1 };
|
||
int[] dy = { -1, 0, 1, -1, 1, -1, 0, 1 };
|
||
|
||
Rectangle rect = new Rectangle(0, 0, width, height);
|
||
BitmapData data = binaryImage.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
||
IntPtr scan0 = data.Scan0;
|
||
int stride = data.Stride;
|
||
int pixelSize = 4;
|
||
|
||
try
|
||
{
|
||
// 遍历每个像素寻找轮廓
|
||
for (int y = 0; y < height; y++)
|
||
{
|
||
for (int x = 0; x < width; x++)
|
||
{
|
||
if (!visited[x, y] && IsBlackPixel(scan0, stride, x, y, pixelSize))
|
||
{
|
||
// 检查是否为轮廓点(有白色邻居)
|
||
if (HasWhiteNeighbor(scan0, stride, x, y, width, height, dx, dy, pixelSize))
|
||
{
|
||
// 跟踪完整轮廓
|
||
List<Point> contour = TraceContour(scan0, stride, x, y, width, height, visited, dx, dy, pixelSize);
|
||
if (contour.Count > 3) // 过滤过小的轮廓
|
||
{
|
||
contours.Add(contour);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 标记内部点为已访问
|
||
visited[x, y] = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
binaryImage.UnlockBits(data);
|
||
}
|
||
|
||
return contours;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查是否为黑色像素(前景)
|
||
/// </summary>
|
||
private bool IsBlackPixel(IntPtr scan0, int stride, int x, int y, int pixelSize)
|
||
{
|
||
IntPtr address = scan0 + y * stride + x * pixelSize;
|
||
return Marshal.ReadByte(address) == 0 && // B
|
||
Marshal.ReadByte(address + 1) == 0 && // G
|
||
Marshal.ReadByte(address + 2) == 0; // R
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查是否有白色邻居(背景)
|
||
/// </summary>
|
||
private bool HasWhiteNeighbor(IntPtr scan0, int stride, int x, int y, int width, int height, int[] dx, int[] dy, int pixelSize)
|
||
{
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
int nx = x + dx[i];
|
||
int ny = y + dy[i];
|
||
|
||
if (nx >= 0 && nx < width && ny >= 0 && ny < height)
|
||
{
|
||
IntPtr address = scan0 + ny * stride + nx * pixelSize;
|
||
if (Marshal.ReadByte(address) == 255 && // B
|
||
Marshal.ReadByte(address + 1) == 255 && // G
|
||
Marshal.ReadByte(address + 2) == 255) // R
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 跟踪完整轮廓
|
||
/// </summary>
|
||
private List<Point> TraceContour(IntPtr scan0, int stride, int startX, int startY, int width, int height, bool[,] visited, int[] dx, int[] dy, int pixelSize)
|
||
{
|
||
var contour = new List<Point>();
|
||
int currentX = startX;
|
||
int currentY = startY;
|
||
int currentDir = 0; // 初始方向
|
||
|
||
do
|
||
{
|
||
contour.Add(new Point(currentX, currentY));
|
||
visited[currentX, currentY] = true;
|
||
|
||
bool found = false;
|
||
// 搜索下一个轮廓点
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
int dir = (currentDir + i + 6) % 8; // 从当前方向的左侧开始搜索
|
||
int nx = currentX + dx[dir];
|
||
int ny = currentY + dy[dir];
|
||
|
||
if (nx >= 0 && nx < width && ny >= 0 && ny < height &&
|
||
!visited[nx, ny] &&
|
||
IsBlackPixel(scan0, stride, nx, ny, pixelSize) &&
|
||
HasWhiteNeighbor(scan0, stride, nx, ny, width, height, dx, dy, pixelSize))
|
||
{
|
||
currentX = nx;
|
||
currentY = ny;
|
||
currentDir = dir;
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!found)
|
||
break;
|
||
|
||
// 防止无限循环
|
||
} while (!(currentX == startX && currentY == startY) && contour.Count < 10000);
|
||
|
||
return contour;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 简化轮廓(使用Douglas-Peucker算法)
|
||
/// </summary>
|
||
private List<List<Point>> SimplifyContours(List<List<Point>> contours, float epsilon = 1.0f)
|
||
{
|
||
var result = new List<List<Point>>();
|
||
foreach (var contour in contours)
|
||
{
|
||
var simplified = SimplifyContour(contour, epsilon);
|
||
if (simplified.Count > 3)
|
||
{
|
||
result.Add(simplified);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 简化单个轮廓
|
||
/// </summary>
|
||
private List<Point> SimplifyContour(List<Point> points, float epsilon)
|
||
{
|
||
if (points.Count <= 2)
|
||
return new List<Point>(points);
|
||
|
||
// 找到距离最大的点
|
||
float maxDist = 0;
|
||
int index = 0;
|
||
Point first = points[0];
|
||
Point last = points[points.Count - 1];
|
||
|
||
for (int i = 1; i < points.Count - 1; i++)
|
||
{
|
||
float dist = PerpendicularDistance(points[i], first, last);
|
||
if (dist > maxDist)
|
||
{
|
||
maxDist = dist;
|
||
index = i;
|
||
}
|
||
}
|
||
|
||
// 如果最大距离大于阈值,则递归简化
|
||
if (maxDist > epsilon)
|
||
{
|
||
var left = SimplifyContour(points.GetRange(0, index + 1), epsilon);
|
||
var right = SimplifyContour(points.GetRange(index, points.Count - index), epsilon);
|
||
|
||
// 合并结果,移除重复点
|
||
left.RemoveAt(left.Count - 1);
|
||
left.AddRange(right);
|
||
return left;
|
||
}
|
||
else
|
||
{
|
||
// 否则保留起点和终点
|
||
return new List<Point> { first, last };
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算点到线段的垂直距离
|
||
/// </summary>
|
||
private float PerpendicularDistance(Point p, Point a, Point b)
|
||
{
|
||
float dx = b.X - a.X;
|
||
float dy = b.Y - a.Y;
|
||
|
||
if (dx == 0 && dy == 0)
|
||
return (float)Math.Sqrt(Math.Pow(p.X - a.X, 2) + Math.Pow(p.Y - a.Y, 2));
|
||
|
||
float t = Math.Clamp(((p.X - a.X) * dx + (p.Y - a.Y) * dy) / (dx * dx + dy * dy), 0f, 1f);
|
||
float projX = a.X + t * dx;
|
||
float projY = a.Y + t * dy;
|
||
|
||
return (float)Math.Sqrt(Math.Pow(p.X - projX, 2) + Math.Pow(p.Y - projY, 2));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成与示例格式一致的SVG片段(贝塞尔曲线+统一样式+正确坐标)
|
||
/// </summary>
|
||
/// <param name="contours">轮廓集合</param>
|
||
/// <param name="color">填充颜色</param>
|
||
/// <param name="coordScale">坐标缩放系数(匹配示例量级,如10)</param>
|
||
/// <returns>SVG的<g>片段</returns>
|
||
private string ConvertContoursToSvg(List<List<Point>> contours, Color color, float coordScale = 1f)
|
||
{
|
||
if (contours.Count == 0)
|
||
return string.Empty;
|
||
|
||
var svgBuilder = new StringBuilder();
|
||
string colorHex = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
|
||
string opacity = color.A < 255 ? $"opacity=\"{color.A / 255.0:0.00}\"" : "";
|
||
|
||
// 1. 构建正确格式的<g>标签:统一样式+无stroke
|
||
svgBuilder.AppendLine($"<g {opacity} fill=\"{colorHex}\" stroke=\"none\">");
|
||
|
||
// 2. 遍历轮廓生成贝塞尔曲线路径(匹配示例的c命令)
|
||
foreach (var contour in contours)
|
||
{
|
||
if (contour.Count < 4)
|
||
continue;
|
||
|
||
// 确保轮廓闭合(起点=终点)
|
||
var closedContour = new List<Point>(contour);
|
||
if (!closedContour[0].Equals(closedContour.Last()))
|
||
{
|
||
closedContour.Add(closedContour[0]);
|
||
}
|
||
|
||
// 3. 生成路径数据(无"f"后缀+贝塞尔曲线c命令)
|
||
svgBuilder.Append(" <path d=\"");
|
||
// 起点:M命令(坐标缩放至示例量级,空格分隔)
|
||
Point start = closedContour[0];
|
||
svgBuilder.Append($"M{(int)(start.X * coordScale)} {(int)(start.Y * coordScale)}");
|
||
|
||
// 4. 用贝塞尔曲线c命令替代直线L命令(平滑路径,匹配示例)
|
||
for (int i = 1; i < closedContour.Count - 1; i++)
|
||
{
|
||
Point curr = closedContour[i];
|
||
Point next = closedContour[i + 1];
|
||
Point prev = closedContour[Math.Max(0, i - 1)];
|
||
|
||
// 计算贝塞尔控制点(简化拟合,确保平滑)
|
||
int c1X = (int)((prev.X + curr.X) * coordScale / 2);
|
||
int c1Y = (int)((prev.Y + curr.Y) * coordScale / 2);
|
||
int c2X = (int)((curr.X + next.X) * coordScale / 2);
|
||
int c2Y = (int)((curr.Y + next.Y) * coordScale / 2);
|
||
int endX = (int)(next.X * coordScale);
|
||
int endY = (int)(next.Y * coordScale);
|
||
|
||
// 贝塞尔曲线命令:c 控制点1X,控制点1Y 控制点2X,控制点2Y 终点X,终点Y
|
||
svgBuilder.Append($" c{c1X},{c1Y} {c2X},{c2Y} {endX},{endY}");
|
||
|
||
// 可选:每5个点换行(匹配示例的换行格式,提升可读性)
|
||
if (i % 5 == 0)
|
||
{
|
||
svgBuilder.AppendLine();
|
||
svgBuilder.Append(" "); // 缩进对齐
|
||
}
|
||
}
|
||
|
||
// 闭合路径:Z命令
|
||
svgBuilder.AppendLine(" Z\" />");
|
||
}
|
||
|
||
// 闭合<g>标签
|
||
svgBuilder.AppendLine("</g>");
|
||
return svgBuilder.ToString();
|
||
}
|
||
|
||
public static string ExtractSvgPath(string rawSvg, byte alpha)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(rawSvg)) return string.Empty;
|
||
|
||
// 匹配<g>标签属性
|
||
var gTagRegex = new Regex(@"<g\s+([^>]*?)>", RegexOptions.IgnoreCase);
|
||
var gTagMatch = gTagRegex.Match(rawSvg);
|
||
string originalGAttributes = gTagMatch.Success ? gTagMatch.Groups[1].Value : "";
|
||
|
||
// 提取所有<path>
|
||
var pathRegex = new Regex(@"<path\s+[^>]*?/>", 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))
|
||
{
|
||
attributes.RemoveAll(a => a.StartsWith("opacity=", StringComparison.OrdinalIgnoreCase));
|
||
attributes.Add(opacityAttribute);
|
||
}
|
||
|
||
// 构建最终<g>标签
|
||
var result = new StringBuilder();
|
||
result.Append($"<g {string.Join(" ", attributes)}>");
|
||
foreach (Match match in matches)
|
||
result.AppendLine(match.Value);
|
||
result.Append("</g>");
|
||
|
||
return result.ToString();
|
||
}
|
||
#endregion
|
||
}
|
||
}
|