comparison mupdf-source/thirdparty/zxing-cpp/core/src/ConcentricFinder.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 2020 Axel Waggershauser
3 */
4 // SPDX-License-Identifier: Apache-2.0
5
6 #include "ConcentricFinder.h"
7
8 #include "LogMatrix.h"
9 #include "RegressionLine.h"
10 #include "ZXAlgorithms.h"
11
12 namespace ZXing {
13
14 std::optional<PointF> AverageEdgePixels(BitMatrixCursorI cur, int range, int numOfEdges)
15 {
16 PointF sum = {};
17 for (int i = 0; i < numOfEdges; ++i) {
18 if (!cur.isIn())
19 return {};
20 cur.stepToEdge(1, range);
21 sum += centered(cur.p) + centered(cur.p + cur.back());
22 log(cur.p + cur.back(), 2);
23 }
24 return sum / (2 * numOfEdges);
25 }
26
27 std::optional<PointF> CenterOfDoubleCross(const BitMatrix& image, PointI center, int range, int numOfEdges)
28 {
29 PointF sum = {};
30 for (auto d : {PointI{0, 1}, {1, 0}, {1, 1}, {1, -1}}) {
31 auto avr1 = AverageEdgePixels({image, center, d}, range, numOfEdges);
32 auto avr2 = AverageEdgePixels({image, center, -d}, range, numOfEdges);
33 if (!avr1 || !avr2)
34 return {};
35 sum += *avr1 + *avr2;
36 }
37 return sum / 8;
38 }
39
40 std::optional<PointF> CenterOfRing(const BitMatrix& image, PointI center, int range, int nth, bool requireCircle)
41 {
42 #if 0
43 if (requireCircle) {
44 // alternative implementation with the aim of discarding closed loops that are not all circle like (M > 5*m)
45 auto points = CollectRingPoints(image, center, range, std::abs(nth), nth < 0);
46 if (points.empty())
47 return {};
48 auto res = Reduce(points, PointF{}, std::plus{}) / Size(points);
49
50 double m = range, M = 0;
51 for (auto p : points)
52 UpdateMinMax(m, M, maxAbsComponent(p - res));
53
54 if (M > 5 * m)
55 return {};
56
57 return res;
58 }
59 #endif
60 // range is the approximate width/height of the nth ring, if nth>1 then it would be plausible to limit the search radius
61 // to approximately range / 2 * sqrt(2) == range * 0.75 but it turned out to be too limiting with realworld/noisy data.
62 int radius = range;
63 bool inner = nth < 0;
64 nth = std::abs(nth);
65 log(center, 3);
66 BitMatrixCursorI cur(image, center, {0, 1});
67 if (!cur.stepToEdge(nth, radius, inner))
68 return {};
69 cur.turnRight(); // move clock wise and keep edge on the right/left depending on backup
70 const auto edgeDir = inner ? Direction::LEFT : Direction::RIGHT;
71
72 uint32_t neighbourMask = 0;
73 auto start = cur.p;
74 PointF sum = {};
75 int n = 0;
76 do {
77 log(cur.p, 4);
78 sum += centered(cur.p);
79 ++n;
80
81 // find out if we come full circle around the center. 8 bits have to be set in the end.
82 neighbourMask |= (1 << (4 + dot(bresenhamDirection(cur.p - center), PointI(1, 3))));
83
84 if (!cur.stepAlongEdge(edgeDir))
85 return {};
86
87 // use L-inf norm, simply because it is a lot faster than L2-norm and sufficiently accurate
88 if (maxAbsComponent(cur.p - center) > radius || center == cur.p || n > 4 * 2 * range)
89 return {};
90 } while (cur.p != start);
91
92 if (requireCircle && neighbourMask != 0b111101111)
93 return {};
94
95 return sum / n;
96 }
97
98 std::optional<PointF> CenterOfRings(const BitMatrix& image, PointF center, int range, int numOfRings)
99 {
100 int n = 1;
101 PointF sum = center;
102 for (int i = 2; i < numOfRings + 1; ++i) {
103 auto c = CenterOfRing(image, PointI(center), range, i);
104 if (!c) {
105 if (n == 1)
106 return {};
107 else
108 return sum / n;
109 } else if (distance(*c, center) > range / numOfRings / 2) {
110 return {};
111 }
112
113 sum += *c;
114 n++;
115 }
116 return sum / n;
117 }
118
119 static std::vector<PointF> CollectRingPoints(const BitMatrix& image, PointF center, int range, int edgeIndex, bool backup)
120 {
121 PointI centerI(center);
122 int radius = range;
123 BitMatrixCursorI cur(image, centerI, {0, 1});
124 if (!cur.stepToEdge(edgeIndex, radius, backup))
125 return {};
126 cur.turnRight(); // move clock wise and keep edge on the right/left depending on backup
127 const auto edgeDir = backup ? Direction::LEFT : Direction::RIGHT;
128
129 uint32_t neighbourMask = 0;
130 auto start = cur.p;
131 std::vector<PointF> points;
132 points.reserve(4 * range);
133
134 do {
135 log(cur.p, 4);
136 points.push_back(centered(cur.p));
137
138 // find out if we come full circle around the center. 8 bits have to be set in the end.
139 neighbourMask |= (1 << (4 + dot(bresenhamDirection(cur.p - centerI), PointI(1, 3))));
140
141 if (!cur.stepAlongEdge(edgeDir))
142 return {};
143
144 // use L-inf norm, simply because it is a lot faster than L2-norm and sufficiently accurate
145 if (maxAbsComponent(cur.p - centerI) > radius || centerI == cur.p || Size(points) > 4 * 2 * range)
146 return {};
147
148 } while (cur.p != start);
149
150 if (neighbourMask != 0b111101111)
151 return {};
152
153 return points;
154 }
155
156 static std::optional<QuadrilateralF> FitQadrilateralToPoints(PointF center, std::vector<PointF>& points)
157 {
158 auto dist2Center = [c = center](auto a, auto b) { return distance(a, c) < distance(b, c); };
159 // rotate points such that the first one is the furthest away from the center (hence, a corner)
160 std::rotate(points.begin(), std::max_element(points.begin(), points.end(), dist2Center), points.end());
161
162 std::array<const PointF*, 4> corners;
163 corners[0] = &points[0];
164 // find the oposite corner by looking for the farthest point near the oposite point
165 corners[2] = std::max_element(&points[Size(points) * 3 / 8], &points[Size(points) * 5 / 8], dist2Center);
166
167 // find the two in between corners by looking for the points farthest from the long diagonal
168 auto dist2Diagonal = [l = RegressionLine(*corners[0], *corners[2])](auto a, auto b) { return l.distance(a) < l.distance(b); };
169 corners[1] = std::max_element(&points[Size(points) * 1 / 8], &points[Size(points) * 3 / 8], dist2Diagonal);
170 corners[3] = std::max_element(&points[Size(points) * 5 / 8], &points[Size(points) * 7 / 8], dist2Diagonal);
171
172 std::array lines{RegressionLine{corners[0] + 1, corners[1]}, RegressionLine{corners[1] + 1, corners[2]},
173 RegressionLine{corners[2] + 1, corners[3]}, RegressionLine{corners[3] + 1, &points.back() + 1}};
174
175 if (std::any_of(lines.begin(), lines.end(), [](auto line) { return !line.isValid(); }))
176 return {};
177
178 std::array<const PointF*, 4> beg = {corners[0] + 1, corners[1] + 1, corners[2] + 1, corners[3] + 1};
179 std::array<const PointF*, 4> end = {corners[1], corners[2], corners[3], &points.back() + 1};
180
181 // check if all points belonging to each line segment are sufficiently close to that line
182 for (int i = 0; i < 4; ++i)
183 for (const PointF* p = beg[i]; p != end[i]; ++p) {
184 auto len = std::distance(beg[i], end[i]);
185 if (len > 3 && lines[i].distance(*p) > std::max(1., std::min(8., len / 8.))) {
186 #ifdef PRINT_DEBUG
187 printf("%d: %.2f > %.2f @ %.fx%.f\n", i, lines[i].distance(*p), std::distance(beg[i], end[i]) / 1., p->x, p->y);
188 #endif
189 return {};
190 }
191 }
192
193 QuadrilateralF res;
194 for (int i = 0; i < 4; ++i)
195 res[i] = intersect(lines[i], lines[(i + 1) % 4]);
196
197 return res;
198 }
199
200 static bool QuadrilateralIsPlausibleSquare(const QuadrilateralF q, int lineIndex)
201 {
202 double m, M;
203 m = M = distance(q[0], q[3]);
204 for (int i = 1; i < 4; ++i)
205 UpdateMinMax(m, M, distance(q[i - 1], q[i]));
206
207 return m >= lineIndex * 2 && m > M / 3;
208 }
209
210 static std::optional<QuadrilateralF> FitSquareToPoints(const BitMatrix& image, PointF center, int range, int lineIndex, bool backup)
211 {
212 auto points = CollectRingPoints(image, center, range, lineIndex, backup);
213 if (points.empty())
214 return {};
215
216 auto res = FitQadrilateralToPoints(center, points);
217 if (!res || !QuadrilateralIsPlausibleSquare(*res, lineIndex - backup))
218 return {};
219
220 return res;
221 }
222
223 std::optional<QuadrilateralF> FindConcentricPatternCorners(const BitMatrix& image, PointF center, int range, int lineIndex)
224 {
225 auto innerCorners = FitSquareToPoints(image, center, range, lineIndex, false);
226 if (!innerCorners)
227 return {};
228
229 auto outerCorners = FitSquareToPoints(image, center, range, lineIndex + 1, true);
230 if (!outerCorners)
231 return {};
232
233 auto res = Blend(*innerCorners, *outerCorners);
234
235 for (auto p : *innerCorners)
236 log(p, 3);
237
238 for (auto p : *outerCorners)
239 log(p, 3);
240
241 for (auto p : res)
242 log(p, 3);
243
244 return res;
245 }
246
247 std::optional<PointF> FinetuneConcentricPatternCenter(const BitMatrix& image, PointF center, int range, int finderPatternSize)
248 {
249 // make sure we have at least one path of white around the center
250 if (auto res1 = CenterOfRing(image, PointI(center), range, 1); res1 && image.get(*res1)) {
251 // and then either at least one more ring around that
252 if (auto res2 = CenterOfRings(image, *res1, range, finderPatternSize / 2); res2 && image.get(*res2))
253 return res2;
254 // or the center can be approximated by a square
255 if (FitSquareToPoints(image, *res1, range, 1, false))
256 return res1;
257 // TODO: this is currently only keeping #258 alive, evaluate if still worth it
258 if (auto res2 = CenterOfDoubleCross(image, PointI(*res1), range, finderPatternSize / 2 + 1); res2 && image.get(*res2))
259 return res2;
260 }
261 return {};
262 }
263
264 } // ZXing