Passed
Push — v5 ( 93618e...84eb31 )
by smiley
01:52
created

FinderPatternFinder::crossCheckVertical()   F

Complexity

Conditions 27
Paths 318

Size

Total Lines 76
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 27
eloc 40
c 1
b 0
f 0
nc 318
nop 4
dl 0
loc 76
rs 1.8583

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
	/** @var int[] */
39
	private array $crossCheckStateCount;
40
41
	/**
42
	 * <p>Creates a finder that will search the image for three finder patterns.</p>
43
	 *
44
	 * @param BitMatrix $bitMatrix image to search
45
	 */
46
	public function __construct(BitMatrix $bitMatrix){
47
		$this->bitMatrix            = $bitMatrix;
48
		$this->possibleCenters      = [];
49
		$this->crossCheckStateCount = $this->getCrossCheckStateCount();
50
	}
51
52
	/**
53
	 * @return \chillerlan\QRCode\Detector\FinderPattern[]
54
	 */
55
	public function find():array{
56
		$dimension = $this->bitMatrix->getDimension();
57
58
		// We are looking for black/white/black/white/black modules in
59
		// 1:1:3:1:1 ratio; this tracks the number of such modules seen so far
60
		// Let's assume that the maximum version QR Code we support takes up 1/4 the height of the
61
		// image, and then account for the center being 3 modules in size. This gives the smallest
62
		// number of pixels the center could be, so skip this often.
63
		$iSkip = (int)((3 * $dimension) / (4 * self::MAX_MODULES));
64
65
		if($iSkip < self::MIN_SKIP){
66
			$iSkip = self::MIN_SKIP;
67
		}
68
69
		$done = false;
70
71
		for($i = $iSkip - 1; $i < $dimension && !$done; $i += $iSkip){
72
			// Get a row of black/white values
73
			$stateCount   = $this->getCrossCheckStateCount();
74
			$currentState = 0;
75
76
			for($j = 0; $j < $dimension; $j++){
77
78
				// Black pixel
79
				if($this->bitMatrix->get($j, $i)){
80
					// Counting white pixels
81
					if(($currentState & 1) === 1){
82
						$currentState++;
83
					}
84
85
					$stateCount[$currentState]++;
86
				}
87
				// White pixel
88
				else{
89
					// Counting black pixels
90
					if(($currentState & 1) === 0){
91
						// A winner?
92
						if($currentState === 4){
93
							// Yes
94
							if($this->foundPatternCross($stateCount)){
95
								$confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
96
97
								if($confirmed){
98
									// Start examining every other line. Checking each line turned out to be too
99
									// expensive and didn't improve performance.
100
									$iSkip = 3;
101
102
									if($this->hasSkipped){
103
										$done = $this->haveMultiplyConfirmedCenters();
104
									}
105
									else{
106
										$rowSkip = $this->findRowSkip();
107
108
										if($rowSkip > $stateCount[2]){
109
											// Skip rows between row of lower confirmed center
110
											// and top of presumed third confirmed center
111
											// but back up a bit to get a full chance of detecting
112
											// it, entire width of center of finder pattern
113
114
											// Skip by rowSkip, but back off by $stateCount[2] (size of last center
115
											// of pattern we saw) to be conservative, and also back off by iSkip which
116
											// is about to be re-added
117
											$i += $rowSkip - $stateCount[2] - $iSkip;
118
											$j = $dimension - 1;
119
										}
120
									}
121
								}
122
								else{
123
									$stateCount   = $this->doShiftCounts2($stateCount);
124
									$currentState = 3;
125
126
									continue;
127
								}
128
								// Clear state to start looking again
129
								$currentState = 0;
130
								$stateCount   = $this->getCrossCheckStateCount();
131
							}
132
							// No, shift counts back by two
133
							else{
134
								$stateCount   = $this->doShiftCounts2($stateCount);
135
								$currentState = 3;
136
							}
137
						}
138
						else{
139
							$stateCount[++$currentState]++;
140
						}
141
					}
142
					// Counting white pixels
143
					else{
144
						$stateCount[$currentState]++;
145
					}
146
				}
147
			}
148
149
			if($this->foundPatternCross($stateCount)){
150
				$confirmed = $this->handlePossibleCenter($stateCount, $i, $dimension);
151
152
				if($confirmed){
153
					$iSkip = $stateCount[0];
154
155
					if($this->hasSkipped){
156
						// Found a third one
157
						$done = $this->haveMultiplyConfirmedCenters();
158
					}
159
				}
160
			}
161
		}
162
163
		return $this->orderBestPatterns($this->selectBestPatterns());
164
	}
165
166
	/**
167
	 * @return int[]
168
	 */
169
	private function getCrossCheckStateCount():array{
170
		return [0, 0, 0, 0, 0];
171
	}
