Mercurial > hgrepos > Python2 > PyMuPDF
comparison mupdf-source/thirdparty/zxing-cpp/core/src/HybridBinarizer.cpp @ 2:b50eed0cc0ef upstream
ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4.
The directory name has changed: no version number in the expanded directory now.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Mon, 15 Sep 2025 11:43:07 +0200 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 1:1d09e1dec1d9 | 2:b50eed0cc0ef |
|---|---|
| 1 /* | |
| 2 * Copyright 2016 Nu-book Inc. | |
| 3 * Copyright 2016 ZXing authors | |
| 4 */ | |
| 5 // SPDX-License-Identifier: Apache-2.0 | |
| 6 | |
| 7 #include "HybridBinarizer.h" | |
| 8 | |
| 9 #include "BitMatrix.h" | |
| 10 #include "Matrix.h" | |
| 11 | |
| 12 #include <algorithm> | |
| 13 #include <cstdint> | |
| 14 #include <fstream> | |
| 15 #include <memory> | |
| 16 | |
| 17 #define USE_NEW_ALGORITHM | |
| 18 | |
| 19 namespace ZXing { | |
| 20 | |
| 21 // This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels. | |
| 22 // So this is the smallest dimension in each axis we can accept. | |
| 23 static constexpr int BLOCK_SIZE = 8; | |
| 24 static constexpr int WINDOW_SIZE = BLOCK_SIZE * (1 + 2 * 2); | |
| 25 static constexpr int MIN_DYNAMIC_RANGE = 24; | |
| 26 | |
| 27 HybridBinarizer::HybridBinarizer(const ImageView& iv) : GlobalHistogramBinarizer(iv) {} | |
| 28 | |
| 29 HybridBinarizer::~HybridBinarizer() = default; | |
| 30 | |
| 31 bool HybridBinarizer::getPatternRow(int row, int rotation, PatternRow& res) const | |
| 32 { | |
| 33 #if 1 | |
| 34 // This is the original "hybrid" behavior: use GlobalHistogram for the 1D case | |
| 35 return GlobalHistogramBinarizer::getPatternRow(row, rotation, res); | |
| 36 #else | |
| 37 // This is an alternative that can be faster in general and perform better in unevenly lit sitations like | |
| 38 // https://github.com/zxing-cpp/zxing-cpp/blob/master/test/samples/ean13-2/21.png. That said, it fairs | |
| 39 // worse in borderline low resolution situations. With the current black box sample set we'd loose 94 | |
| 40 // test cases while gaining 53 others. | |
| 41 auto bits = getBitMatrix(); | |
| 42 if (bits) | |
| 43 GetPatternRow(*bits, row, res, rotation % 180 != 0); | |
| 44 return bits != nullptr; | |
| 45 #endif | |
| 46 } | |
| 47 | |
| 48 using T_t = uint8_t; | |
| 49 | |
| 50 /** | |
| 51 * Applies a single threshold to a block of pixels. | |
| 52 */ | |
| 53 static void ThresholdBlock(const uint8_t* __restrict luminances, int xoffset, int yoffset, T_t threshold, int rowStride, | |
| 54 BitMatrix& matrix) | |
| 55 { | |
| 56 for (int y = yoffset; y < yoffset + BLOCK_SIZE; ++y) { | |
| 57 auto* src = luminances + y * rowStride + xoffset; | |
| 58 auto* const dstBegin = matrix.row(y).begin() + xoffset; | |
| 59 // TODO: fix pixelStride > 1 case | |
| 60 for (auto* dst = dstBegin; dst < dstBegin + BLOCK_SIZE; ++dst, ++src) | |
| 61 *dst = (*src <= threshold) * BitMatrix::SET_V; | |
| 62 } | |
| 63 } | |
| 64 | |
| 65 #ifndef USE_NEW_ALGORITHM | |
| 66 | |
| 67 /** | |
| 68 * Calculates a single black point for each block of pixels and saves it away. | |
| 69 * See the following thread for a discussion of this algorithm: | |
| 70 * http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0 | |
| 71 */ | |
| 72 static Matrix<T_t> CalculateBlackPoints(const uint8_t* __restrict luminances, int subWidth, int subHeight, int width, int height, | |
| 73 int rowStride) | |
| 74 { | |
| 75 Matrix<T_t> blackPoints(subWidth, subHeight); | |
| 76 | |
| 77 for (int y = 0; y < subHeight; y++) { | |
| 78 int yoffset = std::min(y * BLOCK_SIZE, height - BLOCK_SIZE); | |
| 79 for (int x = 0; x < subWidth; x++) { | |
| 80 int xoffset = std::min(x * BLOCK_SIZE, width - BLOCK_SIZE); | |
| 81 int sum = 0; | |
| 82 uint8_t min = luminances[yoffset * rowStride + xoffset]; | |
| 83 uint8_t max = min; | |
| 84 for (int yy = 0, offset = yoffset * rowStride + xoffset; yy < BLOCK_SIZE; yy++, offset += rowStride) { | |
| 85 for (int xx = 0; xx < BLOCK_SIZE; xx++) { | |
| 86 auto pixel = luminances[offset + xx]; | |
| 87 sum += pixel; | |
| 88 if (pixel < min) | |
| 89 min = pixel; | |
| 90 if (pixel > max) | |
| 91 max = pixel; | |
| 92 } | |
| 93 // short-circuit min/max tests once dynamic range is met | |
| 94 if (max - min > MIN_DYNAMIC_RANGE) { | |
| 95 // finish the rest of the rows quickly | |
| 96 for (yy++, offset += rowStride; yy < BLOCK_SIZE; yy++, offset += rowStride) { | |
| 97 for (int xx = 0; xx < BLOCK_SIZE; xx++) { | |
| 98 sum += luminances[offset + xx]; | |
| 99 } | |
| 100 } | |
| 101 } | |
| 102 } | |
| 103 | |
| 104 // The default estimate is the average of the values in the block. | |
| 105 int average = sum / (BLOCK_SIZE * BLOCK_SIZE); | |
| 106 if (max - min <= MIN_DYNAMIC_RANGE) { | |
| 107 // If variation within the block is low, assume this is a block with only light or only | |
| 108 // dark pixels. In that case we do not want to use the average, as it would divide this | |
| 109 // low contrast area into black and white pixels, essentially creating data out of noise. | |
| 110 // | |
| 111 // The default assumption is that the block is light/background. Since no estimate for | |
| 112 // the level of dark pixels exists locally, use half the min for the block. | |
| 113 average = min / 2; | |
| 114 | |
| 115 if (y > 0 && x > 0) { | |
| 116 // Correct the "white background" assumption for blocks that have neighbors by comparing | |
| 117 // the pixels in this block to the previously calculated black points. This is based on | |
| 118 // the fact that dark barcode symbology is always surrounded by some amount of light | |
| 119 // background for which reasonable black point estimates were made. The bp estimated at | |
| 120 // the boundaries is used for the interior. | |
| 121 | |
| 122 // The (min < bp) is arbitrary but works better than other heuristics that were tried. | |
| 123 int averageNeighborBlackPoint = | |
| 124 (blackPoints(x, y - 1) + (2 * blackPoints(x - 1, y)) + blackPoints(x - 1, y - 1)) / 4; | |
| 125 if (min < averageNeighborBlackPoint) { | |
| 126 average = averageNeighborBlackPoint; | |
| 127 } | |
| 128 } | |
| 129 } | |
| 130 blackPoints(x, y) = average; | |
| 131 } | |
| 132 } | |
| 133 return blackPoints; | |
| 134 } | |
| 135 | |
| 136 /** | |
| 137 * For each block in the image, calculate the average black point using a 5x5 grid | |
| 138 * of the blocks around it. Also handles the corner cases (fractional blocks are computed based | |
| 139 * on the last pixels in the row/column which are also used in the previous block). | |
| 140 */ | |
| 141 static std::shared_ptr<BitMatrix> CalculateMatrix(const uint8_t* __restrict luminances, int subWidth, int subHeight, int width, | |
| 142 int height, int rowStride, const Matrix<T_t>& blackPoints) | |
| 143 { | |
| 144 auto matrix = std::make_shared<BitMatrix>(width, height); | |
| 145 | |
| 146 #ifdef PRINT_DEBUG | |
| 147 Matrix<uint8_t> out(width, height); | |
| 148 Matrix<uint8_t> out2(width, height); | |
| 149 #endif | |
| 150 | |
| 151 for (int y = 0; y < subHeight; y++) { | |
| 152 int yoffset = std::min(y * BLOCK_SIZE, height - BLOCK_SIZE); | |
| 153 for (int x = 0; x < subWidth; x++) { | |
| 154 int xoffset = std::min(x * BLOCK_SIZE, width - BLOCK_SIZE); | |
| 155 int left = std::clamp(x, 2, subWidth - 3); | |
| 156 int top = std::clamp(y, 2, subHeight - 3); | |
| 157 int sum = 0; | |
| 158 for (int dy = -2; dy <= 2; ++dy) { | |
| 159 for (int dx = -2; dx <= 2; ++dx) { | |
| 160 sum += blackPoints(left + dx, top + dy); | |
| 161 } | |
| 162 } | |
| 163 int average = sum / 25; | |
| 164 ThresholdBlock(luminances, xoffset, yoffset, average, rowStride, *matrix); | |
| 165 | |
| 166 #ifdef PRINT_DEBUG | |
| 167 for (int yy = 0; yy < 8; ++yy) | |
| 168 for (int xx = 0; xx < 8; ++xx) { | |
| 169 out.set(xoffset + xx, yoffset + yy, blackPoints(x, y)); | |
| 170 out2.set(xoffset + xx, yoffset + yy, average); | |
| 171 } | |
| 172 #endif | |
| 173 } | |
| 174 } | |
| 175 | |
| 176 #ifdef PRINT_DEBUG | |
| 177 std::ofstream file("thresholds.pnm"); | |
| 178 file << "P5\n" << out.width() << ' ' << out.height() << "\n255\n"; | |
| 179 file.write(reinterpret_cast<const char*>(out.data()), out.size()); | |
| 180 std::ofstream file2("thresholds_avg.pnm"); | |
| 181 file2 << "P5\n" << out.width() << ' ' << out.height() << "\n255\n"; | |
| 182 file2.write(reinterpret_cast<const char*>(out2.data()), out2.size()); | |
| 183 #endif | |
| 184 | |
| 185 return matrix; | |
| 186 } | |
| 187 | |
| 188 #else | |
| 189 | |
| 190 // Subdivide the image in blocks of BLOCK_SIZE and calculate one treshold value per block as | |
| 191 // (max - min > MIN_DYNAMIC_RANGE) ? (max + min) / 2 : 0 | |
| 192 static Matrix<T_t> BlockThresholds(const ImageView iv) | |
| 193 { | |
| 194 int subWidth = (iv.width() + BLOCK_SIZE - 1) / BLOCK_SIZE; // ceil(width/BS) | |
| 195 int subHeight = (iv.height() + BLOCK_SIZE - 1) / BLOCK_SIZE; // ceil(height/BS) | |
| 196 | |
| 197 Matrix<T_t> thresholds(subWidth, subHeight); | |
| 198 | |
| 199 for (int y = 0; y < subHeight; y++) { | |
| 200 int y0 = std::min(y * BLOCK_SIZE, iv.height() - BLOCK_SIZE); | |
| 201 for (int x = 0; x < subWidth; x++) { | |
| 202 int x0 = std::min(x * BLOCK_SIZE, iv.width() - BLOCK_SIZE); | |
| 203 uint8_t min = 255; | |
| 204 uint8_t max = 0; | |
| 205 for (int yy = 0; yy < BLOCK_SIZE; yy++) { | |
| 206 auto line = iv.data(x0, y0 + yy); | |
| 207 for (int xx = 0; xx < BLOCK_SIZE; xx++) | |
| 208 UpdateMinMax(min, max, line[xx]); | |
| 209 } | |
| 210 | |
| 211 thresholds(x, y) = (max - min > MIN_DYNAMIC_RANGE) ? (int(max) + min) / 2 : 0; | |
| 212 } | |
| 213 } | |
| 214 | |
| 215 return thresholds; | |
| 216 } | |
| 217 | |
| 218 // Apply gaussian-like smoothing filter over all non-zero thresholds and fill any remainig gaps with nearest neighbor | |
| 219 static Matrix<T_t> SmoothThresholds(Matrix<T_t>&& in) | |
| 220 { | |
| 221 Matrix<T_t> out(in.width(), in.height()); | |
| 222 | |
| 223 constexpr int R = WINDOW_SIZE / BLOCK_SIZE / 2; | |
| 224 for (int y = 0; y < in.height(); y++) { | |
| 225 for (int x = 0; x < in.width(); x++) { | |
| 226 int left = std::clamp(x, R, in.width() - R - 1); | |
| 227 int top = std::clamp(y, R, in.height() - R - 1); | |
| 228 | |
| 229 int sum = in(x, y) * 2; | |
| 230 int n = (sum > 0) * 2; | |
| 231 auto add = [&](int x, int y) { | |
| 232 int t = in(x, y); | |
| 233 sum += t; | |
| 234 n += t > 0; | |
| 235 }; | |
| 236 | |
| 237 for (int dy = -R; dy <= R; ++dy) | |
| 238 for (int dx = -R; dx <= R; ++dx) | |
| 239 add(left + dx, top + dy); | |
| 240 | |
| 241 out(x, y) = n > 0 ? sum / n : 0; | |
| 242 } | |
| 243 } | |
| 244 | |
| 245 // flood fill any remaing gaps of (very large) no-contrast regions | |
| 246 auto last = out.begin() - 1; | |
| 247 for (auto* i = out.begin(); i != out.end(); ++i) { | |
| 248 if (*i) { | |
| 249 if (last != i - 1) | |
| 250 std::fill(last + 1, i, *i); | |
| 251 last = i; | |
| 252 } | |
| 253 } | |
| 254 std::fill(last + 1, out.end(), *(std::max(last, out.begin()))); | |
| 255 | |
| 256 return out; | |
| 257 } | |
| 258 | |
| 259 static std::shared_ptr<BitMatrix> ThresholdImage(const ImageView iv, const Matrix<T_t>& thresholds) | |
| 260 { | |
| 261 auto matrix = std::make_shared<BitMatrix>(iv.width(), iv.height()); | |
| 262 | |
| 263 #ifdef PRINT_DEBUG | |
| 264 Matrix<uint8_t> out(iv.width(), iv.height()); | |
| 265 #endif | |
| 266 | |
| 267 for (int y = 0; y < thresholds.height(); y++) { | |
| 268 int yoffset = std::min(y * BLOCK_SIZE, iv.height() - BLOCK_SIZE); | |
| 269 for (int x = 0; x < thresholds.width(); x++) { | |
| 270 int xoffset = std::min(x * BLOCK_SIZE, iv.width() - BLOCK_SIZE); | |
| 271 ThresholdBlock(iv.data(), xoffset, yoffset, thresholds(x, y), iv.rowStride(), *matrix); | |
| 272 | |
| 273 #ifdef PRINT_DEBUG | |
| 274 for (int yy = 0; yy < 8; ++yy) | |
| 275 for (int xx = 0; xx < 8; ++xx) | |
| 276 out.set(xoffset + xx, yoffset + yy, thresholds(x, y)); | |
| 277 #endif | |
| 278 } | |
| 279 } | |
| 280 | |
| 281 #ifdef PRINT_DEBUG | |
| 282 std::ofstream file("thresholds_new.pnm"); | |
| 283 file << "P5\n" << out.width() << ' ' << out.height() << "\n255\n"; | |
| 284 file.write(reinterpret_cast<const char*>(out.data()), out.size()); | |
| 285 #endif | |
| 286 | |
| 287 return matrix; | |
| 288 } | |
| 289 | |
| 290 #endif | |
| 291 | |
| 292 std::shared_ptr<const BitMatrix> HybridBinarizer::getBlackMatrix() const | |
| 293 { | |
| 294 if (width() >= WINDOW_SIZE && height() >= WINDOW_SIZE) { | |
| 295 #ifdef USE_NEW_ALGORITHM | |
| 296 auto thrs = SmoothThresholds(BlockThresholds(_buffer)); | |
| 297 return ThresholdImage(_buffer, thrs); | |
| 298 #else | |
| 299 const uint8_t* luminances = _buffer.data(); | |
| 300 int subWidth = (width() + BLOCK_SIZE - 1) / BLOCK_SIZE; // ceil(width/BS) | |
| 301 int subHeight = (height() + BLOCK_SIZE - 1) / BLOCK_SIZE; // ceil(height/BS) | |
| 302 auto blackPoints = | |
| 303 CalculateBlackPoints(luminances, subWidth, subHeight, width(), height(), _buffer.rowStride()); | |
| 304 | |
| 305 return CalculateMatrix(luminances, subWidth, subHeight, width(), height(), _buffer.rowStride(), blackPoints); | |
| 306 #endif | |
| 307 } else { | |
| 308 // If the image is too small, fall back to the global histogram approach. | |
| 309 return GlobalHistogramBinarizer::getBlackMatrix(); | |
| 310 } | |
| 311 } | |
| 312 | |
| 313 } // ZXing |
