10#include <opencv2/opencv.hpp>
16namespace ports = core::ports::document_image_processing;
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;
23 if (img.empty())
return;
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);
32 debugger->writeText(
"opencv/error.txt", std::string(
"writeImageViaDebugger failed for ") + relPath);
36static cv::Mat ensureGrayLocal(
const cv::Mat& in) {
37 if (in.empty())
return in;
38 if (in.type() == CV_8UC1)
return in.clone();
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);
46std::vector<cv::Rect> findTextBlocksUsingMorphology(
const cv::Mat& gray) {
47 cv::Mat normalizedGray = ensureGrayLocal(gray);
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));
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);
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);
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);
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; }
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);
85 for (
int xx = x0; xx <= x1; ++xx) {
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; }
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);
98 for (
int x = 0; x < gray.cols; ++x) score += std::abs(
int(b[x]) -
int(a[x]));
102static int edgeScoreX(
const cv::Mat& gray,
int x) {
103 if (gray.empty() || x <= 0 || x >= gray.cols - 1)
return 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)));
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());
114 std::vector<int> out;
116 int bestScore = horizontal ? edgeScoreY(gray, best) : edgeScoreX(gray, best);
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; }
125 bestScore = horizontal ? edgeScoreY(gray, best) : edgeScoreX(gray, best);
132static ports::Rect bboxFromCells(
const std::vector<ports::Cell>& cells,
const cv::Rect& fallback) {
134 out.x = fallback.x; out.y = fallback.y; out.width = fallback.width; out.height = fallback.height;
135 if (cells.empty())
return out;
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);
145 if (minx < maxx && miny < maxy) {
148 out.width = maxx - minx;
149 out.height = maxy - miny;
156 bool horizontal =
true;
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());
169 for (
const auto& c : contours) {
170 cv::Rect r = cv::boundingRect(c);
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 });
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 });
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);
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;
196static bool isAnchoredToBorder(
int v0,
int border,
int tol) {
197 return std::abs(v0 - border) <= tol;
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();
207 return (std::abs(v - a) <= std::abs(v - b)) ? a : b;
210static std::vector<int> completeAxisLinesFromSegments(
211 const std::vector<int>& primaryLines,
212 const std::vector<LineSeg>& partialSegs,
213 const std::vector<int>& orthogonalLines,
221 std::vector<int> out = primaryLines;
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());
230 const int borderStart = 0;
232 for (
const auto& seg : partialSegs) {
233 if (seg.len < minPartialLen)
continue;
234 if (!isAnchoredToBorder(seg.b0, borderStart, borderTol))
continue;
236 int snappedPrimary = seg.a;
237 snappedPrimary = horizontal ? snapYToEdge(gray, snappedPrimary, 4) : snapXToEdge(gray, snappedPrimary, 4);
239 int snappedEnd = nearestValue(orthogonalLines, seg.b1);
240 if (std::abs(snappedEnd - seg.b1) > orthoSnapTol)
continue;
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; }
249 if (!coversInterval)
continue;
251 out.push_back(snappedPrimary);
254 std::sort(out.begin(), out.end());
255 out = dedupeByScore(out, primaryMinDist, gray, horizontal);
256 std::sort(out.begin(), out.end());
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) {
263 outCells = 0; outQuality = 0.0;
264 if (roiGrayIn.empty())
return table;
266 cv::Mat roiGray = ensureGrayLocal(roiGrayIn);
268 cv::Mat hMask = MaskAdapter::makeLineMask(roiGray,
true);
269 cv::Mat vMask = MaskAdapter::makeLineMask(roiGray,
false);
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;
275 auto hFullSegs = extractLineSegsFromMask(hMask,
true, minHorizW, maxThickness);
276 auto vFullSegs = extractLineSegsFromMask(vMask,
false, minVertH, maxThickness);
278 auto yLines = collectAxisFromSegs(hFullSegs);
279 auto xLines = collectAxisFromSegs(vFullSegs);
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);
284 for (
auto& x : xLines) x = snapXToEdge(roiGray, x, 4);
285 for (
auto& y : yLines) y = snapYToEdge(roiGray, y, 4);
287 const int xMinDist = std::max(4, roiGray.cols / 120);
288 const int yMinDist = std::max(4, roiGray.rows / 200);
290 xLines = dedupeByScore(xLines, xMinDist, roiGray,
false);
291 yLines = dedupeByScore(yLines, yMinDist, roiGray,
true);
293 std::sort(xLines.begin(), xLines.end());
294 std::sort(yLines.begin(), yLines.end());
296 if (!xLines.empty()) {
298 xLines.push_back(roiGray.cols - 1);
300 if (!yLines.empty()) {
302 yLines.push_back(roiGray.rows - 1);
305 std::sort(xLines.begin(), xLines.end());
306 std::sort(yLines.begin(), yLines.end());
308 xLines = dedupeByScore(xLines, xMinDist, roiGray,
false);
309 yLines = dedupeByScore(yLines, yMinDist, roiGray,
true);
311 std::sort(xLines.begin(), xLines.end());
312 std::sort(yLines.begin(), yLines.end());
314 yLines = completeAxisLinesFromSegments(
319 std::max(12, roiGray.cols / 50),
320 std::max(12, roiGray.cols / 50),
321 std::max(40, roiGray.cols / 10),
325 xLines = completeAxisLinesFromSegments(
330 std::max(12, roiGray.rows / 50),
331 std::max(12, roiGray.rows / 50),
332 std::max(40, roiGray.rows / 10),
336 if (xLines.size() < 2 || yLines.size() < 2) {
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;
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);
358 outCells =
static_cast<int>(table.cells.size());
359 if (outCells == 0) { outQuality = 0.0;
return table; }
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;
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);
381 return findTextBlocksUsingMorphology(img);
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;
387 if (debugger && debugger->enabled()) debugger->writeText(
"opencv/error.txt",
"Empty image: " + imagePath);
391 auto blocks = findTextBlocksUsingMorphology(img);
393 struct Candidate { ports::Table t;
int cells;
double q; };
394 std::vector<Candidate> candidates;
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;
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 });
406 std::sort(candidates.begin(), candidates.end(), [](
const Candidate& a,
const Candidate& b) {
407 if (a.cells != b.cells) return a.cells > b.cells;
411 if (!candidates.empty()) result.push_back(candidates[0].t);
413 if (debugger && debugger->enabled()) {
415 if (img.channels() == 1) cv::cvtColor(img, vis, cv::COLOR_GRAY2BGR);
416 else vis = img.clone();
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);
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);
427 writeImageViaDebugger(debugger,
"opencv/detect_table_with_cells", vis);
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.