【问题标题】:How to compare between two lines?如何比较两条线?
【发布时间】:2019-08-01 00:26:08
【问题描述】:

我有一个代码可以让我画线并限制可以画的线数。

我的问题是我想创建一条线(例如线渲染器) 然后让用户尝试画一条相似(不一定完全相同)的线,代码需要根据设置知道线是否足够相似,但我想不通。

如果有任何提示,我将不胜感激。

public class DrawLine : MonoBehaviour
{
    public GameObject linePrefab;
    public GameObject currentLine;

    public LineRenderer lineRenderer;
    public EdgeCollider2D edgeCollider;
    public List<Vector2> fingerPositions;

    public Button[] answers;
    public bool isCurrButtonActive;

    int mouseButtonState = 0;

    void Update()
    {
        Debug.Log(rfgrhe);
        if (isCurrButtonActive)
        {
            if (Input.GetMouseButtonDown(0))
            {
                if (mouseButtonState == 0)
                {
                    CreateLine();
                }
            }
            if (Input.GetMouseButtonUp(0))
            {
                mouseButtonState++;
            }
            if (Input.GetMouseButton(0))
            {
                if (mouseButtonState == 1)
                {
                    Debug.Log(Input.mousePosition.ToString());
                    if (Input.mousePosition.x < 100 || Input.mousePosition.y > 420 || Input.mousePosition.x > 660 || Input.mousePosition.y < 7)
                    {
                        return;
                    }
                    Vector2 tempFingerPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
                    if (Vector2.Distance(tempFingerPos, fingerPositions[fingerPositions.Count - 1]) > .1f)
                    {
                        UpdateLine(tempFingerPos);
                    }
                }
            }
        }
    }

    void CreateLine()
    {
        mouseButtonState++;
        currentLine = Instantiate(linePrefab, Vector3.zero, Quaternion.identity);
        lineRenderer = currentLine.GetComponent<LineRenderer>();
        edgeCollider = currentLine.GetComponent<EdgeCollider2D>();
        fingerPositions.Clear();
        fingerPositions.Add(Camera.main.ScreenToWorldPoint(Input.mousePosition));
        fingerPositions.Add(Camera.main.ScreenToWorldPoint(Input.mousePosition));
        lineRenderer.SetPosition(0, fingerPositions[0]);
        lineRenderer.SetPosition(1, fingerPositions[1]);
        edgeCollider.points = fingerPositions.ToArray();
    }

    void UpdateLine(Vector2 newFingerPos)
    {
        fingerPositions.Add(newFingerPos);
        lineRenderer.positionCount++;
        lineRenderer.SetPosition(lineRenderer.positionCount - 1, newFingerPos);
        edgeCollider.points = fingerPositions.ToArray();
    }

    public void ActivateCurrentButton()
    {
        // Debug.Log(isCurrButtonActive);
        isCurrButtonActive = true;
        for (int i = 0; i < answers.Length; i++)
        {
            if (answers[i].CompareTag("onePoint"))
            {
                answers[i].GetComponent<MapLvl>().isCurrButtonActive = false;
            }
            else if (answers[i].CompareTag("TwoPoints"))
            {
                answers[i].GetComponent<DrawLine>().isCurrButtonActive = false;
            }
        }
    }
}

例如,在这种情况下,蓝线是正确的,绿线和红线是用户回答的两个选项。 我想要的是程序只会将绿线确认为正确答案。

【问题讨论】:

  • 我认为您必须弄清楚在您的应用程序上下文中“足够相似”对您意味着什么。也许拿你的源线,缓冲 20 像素,如果输入线完全在缓冲区内并穿过一些控制点,那很好;或者用一些算法简化你的源代码行并对其进行哈希处理,然后以类似的方式简化输入行并比较哈希值。您将不得不决定一种方法并尝试它,然后才能获得有关堆栈溢出的有意义的帮助
  • 您能否详细说明一下,您想比较什么样的“线条”?你的意思是数学二维线?在这种情况下,您可以标准化您的线条并获得点积 - 它会为您提供“垂直度”的度量。但是,当您实际上是指“曲线”时,这并不容易。

标签: c# unity3d


【解决方案1】:

编辑:既然现在我们想要什么更清楚了,这里有一种实现它的方法:

下面的函数float DifferenceBetweenLines(Vector3[], Vector3[]) 可以测量“两条线之间的距离”。

它沿着线行走以匹配最大步长,并为每个点计算到画线上最近点的距离。 它将这些距离的平方相加,然后将它们除以要匹配的 线的长度(不要要求我用数学严谨性来解释这一点)。

返回值越小,第一行与第二行越接近——阈值由你决定。

