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