Skip to content

Commit

Permalink
add CLAHE filter
Browse files Browse the repository at this point in the history
  • Loading branch information
dnjulek committed Oct 5, 2024
1 parent e63c8ca commit c5fb340
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 2 deletions.
4 changes: 2 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
.paths = .{""},
.dependencies = .{
.vapoursynth = .{
.url = "https://github.com/dnjulek/vapoursynth-zig/archive/0fb889752a1ce303ac644632aa03d77e60718030.tar.gz",
.hash = "122021974021744c3795247b0e79eafc085a0cad4e61c8520fb3b940d93bb3021477",
.url = "git+https://github.com/dnjulek/vapoursynth-zig.git#8ec6f7927f57054105a1cda885ca8dfad2ed1101",
.hash = "1220169bc1ed7049fedcc83cf7494c902f325bcd53d3785a07a849ca785989269087",
},
},
}
155 changes: 155 additions & 0 deletions src/filters/clahe.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
const std = @import("std");

const allocator = std.heap.c_allocator;

pub fn applyCLAHE(
comptime T: type,
srcp: []const T,
dstp: []T,
stride: u32,
width: u32,
height: u32,
limit: u32,
tiles: []u32,
) void {
const hist_size: u32 = @as(u32, 1) << @as(u32, @typeInfo(T).int.bits);
const peak: f32 = @floatFromInt(hist_size - 1);
const tile_width: u32 = width / tiles[0];
const tile_height: u32 = height / tiles[1];
const tile_size_total: u32 = tile_width * tile_height;
const lut_scale: f32 = peak / @as(f32, @floatFromInt(tile_size_total));
var clip_limit: i32 = @intCast(limit * tile_size_total / hist_size);
clip_limit = @max(clip_limit, 1);

const lut = allocator.alloc(T, tiles[0] * tiles[1] * hist_size) catch unreachable;
defer allocator.free(lut);

calcLut(T, srcp, stride, width, height, lut, tile_width, tile_height, tiles, clip_limit, lut_scale);
interpolate(T, srcp, dstp, stride, width, height, lut, tile_width, tile_height, tiles);
}

fn calcLut(
comptime T: type,
srcp: []const T,
stride: u32,
width: usize,
height: usize,
lut: []T,
tile_width: usize,
tile_height: usize,
tiles: []u32,
clip_limit: i32,
lut_scale: f32,
) void {
const hist_size: u32 = @as(u32, 1) << @as(u32, @typeInfo(T).int.bits);
const hist_sizei: i32 = @intCast(hist_size);
var tile_hist: [hist_size]i32 = undefined;
const tiles_x = tiles[0];
const tiles_y = tiles[1];

var ty: usize = 0;
while (ty < tiles_y) : (ty += 1) {
var tx: usize = 0;
while (tx < tiles_x) : (tx += 1) {
@memset(&tile_hist, 0);

var y: usize = ty * tile_height;
while (y < @min((ty + 1) * tile_height, height)) : (y += 1) {
var x: usize = tx * tile_width;
while (x < @min((tx + 1) * tile_width, width)) : (x += 1) {
tile_hist[srcp[y * stride + x]] += 1;
}
}

if (clip_limit > 0) {
var clipped: i32 = 0;
for (&tile_hist) |*bin| {
if (bin.* > clip_limit) {
clipped += bin.* - clip_limit;
bin.* = clip_limit;
}
}

const redist_batch: i32 = @divTrunc(clipped, hist_sizei);
var residual: i32 = clipped - redist_batch * hist_sizei;

for (&tile_hist) |*bin| {
bin.* += redist_batch;
}

if (residual != 0) {
const residual_step = @max(@divTrunc(hist_sizei, residual), 1);
var i: usize = 0;
while ((i < hist_size) and (residual > 0)) : (i += residual_step) {
tile_hist[i] += 1;
residual -= 1;
}
}
}

var sum: i32 = 0;
var i: usize = 0;
while (i < hist_size) : (i += 1) {
sum += tile_hist[i];
lut[(ty * tiles_x + tx) * hist_size + i] = @intFromFloat(@as(f32, @floatFromInt(sum)) * lut_scale + 0.5);
}
}
}
}

