Untitled
unknown
kotlin
3 years ago
9.1 kB
7
Indexable
package *.feature.signature.validation import android.graphics.Path import android.graphics.PathMeasure import ua.ideabank.obank.feature.signature.signatureview.TimedPoint import kotlin.math.acos import kotlin.math.pow import *.common.extensions.lastEvenNumber import *.feature.signature.signatureview.Signature import *.feature.signature.signatureview.TimedPoint /** * Signature Validation requirements: * 1. Signature length must be greater than [minLength] pixels. * 2. Signature must contain a corner/curve/intersection with angle less than 70 degree. */ class SignatureValidator( signature: Signature, private val maxAngle: Float, private val minLength: Int ) { private val resampledStrokes = ArrayList<ArrayList<TimedPoint>>() init { signature.strokes.forEach { stroke -> resampledStrokes.add(resample(stroke)) } } /** * Returns true if the signature is valid. */ fun isValid() = resampledStrokes.any { isStrokeValid(it) } /** * Returns true if the provided stroke is valid. */ private fun isStrokeValid(strokeA: ArrayList<TimedPoint>): Boolean { val strokeLengthInPixels = getStrokeLengthInPixels(strokeA.size) if (strokeLengthInPixels < minLength) { // Stroke too short. Stroke isn't valid. return false } return resampledStrokes.any { if (it == strokeA) { isSingleStrokeValid(strokeA) } else intersects( strokeA = strokeA, strokeB = it ) } } /** * Returns true if the provided stroke is valid. * * Logic: * 1. For each stroke point: * Take the next 20-40 points (or iterate over different sizes) to form a line T. * Take first point of lineT as A, mid point of lineT as B and the end point as C. * Calculate acute angle ∠ABC. * If this angle is less than the [maxAngle], signature is valid. */ private fun isSingleStrokeValid(strokePoints: ArrayList<TimedPoint>): Boolean { if (strokePoints.size < minLength) { // Stroke too short. Stroke isn't valid. return false } // Iterate over different segment lengths to account for small and large strokes. val minSegmentSize = minLength.lastEvenNumber() / 2 val maxSegmentSize = minLength.lastEvenNumber() * 2 (minSegmentSize until maxSegmentSize).forEach { segmentSize: Int -> strokePoints.forEachIndexed { index, _ -> val lastIndex = index + segmentSize if (lastIndex > strokePoints.indexOf(strokePoints.last())) { // Reached end of the stroke. Stroke isn't valid. Try larger segment. return@forEach } val lineT = strokePoints.slice(index..lastIndex) val pointA = lineT.first() val pointB = lineT[segmentSize / 2] val pointC = lineT.last() val angle = calculateAngle(pointA, pointB, pointC) if (angle.isNaN()) return@forEachIndexed if (angle <= maxAngle && pointB.distance(pointC) >= minLength / 2) return true } } return false } /** * Returns true if the provided [strokeA] && [strokeB] intersect each other * and are valid (angle of intersection > [maxAngle]). This is fast but not very accurate. * * Logic: * Take the first and last point of stroke A to form lineA * Take the first and last point of stroke B to form lineB * Calculate angle of intersection. */ private fun intersects( strokeA: ArrayList<TimedPoint>, strokeB: ArrayList<TimedPoint> ): Boolean { if (strokeA == strokeB || strokeA.isEmpty() || strokeB.isEmpty()) return false val lineA = Line(strokeA.first().toPointF(), strokeA.last().toPointF()) val lineB = Line(strokeB.first().toPointF(), strokeB.last().toPointF()) return lineA.intersects(lineB) } /** * Returns true if the provided [strokeA] && [strokeB] intersect each other * and are valid (angle of intersection > [maxAngle]). This is accurate but slow. * * Logic: * 1. For each stroke point: * Take the next 20-40 points to form a line A. * For each stroke point in each stroke: * Take the next 20-40 points to for a line B. * Calculate acute angle of intersection * If this angle is less than the [maxAngle], signature is valid. */ private fun intersects2( strokeA: ArrayList<TimedPoint>, strokeB: ArrayList<TimedPoint> ): Boolean { if (strokeA == strokeB || strokeA.isEmpty() || strokeB.isEmpty()) return false fun getNextSegment(stroke: ArrayList<TimedPoint>, startIndex: Int, endIndex: Int): Line { val lineVector = stroke.slice(startIndex..endIndex) return Line( pointA = lineVector.first().toPointF(), pointB = lineVector.last().toPointF() ) } // Iterate over different segment lengths to account for small and large strokes. val minSegmentSize = minLength.lastEvenNumber() val maxSegmentSize = minLength.lastEvenNumber() * 2 (minSegmentSize until maxSegmentSize).forEach { segmentSize: Int -> strokeA.forEachIndexed { startIndexA, _ -> val endIndexA = startIndexA + segmentSize if (endIndexA > strokeA.lastIndex) { // Reached end of the signature stroke. Stroke isn't valid. Try larger segment. return@forEach } val lineA = getNextSegment(strokeA, startIndexA, endIndexA) for (startIndexB in 0..strokeB.lastIndex) { val endIndexB = startIndexB + segmentSize if (endIndexB > strokeB.lastIndex) { // Reached end of the signature strokeB. Stroke isn't valid. Try larger segment. break } val lineB = getNextSegment(strokeB, startIndexB, endIndexB) if (lineA.intersects(lineB)) { val angleOfIntersection = lineA.angleOfIntersection(lineB) if (angleOfIntersection.isNaN()) continue if (angleOfIntersection in 1.0..maxAngle.toDouble()) return true } } } } return false } /** * When a signature is acquired from typical touch sensitive * computing devices, it is typically sampled with non-uniform rate. * The rate depends on the availability of computational resources at a * given time as well as the latency. Therefore, interpolation is used in * order to derive a uniformly sampled signature. * This helps to minimize the variation of signatures due to different * sampling rates. * * For the sake of simplicity, using the [android.graphics.Path] for spline * interpolation and [android.graphics.PathMeasure] to calculate the coordinates. */ private fun resample(strokePoints: ArrayList<TimedPoint>): ArrayList<TimedPoint> { val path = Path() var lastPoint: TimedPoint strokePoints.forEachIndexed { index, timedPoint -> lastPoint = timedPoint if (index == 0) { path.moveTo(timedPoint.x, timedPoint.y) return@forEachIndexed } path.quadTo( timedPoint.x, timedPoint.y, (lastPoint.x + timedPoint.x) / 2, (lastPoint.y + timedPoint.y) / 2 ) } val pathMeasure = PathMeasure(path, false) val length = pathMeasure.length.toInt() var distance = 0f val aCoordinates = FloatArray(2) val resampleStroke: ArrayList<TimedPoint> = ArrayList() repeat(length) { pathMeasure.getPosTan(distance, aCoordinates, null) resampleStroke.add(TimedPoint(aCoordinates[0], aCoordinates[1], 0)) distance += RESAMPLING_FREQUENCY } return resampleStroke } /** * Returns the stroke size in pixels (approx). Used to check stroke length. */ private fun getStrokeLengthInPixels(length: Int) = length * RESAMPLING_FREQUENCY /** * Calculates angle ∠abc given points [a], [b] and [c] */ private fun calculateAngle(a: TimedPoint, b: TimedPoint, c: TimedPoint): Double { val ab = a.distance(b) val bc = b.distance(c) val ca = c.distance(a) val x = (ab.pow(2) + bc.pow(2) - ca.pow(2)) / (2 * ab * bc) val radians = acos(x) return Math.toDegrees(radians) } companion object { private val TAG = SignatureValidator::class.java.simpleName private const val RESAMPLING_FREQUENCY = 20L } }
Editor is loading...