FOSSredder 0.5.0
Loading...
Searching...
No Matches
MaskAdapter.cpp
Go to the documentation of this file.
1
5
10#include <opencv2/opencv.hpp>
11#include <filesystem>
12#include <sstream>
13
14namespace opencv {
15namespace ports = core::ports::document_image_processing;
16
17cv::Mat MaskAdapter::makeLineMask(const cv::Mat& grayIn, bool horizontal) {
18 if (grayIn.empty()) return grayIn;
19 cv::Mat gray;
20 if (grayIn.type() == CV_8UC1) gray = grayIn;
21 else if (grayIn.channels() == 3) cv::cvtColor(grayIn, gray, cv::COLOR_BGR2GRAY);
22 else grayIn.convertTo(gray, CV_8U);
23
24 cv::Mat bin;
25 try {
26 cv::adaptiveThreshold(gray, bin, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY_INV, 31, 10);
27 } catch (...) {
28 cv::threshold(gray, bin, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);
29 }
30
31 const int span = horizontal ? std::max(40, gray.cols / 15) : std::max(40, gray.rows / 15);
32 const int thickness = 2;
33
34 cv::Mat k = horizontal
35 ? cv::getStructuringElement(cv::MORPH_RECT, cv::Size(span, thickness))
36 : cv::getStructuringElement(cv::MORPH_RECT, cv::Size(thickness, span));
37
38 cv::Mat m;
39 try { cv::erode(bin, m, k); } catch (...) { m = bin.clone(); }
40 try { cv::dilate(m, m, k); } catch (...) { m = m.clone(); }
41
42 try {
43 cv::Mat closeK = horizontal
44 ? cv::getStructuringElement(cv::MORPH_RECT, cv::Size(std::max(5, gray.cols / 120), 1))
45 : cv::getStructuringElement(cv::MORPH_RECT, cv::Size(1, std::max(5, gray.rows / 120)));
46 cv::morphologyEx(m, m, cv::MORPH_CLOSE, closeK);
47 } catch (...) { m = m.clone(); }
48
49 return m;
50}
51
52ports::MaskResult MaskAdapter::mask(const ports::MaskRequest& req, std::shared_ptr<core::ports::diagnostics::IDiagnostics> debugger) {
53 ports::MaskResult res;
54 try {
55 cv::Mat img;
56 std::string imgLabel;
57 const auto path = std::filesystem::path(req.imagePath);
58 if (!req.imageBytes.empty()) {
59 img = cv::imdecode(req.imageBytes, cv::IMREAD_COLOR);
60 imgLabel = std::string("<bytes>");
61 }
62
63 if (img.empty() && !req.imagePath.empty()) {
64 if (!std::filesystem::exists(path)) return res;
65 img = cv::imread(path.string(), cv::IMREAD_COLOR);
66 imgLabel = path.string();
67 }
68 if (img.empty()) return res;
69
70 const double imgArea = static_cast<double>(img.cols) * static_cast<double>(img.rows);
71
72 std::vector<cv::Rect> boxes;
73 boxes.reserve(req.textElements.size());
74 for (const auto& e : req.textElements) boxes.emplace_back(e.x, e.y, e.width, e.height);
75
76 if (req.useMorphology) {
77 cv::Mat gray; cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
78 auto blocks = opencv::DetectAdapter::detectTextBlocks(gray, debugger);
79 for (const auto &b : blocks) {
80 double area = static_cast<double>(b.width) * static_cast<double>(b.height);
81 if (area <= imgArea * 0.15) boxes.emplace_back(b.x, b.y, b.width, b.height);
82 }
83 }
84
85 if (!req.tesseractTsv.empty()) {
86 std::istringstream iss(req.tesseractTsv);
87 std::string line;
88 while (std::getline(iss, line)) {
89 if (line.empty()) continue;
90 std::istringstream ls(line);
91 std::string text; int left, top, width, height; double conf;
92 if (!(ls >> text >> left >> top >> width >> height >> conf)) continue;
93 double area = static_cast<double>(width) * static_cast<double>(height);
94 if (area <= imgArea * 0.25) boxes.emplace_back(left, top, width, height);
95 }
96 }
97
98 cv::Mat mask(img.size(), CV_8UC1, cv::Scalar(0));
99
100 // Detect tables in the page and exclude those regions from masking
101 std::vector<cv::Rect> tableBboxes;
102 try {
103 auto tables = opencv::DetectAdapter::detectTables(img, imgLabel, debugger);
104 for (const auto &t : tables) {
105 if (t.bbox.width > 0 && t.bbox.height > 0) tableBboxes.emplace_back(t.bbox.x, t.bbox.y, t.bbox.width, t.bbox.height);
106 }
107 } catch (...) { tableBboxes.clear(); }
108
109 // helper to check if a box center lies inside any detected table bbox
110 auto isInTable = [&](const cv::Rect &r)->bool{
111 const int cx = r.x + r.width/2;
112 const int cy = r.y + r.height/2;
113 for (const auto &tb : tableBboxes) {
114 if (cx >= tb.x && cx < tb.x + tb.width && cy >= tb.y && cy < tb.y + tb.height) return true;
115 }
116 return false;
117 };
118
119 // pad around detected text boxes when forming mask
120 // use zero pad but inset rectangles slightly to avoid touching table grid lines
121 int padPx = 0;
122 const int insetPx = 1; // shrink each mask rect by this amount on all sides
123 for (const auto &r : boxes) {
124 // skip masking text boxes that belong to detected tables
125 if (isInTable(r)) continue;
126 cv::Rect rr(r.x - padPx, r.y - padPx, r.width + 2 * padPx, r.height + 2 * padPx);
127 // inset to avoid bridging lines
128 rr.x += insetPx; rr.y += insetPx; rr.width -= 2 * insetPx; rr.height -= 2 * insetPx;
129 rr &= cv::Rect(0,0,img.cols,img.rows);
130 if (rr.width > 0 && rr.height > 0) cv::rectangle(mask, rr, cv::Scalar(255), cv::FILLED);
131 }
132
133 cv::Mat cleaned;
134 // keep dilation minimal to avoid merging neighbouring boxes across table lines
135 int dilateK = 1;
136 try { cv::dilate(mask, cleaned, cv::getStructuringElement(cv::MORPH_RECT, cv::Size(dilateK, dilateK))); } catch(...) { cleaned = mask.clone(); }
137
138 // avoid additional morphology to keep mask minimal
139 (void)cleaned;
140 try { cv::threshold(cleaned, cleaned, 128, 255, cv::THRESH_BINARY); } catch(...) { cleaned = mask.clone(); }
141
142 // Clear mask inside detected table regions to preserve table grid lines
143 for (const auto &tb : tableBboxes) {
144 cv::Rect rr = tb & cv::Rect(0,0,img.cols,img.rows);
145 if (rr.width > 0 && rr.height > 0) cv::rectangle(cleaned, rr, cv::Scalar(0), cv::FILLED);
146 }
147
148 double nonZero = static_cast<double>(cv::countNonZero(cleaned));
149 // only fallback to raw mask if a very large portion is masked
150 if (nonZero > imgArea * 0.6) cleaned = mask;
151
152 cv::Mat out;
153 // Perform whiteout (fill masked areas with white) but mask is less aggressive now
154 out = img.clone();
155 out.setTo(cv::Scalar(255,255,255), cleaned);
156
157 std::string base;
158 if (!req.uniqIdPrefix.empty()) {
159 base = req.uniqIdPrefix;
160 if (!req.filePrefix.empty()) base += "_" + req.filePrefix;
161 } else {
162 base = req.filePrefix.empty() ? std::string("opencv_mask") : req.filePrefix;
163 }
164
165 try {
166 cv::imencode(".png", cleaned, res.maskImageBytes);
167 } catch (...) { res.maskImageBytes.clear(); }
168 try {
169 cv::imencode(".png", out, res.maskedImageBytes);
170 } catch (...) { res.maskedImageBytes.clear(); }
171
172 if (!req.outputDir.empty()) {
173 std::error_code ec;
174 std::filesystem::create_directories(req.outputDir, ec);
175 const auto outPath = std::filesystem::path(req.outputDir) / (base + ".png");
176 if (cv::imwrite(outPath.string(), out)) {
177 res.maskedImagePath = outPath;
178 }
179 }
180
181 if (debugger && debugger->enabled()) {
182 if (!res.maskImageBytes.empty()) debugger->writeBytes(std::string("opencv_mask_binary"), res.maskImageBytes);
183 if (!res.maskedImageBytes.empty()) debugger->writeBytes(std::string("opencv_mask_whiteout"), res.maskedImageBytes);
184 }
185 } catch (...) {
186 if (debugger && debugger->enabled()) debugger->writeText("opencv/error.txt", "MaskAdapter::mask failed");
187 }
188 return res;
189}
190
191} // 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.
static ports::MaskResult mask(const ports::MaskRequest &req, std::shared_ptr< core::ports::diagnostics::IDiagnostics > debugger=nullptr)
Builds a whiteout mask for the requested image region.
static cv::Mat makeLineMask(const cv::Mat &gray, bool horizontal)
Creates a binary line mask for text or table detection.
Precompiled header for the document-image-processing infrastructure module.