fn interpolate(
comptime T: type,
srcp: []const T,
dstp: []T,
stride: u32,
width: usize,
height: usize,
lut: []const T,
tile_width: usize,
tile_height: usize,
tiles: []u32,
) void {
const hist_size: u32 = @as(u32, 1) << @as(u32, @typeInfo(T).int.bits);
const tiles_x: i32 = @intCast(tiles[0]);
const tiles_y: i32 = @intCast(tiles[1]);

const inv_tw: f32 = 1.0 / @as(f32, @floatFromInt(tile_width));
const inv_th: f32 = 1.0 / @as(f32, @floatFromInt(tile_height));

var y: usize = 0;
while (y < height) : (y += 1) {
const tyf: f32 = @as(f32, @floatFromInt(y)) * inv_th - 0.5;
var ty1: i32 = @intFromFloat(@floor(tyf));
var ty2: i32 = ty1 + 1;
const ya: f32 = tyf - @as(f32, @floatFromInt(ty1));

ty1 = @max(ty1, 0);
ty2 = @min(ty2, tiles_y - 1);
const lut_p1 = ty1 * tiles_x;
const lut_p2 = ty2 * tiles_x;

var x: usize = 0;
while (x < width) : (x += 1) {
const txf: f32 = @as(f32, @floatFromInt(x)) * inv_tw - 0.5;
const _tx1: i32 = @intFromFloat(@floor(txf));
const _tx2: i32 = _tx1 + 1;
const xa: f32 = txf - @as(f32, @floatFromInt(_tx1));

const tx1 = @max(_tx1, 0);
const tx2 = @min(_tx2, tiles_x - 1);

const src_val = srcp[y * stride + x];
const p1_tx1: u32 = @intCast(lut_p1 + tx1);
const p1_tx2: u32 = @intCast(lut_p1 + tx2);
const p2_tx1: u32 = @intCast(lut_p2 + tx1);
const p2_tx2: u32 = @intCast(lut_p2 + tx2);
const lut0: f32 = @floatFromInt(lut[p1_tx1 * hist_size + src_val]);
const lut1: f32 = @floatFromInt(lut[p1_tx2 * hist_size + src_val]);
const lut2: f32 = @floatFromInt(lut[p2_tx1 * hist_size + src_val]);
const lut3: f32 = @floatFromInt(lut[p2_tx2 * hist_size + src_val]);
const res: f32 = (lut0 * (1 - xa) + lut1 * xa) * (1 - ya) + (lut2 * (1 - xa) + lut3 * xa) * ya;

dstp[y * stride + x] = @intFromFloat(res + 0.5);
}
}
}
102 changes: 102 additions & 0 deletions src/vapoursynth/clahe.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const std = @import("std");
const vszip = @import("../vszip.zig");
const helper = @import("../helper.zig");
const filter = @import("../filters/clahe.zig");

const vs = vszip.vs;
const vsh = vszip.vsh;
const zapi = vszip.zapi;
const math = std.math;

const allocator = std.heap.c_allocator;
pub const filter_name = "CLAHE";

const Data = struct {
node: ?*vs.Node,
vi: *const vs.VideoInfo,
limit: u32,
tiles: [2]u32,
};

fn CLAHE(comptime T: type) type {
return struct {
pub fn getFrame(n: c_int, activation_reason: vs.ActivationReason, instance_data: ?*anyopaque, frame_data: ?*?*anyopaque, frame_ctx: ?*vs.FrameContext, core: ?*vs.Core, vsapi: ?*const vs.API) callconv(.C) ?*const vs.Frame {
_ = frame_data;
const d: *Data = @ptrCast(@alignCast(instance_data));

if (activation_reason == .Initial) {
vsapi.?.requestFrameFilter.?(n, d.node, frame_ctx);
} else if (activation_reason == .AllFramesReady) {
var src = zapi.Frame.init(d.node, n, frame_ctx, core, vsapi);
defer src.deinit();
const dst = src.newVideoFrame();

var plane: u32 = 0;
while (plane < d.vi.format.numPlanes) : (plane += 1) {
const srcp = src.getReadSlice2(T, plane);
const dstp = dst.getWriteSlice2(T, plane);
const width, const height, const stride = src.getDimensions2(T, plane);
filter.applyCLAHE(T, srcp, dstp, stride, width, height, d.limit, &d.tiles);
}

dst.setInt("_ColorRange", 0);
return dst.frame;
}

return null;
}
};
}

export fn claheFree(instance_data: ?*anyopaque, core: ?*vs.Core, vsapi: ?*const vs.API) callconv(.C) void {
_ = core;
const d: *Data = @ptrCast(@alignCast(instance_data));
vsapi.?.freeNode.?(d.node);
allocator.destroy(d);
}