172
173
	/**
174
	 * @param int[] $stateCount
175
	 *
176
	 * @return int[]
177
	 */
178
	private function doShiftCounts2(array $stateCount):array{
179
		$stateCount[0] = $stateCount[2];
180
		$stateCount[1] = $stateCount[3];
181
		$stateCount[2] = $stateCount[4];
182
		$stateCount[3] = 1;
183
		$stateCount[4] = 0;
184
185
		return $stateCount;
186
	}
187
188
	/**
189
	 * Given a count of black/white/black/white/black pixels just seen and an end position,
190
	 * figures the location of the center of this run.
191
	 *
192
	 * @param int[] $stateCount
193
	 *
194
	 * @return float
195
	 */
196
	private function centerFromEnd(array $stateCount, int $end):float{
197
		return (float)(($end - $stateCount[4] - $stateCount[3]) - $stateCount[2] / 2.0);
198
	}
199
200
	/**
201
	 * @param int[] $stateCount
202
	 *
203
	 * @return bool
204
	 */
205
	private function foundPatternCross(array $stateCount):bool{
206
		// Allow less than 50% variance from 1-1-3-1-1 proportions
207
		return $this->foundPatternVariance($stateCount, 2.0);
208
	}
209
210
	/**
211
	 * @param int[] $stateCount
212
	 *
213
	 * @return bool
214
	 */
215
	private function foundPatternDiagonal(array $stateCount):bool{
216
		// Allow less than 75% variance from 1-1-3-1-1 proportions
217
		return $this->foundPatternVariance($stateCount, 1.333);
218
	}
219
220
	/**
221
	 * @param int[] $stateCount count of black/white/black/white/black pixels just read
222
	 *
223
	 * @return bool true if the proportions of the counts is close enough to the 1/1/3/1/1 ratios
224
	 *              used by finder patterns to be considered a match
225
	 */
226
	private function foundPatternVariance(array $stateCount, float $variance):bool{
227
		$totalModuleSize = 0;
228
229
		for($i = 0; $i < 5; $i++){
230
			$count = $stateCount[$i];
231
232
			if($count === 0){
233
				return false;
234
			}
235
236
			$totalModuleSize += $count;
237
		}
238
239
		if($totalModuleSize < 7){
240
			return false;
241
		}
242
243
		$moduleSize  = $totalModuleSize / 7.0;
244
		$maxVariance = $moduleSize / $variance;
245
246
		return
247
			abs($moduleSize - $stateCount[0]) < $maxVariance
248
			&& abs($moduleSize - $stateCount[1]) < $maxVariance
249
			&& abs(3.0 * $moduleSize - $stateCount[2]) < 3 * $maxVariance
250
			&& abs($moduleSize - $stateCount[3]) < $maxVariance
251
			&& abs($moduleSize - $stateCount[4]) < $maxVariance;
252
	}
253
254
	/**
255
	 * After a vertical and horizontal scan finds a potential finder pattern, this method
256
	 * "cross-cross-cross-checks" by scanning down diagonally through the center of the possible
257
	 * finder pattern to see if the same proportion is detected.
258
	 *
259
	 * @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...
260
	 * @param $centerJ ; center of the section that appears to cross a finder pattern
261
	 *
262
	 * @return bool true if proportions are withing expected limits
263
	 */
264
	private function crossCheckDiagonal(int $centerI, int $centerJ):bool{
265
		$stateCount = $this->getCrossCheckStateCount();
266
267
		// Start counting up, left from center finding black center mass
268
		$i = 0;
269
270
		while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){
271
			$stateCount[2]++;
272
			$i++;
273
		}
274
275
		if($stateCount[2] === 0){
276
			return false;
277
		}
278
279
		// Continue up, left finding white space
280
		while($centerI >= $i && $centerJ >= $i && !$this->bitMatrix->get($centerJ - $i, $centerI - $i)){
281
			$stateCount[1]++;
282
			$i++;
283
		}
284
285
		if($stateCount[1] === 0){
286
			return false;
287
		}
288
289
		// Continue up, left finding black border
290
		while($centerI >= $i && $centerJ >= $i && $this->bitMatrix->get($centerJ - $i, $centerI - $i)){
291
			$stateCount[0]++;
292
			$i++;
293
		}
294
295
		if($stateCount[0] === 0){
296
			return false;
297
		}
298
299
		$dimension = $this->bitMatrix->getDimension();
300
301
		// Now also count down, right from center
302
		$i = 1;
303
		while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){
304
			$stateCount[2]++;
305
			$i++;
306
		}
307
308
		while($centerI + $i < $dimension && $centerJ + $i < $dimension && !$this->bitMatrix->get($centerJ + $i, $centerI + $i)){
309
			$stateCount[3]++;
310
			$i++;
311
		}
