/** * Evaluates the direction of an arc as being either CW or CCW */ private SweepDirection getDirection(QDInputPointSet pointSet) { float angleDiffSum = 0f; if (pointSet.pts.Count >= 2) { for (int i = 1; i < pointSet.anglesD.Count; i++) { float diff = QDUtils.QDUtils.angleDiffMinorD(pointSet.anglesD[i - 1], pointSet.anglesD[i]); if (Math.Abs(diff) < 30.0f) // Reject significant pt-to-pt angle changes as outliers { angleDiffSum += diff; } } if (angleDiffSum > 0) { return(SweepDirection.Counterclockwise); } else { return(SweepDirection.Clockwise); } } else { return(SweepDirection.Clockwise); } }
/** * Finds the beginning angle of an elliptical arc given its direction, searching points near * the first drawn point in case of small backtracks */ private float findArcEndD(SweepDirection direction, QDInputPointSet ptSet, QDEllipse ellipse) { float finishAngle = QDUtils.QDUtils.getPtToPtAngleD(ellipse.mCentre, ptSet.pts.Last()); if (direction == SweepDirection.Clockwise) { // Check for any points near the first point more CCW than the current to set as start for (int i = ptSet.pts.Count - 2; i > Math.Floor(ptSet.pts.Count * 0.75f); i--) { float thisAngle = QDUtils.QDUtils.getPtToPtAngleD(ellipse.mCentre, ptSet.pts[i]); if (QDUtils.QDUtils.angleDiffMinorD(finishAngle, thisAngle) < 0f) { finishAngle = thisAngle; } } } else if (direction == SweepDirection.Counterclockwise) { // CCW arc for (int i = ptSet.pts.Count - 2; i > Math.Floor(ptSet.pts.Count * 0.75f); i--) { float thisAngle = QDUtils.QDUtils.getPtToPtAngleD(ellipse.mCentre, ptSet.pts[i]); if (QDUtils.QDUtils.angleDiffMinorD(finishAngle, thisAngle) > 0f) { finishAngle = thisAngle; } } } return(finishAngle); }
/** * Finds the final angle of an elliptical arc given its direction, searching points near * the last drawn point in case of small backtracks */ private float findArcStartD(SweepDirection direction, QDInputPointSet ptSet, QDEllipse ellipse) { float startAngle = QDUtils.QDUtils.getPtToPtAngleD(ellipse.mCentre, ptSet.pts[0]); if (direction == SweepDirection.Clockwise) { // Check for any points near the first point more CCW than the current to set as start for (int i = 1; i < Math.Ceiling(ptSet.pts.Count * 0.25f); i++) { float thisAngle = QDUtils.QDUtils.getPtToPtAngleD(ellipse.mCentre, ptSet.pts[i]); if (QDUtils.QDUtils.angleDiffMinorD(startAngle, thisAngle) > 0f) { startAngle = thisAngle; } } } else if (direction == SweepDirection.Counterclockwise) { // CCW arc for (int i = 1; i < Math.Ceiling(ptSet.pts.Count * 0.25f); i++) { float thisAngle = QDUtils.QDUtils.getPtToPtAngleD(ellipse.mCentre, ptSet.pts[i]); if (QDUtils.QDUtils.angleDiffMinorD(startAngle, thisAngle) < 0f) { startAngle = thisAngle; } } } return(startAngle); }
// Returns the list of other shapes modified which need redrawing public List <QDShape> analyse(QDInputPointSet ptSet) { List <QDShape> modifiedShapes = new List <QDShape>(); getIntrinsicConstraints(ptSet); modifiedShapes = getExtrinsicConstraints(ptSet); return(modifiedShapes); }
// Copy constructor public QDInputPointSet(QDInputPointSet other) { this.pts = other.pts; this.anglesD = other.anglesD; this.meanSmoothedAnglesD = other.meanSmoothedAnglesD; this.start = other.start; //this.finish = other.finish; this.angle_max = other.angle_max; this.angle_min = other.angle_min; this.angle_sum = other.angle_sum; }
public void getIntrinsicConstraints(QDInputPointSet ptSet) { if (ptSet.shapeType == QDShapeTypes.LINE) { QDLine line = (QDLine)ptSet.initialFit; float angle = line.angleD; // Check for vertical line and adjust by midpoint pivot if ((90.0f - line_snap_thresh < angle && angle < 90.0f + line_snap_thresh) || (-90.0f - line_snap_thresh < angle && angle < -90.0f + line_snap_thresh) && !line.vertical) { QDPoint newStart, newFinish; if (line.start.y > line.getMidpoint().y) { newStart = new QDPoint(line.midPoint.x, line.midPoint.y + (0.5f * line.length)); newFinish = new QDPoint(line.midPoint.x, line.midPoint.y - (0.5f * line.length)); } else { newStart = new QDPoint(line.midPoint.x, line.midPoint.y - (0.5f * line.length)); newFinish = new QDPoint(line.midPoint.x, line.midPoint.y + (0.5f * line.length)); } ptSet.fittedShape = new QDLine(newStart, newFinish); ptSet.constraints.Add(QDConstraintTypes.VERTICAL_LINE); } // Check for horizontal line and adjust by midpoint pivot else if ((-line_snap_thresh < angle && angle < line_snap_thresh) || (180.0f - line_snap_thresh < angle || angle < -180.0f + line_snap_thresh)) { float length = (float)Math.Sqrt(Math.Pow(line.start.x - line.finish.x, 2.0f) + Math.Pow(line.start.y - line.finish.y, 2.0f)); QDPoint midPt = new QDPoint((line.start.x + line.finish.x) * 0.5f, (line.start.y + line.finish.y) * 0.5f); QDPoint newStart = new QDPoint(); QDPoint newFinish = new QDPoint(); newStart.y = newFinish.y = midPt.y; if (line.start.x > midPt.x) { newStart.x = midPt.x + (0.5f * length); newFinish.x = midPt.x - (0.5f * length); } else { newStart.x = midPt.x - (0.5f * length); newFinish.x = midPt.x + (0.5f * length); } ptSet.fittedShape = new QDLine(newStart, newFinish); ptSet.constraints.Add(QDConstraintTypes.HORIZONTAL_LINE); } } }
/** * Find angle swept by points around the centre of an elliptical arc. Used to determine * Whether it should be treated as a full ellipse or an arc */ private float findSweptArc(QDInputPointSet ptSet, QDEllipse ellipse) { QDPoint centre = ellipse.mCentre; List <QDPoint> pts = ptSet.pts; float lastAngle = QDUtils.QDUtils.getPtToPtAngleD(pts[0], centre); float angleSweep = 0f; foreach (QDPoint pt in pts) { float angle = QDUtils.QDUtils.getPtToPtAngleD(pt, centre); angleSweep += QDUtils.QDUtils.angleDiffMinorD(angle, lastAngle); lastAngle = angle; } return(angleSweep); }
// Add a point to the segment list and check for a corner in the segment // Returns true if a corner was detected, false otherwise public bool addPoint(QDPoint pt) { // Special case for the first point if (pts.Count == 0) { start = pt; pts.Add(pt); sampledMarkerPt = pt; return(false); } // Get the pt to pt gradient float angle = QDUtils.QDUtils.getPtToPtAngleD(pts.Last(), pt); // Store summary statistics if (angle > angle_max) { angle_max = angle; } if (angle < angle_min) { angle_min = angle; } angle_sum += angle; // Only stores the angles between points sufficiently far apart. This is the most // noise robust, and useful for getting the general form of the input shape if (QDUtils.QDUtils.getPtToPtDist(sampledMarkerPt, pt) > SUBSAMP_PT_DIST) { sampledPtAnglesD.Add(QDUtils.QDUtils.getPtToPtAngleD(sampledMarkerPt, pt)); sampledMarkerPt = pt; } // Add values to vectors anglesD.Add(angle); pts.Add(pt); // Step 1: track back to get testPt float TEST_RADIUS = 50.0f; float CNR_RADIUS = 10.0f; QDPoint testPt = null, cnrCandidatePt = null, currPt = pt; int midIdx = -1, testIdx = -1, cnrCandidateIdx = -1; midIdx = backtrackByDist(pts, pts.Count - 2, TEST_RADIUS / 2.0f); if (midIdx < 0) { return(false); } testIdx = backtrackByDist(pts, midIdx, TEST_RADIUS / 2.0f); if (testIdx < 0) { return(false); } testPt = pts[testIdx]; // Step 2: construct line from testPt to currPt QDInfiniteLine testLine = new QDInfiniteLine(testPt, currPt); // Step 3: Find perp dist from each pt between test and curr to the line float maxDist = 0.0f; int maxIdx = -1; for (int i = testIdx + 1; i < pts.Count - 1; i++) { float thisDist = testLine.getDistToPoint(pts[i]); if (thisDist > maxDist) { maxDist = thisDist; maxIdx = i; } } // Step 4: confirm cnrCandidatePt if (maxIdx < 0) { return(false); } cnrCandidatePt = pts[maxIdx]; int cnrIdx = maxIdx; if (!(QDUtils.QDUtils.getPtToPtDist(cnrCandidatePt, currPt) > CNR_RADIUS)) { return(false); } // Step 5: construct lines float oldAngleD = new QDLine(testPt, cnrCandidatePt).angleD; float newAngleD = new QDLine(cnrCandidatePt, currPt).angleD; // Step 6: test angle between lines if (Math.Abs(QDUtils.QDUtils.angleDiffMinorD(oldAngleD, newAngleD)) > CNR_ANGLE_THRESH) { // Step 7: test for continuous curvature int strTestIdx = backtrackByDist(pts, testIdx, CNR_RADIUS); if (strTestIdx < 0) { return(false); } int strTest2Idx = backtrackByDist(pts, strTestIdx, CNR_RADIUS); if (strTest2Idx < 0) { return(false); } float seg1 = new QDLine(pts[strTest2Idx], pts[strTestIdx]).angleD; float seg2 = new QDLine(pts[strTestIdx], pts[testIdx]).angleD; if (Math.Abs(QDUtils.QDUtils.angleDiffMinorD(seg1, seg2)) < 30.0f) { cornerTerminated = true; } else { cornerTerminated = false; } } if (cornerTerminated) { newSeg = new QDInputPointSet(CORNER_WINDOW, CNR_ANGLE_THRESH); newSeg.addPoint(cnrCandidatePt); // Add the corner point int lastIdx = pts.Count - 1; for (int i = 0; i < lastIdx - cnrIdx; i++) // For how many points there are after the corner { newSeg.addPoint(this.pts[cnrIdx + 1]); // Copy points after the corner into new seg this.pts.RemoveAt(cnrIdx + 1); // Delete points after corner from current set this.anglesD.Remove(cnrIdx); // There is one less angle than pts since first two pts make 1 angle } // Remove subsampled angles as well up to before the corner int deleteSubSamp = (int)Math.Ceiling(CNR_RADIUS / SUBSAMP_PT_DIST); for (int i = 0; i < deleteSubSamp; i++) { sampledPtAnglesD.Remove(sampledPtAnglesD.Count - deleteSubSamp + i); } return(true); } return(false); }
public List <QDShape> getExtrinsicConstraints(QDInputPointSet ptSet) { List <QDShape> modifiedShapes = new List <QDShape>(); if (ptSet.shapeType == QDShapeTypes.LINE) { QDLine line = (QDLine)ptSet.fittedShape; List <QDShapeDBPoint> nearStartPts = shapeDB.getShapesNearPoint(line.start, SEARCH_RADIUS); foreach (QDShapeDBPoint pt in nearStartPts) { if (pt.type == QDPointTypes.LINE_START || pt.type == QDPointTypes.LINE_FINISH) { QDLine nearLine = (QDLine)pt.shape; if (!modifiedShapes.Contains(nearLine)) { modifiedShapes.Add(nearLine); } QDInfiniteLine thisInf = new QDInfiniteLine(line.start, line.finish); QDInfiniteLine nearInf = new QDInfiniteLine(nearLine.start, nearLine.finish); // Check for nearly parallel lines if (Math.Abs(QDUtils.QDUtils.angleDiffMinorD(line.angleD, nearLine.angleD)) > 10f) { QDPoint intersectPt = thisInf.intersect(nearInf); if (QDUtils.QDUtils.getPtToPtDist(line.start, intersectPt) < SEARCH_RADIUS * 1.25) { line.start = intersectPt; if (pt.type == QDPointTypes.LINE_START) { nearLine.start = intersectPt; } else { nearLine.finish = intersectPt; } } } } } /** * GOTTA BE A BETTER WAY OF DOING THIS RATHER THAN DOING IT ONCE FOR START AND ONCE FOR FINISH */ List <QDShapeDBPoint> nearFinishPts = shapeDB.getShapesNearPoint(line.finish, SEARCH_RADIUS); foreach (QDShapeDBPoint pt in nearFinishPts) { if (pt.type == QDPointTypes.LINE_START || pt.type == QDPointTypes.LINE_FINISH) { QDLine nearLine = (QDLine)pt.shape; if (!modifiedShapes.Contains(nearLine)) { modifiedShapes.Add(nearLine); } QDInfiniteLine thisInf = new QDInfiniteLine(line.start, line.finish); QDInfiniteLine nearInf = new QDInfiniteLine(nearLine.start, nearLine.finish); // Check for nearly parallel lines if (Math.Abs(QDUtils.QDUtils.angleDiffMinorD(line.angleD, nearLine.angleD)) > 10f) { QDPoint intersectPt = thisInf.intersect(nearInf); if (QDUtils.QDUtils.getPtToPtDist(line.finish, intersectPt) < SEARCH_RADIUS * 1.25) { line.finish = intersectPt; if (pt.type == QDPointTypes.LINE_START) { nearLine.start = intersectPt; } else { nearLine.finish = intersectPt; } } } } } } return(modifiedShapes); }
private QDLine fitLine(QDInputPointSet pointSet) { int n = pointSet.pts.Count - 1; float xsum = 0.0f; float ysum = 0.0f; for (int i = 0; i < n; i++) { xsum += pointSet.pts[i].x; ysum += pointSet.pts[i].y; } float xbar = xsum / n; float ybar = ysum / n; float denom, numer, gradient, diffx; denom = numer = 0.0f; for (int i = 0; i < n; i++) { diffx = pointSet.pts[i].x - xbar; numer += diffx * (pointSet.pts[i].y - ybar); denom += diffx * diffx; } QDLine line = new QDLine(); if (denom < 1e-2f) { // Check for vertical case line.vertical = true; line.start.x = line.finish.x = xbar; // x coords at the average line.start.y = pointSet.pts[0].y; // y coords are limits of drawn line line.finish.y = pointSet.pts[n].y; // Check direction if (line.finish.y > line.start.y) { line.angleD = 90.0f; } else { line.angleD = -90.0f; } } else { // Otherwise choose start and finish values as limits of line gradient = numer / denom; line.angleD = ((float)Math.Atan2(numer, denom)) * 180.0f / ((float)Math.PI); line.intercept = ybar - (gradient * xbar); if (Math.Abs(line.angleD) < 45.0f) { line.start.x = pointSet.pts[0].x; line.start.y = line.start.x * gradient + line.intercept; line.finish.x = pointSet.pts[n].x; line.finish.y = pointSet.pts[n].y; } else { line.start.y = pointSet.pts[0].y; line.start.x = (line.start.y - line.intercept) / gradient; line.finish.y = pointSet.pts[n].y; line.finish.x = (line.finish.y - line.intercept) / gradient; } } return(new QDLine(line.start, line.finish)); }
//float mFitCost = 0f; // Return whether a shape fit has been successfully applied public bool analyse(QDInputPointSet pointSet) { // Determine what type of shape the user has drawn and fit accordingly // Do this using the distance-sampled angles to get the overall shape float angleMinD = 181.0f; float anglePosMinD = 181.0f; float angleMaxD = -181.0f; float angleNegMaxD = -181.0f; if (pointSet.sampledPtAnglesD.Count == 0) { return(false); } foreach (float angle in pointSet.sampledPtAnglesD) { if (angle > angleMaxD) { angleMaxD = angle; } if (angle < angleMinD) { angleMinD = angle; } if (angle >= 0.0f && angle < anglePosMinD) { anglePosMinD = angle; } if (angle < 0.0f && angle > angleNegMaxD) { angleNegMaxD = angle; } } float angleRangeD = 0f; // Only include the positive or negative angular ranges if the curve had +ve or -ve angles if (anglePosMinD < 181.0f) { angleRangeD += (angleMaxD - anglePosMinD); } if (angleNegMaxD > -181.0f) { angleRangeD += (angleNegMaxD - angleMinD); } // A line has a similar angle throughout - the difference between max and min is small and the mean is in between if (angleRangeD < 40.0f) { pointSet.initialFit = pointSet.fittedShape = fitLine(pointSet); pointSet.shapeType = QDShapeTypes.LINE; pointSet.constraints.Add(QDConstraintTypes.STRAIGHT_LINE); if (((QDLine)pointSet.initialFit).vertical) { pointSet.constraints.Add(QDConstraintTypes.VERTICAL_LINE); } } // TODO: other options apart from fit a circle else { pointSet.initialFit = pointSet.fittedShape = ellipseFit(pointSet.pts); pointSet.shapeType = QDShapeTypes.ELLIPSE; // TODO: change the constraint type pointSet.constraints.Add(QDConstraintTypes.CIRCLE); SweepDirection direction = getDirection(pointSet); float sweptAngle = findSweptArc(pointSet, (QDEllipse)pointSet.initialFit); if (sweptAngle < 330f) { float startAngle = findArcStartD(direction, pointSet, (QDEllipse)pointSet.initialFit); float finishAngle = findArcEndD(direction, pointSet, (QDEllipse)pointSet.initialFit); pointSet.shapeType = QDShapeTypes.ELLIPTICAL_ARC; pointSet.initialFit = pointSet.fittedShape = new QDEllipticalArc((QDEllipse)pointSet.initialFit, direction, startAngle, finishAngle); } } return(true); }