Img2Num C++ (Internal Developer Docs)  dev
API Documentation
img2num Namespace Reference

Enumerations

enum class  Error {
  OK = 0 , BAD_ALLOC = 1 , INVALID_ARGUMENT = 2 , RUNTIME = 3 ,
  UNKNOWN = 4
}
 

Functions

Error get_last_error ()
 
const std::string get_last_error_message ()
 
void clear_last_error ()
 
void set_error (Error code, const std::string message)
 
template<typename Func , typename... Args>
void clear_last_error_and_catch (Func &&exception_prone_func, Args &&... args)
 
void gaussian_blur_fft (uint8_t *image, size_t width, size_t height, double sigma)
 Apply a Gaussian blur to an image using FFT. More...
 
void invert_image (uint8_t *ptr, int width, int height)
 Invert the pixel values of an image. More...
 
void threshold_image (uint8_t *ptr, const int width, const int height, const int num_thresholds)
 Apply a thresholding operation to an image. More...
 
void black_threshold_image (uint8_t *ptr, const int width, const int height, const int num_thresholds)
 Apply black-thresholding to an image. More...
 
void kmeans (const uint8_t *data, uint8_t *out_data, int32_t *out_labels, const int32_t width, const int32_t height, const int32_t k, const int32_t max_iter, const uint8_t color_space)
 Perform k-means clustering on image data. More...
 
void bilateral_filter (uint8_t *image, size_t width, size_t height, double sigma_spatial, double sigma_range, uint8_t color_space)
 Apply bilateral filtering to an image. More...
 
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. More...
 

Variables

thread_local Error last_error {Error::OK}
 
thread_local std::string last_error_message {}
 

Detailed Description

Note
All image buffers are assumed to be stored in row-major order, unless otherwise noted.

Function Documentation

◆ bilateral_filter()

void img2num::bilateral_filter ( uint8_t *  image,
size_t  width,
size_t  height,
double  sigma_spatial,
double  sigma_range,
uint8_t  color_space 
)

Apply bilateral filtering to an image.

Parameters
imagePointer to RGBA pixel buffer.
widthWidth of the image in pixels.
heightHeight of the image in pixels.
sigma_spatialStandard deviation for spatial Gaussian (proximity weight).
sigma_rangeStandard deviation for range Gaussian (intensity similarity weight).
color_spaceColor space flag (0 = CIE LAB, 1 = RGB).
Note
The filter modifies the image buffer in-place.
Dox File: doxygen/img2num.h.dox

Definition at line 155 of file bilateral_filter.cpp.

156  {
157  // bad data -> return
158  if (sigma_spatial <= 0.0 || sigma_range <= 0.0 || width <= 0 || height <= 0) return;
159  if (color_space != COLOR_SPACE_OPTION_CIELAB && color_space != COLOR_SPACE_OPTION_RGB) return;
160 
161  const int raw_radius{static_cast<int>(std::ceil(SIGMA_RADIUS_FACTOR * sigma_spatial))};
162  const int radius{std::min(raw_radius, MAX_KERNEL_RADIUS)};
163  const int kernel_diameter{2 * radius + 1};
164 
165  std::vector<uint8_t> result(width * height * 4);
166 
167  std::vector<double> spatial_weights(kernel_diameter * kernel_diameter);
168 
169  // Precompute Spatial Weights (Gaussian Kernel)
170  for (int ky{-radius}; ky <= radius; ++ky) {
171  for (int kx{-radius}; kx <= radius; ++kx) {
172  const double dist{static_cast<double>(std::sqrt(kx * kx + ky * ky))};
173  spatial_weights[(ky + radius) * kernel_diameter + (kx + radius)] =
174  gaussian(dist, sigma_spatial);
175  }
176  }
177 
178  // ========= RGB-only section start =========
179  // Precompute Range Weights
180  std::vector<double> range_lut;
181  if (color_space == COLOR_SPACE_OPTION_RGB) {
182  range_lut.resize(MAX_RGB_DIST_SQ + 1);
183  for (int i{0}; i <= MAX_RGB_DIST_SQ; ++i) {
184  range_lut[i] = gaussian(static_cast<double>(std::sqrt(i)), sigma_range);
185  }
186  }
187  // ========= RGB-only section end =========
188 
189  // ========= CIELAB section start =========
190  // Compute full image RGB - CIELAB conversion
191  std::vector<double> cie_image;
192  if (color_space == COLOR_SPACE_OPTION_CIELAB) {
193  cie_image.resize(width * height * 4);
194 
195  for (int y{0}; y < height; y++) {
196  for (int x{0}; x < width; x++) {
197  int center_idx{(y * static_cast<int>(width) + x) * 4};
198  uint8_t r0{image[center_idx]};
199  uint8_t g0{image[center_idx + 1]};
200  uint8_t b0{image[center_idx + 2]};
201  uint8_t a0{image[center_idx + 3]};
202  double L0, A0, B0;
203  rgb_to_lab<uint8_t, double>(r0, g0, b0, L0, A0, B0);
204 
205  cie_image[center_idx] = L0;
206  cie_image[center_idx + 1] = A0;
207  cie_image[center_idx + 2] = B0;
208  cie_image[center_idx + 3] = 0.0; // unused but keep for indexing purposes
209  }
210  }
211  }
212  // ========= CIELAB section end =========
213 
214  _process(image, cie_image, result, spatial_weights, range_lut, radius, sigma_range, 0,
215  static_cast<int>(height), height, width, color_space);
216 
217  std::memcpy(image, result.data(), result.size());
218 }

