18 if (grayIn.empty())
return grayIn;
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);
26 cv::adaptiveThreshold(gray, bin, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY_INV, 31, 10);
28 cv::threshold(gray, bin, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);
31 const int span = horizontal ? std::max(40, gray.cols / 15) : std::max(40, gray.rows / 15);
32 const int thickness = 2;
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));
39 try { cv::erode(bin, m, k); }
catch (...) { m = bin.clone(); }
40 try { cv::dilate(m, m, k); }
catch (...) { m = m.clone(); }
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(); }
52ports::MaskResult
MaskAdapter::mask(
const ports::MaskRequest& req, std::shared_ptr<core::ports::diagnostics::IDiagnostics> debugger) {
53 ports::MaskResult res;
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>");
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();
68 if (img.empty())
return res;
70 const double imgArea =
static_cast<double>(img.cols) *
static_cast<double>(img.rows);
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);
76 if (req.useMorphology) {
77 cv::Mat gray; cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
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);
85 if (!req.tesseractTsv.empty()) {
86 std::istringstream iss(req.tesseractTsv);
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);
98 cv::Mat
mask(img.size(), CV_8UC1, cv::Scalar(0));
101 std::vector<cv::Rect> tableBboxes;
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);
107 }
catch (...) { tableBboxes.clear(); }
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;
122 const int insetPx = 1;
123 for (
const auto &r : boxes) {
125 if (isInTable(r))
continue;
126 cv::Rect rr(r.x - padPx, r.y - padPx, r.width + 2 * padPx, r.height + 2 * padPx);
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);
136 try { cv::dilate(
mask, cleaned, cv::getStructuringElement(cv::MORPH_RECT, cv::Size(dilateK, dilateK))); }
catch(...) { cleaned =
mask.clone(); }
140 try { cv::threshold(cleaned, cleaned, 128, 255, cv::THRESH_BINARY); }
catch(...) { cleaned =
mask.clone(); }
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);
148 double nonZero =
static_cast<double>(cv::countNonZero(cleaned));
150 if (nonZero > imgArea * 0.6) cleaned =
mask;
155 out.setTo(cv::Scalar(255,255,255), cleaned);
158 if (!req.uniqIdPrefix.empty()) {
159 base = req.uniqIdPrefix;
160 if (!req.filePrefix.empty()) base +=
"_" + req.filePrefix;
162 base = req.filePrefix.empty() ? std::string(
"opencv_mask") : req.filePrefix;
166 cv::imencode(
".png", cleaned, res.maskImageBytes);
167 }
catch (...) { res.maskImageBytes.clear(); }
169 cv::imencode(
".png", out, res.maskedImageBytes);
170 }
catch (...) { res.maskedImageBytes.clear(); }
172 if (!req.outputDir.empty()) {
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;
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);
186 if (debugger && debugger->enabled()) debugger->writeText(
"opencv/error.txt",
"MaskAdapter::mask failed");