CPPxDIC
C++ Digital Image Correlation library for fingertip 3D reconstruction
Loading...
Searching...
No Matches
mask_seed_setup.cpp
Go to the documentation of this file.
1
10
11#include "logging.h"
12
13#include <cctype>
14#include <cmath>
15#include <fstream>
16#include <iostream>
17#include <sstream>
18
19namespace cppxdic {
20namespace stepsABC {
21
22namespace {
23
24// Write a "# header" + list of "x y" rows.
25bool writePointFile(const std::string& path, const std::string& header,
26 const std::vector<cv::Point>& pts, std::string& error_message) {
27 std::ofstream ofs(path);
28 if (!ofs) {
29 error_message = "Cannot open for writing: " + path;
30 return false;
31 }
32 ofs << header << "\n";
33 for (const auto& p : pts) {
34 ofs << p.x << " " << p.y << "\n";
35 }
36 return true;
37}
38
39} // namespace
40
42#ifdef XDIC_STEPSABC_GUI
43 return true;
44#else
45 return false;
46#endif
47}
48
49cv::Mat MaskSeedSetup::polygonToMask(const std::vector<cv::Point>& polygon,
50 const cv::Size& image_size) {
51 cv::Mat mask = cv::Mat::zeros(image_size, CV_8UC1);
52 if (polygon.size() >= 3) {
53 std::vector<std::vector<cv::Point>> polys{polygon};
54 cv::fillPoly(mask, polys, cv::Scalar(255));
55 }
56 return mask;
57}
58
59std::vector<cv::Point> MaskSeedSetup::readPolygonFile(const std::string& path,
60 std::string& error_message) {
61 std::vector<cv::Point> pts;
62 std::ifstream ifs(path);
63 if (!ifs) {
64 error_message = "Cannot open polygon file: " + path;
65 return pts;
66 }
67 std::string line;
68 while (std::getline(ifs, line)) {
69 if (line.empty() || line[0] == '#') continue;
70 std::istringstream iss(line);
71 double x, y;
72 if (iss >> x >> y) {
73 pts.emplace_back(static_cast<int>(std::lround(x)), static_cast<int>(std::lround(y)));
74 }
75 }
76 return pts;
77}
78
79std::vector<SeedPoint> MaskSeedSetup::readSeedFile(const std::string& path,
80 std::string& error_message) {
81 std::vector<SeedPoint> seeds;
82 std::ifstream ifs(path);
83 if (!ifs) {
84 error_message = "Cannot open seed file: " + path;
85 return seeds;
86 }
87 std::string line;
88 while (std::getline(ifs, line)) {
89 if (line.empty() || line[0] == '#') continue;
90 std::istringstream iss(line);
91 double x, y;
92 if (iss >> x >> y) {
93 seeds.emplace_back(static_cast<int>(std::lround(x)), static_cast<int>(std::lround(y)));
94 }
95 }
96 return seeds;
97}
98
99MaskSeedResult MaskSeedSetup::loadFromFiles(const std::string& polygon_or_mask_path,
100 const std::string& seed_path,
101 const cv::Size& image_size,
102 std::string& error_message) {
103 MaskSeedResult result;
104
105 // Decide whether the mask input is a polygon text file or a raster image by
106 // extension. ".poly"/".txt" -> polygon, otherwise try to imread as a mask.
107 std::string lower = polygon_or_mask_path;
108 for (auto& c : lower) c = static_cast<char>(std::tolower(c));
109
110 bool as_polygon = (lower.size() >= 5 && lower.substr(lower.size() - 5) == ".poly") ||
111 (lower.size() >= 4 && lower.substr(lower.size() - 4) == ".txt");
112
113 if (as_polygon) {
114 result.polygon = readPolygonFile(polygon_or_mask_path, error_message);
115 if (result.polygon.size() < 3) {
116 if (error_message.empty())
117 error_message = "Polygon file has fewer than 3 vertices: " + polygon_or_mask_path;
118 return result;
119 }
120 result.mask = polygonToMask(result.polygon, image_size);
121 } else {
122 result.mask = cv::imread(polygon_or_mask_path, cv::IMREAD_GRAYSCALE);
123 if (result.mask.empty()) {
124 error_message = "Cannot read mask image: " + polygon_or_mask_path;
125 return result;
126 }
127 // Normalise to strict 0/255.
128 cv::threshold(result.mask, result.mask, 0, 255, cv::THRESH_BINARY);
129 }
130
131 result.seeds = readSeedFile(seed_path, error_message);
132 if (result.seeds.empty()) {
133 if (error_message.empty()) error_message = "No seed points found in: " + seed_path;
134 return result;
135 }
136
137 result.valid = !result.mask.empty() && !result.seeds.empty();
138 return result;
139}
140
141bool MaskSeedSetup::save(const MaskSeedResult& result, const std::string& out_prefix,
142 std::string& error_message) {
143 if (!result.valid) {
144 error_message = "Refusing to save an invalid mask/seed result";
145 return false;
146 }
147
148 if (!writePointFile(out_prefix + ".poly", "# xdic mask polygon v1", result.polygon,
149 error_message)) {
150 return false;
151 }
152
153 std::vector<cv::Point> seed_pts;
154 seed_pts.reserve(result.seeds.size());
155 for (const auto& s : result.seeds) {
156 seed_pts.emplace_back(s.pw[0], s.pw[1]);
157 }
158 if (!writePointFile(out_prefix + ".seed", "# xdic seed points v1", seed_pts, error_message)) {
159 return false;
160 }
161
162 if (!result.mask.empty()) {
163 if (!cv::imwrite(out_prefix + "_mask.png", result.mask)) {
164 error_message = "Failed to write mask image: " + out_prefix + "_mask.png";
165 return false;
166 }
167 }
168 return true;
169}
170
171// ----------------------------------------------------------------------------
172// GUI path (compiled only when XDIC_STEPSABC_GUI is defined)
173// ----------------------------------------------------------------------------
174
175#ifdef XDIC_STEPSABC_GUI
176
177namespace {
178
179struct GuiState {
180 std::vector<cv::Point> polygon; // mask vertices being drawn
181 std::vector<cv::Point> seeds; // seed points being picked
182 bool collecting_polygon = true; // phase: true=polygon, false=seeds
183 cv::Point cursor{-1, -1};
184};
185
186void renderOverlay(const cv::Mat& base, const GuiState& st, cv::Mat& out) {
187 if (base.channels() == 1) {
188 cv::cvtColor(base, out, cv::COLOR_GRAY2BGR);
189 } else {
190 base.copyTo(out);
191 }
192
193 // Draw polygon edges.
194 for (size_t i = 0; i + 1 < st.polygon.size(); ++i) {
195 cv::line(out, st.polygon[i], st.polygon[i + 1], cv::Scalar(0, 255, 0), 1);
196 }
197 if (st.collecting_polygon && !st.polygon.empty() && st.cursor.x >= 0) {
198 cv::line(out, st.polygon.back(), st.cursor, cv::Scalar(0, 200, 0), 1);
199 }
200 if (!st.collecting_polygon && st.polygon.size() >= 3) {
201 cv::line(out, st.polygon.back(), st.polygon.front(), cv::Scalar(0, 255, 0), 1);
202 }
203 for (const auto& p : st.polygon) {
204 cv::circle(out, p, 3, cv::Scalar(0, 255, 255), -1);
205 }
206 // Draw seeds.
207 for (const auto& s : st.seeds) {
208 cv::drawMarker(out, s, cv::Scalar(0, 0, 255), cv::MARKER_CROSS, 14, 2);
209 }
210}
211
212void onMouse(int event, int x, int y, int /*flags*/, void* userdata) {
213 auto* st = static_cast<GuiState*>(userdata);
214 st->cursor = cv::Point(x, y);
215 if (event == cv::EVENT_LBUTTONDOWN) {
216 if (st->collecting_polygon) {
217 st->polygon.emplace_back(x, y);
218 } else {
219 st->seeds.emplace_back(x, y);
220 }
221 } else if (event == cv::EVENT_RBUTTONDOWN) {
222 if (st->collecting_polygon && st->polygon.size() >= 3) {
223 st->collecting_polygon = false; // close polygon, move to seed phase
224 }
225 }
226}
227
228} // namespace
229
230MaskSeedResult MaskSeedSetup::runGui(const cv::Mat& reference_image, int num_seeds,
231 std::string& error_message) {
232 MaskSeedResult result;
233 if (reference_image.empty()) {
234 error_message = "GUI setup: reference image is empty";
235 return result;
236 }
237 if (num_seeds < 1) num_seeds = 1;
238
239 const std::string win = "xdic_stepsABC: MASK (L-click add, R-click close) then Seed";
240 GuiState st;
241 cv::namedWindow(win, cv::WINDOW_AUTOSIZE);
242 cv::setMouseCallback(win, onMouse, &st);
243
244 cv::Mat display;
245 LOG_INFO << "[gui] Draw mask polygon: left-click vertices, right-click to close. "
246 << "Then click " << num_seeds
247 << " seed point(s). Keys: u=undo, r=reset, ENTER=accept, ESC=cancel.";
248
249 while (true) {
250 renderOverlay(reference_image, st, display);
251 cv::imshow(win, display);
252 int key = cv::waitKey(20) & 0xFF;
253
254 if (key == 27) { // ESC -> cancel
255 error_message = "GUI setup cancelled by user";
256 cv::destroyWindow(win);
257 return result;
258 } else if (key == 'u') { // undo last point of current phase
259 if (st.collecting_polygon && !st.polygon.empty())
260 st.polygon.pop_back();
261 else if (!st.collecting_polygon && !st.seeds.empty())
262 st.seeds.pop_back();
263 } else if (key == 'r') { // reset everything
264 st = GuiState();
265 } else if (key == 13 || key == 10) { // ENTER
266 if (st.collecting_polygon) {
267 if (st.polygon.size() >= 3) st.collecting_polygon = false; // close polygon
268 } else if (static_cast<int>(st.seeds.size()) >= num_seeds) {
269 break; // done
270 }
271 }
272 }
273
274 cv::destroyWindow(win);
275
276 result.polygon = st.polygon;
277 result.mask = polygonToMask(st.polygon, reference_image.size());
278 result.seeds.clear();
279 for (int i = 0; i < num_seeds && i < static_cast<int>(st.seeds.size()); ++i) {
280 result.seeds.emplace_back(st.seeds[i].x, st.seeds[i].y);
281 }
282 result.valid = (result.polygon.size() >= 3) && !result.seeds.empty();
283 if (!result.valid)
284 error_message = "GUI setup incomplete (need polygon >=3 vertices and >=1 seed)";
285 return result;
286}
287
288#else // !XDIC_STEPSABC_GUI
289
290MaskSeedResult MaskSeedSetup::runGui(const cv::Mat& /*reference_image*/, int /*num_seeds*/,
291 std::string& error_message) {
292 MaskSeedResult result;
293 error_message =
294 "GUI mode is not available: this binary was built with XDIC_STEPSABC_GUI=OFF "
295 "(headless). Re-run with --no-gui and provide --mask/--seed files, or rebuild "
296 "with -DXDIC_STEPSABC_GUI=ON on a machine with a display.";
297 return result;
298}
299
300#endif // XDIC_STEPSABC_GUI
301
302} // namespace stepsABC
303} // namespace cppxdic
static MaskSeedResult loadFromFiles(const std::string &polygon_or_mask_path, const std::string &seed_path, const cv::Size &image_size, std::string &error_message)
Non-interactive setup: load mask polygon (or mask image) and seed points from files.
static MaskSeedResult runGui(const cv::Mat &reference_image, int num_seeds, std::string &error_message)
Interactive GUI setup using OpenCV highgui.
static cv::Mat polygonToMask(const std::vector< cv::Point > &polygon, const cv::Size &image_size)
Rasterise a polygon into an 8-bit (0/255) mask of the given size.
static std::vector< cv::Point > readPolygonFile(const std::string &path, std::string &error_message)
Read a ".poly" polygon file.
static std::vector< SeedPoint > readSeedFile(const std::string &path, std::string &error_message)
Read a ".seed" file.
static bool guiAvailable()
Returns true if this binary was compiled with GUI support.
static bool save(const MaskSeedResult &result, const std::string &out_prefix, std::string &error_message)
Persist a result to disk.
Lightweight, dependency-free logging facility for CPPxDIC.
#define LOG_INFO
Definition logging.h:170
DataSerializer - Abstract base class for format-agnostic DIC data I/O.
Result of a mask/seed setup operation.
std::vector< cv::Point > polygon
std::vector< SeedPoint > seeds