Passed
Push — main ( b62539...4e03cd )
by smiley
01:53
created

FinderPatternFinder::crossProductZ()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 3
dl 0
loc 5
rs 10
1
<?php
2
/**
3
 * Class FinderPatternFinder
4
 *
5
 * @created      17.01.2021
6
 * @author       ZXing Authors
7
 * @author       Smiley <[email protected]>
8
 * @copyright    2021 Smiley
9
 * @license      Apache-2.0
10
 *
11
 * @phan-file-suppress PhanTypePossiblyInvalidDimOffset
12
 */
13
14
namespace chillerlan\QRCode\Detector;
15
16
use RuntimeException;
17
use chillerlan\QRCode\Decoder\BitMatrix;
18
use function abs, count, usort;
19
use const PHP_FLOAT_MAX;
20
21
/**
22
 * <p>This class attempts to find finder patterns in a QR Code. Finder patterns are the square
23
 * markers at three corners of a QR Code.</p>
24
 *
25
 * <p>This class is thread-safe but not reentrant. Each thread must allocate its own object.
26
 *
27
 * @author Sean Owen
28
 */
29
final class FinderPatternFinder{
30
31
	private const MIN_SKIP      = 2;
32
	private const MAX_MODULES   = 177; // 1 pixel/module times 3 modules/center
33
	private const CENTER_QUORUM = 2; // support up to version 10 for mobile clients
34
	private BitMatrix $bitMatrix;
35
	/** @var \chillerlan\QRCode\Detector\FinderPattern[] */
36
	private array $possibleCenters;
37
	private bool  $hasSkipped = false;
38
39
	/**
40
	 * <p>Creates a finder that will search the image for three finder patterns.</p>
41
	 *
42
	 * @param BitMatrix $bitMatrix image to search
43
	 */
44
	public function __construct(BitMatrix $bitMatrix){
45
		$this->bitMatrix       = $bitMatrix;
46
		$this->possibleCenters = [];
47
	}
48
49
	/**
50
	 * @return \chillerlan\QRCode\Detector\FinderPattern[]
51
	 */
52
	public function find():array{
53
		$dimension = $this->bitMatrix->getDimension();
54
55
		// We are looking for black/white/black/white/black modules in
56
		// 1:1:3:1:1 ratio; this tracks the number of such modules seen so far
57
		// Let's assume that the maximum version QR Code we support takes up 1/4 the height of the
58
		// image, and then account for the center being 3 modules in size. This gives the smallest
59
		// number of pixels the center could be, so skip this often.
60
		$iSkip = (int)((3 * $dimension) / (4 * self::MAX_MODULES));
61
62
		if($iSkip < self::MIN_SKIP){
63
			$iSkip = self::MIN_SKIP;
64
		}
65
66
		$done = false;
67
68
		for($i = $iSkip - 1; $i < $dimension && !$done; $i += $iSkip){
69
			// Get a row of black/white values
70
			$stateCount   = $this->getCrossCheckStateCount();
71
			$currentState = 0;
72
73
			for($j = 0; $j < $dimension; $j++){
74
75
				// Black pixel
76
				if($this->bitMatrix->get($j, $i)){
77
					// Counting white pixels
78
					if(($currentState & 1) === 1){
79
						$currentState++;
80
					}
81
82
					$stateCount[$currentState]++;
83
				}
84
				// White pixel
85
				else{
86
					// Counting black pixels
87
					if(($currentState & 1) === 0){
88
						// A winner?
89
						if($currentState === 4){
90
							// Yes
91
							if($this->foundPatternCross($stateCount)){
92
								$confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
93
94
								if($confirmed){
95
									// Start examining every other line. Checking each line turned out to be too
96
									// expensive and didn't improve performance.
97
									$iSkip = 3;
98
99
									if($this->hasSkipped){
100
										$done = $this->haveMultiplyConfirmedCenters();
101
									}
102
									else{
103
										$rowSkip = $this->findRowSkip();
104
105
										if($rowSkip > $stateCount[2]){
106
											// Skip rows between row of lower confirmed center
107
											// and top of presumed third confirmed center
108
											// but back up a bit to get a full chance of detecting
109
											// it, entire width of center of finder pattern
110
111
											// Skip by rowSkip, but back off by $stateCount[2] (size of last center
112
											// of pattern we saw) to be conservative, and also back off by iSkip which
113
											// is about to be re-added
114
											$i += $rowSkip - $stateCount[2] - $iSkip;
115
											$j = $dimension - 1;
116
										}
117
									}
118
								}
119
								else{
120
									$stateCount   = $this->doShiftCounts2($stateCount);
121
									$currentState = 3;
122
123
									continue;
124
								}
125
								// Clear state to start looking again
126
								$currentState = 0;
127
								$stateCount   = $this->getCrossCheckStateCount();
128
							}
129
							// No, shift counts back by two
130
							else{
131
								$stateCount   = $this->doShiftCounts2($stateCount);
132
								$currentState = 3;
133
							}
134
						}
135
						else{
136
							$stateCount[++$currentState]++;
137
						}
138
					}
139
					// Counting white pixels
140
					else{
141
						$stateCount[$currentState]++;
142
					}
143
				}
144
			}
145
146
			if($this->foundPatternCross($stateCount)){
147
				$confirmed = $this->handlePossibleCenter($stateCount, $i, $dimension);
148
149
				if($confirmed){
150
					$iSkip = $stateCount[0];
151
152
					if($this->hasSkipped){
153
						// Found a third one
154
						$done = $this->haveMultiplyConfirmedCenters();
155
					}
156
				}
157
			}
158
		}
159
160
		return $this->orderBestPatterns($this->selectBestPatterns());
161
	}
162
163
	/**
164
	 * @return int[]
165
	 */
166
	private function getCrossCheckStateCount():array{
167
		return [0, 0, 0, 0, 0];
168
	}
169
170
	/**
171
	 * @param int[] $stateCount
172
	 *
173
	 * @return int[]
174
	 */
175
	private function doShiftCounts2(array $stateCount):array{
176
		$stateCount[0] = $stateCount[2];
177
		$stateCount[1] = $stateCount[3];
178
		$stateCount[2] = $stateCount[4];
179
		$stateCount[3] = 1;
180
		$stateCount[4] = 0;
181
182
		return $stateCount;
183
	}
184
185
	/**
186
	 * Given a count of black/white/black/white/black pixels just seen and an end position,
187
	 * figures the location of the center of this run.
188
	 *
189
	 * @param int[] $stateCount
190
	 *
191
	 * @return float
192
	 */
193
	private function centerFromEnd(array $stateCount, int $end):float{
194
		return (float)(($end - $stateCount[4] - $stateCount[3]) - $stateCount[2] / 2.0);
195
	}
196
197
	/**
198
	 * @param int[] $stateCount
199
	 *
200
	 * @return bool
201
	 */
202
	private function foundPatternCross(array $stateCount):bool{
203
		// Allow less than 50% variance from 1-1-3-1-1 proportions
204
		return $this->foundPatternVariance($stateCount, 2.0);
205
	}
206
207
	/**
208
	 * @param int[] $stateCount
209
	 *
210
	 * @return bool
211
	 */
212
	private function foundPatternDiagonal(array $stateCount):bool{
213
		// Allow less than 75% variance from 1-1-3-1-1 proportions
214
		return $this->foundPatternVariance($stateCount, 1.333);
215
	}
216
217
	/**
218
	 * @param int[] $stateCount count of black/white/black/white/black pixels just read
219
	 *
220
	 * @return bool true if the proportions of the counts is close enough to the 1/1/3/1/1 ratios
221
	 *              used by finder patterns to be considered a match
222
	 */
223
	private function foundPatternVariance(array $stateCount, float $variance):bool{
224
		$totalModuleSize = 0;
225
226
		for($i = 0; $i < 5; $i++){
227
			$count = $stateCount[$i];
228
229
			if($count === 0){
230
				return false;
231
			}
232
233
			$totalModuleSize += $count;
234
		}
235
236
		if($totalModuleSize < 7){
237
			return false;
238
		}
239
240
		$moduleSize  = $totalModuleSize / 7.0;
241
		$maxVariance = $moduleSize / $variance;
242
243
		return
244
			abs($moduleSize - $stateCount[0]) < $maxVariance
245
			&& abs($moduleSize - $stateCount[1]) < $maxVariance
246
			&& abs(3.0 * $moduleSize - $stateCount[2]) < 3 * $maxVariance
247
			&& abs($moduleSize - $stateCount[3]) < $maxVariance
248
			&& abs($moduleSize - $stateCount[4]) < $maxVariance;
249
	}
250
251
	/**
252
	 * After a vertical and horizontal scan finds a potential finder pattern, this method
253
	 * "cross-cross-cross-checks" by scanning down diagonally through the center of the possible
254
	 * finder pattern to see if the same proportion is detected.
255
	 *
256
	 * @param $centerI ;  row where a finder pattern was detected
0 ignored issues
show
Documentation Bug introduced by
The doc comment ; at position 0 could not be parsed: Unknown type name ';' at position 0 in ;.
Loading history...
257
	 * @param $centerJ ; center of the section that appears to cross a finder pattern
258
	 *
259
	 * @return bool true if proportions are withing expected limits
260
	 */
261
	private function crossCheckDiagonal(int $centerI, int $centerJ):bool{
262
		$stateCount = $this->getCrossCheckStateCount();
263
264
		// Start counting up, left from center finding black center mass
265
		$i = 0;
266
267
		while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){
268
			$stateCount[2]++;
269
			$i++;
270
		}
271
272
		if($stateCount[2] === 0){
273
			return false;
274
		}
275
276
		// Continue up, left finding white space
277
		while($centerI >= $i && $centerJ >= $i && !$this->bitMatrix->get($centerJ - $i, $centerI - $i)){
278
			$stateCount[1]++;
279
			$i++;
280
		}
281
282
		if($stateCount[1] === 0){
283
			return false;
284
		}
285
286
		// Continue up, left finding black border
287
		while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){
288
			$stateCount[0]++;
289
			$i++;
290
		}
