Img2Num C++ (Internal Developer Docs) dev
API Documentation
Loading...
Searching...
No Matches
kmeans.cpp
1#include "img2num.h"
2#include "internal/cielab.h"
3#include "internal/gpu.h"
4#include "internal/Image.h"
5#include "internal/kmeans_gpu.h"
6#include "internal/LABAPixel.h"
7#include "internal/PixelConverters.h"
8#include "internal/RGBAPixel.h"
9
10#include <algorithm>
11#include <cmath>
12#include <cstdlib>
13#include <cstring>
14#include <ctime>
15#include <functional>
16#include <limits>
17#include <numeric>
18#include <random>
19#include <vector>
20
21static constexpr uint8_t COLOR_SPACE_OPTION_CIELAB {0};
22static constexpr uint8_t COLOR_SPACE_OPTION_RGB {1};
23
24// The K-Means++ Initialization Function
25template <typename PixelT>
26void kMeansPlusPlusInit(
27 const ImageLib::Image<PixelT>& pixels, ImageLib::Image<PixelT>& out_centroids, int k
28) {
29 std::vector<PixelT> centroids;
30
31 int num_pixels = pixels.getSize();
32 // Random number generator setup
33 std::random_device rd;
34 std::mt19937 gen(rd());
35
36 // --- Step 1: Choose the first centroid uniformly at random ---
37 std::uniform_int_distribution<> dis(0, num_pixels - 1);
38 int first_index = dis(gen);
39 centroids.push_back(pixels[first_index]);
40
41 // Vector to store the squared distance of each pixel to its NEAREST existing
42 // centroid. Initialize with max double so the first distance calculation
43 // always updates it.
44 std::vector<double> min_dist_sq(num_pixels, std::numeric_limits<double>::max());
45
46 // --- Step 2 & 3: Repeat until we have k centroids ---
47 for (int i = 1; i < k; ++i) {
48 double sum_dist_sq = 0.0;
49
50 // Update distances relative to the LAST added centroid (centroids.back())
51 // We don't need to recheck previous centroids; min_dist_sq already holds
52 // the best distance to them.
53 for (int j = 0; j < num_pixels; ++j) {
54 double d = PixelT::colorDistance(pixels[j], centroids.back());
55
56 // If this new centroid is closer than the previous best, update the min
57 // distance
58 if (d < min_dist_sq[j]) {
59 min_dist_sq[j] = d;
60 }
61 sum_dist_sq += min_dist_sq[j];
62 }
63
64 // --- Step 3: Choose new center with probability proportional to D(x)^2 ---
65 // We use a weighted random selection (Roulette Wheel Selection)
66 std::uniform_real_distribution<> dist_selector(0.0, sum_dist_sq);
67 double random_value = dist_selector(gen);
68
69 double current_sum = 0.0;
70 int selected_index = -1;
71
72 // Iterate to find the pixel corresponding to the random_value
73 for (int j = 0; j < num_pixels; ++j) {
74 current_sum += min_dist_sq[j];
75 if (current_sum >= random_value) {
76 selected_index = j;
77 break;
78 }
79 }
80
81 // Fallback for floating point rounding errors (pick last one if loop
82 // finishes)
83 if (selected_index == -1) {
84 selected_index = num_pixels - 1;
85 }
86
87 centroids.push_back(pixels[selected_index]);
88 }
89
90 std::copy(centroids.begin(), centroids.end(), out_centroids.begin());
91}
92
93void kmeans_cpu(
94 const uint8_t* data, uint8_t* out_data, int32_t* out_labels, const int32_t width,
95 const int32_t height, const int32_t k, const int32_t max_iter, const uint8_t color_space
96) {
98 pixels.loadFromBuffer(data, width, height, ImageLib::RGBA_CONVERTER<float>);
99 const int32_t num_pixels {pixels.getSize()};
100
101 // width = k, height = 1
102 // k centroids, initialized to rgba(0,0,0,255)
103 // Init of each pixel is from default in Image constructor
105 ImageLib::Image<ImageLib::LABAPixel<float>> centroids_lab {k, 1};
106 std::vector<int32_t> labels(num_pixels, 0);
107
108 ImageLib::Image<ImageLib::LABAPixel<float>> lab(pixels.getWidth(), pixels.getHeight());
109 if (color_space == COLOR_SPACE_OPTION_CIELAB) {
110 for (int i {0}; i < pixels.getSize(); ++i) {
111 rgb_to_lab<float, float>(pixels[i], lab[i]);
112 }
113 }
114
115 // Step 2: Initialize centroids randomly
116
117 switch (color_space) {
118 case COLOR_SPACE_OPTION_RGB: {
119 kMeansPlusPlusInit<ImageLib::RGBAPixel<float>>(pixels, centroids, k);
120 break;
121 }
122 case COLOR_SPACE_OPTION_CIELAB: {
123 kMeansPlusPlusInit<ImageLib::LABAPixel<float>>(lab, centroids_lab, k);
124 break;
125 }
126 }
127
128 // Step 3: Run k-means iterations
129
130 // Assignment step
131 for (int32_t iter {0}; iter < max_iter; ++iter) {
132 bool changed {false};
133
134 // Iterate over pixels
135 for (int32_t i {0}; i < num_pixels; ++i) {
136 float min_color_dist {std::numeric_limits<float>::max()};
137 int32_t best_cluster {0};
138
139 // Iterate over centroids to find centroid with most similar color to
140 // pixels[i]
141 float dist;
142 for (int32_t j {0}; j < k; ++j) {
143 switch (color_space) {
144 case COLOR_SPACE_OPTION_RGB: {
145 dist = ImageLib::RGBAPixel<float>::colorDistance(pixels[i], centroids[j]);
146 break;
147 }
148 case COLOR_SPACE_OPTION_CIELAB: {
149 dist = ImageLib::LABAPixel<float>::colorDistance(lab[i], centroids_lab[j]);
150 break;
151 }
152 }
153 if (dist < min_color_dist) {
154 min_color_dist = dist;
155 best_cluster = j;
156 }
157 }
158
159 if (labels[i] != best_cluster) {
160 changed = true;
161 labels[i] = best_cluster;
162 }
163 }
164
165 // Stop if no changes
166 if (!changed) {
167 break;
168 }
169
170 // Update step
171 ImageLib::Image<ImageLib::RGBAPixel<float>> new_centroids(k, 1, 0);
172 ImageLib::Image<ImageLib::LABAPixel<float>> new_centroids_lab(k, 1, 0);
173 std::vector<int32_t> counts(k, 0);
174
175 for (int32_t i = 0; i < num_pixels; ++i) {
176 int32_t cluster = labels[i];
177 switch (color_space) {
178 case COLOR_SPACE_OPTION_RGB: {
179 new_centroids[cluster].red += pixels[i].red;
180 new_centroids[cluster].green += pixels[i].green;
181 new_centroids[cluster].blue += pixels[i].blue;
182 break;
183 }
184 case COLOR_SPACE_OPTION_CIELAB: {
185 new_centroids_lab[cluster].l += lab[i].l;
186 new_centroids_lab[cluster].a += lab[i].a;
187 new_centroids_lab[cluster].b += lab[i].b;
188 break;
189 }
190 }
191 counts[cluster]++;
192 }
193
194 for (int32_t j = 0; j < k; ++j) {
195 /*
196 A centroid may become a dead centroid if it never gets pixels assigned
197 to it. May be good idea to reinitialize these dead centroids.
198 */
199 if (counts[j] > 0) {
200 switch (color_space) {
201 case COLOR_SPACE_OPTION_RGB: {
202 centroids[j].red = new_centroids[j].red / counts[j];
203 centroids[j].green = new_centroids[j].green / counts[j];
204 centroids[j].blue = new_centroids[j].blue / counts[j];
205 break;
206 }
207 case COLOR_SPACE_OPTION_CIELAB: {
208 centroids_lab[j].l = new_centroids_lab[j].l / counts[j];
209 centroids_lab[j].a = new_centroids_lab[j].a / counts[j];
210 centroids_lab[j].b = new_centroids_lab[j].b / counts[j];
211 break;
212 }
213 }
214 }
215 }
216 }
217
218 if (color_space == COLOR_SPACE_OPTION_CIELAB) {
219 for (int32_t i {0}; i < k; ++i) {
220 lab_to_rgb<float, float>(centroids_lab[i], centroids[i]);
221 }
222 }
223
224 // Write the final centroid values to each pixel in the cluster
225 for (int32_t i = 0; i < num_pixels; ++i) {
226 const int32_t cluster = labels[i];
227 out_data[i * 4 + 0] =
228 static_cast<uint8_t>(std::clamp(centroids[cluster].red, 0.0f, 255.0f));
229 out_data[i * 4 + 1] =
230 static_cast<uint8_t>(std::clamp(centroids[cluster].green, 0.0f, 255.0f));
231 out_data[i * 4 + 2] =
232 static_cast<uint8_t>(std::clamp(centroids[cluster].blue, 0.0f, 255.0f));
233 out_data[i * 4 + 3] = 255;
234 }
235
236 // Write labels to out_labels
237 std::memcpy(out_labels, labels.data(), labels.size() * sizeof(int32_t));
238}
239
240namespace img2num {
242 const uint8_t* data, uint8_t* out_data, int32_t* out_labels, const int32_t width,
243 const int32_t height, const int32_t k, const int32_t max_iter, const uint8_t color_space
244) {
245 GPU::getClassInstance().init_gpu();
246
247 if (GPU::getClassInstance().is_initialized()) {
248 kmeans_gpu(data, out_data, out_labels, width, height, k, max_iter, color_space);
249 } else {
250 kmeans_cpu(data, out_data, out_labels, width, height, k, max_iter, color_space);
251 }
252}
253} // namespace img2num
Core image processing functions for img2num project.
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:241