Img2Num C++ (Internal Developer Docs)  dev
API Documentation
labels_to_svg.cpp
1 #include <array>
2 #include <cmath>
3 #include <cstdint>
4 #include <cstdlib>
5 #include <cstring>
6 #include <ctime>
7 #include <iomanip>
8 #include <limits>
9 #include <map>
10 #include <memory>
11 #include <queue>
12 #include <random>
13 #include <set>
14 #include <sstream>
15 #include <vector>
16 
17 #include "img2num.h"
18 #include "internal/bezier.h"
19 #include "internal/contours.h"
20 #include "internal/graph.h"
21 
22 /* Flood fill */
23 int flood_fill(std::vector<int32_t> &label_array, std::vector<int32_t> &region_array,
24  const uint8_t *color_array, int x, int y, int target_value, int label_value,
25  size_t width, size_t height, std::unique_ptr<std::vector<RGBXY>> &out_pixels) {
26  std::queue<XY> queue;
27  auto index = [width](int x, int y) { return y * width + x; };
28 
29  int count = 0;
30  int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
31 
32  RGBXY pix =
33  RGBXY{color_array[4 * size_t(index(x, y))], color_array[4 * size_t(index(x, y)) + 1],
34  color_array[4 * size_t(index(x, y)) + 2], x, y};
35 
36  queue.push({x, y});
37 
38  region_array[size_t(index(x, y))] = label_value;
39 
40  out_pixels->push_back(pix);
41  count++;
42 
43  while (!queue.empty()) {
44  XY c = queue.front();
45  queue.pop();
46  for (auto &d : dirs) {
47  int x1 = c.x + d[0];
48  int y1 = c.y + d[1];
49 
50  // check conditions
51  if ((x1 >= 0) && (x1 < int(width)) && (y1 >= 0) && (y1 < int(height)) &&
52  (label_array[size_t(index(x1, y1))] == target_value) &&
53  (region_array[size_t(index(x1, y1))] == -1)) {
54  RGBXY pix1 = RGBXY{color_array[4 * size_t(index(x1, y1))],
55  color_array[4 * size_t(index(x1, y1)) + 1],
56  color_array[4 * size_t(index(x1, y1)) + 2], x1, y1};
57  region_array[size_t(index(x1, y1))] = label_value;
58  out_pixels->push_back(pix1);
59  count++;
60 
61  queue.push({x1, y1});
62  }
63  }
64  }
65 
66  return count;
67 }
68 
69 void region_labeling(const uint8_t *data, std::vector<int32_t> &labels,
70  std::vector<int32_t> &regions, int width, int height,
71  std::vector<Node_ptr> &nodes) {
72  auto index = [width](int x, int y) { return y * width + x; };
73 
74  regions.resize(static_cast<size_t>(height) * static_cast<size_t>(width), -1);
75  int r_lbl = -1;
76 
77  for (int i = 0; i < width; i++) {
78  for (int j = 0; j < height; j++) {
79  int label{labels[size_t(index(i, j))]};
80  int rlab{regions[size_t(index(i, j))]};
81 
82  if (rlab == -1) {
83  r_lbl++;
84  // track pixels for this region
85  // std::vector<RGBXY> pixels;
86  std::unique_ptr<std::vector<RGBXY>> p_ptr = std::make_unique<std::vector<RGBXY>>();
87  int counts =
88  flood_fill(labels, regions, data, i, j, label, r_lbl, width, height, p_ptr);
89  int num_pixels = p_ptr->size();
90 
91  // num_pixels == counts always
92  Node_ptr n_ptr = std::make_shared<Node>(r_lbl, p_ptr);
93  nodes.push_back(n_ptr);
94  }
95  }
96  }
97 }
98 
99 void visualize_contours(const std::vector<std::vector<Point>> &contours,
100  ImageLib::Image<ImageLib::RGBAPixel<uint8_t>> &results, int width,
101  int height, int xmin = 0, int ymin = 0) {
102  // Random generator for colors
103  static std::mt19937 rng(std::random_device{}());
104  static std::uniform_int_distribution<int32_t> dist(0, 255);
105  for (const auto &c : contours) {
106  ImageLib::RGBAPixel<uint8_t> rand_color{static_cast<uint8_t>(dist(rng)),
107  static_cast<uint8_t>(dist(rng)),
108  static_cast<uint8_t>(dist(rng)), 255};
109 
110  for (const auto &p : c) {
111  int32_t _x{static_cast<int32_t>(p.x) + xmin};
112  int32_t _y{static_cast<int32_t>(p.y) + ymin};
113 
114  // Ensure within bounds
115  if (_x < 0 || _x >= width || _y < 0 || _y >= height) continue;
116 
117  results(_x, _y) = rand_color;
118  }
119  }
120 }
121 
122 std::string contourToSVGPath(const std::vector<Point> &contour) {
123  if (contour.empty()) return "";
124 
125  std::ostringstream path;
126  path << std::fixed << std::setprecision(2);
127 
128  // Move to the first point
129  path << "M " << contour[0].x << " " << contour[0].y << " ";
130 
131  // Draw lines to the remaining points
132  for (size_t i = 1; i < contour.size(); ++i) {
133  path << "L " << contour[i].x << " " << contour[i].y << " ";
134  }
135 
136  // Close the path
137  path << "Z";
138  return path.str();
139 }
140 
141 std::string contourToSVGCurve(const std::vector<QuadBezier> &curves) {
142  if (curves.empty()) return "";
143 
144  std::ostringstream path;
145  path << std::fixed << std::setprecision(2);
146 
147  for (size_t i = 0; i < curves.size(); ++i) {
148  const auto &c = curves[i];
149  if (i == 0) path << "M " << c.p0.x << " " << c.p0.y << " ";
150  path << "Q " << c.p1.x << " " << c.p1.y << " " << c.p2.x << " " << c.p2.y << " ";
151  }
152 
153  // Close the path
154  path << "Z";
155  return path.str();
156 }
157 
158 std::string contoursResultToSVG(const ColoredContours &result, const int width, const int height) {
159  std::ostringstream svg;
160  svg << "<svg xmlns=\"http://www.w3.org/2000/svg\" fill-rule=\"evenodd\" "
161  "width=\""
162  << width << "\" height=\"" << height << "\">\n";
163 
164  for (size_t i = 0; i < result.curves.size(); ++i) {
165  std::string pathData = contourToSVGCurve(result.curves[i]);
166 
167  const auto &px = result.colors[i];
168  std::ostringstream oss;
169  oss << "#" << std::hex << std::uppercase << std::setw(2) << std::setfill('0')
170  << static_cast<int>(px.red) << std::setw(2) << std::setfill('0')
171  << static_cast<int>(px.green) << std::setw(2) << std::setfill('0')
172  << static_cast<int>(px.blue);
173 
174  // You can optionally style holes differently or rely on fill-rule
175  svg << " <path d=\"" << pathData << "\" fill=\"" << oss.str() << "\" />\n";
176  }
177 
178  svg << "</svg>\n";
179  return svg.str();
180 }
181 
182 namespace img2num {
183 /*
184 data: uint8_t* -> output image from K-Means (or similar) in RGBA repeating
185 format ([r,g,b,a, r,g,b,a, ...]) labels: int32_t* -> output of labelled regions
186 from K-Means, should be 1/4 the size of data since data is RGBA labels : width *
187 height : number of pixels in image = 1 : 1 : 1
188 */
189 char *labels_to_svg(uint8_t *data, int32_t *labels, const int width, const int height,
190  const int min_area, const bool draw_contour_borders) {
191  const int32_t num_pixels{width * height};
192  std::vector<int32_t> labels_vector{labels, labels + num_pixels};
193  std::vector<int32_t> region_labels;
194 
195  // 1. enumerate regions and convert to Nodes
196  std::vector<Node_ptr> nodes;
197  region_labeling(data, labels_vector, region_labels, width, height, nodes);
198 
199  // 2. initialize Graph from all Nodes
200  std::unique_ptr<std::vector<Node_ptr>> node_ptr =
201  std::make_unique<std::vector<Node_ptr>>(std::move(nodes));
202  Graph G(node_ptr, width, height);
203 
204  // 3. Discover node adjacencies - add edges to Graph
205  G.discover_edges(region_labels, width, height);
206 
207  // 4. Merge small area nodes until all nodes are minArea or larger
208  G.merge_small_area_nodes(min_area);
209 
210  // 5. recolor image on new regions
211  ImageLib::Image<ImageLib::RGBAPixel<uint8_t>> results{width, height};
212  for (auto &n : G.get_nodes()) {
213  if (n->area() == 0) continue;
214 
215  auto [r, g, b] = n->color();
216  for (auto &[_, p] : n->get_pixels()) {
217  results(p.x, p.y) = {r, g, b};
218  }
219  }
220 
221  // 6. Contours
222  // graph will manage computing contours
223  G.compute_contours();
224 
225  // accumulate all contours for svg export
226  ColoredContours all_contours;
227  for (auto &n : G.get_nodes()) {
228  if (n->area() == 0) continue;
229  ColoredContours node_contours = n->get_contours();
230  for (auto &c : node_contours.contours) {
231  all_contours.contours.push_back(c);
232  }
233  for (auto &c : node_contours.hierarchy) {
234  all_contours.hierarchy.push_back(c);
235  }
236  for (bool b : node_contours.is_hole) {
237  all_contours.is_hole.push_back(b);
238  }
239  for (auto &c : node_contours.colors) {
240  all_contours.colors.push_back(c);
241  }
242  for (auto &c : node_contours.curves) {
243  all_contours.curves.push_back(c);
244  }
245  }
246 
247  // 7. Copy recolored image back
248  const auto &modified = results.getData();
249  std::memcpy(data, modified.data(), modified.size() * sizeof(ImageLib::RGBAPixel<uint8_t>));
250 
251  // 8. Return SVG if requested
252  if (!draw_contour_borders) {
253  std::string svg{contoursResultToSVG(all_contours, width, height)};
254 
255  // Dynamic C-style allocation (since returned over C ABI)
256  char *res_svg{static_cast<char *>(std::malloc(svg.size() + 1))};
257  if (!res_svg) {
258  return nullptr; // Allocation failed
259  }
260  std::memcpy(res_svg, svg.c_str(), svg.size() + 1);
261 
262  return res_svg;
263  }
264 
265  return nullptr; // no SVG
266 }
267 } // namespace img2num
Definition: graph.h:33
Core image processing functions for img2num project.
Definition: Error.h:7
char * labels_to_svg(uint8_t *data, int32_t *labels, const int width, const int height, const int min_area, const bool draw_contour_borders)
Convert labeled regions of an image into an SVG string.
Definition: node.h:49
Definition: node.h:38