You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
108 lines
3.0 KiB
Dart
108 lines
3.0 KiB
Dart
import 'dart:convert';
|
|
import 'dart:math';
|
|
import 'package:image/image.dart' as img;
|
|
|
|
class FingerprintValidation {
|
|
final bool isComplete;
|
|
final double coverageRatio; // % of "ridge" pixels
|
|
final double contrast; // std dev of pixel intensity
|
|
final String reason;
|
|
|
|
FingerprintValidation({
|
|
required this.isComplete,
|
|
required this.coverageRatio,
|
|
required this.contrast,
|
|
required this.reason,
|
|
});
|
|
}
|
|
|
|
FingerprintValidation validateFingerprintBase64(
|
|
String base64Image, {
|
|
double minCoverage = 0.20, // tune per scanner DPI/size
|
|
double maxCoverage = 0.65,
|
|
double minContrast = 25.0,
|
|
double edgeCutoffThreshold = 0.45, // % of border pixels that are "ridge"
|
|
}) {
|
|
final bytes = base64Decode(base64Image);
|
|
final image = img.decodeImage(bytes);
|
|
|
|
if (image == null) {
|
|
return FingerprintValidation(
|
|
isComplete: false,
|
|
coverageRatio: 0,
|
|
contrast: 0,
|
|
reason: 'decode_failed',
|
|
);
|
|
}
|
|
|
|
final gray = img.grayscale(image);
|
|
final w = gray.width, h = gray.height;
|
|
|
|
int ridgePixels = 0;
|
|
int total = 0;
|
|
double sum = 0, sumSq = 0;
|
|
|
|
// simple Otsu-ish threshold: use mean as baseline, refine below
|
|
final List<int> intensities = [];
|
|
for (final px in gray) {
|
|
final v = img.getLuminance(px).toDouble();
|
|
intensities.add(v.toInt());
|
|
sum += v;
|
|
total++;
|
|
}
|
|
final mean = sum / total;
|
|
for (final v in intensities) {
|
|
sumSq += (v - mean) * (v - mean);
|
|
}
|
|
final contrast = sqrt(sumSq / total);
|
|
|
|
// threshold: pixels darker than mean - 0.5*std are "ridges"
|
|
final threshold = mean - (contrast * 0.5);
|
|
for (final v in intensities) {
|
|
if (v < threshold) ridgePixels++;
|
|
}
|
|
final coverage = ridgePixels / total;
|
|
|
|
// edge/border check — if ridge pattern is heavily cut off at edges,
|
|
// finger was likely partial or misplaced
|
|
int borderRidge = 0, borderTotal = 0;
|
|
for (int x = 0; x < w; x++) {
|
|
for (final y in [0, h - 1]) {
|
|
final v = img.getLuminance(gray.getPixel(x, y)).toDouble();
|
|
borderTotal++;
|
|
if (v < threshold) borderRidge++;
|
|
}
|
|
}
|
|
for (int y = 0; y < h; y++) {
|
|
for (final x in [0, w - 1]) {
|
|
final v = img.getLuminance(gray.getPixel(x, y)).toDouble();
|
|
borderTotal++;
|
|
if (v < threshold) borderRidge++;
|
|
}
|
|
}
|
|
final borderRatio = borderTotal == 0 ? 0 : borderRidge / borderTotal;
|
|
|
|
String reason = 'ok';
|
|
bool isComplete = true;
|
|
|
|
if (contrast < minContrast) {
|
|
isComplete = false;
|
|
reason = 'Press finger on the scanner with more pressure';
|
|
} else if (coverage < minCoverage) {
|
|
isComplete = false;
|
|
reason = 'Place finger fully on the scanner';
|
|
} else if (coverage > maxCoverage) {
|
|
isComplete = false;
|
|
reason = 'Place finger lightly on the scanner';
|
|
} else if (borderRatio > edgeCutoffThreshold) {
|
|
isComplete = false;
|
|
reason = 'Finger is not centered on the scanner';
|
|
}
|
|
|
|
return FingerprintValidation(
|
|
isComplete: isComplete,
|
|
coverageRatio: coverage,
|
|
contrast: contrast,
|
|
reason: reason,
|
|
);
|
|
} |