AlignmentPatternFinder.php 11 KB
Newer Older
Ketan's avatar
Ketan committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
<?php
/*
 * Copyright 2007 ZXing authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace Zxing\Qrcode\Detector;

use Zxing\NotFoundException;
use Zxing\ResultPointCallback;
use Zxing\Common\BitMatrix;

/**
 * <p>This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder
 * patterns but are smaller and appear at regular intervals throughout the image.</p>
 *
 * <p>At the moment this only looks for the bottom-right alignment pattern.</p>
 *
 * <p>This is mostly a simplified copy of {@link FinderPatternFinder}. It is copied,
 * pasted and stripped down here for maximum performance but does unfortunately duplicate
 * some code.</p>
 *
 * <p>This class is thread-safe but not reentrant. Each thread must allocate its own object.</p>
 *
 * @author Sean Owen
 */
final class AlignmentPatternFinder
{
    private $image;
    private $possibleCenters;
    private $startX;
    private $startY;
    private $width;
    private $height;
    private $moduleSize;
    private $crossCheckStateCount;
    private $resultPointCallback;

    /**
     * <p>Creates a finder that will look in a portion of the whole image.</p>
     *
     * @param image      image to search
     * @param startX     left column from which to start searching
     * @param startY     top row from which to start searching
     * @param width      width of region to search
     * @param height     height of region to search
     * @param moduleSize estimated module size so far
     */
    public function __construct($image,
                                $startX,
                                $startY,
                                $width,
                                $height,
                                $moduleSize,
                                $resultPointCallback)
    {
        $this->image                = $image;
        $this->possibleCenters      = [];
        $this->startX               = $startX;
        $this->startY               = $startY;
        $this->width                = $width;
        $this->height               = $height;
        $this->moduleSize           = $moduleSize;
        $this->crossCheckStateCount = [];
        $this->resultPointCallback  = $resultPointCallback;
    }

    /**
     * <p>This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since
     * it's pretty performance-critical and so is written to be fast foremost.</p>
     *
     * @return {@link AlignmentPattern} if found
     * @throws NotFoundException if not found
     */
    public function find()
    {
        $startX  = $this->startX;
        $height  = $this->height;
        $maxJ    = $startX + $this->width;
        $middleI = $this->startY + ($height / 2);
        // We are looking for black/white/black modules in 1:1:1 ratio;
        // this tracks the number of black/white/black modules seen so far
        $stateCount = [];
        for ($iGen = 0; $iGen < $height; $iGen++) {
            // Search from middle outwards
            $i             = $middleI + (($iGen & 0x01) == 0 ? ($iGen + 1) / 2 : -(($iGen + 1) / 2));
            $i             = (int)($i);
            $stateCount[0] = 0;
            $stateCount[1] = 0;
            $stateCount[2] = 0;
            $j             = $startX;
            // Burn off leading white pixels before anything else; if we start in the middle of
            // a white run, it doesn't make sense to count its length, since we don't know if the
            // white run continued to the left of the start point
            while ($j < $maxJ && !$this->image->get($j, $i)) {
                $j++;
            }
            $currentState = 0;
            while ($j < $maxJ) {
                if ($this->image->get($j, $i)) {
                    // Black pixel
                    if ($currentState == 1) { // Counting black pixels
                        $stateCount[$currentState]++;
                    } else { // Counting white pixels
                        if ($currentState == 2) { // A winner?
                            if ($this->foundPatternCross($stateCount)) { // Yes
                                $confirmed = $this->handlePossibleCenter($stateCount, $i, $j);
                                if ($confirmed != null) {
                                    return $confirmed;
                                }
                            }
                            $stateCount[0] = $stateCount[2];
                            $stateCount[1] = 1;
                            $stateCount[2] = 0;
                            $currentState  = 1;
                        } else {
                            $stateCount[++$currentState]++;
                        }
                    }
                } else { // White pixel
                    if ($currentState == 1) { // Counting black pixels
                        $currentState++;
                    }
                    $stateCount[$currentState]++;
                }
                $j++;
            }
            if ($this->foundPatternCross($stateCount)) {
                $confirmed = $this->handlePossibleCenter($stateCount, $i, $maxJ);
                if ($confirmed != null) {
                    return $confirmed;
                }
            }

        }

        // Hmm, nothing we saw was observed and confirmed twice. If we had
        // any guess at all, return it.
        if (count($this->possibleCenters)) {
            return $this->possibleCenters[0];
        }

        throw  NotFoundException::getNotFoundInstance();
    }