291
292
		if($stateCount[0] === 0){
293
			return false;
294
		}
295
296
		$dimension = $this->bitMatrix->getDimension();
297
298
		// Now also count down, right from center
299
		$i = 1;
300
		while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){
301
			$stateCount[2]++;
302
			$i++;
303
		}
304
305
		while($centerI + $i < $dimension && $centerJ + $i < $dimension && !$this->bitMatrix->get($centerJ + $i, $centerI + $i)){
306
			$stateCount[3]++;
307
			$i++;
308
		}
309
310
		if($stateCount[3] === 0){
311
			return false;
312
		}
313
314
		while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){
315
			$stateCount[4]++;
316
			$i++;
317
		}
318
319
		if($stateCount[4] === 0){
320
			return false;
321
		}
322
323
		return $this->foundPatternDiagonal($stateCount);
324
	}
325
326
	/**
327
	 * <p>After a horizontal scan finds a potential finder pattern, this method
328
	 * "cross-checks" by scanning down vertically through the center of the possible
329
	 * finder pattern to see if the same proportion is detected.</p>
330
	 *
331
	 * @param int $startI   ;  row where a finder pattern was detected
332
	 * @param int $centerJ  ; center of the section that appears to cross a finder pattern
333
	 * @param int $maxCount ; maximum reasonable number of modules that should be
334
	 *                      observed in any reading state, based on the results of the horizontal scan
335
	 * @param int $originalStateCountTotal
336
	 *
337
	 * @return float|null vertical center of finder pattern, or null if not found
338
	 */
