简体   繁体   English

如何比较两条线?

[英]How to compare between two lines?

I have a code that allows me to draw lines and limit the number of lines that can be drawn.我有一个代码可以让我画线并限制可以画的线数。

My problem is that I want to create a line (with for example line renderer) and then allow the user to try drawing a similar (not necessarily exactly the same) line and the code needs to know according to the setting if the line is similar enough or not, but I can't figure it.我的问题是我想创建一条线(例如使用线渲染器),然后允许用户尝试绘制一条相似(不一定完全相同)的线,并且代码需要根据设置知道该线是否相似够不够,但我想不通。

I would appreciate any tips.我将不胜感激任何提示。

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;
            }
        }
    }
}

在此处输入图片说明

For example in that case, the blue line is the correct one, the green and the red ones are two options of an answer from the user.例如,在这种情况下,蓝线是正确的,绿线和红线是用户回答的两个选项。 What I want is that the program will acknolage only the green line as a correct answer.我想要的是该程序只会将绿线确认为正确答案。

EDIT: Since it's clearer now what we want, here's a way to achieve it:编辑:因为现在我们想要的更清楚了,这里有一种实现它的方法:

The function float DifferenceBetweenLines(Vector3[], Vector3[]) below gives you a measure of the "distance between the two lines".下面的函数float DifferenceBetweenLines(Vector3[], Vector3[])为您提供了“两条线之间的距离”的度量。

It walks along the line to match with a maximum step length, and for each point, computes the distance from the closest point on the draw line .它沿着线行走以匹配最大步长,并为每个点计算与绘制线上最近点的距离。 It sums the squares of those distances and divide them by the length of the line to match (don't ask me to explain this with mathematical rigor).它将这些距离的平方相加并将它们除以匹配线的长度(不要让我用数学严谨来解释这一点)。

The smaller the return value, the closer the first line matches the second -- the threshold is yours to decide.返回值越小,第一行与第二行越接近——阈值由您决定。

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;
    }
}

The math functions at the end are from 3d Math Functions - Unify Community Wiki最后的数学函数来自3d Math Functions - Unify Community Wiki

Here is how it can be used to compare a LineRenderer against another:以下是如何使用它来比较 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...");

A few things to consider:需要考虑的几点:

  • The performance of SqrDistanceToLine could definitely be improved on SqrDistanceToLine的性能肯定可以改进
  • You get a measure of how close the first line matches the second, not the other way around -- that is, the first line can be longer or go for a walk mid-way as long as it comes back on track and "covers" the other line closely enough.您可以衡量第一条线与第二条线的接近程度,而不是相反——也就是说,第一条线可以更长,或者在中途散步,只要它回到正轨并“覆盖”另一条线足够紧密。 You can solve this by calling DifferenceBetweenLines a second time, swapping the arguments, and taking the biggest result of them two.您可以通过第二次调用DifferenceBetweenLines来解决这个问题,交换参数,并取两者中最大的结果。
  • We could work with Vector2 instead of Vector3我们可以使用 Vector2 而不是 Vector3


Original answer: 原答案:

Similar?相似的?

As @Jonathan pointed out, you need to be more precise about "similar enough" :正如@Jonathan 指出的那样,您需要更精确地了解“足够相似”

  • does similarity in size matter ?大小的相似性重要吗?
  • does orientation matter ?方向重要吗?
  • do similarity in proportions matter (or only the "changes of direction" of the line) ?比例的相似性(或仅线的“方向变化”)重要吗?
  • ... ...

As you might guess, the fewer of those criteria matter, the harder it will be ;正如您可能猜到的,这些标准越少,就越难 because your concept of similarity will become more and more abstract from the raw positions you've got in the first place.因为您的相似性概念将从您最初获得的原始位置变得越来越抽象。

  • For example, if the user needs to draw a cross, with exactly two strokes, that cover more or less a defined area, the task is as easy as it gets:例如,如果用户需要绘制一个十字,恰好有两个笔划,或多或少覆盖一个定义的区域,那么任务就变得很简单:

    You can measure the distance between the area's corners and each stroke's first and last points, and check that the lines are kind of straight.您可以测量该区域的角与每个笔划的第一个和最后一个点之间的距离,并检查线条是否笔直。

  • If you want to check if the user drew a perfect heart-shape, in any orientation, it's noticeably trickier...如果您想检查用户是否在任何方向绘制了一个完美的心形,这显然更加棘手......

    You might have to resort to a specialized library for that.为此,您可能不得不求助于专门的图书馆。

