1
0
Fork 0
mirror of https://github.com/zeldaret/oot.git synced 2025-06-07 17:11:50 +00:00
oot/tools/assets/n64texconv/lib/libimagequant/remap.c
Tharo 3d61fb85ef
Add n64texconv and bin2c tools to convert extracted .png and .bin to C arrays during build (#2477)
* n64texconv and bin2c

* mv tools/n64texconv tools/assets/

* fix

* more light fixes

* Silence -Wshadow for libimagequant

* Add reference counting gc for palette objects in python bindings

* Fix missing alignment in n64texconv_*_to_c functions

Co-authored-by: Dragorn421 <Dragorn421@users.noreply.github.com>

* Check palette size in n64texconv_image_from_png

* accept memoryview as well as bytes for binary data

* minimal doc on n64texconv_quantize_shared

* fix a buffer size passed to libimagequant

* assert pal count <= 256 on png write

* Disable palette size check for input pngs, ZAPD fails the check

* No OpenMP for clang

* When reading an indexed png into a CI format, requantize if there are too many colors for the target texel size

---------

Co-authored-by: Dragorn421 <Dragorn421@users.noreply.github.com>
2025-02-17 17:09:42 -05:00

300 lines
13 KiB
C

#include <stdlib.h>
#include <string.h>
#include "libimagequant.h"
#include "pam.h"
#include "libimagequant_private.h"
#include "nearest.h"
#include "kmeans.h"
LIQ_PRIVATE LIQ_NONNULL float remap_to_palette(liq_image *const input_image, unsigned char *const *const output_pixels, colormap *const map)
{
const int rows = input_image->height;
const unsigned int cols = input_image->width;
double remapping_error=0;
if (!liq_image_get_row_f_init(input_image)) {
return -1;
}
if (input_image->background && !liq_image_get_row_f_init(input_image->background)) {
return -1;
}
const colormap_item *acolormap = map->palette;
struct nearest_map *const n = nearest_init(map);
liq_image *background = input_image->background;
const int transparent_index = background ? nearest_search(n, &(f_pixel){0,0,0,0}, 0, NULL) : -1;
if (background && acolormap[transparent_index].acolor.a > 1.f/256.f) {
// palette unsuitable for using the bg
background = NULL;
}
const unsigned int max_threads = omp_get_max_threads();
LIQ_ARRAY(kmeans_state, average_color, (KMEANS_CACHE_LINE_GAP+map->colors) * max_threads);
kmeans_init(map, max_threads, average_color);
#if __GNUC__ >= 9 || __clang__
#pragma omp parallel for if (rows*cols > 3000) \
schedule(static) default(none) shared(background,acolormap,average_color,cols,input_image,map,n,output_pixels,rows,transparent_index) reduction(+:remapping_error)
#endif
for(int row = 0; row < rows; ++row) {
const f_pixel *const row_pixels = liq_image_get_row_f(input_image, row);
const f_pixel *const bg_pixels = background && acolormap[transparent_index].acolor.a < MIN_OPAQUE_A ? liq_image_get_row_f(background, row) : NULL;
unsigned int last_match=0;
for(unsigned int col = 0; col < cols; ++col) {
float diff;
last_match = nearest_search(n, &row_pixels[col], last_match, &diff);
if (bg_pixels) {
float bg_diff = colordifference(bg_pixels[col], acolormap[last_match].acolor);
if (bg_diff <= diff) {
diff = bg_diff;
last_match = transparent_index;
}
}
output_pixels[row][col] = last_match;
remapping_error += diff;
if (last_match != transparent_index) {
kmeans_update_color(row_pixels[col], 1.0, map, last_match, omp_get_thread_num(), average_color);
}
}
}
kmeans_finalize(map, max_threads, average_color);
nearest_free(n);
return remapping_error / (input_image->width * input_image->height);
}
inline static f_pixel get_dithered_pixel(const float dither_level, const float max_dither_error, const f_pixel thiserr, const f_pixel px)
{
/* Use Floyd-Steinberg errors to adjust actual color. */
const float sr = thiserr.r * dither_level,
sg = thiserr.g * dither_level,
sb = thiserr.b * dither_level,
sa = thiserr.a * dither_level;
float ratio = 1.0;
const float max_overflow = 1.1f;
const float max_underflow = -0.1f;
// allowing some overflow prevents undithered bands caused by clamping of all channels
if (px.r + sr > max_overflow) ratio = MIN(ratio, (max_overflow -px.r)/sr);
else { if (px.r + sr < max_underflow) ratio = MIN(ratio, (max_underflow-px.r)/sr); }
if (px.g + sg > max_overflow) ratio = MIN(ratio, (max_overflow -px.g)/sg);
else { if (px.g + sg < max_underflow) ratio = MIN(ratio, (max_underflow-px.g)/sg); }
if (px.b + sb > max_overflow) ratio = MIN(ratio, (max_overflow -px.b)/sb);
else { if (px.b + sb < max_underflow) ratio = MIN(ratio, (max_underflow-px.b)/sb); }
float a = px.a + sa;
if (a > 1.f) { a = 1.f; }
else if (a < 0) { a = 0; }
// If dithering error is crazy high, don't propagate it that much
// This prevents crazy geen pixels popping out of the blue (or red or black! ;)
const float dither_error = sr*sr + sg*sg + sb*sb + sa*sa;
if (dither_error > max_dither_error) {
ratio *= 0.8f;
} else if (dither_error < 2.f/256.f/256.f) {
// don't dither areas that don't have noticeable error — makes file smaller
return px;
}
return (f_pixel) {
.r=px.r + sr * ratio,
.g=px.g + sg * ratio,
.b=px.b + sb * ratio,
.a=a,
};
}
/**
Uses edge/noise map to apply dithering only to flat areas. Dithering on edges creates jagged lines, and noisy areas are "naturally" dithered.
If output_image_is_remapped is true, only pixels noticeably changed by error diffusion will be written to output image.
*/
LIQ_PRIVATE LIQ_NONNULL bool remap_to_palette_floyd(liq_image *input_image, unsigned char *const output_pixels[], liq_remapping_result *quant, const float max_dither_error, const bool output_image_is_remapped)
{
const int rows = input_image->height, cols = input_image->width;
const unsigned char *dither_map = quant->use_dither_map ? (input_image->dither_map ? input_image->dither_map : input_image->edges) : NULL;
const colormap *map = quant->palette;
const colormap_item *acolormap = map->palette;
if (!liq_image_get_row_f_init(input_image)) {
return false;
}
if (input_image->background && !liq_image_get_row_f_init(input_image->background)) {
return false;
}
/* Initialize Floyd-Steinberg error vectors. */
const size_t errwidth = cols+2;
f_pixel *restrict thiserr = input_image->malloc(errwidth * sizeof(thiserr[0]) * 2); // +2 saves from checking out of bounds access
if (!thiserr) return false;
f_pixel *restrict nexterr = thiserr + errwidth;
memset(thiserr, 0, errwidth * sizeof(thiserr[0]));
bool ok = true;
struct nearest_map *const n = nearest_init(map);
liq_image *background = input_image->background;
const int transparent_index = background ? nearest_search(n, &(f_pixel){0,0,0,0}, 0, NULL) : -1;
if (background && acolormap[transparent_index].acolor.a > 1.f/256.f) {
// palette unsuitable for using the bg
background = NULL;
}
// response to this value is non-linear and without it any value < 0.8 would give almost no dithering
float base_dithering_level = quant->dither_level;
base_dithering_level = 1.f - (1.f-base_dithering_level)*(1.f-base_dithering_level);
if (dither_map) {
base_dithering_level *= 1.f/255.f; // convert byte to float
}
base_dithering_level *= 15.f/16.f; // prevent small errors from accumulating
int fs_direction = 1;
unsigned int last_match=0;
for (int row = 0; row < rows; ++row) {
if (liq_remap_progress(quant, quant->progress_stage1 + row * (100.f - quant->progress_stage1) / rows)) {
ok = false;
break;
}
memset(nexterr, 0, errwidth * sizeof(nexterr[0]));
int col = (fs_direction > 0) ? 0 : (cols - 1);
const f_pixel *const row_pixels = liq_image_get_row_f(input_image, row);
const f_pixel *const bg_pixels = background && acolormap[transparent_index].acolor.a < MIN_OPAQUE_A ? liq_image_get_row_f(background, row) : NULL;
int undithered_bg_used = 0;
do {
float dither_level = base_dithering_level;
if (dither_map) {
dither_level *= dither_map[row*cols + col];
}
const f_pixel spx = get_dithered_pixel(dither_level, max_dither_error, thiserr[col + 1], row_pixels[col]);
const unsigned int guessed_match = output_image_is_remapped ? output_pixels[row][col] : last_match;
float dither_diff;
last_match = nearest_search(n, &spx, guessed_match, &dither_diff);
f_pixel output_px = acolormap[last_match].acolor;
// this is for animgifs
if (bg_pixels) {
// if the background makes better match *with* dithering, it's a definitive win
float bg_for_dither_diff = colordifference(spx, bg_pixels[col]);
if (bg_for_dither_diff <= dither_diff) {
output_px = bg_pixels[col];
last_match = transparent_index;
} else if (undithered_bg_used > 1) {
// the undithered fallback can cause artifacts when too many undithered pixels accumulate a big dithering error
// so periodically ignore undithered fallback to prevent that
undithered_bg_used = 0;
} else {
// if dithering is not applied, there's a high risk of creating artifacts (flat areas, error accumulating badly),
// OTOH poor dithering disturbs static backgrounds and creates oscilalting frames that break backgrounds
// back and forth in two differently bad ways
float max_diff = colordifference(row_pixels[col], bg_pixels[col]);
float dithered_diff = colordifference(row_pixels[col], output_px);
// if dithering is worse than natural difference between frames
// (this rule dithers moving areas, but does not dither static areas)
if (dithered_diff > max_diff) {
// then see if an undithered color is closer to the ideal
float undithered_diff = colordifference(row_pixels[col], acolormap[guessed_match].acolor);
if (undithered_diff < max_diff) {
undithered_bg_used++;
output_px = acolormap[guessed_match].acolor;
last_match = guessed_match;
}
}
}
}
output_pixels[row][col] = last_match;
f_pixel err = {
.r = (spx.r - output_px.r),
.g = (spx.g - output_px.g),
.b = (spx.b - output_px.b),
.a = (spx.a - output_px.a),
};
// If dithering error is crazy high, don't propagate it that much
// This prevents crazy geen pixels popping out of the blue (or red or black! ;)
if (err.r*err.r + err.g*err.g + err.b*err.b + err.a*err.a > max_dither_error) {
err.r *= 0.75f;
err.g *= 0.75f;
err.b *= 0.75f;
err.a *= 0.75f;
}
/* Propagate Floyd-Steinberg error terms. */
if (fs_direction > 0) {
thiserr[col + 2].a += err.a * (7.f/16.f);
thiserr[col + 2].r += err.r * (7.f/16.f);
thiserr[col + 2].g += err.g * (7.f/16.f);
thiserr[col + 2].b += err.b * (7.f/16.f);
nexterr[col + 2].a = err.a * (1.f/16.f);
nexterr[col + 2].r = err.r * (1.f/16.f);
nexterr[col + 2].g = err.g * (1.f/16.f);
nexterr[col + 2].b = err.b * (1.f/16.f);
nexterr[col + 1].a += err.a * (5.f/16.f);
nexterr[col + 1].r += err.r * (5.f/16.f);
nexterr[col + 1].g += err.g * (5.f/16.f);
nexterr[col + 1].b += err.b * (5.f/16.f);
nexterr[col ].a += err.a * (3.f/16.f);
nexterr[col ].r += err.r * (3.f/16.f);
nexterr[col ].g += err.g * (3.f/16.f);
nexterr[col ].b += err.b * (3.f/16.f);
} else {
thiserr[col ].a += err.a * (7.f/16.f);
thiserr[col ].r += err.r * (7.f/16.f);
thiserr[col ].g += err.g * (7.f/16.f);
thiserr[col ].b += err.b * (7.f/16.f);
nexterr[col ].a = err.a * (1.f/16.f);
nexterr[col ].r = err.r * (1.f/16.f);
nexterr[col ].g = err.g * (1.f/16.f);
nexterr[col ].b = err.b * (1.f/16.f);
nexterr[col + 1].a += err.a * (5.f/16.f);
nexterr[col + 1].r += err.r * (5.f/16.f);
nexterr[col + 1].g += err.g * (5.f/16.f);
nexterr[col + 1].b += err.b * (5.f/16.f);
nexterr[col + 2].a += err.a * (3.f/16.f);
nexterr[col + 2].r += err.r * (3.f/16.f);
nexterr[col + 2].g += err.g * (3.f/16.f);
nexterr[col + 2].b += err.b * (3.f/16.f);
}
// remapping is done in zig-zag
col += fs_direction;
if (fs_direction > 0) {
if (col >= cols) break;
} else {
if (col < 0) break;
}
} while(1);
f_pixel *const temperr = thiserr;
thiserr = nexterr;
nexterr = temperr;
fs_direction = -fs_direction;
}
input_image->free(MIN(thiserr, nexterr)); // MIN because pointers were swapped
nearest_free(n);
return ok;
}