FOSSredder 0.5.0
Loading...
Searching...
No Matches
DetectAdapter.cpp
Go to the documentation of this file.
1
5
10#include <opencv2/opencv.hpp>
11#include <filesystem>
12#include <sstream>
13#include <algorithm>
14
15namespace opencv {
16namespace ports = core::ports::document_image_processing;
17namespace {
18
19void writeImageViaDebugger(std::shared_ptr<core::ports::diagnostics::IDiagnostics> debugger, const std::string& relPath, const cv::Mat& img) {
20 if (!debugger) return;
21 std::vector<uint8_t> buf;
22 try {
23 if (img.empty()) return;
24 cv::Mat out;
25 if (img.type() == CV_8UC1) cv::cvtColor(img, out, cv::COLOR_GRAY2BGR);
26 else if (img.type() == CV_8UC3) out = img;
27 else { img.convertTo(out, CV_8U, 255.0); if (out.channels() == 1) cv::cvtColor(out, out, cv::COLOR_GRAY2BGR); }
28 bool ok = cv::imencode(".png", out, buf);
29 if (!ok || buf.empty()) return;
30 debugger->writeBytes(relPath + ".png", buf);
31 } catch (...) {
32 debugger->writeText("opencv/error.txt", std::string("writeImageViaDebugger failed for ") + relPath);
33 }
34}
35
36static cv::Mat ensureGrayLocal(const cv::Mat& in) {
37 if (in.empty()) return in;
38 if (in.type() == CV_8UC1) return in.clone();
39 cv::Mat out;
40 if (in.channels() == 4) cv::cvtColor(in, out, cv::COLOR_BGRA2GRAY);
41 else if (in.channels() >= 3) cv::cvtColor(in, out, cv::COLOR_BGR2GRAY);
42 else in.convertTo(out, CV_8U);
43 return out;
44}
45
46std::vector<cv::Rect> findTextBlocksUsingMorphology(const cv::Mat& gray) {
47 cv::Mat normalizedGray = ensureGrayLocal(gray);
48 cv::Mat bin;
49 cv::adaptiveThreshold(normalizedGray, bin, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY_INV, 15, 10);
50 cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(30, 3));
51 cv::Mat morph;
52 cv::morphologyEx(bin, morph, cv::MORPH_CLOSE, kernel);
53 std::vector<std::vector<cv::Point>> contours;
54 cv::findContours(morph, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
55 std::vector<cv::Rect> blocks;
56 for (const auto& contour : contours) {
57 cv::Rect rect = cv::boundingRect(contour);
58 if (rect.width > 50 && rect.height > 10) blocks.push_back(rect);
59 }
60 return blocks;
61}
62
63static int snapYToEdge(const cv::Mat& gray, int y, int window) {
64 if (gray.empty()) return y;
65 int y0 = std::max(1, y - window);
66 int y1 = std::min(gray.rows - 2, y + window);
67 int bestY = y;
68 int bestScore = -1;
69 for (int yy = y0; yy <= y1; ++yy) {
70 const uint8_t* a = gray.ptr<uint8_t>(yy - 1);
71 const uint8_t* b = gray.ptr<uint8_t>(yy + 1);
72 int score = 0;
73 for (int x = 0; x < gray.cols; ++x) score += std::abs(int(b[x]) - int(a[x]));
74 if (score > bestScore) { bestScore = score; bestY = yy; }
75 }
76 return bestY;
77}
78
79static int snapXToEdge(const cv::Mat& gray, int x, int window) {
80 if (gray.empty()) return x;
81 int x0 = std::max(1, x - window);
82 int x1 = std::min(gray.cols - 2, x + window);
83 int bestX = x;
84 int bestScore = -1;
85 for (int xx = x0; xx <= x1; ++xx) {
86 int score = 0;
87 for (int y = 0; y < gray.rows; ++y) score += std::abs(int(gray.at<uint8_t>(y, xx + 1)) - int(gray.at<uint8_t>(y, xx - 1)));
88 if (score > bestScore) { bestScore = score; bestX = xx; }
89 }
90 return bestX;
91}
92
93static int edgeScoreY(const cv::Mat& gray, int y) {
94 if (gray.empty() || y <= 0 || y >= gray.rows - 1) return 0;
95 const uint8_t* a = gray.ptr<uint8_t>(y - 1);
96 const uint8_t* b = gray.ptr<uint8_t>(y + 1);
97 int score = 0;
98 for (int x = 0; x < gray.cols; ++x) score += std::abs(int(b[x]) - int(a[x]));
99 return score;
100}
101
102static int edgeScoreX(const cv::Mat& gray, int x) {
103 if (gray.empty() || x <= 0 || x >= gray.cols - 1) return 0;
104 int score = 0;
105 for (int y = 0; y < gray.rows; ++y) score += std::abs(int(gray.at<uint8_t>(y, x + 1)) - int(gray.at<uint8_t>(y, x - 1)));
106 return score;
107}
108
109static std::vector<int> dedupeByScore(const std::vector<int>& in, int minDist, const cv::Mat& gray, bool horizontal) {
110 if (in.empty()) return {};
111 std::vector<int> s = in;
112 std::sort(s.begin(), s.end());
113
114 std::vector<int> out;
115 int best = s[0];
116 int bestScore = horizontal ? edgeScoreY(gray, best) : edgeScoreX(gray, best);
117
118 for (size_t i = 1; i < s.size(); ++i) {
119 if (std::abs(s[i] - best) < minDist) {
120 int sc = horizontal ? edgeScoreY(gray, s[i]) : edgeScoreX(gray, s[i]);
121 if (sc > bestScore) { best = s[i]; bestScore = sc; }
122 } else {
123 out.push_back(best);
124 best = s[i];
125 bestScore = horizontal ? edgeScoreY(gray, best) : edgeScoreX(gray, best);
126 }
127 }
128 out.push_back(best);
129 return out;
130}
131
132static ports::Rect bboxFromCells(const std::vector<ports::Cell>& cells, const cv::Rect& fallback) {
133 ports::Rect out;
134 out.x = fallback.x; out.y = fallback.y; out.width = fallback.width; out.height = fallback.height;
135 if (cells.empty()) return out;
136
137 int minx = INT_MAX, miny = INT_MAX, maxx = 0, maxy = 0;
138 for (const auto& c : cells) {
139 minx = std::min(minx, c.bbox.x);
140 miny = std::min(miny, c.bbox.y);
141 maxx = std::max(maxx, c.bbox.x + c.bbox.width);
142 maxy = std::max(maxy, c.bbox.y + c.bbox.height);
143 }
144
145 if (minx < maxx && miny < maxy) {
146 out.x = minx;
147 out.y = miny;
148 out.width = maxx - minx;
149 out.height = maxy - miny;
150 }
151
152 return out;
153}
154
155struct LineSeg {
156 bool horizontal = true;
157 int a = 0;
158 int b0 = 0;
159 int b1 = 0;
160 int len = 0;
161};
162
163static std::vector<LineSeg> extractLineSegsFromMask(const cv::Mat& mask, bool horizontal, int minLen, int maxThickness) {
164 std::vector<std::vector<cv::Point>> contours;
165 cv::findContours(mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
166 std::vector<LineSeg> segs;
167 segs.reserve(contours.size());
168
169 for (const auto& c : contours) {
170 cv::Rect r = cv::boundingRect(c);
171 if (horizontal) {
172 if (r.width >= minLen && r.height <= maxThickness)
173 segs.push_back({ true, r.y + r.height / 2, r.x, r.x + r.width, r.width });
174 } else {
175 if (r.height >= minLen && r.width <= maxThickness)
176 segs.push_back({ false, r.x + r.width / 2, r.y, r.y + r.height, r.height });
177 }
178 }
179
180 return segs;
181}
182
183static std::vector<int> collectAxisFromSegs(const std::vector<LineSeg>& segs) {
184 std::vector<int> out;
185 out.reserve(segs.size());
186 for (const auto& s : segs) out.push_back(s.a);
187 return out;
188}
189
190static bool overlaps(int a0, int a1, int b0, int b1, int minOverlap) {
191 int lo = std::max(a0, b0);
192 int hi = std::min(a1, b1);
193 return (hi - lo) >= minOverlap;
194}
195
196static bool isAnchoredToBorder(int v0, int border, int tol) {
197 return std::abs(v0 - border) <= tol;
198}
199
200static int nearestValue(const std::vector<int>& s, int v) {
201 if (s.empty()) return v;
202 auto it = std::lower_bound(s.begin(), s.end(), v);
203 if (it == s.begin()) return *it;
204 if (it == s.end()) return s.back();
205 int a = *(it - 1);
206 int b = *it;
207 return (std::abs(v - a) <= std::abs(v - b)) ? a : b;
208}
209
210static std::vector<int> completeAxisLinesFromSegments(
211 const std::vector<int>& primaryLines,
212 const std::vector<LineSeg>& partialSegs,
213 const std::vector<int>& orthogonalLines,
214 int primaryMinDist,
215 int orthoSnapTol,
216 int borderTol,
217 int minPartialLen,
218 const cv::Mat& gray,
219 bool horizontal) {
220
221 std::vector<int> out = primaryLines;
222
223 if (partialSegs.empty() || orthogonalLines.size() < 2) {
224 std::sort(out.begin(), out.end());
225 out = dedupeByScore(out, primaryMinDist, gray, horizontal);
226 std::sort(out.begin(), out.end());
227 return out;
228 }
229
230 const int borderStart = 0;
231
232 for (const auto& seg : partialSegs) {
233 if (seg.len < minPartialLen) continue;
234 if (!isAnchoredToBorder(seg.b0, borderStart, borderTol)) continue;
235
236 int snappedPrimary = seg.a;
237 snappedPrimary = horizontal ? snapYToEdge(gray, snappedPrimary, 4) : snapXToEdge(gray, snappedPrimary, 4);
238
239 int snappedEnd = nearestValue(orthogonalLines, seg.b1);
240 if (std::abs(snappedEnd - seg.b1) > orthoSnapTol) continue;
241
242 bool coversInterval = false;
243 for (size_t i = 0; i + 1 < orthogonalLines.size(); ++i) {
244 int o0 = orthogonalLines[i];
245 int o1 = orthogonalLines[i + 1];
246 if (o1 <= 0) continue;
247 if (overlaps(seg.b0, snappedEnd, o0, o1, std::max(12, (o1 - o0) / 2))) { coversInterval = true; break; }
248 }
249 if (!coversInterval) continue;
250
251 out.push_back(snappedPrimary);
252 }
253
254 std::sort(out.begin(), out.end());
255 out = dedupeByScore(out, primaryMinDist, gray, horizontal);
256 std::sort(out.begin(), out.end());
257
258 return out;
259}
260
261ports::Table detectTableGridHough(const cv::Mat& roiGrayIn, const cv::Rect& roiOffset, const std::vector<cv::Rect>&, int& outCells, double& outQuality, const std::string&, std::shared_ptr<core::ports::diagnostics::IDiagnostics> debugger) {
262 ports::Table table;
263 outCells = 0; outQuality = 0.0;
264 if (roiGrayIn.empty()) return table;
265
266 cv::Mat roiGray = ensureGrayLocal(roiGrayIn);
267
268 cv::Mat hMask = MaskAdapter::makeLineMask(roiGray, true);
269 cv::Mat vMask = MaskAdapter::makeLineMask(roiGray, false);
270
271 const int minHorizW = std::max((roiGray.cols * 8) / 10, 160);
272 const int minVertH = std::max((roiGray.rows * 5) / 10, 100);
273 const int maxThickness = 12;
274
275 auto hFullSegs = extractLineSegsFromMask(hMask, true, minHorizW, maxThickness);
276 auto vFullSegs = extractLineSegsFromMask(vMask, false, minVertH, maxThickness);
277
278 auto yLines = collectAxisFromSegs(hFullSegs);
279 auto xLines = collectAxisFromSegs(vFullSegs);
280
281 auto hPartialSegs = extractLineSegsFromMask(hMask, true, std::max(roiGray.cols / 5, 80), maxThickness);
282 auto vPartialSegs = extractLineSegsFromMask(vMask, false, std::max(roiGray.rows / 6, 60), maxThickness);
283
284 for (auto& x : xLines) x = snapXToEdge(roiGray, x, 4);
285 for (auto& y : yLines) y = snapYToEdge(roiGray, y, 4);
286
287 const int xMinDist = std::max(4, roiGray.cols / 120);
288 const int yMinDist = std::max(4, roiGray.rows / 200);
289
290 xLines = dedupeByScore(xLines, xMinDist, roiGray, false);
291 yLines = dedupeByScore(yLines, yMinDist, roiGray, true);
292
293 std::sort(xLines.begin(), xLines.end());
294 std::sort(yLines.begin(), yLines.end());
295
296 if (!xLines.empty()) {
297 xLines.push_back(0);
298 xLines.push_back(roiGray.cols - 1);
299 }
300 if (!yLines.empty()) {
301 yLines.push_back(0);
302 yLines.push_back(roiGray.rows - 1);
303 }
304
305 std::sort(xLines.begin(), xLines.end());
306 std::sort(yLines.begin(), yLines.end());
307
308 xLines = dedupeByScore(xLines, xMinDist, roiGray, false);
309 yLines = dedupeByScore(yLines, yMinDist, roiGray, true);
310
311 std::sort(xLines.begin(), xLines.end());
312 std::sort(yLines.begin(), yLines.end());
313
314 yLines = completeAxisLinesFromSegments(
315 yLines,
316 hPartialSegs,
317 xLines,
318 yMinDist,
319 std::max(12, roiGray.cols / 50),
320 std::max(12, roiGray.cols / 50),
321 std::max(40, roiGray.cols / 10),
322 roiGray,
323 true);
324
325 xLines = completeAxisLinesFromSegments(
326 xLines,
327 vPartialSegs,
328 yLines,
329 xMinDist,
330 std::max(12, roiGray.rows / 50),
331 std::max(12, roiGray.rows / 50),
332 std::max(40, roiGray.rows / 10),
333 roiGray,
334 false);
335
336 if (xLines.size() < 2 || yLines.size() < 2) {
337 outQuality = 0.0;
338 return table;
339 }
340
341 for (size_t r = 0; r + 1 < yLines.size(); ++r) {
342 for (size_t c = 0; c + 1 < xLines.size(); ++c) {
343 int x0 = xLines[c], x1 = xLines[c + 1];
344 int y0 = yLines[r], y1 = yLines[r + 1];
345 int w = x1 - x0, h = y1 - y0;
346 if (w <= 6 || h <= 6) continue;
347 cv::Rect crel(x0, y0, w, h);
348 crel &= cv::Rect(0, 0, roiGray.cols, roiGray.rows);
349 if (crel.width <= 0 || crel.height <= 0) continue;
350 ports::Cell cell;
351 cell.bbox.x = crel.x + roiOffset.x; cell.bbox.y = crel.y + roiOffset.y; cell.bbox.width = crel.width; cell.bbox.height = crel.height;
352 cell.row = static_cast<int>(r);
353 cell.col = static_cast<int>(c);
354 table.cells.push_back(cell);
355 }
356 }
357
358 outCells = static_cast<int>(table.cells.size());
359 if (outCells == 0) { outQuality = 0.0; return table; }
360
361 double avgW = 0.0, avgH = 0.0;
362 for (const auto& c : table.cells) { avgW += c.bbox.width; avgH += c.bbox.height; }
363 avgW /= table.cells.size(); avgH /= table.cells.size();
364 double sizeScore = std::clamp((avgW * avgH) / double(roiGray.cols * roiGray.rows), 0.0, 1.0);
365 double lineScore = std::clamp((double)(xLines.size() + yLines.size()) / 12.0, 0.0, 1.0);
366 outQuality = 0.6 * lineScore + 0.4 * sizeScore;
367
368 table.bbox = bboxFromCells(table.cells, roiOffset);
369 if (!table.cells.empty()) {
370 table.cols = static_cast<int>(xLines.size() > 0 ? xLines.size() - 1 : 0);
371 table.rows = static_cast<int>(yLines.size() > 0 ? yLines.size() - 1 : 0);
372 }
373
374 return table;
375}
376
377} // anonymous namespace
378
379// Expose DetectTextBlocks implementation
380std::vector<cv::Rect> DetectAdapter::detectTextBlocks(const cv::Mat& img, std::shared_ptr<core::ports::diagnostics::IDiagnostics> debugger) {
381 return findTextBlocksUsingMorphology(img);
382}
383
384std::vector<ports::Table> DetectAdapter::detectTables(const cv::Mat& img, const std::string& imagePath, std::shared_ptr<core::ports::diagnostics::IDiagnostics> debugger) {
385 std::vector<ports::Table> result;
386 if (img.empty()) {
387 if (debugger && debugger->enabled()) debugger->writeText("opencv/error.txt", "Empty image: " + imagePath);
388 return result;
389 }
390
391 auto blocks = findTextBlocksUsingMorphology(img);
392
393 struct Candidate { ports::Table t; int cells; double q; };
394 std::vector<Candidate> candidates;
395
396 for (const auto& b : blocks) {
397 if (b.width < img.cols * 0.12) continue;
398 cv::Mat roi = img(b);
399 std::vector<cv::Rect> emptyBoxes;
400
401 int cells = 0; double q = 0.0;
402 auto tab = detectTableGridHough(roi, cv::Rect(b.x, b.y, b.width, b.height), emptyBoxes, cells, q, std::string(), debugger);
403 if (cells > 0 && q > 0.05) candidates.push_back({ tab, cells, q });
404 }
405
406 std::sort(candidates.begin(), candidates.end(), [](const Candidate& a, const Candidate& b) {
407 if (a.cells != b.cells) return a.cells > b.cells;
408 return a.q > b.q;
409 });
410
411 if (!candidates.empty()) result.push_back(candidates[0].t);
412
413 if (debugger && debugger->enabled()) {
414 cv::Mat vis;
415 if (img.channels() == 1) cv::cvtColor(img, vis, cv::COLOR_GRAY2BGR);
416 else vis = img.clone();
417
418 for (const auto &t : result)
419 cv::rectangle(vis, cv::Rect(t.bbox.x, t.bbox.y, t.bbox.width, t.bbox.height), cv::Scalar(0,255,0), 2);
420
421 if (!result.empty()) {
422 const auto &t = result[0];
423 for (const auto &c : t.cells)
424 cv::rectangle(vis, cv::Rect(c.bbox.x, c.bbox.y, c.bbox.width, c.bbox.height), cv::Scalar(255,0,0), 2);
425 }
426
427 writeImageViaDebugger(debugger, "opencv/detect_table_with_cells", vis);
428 }
429
430 return result;
431}
432
433} // namespace opencv
Declares the OpenCV detection adapter used by the import workflow.
Declares the diagnostics artifact writing port.
Declares the OpenCV masking adapter used by the import workflow.
static std::vector< cv::Rect > detectTextBlocks(const cv::Mat &img, std::shared_ptr< core::ports::diagnostics::IDiagnostics > debugger=nullptr)
Detects text blocks in a source image.
static std::vector< ports::Table > detectTables(const cv::Mat &img, const std::string &imagePath, std::shared_ptr< core::ports::diagnostics::IDiagnostics > debugger=nullptr)
Detects table structures in a source image.
Precompiled header for the document-image-processing infrastructure module.