Another thing to consider is, does the user really need to make a line similar to another one, or should it only be close enough that it can be differentiated from other possible lines ?另一件要考虑的事情是,用户是否真的需要将一条线与另一条线相似,还是应该足够接近以便与其他可能的线区分开来 Consider this example:考虑这个例子:

The user needs to draw either a cross (X) or a circle (O):用户需要绘制一个十字 (X) 或一个圆 (O):

  • If there is only one stroke that comes back close to the starting point, assume a circle.如果只有一个笔划回到起点附近,假设一个圆圈。
  • If there is strokes whose general directions are orthogonal, assume a cross.如果笔画的一般方向是正交的,则假设为十字。

In this case, a more involved system would probably be overkill.在这种情况下,一个更复杂的系统可能会矫枉过正。

A few "raw pointers"一些“原始指针”

Assuming simple requirements (because assuming the opposite, I wouldn't be able to help much), here are a few elements:假设简单的需求(因为假设相反,我将无能为力),这里有一些元素:

Exact match完全符合

The user has to draw on top of a visible line : this is the easiest scenario.用户必须在可见线的顶部绘制:这是最简单的场景。

For each point of his line, find out the distance from the closest point on the reference line.对于他的直线上的每个点,找出与参考线上最近点的距离。 Sum the square of those distances -- for some reason it works better than summing the distances themselves, and it's also cheaper to compute the square distance directly.对这些距离的平方求和——出于某种原因,它比对距离本身求和更有效,而且直接计算平方距离也更便宜。

LineRenderer.Simplify LineRenderer.Simplify

Very specific to your use-case, since you're using Unity's LineRenderer, it's worth knowing that it packs a Simplify(float) method, that decreases the resolution of your curve, making it easier to process, and particularly effective if the line to match is made of somewhat straight segments (not complex curves).非常特定于您的用例,因为您使用的是 Unity 的 LineRenderer,所以值得知道它包含了一个Simplify(float)方法,该方法降低了曲线的分辨率,使其更易于处理,并且如果线条为匹配由一些直线段(不是复杂的曲线)组成。

Use the angles使用角度

Sometimes you'll want to check the angles between the different sub-segments of your line, instead of their (relative) lengths.有时您需要检查线的不同子线段之间的角度,而不是它们的(相对)长度。 It will measure changes in direction regardless of the proportions, which can be more intuitive.无论比例如何,它都会测量方向的变化,这可以更直观。

Example例子

A simple example that detects equilateral triangles:检测等边三角形的简单示例:

  • LineRenderer.Simplify LineRenderer.Simplify
  • close the shape if the ends are close enough如果末端足够接近,则关闭形状
  • check the angles are ~60deg each:检查每个角度都是 ~60 度:

测试三角形

For arbitrary lines, you could run the line to match through the same "filter" as the lines the user draws, and compare the values.对于任意线,您可以运行该线以通过与用户绘制的线相同的“过滤器”进行匹配,并比较这些值。 It will be yours to decide what properties matter most (angles/distances/proportions...), and what's the threshold.您可以决定哪些属性最重要(角度/距离/比例...),以及阈值是多少。

Personally I would take points along the users line and then figure out the angles on the lines and if the average angle is within a specific range then it is acceptable.我个人会沿着用户线取点,然后找出线上的角度,如果平均角度在特定范围内,那么这是可以接受的。 If the points you draw angles from are close enough together then you should have a pretty accurate idea whether the user is close to the same line.如果您绘制角度的点足够接近,那么您应该非常准确地了解用户是否靠近同一条线。

Also, if the line needs to be in a particular area then you can just check and make sure the line is within a specified distance of the "control" line.此外,如果该线需要在特定区域内,那么您只需检查并确保该线位于“控制”线的指定距离内。 The math for these should be pretty simple once you have the points.一旦你有了积分,这些的数学就应该很简单了。 I am sure there are many other ways to implement this, but I personally would do this.我确信还有很多其他方法可以实现这一点,但我个人会这样做。 Hope this helps!希望这可以帮助!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM