Img2Num C++ (Internal Developer Docs)  dev
API Documentation
kmeans.cpp
1 #include <algorithm>
2 #include <cmath>
3 #include <cstdlib>
4 #include <cstring>
5 #include <ctime>
6 #include <functional>
7 #include <limits>
8 #include <numeric>
9 #include <random>
10 #include <vector>
11 
12 #include "img2num.h"
13 #include "internal/Image.h"
14 #include "internal/LABAPixel.h"
15 #include "internal/PixelConverters.h"
16 #include "internal/RGBAPixel.h"
17 #include "internal/cielab.h"
18 
19 static constexpr uint8_t COLOR_SPACE_OPTION_CIELAB{0};
20 static constexpr uint8_t COLOR_SPACE_OPTION_RGB{1};
21 
22 // The K-Means++ Initialization Function
23 template <typename PixelT>
24 void kMeansPlusPlusInit(const ImageLib::Image<PixelT> &pixels,
25  ImageLib::Image<PixelT> &out_centroids, int k) {
26  std::vector<PixelT> centroids;
27 
28  int num_pixels = pixels.getSize();
29  // Random number generator setup
30  std::random_device rd;
31  std::mt19937 gen(rd());
32 
33  // --- Step 1: Choose the first centroid uniformly at random ---
34  std::uniform_int_distribution<> dis(0, num_pixels - 1);
35  int first_index = dis(gen);
36  centroids.push_back(pixels[first_index]);
37 
38  // Vector to store the squared distance of each pixel to its NEAREST existing
39  // centroid. Initialize with max double so the first distance calculation
40  // always updates it.
41  std::vector<double> min_dist_sq(num_pixels, std::numeric_limits<double>::max());
42 
43  // --- Step 2 & 3: Repeat until we have k centroids ---
44  for (int i = 1; i < k; ++i) {
45  double sum_dist_sq = 0.0;
46 
47  // Update distances relative to the LAST added centroid (centroids.back())
48  // We don't need to recheck previous centroids; min_dist_sq already holds
49  // the best distance to them.
50  for (int j = 0; j < num_pixels; ++j) {
51  double d = PixelT::colorDistance(pixels[j], centroids.back());
52 
53  // If this new centroid is closer than the previous best, update the min
54  // distance
55  if (d < min_dist_sq[j]) {
56  min_dist_sq[j] = d;
57  }
58  sum_dist_sq += min_dist_sq[j];
59  }
60 
61  // --- Step 3: Choose new center with probability proportional to D(x)^2 ---
62  // We use a weighted random selection (Roulette Wheel Selection)
63  std::uniform_real_distribution<> dist_selector(0.0, sum_dist_sq);
64  double random_value = dist_selector(gen);
65 
66  double current_sum = 0.0;
67  int selected_index = -1;
68 
69  // Iterate to find the pixel corresponding to the random_value
70  for (int j = 0; j < num_pixels; ++j) {
71  current_sum += min_dist_sq[j];
72  if (current_sum >= random_value) {
73  selected_index = j;
74  break;
75  }
76  }
77 
78  // Fallback for floating point rounding errors (pick last one if loop
79  // finishes)
80  if (selected_index == -1) {
81  selected_index = num_pixels - 1;
82  }
83 
84  centroids.push_back(pixels[selected_index]);
85  }
86 
87  std::copy(centroids.begin(), centroids.end(), out_centroids.begin());
88 }
89 
90 namespace img2num {
91 void kmeans(const uint8_t *data, uint8_t *out_data, int32_t *out_labels, const int32_t width,
92  const int32_t height, const int32_t k, const int32_t max_iter,
93  const uint8_t color_space) {
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 }
236 } // namespace img2num
Core image processing functions for img2num project.
Definition: Error.h:7
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.
Definition: kmeans.cpp:91