Passed
Push — gh-pages ( 20c441...dd59e5 )
by
unknown
02:54 queued 01:00
created

Detector::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 2
rs 10
1
<?php
2
/**
3
 * Class Detector
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
12
namespace chillerlan\QRCode\Detector;
13
14
use RuntimeException;
15
use chillerlan\QRCode\Common\Version;
16
use chillerlan\QRCode\Decoder\BitMatrix;
17
use function abs, is_nan, max, min, round;
18
use const NAN;
19
20
/**
21
 * <p>Encapsulates logic that can detect a QR Code in an image, even if the QR Code
22
 * is rotated or skewed, or partially obscured.</p>
23
 *
24
 * @author Sean Owen
25
 */
26
final class Detector{
27
28
	private BitMatrix $bitMatrix;
29
30
	/**
31
	 * Detector constructor.
32
	 */
33
	public function __construct(BitMatrix $image){
34
		$this->bitMatrix = $image;
35
	}
36
37
	/**
38
	 * <p>Detects a QR Code in an image.</p>
39
	 */
40
	public function detect():BitMatrix{
41
		[$bottomLeft, $topLeft, $topRight] = (new FinderPatternFinder($this->bitMatrix))->find();
42
43
		$moduleSize         = (float)$this->calculateModuleSize($topLeft, $topRight, $bottomLeft);
44
		$dimension          = $this->computeDimension($topLeft, $topRight, $bottomLeft, $moduleSize);
45
		$provisionalVersion = new Version((int)(($dimension - 17) / 4));
46
		$alignmentPattern   = null;
47
48
		// Anything above version 1 has an alignment pattern
49
		if(!empty($provisionalVersion->getAlignmentPattern())){
50
			// Guess where a "bottom right" finder pattern would have been
51
			$bottomRightX = $topRight->getX() - $topLeft->getX() + $bottomLeft->getX();
52
			$bottomRightY = $topRight->getY() - $topLeft->getY() + $bottomLeft->getY();
53
54
			// Estimate that alignment pattern is closer by 3 modules
55
			// from "bottom right" to known top left location
56
			$correctionToTopLeft = 1.0 - 3.0 / (float)($provisionalVersion->getDimension() - 7);
57
			$estAlignmentX       = (int)($topLeft->getX() + $correctionToTopLeft * ($bottomRightX - $topLeft->getX()));
58
			$estAlignmentY       = (int)($topLeft->getY() + $correctionToTopLeft * ($bottomRightY - $topLeft->getY()));
59
60
			// Kind of arbitrary -- expand search radius before giving up
61
			for($i = 4; $i <= 16; $i <<= 1){//??????????
62
				$alignmentPattern = $this->findAlignmentInRegion($moduleSize, $estAlignmentX, $estAlignmentY, (float)$i);
63
64
				if($alignmentPattern !== null){
65
					break;
66
				}
67
			}
68
			// If we didn't find alignment pattern... well try anyway without it
69
		}
70
71
		$transform = $this->createTransform($topLeft, $topRight, $bottomLeft, $dimension, $alignmentPattern);
72
73
		return (new GridSampler)->sampleGrid($this->bitMatrix, $dimension, $transform);
74
	}
75
76
	/**
77
	 * <p>Computes an average estimated module size based on estimated derived from the positions
78
	 * of the three finder patterns.</p>
79
	 *
80
	 * @throws \RuntimeException
81
	 */
82
	private function calculateModuleSize(FinderPattern $topLeft, FinderPattern $topRight, FinderPattern $bottomLeft):float{
83
		// Take the average
84
		$moduleSize = (
85
			$this->calculateModuleSizeOneWay($topLeft, $topRight) +
86
			$this->calculateModuleSizeOneWay($topLeft, $bottomLeft)
87
		) / 2.0;
88
89
		if($moduleSize < 1.0){
90
			throw new RuntimeException('module size < 1.0');
91
		}
92
93
		return $moduleSize;
94
	}
95
96
	/**
97
	 * <p>Estimates module size based on two finder patterns -- it uses
98
	 * {@link #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int)} to figure the
99
	 * width of each, measuring along the axis between their centers.</p>
100
	 */
101
	private function calculateModuleSizeOneWay(FinderPattern $pattern, FinderPattern $otherPattern):float{
102
103
		$moduleSizeEst1 = $this->sizeOfBlackWhiteBlackRunBothWays(
104
			$pattern->getX(),
105
			$pattern->getY(),
106
			$otherPattern->getX(),
107
			$otherPattern->getY()
108
		);
109
110
		$moduleSizeEst2 = $this->sizeOfBlackWhiteBlackRunBothWays(
111
			$otherPattern->getX(),
112
			$otherPattern->getY(),
113
			$pattern->getX(),
114
			$pattern->getY()
115
		);
116
117
		if(is_nan($moduleSizeEst1)){
118
			return $moduleSizeEst2 / 7.0;
119
		}
120
121
		if(is_nan($moduleSizeEst2)){
122
			return $moduleSizeEst1 / 7.0;
123
		}
124
		// Average them, and divide by 7 since we've counted the width of 3 black modules,
125
		// and 1 white and 1 black module on either side. Ergo, divide sum by 14.
126
		return ($moduleSizeEst1 + $moduleSizeEst2) / 14.0;
127
	}
128
129
	/**
130
	 * See {@link #sizeOfBlackWhiteBlackRun(int, int, int, int)}; computes the total width of
131
	 * a finder pattern by looking for a black-white-black run from the center in the direction
132
	 * of another po$(another finder pattern center), and in the opposite direction too.</p>
133
	 */
134
	private function sizeOfBlackWhiteBlackRunBothWays(float $fromX, float $fromY, float $toX, float $toY):float{
135
		$result    = $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, (int)$toX, (int)$toY);
136
		$dimension = $this->bitMatrix->getDimension();
137
		// Now count other way -- don't run off image though of course
138
		$scale     = 1.0;
139
		$otherToX  = $fromX - ($toX - $fromX);
140
141
		if($otherToX < 0){
142
			$scale    = $fromX / ($fromX - $otherToX);
143
			$otherToX = 0;
144
		}
145
		elseif($otherToX >= $dimension){
146
			$scale    = ($dimension - 1 - $fromX) / ($otherToX - $fromX);
147
			$otherToX = $dimension - 1;
148
		}
149
150
		$otherToY = (int)($fromY - ($toY - $fromY) * $scale);
151
		$scale    = 1.0;
152
153
		if($otherToY < 0){
154
			$scale    = $fromY / ($fromY - $otherToY);
155
			$otherToY = 0;
156
		}
157
		elseif($otherToY >= $dimension){
158
			$scale    = ($dimension - 1 - $fromY) / ($otherToY - $fromY);
159
			$otherToY = $dimension - 1;
160
		}
161
162
		$otherToX = (int)($fromX + ($otherToX - $fromX) * $scale);
163
		$result   += $this->sizeOfBlackWhiteBlackRun((int)$fromX, (int)$fromY, (int)$otherToX, (int)$otherToY);
164
165
		// Middle pixel is double-counted this way; subtract 1
166
		return $result - 1.0;
167
	}