pub export fn claheCreate(in: ?*const vs.Map, out: ?*vs.Map, user_data: ?*anyopaque, core: ?*vs.Core, vsapi: ?*const vs.API) callconv(.C) void {
_ = user_data;
var d: Data = undefined;
var map = zapi.Map.init(in, out, vsapi);

d.node, d.vi = map.getNodeVi("clip");

if ((d.vi.format.sampleType != .Integer)) {
map.setError(filter_name ++ ": only 8-16 bit int formats supported.");
vsapi.?.freeNode.?(d.node);
return;
}

d.limit = map.getInt(u32, "limit") orelse 7;
const in_arr = map.getIntArray("tiles");
const df_arr = [2]i64{ 3, 3 };
const tiles_arr = if ((in_arr == null) or (in_arr.?.len == 0)) &df_arr else in_arr.?;
d.tiles[0] = @intCast(tiles_arr[0]);
switch (tiles_arr.len) {
1 => {
d.tiles[1] = @intCast(tiles_arr[0]);
},
2 => {
d.tiles[1] = @intCast(tiles_arr[1]);
},
else => {
map.setError(filter_name ++ " : tiles array can't have more than 2 values.");
vsapi.?.freeNode.?(d.node);
return;
},
}

const data: *Data = allocator.create(Data) catch unreachable;
data.* = d;

var deps = [_]vs.FilterDependency{
vs.FilterDependency{
.source = d.node,
.requestPattern = .StrictSpatial,
},
};

const getFrame = if (d.vi.format.bytesPerSample == 1) &CLAHE(u8).getFrame else &CLAHE(u16).getFrame;
vsapi.?.createVideoFilter.?(out, filter_name, d.vi, getFrame, claheFree, .Parallel, &deps, deps.len, data, core);
}
2 changes: 2 additions & 0 deletions src/vszip.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const vapoursynth = @import("vapoursynth");
const bilateral = @import("vapoursynth/bilateral.zig");
const boxblur = @import("vapoursynth/boxblur.zig");
const clahe = @import("vapoursynth/clahe.zig");
const metrics = @import("vapoursynth/metrics.zig");
const pavg = @import("vapoursynth/planeaverage.zig");
const pmm = @import("vapoursynth/planeminmax.zig");
Expand All @@ -14,6 +15,7 @@ export fn VapourSynthPluginInit2(plugin: *vs.Plugin, vspapi: *const vs.PLUGINAPI
_ = vspapi.configPlugin.?("com.julek.vszip", "vszip", "VapourSynth Zig Image Process", vs.makeVersion(3, 0), vs.VAPOURSYNTH_API_VERSION, 0, plugin);
_ = vspapi.registerFunction.?(bilateral.filter_name, "clip:vnode;ref:vnode:opt;sigmaS:float[]:opt;sigmaR:float[]:opt;planes:int[]:opt;algorithm:int[]:opt;PBFICnum:int[]:opt", "clip:vnode;", bilateral.bilateralCreate, null, plugin);
_ = vspapi.registerFunction.?(boxblur.filter_name, "clip:vnode;planes:int[]:opt;hradius:int:opt;hpasses:int:opt;vradius:int:opt;vpasses:int:opt", "clip:vnode;", boxblur.boxBlurCreate, null, plugin);
_ = vspapi.registerFunction.?(clahe.filter_name, "clip:vnode;limit:int:opt;tiles:int[]:opt", "clip:vnode;", clahe.claheCreate, null, plugin);
_ = vspapi.registerFunction.?(metrics.filter_name, "reference:vnode;distorted:vnode;mode:int:opt;", "clip:vnode;", metrics.MetricsCreate, null, plugin);
_ = vspapi.registerFunction.?(pavg.filter_name, "clipa:vnode;exclude:int[];clipb:vnode:opt;planes:int[]:opt;prop:data:opt;", "clip:vnode;", pavg.planeAverageCreate, null, plugin);
_ = vspapi.registerFunction.?(pmm.filter_name, "clipa:vnode;minthr:float:opt;maxthr:float:opt;clipb:vnode:opt;planes:int[]:opt;prop:data:opt;", "clip:vnode;", pmm.planeMinMaxCreate, null, plugin);
Expand Down

0 comments on commit c5fb340

Please sign in to comment.