From 89618bff587bd6d3c7e94ea44a9bdd463eed45f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= <1671644+arrufat@users.noreply.github.com> Date: Fri, 10 May 2024 10:34:54 +0900 Subject: [PATCH] Add Integral image (#3) * Add preliminary version of integral image * Add missing imports * point to local zignal dependency * Fix integral image computation and at tests * simplify isStruct --- build.zig | 1 + examples/build.zig.zon | 3 +- src/image.zig | 97 +++++++++++++++++++++++++++++++++++++++++- src/meta.zig | 13 ++++++ 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 4c3836b..a1301cb 100644 --- a/build.zig +++ b/build.zig @@ -15,6 +15,7 @@ pub fn build(b: *std.Build) void { const test_step = b.step("test", "Run library tests"); for ([_][]const u8{ "color", + "image", "geometry", "matrix", "svd", diff --git a/examples/build.zig.zon b/examples/build.zig.zon index 230ac1f..3d75fb8 100644 --- a/examples/build.zig.zon +++ b/examples/build.zig.zon @@ -4,8 +4,7 @@ .dependencies = .{ .zignal = .{ - .url = "https://github.com/bfactory-ai/zignal/archive/c784809c4c0eb914694998b5434c25d9efd379b9.tar.gz", - .hash = "1220a6447aee50bedc4ecea70a562ef2833267011878db344661627485cdce756b80", + .path = "../", }, }, diff --git a/src/image.zig b/src/image.zig index a90ef3a..7b32183 100644 --- a/src/image.zig +++ b/src/image.zig @@ -1,7 +1,10 @@ const std = @import("std"); +const expectEqual = std.testing.expectEqual; const Allocator = std.mem.Allocator; const Rgba = @import("color.zig").Rgba; const as = @import("meta.zig").as; +const isScalar = @import("meta.zig").isScalar; +const isStruct = @import("meta.zig").isStruct; const Rectangle = @import("geometry.zig").Rectangle(f32); const Point2d = @import("point.zig").Point2d(f32); @@ -109,7 +112,6 @@ pub fn Image(comptime T: type) type { } } - /// Rotates the image by angle (in radians) from the given center. It must be freed on the caller side. pub fn rotateFrom(self: Self, allocator: Allocator, center: Point2d, angle: f32, rotated: *Self) !void { var array = std.ArrayList(T).init(allocator); try array.resize(self.rows * self.cols); @@ -151,5 +153,98 @@ pub fn Image(comptime T: type) type { } } } + + /// Computes the integral image of self. + pub fn integralImage( + self: Self, + allocator: Allocator, + integral: *Image(if (isScalar(T)) f32 else if (isStruct(T)) [std.meta.fields(T).len]f32 else @compileError("Can't compute the integral image of " ++ @typeName(T) ++ ".")), + ) !void { + switch (@typeInfo(T)) { + .ComptimeInt, .Int, .ComptimeFloat, .Float => { + integral.* = try Image(f32).initAlloc(allocator, self.rows, self.cols); + var tmp: f32 = 0; + for (0..self.cols) |c| { + tmp += as(f32, (self.data[c])); + integral.data[c] = tmp; + } + for (1..self.rows) |r| { + tmp = 0; + for (0..self.cols) |c| { + const curr_pos = r * self.cols + c; + const prev_pos = (r - 1) * self.cols + c; + tmp += as(f32, self.data[curr_pos]); + integral.data[curr_pos] = tmp + integral.data[prev_pos]; + } + } + }, + .Struct => { + const num_fields = std.meta.fields(T).len; + integral.* = try Image([num_fields]f32).initAlloc(allocator, self.rows, self.cols); + var tmp = [_]f32{0} ** num_fields; + for (0..self.cols) |c| { + inline for (std.meta.fields(T), 0..) |f, i| { + if (c < 1) { + std.log.debug("{s}: {d}", .{ f.name, @field(self.data[c], f.name) }); + } + tmp[i] += as(f32, @field(self.data[c], f.name)); + integral.data[c][i] = tmp[i]; + } + } + for (1..self.rows) |r| { + tmp = [_]f32{0} ** num_fields; + for (0..self.cols) |c| { + const curr_pos = r * self.cols + c; + const prev_pos = (r - 1) * self.cols + c; + inline for (std.meta.fields(T), 0..) |f, i| { + tmp[i] += as(f32, @field(self.data[curr_pos], f.name)); + integral.data[curr_pos][i] = tmp[i] + integral.data[prev_pos][i]; + } + } + } + }, + else => @compileError("Can't compute the integral image of " ++ @typeName(T) ++ "."), + } + } }; } + +test "integral image scalar" { + var image = try Image(u8).initAlloc(std.testing.allocator, 21, 13); + defer image.deinit(std.testing.allocator); + for (image.data) |*i| i.* = 1; + var integral: Image(f32) = undefined; + try image.integralImage(std.testing.allocator, &integral); + defer integral.deinit(std.testing.allocator); + try expectEqual(image.rows, integral.rows); + try expectEqual(image.cols, integral.cols); + try expectEqual(image.data.len, integral.data.len); + for (0..image.rows) |r| { + for (0..image.cols) |c| { + const pos = r * image.cols + c; + const area_at_pos: f32 = @floatFromInt((r + 1) * (c + 1)); + try expectEqual(area_at_pos, integral.data[pos]); + } + } +} + +test "integral image struct" { + var image = try Image(Rgba).initAlloc(std.testing.allocator, 21, 13); + defer image.deinit(std.testing.allocator); + for (image.data) |*i| i.* = .{ .r = 1, .g = 1, .b = 1, .a = 1 }; + var integral: Image([4]f32) = undefined; + try image.integralImage(std.testing.allocator, &integral); + defer integral.deinit(std.testing.allocator); + try expectEqual(image.rows, integral.rows); + try expectEqual(image.cols, integral.cols); + try expectEqual(image.data.len, integral.data.len); + for (0..image.rows) |r| { + for (0..image.cols) |c| { + const pos = r * image.cols + c; + const area_at_pos: f32 = @floatFromInt((r + 1) * (c + 1)); + for (0..4) |i| { + try expectEqual(area_at_pos, integral.data[pos][i]); + } + } + } +} diff --git a/src/meta.zig b/src/meta.zig index ac0ae23..5e5a4b2 100644 --- a/src/meta.zig +++ b/src/meta.zig @@ -25,3 +25,16 @@ pub inline fn as(comptime T: type, from: anytype) T { else => @compileError(@typeName(@TypeOf(from) ++ " is not supported.")), } } + +/// Returns true if and only if T represents a scalar type. +pub inline fn isScalar(comptime T: type) bool { + return switch (@typeInfo(T)) { + .ComptimeInt, .Int, .ComptimeFloat, .Float => true, + else => false, + }; +} + +/// Returns true if and only if T represents a struct type. +pub inline fn isStruct(comptime T: type) bool { + return @typeInfo(T) == .Struct; +}