@@ -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
0 commit comments