XPrintServer/XPrint.Image/Tools/VectorizationTool.cs
2025-11-16 19:33:01 +08:00

472 lines
18 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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的&lt;g&gt;片段</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
}
}