◆ black_threshold_image()

void img2num::black_threshold_image ( uint8_t *  ptr,
const int  width,
const int  height,
const int  num_thresholds 
)

Apply black-thresholding to an image.

Parameters
ptrPointer to the image buffer.
widthWidth of the image in pixels.
heightHeight of the image in pixels.
num_thresholdsNumber of thresholds to apply.
Note
Similar to threshold_image but prioritizes darker pixels.
Dox File: doxygen/img2num.h.dox

Definition at line 130 of file image_utils.cpp.

131  {
133  img.loadFromBuffer(ptr, width, height, ImageLib::RGBA_CONVERTER<uint8_t>);
134 
135  const auto imgWidth{img.getWidth()}, imgHeight{img.getHeight()};
136  for (ImageLib::RGBAPixel<uint8_t> &p : img) {
137  const bool R{p.red < num_thresholds};
138  const bool G{p.green < num_thresholds};
139  const bool B{p.blue < num_thresholds};
140  if (R && B && G) {
141  p.setGray(0);
142  }
143  }
144 
145  const auto &modified = img.getData();
146  std::memcpy(ptr, modified.data(), modified.size() * sizeof(ImageLib::RGBAPixel<uint8_t>));
147 }

◆ gaussian_blur_fft()

void img2num::gaussian_blur_fft ( uint8_t *  image,
size_t  width,
size_t  height,
double  sigma 
)

Apply a Gaussian blur to an image using FFT.

Parameters
imagePointer to the image buffer (RGBA).
widthWidth of the image in pixels.
heightHeight of the image in pixels.
sigmaStandard deviation for Gaussian kernel.
Note
The operation modifies the image buffer in-place.
Dox File: doxygen/img2num.h.dox

Definition at line 43 of file image_utils.cpp.

43  {
44  if (!image || width == 0 || height == 0 || sigma_pixels <= 0) return;
45 
46  const size_t Npix = width * height;
47 
48  // Compute padded dimensions (next power of two)
49  const size_t W = fft::next_power_of_two(width);
50  const size_t H = fft::next_power_of_two(height);
51  const size_t Npix_padded = W * H;
52 
53  // Frequency coordinates helper (DC at corner)
54  auto freq_coord = [](int k, int dim) -> double {
55  return (k <= dim / 2) ? double(k) / dim : double(k - dim) / dim;
56  };
57 
58  // Precompute Gaussian factor in frequency domain
59  const double two_pi2_sigma2 = 2.0 * M_PI * M_PI * sigma_pixels * sigma_pixels;
60 
61  for (int channel = 0; channel < 3; channel++) {
62  // Allocate padded buffer
63  std::vector<fft::cd> data(Npix_padded, {0.0, 0.0});
64 
65  // Copy original image channel into padded buffer
66  for (size_t y = 0; y < height; y++)
67  for (size_t x = 0; x < width; x++)
68  data[y * W + x] = fft::cd(image[(y * width + x) * 4 + channel], 0.0);
69 
70  // Forward 2D FFT
71  fft::iterative_fft_2d(data, W, H, false);
72 
73  // Apply Gaussian filter in frequency domain
74  for (size_t y = 0; y < H; y++) {
75  double fy2 = freq_coord(y, H) * freq_coord(y, H);
76  for (size_t x = 0; x < W; x++) {
77  double fx2 = freq_coord(x, W) * freq_coord(x, W);
78  double gain = std::exp(-two_pi2_sigma2 * (fx2 + fy2));
79  data[y * W + x] *= gain;
80  }
81  }
82 
83  // Inverse 2D FFT
84  fft::iterative_fft_2d(data, W, H, true);
85 
86  // Copy back only the original width/height and clamp
87  for (size_t y = 0; y < height; y++)
88  for (size_t x = 0; x < width; x++) {
89  double v = data[y * W + x].real();
90  v = std::clamp(v, 0.0, 255.0);
91  image[(y * width + x) * 4 + channel] = static_cast<uint8_t>(std::lrint(v));
92  }
93  }
94 
95  // Alpha channel remains unchanged
96 }