168
169
	/**
170
	 * <p>This method traces a line from a po$in the image, in the direction towards another point.
171
	 * It begins in a black region, and keeps going until it finds white, then black, then white again.
172
	 * It reports the distance from the start to this point.</p>
173
	 *
174
	 * <p>This is used when figuring out how wide a finder pattern is, when the finder pattern
175
	 * may be skewed or rotated.</p>
176
	 */
177
	private function sizeOfBlackWhiteBlackRun(int $fromX, int $fromY, int $toX, int $toY):float{
178
		// Mild variant of Bresenham's algorithm;
179
		// see http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
180
		$steep = abs($toY - $fromY) > abs($toX - $fromX);
181
182
		if($steep){
183
			$temp  = $fromX;
184
			$fromX = $fromY;
185
			$fromY = $temp;
186
			$temp  = $toX;
187
			$toX   = $toY;
188
			$toY   = $temp;
189
		}
190
191
		$dx    = abs($toX - $fromX);
192
		$dy    = abs($toY - $fromY);
193
		$error = -$dx / 2;
194
		$xstep = $fromX < $toX ? 1 : -1;
195
		$ystep = $fromY < $toY ? 1 : -1;
196
197
		// In black pixels, looking for white, first or second time.
198
		$state  = 0;
199
		// Loop up until x == toX, but not beyond
200
		$xLimit = $toX + $xstep;
201
202
		for($x = $fromX, $y = $fromY; $x !== $xLimit; $x += $xstep){
203
			$realX = $steep ? $y : $x;
204
			$realY = $steep ? $x : $y;
205
206
			// Does current pixel mean we have moved white to black or vice versa?
207
			// Scanning black in state 0,2 and white in state 1, so if we find the wrong
208
			// color, advance to next state or end if we are in state 2 already
209
			if(($state === 1) === $this->bitMatrix->get($realX, $realY)){
210
211
				if($state === 2){
212
					return FinderPattern::distance($x, $y, $fromX, $fromY);
213
				}
214
215
				$state++;
216
			}
217
218
			$error += $dy;
219
220
			if($error > 0){
221
222
				if($y === $toY){
223
					break;
224
				}
225
226
				$y     += $ystep;
227
				$error -= $dx;
228
			}
229
		}
230
231
		// Found black-white-black; give the benefit of the doubt that the next pixel outside the image
232
		// is "white" so this last po$at (toX+xStep,toY) is the right ending. This is really a
233
		// small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this.
234
		if($state === 2){
235
			return FinderPattern::distance($toX + $xstep, $toY, $fromX, $fromY);
236
		}
237
238
		// else we didn't find even black-white-black; no estimate is really possible
239
		return NAN;
240
	}