339
	private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{
340
		$maxI       = $this->bitMatrix->getDimension();
341
		$stateCount = $this->getCrossCheckStateCount();
342
343
		// Start counting up from center
344
		$i = $startI;
345
		while($i >= 0 && $this->bitMatrix->get($centerJ, $i)){
346
			$stateCount[2]++;
347
			$i--;
348
		}
349
350
		if($i < 0){
351
			return null;
352
		}
353
354
		while($i >= 0 && !$this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){
355
			$stateCount[1]++;
356
			$i--;
357
		}
358
359
		// If already too many modules in this state or ran off the edge:
360
		if($i < 0 || $stateCount[1] > $maxCount){
361
			return null;
362
		}
363
364
		while($i >= 0 && $this->bitMatrix->get($centerJ, $i) && $stateCount[0] <= $maxCount){
365
			$stateCount[0]++;
366
			$i--;
367
		}
368
369
		if($stateCount[0] > $maxCount){
370
			return null;
371
		}
372
373
		// Now also count down from center
374
		$i = $startI + 1;
375
		while($i < $maxI && $this->bitMatrix->get($centerJ, $i)){
376
			$stateCount[2]++;
377
			$i++;
378
		}
379
380
		if($i === $maxI){
381
			return null;
382
		}
383
384
		while($i < $maxI && !$this->bitMatrix->get($centerJ, $i) && $stateCount[3] < $maxCount){
385
			$stateCount[3]++;
386
			$i++;
387
		}
388
389
		if($i === $maxI || $stateCount[3] >= $maxCount){
390
			return null;
391
		}
392
393
		while($i < $maxI && $this->bitMatrix->get($centerJ, $i) && $stateCount[4] < $maxCount){
394
			$stateCount[4]++;
395
			$i++;
396
		}
397
398
		if($stateCount[4] >= $maxCount){
399
			return null;
400
		}
401
402
		// If we found a finder-pattern-like section, but its size is more than 40% different than
403
		// the original, assume it's a false positive
404
		$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
405
406
		if(5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal){
407
			return null;
408
		}
409
410
		if(!$this->foundPatternCross($stateCount)){
411
			return null;
412
		}
413
414
		return $this->centerFromEnd($stateCount, $i);
415
	}