◆ invert_image()

void img2num::invert_image ( uint8_t *  ptr,
int  width,
int  height 
)

Invert the pixel values of an image.

Parameters
ptrPointer to the image buffer.
widthWidth of the image in pixels.
heightHeight of the image in pixels.
Note
Each pixel value is replaced by 255 - original_value.
Dox File: doxygen/img2num.h.dox

Definition at line 99 of file image_utils.cpp.

99  {
101  img.loadFromBuffer(ptr, width, height, ImageLib::RGBA_CONVERTER<uint8_t>);
102 
103  for (ImageLib::RGBAPixel<uint8_t> &p : img) {
104  p.red = 255 - p.red;
105  p.blue = 255 - p.blue;
106  p.green = 255 - p.green;
107  }
108 
109  const auto &modified = img.getData();
110  std::memcpy(ptr, modified.data(), modified.size() * sizeof(ImageLib::RGBAPixel<uint8_t>));
111 }

◆ kmeans()

void img2num::kmeans ( const uint8_t *  data,
uint8_t *  out_data,
int32_t *  out_labels,
const int32_t  width,
const int32_t  height,
const int32_t  k,
const int32_t  max_iter,
const uint8_t  color_space 
)

Perform k-means clustering on image data.

Parameters
dataPointer to input image data buffer.
out_dataPointer to output buffer where clustered pixel values are stored.
out_labelsPointer to output buffer for cluster labels per pixel.
widthWidth of the image in pixels.
heightHeight of the image in pixels.
kNumber of clusters to compute.
max_iterMaximum number of iterations for the algorithm.
color_spaceColor space flag (0 = CIE LAB, 1 = RGB).
Note
The function does not modify the input buffer.
Dox File: doxygen/img2num.h.dox

Definition at line 91 of file kmeans.cpp.

93  {
95  pixels.loadFromBuffer(data, width, height, ImageLib::RGBA_CONVERTER<float>);
96  const int32_t num_pixels{pixels.getSize()};
97 
98  // width = k, height = 1
99  // k centroids, initialized to rgba(0,0,0,255)
100  // Init of each pixel is from default in Image constructor
102  ImageLib::Image<ImageLib::LABAPixel<float>> centroids_lab{k, 1};
103  std::vector<int32_t> labels(num_pixels, 0);
104 
105  ImageLib::Image<ImageLib::LABAPixel<float>> lab(pixels.getWidth(), pixels.getHeight());
106  if (color_space == COLOR_SPACE_OPTION_CIELAB) {
107  for (int i{0}; i < pixels.getSize(); ++i) {
108  rgb_to_lab<float, float>(pixels[i], lab[i]);
109  }
110  }
111 
112  // Step 2: Initialize centroids randomly
113 
114  switch (color_space) {
115  case COLOR_SPACE_OPTION_RGB: {
116  kMeansPlusPlusInit<ImageLib::RGBAPixel<float>>(pixels, centroids, k);
117  break;
118  }
119  case COLOR_SPACE_OPTION_CIELAB: {
120  kMeansPlusPlusInit<ImageLib::LABAPixel<float>>(lab, centroids_lab, k);
121  break;
122  }
123  }
124 
125  // Step 3: Run k-means iterations
126 
127  // Assignment step
128  for (int32_t iter{0}; iter < max_iter; ++iter) {
129  bool changed{false};
130 
131  // Iterate over pixels
132  for (int32_t i{0}; i < num_pixels; ++i) {
133  float min_color_dist{std::numeric_limits<float>::max()};
134  int32_t best_cluster{0};
135 
136  // Iterate over centroids to find centroid with most similar color to
137  // pixels[i]
138  float dist;
139  for (int32_t j{0}; j < k; ++j) {
140  switch (color_space) {
141  case COLOR_SPACE_OPTION_RGB: {
142  dist = ImageLib::RGBAPixel<float>::colorDistance(pixels[i], centroids[j]);
143  break;
144  }
145  case COLOR_SPACE_OPTION_CIELAB: {
146  dist = ImageLib::LABAPixel<float>::colorDistance(lab[i], centroids_lab[j]);
147  break;
148  }
149  }
150  if (dist < min_color_dist) {
151  min_color_dist = dist;
152  best_cluster = j;
153  }
154  }
155 
156  if (labels[i] != best_cluster) {
157  changed = true;
158  labels[i] = best_cluster;
159  }
160  }
161 
162  // Stop if no changes
163  if (!changed) {
164  break;
165  }
166 
167  // Update step
168  ImageLib::Image<ImageLib::RGBAPixel<float>> new_centroids(k, 1, 0);
169  ImageLib::Image<ImageLib::LABAPixel<float>> new_centroids_lab(k, 1, 0);
170  std::vector<int32_t> counts(k, 0);
171 
172  for (int32_t i = 0; i < num_pixels; ++i) {
173  int32_t cluster = labels[i];
174  switch (color_space) {
175  case COLOR_SPACE_OPTION_RGB: {
176  new_centroids[cluster].red += pixels[i].red;
177  new_centroids[cluster].green += pixels[i].green;
178  new_centroids[cluster].blue += pixels[i].blue;
179  break;
180  }
181  case COLOR_SPACE_OPTION_CIELAB: {
182  new_centroids_lab[cluster].l += lab[i].l;
183  new_centroids_lab[cluster].a += lab[i].a;
184  new_centroids_lab[cluster].b += lab[i].b;
185  break;
186  }
187  }
188  counts[cluster]++;
189  }
190 
191  for (int32_t j = 0; j < k; ++j) {
192  /*
193  A centroid may become a dead centroid if it never gets pixels assigned
194  to it. May be good idea to reinitialize these dead centroids.
195  */
196  if (counts[j] > 0) {
197  switch (color_space) {
198  case COLOR_SPACE_OPTION_RGB: {
199  centroids[j].red = new_centroids[j].red / counts[j];
200  centroids[j].green = new_centroids[j].green / counts[j];
201  centroids[j].blue = new_centroids[j].blue / counts[j];
202  break;
203  }
204  case COLOR_SPACE_OPTION_CIELAB: {
205  centroids_lab[j].l = new_centroids_lab[j].l / counts[j];
206  centroids_lab[j].a = new_centroids_lab[j].a / counts[j];
207  centroids_lab[j].b = new_centroids_lab[j].b / counts[j];
208  break;
209  }
210  }
211  }
212  }
213  }
214 
215  if (color_space == COLOR_SPACE_OPTION_CIELAB) {
216  for (int32_t i{0}; i < k; ++i) {
217  lab_to_rgb<float, float>(centroids_lab[i], centroids[i]);
218  }
219  }
220 
221  // Write the final centroid values to each pixel in the cluster
222  for (int32_t i = 0; i < num_pixels; ++i) {
223  const int32_t cluster = labels[i];
224  out_data[i * 4 + 0] =
225  static_cast<uint8_t>(std::clamp(centroids[cluster].red, 0.0f, 255.0f));
226  out_data[i * 4 + 1] =
227  static_cast<uint8_t>(std::clamp(centroids[cluster].green, 0.0f, 255.0f));
228  out_data[i * 4 + 2] =
229  static_cast<uint8_t>(std::clamp(centroids[cluster].blue, 0.0f, 255.0f));
230  out_data[i * 4 + 3] = 255;
231  }
232 
233  // Write labels to out_labels
234  std::memcpy(out_labels, labels.data(), labels.size() * sizeof(int32_t));
235 }

