Skip to content

Commit 26f46be

Browse files
asolntsevclaude
andcommitted
#1784 add minutiae to fingerprint generator for more realistic output
Each generated print now contains 18-36 random ridge endings and bifurcations on top of the existing loop / whorl / arch base pattern. A minutia is a Gaussian phase bump of peak height ridgePeriod/2, which shifts the local ridge phase by half a period and produces a clean ridge ending (+) or bifurcation (-) without introducing branch-cut artefacts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e06566c commit 26f46be

2 files changed

Lines changed: 45 additions & 6 deletions

File tree

src/main/java/net/datafaker/providers/base/Fingerprint.java

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ private BufferedImage render(int width, int height, PatternType patternType) {
8484
double ridgePeriod = 11.0 + faker.random().nextDouble(-2.0, 3.0);
8585
double ridgeFraction = 0.42 + faker.random().nextDouble(-0.04, 0.06);
8686

87-
// Pattern-specific parameters
87+
// Global pattern parameters (drive the overall shape – spiral / loop / arch)
8888
double spiralFactor = 0.5 + faker.random().nextDouble(0.0, 0.5);
8989
double loopAmplitude = height * (0.18 + faker.random().nextDouble(0.0, 0.10));
9090
double loopSigma = Math.min(width, height) * (0.28 + faker.random().nextDouble(0.0, 0.12));
@@ -107,6 +107,26 @@ private BufferedImage render(int width, int height, PatternType patternType) {
107107
double maskB = height * 0.46;
108108
double fadeZone = 0.13; // fraction of mask radius used for edge fade
109109

110+
// Minutiae: random ridge endings (+) and bifurcations (−) scattered
111+
// across the print. Each is a Gaussian phase bump with peak height
112+
// ridgePeriod/2, which produces a half-period local shift — exactly
113+
// the topological change a real ending or bifurcation creates.
114+
int numMinutiae = 18 + faker.random().nextInt(0, 18);
115+
double minutiaSigma = ridgePeriod * 0.85;
116+
double twoSigmaMinSq = 2 * minutiaSigma * minutiaSigma;
117+
double minutiaCutoffSq = 9 * minutiaSigma * minutiaSigma; // ≈3σ
118+
double[][] minutiae = new double[numMinutiae][3];
119+
for (int i = 0; i < numMinutiae; i++) {
120+
double rx, ry;
121+
do {
122+
rx = faker.random().nextDouble(-0.40, 0.40);
123+
ry = faker.random().nextDouble(-0.42, 0.42);
124+
} while (rx * rx / 0.16 + ry * ry / 0.18 > 0.85); // stay within mask
125+
minutiae[i][0] = width * 0.5 + rx * width;
126+
minutiae[i][1] = height * 0.5 + ry * height;
127+
minutiae[i][2] = (faker.random().nextBoolean() ? 1.0 : -1.0) * ridgePeriod * 0.5;
128+
}
129+
110130
// Ink tones
111131
int ridgeGray = faker.random().nextInt(25, 65);
112132
int valleyGray = faker.random().nextInt(195, 235);
@@ -122,15 +142,17 @@ private BufferedImage render(int width, int height, PatternType patternType) {
122142
noiseY += noiseAmp[i] * Math.sin(y * f + x * f * 0.6 + phasesY[i]);
123143
}
124144

125-
double dx = x + noiseX - cx;
126-
double dy = y + noiseY - cy;
145+
double px = x + noiseX;
146+
double py = y + noiseY;
147+
double dx = px - cx;
148+
double dy = py - cy;
127149
double r = Math.sqrt(dx * dx + dy * dy);
128150

129-
// Ridge value – periodic over ridgePeriod
151+
// Global ridge value – periodic over ridgePeriod
130152
double ridgeVal = switch (patternType) {
131153
case WHORL -> {
132154
// Spiral: angle controls how much the ridges rotate
133-
double theta = Math.atan2(dy, dx); // −PI … PI
155+
double theta = Math.atan2(dy, dx);
134156
yield r + spiralFactor * ridgePeriod * theta / Math.PI;
135157
}
136158
case LOOP -> {
@@ -146,6 +168,18 @@ private BufferedImage render(int width, int height, PatternType patternType) {
146168
}
147169
};
148170

171+
// Minutiae: localised half-period phase bumps create ridge
172+
// endings and bifurcations, the small features that give real
173+
// fingerprints their characteristic broken-up appearance.
174+
for (double[] m : minutiae) {
175+
double dxm = px - m[0];
176+
double dym = py - m[1];
177+
double rm2 = dxm * dxm + dym * dym;
178+
if (rm2 < minutiaCutoffSq) {
179+
ridgeVal += m[2] * Math.exp(-rm2 / twoSigmaMinSq);
180+
}
181+
}
182+
149183
double mod = ((ridgeVal % ridgePeriod) + ridgePeriod) % ridgePeriod;
150184
boolean isRidge = mod < ridgePeriod * ridgeFraction;
151185

src/test/java/net/datafaker/providers/base/FingerprintTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import org.junit.jupiter.api.Test;
55
import org.junit.jupiter.params.ParameterizedTest;
66
import org.junit.jupiter.params.provider.EnumSource;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
79

810
import javax.imageio.ImageIO;
911
import java.awt.image.BufferedImage;
@@ -16,6 +18,7 @@
1618
import static org.assertj.core.api.Assertions.assertThat;
1719

1820
class FingerprintTest {
21+
private static final Logger log = LoggerFactory.getLogger(FingerprintTest.class);
1922

2023
private final Faker faker = new Faker();
2124

@@ -76,7 +79,9 @@ void eachPatternTypeProducesValidPng(Fingerprint.PatternType pattern) throws IOE
7679

7780
File folder = new File("target/surefire-reports");
7881
folder.mkdirs();
79-
ImageIO.write(image, "png", new File(folder, "fingerprint-" + pattern + ".png"));
82+
File output = new File(folder, "fingerprint-" + pattern + ".png");
83+
ImageIO.write(image, "png", output);
84+
log.info("Generated fingerprint for {} pattern: {}", pattern, output.getAbsolutePath());
8085
}
8186

8287
@Test

0 commit comments

Comments
 (0)