416
417
	/**
418
	 * <p>Like {@link #crossCheckVertical(int, int, int, int)}, and in fact is basically identical,
419
	 * except it reads horizontally instead of vertically. This is used to cross-cross
420
	 * check a vertical cross check and locate the real center of the alignment pattern.</p>
421
	 */
422
	private function crossCheckHorizontal(int $startJ, int $centerI, int $maxCount, int $originalStateCountTotal):?float{
423
		$maxJ       = $this->bitMatrix->getDimension();
424
		$stateCount = $this->getCrossCheckStateCount();
425
426
		$j = $startJ;
427
		while($j >= 0 && $this->bitMatrix->get($j, $centerI)){
428
			$stateCount[2]++;
429
			$j--;
430
		}
431
432
		if($j < 0){
433
			return null;
434
		}
435
436
		while($j >= 0 && !$this->bitMatrix->get($j, $centerI) && $stateCount[1] <= $maxCount){
437
			$stateCount[1]++;
438
			$j--;
439
		}
440
441
		if($j < 0 || $stateCount[1] > $maxCount){
442
			return null;
443
		}
444
445
		while($j >= 0 && $this->bitMatrix->get($j, $centerI) && $stateCount[0] <= $maxCount){
446
			$stateCount[0]++;
447
			$j--;
448
		}
449
450
		if($stateCount[0] > $maxCount){
451
			return null;
452
		}
453
454
		$j = $startJ + 1;
455
		while($j < $maxJ && $this->bitMatrix->get($j, $centerI)){
456
			$stateCount[2]++;
457
			$j++;
458
		}
459
460
		if($j === $maxJ){
461
			return null;
462
		}
463
464
		while($j < $maxJ && !$this->bitMatrix->get($j, $centerI) && $stateCount[3] < $maxCount){
465
			$stateCount[3]++;
466
			$j++;
467
		}
468
469
		if($j === $maxJ || $stateCount[3] >= $maxCount){
470
			return null;
471
		}
472
473
		while($j < $maxJ && $this->bitMatrix->get($j, $centerI) && $stateCount[4] < $maxCount){
474
			$stateCount[4]++;
475
			$j++;
476
		}
477
478
		if($stateCount[4] >= $maxCount){
479
			return null;
480
		}
481
482
		// If we found a finder-pattern-like section, but its size is significantly different than
483
		// the original, assume it's a false positive
484
		$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
485
486
		if(5 * abs($stateCountTotal - $originalStateCountTotal) >= $originalStateCountTotal){
487
			return null;
488
		}
489
490
		if(!$this->foundPatternCross($stateCount)){
491
			return null;
492
		}
493
494
		return $this->centerFromEnd($stateCount, $j);
495
	}
