rxing/qrcode/detector/finder_pattern_finder.rs
1/*
2 * Copyright 2007 ZXing authors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::ops::Div;
18
19use crate::{
20 common::{BitMatrix, Result},
21 result_point_utils, DecodeHints, Exceptions, Point, PointCallback,
22};
23
24use super::{FinderPattern, FinderPatternInfo};
25
26/**
27 * <p>This class attempts to find finder patterns in a QR Code. Finder patterns are the square
28 * markers at three corners of a QR Code.</p>
29 *
30 * <p>This class is thread-safe but not reentrant. Each thread must allocate its own object.
31 *
32 * @author Sean Owen
33 */
34pub struct FinderPatternFinder<'a> {
35 image: &'a BitMatrix,
36 possibleCenters: Vec<FinderPattern>,
37 hasSkipped: bool,
38 // crossCheckStateCount: [u32; 5],
39 resultPointCallback: Option<PointCallback>,
40}
41impl<'a> FinderPatternFinder<'_> {
42 pub const CENTER_QUORUM: usize = 2;
43 pub const MIN_SKIP: u32 = 3; // 1 pixel/module times 3 modules/center
44 pub const MAX_MODULES: u32 = 97; // support up to version 20 for mobile clients
45
46 /**
47 * <p>Creates a finder that will search the image for three finder patterns.</p>
48 *
49 * @param image image to search
50 */
51 pub fn new(image: &'a BitMatrix) -> FinderPatternFinder<'a> {
52 Self::with_callback(image, None)
53 }
54
55 pub fn with_callback(
56 image: &'a BitMatrix,
57 resultPointCallback: Option<PointCallback>,
58 ) -> FinderPatternFinder<'a> {
59 FinderPatternFinder {
60 image,
61 possibleCenters: Vec::new(),
62 hasSkipped: false,
63 // crossCheckStateCount: [0u32; 5],
64 resultPointCallback,
65 }
66 }
67
68 pub fn getImage(&self) -> &BitMatrix {
69 self.image
70 }
71
72 pub fn getPossibleCenters(&self) -> &Vec<FinderPattern> {
73 &self.possibleCenters
74 }
75
76 pub fn find(&mut self, hints: &DecodeHints) -> Result<FinderPatternInfo> {
77 let tryHarder = matches!(hints.TryHarder, Some(true));
78 let maxI = self.image.getHeight();
79 let maxJ = self.image.getWidth();
80 // We are looking for black/white/black/white/black modules in
81 // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far
82
83 // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the
84 // image, and then account for the center being 3 modules in size. This gives the smallest
85 // number of pixels the center could be, so skip this often. When trying harder, look for all
86 // QR versions regardless of how dense they are.
87 let mut iSkip = (3 * maxI) / (4 * Self::MAX_MODULES);
88 if iSkip < Self::MIN_SKIP || tryHarder {
89 iSkip = Self::MIN_SKIP;
90 }
91
92 let mut done = false;
93 let mut stateCount = [0u32; 5];
94 let mut i = iSkip as i32 - 1;
95 while i < maxI as i32 && !done {
96 // Get a row of black/white values
97 FinderPatternFinder::doClearCounts(&mut stateCount);
98 let mut currentState = 0;
99 let mut j = 0;
100 while j < maxJ {
101 if self.image.get(j, i as u32) {
102 // Black pixel
103 if (currentState & 1) == 1 {
104 // Counting white pixels
105 currentState += 1;
106 }
107 stateCount[currentState] += 1;
108 } else {
109 // White pixel
110 if (currentState & 1) == 0 {
111 // Counting black pixels
112 if currentState == 4 {
113 // A winner?
114 if FinderPatternFinder::foundPatternCross(&stateCount) {
115 // Yes
116 let confirmed = self.handlePossibleCenter(&stateCount, i as u32, j);
117 if confirmed {
118 // Start examining every other line. Checking each line turned out to be too
119 // expensive and didn't improve performance.
120 iSkip = 2;
121 if self.hasSkipped {
122 done = self.haveMultiplyConfirmedCenters();
123 } else {
124 let rowSkip = self.findRowSkip();
125 if rowSkip > stateCount[2] {
126 // Skip rows between row of lower confirmed center
127 // and top of presumed third confirmed center
128 // but back up a bit to get a full chance of detecting
129 // it, entire width of center of finder pattern
130
131 // Skip by rowSkip, but back off by stateCount[2] (size of last center
132 // of pattern we saw) to be conservative, and also back off by iSkip which
133 // is about to be re-added
134 i += rowSkip as i32
135 - stateCount[2] as i32
136 - iSkip as i32;
137 // i += rowSkip - stateCount[2] - iSkip ;
138 j = maxJ - 1;
139 }
140 }
141 } else {
142 FinderPatternFinder::doShiftCounts2(&mut stateCount);
143 currentState = 3;
144 j += 1;
145 continue;
146 }
147 // Clear state to start looking again
148 currentState = 0;
149 FinderPatternFinder::doClearCounts(&mut stateCount);
150 } else {
151 // No, shift counts back by two
152 FinderPatternFinder::doShiftCounts2(&mut stateCount);
153 currentState = 3;
154 }
155 } else {
156 currentState += 1;
157 stateCount[currentState] += 1;
158 }
159 } else {
160 // Counting white pixels
161 stateCount[currentState] += 1;
162 }
163 }
164 j += 1;
165 }
166 if FinderPatternFinder::foundPatternCross(&stateCount) {
167 let confirmed = self.handlePossibleCenter(&stateCount, i as u32, maxJ);
168 if confirmed {
169 iSkip = stateCount[0];
170 if self.hasSkipped {
171 // Found a third one
172 done = self.haveMultiplyConfirmedCenters();
173 }
174 }
175 }
176
177 i += iSkip as i32;
178 }
179
180 let mut patternInfo = self.selectBestPatterns()?;
181 result_point_utils::orderBestPatterns(&mut patternInfo);
182
183 Ok(FinderPatternInfo::new(patternInfo))
184 }
185
186 /**
187 * Given a count of black/white/black/white/black pixels just seen and an end position,
188 * figures the location of the center of this run.
189 */
190 fn centerFromEnd(stateCount: &[u32], end: u32) -> f32 {
191 (end - stateCount[4] - stateCount[3]) as f32 - ((stateCount[2] as f32) / 2.0)
192 }
193
194 /**
195 * @param stateCount count of black/white/black/white/black pixels just read
196 * @return true iff the proportions of the counts is close enough to the 1/1/3/1/1 ratios
197 * used by finder patterns to be considered a match
198 */
199 pub fn foundPatternCross(stateCount: &[u32]) -> bool {
200 let mut totalModuleSize = 0;
201 for count in stateCount.iter().take(5) {
202 if *count == 0 {
203 return false;
204 }
205 totalModuleSize += *count;
206 }
207 if totalModuleSize < 7 {
208 return false;
209 }
210 let moduleSize = totalModuleSize as f64 / 7.0;
211 let maxVariance = moduleSize / 2.0;
212 // Allow less than 50% variance from 1-1-3-1-1 proportions
213 ((moduleSize - stateCount[0] as f64).abs()) < maxVariance
214 && ((moduleSize - stateCount[1] as f64).abs()) < maxVariance
215 && ((3.0 * moduleSize - stateCount[2] as f64).abs()) < 3.0 * maxVariance
216 && (moduleSize - stateCount[3] as f64).abs() < maxVariance
217 && (moduleSize - stateCount[4] as f64).abs() < maxVariance
218 }
219
220 /**
221 * @param stateCount count of black/white/black/white/black pixels just read
222 * @return true iff the proportions of the counts is close enough to the 1/1/3/1/1 ratios
223 * used by finder patterns to be considered a match
224 */
225 pub fn foundPatternDiagonal(stateCount: &[u32]) -> bool {
226 let mut totalModuleSize = 0;
227 for count in stateCount.iter().take(5) {
228 if *count == 0 {
229 return false;
230 }
231 totalModuleSize += *count;
232 }
233 if totalModuleSize < 7 {
234 return false;
235 }
236 let moduleSize = totalModuleSize as f64 / 7.0;
237 let maxVariance = moduleSize / 1.333;
238 // Allow less than 75% variance from 1-1-3-1-1 proportions
239 (moduleSize - stateCount[0] as f64).abs() < maxVariance
240 && (moduleSize - stateCount[1] as f64).abs() < maxVariance
241 && (3.0 * moduleSize - stateCount[2] as f64).abs() < 3.0 * maxVariance
242 && (moduleSize - stateCount[3] as f64).abs() < maxVariance
243 && (moduleSize - stateCount[4] as f64).abs() < maxVariance
244 }
245
246 #[deprecated]
247 pub fn clearCounts(&self, counts: &mut [u32; 5]) {
248 Self::doClearCounts(counts);
249 }
250
251 #[deprecated]
252 pub fn shiftCounts2(&self, stateCount: &mut [u32; 5]) {
253 Self::doShiftCounts2(stateCount);
254 }
255
256 pub fn doClearCounts(counts: &mut [u32; 5]) {
257 counts.fill(0)
258 }
259
260 pub fn doShiftCounts2(stateCount: &mut [u32]) {
261 stateCount[0] = stateCount[2];
262 stateCount[1] = stateCount[3];
263 stateCount[2] = stateCount[4];
264 stateCount[3] = 1;
265 stateCount[4] = 0;
266 }
267
268 /**
269 * After a vertical and horizontal scan finds a potential finder pattern, this method
270 * "cross-cross-cross-checks" by scanning down diagonally through the center of the possible
271 * finder pattern to see if the same proportion is detected.
272 *
273 * @param centerI row where a finder pattern was detected
274 * @param centerJ center of the section that appears to cross a finder pattern
275 * @return true if proportions are withing expected limits
276 */
277 fn crossCheckDiagonal(&self, centerI: u32, centerJ: u32) -> bool {
278 let mut crossCheckStateCount = [0u32; 5];
279
280 // Start counting up, left from center finding black center mass
281 let mut i = 0;
282 while centerI >= i && centerJ >= i && self.image.get(centerJ - i, centerI - i) {
283 crossCheckStateCount[2] += 1;
284 i += 1;
285 }
286 if crossCheckStateCount[2] == 0 {
287 return false;
288 }
289
290 // Continue up, left finding white space
291 while centerI >= i && centerJ >= i && !self.image.get(centerJ - i, centerI - i) {
292 crossCheckStateCount[1] += 1;
293 i += 1;
294 }
295 if crossCheckStateCount[1] == 0 {
296 return false;
297 }
298
299 // Continue up, left finding black border
300 while centerI >= i && centerJ >= i && self.image.get(centerJ - i, centerI - i) {
301 crossCheckStateCount[0] += 1;
302 i += 1;
303 }
304 if crossCheckStateCount[0] == 0 {
305 return false;
306 }
307
308 let maxI = self.image.getHeight();
309 let maxJ = self.image.getWidth();
310
311 // Now also count down, right from center
312 i = 1;
313 while centerI + i < maxI && centerJ + i < maxJ && self.image.get(centerJ + i, centerI + i) {
314 crossCheckStateCount[2] += 1;
315 i += 1;
316 }
317
318 while centerI + i < maxI && centerJ + i < maxJ && !self.image.get(centerJ + i, centerI + i)
319 {
320 crossCheckStateCount[3] += 1;
321 i += 1;
322 }
323 if crossCheckStateCount[3] == 0 {
324 return false;
325 }
326
327 while centerI + i < maxI && centerJ + i < maxJ && self.image.get(centerJ + i, centerI + i) {
328 crossCheckStateCount[4] += 1;
329 i += 1;
330 }
331 if crossCheckStateCount[4] == 0 {
332 return false;
333 }
334
335 Self::foundPatternDiagonal(&crossCheckStateCount)
336 }
337
338 /**
339 * <p>After a horizontal scan finds a potential finder pattern, this method
340 * "cross-checks" by scanning down vertically through the center of the possible
341 * finder pattern to see if the same proportion is detected.</p>
342 *
343 * @param startI row where a finder pattern was detected
344 * @param centerJ center of the section that appears to cross a finder pattern
345 * @param maxCount maximum reasonable number of modules that should be
346 * observed in any reading state, based on the results of the horizontal scan
347 * @return vertical center of finder pattern, or {@link Float#NaN} if not found
348 */
349 fn crossCheckVertical(
350 &self,
351 startI: u32,
352 centerJ: u32,
353 maxCount: u32,
354 originalStateCountTotal: u32,
355 ) -> f32 {
356 let maxI = self.image.getHeight() as i32;
357 let mut crossCheckStateCount = [0u32; 5];
358
359 // Start counting up from center
360 let mut i = startI as i32;
361 while i >= 0 && self.image.get(centerJ, i as u32) {
362 crossCheckStateCount[2] += 1;
363 i -= 1;
364 }
365 if i < 0 {
366 return f32::NAN;
367 }
368 while i >= 0 && !self.image.get(centerJ, i as u32) && crossCheckStateCount[1] <= maxCount {
369 crossCheckStateCount[1] += 1;
370 i -= 1;
371 }
372 // If already too many modules in this state or ran off the edge:
373 if i < 0 || crossCheckStateCount[1] > maxCount {
374 return f32::NAN;
375 }
376 while i >= 0 && self.image.get(centerJ, i as u32) && crossCheckStateCount[0] <= maxCount {
377 crossCheckStateCount[0] += 1;
378 i -= 1;
379 }
380 if crossCheckStateCount[0] > maxCount {
381 return f32::NAN;
382 }
383
384 // Now also count down from center
385 i = startI as i32 + 1;
386 while i < maxI && self.image.get(centerJ, i as u32) {
387 crossCheckStateCount[2] += 1;
388 i += 1;
389 }
390 if i == maxI {
391 return f32::NAN;
392 }
393 while i < maxI && !self.image.get(centerJ, i as u32) && crossCheckStateCount[3] < maxCount {
394 crossCheckStateCount[3] += 1;
395 i += 1;
396 }
397 if i == maxI || crossCheckStateCount[3] >= maxCount {
398 return f32::NAN;
399 }
400 while i < maxI && self.image.get(centerJ, i as u32) && crossCheckStateCount[4] < maxCount {
401 crossCheckStateCount[4] += 1;
402 i += 1;
403 }
404 if crossCheckStateCount[4] >= maxCount {
405 return f32::NAN;
406 }
407
408 // If we found a finder-pattern-like section, but its size is more than 40% different than
409 // the original, assume it's a false positive
410 let stateCountTotal = crossCheckStateCount.iter().sum::<u32>();
411
412 if 5 * (stateCountTotal as i64 - originalStateCountTotal as i64)
413 >= 2 * originalStateCountTotal as i64
414 {
415 return f32::NAN;
416 }
417
418 if Self::foundPatternCross(&crossCheckStateCount) {
419 Self::centerFromEnd(&crossCheckStateCount, i as u32)
420 } else {
421 f32::NAN
422 }
423 }
424
425 /**
426 * <p>Like {@link #crossCheckVertical(int, int, int, int)}, and in fact is basically identical,
427 * except it reads horizontally instead of vertically. This is used to cross-cross
428 * check a vertical cross check and locate the real center of the alignment pattern.</p>
429 */
430 fn crossCheckHorizontal(
431 &self,
432 startJ: u32,
433 centerI: u32,
434 maxCount: u32,
435 originalStateCountTotal: u32,
436 ) -> f32 {
437 let maxJ = self.image.getWidth();
438 let mut crossCheckStateCount = [0u32; 5];
439
440 let mut j = startJ as i32;
441 while j >= 0 && self.image.get(j as u32, centerI) {
442 crossCheckStateCount[2] += 1;
443 j -= 1;
444 }
445 if j < 0 {
446 return f32::NAN;
447 }
448
449 while j >= 0 && !self.image.get(j as u32, centerI) && crossCheckStateCount[1] <= maxCount {
450 crossCheckStateCount[1] += 1;
451 j -= 1;
452 }
453 if j < 0 || crossCheckStateCount[1] > maxCount {
454 return f32::NAN;
455 }
456
457 while j >= 0 && self.image.get(j as u32, centerI) && crossCheckStateCount[0] <= maxCount {
458 crossCheckStateCount[0] += 1;
459 j -= 1;
460 }
461 if crossCheckStateCount[0] > maxCount {
462 return f32::NAN;
463 }
464
465 j = startJ as i32 + 1;
466 while j < (maxJ as i32) && self.image.get(j as u32, centerI) {
467 crossCheckStateCount[2] += 1;
468 j += 1;
469 }
470 if j == maxJ as i32 {
471 return f32::NAN;
472 }
473
474 while j < maxJ as i32
475 && !self.image.get(j as u32, centerI)
476 && crossCheckStateCount[3] < maxCount
477 {
478 crossCheckStateCount[3] += 1;
479 j += 1;
480 }
481 if j == (maxJ as i32) || crossCheckStateCount[3] >= maxCount {
482 return f32::NAN;
483 }
484
485 while j < (maxJ as i32)
486 && self.image.get(j as u32, centerI)
487 && crossCheckStateCount[4] < maxCount
488 {
489 crossCheckStateCount[4] += 1;
490 j += 1;
491 }
492 if crossCheckStateCount[4] >= maxCount {
493 return f32::NAN;
494 }
495
496 // If we found a finder-pattern-like section, but its size is significantly different than
497 // the original, assume it's a false positive
498 let stateCountTotal = crossCheckStateCount.iter().sum::<u32>();
499
500 if 5 * (stateCountTotal as i64 - originalStateCountTotal as i64)
501 >= originalStateCountTotal as i64
502 {
503 return f32::NAN;
504 }
505
506 if Self::foundPatternCross(&crossCheckStateCount) {
507 Self::centerFromEnd(&crossCheckStateCount, j as u32)
508 } else {
509 f32::NAN
510 }
511 }
512
513 /**
514 * @param stateCount reading state module counts from horizontal scan
515 * @param i row where finder pattern may be found
516 * @param j end of possible finder pattern in row
517 * @param pureBarcode ignored
518 * @return true if a finder pattern candidate was found this time
519 * @deprecated only exists for backwards compatibility
520 * @see #handlePossibleCenter(int[], int, int)
521 */
522 #[deprecated]
523 pub fn handlePossibleCenterWithPureBarcodeFlag(
524 &mut self,
525 stateCount: &[u32],
526 i: u32,
527 j: u32,
528 _pureBarcode: bool,
529 ) -> bool {
530 self.handlePossibleCenter(stateCount, i, j)
531 }
532
533 /**
534 * <p>This is called when a horizontal scan finds a possible alignment pattern. It will
535 * cross check with a vertical scan, and if successful, will, ah, cross-cross-check
536 * with another horizontal scan. This is needed primarily to locate the real horizontal
537 * center of the pattern in cases of extreme skew.
538 * And then we cross-cross-cross check with another diagonal scan.</p>
539 *
540 * <p>If that succeeds the finder pattern location is added to a list that tracks
541 * the number of times each location has been nearly-matched as a finder pattern.
542 * Each additional find is more evidence that the location is in fact a finder
543 * pattern center
544 *
545 * @param stateCount reading state module counts from horizontal scan
546 * @param i row where finder pattern may be found
547 * @param j end of possible finder pattern in row
548 * @return true if a finder pattern candidate was found this time
549 */
550 pub fn handlePossibleCenter(&mut self, stateCount: &[u32], i: u32, j: u32) -> bool {
551 let stateCountTotal =
552 stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4];
553 let mut centerJ = Self::centerFromEnd(stateCount, j);
554 let centerI =
555 self.crossCheckVertical(i, centerJ.floor() as u32, stateCount[2], stateCountTotal);
556 if !centerI.is_nan() {
557 // Re-cross check
558 centerJ = self.crossCheckHorizontal(
559 centerJ.floor() as u32,
560 centerI.floor() as u32,
561 stateCount[2],
562 stateCountTotal,
563 );
564 if !centerJ.is_nan()
565 && self.crossCheckDiagonal(centerI.floor() as u32, centerJ.floor() as u32)
566 {
567 let estimatedModuleSize = stateCountTotal as f32 / 7.0;
568 let mut found = false;
569 for center in self.possibleCenters.iter_mut() {
570 // Look for about the same center and module size:
571 if center.aboutEquals(estimatedModuleSize, centerI, centerJ) {
572 *center = center.combineEstimate(centerI, centerJ, estimatedModuleSize);
573 found = true;
574 break;
575 }
576 }
577 if !found {
578 let point = FinderPattern::new(centerJ, centerI, estimatedModuleSize);
579 self.possibleCenters.push(point);
580 if let Some(rpc) = self.resultPointCallback.clone() {
581 rpc((&point).into());
582 }
583 }
584 return true;
585 }
586 }
587 false
588 }
589
590 /**
591 * @return number of rows we could safely skip during scanning, based on the first
592 * two finder patterns that have been located. In some cases their position will
593 * allow us to infer that the third pattern must lie below a certain point farther
594 * down in the image.
595 */
596 fn findRowSkip(&mut self) -> u32 {
597 let max = self.possibleCenters.len();
598 if max <= 1 {
599 return 0;
600 }
601 let mut firstConfirmedCenter: Option<&FinderPattern> = None;
602 for center in &self.possibleCenters {
603 if center.getCount() >= Self::CENTER_QUORUM {
604 if let Some(fnp) = firstConfirmedCenter {
605 // We have two confirmed centers
606 // How far down can we skip before resuming looking for the next
607 // pattern? In the worst case, only the difference between the
608 // difference in the x / y coordinates of the two centers.
609 // This is the case where you find top left last.
610 self.hasSkipped = true;
611
612 return (Point::from(fnp) - Point::from(center))
613 .abs()
614 .fold(|x, y| x - y)
615 .div(2.0)
616 .floor() as u32;
617 } else {
618 firstConfirmedCenter.replace(center);
619 }
620 }
621 }
622 0
623 }
624
625 /**
626 * @return true iff we have found at least 3 finder patterns that have been detected
627 * at least {@link #CENTER_QUORUM} times each, and, the estimated module size of the
628 * candidates is "pretty similar"
629 */
630 fn haveMultiplyConfirmedCenters(&self) -> bool {
631 let mut confirmedCount = 0;
632 let mut totalModuleSize = 0.0;
633 let max = self.possibleCenters.len();
634 for pattern in &self.possibleCenters {
635 if pattern.getCount() >= Self::CENTER_QUORUM {
636 confirmedCount += 1;
637 totalModuleSize += pattern.getEstimatedModuleSize();
638 }
639 }
640 if confirmedCount < 3 {
641 return false;
642 }
643 // OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive"
644 // and that we need to keep looking. We detect this by asking if the estimated module sizes
645 // vary too much. We arbitrarily say that when the total deviation from average exceeds
646 // 5% of the total module size estimates, it's too much.
647 let average = totalModuleSize / max as f32;
648 let totalDeviation = self.possibleCenters.iter().fold(0.0, |acc, pattern| {
649 acc + (pattern.getEstimatedModuleSize() - average).abs()
650 });
651
652 totalDeviation <= 0.05 * totalModuleSize
653 }
654
655 /**
656 * Get square of distance between a and b.
657 */
658 fn squaredDistance(a: &FinderPattern, b: &FinderPattern) -> f64 {
659 Point::from(a).squaredDistance(Point::from(b)) as f64
660 }
661
662 /**
663 * @return the 3 best {@link FinderPattern}s from our list of candidates. The "best" are
664 * those have similar module size and form a shape closer to a isosceles right triangle.
665 * @throws NotFoundException if 3 such finder patterns do not exist
666 */
667 fn selectBestPatterns(&mut self) -> Result<[FinderPattern; 3]> {
668 let startSize = self.possibleCenters.len();
669 if startSize < 3 {
670 // Couldn't find enough finder patterns
671 return Err(Exceptions::NOT_FOUND);
672 }
673
674 self.possibleCenters
675 .retain(|fp| fp.getCount() >= Self::CENTER_QUORUM);
676
677 self.possibleCenters.sort_unstable_by(|x, y| {
678 x.getEstimatedModuleSize()
679 .partial_cmp(&y.getEstimatedModuleSize())
680 .unwrap_or(std::cmp::Ordering::Less) // we are making a weird assumption that uncomparable items are result in Less
681 });
682
683 let mut distortion = f64::MAX;
684 let mut bestPatterns = [None; 3];
685
686 for i in 0..self.possibleCenters.len() {
687 let Some(fpi) = self.possibleCenters.get(i) else {
688 return Err(Exceptions::NOT_FOUND);
689 };
690 let minModuleSize = fpi.getEstimatedModuleSize();
691
692 for j in (i + 1)..(self.possibleCenters.len() - 1) {
693 let Some(fpj) = self.possibleCenters.get(j) else {
694 return Err(Exceptions::NOT_FOUND);
695 };
696 let squares0 = Self::squaredDistance(fpi, fpj);
697
698 for k in (j + 1)..(self.possibleCenters.len()) {
699 let Some(fpk) = self.possibleCenters.get(k) else {
700 return Err(Exceptions::NOT_FOUND);
701 };
702 let maxModuleSize = fpk.getEstimatedModuleSize();
703 if maxModuleSize > minModuleSize * 1.4 {
704 // module size is not similar
705 continue;
706 }
707
708 let mut a = squares0;
709 let mut b = Self::squaredDistance(fpj, fpk);
710 let mut c = Self::squaredDistance(fpi, fpk);
711
712 // sorts ascending - inlined
713 if a < b {
714 if b > c {
715 if a < c {
716 std::mem::swap(&mut b, &mut c)
717 } else {
718 let temp = a;
719 a = c;
720 c = b;
721 b = temp;
722 }
723 }
724 } else if b < c {
725 if a < c {
726 std::mem::swap(&mut a, &mut b)
727 } else {
728 let temp = a;
729 a = b;
730 b = c;
731 c = temp;
732 }
733 } else {
734 std::mem::swap(&mut a, &mut c);
735 }
736
737 // a^2 + b^2 = c^2 (Pythagorean theorem), and a = b (isosceles triangle).
738 // Since any right triangle satisfies the formula c^2 - b^2 - a^2 = 0,
739 // we need to check both two equal sides separately.
740 // The value of |c^2 - 2 * b^2| + |c^2 - 2 * a^2| increases as dissimilarity
741 // from isosceles right triangle.
742 let d = (c - 2.0 * b).abs() + (c - 2.0 * a).abs();
743 if d < distortion {
744 distortion = d;
745 bestPatterns = [Some(*fpi), Some(*fpj), Some(*fpk)];
746 }
747 }
748 }
749 }
750
751 if distortion == f64::MAX {
752 return Err(Exceptions::NOT_FOUND);
753 }
754
755 if bestPatterns[0].is_none() {
756 return Err(Exceptions::NOT_FOUND);
757 }
758
759 let p1 = bestPatterns[0].ok_or(Exceptions::NOT_FOUND)?;
760 let p2 = bestPatterns[1].ok_or(Exceptions::NOT_FOUND)?;
761 let p3 = bestPatterns[2].ok_or(Exceptions::NOT_FOUND)?;
762
763 Ok([p1, p2, p3])
764 }
765}