float DifferenceBetweenLines(Vector3[] drawn, Vector3[] toMatch) {
    float sqrDistAcc = 0f;
    float length = 0f;

    Vector3 prevPoint = toMatch[0];

    foreach (var toMatchPoint in WalkAlongLine(toMatch)) {
        sqrDistAcc += SqrDistanceToLine(drawn, toMatchPoint);
        length += Vector3.Distance(toMatchPoint, prevPoint);

        prevPoint = toMatchPoint;
    }

    return sqrDistAcc / length;
}

/// <summary>
/// Move a point from the beginning of the line to its end using a maximum step, yielding the point at each step.
/// </summary>
IEnumerable<Vector3> WalkAlongLine(IEnumerable<Vector3> line, float maxStep = .1f) {
    using (var lineEnum = line.GetEnumerator()) {
        if (!lineEnum.MoveNext())
            yield break;

        var pos = lineEnum.Current;

        while (lineEnum.MoveNext()) {
            Debug.Log(lineEnum.Current);
            var target = lineEnum.Current;
            while (pos != target) {
                yield return pos = Vector3.MoveTowards(pos, target, maxStep);
            }
        }
    }
}

static float SqrDistanceToLine(Vector3[] line, Vector3 point) {
    return ListSegments(line)
        .Select(seg => SqrDistanceToSegment(seg.a, seg.b, point))
        .Min();
}

static float SqrDistanceToSegment(Vector3 linePoint1, Vector3 linePoint2, Vector3 point) {
    var projected = ProjectPointOnLineSegment(linePoint1, linePoint1, point);
    return (projected - point).sqrMagnitude;
}

/// <summary>
/// Outputs each position of the line (but the last) and the consecutive one wrapped in a Segment.
/// Example: a, b, c, d --> (a, b), (b, c), (c, d)
/// </summary>
static IEnumerable<Segment> ListSegments(IEnumerable<Vector3> line) {
    using (var pt1 = line.GetEnumerator())
    using (var pt2 = line.GetEnumerator()) {
        pt2.MoveNext();

        while (pt2.MoveNext()) {
            pt1.MoveNext();

            yield return new Segment { a = pt1.Current, b = pt2.Current };
        }
    }
}
struct Segment {
    public Vector3 a;
    public Vector3 b;
}

//This function finds out on which side of a line segment the point is located.
//The point is assumed to be on a line created by linePoint1 and linePoint2. If the point is not on
//the line segment, project it on the line using ProjectPointOnLine() first.
//Returns 0 if point is on the line segment.
//Returns 1 if point is outside of the line segment and located on the side of linePoint1.
//Returns 2 if point is outside of the line segment and located on the side of linePoint2.
static int PointOnWhichSideOfLineSegment(Vector3 linePoint1, Vector3 linePoint2, Vector3 point){
    Vector3 lineVec = linePoint2 - linePoint1;
    Vector3 pointVec = point - linePoint1;

    if (Vector3.Dot(pointVec, lineVec) > 0) {
        return pointVec.magnitude <= lineVec.magnitude ? 0 : 2;
    } else {
        return 1;
    }
}

//This function returns a point which is a projection from a point to a line.
//The line is regarded infinite. If the line is finite, use ProjectPointOnLineSegment() instead.
static Vector3 ProjectPointOnLine(Vector3 linePoint, Vector3 lineVec, Vector3 point){
    //get vector from point on line to point in space
    Vector3 linePointToPoint = point - linePoint;
    float t = Vector3.Dot(linePointToPoint, lineVec);
    return linePoint + lineVec * t;
}

//This function returns a point which is a projection from a point to a line segment.
//If the projected point lies outside of the line segment, the projected point will
//be clamped to the appropriate line edge.
//If the line is infinite instead of a segment, use ProjectPointOnLine() instead.
static Vector3 ProjectPointOnLineSegment(Vector3 linePoint1, Vector3 linePoint2, Vector3 point){
    Vector3 vector = linePoint2 - linePoint1;
    Vector3 projectedPoint = ProjectPointOnLine(linePoint1, vector.normalized, point);

    switch (PointOnWhichSideOfLineSegment(linePoint1, linePoint2, projectedPoint)) {
        case 0:
            return projectedPoint;
        case 1:
            return linePoint1;
        case 2:
            return linePoint2;
        default:
            //output is invalid
            return Vector3.zero;
    }
}

最后的数学函数来自3d Math Functions - Unify Community Wiki

以下是如何将 LineRenderer 与另一个进行比较:

Array.Resize(ref lineBuffer1, lineRenderer1.positionCount);
Array.Resize(ref lineBuffer2, lineRenderer2.positionCount);