496
497
	/**
498
	 * <p>This is called when a horizontal scan finds a possible alignment pattern. It will
499
	 * cross check with a vertical scan, and if successful, will, ah, cross-cross-check
500
	 * with another horizontal scan. This is needed primarily to locate the real horizontal
501
	 * center of the pattern in cases of extreme skew.
502
	 * And then we cross-cross-cross check with another diagonal scan.</p>
503
	 *
504
	 * <p>If that succeeds the finder pattern location is added to a list that tracks
505
	 * the number of times each location has been nearly-matched as a finder pattern.
506
	 * Each additional find is more evidence that the location is in fact a finder
507
	 * pattern center
508
	 *
509
	 * @param int[] $stateCount reading state module counts from horizontal scan
510
	 * @param int   $i          row where finder pattern may be found
511
	 * @param int   $j          end of possible finder pattern in row
512
	 *
513
	 * @return bool if a finder pattern candidate was found this time
514
	 */
515
	private function handlePossibleCenter(array $stateCount, int $i, int $j):bool{
516
		$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
517
		$centerJ         = $this->centerFromEnd($stateCount, $j);
518
		$centerI         = $this->crossCheckVertical($i, (int)$centerJ, $stateCount[2], $stateCountTotal);
519
520
		if($centerI !== null){
521
			// Re-cross check
522
			$centerJ = $this->crossCheckHorizontal((int)$centerJ, (int)$centerI, $stateCount[2], $stateCountTotal);
523
			if($centerJ !== null && ($this->crossCheckDiagonal((int)$centerI, (int)$centerJ))){
524
				$estimatedModuleSize = $stateCountTotal / 7.0;
525
				$found               = false;
526
527
				for($index = 0; $index < count($this->possibleCenters); $index++){
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
528
					$center = $this->possibleCenters[$index];
529
					// Look for about the same center and module size:
530
					if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){
531
						$this->possibleCenters[$index] = $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
532
						$found                         = true;
533
						break;
534
					}
535
				}
536
537
				if(!$found){
538
					$point                   = new FinderPattern($centerJ, $centerI, $estimatedModuleSize);
539
					$this->possibleCenters[] = $point;
540
				}
541
542
				return true;
543
			}
544
		}
545
546
		return false;
547
	}
548
549
	/**
550
	 * @return int number of rows we could safely skip during scanning, based on the first
551
	 *         two finder patterns that have been located. In some cases their position will
552
	 *         allow us to infer that the third pattern must lie below a certain point farther
553
	 *         down in the image.
554
	 */
