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);
29 error_message =
"Cannot open for writing: " + path;
32 ofs << header <<
"\n";
33 for (
const auto& p : pts) {
34 ofs << p.x <<
" " << p.y <<
"\n";
42#ifdef XDIC_STEPSABC_GUI
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));
60 std::string& error_message) {
61 std::vector<cv::Point> pts;
62 std::ifstream ifs(path);
64 error_message =
"Cannot open polygon file: " + path;
68 while (std::getline(ifs, line)) {
69 if (line.empty() || line[0] ==
'#')
continue;
70 std::istringstream iss(line);
73 pts.emplace_back(
static_cast<int>(std::lround(x)),
static_cast<int>(std::lround(y)));
80 std::string& error_message) {
81 std::vector<SeedPoint> seeds;
82 std::ifstream ifs(path);
84 error_message =
"Cannot open seed file: " + path;
88 while (std::getline(ifs, line)) {
89 if (line.empty() || line[0] ==
'#')
continue;
90 std::istringstream iss(line);
93 seeds.emplace_back(
static_cast<int>(std::lround(x)),
static_cast<int>(std::lround(y)));
100 const std::string& seed_path,
101 const cv::Size& image_size,
102 std::string& error_message) {
107 std::string lower = polygon_or_mask_path;
108 for (
auto& c : lower) c =
static_cast<char>(std::tolower(c));
110 bool as_polygon = (lower.size() >= 5 && lower.substr(lower.size() - 5) ==
".poly") ||
111 (lower.size() >= 4 && lower.substr(lower.size() - 4) ==
".txt");
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;
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;
128 cv::threshold(result.
mask, result.
mask, 0, 255, cv::THRESH_BINARY);
132 if (result.
seeds.empty()) {
133 if (error_message.empty()) error_message =
"No seed points found in: " + seed_path;
142 std::string& error_message) {
144 error_message =
"Refusing to save an invalid mask/seed result";
148 if (!writePointFile(out_prefix +
".poly",
"# xdic mask polygon v1", result.
polygon,
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]);
158 if (!writePointFile(out_prefix +
".seed",
"# xdic seed points v1", seed_pts, error_message)) {
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";
175#ifdef XDIC_STEPSABC_GUI
180 std::vector<cv::Point> polygon;
181 std::vector<cv::Point> seeds;
182 bool collecting_polygon =
true;
183 cv::Point cursor{-1, -1};
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);
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);
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);
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);
203 for (
const auto& p : st.polygon) {
204 cv::circle(out, p, 3, cv::Scalar(0, 255, 255), -1);
207 for (
const auto& s : st.seeds) {
208 cv::drawMarker(out, s, cv::Scalar(0, 0, 255), cv::MARKER_CROSS, 14, 2);
212void onMouse(
int event,
int x,
int y,
int ,
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);
219 st->seeds.emplace_back(x, y);
221 }
else if (event == cv::EVENT_RBUTTONDOWN) {
222 if (st->collecting_polygon && st->polygon.size() >= 3) {
223 st->collecting_polygon =
false;
231 std::string& error_message) {
232 MaskSeedResult result;
233 if (reference_image.empty()) {
234 error_message =
"GUI setup: reference image is empty";
237 if (num_seeds < 1) num_seeds = 1;
239 const std::string win =
"xdic_stepsABC: MASK (L-click add, R-click close) then Seed";
241 cv::namedWindow(win, cv::WINDOW_AUTOSIZE);
242 cv::setMouseCallback(win, onMouse, &st);
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.";
250 renderOverlay(reference_image, st, display);
251 cv::imshow(win, display);
252 int key = cv::waitKey(20) & 0xFF;
255 error_message =
"GUI setup cancelled by user";
256 cv::destroyWindow(win);
258 }
else if (key ==
'u') {
259 if (st.collecting_polygon && !st.polygon.empty())
260 st.polygon.pop_back();
261 else if (!st.collecting_polygon && !st.seeds.empty())
263 }
else if (key ==
'r') {
265 }
else if (key == 13 || key == 10) {
266 if (st.collecting_polygon) {
267 if (st.polygon.size() >= 3) st.collecting_polygon =
false;
268 }
else if (
static_cast<int>(st.seeds.size()) >= num_seeds) {
274 cv::destroyWindow(win);
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);
282 result.valid = (result.polygon.size() >= 3) && !result.seeds.empty();
284 error_message =
"GUI setup incomplete (need polygon >=3 vertices and >=1 seed)";
291 std::string& 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.";
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.
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