曲线(笔迹)简化算法

曲线(笔迹)简化算法

场景是这样的,在一端进行书写,另一端还原书写的笔迹。要实现笔迹的还原,就得记录笔迹经过的轨迹点,当然这些都是可以在书写过程获取到的。但是问题在于,笔迹中的轨迹点比较多,数据量比较大,不便于传输。因此,我们需要通过某种算法,剔除某些不重要的点,从而减少数据量。

下面的代码是在 .NET Framework 框架下编写的,引用了 System.Windows.Media 命名空间中的一些类型。大多是数学算法,可以轻松地转换为其它平台的代码。

径向距离算法

径向距离算法 非常简单,如果笔迹中的相邻两点的距离小于设定的阈值,则舍去其中一个点。首先,以笔迹中的第一个点为基准点,计算第二个点与第一个点之间的距离,如果此距离小于给定的阈值,则舍去第二个点;如果此距离大于给定的阈值,则保留第二个点,并以其为新的基准点。然后再拿第三个点来与基准点判断距离,如此向后循环。

public override PointCollection Filter(PointCollection points)
{
    if (points.Count < 2)
    {
        return new PointCollection(points);
    }
    else
    {
        var previousPoint = points[0];
        var newPoints = new PointCollection() { previousPoint };

        // Tolerance 是容差值,用距离的平方来判断相邻点的距离
        var toleranceSquared = Tolerance * Tolerance;
        for (var i = 1; i < points.Count; i++)
        {
            var point = points[i];
            if ((point - previousPoint).LengthSquared < toleranceSquared)
            {
                newPoints.Add(point);
                previousPoint = point;
            }
        }

        // 确保最后一点在过滤后的点集中,这样还原效果会更佳
        if (newPoints.Last() != points.Last())
        {
            newPoints.Add(points.Last());
        }

        return newPoints;
    }
}public override PointCollection Filter(PointCollection points)
{
    if (points.Count < 2)
    {
        return new PointCollection(points);
    }
    else
    {
        var previousPoint = points[0];
        var newPoints = new PointCollection() { previousPoint };

        // Tolerance 是容差值,用距离的平方来判断相邻点的距离
        var toleranceSquared = Tolerance * Tolerance;
        for (var i = 1; i < points.Count; i++)
        {
            var point = points[i];
            if ((point - previousPoint).LengthSquared < toleranceSquared)
            {
                newPoints.Add(point);
                previousPoint = point;
            }
        }

        // 确保最后一点在过滤后的点集中,这样还原效果会更佳
        if (newPoints.Last() != points.Last())
        {
            newPoints.Add(points.Last());
        }

        return newPoints;
    }
}

曲率变化算法

曲率变化法即判断每个点处的曲率变化是否较大,如果大过了阈值,则认为此点为关键点,需要保留。假设要判断 P2 点处的曲率,则取其前后两个相邻点 P1P3,并构建向量 P1P2P2P3 ,然后计算这两个向量间的夹角。如果夹角大于指定的阈值,则认为 P2 点的曲率较大。

为了后续讨论的方便,我们暂且称 P1P2P2 点的 前置向量P2P3P2 点的后置向量。

如果第 P2 点被保留了,那么继续向后判断 P3 点的曲率情况,分别取 P2P3P3P4P3 点的 前置向量后置向量,然后向上面那样判断,如是循环。

但是,如果 P2 点被剔除了,那么我们在判断 P3 点的曲率时应该如何取 前置向量 呢?此处有三种取法:P1P2P1P3P2P3,无论哪种取法,P3 的后置向量都是 P3P4。这三种取法各有利弊,前两者效果较好,后面一种处理平滑曲线的能力较弱。

以上三种 前置向量 取法的代码差不多,下面给大家展示一下 1-3-4 曲率变化算法的代码:

public override PointCollection Filter(PointCollection points)
{
    if (points.Count < 3)
    {
        return new PointCollection(points);
    }
    else
    {
        var point1 = points[0];
        var newPoints = new PointCollection() {point1};

        for (int i = 1; i < points.Count - 2; i++)
        {
            var point2 = points[i];
            var point3 = points[i + 1];

            var vector1 = point2 - point1;
            var vector2 = point3 - point2;

            if (IsAngleTolerable(vector1, vector2))
            {
                // 不是特征拐点,舍去之
            }
            else
            {
                newPoints.Add(point2);
                point1 = point2;
            }
        }

        newPoints.Add(points.Last());
        return newPoints;
    }
}

protected bool IsAngleTolerable(Vector vector1, Vector vector2)
{
    return Math.Abs(Vector.AngleBetween(vector2, vector1)) < Tolerance;
}

道格拉斯-普克算法

最后给大家介绍 道格拉斯-普克 提出的一种迭代适应点算法,它将曲线近似表示为一系列点,并减少点的数量。算法的基本思路是:对每一条曲线的首末点虚连一条直线,求所有点与直线的距离,并找出最大距离值 dmax ,用 dmax 与阈值 D 相比。若 dmax < D ,这条曲线上的中间点全部舍去;若 dmax ≥ D ,则保留 dmax 对应的坐标点,并以该点为界,把曲线分为两部分,对这两部分循环使用该方法。

整个过程如下动图所示:

public override PointCollection Filter(PointCollection points)
{
    if (points.Count < 2)
    {
        return new PointCollection(points);
    }
    else
    {
        int? first = 0;
        int? last = points.Count - 1;

        var markers = new bool[points.Count];
        markers[first.Value] = markers[last.Value] = true;

        var index = 0;
        var stack = new Stack<int>();

        var newPoints = new PointCollection();
        while (last.HasValue && first.HasValue)
        {
            var maxDistance = 0.0d;
            for (var i = first.Value + 1; i < last.Value; i++)
            {
                var distance = GetPointToSegmentDistance(points[i], points[first.Value], points[last.Value]);
                if (distance > maxDistance)
                {
                    index = i;
                    maxDistance = distance;
                }
            }

            if (maxDistance > Tolerance * Tolerance)
            {
                markers[index] = true;

                stack.Push(first.Value);
                stack.Push(index);
                stack.Push(index);
                stack.Push(last.Value);
            }

            if (stack.Count > 0)
            {
                last = stack.Pop();
            }
            else
            {
                last = null;
            }

            if (stack.Count > 0)
            {
                first = stack.Pop();
            }
            else
            {
                first = null;
            }
        }

        for (var i = 0; i < markers.Length; i++)
        {
            if (markers[i])
            {
                newPoints.Add(points[i]);
            }
        }

        return newPoints;
    }
}

/// <summary>
/// 计算点到线段的距离。
/// </summary>
/// <param name="p">需要计算距离的点</param>
/// <param name="p1">线段的起点</param>
/// <param name="p2">线段的终点</param>
/// <returns></returns>
private static double GetPointToSegmentDistance(Point p, Point p1, Point p2)
{
    // 根据向量叉积的定义来推算点到线段的距离
    // (a cross b) = |a||b|sin(θ) => |a|sin(θ) = (a cross b)/|b|

    var a = p - p1;
    var b = p2 - p1;

    if (b.LengthSquared < double.Epsilon)
    {
        return a.LengthSquared;
    }

    var distance = Vector.CrossProduct(a, b) / b.Length;
    return Math.Pow(distance, 2);
}

算法对比

对比三种算法来看,道格拉斯-普克法 的时间复杂度和空间复杂度要明显高于其它两种,但是它的效果较好,剔除率和剔除后的符合度均较高。在 曲率变化算法 算法中,从 前置向量 的效果来看, 1-3-4 > 1-2-4 > 2-3-4 。在我们的项目应用中,如果对时间没较大要求,可以先进行 径向直径 算法,然后再使用 曲线变化率算法道格拉斯-普克法 中的一种。

最后,为了尽可能地减少数据量,可以将阈值设大一些,在还原时可以采用贝塞尔算法来进行拟合,可参考:一种简单的贝塞尔拟合算法

参考资料

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ironyho

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值