241
242
	/**
243
	 * <p>Computes the dimension (number of modules on a size) of the QR Code based on the position
244
	 * of the finder patterns and estimated module size.</p>
245
	 *
246
	 * @throws \RuntimeException
247
	 */
248
	private function computeDimension(
249
		FinderPattern $topLeft,
250
		FinderPattern $topRight,
251
		FinderPattern $bottomLeft,
252
		float $moduleSize
253
	):int{
254
		$tltrCentersDimension = (int)round($topLeft->getDistance($topRight) / $moduleSize);
255
		$tlblCentersDimension = (int)round($topLeft->getDistance($bottomLeft) / $moduleSize);
256
		$dimension            = (int)((($tltrCentersDimension + $tlblCentersDimension) / 2) + 7);
257
258
		switch($dimension % 4){
259
			case 0:
260
				$dimension++;
261
				break;
262
			// 1? do nothing
263
			case 2:
264
				$dimension--;
265
				break;
266
			case 3:
267
				throw new RuntimeException('estimated dimension: '.$dimension);
268
		}
269
270
		if($dimension % 4 !== 1){
271
			throw new RuntimeException('dimension mod 4 is not 1');
272
		}
273
274
		return $dimension;
275
	}
276
277
	/**
278
	 * <p>Attempts to locate an alignment pattern in a limited region of the image, which is
279
	 * guessed to contain it.</p>
280
	 *
281
	 * @param float $overallEstModuleSize estimated module size so far
282
	 * @param int   $estAlignmentX        x coordinate of center of area probably containing alignment pattern
283
	 * @param int   $estAlignmentY        y coordinate of above
284
	 * @param float $allowanceFactor      number of pixels in all directions to search from the center
285
	 *
286
	 * @return \chillerlan\QRCode\Detector\AlignmentPattern|null if found, or null otherwise
287
	 */
288
	private function findAlignmentInRegion(
289
		float $overallEstModuleSize,
290
		int $estAlignmentX,
291
		int $estAlignmentY,
292
		float $allowanceFactor
293
	):?AlignmentPattern{
294
		// Look for an alignment pattern (3 modules in size) around where it should be
295
		$dimension           = $this->bitMatrix->getDimension();
296
		$allowance           = (int)($allowanceFactor * $overallEstModuleSize);
297
		$alignmentAreaLeftX  = max(0, $estAlignmentX - $allowance);
298
		$alignmentAreaRightX = min($dimension - 1, $estAlignmentX + $allowance);
299
300
		if($alignmentAreaRightX - $alignmentAreaLeftX < $overallEstModuleSize * 3){
301
			return null;
302
		}
303
304
		$alignmentAreaTopY    = max(0, $estAlignmentY - $allowance);
305
		$alignmentAreaBottomY = min($dimension - 1, $estAlignmentY + $allowance);
306
307
		if($alignmentAreaBottomY - $alignmentAreaTopY < $overallEstModuleSize * 3){
308
			return null;
309
		}
310
311
		return (new AlignmentPatternFinder($this->bitMatrix, $overallEstModuleSize))->find(
312
			$alignmentAreaLeftX,
313
			$alignmentAreaTopY,
314
			$alignmentAreaRightX - $alignmentAreaLeftX,
315
			$alignmentAreaBottomY - $alignmentAreaTopY,
316
		);
317
	}
318
319
	/**
320
	 *
321
	 */
322
	private function createTransform(
323
		FinderPattern $topLeft,
324
		FinderPattern $topRight,
325
		FinderPattern $bottomLeft,
326
		int $dimension,
327
		AlignmentPattern $alignmentPattern = null
328
	):PerspectiveTransform{
329
		$dimMinusThree = (float)$dimension - 3.5;
330
331
		if($alignmentPattern instanceof AlignmentPattern){
332
			$bottomRightX       = $alignmentPattern->getX();
333
			$bottomRightY       = $alignmentPattern->getY();
334
			$sourceBottomRightX = $dimMinusThree - 3.0;
335
			$sourceBottomRightY = $sourceBottomRightX;
336
		}
337
		else{
338
			// Don't have an alignment pattern, just make up the bottom-right point
339
			$bottomRightX       = ($topRight->getX() - $topLeft->getX()) + $bottomLeft->getX();
340
			$bottomRightY       = ($topRight->getY() - $topLeft->getY()) + $bottomLeft->getY();
341
			$sourceBottomRightX = $dimMinusThree;
342
			$sourceBottomRightY = $dimMinusThree;
343
		}
344
345
		return (new PerspectiveTransform)->quadrilateralToQuadrilateral(
346
			3.5, 3.5,
347
			$dimMinusThree, 3.5,
348
			$sourceBottomRightX, $sourceBottomRightY,
349
			3.5, $dimMinusThree,
350
			$topLeft->getX(), $topLeft->getY(),
351
			$topRight->getX(), $topRight->getY(),
352
			$bottomRightX, $bottomRightY,
353
			$bottomLeft->getX(), $bottomLeft->getY()
354
		);
355
	}
356
357
}
358