简体   繁体   中英

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".

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

Here is how it can be used to compare a LineRenderer against another:

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
  • 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.
  • We could work with Vector2 instead of Vector3


Original answer:

Similar?

As @Jonathan pointed out, you need to be more precise about "similar enough" :

  • 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):

  • 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

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).

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
  • close the shape if the ends are close enough
  • check the angles are ~60deg each:

测试三角形

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!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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