Untitled
unknown
kotlin
4 years ago
9.1 kB
8
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...