diff --git a/CMakeLists.txt b/CMakeLists.txt index a5968df..f7ad32c 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,6 +13,7 @@ project(${PROJECT_NAME} VERSION ${PROJECT_VERSION}) list(APPEND CMAKE_MODULE_PATH "/opt/cmake") find_package(STB) +find_package(LIQ) include(FetchContent) FetchContent_Declare( @@ -51,3 +52,5 @@ target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_INCLUDE}) target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_LIBRARY}) target_compile_definitions(${PROJECT_NAME} PUBLIC ${PROJECT_DEFINITION}) set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/dist") + +install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin) diff --git a/src/header.hpp b/src/header.hpp index 7fb4b94..10f0687 100644 --- a/src/header.hpp +++ b/src/header.hpp @@ -46,29 +46,30 @@ enum Format Format_PALETTE_16, }; -struct Color { - double r = 0; - double g = 0; - double b = 0; - double a = 255; - - inline uint16_t toRGB16(uint8_t alpha_threshold = 128) const { - const uint16_t alpha_bit = (a >= alpha_threshold) ? 0b1000000000000000 : 0; - return alpha_bit - | (((uint8_t)r >> 3) & 0b00011111) - | (((uint8_t)g >> 3) & 0b00011111) << 5 - | (((uint8_t)b >> 3) & 0b00011111) << 10; - } -}; +static inline uint16_t RGB255_RGB16(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255, uint8_t alpha_threshold = 128) { + const uint16_t alpha_bit = (a >= alpha_threshold) ? 0b1000000000000000 : 0; + // const uint16_t alpha_bit = 0b1000000000000000; + return alpha_bit + | (((uint8_t)r >> 3) & 0b00011111) + | (((uint8_t)g >> 3) & 0b00011111) << 5 + | (((uint8_t)b >> 3) & 0b00011111) << 10; +} struct Image { int width = 0, height = 0; - std::vector data; + std::vector data; ~Image() {} void Load(const std::filesystem::path &path); }; +struct Color { + uint8_t r, g, b, a; +}; + +typedef std::vector ImageMapped; +typedef std::vector Palette; + struct Options { std::vector path_input; std::string path_output; @@ -78,11 +79,4 @@ struct Options { void Verify(); void Convert(); -std::vector QuantizePalette(const int numColor, const Image &image); -std::vector RemapImage(const Image &image, const std::vector &palette, const int &format); - -std::vector inc_online_kmeans ( const Image *img, const int num_colors, - const double lr_exp = 0.5, const double sample_rate = 0.5); - -/* Max. L_2^2 distance in 24-bit RGB space = 3 * 255 * 255 */ -#define MAX_RGB_DIST 195075 \ No newline at end of file +std::vector Quantize(const std::vector& images, Palette &palette_output, int num_colors, bool dither, uint8_t format); \ No newline at end of file diff --git a/src/kmean.cpp b/src/kmean.cpp deleted file mode 100644 index 5f4ac97..0000000 --- a/src/kmean.cpp +++ /dev/null @@ -1,341 +0,0 @@ -/* -https://github.com/AmberAbernathy/Color_Quantization/blob/main/test_km_algs.cpp - -Online K-Means (MacQueen, 1967) -Incremental Online K-Means (Abernathy & Celebi, 2022) - -Authors: Amber Abernathy & M. Emre Celebi - -Contact email: ecelebi@uca.edu - -If you find this program useful, please cite: -A. D. Abernathy and M. E. Celebi, -The Incremental Online K-Means Clustering Algorithm -and Its Application to Color Quantization, -Expert Systems with Applications, -in press, https://doi.org/10.1016/j.eswa.2022.117927, 2022. -*/ - -#include -#include -#include "header.hpp" - -typedef unsigned char uchar; -typedef unsigned int uint; -typedef unsigned long ulong; - -struct RGB_Cluster -{ - int size; - Color center; -}; - -/* Max. # colors that can be requested */ -#define MAX_NUM_COLORS 256 - -/* - Powers of two for 0, 1, ..., 16. Note that 2^16 must equal MAX_NUM_COLORS. - If you want to quantize to more than MAX_NUM_COLORS colors, extend the POW2 - array accordingly. - */ -static inline int POW2[] = { 1, 2, 4, 8, 16, 32, 64, 128, 256 }; - -/* - Function to generate two quasirandom numbers from - a 2D Sobol sequence. Adapted from Numerical Recipies - in C. Upon return, X and Y fall in [0,1). - */ - -#define MAX_BIT 30 - -void -sob_seq ( double *x, double *y ) -{ - int j, k, l; - ulong i, im, ipp; - static double fac; - static int init = 0; - static ulong ix1, ix2; - static ulong in, *iu[2 * MAX_BIT + 1]; - static ulong mdeg[3] = { 0, 1, 2 }; - static ulong ip[3] = { 0, 0, 1 }; - static ulong iv[2 * MAX_BIT + 1] = - { 0, 1, 1, 1, 1, 1, 1, 3, 1, 3, 3, 1, 1, 5, 7, 7, 3, 3, 5, 15, 11, 5, 15, 13, 9 }; - - if ( !init ) - { - init = 1; - for ( j = 1, k = 0; j <= MAX_BIT; j++, k += 2 ) - { - iu[j] = &iv[k]; - } - - for ( k = 1; k <= 2; k++ ) - { - for ( j = 1; j <= ( int ) mdeg[k]; j++ ) - { - iu[j][k] <<= ( MAX_BIT - j ); - } - - for ( j = mdeg[k] + 1; j <= MAX_BIT; j++ ) - { - ipp = ip[k]; - i = iu[j - mdeg[k]][k]; - i ^= ( i >> mdeg[k] ); - - for ( l = mdeg[k] - 1; l >= 1; l-- ) - { - if ( ipp & 1 ) - { - i ^= iu[j - l][k]; - } - - ipp >>= 1; - } - - iu[j][k] = i; - } - } - - fac = 1.0 / ( 1L << MAX_BIT ); - in = 0; - } - - im = in; - for ( j = 1; j <= MAX_BIT; j++ ) - { - if ( ! ( im & 1 ) ) - { - break; - } - - im >>= 1; - } - - im = (j - 1) * 2; - *x = (ix1 ^= iv[im + 1]) * fac; - *y = (ix2 ^= iv[im + 2]) * fac; - in++; -} - -#undef MAX_BIT - -/* - Function to determine if an integer is a power of 2: - http://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 -*/ - -bool -is_pow2 ( const int x ) -{ - uint ux = ( uint ) x; - - return ux && !( ux & ( ux - 1 ) ); -} - -/* - Online K-Means Algorithm: - S. Thompson, M. E. Celebi, and K. H. Buck, - Fast Color Quantization Using MacQueen’s K-Means Algorithm, - Journal of Real-Time Image Processing, - 17(5): 1609-1624, 2020. - - Notes: - 1) LR_EXP: Learning rate exponent (must be in [0.5, 1]) - 2) SAMPLE_RATE: Fraction of the input pixels (must be in (0, 1]) - used during the clustering process. - 3) CLUST: When the function is called, CLUST represents the initial - centers. Upon return, CLUST represents the final centers. - */ - -void -online_kmeans ( const Image *img, const int num_colors, const double lr_exp, - const double sample_rate, RGB_Cluster *clust ) -{ - int min_dist_index; - int old_size, new_size; - int row_idx, col_idx; - int num_samples; - double sob_x, sob_y; - double del_red, del_green, del_blue; - double dist, min_dist; - double learn_rate; - Color rand_pixel; - - if ( lr_exp < 0.5 || lr_exp > 1. ) - { - fprintf ( stderr, "Learning rate exponent (%g) must be in [0.5, 1]\n", lr_exp ); - exit ( EXIT_FAILURE ); - } - else if ( sample_rate <= 0.0 || sample_rate > 1. ) - { - fprintf ( stderr, "Sampling rate (%g) must be in (0, 1]\n", sample_rate ); - exit ( EXIT_FAILURE ); - } - - num_samples = ( int ) ( sample_rate * (img->width*img->height) + 0.5 ); /* round */ - - for ( int i = 0; i < num_samples; i++ ) - { - /* Sample the image quasirandomly based on a Sobol' sequence */ - sob_seq ( &sob_x, &sob_y ); - - /* Find the corresponding row/column indices */ - row_idx = ( int ) ( sob_y * img->height + 0.5 ); /* round */ - if ( row_idx == img->height ) - { - row_idx--; - } - - col_idx = ( int ) ( sob_x * img->width + 0.5 ); /* round */ - if ( col_idx == img->width ) - { - col_idx--; - } - - rand_pixel = img->data[row_idx * img->width + col_idx]; - - /* Find the nearest center */ - min_dist = MAX_RGB_DIST; - min_dist_index = -INT_MAX; - for ( int j = 0; j < num_colors; j++ ) - { - del_red = clust[j].center.r - rand_pixel.r; - del_green = clust[j].center.g - rand_pixel.g; - del_blue = clust[j].center.b - rand_pixel.b; - - dist = del_red * del_red + del_green * del_green + del_blue * del_blue; - if ( dist < min_dist ) - { - min_dist = dist; - min_dist_index = j; - } - } - - /* Update the size of the nearest cluster */ - old_size = clust[min_dist_index].size; - new_size = old_size + 1; - - /* Compute the learning rate */ - learn_rate = pow ( new_size, -lr_exp ); - - /* Update the center of the nearest cluster */ - clust[min_dist_index].center.r += learn_rate * - ( rand_pixel.r - clust[min_dist_index].center.r ); - clust[min_dist_index].center.g += learn_rate * - ( rand_pixel.g - clust[min_dist_index].center.g ); - clust[min_dist_index].center.b += learn_rate * - ( rand_pixel.b - clust[min_dist_index].center.b ); - clust[min_dist_index].size = new_size; - } -} - -/* Function to compute the centroid of an image */ - -RGB_Cluster -compute_centroid ( const Image *img ) -{ - double sum_red = 0.0, sum_green = 0.0, sum_blue = 0.0; - Color pixel; - RGB_Cluster centroid; - - for (int i = 0; i < (img->width*img->height); i++) - { - pixel = img->data[i]; - sum_red += pixel.r; - sum_green += pixel.g; - sum_blue += pixel.b; - } - - centroid.center.r = sum_red / (img->width*img->height); - centroid.center.g = sum_green / (img->width*img->height); - centroid.center.b = sum_blue / (img->width*img->height); - - return centroid; -} - -/* - Incremental Online K-Means Algorithm: - - A. D. Abernathy and M. E. Celebi, - The Incremental Online K-Means Clustering Algorithm - and Its Application to Color Quantization, - Expert Systems with Applications, - accepted for publication, 2022. - - Notes: - 1) NUM_COLORS must be a power of 2 (otherwise the code must be - modified slightly, see Abernathy & Celebi, 2022). - 2) LR_EXP: Learning rate exponent (must be in [0.5, 1]) - 3) SAMPLE_RATE: Fraction of the input pixels (must be in (0, 1]) - used during the clustering process. - */ - -std::vector -inc_online_kmeans ( const Image *img, const int num_colors, - const double lr_exp, const double sample_rate ) -{ - int index, num_splits; - Color pixel; - RGB_Cluster *tmp_clust, *clust; - - if ( !is_pow2 ( num_colors ) ) - { - fprintf ( stderr, "Number of colors (%d) must be a power of 2!\n", num_colors ); - exit ( EXIT_FAILURE ); - } - - /* Compute log2 ( num_colors ) */ - num_splits = ( int ) ( log ( num_colors ) / log ( 2 ) + 0.5 ); /* round */ - - tmp_clust = ( RGB_Cluster * ) malloc ( ( 2 * num_colors - 1 ) * sizeof ( RGB_Cluster ) ); - clust = ( RGB_Cluster * ) malloc ( num_colors * sizeof ( RGB_Cluster ) ); - - /* Set first center to be the dataset centroid */ - tmp_clust[0] = compute_centroid ( img ); - tmp_clust[0].size = 0; - - for ( int t = 0; t < num_splits; t++ ) - { - for ( int n = POW2[t] - 1; n < POW2[t + 1] - 1; n++ ) - { - /* Split c_n into c_{2n + 1} and c_{2n + 2} */ - pixel = tmp_clust[n].center; - - /* Left child */ - index = 2 * n + 1; - tmp_clust[index].center.r = pixel.r; - tmp_clust[index].center.g = pixel.g; - tmp_clust[index].center.b = pixel.b; - tmp_clust[index].size = 0; - - /* Right child */ - index++; - tmp_clust[index].center.r = pixel.r; - tmp_clust[index].center.g = pixel.g; - tmp_clust[index].center.b = pixel.b; - tmp_clust[index].size = 0; - } - - /* Refine the new centers using online k-means */ - online_kmeans ( img, POW2[t + 1], lr_exp, sample_rate, - tmp_clust + POW2[t + 1] - 1 ); - } - - /* Last NUM_COLORS centers are the final centers */ - for ( int j = 0; j < num_colors; j++ ) - { - clust[j].center.r = tmp_clust[j + num_colors - 1].center.r; - clust[j].center.g = tmp_clust[j + num_colors - 1].center.g; - clust[j].center.b = tmp_clust[j + num_colors - 1].center.b; - } - - free (tmp_clust); - - std::vector result(num_colors); - for (int i=0; i img16(img.width*img.height); // convert into RGB16 color for (int i=0; i palette; + Palette palette; + std::vector palette16; std::vector images(opt.path_input.size()); for (int i=0; i 1) - { - int combined_size = 0; - Image combined; - - // pre-calclate size - for (auto &image : images) - combined_size += image.data.size(); + auto remap = Quantize(images, palette, numcol, false, format); + + palette16.resize(palette.size()); + for (int i=1; i(palette16), sizeof(uint16_t) * palette.size(), 0); + uint32_t hash = murmurhash(reinterpret_cast(palette16.data()), sizeof(uint16_t)*palette16.size(), 0); for (int i=0; i remap = RemapImage(images[i], palette, format); - // std::vector remap = {1,2,3,4,54,5,6,7,78,23,34,32,32,12,5,34}; - Metadata meta; meta.format = format; meta.width = images[i].width; meta.height = images[i].height; - meta.palette_count = palette.size(); + meta.palette_count = palette16.size(); meta.palette_hash = hash; - meta.original_size = remap.size(); + meta.original_size = remap[i].size(); uint8_t compress[uint64_t(meta.original_size*1.5)]; - meta.compress_size = fastlz_compress_level(1, remap.data(), meta.original_size, compress); + meta.compress_size = fastlz_compress_level(2, remap[i].data(), meta.original_size, compress); auto output = fs::path(opt.path_output) / std::format("{}.sillyimg", fs::path(opt.path_input[i]).stem().string()); - // std::ofstream out(output, std::ios::binary); + std::ofstream out(output, std::ios::binary); - // out.write(reinterpret_cast(&meta), sizeof(meta)); - // out.write(reinterpret_cast(palette16), sizeof(uint16_t) * palette.size()); - // out.write(reinterpret_cast(compress), meta.compress_size); - // out.close(); + out.write(reinterpret_cast(&meta), sizeof(meta)); + out.write(reinterpret_cast(palette16.data()), sizeof(uint16_t)*palette.size()); + out.write(reinterpret_cast(compress), meta.compress_size); + out.close(); } } } @@ -248,134 +230,14 @@ int main(int argc, char **argv) // // ---- -std::vector QuantizePalette(const int numColor, const Image &image) -{ - return inc_online_kmeans(&image, numColor); -} - -std::vector RemapImage(const Image &image, const std::vector &palette, const int &format) -{ - std::vector remap(image.width*image.height); - - int min_dist_index; - double del_red, del_green, del_blue; - double dist, min_dist; - - // TODO: explore more distance algorithm - for (int i=0; i packed; - packed.reserve((remap.size() + 3) / 4); // Round up - - for (size_t i = 0; i < remap.size(); ) { - uint8_t byte = 0; - byte |= (remap[i] & 0b00000011); - - if (++i < remap.size()) byte |= (remap[i] & 0b00000011) << 2; - if (++i < remap.size()) byte |= (remap[i] & 0b00000011) << 4; - if (++i < remap.size()) byte |= (remap[i] & 0b00000011) << 6; - - packed.push_back(byte); - i++; // Move to next group - } - - return packed; - } - - else if (format == Format_INDEXED_16) - { - std::vector packed; - packed.reserve((remap.size() + 1) / 2); // Round up - - for (int i = 0; i < remap.size(); ) { - uint8_t byte = 0; - byte |= (remap[i] & 0b00001111); - - if (++i < remap.size()) { - byte |= (remap[i] & 0b00001111) << 4; - } - - packed.push_back(byte); - i++; // Move to next group - } - - return packed; - } - - else if (format == Format_INDEXED_8A32) - { - std::vector packed; - packed.reserve(remap.size()); - - for (size_t i = 0; i < remap.size(); ++i) { - uint8_t color_index = remap[i] & 0b00000111; // 3-bit mask - uint8_t alpha = ((uint8_t)palette[remap[i]].a >> 3) & 0b00011111; // 5-bit mask - - packed.push_back((alpha << 3) | color_index); - } - - return packed; - } - - else if (format == Format_INDEXED_32A8) - { - std::vector packed; - packed.reserve(remap.size()); - - for (size_t i = 0; i < remap.size(); ++i) { - uint8_t color_index = remap[i] & 0b00011111; // 5-bit mask - uint8_t alpha = ((uint8_t)palette[remap[i]].a >> 5) & 0b00000111; // 3-bit mask - - packed.push_back((alpha << 5) | color_index); - } - - return packed; - } - - return remap; -} - -// // ---- - void Image::Load(const std::filesystem::path &path) { int comp; - stbi_uc *img = stbi_load(path.c_str(), &width, &height, &comp, 4); - if (img == nullptr) throw std::runtime_error(stbi_failure_reason()); + stbi_uc *ptr_img = stbi_load(path.c_str(), &width, &height, &comp, 4); + if (ptr_img == nullptr) throw std::runtime_error(stbi_failure_reason()); - data.resize(width*height); - for (int i=0; i +#include +#include "header.hpp" + +std::vector Quantize(const std::vector& images, Palette &palette_output, int num_colors, bool dither, uint8_t format) +{ + liq_attr *attr = liq_attr_create(); + liq_histogram *hist = liq_histogram_create(attr); + + liq_set_max_colors(attr, num_colors); + + std::vector liq_images; + liq_images.reserve(images.size()); + + for (const Image &img : images) + { + liq_image *liq_img = liq_image_create_rgba(attr, img.data.data(), img.width, img.height, 0); + liq_histogram_add_image(hist, attr, liq_img); + liq_images.push_back(liq_img); + } + + liq_result *result; + if (LIQ_OK != liq_histogram_quantize(hist, attr, &result)) + throw std::runtime_error("failed to quantize histogram"); + + if (dither) liq_set_dithering_level(result, 1.0f); + else liq_set_dithering_level(result, 0.0f); + + const liq_palette *pal = liq_get_palette(result); + palette_output.resize(pal->count); + for (int i=0; icount; i++) + { + palette_output[i].r = pal->entries[i].r; + palette_output[i].g = pal->entries[i].g; + palette_output[i].b = pal->entries[i].b; + palette_output[i].a = pal->entries[i].a; + } + + std::vector output(images.size()); + + for (int i=0; i packed(output.size()); + + for (int i=0; i packed(output.size()); + + for (int i=0; i packed(output.size()); + + for (int i=0; i> 3; + current_byte |= (output[i][offset] & 0b111) | (alpha << 3); + packed[i].push_back(current_byte); + } + } + + return packed; + } + + else if (format == Format_INDEXED_32A8) + { + std::vector packed(output.size()); + + for (int i=0; i> 5; + current_byte |= (output[i][offset] & 0b11111) | (alpha << 5); + packed[i].push_back(current_byte); + } + } + + return packed; + } + + return output; +} \ No newline at end of file