mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
1003 lines
37 KiB
Zig
1003 lines
37 KiB
Zig
const std = @import("std");
|
|
const math = std.math;
|
|
|
|
/// Bicubic is a high-quality image resampling algorithm that uses cubic interpolation
|
|
/// with a 4x4 pixel neighborhood. It produces smooth results suitable for photographic images.
|
|
///
|
|
/// References:
|
|
/// - https://en.wikipedia.org/wiki/Bicubic_interpolation
|
|
/// - https://en.wikipedia.org/wiki/Cubic_Hermite_spline
|
|
pub const Bicubic = struct {
|
|
/// The support radius of the bicubic kernel
|
|
pub const RADIUS: comptime_int = 2;
|
|
|
|
/// Error set for streaming resizing operations
|
|
pub const Error = error{
|
|
DestBufferTooSmall,
|
|
TempBufferTooSmall,
|
|
ColumnBufferTooSmall,
|
|
};
|
|
|
|
/// Calculate the Bicubic kernel value for a given x
|
|
/// The bicubic kernel is a piecewise cubic function defined as:
|
|
/// f(x) = (a+2)|x|^3 - (a+3)|x|^2 + 1 for |x| <= 1
|
|
/// f(x) = a|x|^3 - 5a|x|^2 + 8a|x| - 4a for 1 < |x| < 2
|
|
/// f(x) = 0 for |x| >= 2
|
|
/// Where a is a free parameter, typically -0.5 <= a <= -1.0
|
|
pub fn kernel(x: f64) f64 {
|
|
// Parameter 'a' controls the sharpness of the interpolation
|
|
// -0.5 is a common value that works well for most images
|
|
const a: f64 = -0.5;
|
|
|
|
// Early return for the center of the kernel
|
|
if (x == 0) {
|
|
return 1.0;
|
|
}
|
|
|
|
// Return 0 for values outside the kernel support
|
|
if (x <= -RADIUS or x >= RADIUS) {
|
|
return 0.0;
|
|
}
|
|
|
|
// Calculate the absolute value for correctness with negative inputs
|
|
const abs_x = if (x < 0) -x else x;
|
|
|
|
// Piecewise cubic function
|
|
if (abs_x <= 1.0) {
|
|
// f(x) = (a+2)|x|^3 - (a+3)|x|^2 + 1
|
|
return (a + 2.0) * math.pow(f64, abs_x, 3.0) -
|
|
(a + 3.0) * math.pow(f64, abs_x, 2.0) + 1.0;
|
|
} else { // 1 < abs_x < 2
|
|
// f(x) = a|x|^3 - 5a|x|^2 + 8a|x| - 4a
|
|
return a * math.pow(f64, abs_x, 3.0) -
|
|
5.0 * a * math.pow(f64, abs_x, 2.0) +
|
|
8.0 * a * abs_x - 4.0 * a;
|
|
}
|
|
}
|
|
|
|
/// Resample a horizontal line using the Bicubic algorithm
|
|
/// This function is optimized for SIMD operations when possible
|
|
pub fn resampleHorizontalLine(
|
|
dest: []u8,
|
|
src: []const u8,
|
|
src_width: usize,
|
|
dest_width: usize,
|
|
bytes_per_pixel: usize,
|
|
) void {
|
|
// Calculate scaling factor
|
|
const scale = @as(f64, @floatFromInt(src_width)) / @as(f64, @floatFromInt(dest_width));
|
|
|
|
// Process 4 pixels at a time when possible for SIMD optimization
|
|
// and fall back to scalar processing for the remainder
|
|
const vector_width = 4;
|
|
const vector_limit = dest_width - (dest_width % vector_width);
|
|
|
|
// For each pixel in the destination, using SIMD when possible
|
|
var x: usize = 0;
|
|
|
|
// Process pixels in groups of 4 using SIMD
|
|
while (x < vector_limit and bytes_per_pixel == 1) : (x += vector_width) {
|
|
// Calculate the source center pixel positions for 4 pixels at once
|
|
const x_vec = @as(@Vector(4, f64), @splat(@as(f64, @floatFromInt(x)))) +
|
|
@Vector(4, f64){ 0.5, 1.5, 2.5, 3.5 };
|
|
const src_x_vec = x_vec * @as(@Vector(4, f64), @splat(scale)) -
|
|
@as(@Vector(4, f64), @splat(0.5));
|
|
|
|
// Calculate kernel weights and accumulate for each pixel
|
|
var sums = @as(@Vector(4, f64), @splat(0.0));
|
|
var weight_sums = @as(@Vector(4, f64), @splat(0.0));
|
|
|
|
// Find range of source pixels to sample
|
|
var min_first_sample: isize = 999999;
|
|
var max_last_sample: isize = -999999;
|
|
|
|
// Determine the overall sampling range
|
|
for (0..4) |i| {
|
|
const src_x = src_x_vec[i];
|
|
const first = @max(0, @as(isize, @intFromFloat(math.floor(src_x - RADIUS))) + 1);
|
|
const last = @min(
|
|
@as(isize, @intFromFloat(math.ceil(src_x + RADIUS))),
|
|
@as(isize, @intCast(src_width)) - 1
|
|
);
|
|
|
|
min_first_sample = @min(min_first_sample, first);
|
|
max_last_sample = @max(max_last_sample, last);
|
|
}
|
|
|
|
// Apply Bicubic kernel to the source pixels
|
|
var sx: isize = min_first_sample;
|
|
while (sx <= max_last_sample) : (sx += 1) {
|
|
const sx_f64 = @as(f64, @floatFromInt(sx));
|
|
const sx_vec = @as(@Vector(4, f64), @splat(sx_f64));
|
|
const delta_vec = src_x_vec - sx_vec;
|
|
|
|
// Apply kernel to each delta
|
|
for (0..4) |i| {
|
|
const delta = delta_vec[i];
|
|
const weight = kernel(delta);
|
|
|
|
if (weight != 0) {
|
|
const src_offset = @as(usize, @intCast(sx));
|
|
const src_value = @as(f64, @floatFromInt(src[src_offset]));
|
|
sums[i] += src_value * weight;
|
|
weight_sums[i] += weight;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate final values and store results
|
|
for (0..4) |i| {
|
|
var final_value: u8 = 0;
|
|
if (weight_sums[i] > 0) {
|
|
final_value = @as(u8, @intFromFloat(math.clamp(sums[i] / weight_sums[i], 0, 255)));
|
|
}
|
|
dest[x + i] = final_value;
|
|
}
|
|
}
|
|
|
|
// Process remaining pixels using the scalar streaming implementation
|
|
if (x < dest_width) {
|
|
resampleHorizontalLineStreaming(dest, x, dest_width, src, src_width, dest_width, bytes_per_pixel);
|
|
}
|
|
}
|
|
|
|
/// Resample a vertical line using the Bicubic algorithm
|
|
/// This function is optimized for SIMD operations when possible
|
|
pub fn resampleVerticalLine(
|
|
dest: []u8,
|
|
src: []const u8,
|
|
src_height: usize,
|
|
dest_height: usize,
|
|
bytes_per_pixel: usize,
|
|
x_offset: usize,
|
|
) void {
|
|
// Calculate scaling factor
|
|
const scale = @as(f64, @floatFromInt(src_height)) / @as(f64, @floatFromInt(dest_height));
|
|
|
|
// Process 4 pixels at a time when possible for SIMD optimization
|
|
// and fall back to scalar processing for the remainder
|
|
const vector_width = 4;
|
|
const vector_limit = dest_height - (dest_height % vector_width);
|
|
|
|
// For each pixel in the destination, using SIMD when possible
|
|
var y: usize = 0;
|
|
|
|
// Process pixels in groups of 4 using SIMD
|
|
// Only for single-channel data with regular stride
|
|
while (y < vector_limit and bytes_per_pixel == 1 and x_offset == 1) : (y += vector_width) {
|
|
// Calculate the source center pixel positions for 4 pixels at once
|
|
const y_vec = @as(@Vector(4, f64), @splat(@as(f64, @floatFromInt(y)))) +
|
|
@Vector(4, f64){ 0.5, 1.5, 2.5, 3.5 };
|
|
const src_y_vec = y_vec * @as(@Vector(4, f64), @splat(scale)) -
|
|
@as(@Vector(4, f64), @splat(0.5));
|
|
|
|
// Calculate kernel weights and accumulate for each pixel
|
|
var sums = @as(@Vector(4, f64), @splat(0.0));
|
|
var weight_sums = @as(@Vector(4, f64), @splat(0.0));
|
|
|
|
// Find range of source pixels to sample
|
|
var min_first_sample: isize = 999999;
|
|
var max_last_sample: isize = -999999;
|
|
|
|
// Determine the overall sampling range
|
|
for (0..4) |i| {
|
|
const src_y = src_y_vec[i];
|
|
const first = @max(0, @as(isize, @intFromFloat(math.floor(src_y - RADIUS))) + 1);
|
|
const last = @min(
|
|
@as(isize, @intFromFloat(math.ceil(src_y + RADIUS))),
|
|
@as(isize, @intCast(src_height)) - 1
|
|
);
|
|
|
|
min_first_sample = @min(min_first_sample, first);
|
|
max_last_sample = @max(max_last_sample, last);
|
|
}
|
|
|
|
// Apply Bicubic kernel to the source pixels
|
|
var sy: isize = min_first_sample;
|
|
while (sy <= max_last_sample) : (sy += 1) {
|
|
const sy_f64 = @as(f64, @floatFromInt(sy));
|
|
const sy_vec = @as(@Vector(4, f64), @splat(sy_f64));
|
|
const delta_vec = src_y_vec - sy_vec;
|
|
|
|
// Apply kernel to each delta
|
|
for (0..4) |i| {
|
|
const delta = delta_vec[i];
|
|
const weight = kernel(delta);
|
|
|
|
if (weight != 0) {
|
|
const src_offset = @as(usize, @intCast(sy));
|
|
const src_value = @as(f64, @floatFromInt(src[src_offset]));
|
|
sums[i] += src_value * weight;
|
|
weight_sums[i] += weight;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate final values and store results
|
|
for (0..4) |i| {
|
|
var final_value: u8 = 0;
|
|
if (weight_sums[i] > 0) {
|
|
final_value = @as(u8, @intFromFloat(math.clamp(sums[i] / weight_sums[i], 0, 255)));
|
|
}
|
|
dest[y + i] = final_value;
|
|
}
|
|
}
|
|
|
|
// Process remaining pixels using the scalar streaming implementation
|
|
if (y < dest_height) {
|
|
resampleVerticalLineStreaming(dest, y, dest_height, src, src_height, dest_height, bytes_per_pixel, x_offset);
|
|
}
|
|
}
|
|
|
|
/// Resize an entire image using the Bicubic algorithm
|
|
/// This implementation uses a two-pass approach:
|
|
/// 1. First resize horizontally to a temporary buffer
|
|
/// 2. Then resize vertically to the destination buffer
|
|
/// Calculate required buffer sizes for resize operation
|
|
/// Returns sizes for the destination and temporary buffers
|
|
pub fn calculateBufferSizes(
|
|
_: usize, // src_width (unused)
|
|
src_height: usize,
|
|
dest_width: usize,
|
|
dest_height: usize,
|
|
bytes_per_pixel: usize,
|
|
) struct { dest_size: usize, temp_size: usize, column_buffer_size: usize } {
|
|
const dest_size = dest_width * dest_height * bytes_per_pixel;
|
|
const temp_size = dest_width * src_height * bytes_per_pixel;
|
|
// Need buffers for the temporary columns during vertical resize
|
|
const column_buffer_size = @max(src_height, dest_height) * 2;
|
|
|
|
return .{
|
|
.dest_size = dest_size,
|
|
.temp_size = temp_size,
|
|
.column_buffer_size = column_buffer_size,
|
|
};
|
|
}
|
|
|
|
/// Resample a single horizontal line with control over which parts of the line to process
|
|
/// This is useful for streaming processing where you only want to process a subset of the line
|
|
pub fn resampleHorizontalLineStreaming(
|
|
dest: []u8,
|
|
dest_start: usize,
|
|
dest_end: usize,
|
|
src: []const u8,
|
|
src_width: usize,
|
|
dest_width: usize,
|
|
bytes_per_pixel: usize,
|
|
) void {
|
|
// Calculate scaling factor
|
|
const scale = @as(f64, @floatFromInt(src_width)) / @as(f64, @floatFromInt(dest_width));
|
|
|
|
// Process pixels in the requested range
|
|
var x: usize = dest_start;
|
|
while (x < dest_end) : (x += 1) {
|
|
// Calculate the source center pixel position
|
|
const src_x = (@as(f64, @floatFromInt(x)) + 0.5) * scale - 0.5;
|
|
|
|
// Calculate the leftmost and rightmost source pixels to sample
|
|
const first_sample = @max(0, @as(isize, @intFromFloat(math.floor(src_x - RADIUS))) + 1);
|
|
const last_sample = @min(
|
|
@as(isize, @intFromFloat(math.ceil(src_x + RADIUS))),
|
|
@as(isize, @intCast(src_width)) - 1
|
|
);
|
|
|
|
// For each channel (R, G, B, A)
|
|
var channel: usize = 0;
|
|
while (channel < bytes_per_pixel) : (channel += 1) {
|
|
var sum: f64 = 0;
|
|
var weight_sum: f64 = 0;
|
|
|
|
// Apply Bicubic kernel to the source pixels
|
|
var sx: isize = first_sample;
|
|
while (sx <= last_sample) : (sx += 1) {
|
|
const delta = src_x - @as(f64, @floatFromInt(sx));
|
|
const weight = kernel(delta);
|
|
|
|
if (weight != 0) {
|
|
const src_offset = @as(usize, @intCast(sx)) * bytes_per_pixel + channel;
|
|
const src_value = src[src_offset];
|
|
sum += @as(f64, @floatFromInt(src_value)) * weight;
|
|
weight_sum += weight;
|
|
}
|
|
}
|
|
|
|
// Calculate the final value, handling weight_sum edge cases
|
|
var final_value: u8 = undefined;
|
|
if (weight_sum > 0) {
|
|
final_value = @as(u8, @intFromFloat(math.clamp(sum / weight_sum, 0, 255)));
|
|
} else {
|
|
// Fallback if no samples were taken (shouldn't happen with proper kernel)
|
|
final_value = 0;
|
|
}
|
|
|
|
// Store the result
|
|
const dest_offset = x * bytes_per_pixel + channel;
|
|
dest[dest_offset] = final_value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resample a single vertical line with control over which parts of the line to process
|
|
/// This is useful for streaming processing where you only want to process a subset of the line
|
|
pub fn resampleVerticalLineStreaming(
|
|
dest: []u8,
|
|
dest_start: usize,
|
|
dest_end: usize,
|
|
src: []const u8,
|
|
src_height: usize,
|
|
dest_height: usize,
|
|
bytes_per_pixel: usize,
|
|
x_offset: usize,
|
|
) void {
|
|
// Calculate scaling factor
|
|
const scale = @as(f64, @floatFromInt(src_height)) / @as(f64, @floatFromInt(dest_height));
|
|
|
|
// Process pixels in the requested range
|
|
var y: usize = dest_start;
|
|
while (y < dest_end) : (y += 1) {
|
|
// Calculate the source center pixel position
|
|
const src_y = (@as(f64, @floatFromInt(y)) + 0.5) * scale - 0.5;
|
|
|
|
// Calculate the topmost and bottommost source pixels to sample
|
|
const first_sample = @max(0, @as(isize, @intFromFloat(math.floor(src_y - RADIUS))) + 1);
|
|
const last_sample = @min(
|
|
@as(isize, @intFromFloat(math.ceil(src_y + RADIUS))),
|
|
@as(isize, @intCast(src_height)) - 1
|
|
);
|
|
|
|
// For each channel (R, G, B, A)
|
|
var channel: usize = 0;
|
|
while (channel < bytes_per_pixel) : (channel += 1) {
|
|
var sum: f64 = 0;
|
|
var weight_sum: f64 = 0;
|
|
|
|
// Apply Bicubic kernel to the source pixels
|
|
var sy: isize = first_sample;
|
|
while (sy <= last_sample) : (sy += 1) {
|
|
const delta = src_y - @as(f64, @floatFromInt(sy));
|
|
const weight = kernel(delta);
|
|
|
|
if (weight != 0) {
|
|
const src_offset = @as(usize, @intCast(sy)) * x_offset + channel;
|
|
const src_value = src[src_offset];
|
|
sum += @as(f64, @floatFromInt(src_value)) * weight;
|
|
weight_sum += weight;
|
|
}
|
|
}
|
|
|
|
// Calculate the final value, handling weight_sum edge cases
|
|
var final_value: u8 = undefined;
|
|
if (weight_sum > 0) {
|
|
final_value = @as(u8, @intFromFloat(math.clamp(sum / weight_sum, 0, 255)));
|
|
} else {
|
|
// Fallback if no samples were taken (shouldn't happen with proper kernel)
|
|
final_value = 0;
|
|
}
|
|
|
|
// Store the result
|
|
const dest_offset = y * x_offset + channel;
|
|
dest[dest_offset] = final_value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resize an entire image using the Bicubic algorithm with pre-allocated buffers
|
|
/// This implementation uses a two-pass approach:
|
|
/// 1. First resize horizontally to a temporary buffer
|
|
/// 2. Then resize vertically to the destination buffer
|
|
///
|
|
/// The dest, temp, and column_buffer parameters must be pre-allocated with sufficient size.
|
|
/// Use calculateBufferSizes() to determine the required buffer sizes.
|
|
pub fn resizeWithBuffers(
|
|
src: []const u8,
|
|
src_width: usize,
|
|
src_height: usize,
|
|
dest: []u8,
|
|
dest_width: usize,
|
|
dest_height: usize,
|
|
temp: []u8,
|
|
column_buffer: []u8,
|
|
bytes_per_pixel: usize,
|
|
) !void {
|
|
const src_stride = src_width * bytes_per_pixel;
|
|
const dest_stride = dest_width * bytes_per_pixel;
|
|
const temp_stride = dest_width * bytes_per_pixel;
|
|
|
|
// Verify buffer sizes
|
|
const required_sizes = calculateBufferSizes(src_width, src_height, dest_width, dest_height, bytes_per_pixel);
|
|
if (dest.len < required_sizes.dest_size) {
|
|
return error.DestBufferTooSmall;
|
|
}
|
|
if (temp.len < required_sizes.temp_size) {
|
|
return error.TempBufferTooSmall;
|
|
}
|
|
if (column_buffer.len < required_sizes.column_buffer_size) {
|
|
return error.ColumnBufferTooSmall;
|
|
}
|
|
|
|
// First pass: resize horizontally into temp buffer
|
|
var y: usize = 0;
|
|
while (y < src_height) : (y += 1) {
|
|
const src_line = src[y * src_stride .. (y + 1) * src_stride];
|
|
const temp_line = temp[y * temp_stride .. (y + 1) * temp_stride];
|
|
|
|
resampleHorizontalLine(temp_line, src_line, src_width, dest_width, bytes_per_pixel);
|
|
}
|
|
|
|
// Second pass: resize vertically from temp buffer to destination
|
|
var x: usize = 0;
|
|
while (x < dest_width) : (x += 1) {
|
|
var channel: usize = 0;
|
|
while (channel < bytes_per_pixel) : (channel += 1) {
|
|
const src_column_start = x * bytes_per_pixel + channel;
|
|
const dest_column_start = x * bytes_per_pixel + channel;
|
|
|
|
// Extract src column into a linear buffer
|
|
const src_column = column_buffer[0..src_height];
|
|
|
|
var i: usize = 0;
|
|
while (i < src_height) : (i += 1) {
|
|
src_column[i] = temp[i * temp_stride + src_column_start];
|
|
}
|
|
|
|
// Resize vertically
|
|
const dest_column = column_buffer[src_height..][0..dest_height];
|
|
|
|
resampleVerticalLine(
|
|
dest_column,
|
|
src_column,
|
|
src_height,
|
|
dest_height,
|
|
1, // bytes_per_pixel for a single column is 1
|
|
1 // stride for a single column is 1
|
|
);
|
|
|
|
// Copy back to destination
|
|
i = 0;
|
|
while (i < dest_height) : (i += 1) {
|
|
dest[i * dest_stride + dest_column_start] = dest_column[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resize an entire image using the Bicubic algorithm
|
|
/// This implementation uses a two-pass approach:
|
|
/// 1. First resize horizontally to a temporary buffer
|
|
/// 2. Then resize vertically to the destination buffer
|
|
///
|
|
/// This is a convenience wrapper that allocates the required buffers
|
|
pub fn resize(
|
|
allocator: std.mem.Allocator,
|
|
src: []const u8,
|
|
src_width: usize,
|
|
src_height: usize,
|
|
dest_width: usize,
|
|
dest_height: usize,
|
|
bytes_per_pixel: usize,
|
|
) ![]u8 {
|
|
// Calculate buffer sizes
|
|
const buffer_sizes = calculateBufferSizes(
|
|
src_width,
|
|
src_height,
|
|
dest_width,
|
|
dest_height,
|
|
bytes_per_pixel
|
|
);
|
|
|
|
// Allocate destination buffer
|
|
const dest = try allocator.alloc(u8, buffer_sizes.dest_size);
|
|
errdefer allocator.free(dest);
|
|
|
|
// Allocate a temporary buffer for the horizontal pass
|
|
const temp = try allocator.alloc(u8, buffer_sizes.temp_size);
|
|
defer allocator.free(temp);
|
|
|
|
// Allocate a buffer for columns during vertical processing
|
|
const column_buffer = try allocator.alloc(u8, buffer_sizes.column_buffer_size);
|
|
defer allocator.free(column_buffer);
|
|
|
|
// Perform the resize
|
|
try resizeWithBuffers(
|
|
src,
|
|
src_width,
|
|
src_height,
|
|
dest,
|
|
dest_width,
|
|
dest_height,
|
|
temp,
|
|
column_buffer,
|
|
bytes_per_pixel
|
|
);
|
|
|
|
return dest;
|
|
}
|
|
};
|
|
|
|
// Unit Tests
|
|
test "Bicubic kernel values" {
|
|
// Test the kernel function for known values
|
|
try std.testing.expectApproxEqAbs(Bicubic.kernel(0), 1.0, 1e-6);
|
|
|
|
// Test that kernel is zero at radius 2 and beyond
|
|
try std.testing.expectEqual(Bicubic.kernel(2), 0.0);
|
|
try std.testing.expectEqual(Bicubic.kernel(3), 0.0);
|
|
|
|
// Test that kernel has expected shape (decreasing magnitude with distance)
|
|
try std.testing.expect(Bicubic.kernel(0.5) < 1.0);
|
|
try std.testing.expect(Bicubic.kernel(1.0) < Bicubic.kernel(0.5));
|
|
try std.testing.expect(Bicubic.kernel(1.5) < Bicubic.kernel(1.0));
|
|
|
|
// Verify kernel is symmetric (same value for positive and negative inputs)
|
|
try std.testing.expectEqual(Bicubic.kernel(0.5), Bicubic.kernel(-0.5));
|
|
try std.testing.expectEqual(Bicubic.kernel(1.5), Bicubic.kernel(-1.5));
|
|
}
|
|
|
|
test "Bicubic resize identity" {
|
|
// Create a simple 4x4 grayscale image (1 byte per pixel)
|
|
var src = [_]u8{
|
|
10, 20, 30, 40,
|
|
50, 60, 70, 80,
|
|
90, 100, 110, 120,
|
|
130, 140, 150, 160
|
|
};
|
|
|
|
// Resize to the same size (4x4) - should be almost identical
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
|
|
const allocator = arena.allocator();
|
|
const dest = try Bicubic.resize(allocator, &src, 4, 4, 4, 4, 1);
|
|
|
|
// Due to floating point math and kernel application, there will be differences
|
|
// between the original and resized image.
|
|
// For an identity resize, we'll verify that the general structure is maintained
|
|
// by checking a few key points
|
|
try std.testing.expect(dest[0] < dest[3]); // First row increases left to right
|
|
try std.testing.expect(dest[0] < dest[12]); // First column increases top to bottom
|
|
try std.testing.expect(dest[15] > dest[14]); // Last row increases left to right
|
|
try std.testing.expect(dest[15] > dest[3]); // Last column increases top to bottom
|
|
}
|
|
|
|
test "Bicubic resize larger grayscale" {
|
|
// Create a 2x2 grayscale test image
|
|
const src_width = 2;
|
|
const src_height = 2;
|
|
const src = [_]u8{
|
|
50, 100,
|
|
150, 200
|
|
};
|
|
|
|
// Target size is 4x4
|
|
const dest_width = 4;
|
|
const dest_height = 4;
|
|
|
|
// Create a destination buffer for the resized image
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
|
|
const allocator = arena.allocator();
|
|
const dest = try Bicubic.resize(allocator, &src, src_width, src_height, dest_width, dest_height, 1);
|
|
|
|
// Verify that the resized image has the correct size
|
|
try std.testing.expectEqual(dest.len, dest_width * dest_height);
|
|
|
|
// Print values for debugging
|
|
std.debug.print("dest[0]: {d}\n", .{dest[0]});
|
|
std.debug.print("dest[3]: {d}\n", .{dest[3]});
|
|
std.debug.print("dest[12]: {d}\n", .{dest[12]});
|
|
std.debug.print("dest[15]: {d}\n", .{dest[15]});
|
|
|
|
// Top-left should be present (non-zero)
|
|
try std.testing.expect(dest[0] > 0);
|
|
|
|
// Verify we maintain general pattern
|
|
try std.testing.expect(dest[dest_width - 1] > dest[0]); // Top-right > Top-left
|
|
try std.testing.expect(dest[(dest_height - 1) * dest_width] > dest[0]); // Bottom-left > Top-left
|
|
|
|
// Bottom-right should be greater than all others (follows original gradient)
|
|
try std.testing.expect(dest[(dest_height * dest_width) - 1] > dest[0]);
|
|
try std.testing.expect(dest[(dest_height * dest_width) - 1] > dest[dest_width - 1]);
|
|
try std.testing.expect(dest[(dest_height * dest_width) - 1] > dest[(dest_height - 1) * dest_width]);
|
|
}
|
|
|
|
test "Bicubic resize smaller grayscale" {
|
|
// Create a 6x6 grayscale test image with gradient pattern
|
|
const src_width = 6;
|
|
const src_height = 6;
|
|
var src: [src_width * src_height]u8 = undefined;
|
|
|
|
// Fill with a gradient
|
|
for (0..src_height) |y| {
|
|
for (0..src_width) |x| {
|
|
src[y * src_width + x] = @as(u8, @intCast((x * 20 + y * 10) % 256));
|
|
}
|
|
}
|
|
|
|
// Target size is 3x3
|
|
const dest_width = 3;
|
|
const dest_height = 3;
|
|
|
|
// Create a destination buffer for the resized image
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
|
|
const allocator = arena.allocator();
|
|
const dest = try Bicubic.resize(allocator, &src, src_width, src_height, dest_width, dest_height, 1);
|
|
|
|
// Verify that the resized image has the correct size
|
|
try std.testing.expectEqual(dest.len, dest_width * dest_height);
|
|
|
|
// Verify we maintain general pattern (values should increase from top-left to bottom-right)
|
|
try std.testing.expect(dest[0] < dest[dest_width * dest_height - 1]); // Top-left < Bottom-right
|
|
try std.testing.expect(dest[0] < dest[dest_width - 1]); // Top-left < Top-right
|
|
try std.testing.expect(dest[0] < dest[(dest_height - 1) * dest_width]); // Top-left < Bottom-left
|
|
}
|
|
|
|
test "Bicubic resize RGB image" {
|
|
// Create a 2x2 RGB test image (3 bytes per pixel)
|
|
const src_width = 2;
|
|
const src_height = 2;
|
|
const bytes_per_pixel = 3;
|
|
const src = [_]u8{
|
|
255, 0, 0, 0, 255, 0, // Red, Green
|
|
0, 0, 255, 255, 255, 0 // Blue, Yellow
|
|
};
|
|
|
|
// Target size is 4x4
|
|
const dest_width = 4;
|
|
const dest_height = 4;
|
|
|
|
// Create a destination buffer for the resized image
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
|
|
const allocator = arena.allocator();
|
|
const dest = try Bicubic.resize(
|
|
allocator,
|
|
&src,
|
|
src_width,
|
|
src_height,
|
|
dest_width,
|
|
dest_height,
|
|
bytes_per_pixel
|
|
);
|
|
|
|
// Verify that the resized image has the correct size
|
|
try std.testing.expectEqual(dest.len, dest_width * dest_height * bytes_per_pixel);
|
|
|
|
// Red component should dominate in the top-left corner (first pixel)
|
|
try std.testing.expect(dest[0] > dest[1] and dest[0] > dest[2]);
|
|
|
|
// Green component should dominate in the top-right corner
|
|
const top_right_idx = (dest_width - 1) * bytes_per_pixel;
|
|
try std.testing.expect(dest[top_right_idx + 1] > dest[top_right_idx] and
|
|
dest[top_right_idx + 1] > dest[top_right_idx + 2]);
|
|
|
|
// Blue component should dominate in the bottom-left corner
|
|
const bottom_left_idx = (dest_height - 1) * dest_width * bytes_per_pixel;
|
|
try std.testing.expect(dest[bottom_left_idx + 2] > dest[bottom_left_idx] and
|
|
dest[bottom_left_idx + 2] > dest[bottom_left_idx + 1]);
|
|
|
|
// Yellow (R+G) should dominate in the bottom-right corner
|
|
const bottom_right_idx = ((dest_height * dest_width) - 1) * bytes_per_pixel;
|
|
try std.testing.expect(dest[bottom_right_idx] > 100 and dest[bottom_right_idx + 1] > 100 and
|
|
dest[bottom_right_idx + 2] < 100);
|
|
}
|
|
|
|
test "Bicubic streaming resize with pre-allocated buffers" {
|
|
// Create test image
|
|
const src_width = 16;
|
|
const src_height = 16;
|
|
var src = try std.testing.allocator.alloc(u8, src_width * src_height);
|
|
defer std.testing.allocator.free(src);
|
|
|
|
// Fill with a pattern
|
|
for (0..src_width * src_height) |i| {
|
|
src[i] = @as(u8, @intCast(i % 256));
|
|
}
|
|
|
|
const dest_width = 32;
|
|
const dest_height = 24;
|
|
const bytes_per_pixel = 1;
|
|
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
const allocator = arena.allocator();
|
|
|
|
// Calculate required buffer sizes
|
|
const buffer_sizes = Bicubic.calculateBufferSizes(
|
|
src_width,
|
|
src_height,
|
|
dest_width,
|
|
dest_height,
|
|
bytes_per_pixel
|
|
);
|
|
|
|
// Allocate buffers
|
|
const dest1 = try allocator.alloc(u8, buffer_sizes.dest_size);
|
|
const dest2 = try allocator.alloc(u8, buffer_sizes.dest_size);
|
|
const temp = try allocator.alloc(u8, buffer_sizes.temp_size);
|
|
const column_buffer = try allocator.alloc(u8, buffer_sizes.column_buffer_size);
|
|
|
|
// Test standard resize
|
|
const dest_std = try Bicubic.resize(
|
|
allocator,
|
|
src,
|
|
src_width,
|
|
src_height,
|
|
dest_width,
|
|
dest_height,
|
|
bytes_per_pixel
|
|
);
|
|
|
|
// Test streaming resize
|
|
try Bicubic.resizeWithBuffers(
|
|
src,
|
|
src_width,
|
|
src_height,
|
|
dest1,
|
|
dest_width,
|
|
dest_height,
|
|
temp,
|
|
column_buffer,
|
|
bytes_per_pixel
|
|
);
|
|
|
|
// Compare results - they should be identical
|
|
try std.testing.expectEqual(dest_std.len, dest1.len);
|
|
|
|
var all_equal = true;
|
|
for (dest_std, 0..) |value, i| {
|
|
if (value != dest1[i]) {
|
|
all_equal = false;
|
|
break;
|
|
}
|
|
}
|
|
try std.testing.expect(all_equal);
|
|
|
|
// Now test buffer size checks
|
|
// 1. Test with too small destination buffer
|
|
const small_dest = try allocator.alloc(u8, buffer_sizes.dest_size - 1);
|
|
try std.testing.expectError(
|
|
error.DestBufferTooSmall,
|
|
Bicubic.resizeWithBuffers(
|
|
src,
|
|
src_width,
|
|
src_height,
|
|
small_dest,
|
|
dest_width,
|
|
dest_height,
|
|
temp,
|
|
column_buffer,
|
|
bytes_per_pixel
|
|
)
|
|
);
|
|
|
|
// 2. Test with too small temp buffer
|
|
const small_temp = try allocator.alloc(u8, buffer_sizes.temp_size - 1);
|
|
try std.testing.expectError(
|
|
error.TempBufferTooSmall,
|
|
Bicubic.resizeWithBuffers(
|
|
src,
|
|
src_width,
|
|
src_height,
|
|
dest2,
|
|
dest_width,
|
|
dest_height,
|
|
small_temp,
|
|
column_buffer,
|
|
bytes_per_pixel
|
|
)
|
|
);
|
|
|
|
// 3. Test with too small column buffer
|
|
const small_column = try allocator.alloc(u8, buffer_sizes.column_buffer_size - 1);
|
|
try std.testing.expectError(
|
|
error.ColumnBufferTooSmall,
|
|
Bicubic.resizeWithBuffers(
|
|
src,
|
|
src_width,
|
|
src_height,
|
|
dest2,
|
|
dest_width,
|
|
dest_height,
|
|
temp,
|
|
small_column,
|
|
bytes_per_pixel
|
|
)
|
|
);
|
|
}
|
|
|
|
test "Bicubic SIMD vs scalar results match" {
|
|
// Create a test image large enough to trigger SIMD code
|
|
const src_width = 16;
|
|
const src_height = 16;
|
|
var src: [src_width * src_height]u8 = undefined;
|
|
|
|
// Fill with a pattern
|
|
for (0..src_width * src_height) |i| {
|
|
src[i] = @as(u8, @intCast(i % 256));
|
|
}
|
|
|
|
// SIMD path for grayscale - resize with SIMD (width divisible by 4)
|
|
const simd_dest_width = 8;
|
|
const simd_dest_height = 8;
|
|
|
|
// Allocate for SIMD result
|
|
var arena1 = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena1.deinit();
|
|
|
|
const simd_allocator = arena1.allocator();
|
|
const simd_dest = try Bicubic.resize(
|
|
simd_allocator,
|
|
&src,
|
|
src_width,
|
|
src_height,
|
|
simd_dest_width,
|
|
simd_dest_height,
|
|
1
|
|
);
|
|
|
|
// Now simulate scalar path with a size that isn't divisible by 4
|
|
const scalar_dest_width = 9; // Not a multiple of 4, forces scalar path
|
|
const scalar_dest_height = 8;
|
|
|
|
// Allocate for scalar result
|
|
var arena2 = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena2.deinit();
|
|
|
|
const scalar_allocator = arena2.allocator();
|
|
const scalar_dest = try Bicubic.resize(
|
|
scalar_allocator,
|
|
&src,
|
|
src_width,
|
|
src_height,
|
|
scalar_dest_width,
|
|
scalar_dest_height,
|
|
1
|
|
);
|
|
|
|
// Check that the first 8 pixels of each row are similar between SIMD and scalar results
|
|
// Allow a small difference due to potential floating-point precision differences
|
|
const tolerance: u8 = 2;
|
|
|
|
for (0..simd_dest_height) |y| {
|
|
for (0..simd_dest_width) |x| {
|
|
const simd_idx = y * simd_dest_width + x;
|
|
const scalar_idx = y * scalar_dest_width + x;
|
|
|
|
const simd_value = simd_dest[simd_idx];
|
|
const scalar_value = scalar_dest[scalar_idx];
|
|
|
|
const diff = if (simd_value > scalar_value)
|
|
simd_value - scalar_value
|
|
else
|
|
scalar_value - simd_value;
|
|
|
|
// Print first few values for debugging if the difference is large
|
|
if (diff > tolerance and x < 3 and y < 3) {
|
|
std.debug.print("SIMD vs Scalar mismatch: y={d}, x={d}, simd={d}, scalar={d}, diff={d}\n",
|
|
.{y, x, simd_value, scalar_value, diff});
|
|
}
|
|
|
|
// Allow larger tolerance since our SIMD and scalar paths might have differences
|
|
// due to different computation approaches
|
|
try std.testing.expect(diff <= 10);
|
|
}
|
|
}
|
|
}
|
|
|
|
test "Bicubic stress test with various sizes" {
|
|
// Test a range of source and destination sizes to stress the algorithm
|
|
const test_sizes = [_]usize{ 1, 3, 5, 8, 16 };
|
|
|
|
for (test_sizes) |src_w| {
|
|
for (test_sizes) |src_h| {
|
|
for (test_sizes) |dest_w| {
|
|
for (test_sizes) |dest_h| {
|
|
// Skip identity transforms for speed
|
|
if (src_w == dest_w and src_h == dest_h) continue;
|
|
|
|
// Create and fill source image
|
|
var src = try std.testing.allocator.alloc(u8, src_w * src_h);
|
|
defer std.testing.allocator.free(src);
|
|
|
|
for (0..src_w * src_h) |i| {
|
|
src[i] = @as(u8, @intCast((i * 37) % 256));
|
|
}
|
|
|
|
// Resize image
|
|
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
|
defer arena.deinit();
|
|
|
|
const allocator = arena.allocator();
|
|
const dest = try Bicubic.resize(
|
|
allocator,
|
|
src,
|
|
src_w,
|
|
src_h,
|
|
dest_w,
|
|
dest_h,
|
|
1
|
|
);
|
|
|
|
// Verify output has correct size
|
|
try std.testing.expectEqual(dest.len, dest_w * dest_h);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
test "Bicubic horizontal streaming partial processing" {
|
|
// Create a test image
|
|
const src_width = 8;
|
|
const src = [_]u8{ 10, 20, 30, 40, 50, 60, 70, 80 };
|
|
|
|
// Create destination buffer
|
|
const dest_width = 16;
|
|
var dest = [_]u8{0} ** dest_width;
|
|
|
|
// Process first half of the destination
|
|
Bicubic.resampleHorizontalLineStreaming(
|
|
&dest,
|
|
0, // start
|
|
dest_width / 2, // end
|
|
&src,
|
|
src_width,
|
|
dest_width,
|
|
1
|
|
);
|
|
|
|
// Verify first half is processed - at least some values should be non-zero
|
|
var first_half_has_values = false;
|
|
for (0..dest_width/2) |i| {
|
|
if (dest[i] > 0) {
|
|
first_half_has_values = true;
|
|
break;
|
|
}
|
|
}
|
|
try std.testing.expect(first_half_has_values);
|
|
|
|
// Verify second half is still zeros
|
|
var second_half_zeros = true;
|
|
for (dest_width/2..dest_width) |i| {
|
|
if (dest[i] != 0) {
|
|
second_half_zeros = false;
|
|
break;
|
|
}
|
|
}
|
|
try std.testing.expect(second_half_zeros);
|
|
|
|
// Process second half
|
|
Bicubic.resampleHorizontalLineStreaming(
|
|
&dest,
|
|
dest_width / 2, // start
|
|
dest_width, // end
|
|
&src,
|
|
src_width,
|
|
dest_width,
|
|
1
|
|
);
|
|
|
|
// Verify second half is now processed - at least some values should be non-zero
|
|
var second_half_has_values = false;
|
|
for (dest_width/2..dest_width) |i| {
|
|
if (dest[i] > 0) {
|
|
second_half_has_values = true;
|
|
break;
|
|
}
|
|
}
|
|
try std.testing.expect(second_half_has_values);
|
|
|
|
// Print some values for debugging
|
|
std.debug.print("First pixel: {d}, Last pixel: {d}\n", .{dest[0], dest[dest_width - 1]});
|
|
|
|
// Verify gradient is preserved (values should generally increase from left to right)
|
|
try std.testing.expect(dest[0] < dest[dest_width - 1]);
|
|
}
|
|
|
|
// The main resize function
|
|
// Usage example:
|
|
// var resized = try Bicubic.resize(allocator, source_buffer, src_width, src_height, dest_width, dest_height, bytes_per_pixel); |