Img2Num C++ (Internal Developer Docs) dev
API Documentation
Loading...
Searching...
No Matches
kmeans.cpp
1#include <algorithm>
2#include <cmath>
3#include <cstdlib>
4#include <cstring>
5#include <ctime>
6#include <functional>
7#include <iostream>
8#include <limits>
9#include <numeric>
10#include <random>
11#include <vector>
12
13#include "img2num.h"
14#include "internal/Image.h"
15#include "internal/LABAPixel.h"
16#include "internal/PixelConverters.h"
17#include "internal/RGBAPixel.h"
18#include "internal/cielab.h"
19#include "internal/gpu.h"
20#include "internal/kmeans_gpu.h"
21
22static constexpr uint8_t COLOR_SPACE_OPTION_CIELAB{0};
23static constexpr uint8_t COLOR_SPACE_OPTION_RGB{1};
24
25// The K-Means++ Initialization Function
26template <typename PixelT>
27void kMeansPlusPlusInit(const ImageLib::Image<PixelT> &pixels,
28 ImageLib::Image<PixelT> &out_centroids, int k) {
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(const uint8_t *data, uint8_t *out_data, int32_t *out_labels, const int32_t width,
94 const int32_t height, const int32_t k, const int32_t max_iter,
95 const uint8_t color_space) {
97 pixels.loadFromBuffer(data, width, height, ImageLib::RGBA_CONVERTER<float>);
98 const int32_t num_pixels{pixels.getSize()};
99
100 // width = k, height = 1
101 // k centroids, initialized to rgba(0,0,0,255)
102 // Init of each pixel is from default in Image constructor
105 std::vector<int32_t> labels(num_pixels, 0);
106
107 ImageLib::Image<ImageLib::LABAPixel<float>> lab(pixels.getWidth(), pixels.getHeight());
108 if (color_space == COLOR_SPACE_OPTION_CIELAB) {
109 for (int i{0}; i < pixels.getSize(); ++i) {
110 rgb_to_lab<float, float>(pixels[i], lab[i]);
111 }
112 }
113
114 // Step 2: Initialize centroids randomly
115
116 switch (color_space) {
117 case COLOR_SPACE_OPTION_RGB: {
118 kMeansPlusPlusInit<ImageLib::RGBAPixel<float>>(pixels, centroids, k);
119 break;
120 }
121 case COLOR_SPACE_OPTION_CIELAB: {
122 kMeansPlusPlusInit<ImageLib::LABAPixel<float>>(lab, centroids_lab, k);
123 break;
124 }
125 }
126
127 // Step 3: Run k-means iterations
128
129 // Assignment step
130 for (int32_t iter{0}; iter < max_iter; ++iter) {
131 bool changed{false};
132
133 // Iterate over pixels
134 for (int32_t i{0}; i < num_pixels; ++i) {
135 float min_color_dist{std::numeric_limits<float>::max()};
136 int32_t best_cluster{0};
137
138 // Iterate over centroids to find centroid with most similar color to
139 // pixels[i]
140 float dist;
141 for (int32_t j{0}; j < k; ++j) {
142 switch (color_space) {
143 case COLOR_SPACE_OPTION_RGB: {
144 dist = ImageLib::RGBAPixel<float>::colorDistance(pixels[i], centroids[j]);
145 break;
146 }
147 case COLOR_SPACE_OPTION_CIELAB: {
148 dist = ImageLib::LABAPixel<float>::colorDistance(lab[i], centroids_lab[j]);
149 break;
150 }
151 }
152 if (dist < min_color_dist) {
153 min_color_dist = dist;
154 best_cluster = j;
155 }
156 }
157
158 if (labels[i] != best_cluster) {
159 changed = true;
160 labels[i] = best_cluster;
161 }
162 }
163
164 // Stop if no changes
165 if (!changed) {
166 break;
167 }
168
169 // Update step
170 ImageLib::Image<ImageLib::RGBAPixel<float>> new_centroids(k, 1, 0);
171 ImageLib::Image<ImageLib::LABAPixel<float>> new_centroids_lab(k, 1, 0);
172 std::vector<int32_t> counts(k, 0);
173
174 for (int32_t i = 0; i < num_pixels; ++i) {
175 int32_t cluster = labels[i];
176 switch (color_space) {
177 case COLOR_SPACE_OPTION_RGB: {
178 new_centroids[cluster].red += pixels[i].red;
179 new_centroids[cluster].green += pixels[i].green;
180 new_centroids[cluster].blue += pixels[i].blue;
181 break;
182 }
183 case COLOR_SPACE_OPTION_CIELAB: {
184 new_centroids_lab[cluster].l += lab[i].l;
185 new_centroids_lab[cluster].a += lab[i].a;
186 new_centroids_lab[cluster].b += lab[i].b;
187 break;
188 }
189 }
190 counts[cluster]++;
191 }
192
193 for (int32_t j = 0; j < k; ++j) {
194 /*
195 A centroid may become a dead centroid if it never gets pixels assigned
196 to it. May be good idea to reinitialize these dead centroids.
197 */
198 if (counts[j] > 0) {
199 switch (color_space) {
200 case COLOR_SPACE_OPTION_RGB: {
201 centroids[j].red = new_centroids[j].red / counts[j];
202 centroids[j].green = new_centroids[j].green / counts[j];
203 centroids[j].blue = new_centroids[j].blue / counts[j];
204 break;
205 }
206 case COLOR_SPACE_OPTION_CIELAB: {
207 centroids_lab[j].l = new_centroids_lab[j].l / counts[j];
208 centroids_lab[j].a = new_centroids_lab[j].a / counts[j];
209 centroids_lab[j].b = new_centroids_lab[j].b / counts[j];
210 break;
211 }
212 }
213 }
214 }
215 }
216
217 if (color_space == COLOR_SPACE_OPTION_CIELAB) {
218 for (int32_t i{0}; i < k; ++i) {
219 lab_to_rgb<float, float>(centroids_lab[i], centroids[i]);
220 }
221 }
222
223 // Write the final centroid values to each pixel in the cluster
224 for (int32_t i = 0; i < num_pixels; ++i) {
225 const int32_t cluster = labels[i];
226 out_data[i * 4 + 0] =
227 static_cast<uint8_t>(std::clamp(centroids[cluster].red, 0.0f, 255.0f));
228 out_data[i * 4 + 1] =
229 static_cast<uint8_t>(std::clamp(centroids[cluster].green, 0.0f, 255.0f));
230 out_data[i * 4 + 2] =
231 static_cast<uint8_t>(std::clamp(centroids[cluster].blue, 0.0f, 255.0f));
232 out_data[i * 4 + 3] = 255;
233 }
234
235 // Write labels to out_labels
236 std::memcpy(out_labels, labels.data(), labels.size() * sizeof(int32_t));
237}
238
239namespace img2num {
240void kmeans(const uint8_t *data, uint8_t *out_data, int32_t *out_labels, const int32_t width,
241 const int32_t height, const int32_t k, const int32_t max_iter,
242 const uint8_t color_space) {
243 GPU::getClassInstance().init_gpu();
244
245 if (GPU::getClassInstance().is_initialized()) {
246 std::cout << "kmeans gpu" << std::endl;
247 kmeans_gpu(data, out_data, out_labels, width, height, k, max_iter, color_space);
248 } else {
249 std::cout << "kmeans cpu" << std::endl;
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:240