    /**
     * @param stateCount count of black/white/black pixels just read
     *
     * @return true iff the proportions of the counts is close enough to the 1/1/1 ratios
     *         used by alignment patterns to be considered a match
     */
    private function foundPatternCross($stateCount)
    {
        $moduleSize  = $this->moduleSize;
        $maxVariance = $moduleSize / 2.0;
        for ($i = 0; $i < 3; $i++) {
            if (abs($moduleSize - $stateCount[$i]) >= $maxVariance) {
                return false;
            }
        }

        return true;
    }

    /**
     * <p>This is called when a horizontal scan finds a possible alignment pattern. It will
     * cross check with a vertical scan, and if successful, will see if this pattern had been
     * found on a previous horizontal scan. If so, we consider it confirmed and conclude we have
     * found the alignment pattern.</p>
     *
     * @param stateCount reading state module counts from horizontal scan
     * @param i          row where alignment pattern may be found
     * @param j          end of possible alignment pattern in row
     *
     * @return {@link AlignmentPattern} if we have found the same pattern twice, or null if not
     */
    private function handlePossibleCenter($stateCount, $i, $j)
    {
        $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2];
        $centerJ         = $this->centerFromEnd($stateCount, $j);
        $centerI         = $this->crossCheckVertical($i, (int)$centerJ, 2 * $stateCount[1], $stateCountTotal);
        if (!is_nan($centerI)) {
            $estimatedModuleSize = (float)($stateCount[0] + $stateCount[1] + $stateCount[2]) / 3.0;
            foreach ($this->possibleCenters as $center) {
                // Look for about the same center and module size:
                if ($center->aboutEquals($estimatedModuleSize, $centerI, $centerJ)) {
                    return $center->combineEstimate($centerI, $centerJ, $estimatedModuleSize);
                }
            }
            // Hadn't found this before; save it
            $point                   = new AlignmentPattern($centerJ, $centerI, $estimatedModuleSize);
            $this->possibleCenters[] = $point;
            if ($this->resultPointCallback != null) {
                $this->resultPointCallback->foundPossibleResultPoint($point);
            }
        }

        return null;
    }

    /**
     * Given a count of black/white/black pixels just seen and an end position,
     * figures the location of the center of this black/white/black run.
     */
    private static function centerFromEnd($stateCount, $end)
    {
        return (float)($end - $stateCount[2]) - $stateCount[1] / 2.0;
    }

    /**
     * <p>After a horizontal scan finds a potential alignment pattern, this method
     * "cross-checks" by scanning down vertically through the center of the possible
     * alignment pattern to see if the same proportion is detected.</p>
     *
     * @param startI   row where an alignment pattern was detected
     * @param centerJ  center of the section that appears to cross an alignment pattern
     * @param maxCount maximum reasonable number of modules that should be
     *                 observed in any reading state, based on the results of the horizontal scan
     *
     * @return vertical center of alignment pattern, or {@link Float#NaN} if not found
     */
    private function crossCheckVertical($startI, $centerJ, $maxCount,
                                        $originalStateCountTotal)
    {
        $image = $this->image;

        $maxI          = $image->getHeight();
        $stateCount    = $this->crossCheckStateCount;
        $stateCount[0] = 0;
        $stateCount[1] = 0;
        $stateCount[2] = 0;

        // Start counting up from center
        $i = $startI;
        while ($i >= 0 && $image->get($centerJ, $i) && $stateCount[1] <= $maxCount) {
            $stateCount[1]++;
            $i--;
        }
        // If already too many modules in this state or ran off the edge:
        if ($i < 0 || $stateCount[1] > $maxCount) {
            return NAN;
        }
        while ($i >= 0 && !$image->get($centerJ, $i) && $stateCount[0] <= $maxCount) {
            $stateCount[0]++;
            $i--;
        }
        if ($stateCount[0] > $maxCount) {
            return NAN;
        }

        // Now also count down from center
        $i = $startI + 1;
        while ($i < $maxI && $image->get($centerJ, $i) && $stateCount[1] <= $maxCount) {
            $stateCount[1]++;
            $i++;
        }
        if ($i == $maxI || $stateCount[1] > $maxCount) {
            return NAN;
        }
        while ($i < $maxI && !$image->get($centerJ, $i) && $stateCount[2] <= $maxCount) {
            $stateCount[2]++;
            $i++;
        }
        if ($stateCount[2] > $maxCount) {
            return NAN;
        }

        $stateCountTotal = $stateCount[0] + $stateCount[1] + $stateCount[2];
        if (5 * abs($stateCountTotal - $originalStateCountTotal) >= 2 * $originalStateCountTotal) {
            return NAN;
        }

        return $this->foundPatternCross($stateCount) ? $this->centerFromEnd($stateCount, $i) : NAN;
    }
}