基于 SkiaSharp 的轮廓获取

Fork Me On Github
zodream 编程技术 C# 11月22日

基于 SkiaSharp 的轮廓获取

源代码

示例

事前须知

本文基于透明图片的透明度获取轮廓;如不透明图片获取轮廓需要先把图片转成灰度图片,根据灰度值获取轮廓。

原理

  1. 需要获取一个物体的起始点,通常是从左至右逐行遍历像素点,获取第一个不透明点
  2. 获取相邻的下一个不透明点,通常是以当前点为中心,顺时针遍历八个方向的点,找到的一个个点就是下一个点。
  3. 初始点第一个方向为正上个方,下一个点的第一个方向就应该是相对与上一个点的方向顺时针转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 
123456789

代码

c#
                                                         
/// <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;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657

升级版:获取外轮廓,允许点的

c#
                                                                                                                                                                                
/// <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;
    }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
点击查看全文
0 26 0