312
313
		if($stateCount[3] === 0){
314
			return false;
315
		}
316
317
		while($centerI + $i < $dimension && $centerJ + $i < $dimension && $this->bitMatrix->get($centerJ + $i, $centerI + $i)){
318
			$stateCount[4]++;
319
			$i++;
320
		}
321
322
		if($stateCount[4] === 0){
323
			return false;
324
		}
325
326
		return $this->foundPatternDiagonal($stateCount);
327
	}
328
329
	/**
330
	 * <p>After a horizontal scan finds a potential finder pattern, this method
331
	 * "cross-checks" by scanning down vertically through the center of the possible
332
	 * finder pattern to see if the same proportion is detected.</p>
333
	 *
334
	 * @param int $startI   ;  row where a finder pattern was detected
335
	 * @param int $centerJ  ; center of the section that appears to cross a finder pattern
336
	 * @param int $maxCount ; maximum reasonable number of modules that should be
337
	 *                      observed in any reading state, based on the results of the horizontal scan
338
	 * @param int $originalStateCountTotal
339
	 *
340
	 * @return float|null vertical center of finder pattern, or null if not found
341
	 */
342
	private function crossCheckVertical(int $startI, int $centerJ, int $maxCount, int $originalStateCountTotal):?float{
343
		$maxI       = $this->bitMatrix->getDimension();
344
		$stateCount = $this->getCrossCheckStateCount();
345
346
		// Start counting up from center
347
		$i = $startI;
348
		while($i >= 0 && $this->bitMatrix->get($centerJ, $i)){
349
			$stateCount[2]++;
350
			$i--;
351
		}
352
353
		if($i < 0){
354
			return null;
355
		}
356
357
		while($i >= 0 && !$this->bitMatrix->get($centerJ, $i) && $stateCount[1] <= $maxCount){
358
			$stateCount[1]++;
359
			$i--;
360
		}
361
362
		// If already too many modules in this state or ran off the edge:
363
		if($i < 0 || $stateCount[1] > $maxCount){
364
			return null;
365
		}
366
367
		while($i >= 0 && $this->bitMatrix->get($centerJ, $i) && $stateCount[0] <= $maxCount){
368
			$stateCount[0]++;
369
			$i--;
370
		}
371
372
		if($stateCount[0] > $maxCount){
373
			return null;
374
		}
375
376
		// Now also count down from center
377
		$i = $startI + 1;
378
		while($i < $maxI && $this->bitMatrix->get($centerJ, $i)){
379
			$stateCount[2]++;
380
			$i++;
381
		}
382
383
		if($i === $maxI){
384
			return null;
385
		}
386
387
		while($i < $maxI && !$this->bitMatrix->get($centerJ, $i) && $stateCount[3] < $maxCount){
388
			$stateCount[3]++;
389
			$i++;
390
		}
391
392
		if($i === $maxI || $stateCount[3] >= $maxCount){
393
			return null;
394
		}
395
396
		while($i < $maxI && $this->bitMatrix->get($centerJ, $i) && $stateCount[4] < $maxCount){
397
			$stateCount[4]++;
398
			$i++;
399
		}
400
401
		if($stateCount[4] >= $maxCount){
402
			return null;
403
		}
404
405
		// If we found a finder-pattern-like section, but its size is more than 40% different than
406
		// the original, assume it's a false positive
407
		$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
408
409
		if(5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal){
410
			return null;
411
		}
412
413
		if(!$this->foundPatternCross($stateCount)){
414
			return null;
415
		}
416
417
		return $this->centerFromEnd($stateCount, $i);
418
	}
419
420
	/**
421
	 * <p>Like {@link #crossCheckVertical(int, int, int, int)}, and in fact is basically identical,
422
	 * except it reads horizontally instead of vertically. This is used to cross-cross
423
	 * check a vertical cross check and locate the real center of the alignment pattern.</p>
424
	 */
