基于 SkiaSharp 的轮廓获取
事前须知
本文基于透明图片的透明度获取轮廓;如不透明图片获取轮廓需要先把图片转成灰度图片,根据灰度值获取轮廓。
原理
- 需要获取一个物体的起始点,通常是从左至右逐行遍历像素点,获取第一个不透明点
- 获取相邻的下一个不透明点,通常是以当前点为中心,顺时针遍历八个方向的点,找到的一个个点就是下一个点。
- 初始点第一个方向为正上个方,下一个点的第一个方向就应该是相对与上一个点的方向顺时针转2下。
o c o o
o o b o
o a o o
o o o
// 从 a 找到 b,方向为 1 (0 是正上方)
// 那个 b 就在 a 的 1 方向,a 就在 b 的 5方向
// 但 b 的 6 方向已经被 a 找过了, 所以 b 的起始方向就是 7
// 总结 b 在 a 的 n 方向,则 b 的起始方向为 n + 6
代码
/// <summary>
/// 边界算法
/// </summary>
/// <returns></returns>
private static SKPath? TraceContour(SKPixmap pixMap, int beginX, int beginY)
{
var path = new SKPath();
path.MoveTo(beginX, beginY);
var directItems = new int[][] {
[0, -1], [1, -1],
[1, 0],
[1, 1], [0, 1], [-1, 1],
[-1, 0], [-1, -1]
};
var beginDirect = 0;
var isBegin = false;
var curX = beginX;
var curY = beginY;
while (!isBegin)
{
var i = 0;
var direct = beginDirect;
var hasPoint = false;
while (i ++ <= directItems.Length)
{
var x = curX + directItems[direct][0];
var y = curY + directItems[direct][1];
// 判断点是否是透明像素点
if (IsTransparent(pixMap, x, y))
{
direct = (direct + 1) % directItems.Length;
continue;
}
hasPoint = true;
curX = x;
curY = y;
if (curX == beginX && curY == beginY)
{
isBegin = true;
path.Close();
}
else
{
path.LineTo(curX, curY);
}
beginDirect = (direct + 6) % directItems.Length;
break;
}
if (!hasPoint)
{
// 所有方向都没有找到下一个不透明点,表明这就是一个孤点
return null;
}
}
return path;
}
升级版:获取外轮廓,允许点的
/// <summary>
/// 物体轮廓获取
/// </summary>
public class ImageContourTrace
{
public ImageContourTrace()
{
}
public ImageContourTrace(bool isOutline)
{
IsOutline = isOutline;
}
/// <summary>
/// 外边框,即靠近物体的透明区域
/// </summary>
public bool IsOutline { get; set; }
/// <summary>
/// 是否需要获取一个点
/// </summary>
public bool IsAllowDot { get; set; }
/// <summary>
/// 获取图片上所有物体轮廓
/// </summary>
/// <param name="image"></param>
/// <returns></returns>
public async Task<SKPath[]> GetContourAsync(SKBitmap image, CancellationToken token = default)
{
using var imagePixMap = image.PeekPixels();
return await GetContourAsync(imagePixMap, token);
}
/// <summary>
/// 获取图片上所有物体轮廓
/// </summary>
/// <param name="image"></param>
/// <returns></returns>
public async Task<SKPath[]> GetContourAsync(SKImage image, CancellationToken token = default)
{
using var imagePixMap = image.PeekPixels();
return await GetContourAsync(imagePixMap, token);
}
/// <summary>
/// 获取图片上所有物体轮廓
/// </summary>
/// <param name="image"></param>
/// <returns></returns>
public Task<SKPath[]> GetContourAsync(SKPixmap pixMap, CancellationToken token = default)
{
return Task.Factory.StartNew(() => {
return GetContour(pixMap, token);
}, token);
}
/// <summary>
/// 获取所有物体的轮廓
/// </summary>
/// <param name="pixMap"></param>
/// <returns></returns>
public SKPath[] GetContour(SKPixmap pixMap, CancellationToken token = default)
{
var items = new List<SKPath>();
for (var i = 0; i < pixMap.Height; i++)
{
for (var j = 0; j < pixMap.Width; j++)
{
if (token.IsCancellationRequested)
{
return [..items];
}
if (IsTransparent(pixMap, j, i) || Contains(items, j, i))
{
continue;
}
var path = GetContour(pixMap, j, i);
if (path is null)
{
continue;
}
items.Add(path);
}
}
return [.. items];
}
/// <summary>
/// 根据坐标获取轮廓边界算法
/// </summary>
/// <param name="pixMap"></param>
/// <param name="beginX"></param>
/// <param name="beginY"></param>
/// <returns></returns>
public SKPath? GetContour(SKPixmap pixMap, int beginX, int beginY)
{
var path = new SKPath();
path.MoveTo(beginX, beginY - (IsOutline ? 1 : 0));
var directItems = new int[][] {
[0, -1], [1, -1],
[1, 0],
[1, 1], [0, 1], [-1, 1],
[-1, 0], [-1, -1]
};
var beginDirect = 0;
var isBegin = false;
var curX = beginX;
var curY = beginY;
while (!isBegin)
{
var i = 0;
var direct = beginDirect;
var hasPoint = false;
while (i++ <= directItems.Length)
{
var x = curX + directItems[direct][0];
var y = curY + directItems[direct][1];
if (IsTransparent(pixMap, x, y))
{
direct = (direct + 1) % directItems.Length;
if (IsOutline)
{
path.LineTo(x, y);
}
continue;
}
hasPoint = true;
curX = x;
curY = y;
if (curX == beginX && curY == beginY)
{
isBegin = true;
path.Close();
}
else if (!IsOutline)
{
path.LineTo(curX, curY);
}
beginDirect = (direct + 6) % directItems.Length;
break;
}
if (!hasPoint)
{
if (IsOutline)
{
path.Close();
return path;
}
// 所有方向都没有不透明点,就是一个孤点
return IsAllowDot ? path : null;
}
}
return path;
}
private static bool Contains(IEnumerable<SKPath> items, int x, int y)
{
foreach (var item in items)
{
if (item.Contains(x, y))
{
return true;
}
}
return false;
}
private static bool IsTransparent(SKPixmap pixMap, int x, int y)
{
if (x < 0 || y < 0 || x >= pixMap.Width || y >= pixMap.Height)
{
return true;
}
return pixMap.GetPixelColor(x, y).Alpha == 0;
}
}
转载请保留原文链接: https://zodream.cn/blog/id/266.html