◆ labels_to_svg()

char * img2num::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.

Parameters
dataPointer to image data buffer.
labelsPointer to label buffer, indicating region for each pixel.
widthWidth of the image in pixels.
heightHeight of the image in pixels.
min_areaMinimum area (in pixels) for a region to be included in the SVG.
draw_contour_bordersIf true, contours of labeled regions will be drawn.
Returns
Pointer to a dynamically allocated C-string containing the SVG data.
Note
Caller is responsible for freeing the returned string.
Dox File: doxygen/img2num.h.dox

Definition at line 189 of file labels_to_svg.cpp.

190  {
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 }
Definition: graph.h:33

◆ threshold_image()

void img2num::threshold_image ( uint8_t *  ptr,
const int  width,
const int  height,
const int  num_thresholds 
)

Apply a thresholding operation to an image.

Parameters
ptrPointer to the image buffer.
widthWidth of the image in pixels.
heightHeight of the image in pixels.
num_thresholdsNumber of thresholds to apply.
Note
Thresholds split pixel intensity ranges into discrete levels.
Dox File: doxygen/img2num.h.dox

Definition at line 113 of file image_utils.cpp.

113  {
114  const uint8_t REGION_SIZE(255 / num_thresholds); // Size of buckets per colour
115 
117  img.loadFromBuffer(ptr, width, height, ImageLib::RGBA_CONVERTER<uint8_t>);
118 
119  const auto imgWidth{img.getWidth()}, imgHeight{img.getHeight()};
120  for (ImageLib::RGBAPixel<uint8_t> &p : img) {
121  p.red = quantize(p.red, REGION_SIZE);
122  p.green = quantize(p.green, REGION_SIZE);
123  p.blue = quantize(p.blue, REGION_SIZE);
124  }
125 
126  const auto &modified = img.getData();
127  std::memcpy(ptr, modified.data(), modified.size() * sizeof(ImageLib::RGBAPixel<uint8_t>));
128 }