lineRenderer1.GetPositions(lineBuffer1);
lineRenderer2.GetPositions(lineBuffer2);

float diff = DifferenceBetweenLines(lineBuffer1, lineBuffer2);
const float threshold = 5f;

Debug.Log(diff < threshold ? "Pretty close!" : "Not that close...");

需要考虑的几点:

  • SqrDistanceToLine 的性能肯定可以提高
  • 您可以衡量第一行与第二行的接近程度,而不是相反 - 也就是说,第一行可以更长,也可以在中途散步,只要它来了回到正轨并足够紧密地“覆盖”另一条线。您可以通过再次调用DifferenceBetweenLines 来解决此问题,交换参数,然后取两者中最大的一个。
  • 我们可以使用 Vector2 而不是 Vector3


原答案:

类似的?

正如@Jonathan 指出的,您需要更准确地了解“足够相似”

  • 大小相似是否重要?
  • 方向重要吗?
  • 比例上的相似性是否重要(或仅与线条的“方向变化”有关)?
  • ...

您可能猜到了,这些标准越少,就越难;因为 您的相似性概念将从原始位置变得越来越抽象 你首先得到了。

  • 例如,如果用户需要画一个十字,正好有两笔, 覆盖或多或少定义的区域,任务就这么简单:

    您可以测量该区域的角与每个笔划的第一个之间的距离 最后一点,检查线条是否笔直。

  • 如果您想检查用户是否在任何方向上绘制了完美的心形, 它明显更棘手......

    您可能不得不为此求助于专门的图书馆。

要考虑的另一件事是,用户真的需要制作一条与另一条相似的线吗? 或者是否应该只足够接近才能与其他可能的线条区分开来? 考虑这个例子:

用户需要画一个十字(X)或一个圆(O):

  • 如果只有一个笔划回到起点附近,假设一个圆圈。
  • 如果存在大体方向正交的笔画,则假设为十字形。

在这种情况下,一个涉及更多的系统可能会有点矫枉过正。

一些“原始指针”

假设简单的要求(因为假设相反,我将无能为力), 这里有一些元素:

完全匹配

用户必须在可见线上方绘图:这是最简单的方案。

对于他的线的每个点,找出与参考线上最近点的距离。 对这些距离的平方求和——由于某种原因,它比对距离求和更好 自己,而且直接计算平方距离也更便宜。

LineRenderer.Simplify

​​>

非常具体到您的用例,因为您使用的是 Unity 的 LineRenderer, 值得知道的是,它包含一个 Simplify(float) 方法,可以减少 曲线的分辨率,使其更易于处理,并且特别有效 如果要匹配的线是由一些直线段(不是复杂的曲线)组成的。

使用角度

有时您需要检查线的不同子段之间的角度, 而不是它们的(相对)长度。它将测量方向的变化,无论 的比例,可以更直观。

示例

检测等边三角形的简单示例:

  • LineRenderer.Simplify
  • 如果两端足够接近,则关闭形状
  • 检查每个角度约为 60 度:

对于任意线,您可以运行该线以通过与用户绘制的线相同的“过滤器”进行匹配,并比较这些值。您可以决定哪些属性最重要(角度/距离/比例...),以及阈值是多少。

【讨论】:

  • 首先感谢您非常详细的回答。为了澄清我需要的是比较两条线,屏幕上的形状,距离和位置将接近正确的线。我不需要检查颜色或形状或任何化妆品参数。预期的线条非常简单......提前致谢
  • 我在说明中添加了一张图片,仅供参考
  • 我明白了!您是否阅读了我的答案的“完全匹配”部分?这对您有意义还是需要更多扩展或工作代码 sn-ps?
  • 再次感谢大家的帮助!但是我仍然无法理解您的真正意思,如果可以的话,查看一个进行这种比较的代码示例会很有帮助。再次非常感谢你
  • @liel7200 我已经用一个可行的解决方案编辑了我的答案
【解决方案2】:

我个人会沿着用户线取点,然后计算线上的角度,如果平均角度在特定范围内,那么它是可以接受的。如果您绘制角度的点足够靠近,那么您应该非常准确地了解用户是否靠近同一条线。

另外,如果线需要位于特定区域,那么您只需检查并确保线在“控制”线的指定距离内。一旦你有了分数,这些的数学应该很简单。我相信还有很多其他方法可以实现这一点,但我个人会这样做。希望这会有所帮助!

【讨论】:

    猜你喜欢
    • 2019-05-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-14
    • 1970-01-01
    • 1970-01-01
    • 2020-06-07
    相关资源
    最近更新 更多