Img2Num C++ (Internal Developer Docs)  dev
API Documentation
graph.cpp
1 #include "internal/graph.h"
2 
3 #include <algorithm>
4 #include <iterator>
5 #include <queue>
6 #include <string>
7 
8 #include "internal/Pixel.h"
9 #include "internal/bezier.h"
10 /*
11  Graph class - manages Node class
12 */
13 
14 static inline float colorDistance(const ImageLib::RGBPixel<uint8_t> &a,
15  const ImageLib::RGBPixel<uint8_t> &b) {
16  ImageLib::RGBPixel<float> af{static_cast<float>(a.red), static_cast<float>(a.green),
17  static_cast<float>(a.blue)};
18  ImageLib::RGBPixel<float> bf{static_cast<float>(b.red), static_cast<float>(b.green),
19  static_cast<float>(b.blue)};
20  return std::sqrt((af.red - bf.red) * (af.red - bf.red) +
21  (af.green - bf.green) * (af.green - bf.green) +
22  (af.blue - bf.blue) * (af.blue - bf.blue));
23 }
24 
25 /*
26 To quickly search m_nodes (std::vector) for the index of a node id
27 create an std::unordered_map of node id - index pairs
28 indexing time of std::vector by value is O(N)
29 lookup time of std::unordered_map by key is O(log(N))
30 */
31 void Graph::hash_node_ids() {
32  for (int32_t i{0}; i < m_nodes->size(); i++) {
33  const int32_t key{m_nodes->at(i)->id()};
34  m_node_ids[key] = i;
35  }
36 }
37 
38 bool Graph::all_areas_bigger_than(int32_t min_area) {
39  for (auto &n : *m_nodes) {
40  if (n->area() < min_area) {
41  return false;
42  }
43  }
44 
45  return true;
46 }
47 
48 bool Graph::add_edge(int32_t node_id1, int32_t node_id2) {
49  auto end_node_ids{m_node_ids.end()};
50  auto node1_it{m_node_ids.find(node_id1)};
51  auto node2_it{m_node_ids.find(node_id2)};
52 
53  if (node1_it == end_node_ids || node2_it == end_node_ids) {
54  return false;
55  }
56 
57  const int32_t idx1{node1_it->second};
58  const int32_t idx2{node2_it->second};
59 
60  m_nodes->at(idx1)->add_edge(m_nodes->at(idx2));
61  m_nodes->at(idx2)->add_edge(m_nodes->at(idx1));
62  return true;
63 }
64 
65 bool Graph::merge_nodes(const Node_ptr &node_to_keep, const Node_ptr &node_to_remove) {
66  auto end_node_ids{m_node_ids.end()};
67  auto node1_it{m_node_ids.find(node_to_keep->id())};
68  auto node2_it{m_node_ids.find(node_to_remove->id())};
69 
70  if (node1_it == end_node_ids || node2_it == end_node_ids) {
71  return false;
72  }
73 
74  const int32_t idx_k{node1_it->second};
75  const int32_t idx_r{node2_it->second};
76 
77  // transfer edges from node_to_remove to node_to_keep
78  for (Node_ptr n : m_nodes->at(idx_r)->edges()) {
79  if (n->id() != node_to_keep->id()) {
80  // prevents self referencing
81  n->remove_edge(node_to_remove);
82  n->add_edge(node_to_keep);
83  node_to_keep->add_edge(n);
84  }
85  }
86 
87  node_to_keep->add_pixels(node_to_remove->get_pixels());
88 
89  node_to_remove->clear_all();
90 
91  return true;
92 }
93 
94 void Graph::clear_unconnected_nodes() {
95  std::vector<Node_ptr> &nodes{*m_nodes};
96 
97  nodes.erase(std::remove_if(nodes.begin(), nodes.end(),
98  [](const Node_ptr &n) { return n->area() == 0; }),
99  nodes.end());
100 
101  hash_node_ids();
102 }
103 
104 void Graph::discover_edges(const std::vector<int32_t> &region_labels, const int32_t width,
105  const int32_t height) {
106  // Moore 8-connected neighbourhood
107  constexpr int8_t dirs[8][2]{{1, 0}, {-1, 0}, {0, 1}, {0, -1},
108  {1, 1}, {-1, -1}, {-1, 1}, {1, -1}};
109 
110  int32_t rneigh[8];
111 
112  for (int32_t y{0}; y < height; ++y) {
113  for (int32_t x{0}; x < width; ++x) {
114  const int32_t idx{y * width + x};
115  const int32_t rid{region_labels[idx]};
116 
117  for (int32_t k{0}; k < 8; ++k) {
118  const int32_t nx{x + dirs[k][0]};
119  const int32_t ny{y + dirs[k][1]};
120 
121  if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
122  rneigh[k] = region_labels[ny * width + nx];
123  } else {
124  rneigh[k] = rid; // ignore out-of-bounds
125  }
126  }
127 
128  for (int32_t r : rneigh) {
129  if (r != rid) {
130  add_edge(rid, r);
131  }
132  }
133  }
134  }
135 }
136 
137 void Graph::compute_contours() {
138  // overlap edge pixels
139  // then compute contours
140 
141  std::set<std::pair<int, int>> adjusted_neighbors{};
142 
143  constexpr int8_t dirs[8][2]{{1, 0}, {-1, 0}, {0, 1}, {0, -1},
144  {1, 1}, {-1, -1}, {-1, 1}, {1, -1}};
145 
146  for (const Node_ptr &n : get_nodes()) {
147  if (n->area() == 0) continue;
148 
149  std::vector<std::vector<uint8_t>> full_neighborhood;
150  std::vector<std::array<int32_t, 4>> xywh;
151 
152  std::vector<uint8_t> node_binary;
153  std::array<int32_t, 4> xywh0 = n->create_binary_image(node_binary);
154 
155  full_neighborhood.push_back(node_binary);
156  xywh.push_back(xywh0);
157 
158  std::vector<Node_ptr> considered_neigbors;
159  for (const auto &neighbor : n->edges()) {
160  if (neighbor->area() == 0) continue;
161 
162  // check if this neighbor pairing has already been addressed
163  std::pair<int, int> id1 = std::make_pair(n->id(), neighbor->id());
164  std::pair<int, int> id2 = std::make_pair(neighbor->id(), n->id());
165  auto _end{adjusted_neighbors.end()};
166  auto _it1{adjusted_neighbors.find(id1)};
167  auto _it2{adjusted_neighbors.find(id2)};
168 
169  if (_it1 != _end || _it2 != _end) {
170  continue;
171  }
172 
173  considered_neigbors.push_back(neighbor);
174 
175  std::vector<uint8_t> neighbor_binary;
176  std::array<int32_t, 4> xywh1 = neighbor->create_binary_image(neighbor_binary);
177  full_neighborhood.push_back(neighbor_binary);
178  xywh.push_back(xywh1);
179 
180  adjusted_neighbors.insert(id1);
181  }
182 
183  // address overlaps
184  std::vector<uint8_t> neighborhood;
185  std::array<int32_t, 4> bounds = {std::numeric_limits<int>::max(),
186  std::numeric_limits<int>::max(), -1, -1};
187  for (auto &_xywh : xywh) {
188  if (_xywh[0] < bounds[0]) {
189  bounds[0] = _xywh[0];
190  } // xmin
191  if (_xywh[1] < bounds[1]) {
192  bounds[1] = _xywh[1];
193  } // ymin
194  if (_xywh[2] + _xywh[0] - 1 > bounds[2]) {
195  bounds[2] = _xywh[2] + _xywh[0] - 1;
196  } // xmax
197  if (_xywh[3] + _xywh[1] - 1 > bounds[3]) {
198  bounds[3] = _xywh[3] + _xywh[1] - 1;
199  } // ymax
200  }
201 
202  bounds[2] = bounds[2] - bounds[0] + 1; // w
203  bounds[3] = bounds[3] - bounds[1] + 1; // h
204 
205  // build joined neighborhood map
206  neighborhood.resize(
207  static_cast<std::size_t>(bounds[2]) * static_cast<std::size_t>(bounds[3]), 0);
208 
209  for (int i = 0; i < full_neighborhood.size(); ++i) {
210  for (int y = 0; y < xywh[i][3]; ++y) {
211  for (int x = 0; x < xywh[i][2]; ++x) {
212  int global_y = y + xywh[i][1] - bounds[1];
213  int global_x = x + xywh[i][0] - bounds[0];
214  uint8_t val = full_neighborhood[i][y * xywh[i][2] + x];
215  if (val != 0) {
216  neighborhood[global_y * bounds[2] + global_x] = (i + 1);
217  }
218  }
219  }
220  }
221 
222  // 0 = background, 1 = this node, 2+ = neighboring nodes
223 
224  // find touching edges
225  for (int y = 0; y < bounds[3]; ++y) {
226  for (int x = 0; x < bounds[2]; ++x) {
227  uint8_t val = neighborhood[y * bounds[2] + x];
228  // check neighbors
229  if (val == 1) {
230  for (int32_t k{0}; k < 8; ++k) {
231  int32_t nx{x + dirs[k][0]};
232  int32_t ny{y + dirs[k][1]};
233  nx = std::clamp(nx, 0, bounds[2] - 1);
234  ny = std::clamp(ny, 0, bounds[3] - 1);
235  uint8_t n_val = neighborhood[ny * bounds[2] + nx];
236  if ((n_val != val) & (n_val != 0)) {
237  // need a smarter approach to prevent pinching
238  bool is_too_thin = false;
239  // check around (nx,ny) if we can stretch into another region,
240  // then it's too thin
241  for (int32_t k{0}; k < 8; ++k) {
242  int32_t mx{nx + dirs[k][0]};
243  int32_t my{ny + dirs[k][1]};
244  mx = std::clamp(mx, 0, bounds[2] - 1);
245  my = std::clamp(my, 0, bounds[3] - 1);
246  uint8_t m_val = neighborhood[my * bounds[2] + mx];
247 
248  if ((m_val != val) & (m_val != n_val)) {
249  is_too_thin = true;
250  }
251  }
252 
253  if (is_too_thin) {
254  considered_neigbors[n_val - 2]->add_edge_pixel(
255  XY{x + bounds[0], y + bounds[1]});
256  } else {
257  n->add_edge_pixel(XY{nx + bounds[0], ny + bounds[1]});
258  }
259  }
260  }
261  }
262  }
263  }
264  }
265 
266  // ask each Node to compute contours
267  for (const Node_ptr &n : get_nodes()) {
268  if (n->area() == 0) continue;
269  n->compute_contour();
270  }
271 
272  // smoothing
273  std::vector<std::vector<Point>> all_contours;
274  for (const Node_ptr &n : get_nodes()) {
275  if (n->area() == 0) continue;
276 
277  ColoredContours *c0 = &n->m_contours;
278  for (size_t i = 0; i < c0->contours.size(); ++i) {
279  all_contours.push_back(c0->contours[i]);
280  }
281  }
282 
283  contours::coupled_smooth(
284  all_contours, Rect{0.0f, 0.0f, static_cast<float>(m_width), static_cast<float>(m_height)});
285 
286  std::vector<std::vector<QuadBezier>> all_curves;
287  fit_curve_reduction(all_contours, all_curves, 0.5f);
288 
289  int j = 0;
290  for (const Node_ptr &n : get_nodes()) {
291  if (n->area() == 0) continue;
292 
293  ColoredContours *c0 = &n->m_contours;
294  for (size_t i = 0; i < c0->contours.size(); ++i) {
295  std::copy(all_contours[j].begin(), all_contours[j].end(), c0->contours[i].begin());
296 
297  c0->curves[i].resize(all_curves[j].size());
298  std::copy(all_curves[j].begin(), all_curves[j].end(), c0->curves[i].begin());
299  j++;
300  }
301  }
302 }
303 
304 void Graph::merge_small_area_nodes(const int32_t min_area) {
305  int32_t counter{0};
306  while (!all_areas_bigger_than(min_area)) {
307  for (const Node_ptr &n : get_nodes()) {
308  if (n->area() < min_area) {
309  std::vector<Node_ptr> neighbors;
310  neighbors.reserve(n->num_edges());
311  std::copy(n->edges().begin(), n->edges().end(), std::back_inserter(neighbors));
312 
313  ImageLib::RGBPixel<uint8_t> col = n->color();
314  // sort by size and color similarity
315  std::sort(neighbors.begin(), neighbors.end(), [col](Node_ptr a, Node_ptr b) {
316  float cdista = ImageLib::RGBPixel<uint8_t>::colorDistance(a->color(), col);
317  float cdistb = ImageLib::RGBPixel<uint8_t>::colorDistance(b->color(), col);
318  return (static_cast<float>(a->area()) + 10.f * cdista) <
319  (static_cast<float>(b->area()) + 10.f * cdistb);
320  });
321 
322  int32_t idx{0};
323  // find first non-zero area neighbor
324  for (Node_ptr &ne : neighbors) {
325  if (ne->area() > 0) {
326  break;
327  }
328  ++idx;
329  }
330 
331  // no valid neighbor found, skip this node
332  if (idx >= static_cast<int32_t>(neighbors.size())) {
333  continue;
334  }
335 
336  if (neighbors[idx]->area() >= n->area()) {
337  merge_nodes(neighbors[idx], n);
338  } else {
339  merge_nodes(n, neighbors[idx]);
340  }
341  }
342  }
343 
344  clear_unconnected_nodes();
345  ++counter;
346  }
347 }
Definition: contours.h:22
Definition: node.h:38