425
	private function crossCheckHorizontal(int $startJ, int $centerI, int $maxCount, int $originalStateCountTotal):?float{
426
		$maxJ       = $this->bitMatrix->getDimension();
427
		$stateCount = $this->getCrossCheckStateCount();
428
429
		$j = $startJ;
430
		while($j >= 0 && $this->bitMatrix->get($j, $centerI)){
431
			$stateCount[2]++;
432
			$j--;
433
		}
434
435
		if($j < 0){
436
			return null;
437
		}
438
439
		while($j >= 0 && !$this->bitMatrix->get($j, $centerI) && $stateCount[1] <= $maxCount){
440
			$stateCount[1]++;
441
			$j--;
442
		}
443
444
		if($j < 0 || $stateCount[1] > $maxCount){
445
			return null;
446
		}
447
448
		while($j >= 0 && $this->bitMatrix->get($j, $centerI) && $stateCount[0] <= $maxCount){
449
			$stateCount[0]++;
450
			$j--;
451
		}
452
453
		if($stateCount[0] > $maxCount){
454
			return null;
455
		}
456
457
		$j = $startJ + 1;
458
		while($j < $maxJ && $this->bitMatrix->get($j, $centerI)){
459
			$stateCount[2]++;
460
			$j++;
461
		}
462
463
		if($j === $maxJ){
464
			return null;
465
		}
466
467
		while($j < $maxJ && !$this->bitMatrix->get($j, $centerI) && $stateCount[3] < $maxCount){
468
			$stateCount[3]++;
469
			$j++;
470
		}
471
472
		if($j === $maxJ || $stateCount[3] >= $maxCount){
473
			return null;
474
		}
475
476
		while($j < $maxJ && $this->bitMatrix->get($j, $centerI) && $stateCount[4] < $maxCount){
477
			$stateCount[4]++;
478
			$j++;
479
		}
480
481
		if($stateCount[4] >= $maxCount){
482
			return null;
483
		}
484
485
		// If we found a finder-pattern-like section, but its size is significantly different than
486
		// the original, assume it's a false positive
487
		$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
488
489
		if(5 * abs($stateCountTotal - $originalStateCountTotal) >= $originalStateCountTotal){
490
			return null;
491
		}
492
493
		if(!$this->foundPatternCross($stateCount)){
494
			return null;
495
		}
496
497
		return $this->centerFromEnd($stateCount, $j);
498
	}
499
500
	/**
501
	 * <p>This is called when a horizontal scan finds a possible alignment pattern. It will
502
	 * cross check with a vertical scan, and if successful, will, ah, cross-cross-check
503
	 * with another horizontal scan. This is needed primarily to locate the real horizontal
504
	 * center of the pattern in cases of extreme skew.
505
	 * And then we cross-cross-cross check with another diagonal scan.</p>
506
	 *
507
	 * <p>If that succeeds the finder pattern location is added to a list that tracks
508
	 * the number of times each location has been nearly-matched as a finder pattern.
509
	 * Each additional find is more evidence that the location is in fact a finder
510
	 * pattern center
511
	 *
512
	 * @param int[] $stateCount reading state module counts from horizontal scan
513
	 * @param int   $i          row where finder pattern may be found
514
	 * @param int   $j          end of possible finder pattern in row
515
	 *
516
	 * @return bool if a finder pattern candidate was found this time
517
	 */
518
	private function handlePossibleCenter(array $stateCount, int $i, int $j):bool{
519
		$stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2] + $stateCount[3] + $stateCount[4];
520
		$centerJ         = $this->centerFromEnd($stateCount, $j);
521
		$centerI         = $this->crossCheckVertical($i, (int)$centerJ, $stateCount[2], $stateCountTotal);
522
523
		if($centerI !== null){
524
			// Re-cross check
525
			$centerJ = $this->crossCheckHorizontal((int)$centerJ, (int)$centerI, $stateCount[2], $stateCountTotal);
526
			if($centerJ !== null && ($this->crossCheckDiagonal((int)$centerI, (int)$centerJ))){
527
				$estimatedModuleSize = $stateCountTotal / 7.0;
528
				$found               = false;
529
530
				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...
531
					$center = $this->possibleCenters[$index];
532
					// Look for about the same center and module size:
533
					if($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)){
534
						$this->possibleCenters[$index] = $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
535
						$found                         = true;
536
						break;
537
					}
538
				}
539
540
				if(!$found){
541
					$point                   = new FinderPattern($centerJ, $centerI, $estimatedModuleSize);
542
					$this->possibleCenters[] = $point;
543
				}
544
545
				return true;
546
			}
547
		}
548
549
		return false;
550
	}
551
552
	/**
553
	 * @return int number of rows we could safely skip during scanning, based on the first
554
	 *         two finder patterns that have been located. In some cases their position will
555
	 *         allow us to infer that the third pattern must lie below a certain point farther
556
	 *         down in the image.
557
	 */
558
	private function findRowSkip():int{
559
		$max = count($this->possibleCenters);
560
561
		if($max <= 1){
562
			return 0;
563
		}
564
565
		$firstConfirmedCenter = null;
566
567
		foreach($this->possibleCenters as $center){
568
569
			if($center->getCount() >= self::CENTER_QUORUM){
570
571
				if($firstConfirmedCenter === null){
572
					$firstConfirmedCenter = $center;
573
				}
574
				else{
575
					// We have two confirmed centers
576
					// How far down can we skip before resuming looking for the next
577
					// pattern? In the worst case, only the difference between the
578
					// difference in the x / y coordinates of the two centers.
579
					// This is the case where you find top left last.
580
					$this->hasSkipped = true;
581
582
					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

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