555
	private function findRowSkip():int{
556
		$max = count($this->possibleCenters);
557
558
		if($max <= 1){
559
			return 0;
560
		}
561
562
		$firstConfirmedCenter = null;
563
564
		foreach($this->possibleCenters as $center){
565
566
			if($center->getCount() >= self::CENTER_QUORUM){
567
568
				if($firstConfirmedCenter === null){
569
					$firstConfirmedCenter = $center;
570
				}
571
				else{
572
					// We have two confirmed centers
573
					// How far down can we skip before resuming looking for the next
574
					// pattern? In the worst case, only the difference between the
575
					// difference in the x / y coordinates of the two centers.
576
					// This is the case where you find top left last.
577
					$this->hasSkipped = true;
578
579
					return (int)((abs($firstConfirmedCenter->getX() - $center->getX()) -
0 ignored issues
show
Bug introduced by
The method getX() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

579
					return (int)((abs($firstConfirmedCenter->/** @scrutinizer ignore-call */ getX() - $center->getX()) -

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
580
					              abs($firstConfirmedCenter->getY() - $center->getY())) / 2);
581
				}
582
			}
583
		}
584
585
		return 0;
586
	}
587
588
	/**
589
	 * @return bool true if we have found at least 3 finder patterns that have been detected
590
	 *              at least {@link #CENTER_QUORUM} times each, and, the estimated module size of the
591
	 *              candidates is "pretty similar"
592
	 */
593
	private function haveMultiplyConfirmedCenters():bool{
594
		$confirmedCount  = 0;
595
		$totalModuleSize = 0.0;
596
		$max             = count($this->possibleCenters);
597
598
		foreach($this->possibleCenters as $pattern){
599
			if($pattern->getCount() >= self::CENTER_QUORUM){
600
				$confirmedCount++;
601
				$totalModuleSize += $pattern->getEstimatedModuleSize();
602
			}
603
		}
604
605
		if($confirmedCount < 3){
606
			return false;
607
		}
608
		// OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive"
609
		// and that we need to keep looking. We detect this by asking if the estimated module sizes
610
		// vary too much. We arbitrarily say that when the total deviation from average exceeds
611
		// 5% of the total module size estimates, it's too much.
612
		$average        = $totalModuleSize / (float)$max;
613
		$totalDeviation = 0.0;
614
615
		foreach($this->possibleCenters as $pattern){
616
			$totalDeviation += abs($pattern->getEstimatedModuleSize() - $average);
617
		}
618
619
		return $totalDeviation <= 0.05 * $totalModuleSize;
620
	}
621
622
	/**
623
	 * @return \chillerlan\QRCode\Detector\FinderPattern[] the 3 best {@link FinderPattern}s from our list of candidates. The "best" are
624
	 *         those that have been detected at least {@link #CENTER_QUORUM} times, and whose module
625
	 *         size differs from the average among those patterns the least
626
	 * @throws \RuntimeException if 3 such finder patterns do not exist
627
	 */
628
	private function selectBestPatterns():array{
629
		$startSize = count($this->possibleCenters);
630
631
		if($startSize < 3){
632
			throw new RuntimeException('could not find enough finder patterns');
633
		}
634
635
		usort(
636
			$this->possibleCenters,
637
			fn(FinderPattern $a, FinderPattern $b) => $a->getEstimatedModuleSize() <=> $b->getEstimatedModuleSize()
638
		);
639
640
		$distortion   = PHP_FLOAT_MAX;
641
		$bestPatterns = [];
642
643
		for($i = 0; $i < $startSize - 2; $i++){
644
			$fpi           = $this->possibleCenters[$i];
645
			$minModuleSize = $fpi->getEstimatedModuleSize();
646
647
			for($j = $i + 1; $j < $startSize - 1; $j++){
648
				$fpj      = $this->possibleCenters[$j];
649
				$squares0 = $fpi->getSquaredDistance($fpj);
650
651
				for($k = $j + 1; $k < $startSize; $k++){
652
					$fpk           = $this->possibleCenters[$k];
653
					$maxModuleSize = $fpk->getEstimatedModuleSize();
654
655
					// module size is not similar
656
					if($maxModuleSize > $minModuleSize * 1.4){
657
						continue;
658
					}
659
660
					$a = $squares0;
661
					$b = $fpj->getSquaredDistance($fpk);
662
					$c = $fpi->getSquaredDistance($fpk);
663
664
					// sorts ascending - inlined
665
					if($a < $b){
666
						if($b > $c){
667
							if($a < $c){
668
								$temp = $b;
669
								$b    = $c;
670
								$c    = $temp;
671
							}
672
							else{
673
								$temp = $a;
674
								$a    = $c;
675
								$c    = $b;
676
								$b    = $temp;
677
							}
678
						}
679
					}
680
					else{
681
						if($b < $c){
682
							if($a < $c){
683
								$temp = $a;
684
								$a    = $b;
685
								$b    = $temp;
686
							}
687
							else{
688
								$temp = $a;
689
								$a    = $b;
690
								$b    = $c;
691
								$c    = $temp;
692
							}
693
						}
694
						else{
695
							$temp = $a;
696
							$a    = $c;
697
							$c    = $temp;
698
						}
699
					}
700
701
					// a^2 + b^2 = c^2 (Pythagorean theorem), and a = b (isosceles triangle).
702
					// Since any right triangle satisfies the formula c^2 - b^2 - a^2 = 0,
703
					// we need to check both two equal sides separately.
704
					// The value of |c^2 - 2 * b^2| + |c^2 - 2 * a^2| increases as dissimilarity
705
					// from isosceles right triangle.
706
					$d = abs($c - 2 * $b) + abs($c - 2 * $a);
707
708
					if($d < $distortion){
709
						$distortion   = $d;
710
						$bestPatterns = [$fpi, $fpj, $fpk];
711
					}
712
				}
713
			}
714
		}
715
716
		if($distortion === PHP_FLOAT_MAX){
717
			throw new RuntimeException('finder patterns may be too distorted');
718
		}
719
720
		return $bestPatterns;
721
	}
722
723
	/**
724
	 * Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC
725
	 * and BC is less than AC, and the angle between BC and BA is less than 180 degrees.
726
	 *
727
	 * @param \chillerlan\QRCode\Detector\FinderPattern[] $patterns array of three FinderPattern to order
728
	 *
729
	 * @return \chillerlan\QRCode\Detector\FinderPattern[]
730
	 */
731
	private function orderBestPatterns(array $patterns):array{
732
733
		// Find distances between pattern centers
734
		$zeroOneDistance = $patterns[0]->getDistance($patterns[1]);
735
		$oneTwoDistance  = $patterns[1]->getDistance($patterns[2]);
736
		$zeroTwoDistance = $patterns[0]->getDistance($patterns[2]);
737
738
		// Assume one closest to other two is B; A and C will just be guesses at first
739
		if($oneTwoDistance >= $zeroOneDistance && $oneTwoDistance >= $zeroTwoDistance){
740
			[$pointB, $pointA, $pointC] = $patterns;
741
		}
742
		elseif($zeroTwoDistance >= $oneTwoDistance && $zeroTwoDistance >= $zeroOneDistance){
743
			[$pointA, $pointB, $pointC] = $patterns;
744
		}
745
		else{
746
			[$pointA, $pointC, $pointB] = $patterns;
747
		}
748
749
		// Use cross product to figure out whether A and C are correct or flipped.
750
		// This asks whether BC x BA has a positive z component, which is the arrangement
751
		// we want for A, B, C. If it's negative, then we've got it flipped around and
752
		// should swap A and C.
753
		if($this->crossProductZ($pointA, $pointB, $pointC) < 0.0){
754
			$temp   = $pointA;
755
			$pointA = $pointC;
756
			$pointC = $temp;
757
		}
758
759
		return [$pointA, $pointB, $pointC];
760
	}
761
762
	/**
763
	 * Returns the z component of the cross product between vectors BC and BA.
764
	 */
765
	private function crossProductZ(FinderPattern $pointA, FinderPattern $pointB, FinderPattern $pointC):float{
766
		$bX = $pointB->getX();
767
		$bY = $pointB->getY();
768
769
		return (($pointC->getX() - $bX) * ($pointA->getY() - $bY)) - (($pointC->getY() - $bY) * ($pointA->getX() - $bX));
770
	}
771
772
}
773