feat: improve distance field generator perfs and quality using 1d squared euclidian distance

This commit is contained in:
2026-04-11 15:48:37 +02:00
parent 2b9c503c9e
commit fd5ef6753b
2 changed files with 133 additions and 72 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

@@ -1,5 +1,6 @@
#include <raylib.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <omp.h>
@@ -7,104 +8,164 @@ static bool is_inside_shape(Color pixel) {
return pixel.r == 255 && pixel.g == 255 && pixel.b == 255;
}
static Image generate_distance_field(Image input_texture, int search_radius, int offset_y) {
// 1D squared Euclidean distance transform (felzenszwalb & huttenlocher)
static void edt_1d(float *f, int n, int *v, float *z, float *tmp) {
int k = 0;
v[0] = 0;
z[0] = -1e30f;
z[1] = 1e30f;
for (int q = 1; q < n; q += 1) {
float fq = f[q];
float s = ((fq + (float)(q * q)) - (f[v[k]] + (float)(v[k] * v[k]))) / (float)(2 * (q - v[k]));
while (s <= z[k]) {
k -= 1;
s = ((fq + (float)(q * q)) - (f[v[k]] + (float)(v[k] * v[k]))) / (float)(2 * (q - v[k]));
}
k += 1;
v[k] = q;
z[k] = s;
z[k + 1] = 1e30f;
}
int j = 0;
for (int q = 0; q < n; q += 1) {
while (z[j + 1] < (float)q) j += 1;
float d = (float)(q - v[j]);
tmp[q] = d * d + f[v[j]];
}
memcpy(f, tmp, n * sizeof(float));
}
static float *compute_edt(bool *seeds, int width, int height) {
float large = (float)((width + height) * (width + height));
float *grid = malloc(width * height * sizeof(float));
for (int i = 0; i < width * height; i += 1) {
grid[i] = seeds[i] ? 0.0f : large;
}
int maxdim = width > height ? width : height;
#pragma omp parallel
{
int *v = malloc(maxdim * sizeof(int));
float *z = malloc((maxdim + 1) * sizeof(float));
float *tmp = malloc(maxdim * sizeof(float));
float *col = malloc(maxdim * sizeof(float));
// 1D EDT along each column
#pragma omp for nowait
for (int x = 0; x < width; x += 1) {
for (int y = 0; y < height; y += 1) col[y] = grid[y * width + x];
edt_1d(col, height, v, z, tmp);
for (int y = 0; y < height; y += 1) grid[y * width + x] = col[y];
}
#pragma omp barrier
// 1D EDT along each row
#pragma omp for nowait
for (int y = 0; y < height; y += 1) {
memcpy(col, &grid[y * width], width * sizeof(float));
edt_1d(col, width, v, z, tmp);
memcpy(&grid[y * width], col, width * sizeof(float));
}
free(v);
free(z);
free(tmp);
free(col);
}
return grid;
}
static Image generate_distance_field(Image input_texture, int offset_y) {
int width = input_texture.width;
int height = input_texture.height;
Image distance_field = GenImageColor(width, height, (Color){128, 128, 128, 255});
Color *input_pixels = LoadImageColors(input_texture);
Color *output_pixels = LoadImageColors(distance_field);
bool *inside_mask = calloc(width * height, sizeof(bool));
TraceLog(LOG_INFO, "Using %d threads for parallel processing", omp_get_max_threads());
#pragma omp parallel for schedule(dynamic, 64)
for (int y = 0; y < height; y += 1) {
if (omp_get_thread_num() == 0 && y % (height / 10) == 0) {
TraceLog(LOG_INFO, "Progress: %d%%", (y * 100) / height);
}
for (int x = 0; x < width; x += 1) {
int index = y * width + x;
int src_y = y + offset_y;
bool is_inside = (src_y >= 0 && src_y < height)
? is_inside_shape(input_pixels[src_y * width + x])
: false;
float min_distance_sq = search_radius * search_radius;
bool found_boundary = false;
for (int dy = -search_radius; dy <= search_radius; dy += 1) {
for (int dx = -search_radius; dx <= search_radius; dx += 1) {
int sample_x = x + dx;
int sample_y = y + dy + offset_y;
if (sample_x < 0 || sample_x >= width || sample_y < 0 || sample_y >= height) continue;
int sample_index = sample_y * width + sample_x;
bool sample_is_inside = is_inside_shape(input_pixels[sample_index]);
if (sample_is_inside != is_inside) {
float distance_sq = dx * dx + dy * dy;
if (distance_sq < min_distance_sq) {
min_distance_sq = distance_sq;
found_boundary = true;
if (src_y < 0 || src_y >= height) continue;
for (int x = 0; x < width; x += 1) {
inside_mask[y * width + x] = is_inside_shape(input_pixels[src_y * width + x]);
}
}
}
}
float distance = found_boundary ? sqrtf(min_distance_sq) : (float)search_radius;
float normalized_distance = distance / (float)search_radius;
unsigned char distance_value = is_inside ?
(unsigned char)(127.0f - 127.0f * normalized_distance) :
(unsigned char)(128.0f + 127.0f * normalized_distance);
output_pixels[index] = (Color){distance_value, distance_value, distance_value, 255};
}
}
for (int i = 0; i < width * height; i += 1) {
ImageDrawPixel(&distance_field, i % width, i / width, output_pixels[i]);
}
UnloadImageColors(input_pixels);
UnloadImageColors(output_pixels);
return distance_field;
bool *outside_mask = malloc(width * height * sizeof(bool));
for (int i = 0; i < width * height; i += 1) outside_mask[i] = !inside_mask[i];
TraceLog(LOG_INFO, "Computing inside EDT...");
float *dist_to_outside = compute_edt(outside_mask, width, height);
TraceLog(LOG_INFO, "Computing outside EDT...");
float *dist_to_inside = compute_edt(inside_mask, width, height);
free(outside_mask);
// pixel_value = clamp(sdf, -128, 127) + 128
Color *out_pixels = malloc(width * height * sizeof(Color));
for (int i = 0; i < width * height; i += 1) {
float sdf = inside_mask[i]
? -sqrtf(dist_to_outside[i]) // negative = inside
: +sqrtf(dist_to_inside[i]); // positive = outside
float clamped = fmaxf(-128.0f, fminf(127.0f, sdf));
unsigned char v = (unsigned char)(clamped + 128.0f);
out_pixels[i] = (Color){v, v, v, 255};
}
int main(int argc, char *argv[])
{
free(inside_mask);
free(dist_to_inside);
free(dist_to_outside);
Image out = {
.data = out_pixels,
.width = width,
.height = height,
.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
.mipmaps = 1,
};
return out;
}
int main(int argc, char *argv[]) {
if (argc < 3) {
TraceLog(LOG_ERROR, "Usage: %s <input_image> <output_image> [search_radius] [ball_radius]", argv[0]);
TraceLog(LOG_ERROR, "Usage: %s <input_image> <output_image> [ball_radius]", argv[0]);
return 1;
}
const char *input_path = argv[1];
const char *output_path = argv[2];
int search_radius = (argc >= 4) ? atoi(argv[3]) : 100;
int ball_radius = (argc >= 5) ? atoi(argv[4]) : 0;
int offset_y = ball_radius / 2;
int ball_radius = (argc >= 4) ? atoi(argv[3]) : 0;
int offset_y = (ball_radius + 10) / 2;
Image input_image = LoadImage(input_path);
if (input_image.data == NULL) {
TraceLog(LOG_ERROR, "Error: Failed to load input image '%s'", input_path);
TraceLog(LOG_ERROR, "Failed to load input image '%s'", input_path);
return 1;
}
Image distance_field_image = generate_distance_field(input_image, search_radius, offset_y);
if (!ExportImage(distance_field_image, output_path)) {
TraceLog(LOG_INFO, "Generating distance field (%dx%d, offset_y=%d)...", input_image.width, input_image.height, offset_y);
Image distance_field = generate_distance_field(input_image, offset_y);
if (!ExportImage(distance_field, output_path)) {
TraceLog(LOG_ERROR, "Failed to save output image '%s'", output_path);
UnloadImage(input_image);
UnloadImage(distance_field_image);
UnloadImage(distance_field);
return 1;
}
TraceLog(LOG_INFO, "Distance field saved to '%s'", output_path);
UnloadImage(input_image);
UnloadImage(distance_field_image);
UnloadImage(distance_field);
return 0;
}