implement basic seam carving algorithm
This commit is contained in:
parent
bb656a4ce0
commit
397d92c4e0
13
README.md
13
README.md
@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
https://en.wikipedia.org/wiki/Seam_carving
|
https://en.wikipedia.org/wiki/Seam_carving
|
||||||
|
|
||||||
|
## Feature wishlist
|
||||||
|
|
||||||
|
* Drag & drop custom images
|
||||||
|
* Make it run well in debug mode
|
||||||
|
* Incremental/partial updating of energy field
|
||||||
|
* Multi-threaded carving
|
||||||
|
* Seam insertion/generation
|
||||||
|
* Marking areas to preserve or remove
|
||||||
|
* Realtime (a.k.a. 60fps) seam carving
|
||||||
|
* Fix web build
|
||||||
|
|
||||||
## Building & run
|
## Building & run
|
||||||
|
|
||||||
Windows & linux
|
Windows & linux
|
||||||
@ -9,7 +20,7 @@ Windows & linux
|
|||||||
zig build run
|
zig build run
|
||||||
```
|
```
|
||||||
|
|
||||||
Web
|
Web (currently broken)
|
||||||
```shell
|
```shell
|
||||||
emsdk install latest
|
emsdk install latest
|
||||||
zig build -Dtarget=wasm32-emscripten --sysroot [path to emsdk]/upstream/emscripten run
|
zig build -Dtarget=wasm32-emscripten --sysroot [path to emsdk]/upstream/emscripten run
|
||||||
|
@ -106,6 +106,9 @@ pub fn build(b: *std.Build) !void {
|
|||||||
|
|
||||||
const raylib = raylib_dep.module("raylib");
|
const raylib = raylib_dep.module("raylib");
|
||||||
const raylib_artifact = raylib_dep.artifact("raylib");
|
const raylib_artifact = raylib_dep.artifact("raylib");
|
||||||
|
raylib_artifact.defineCMacro("SUPPORT_FILEFORMAT_JPG", null);
|
||||||
|
|
||||||
|
const run_label = try std.fmt.allocPrint(b.allocator, "Run {s}", .{project_name});
|
||||||
|
|
||||||
//web exports are completely separate
|
//web exports are completely separate
|
||||||
if (target.query.os_tag == .emscripten) {
|
if (target.query.os_tag == .emscripten) {
|
||||||
@ -121,7 +124,6 @@ pub fn build(b: *std.Build) !void {
|
|||||||
const run_step = try rlz.emcc.emscriptenRunStep(b);
|
const run_step = try rlz.emcc.emscriptenRunStep(b);
|
||||||
run_step.step.dependOn(&link_step.step);
|
run_step.step.dependOn(&link_step.step);
|
||||||
|
|
||||||
const run_label = try std.fmt.allocPrint(b.allocator, "Run {s}", .{project_name});
|
|
||||||
const run_option = b.step("run", run_label);
|
const run_option = b.step("run", run_label);
|
||||||
run_option.dependOn(&run_step.step);
|
run_option.dependOn(&run_step.step);
|
||||||
return;
|
return;
|
||||||
@ -133,7 +135,6 @@ pub fn build(b: *std.Build) !void {
|
|||||||
exe.root_module.addImport("raylib", raylib);
|
exe.root_module.addImport("raylib", raylib);
|
||||||
|
|
||||||
const run_cmd = b.addRunArtifact(exe);
|
const run_cmd = b.addRunArtifact(exe);
|
||||||
const run_label = try std.fmt.allocPrint(b.allocator, "Run {s}", .{project_name});
|
|
||||||
const run_step = b.step("run", run_label);
|
const run_step = b.step("run", run_label);
|
||||||
run_step.dependOn(&run_cmd.step);
|
run_step.dependOn(&run_cmd.step);
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
.@"raylib-zig" = .{
|
.@"raylib-zig" = .{
|
||||||
.url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz",
|
.url = "https://github.com/Not-Nik/raylib-zig/archive/43d15b05c2b97cab30103fa2b46cff26e91619ec.tar.gz",
|
||||||
.hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212",
|
.hash = "12204a223b19043e17b79300413d02f60fc8004c0d9629b8d8072831e352a78bf212",
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Specifies the set of files and directories that are included in this package.
|
// Specifies the set of files and directories that are included in this package.
|
||||||
|
BIN
src/assets/example.jpg
Normal file
BIN
src/assets/example.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
227
src/main.zig
227
src/main.zig
@ -1,5 +1,8 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const rl = @import("raylib");
|
const rl = @import("raylib");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const SeamCarver = @import("./seam-carver.zig");
|
||||||
|
|
||||||
fn toTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel {
|
fn toTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel {
|
||||||
return switch (log_level) {
|
return switch (log_level) {
|
||||||
@ -10,23 +13,241 @@ fn toTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convertF32ImageToU8(allocator: Allocator, image: []f32, width: usize, height: usize) ![]u8 {
|
||||||
|
const image_u8 = try allocator.alloc(u8, width * height);
|
||||||
|
errdefer allocator.free(image_u8);
|
||||||
|
|
||||||
|
var min_value: f32 = image[0];
|
||||||
|
var max_value: f32 = image[0];
|
||||||
|
for (image[1..]) |value| {
|
||||||
|
min_value = @min(min_value, value);
|
||||||
|
max_value = @max(max_value, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (0.., image) |i, value| {
|
||||||
|
const value_01 = (value - min_value) / (max_value - min_value);
|
||||||
|
image_u8[i] = @intFromFloat(value_01 * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
return image_u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insetRect(rect: rl.Rectangle, margin: f32) rl.Rectangle{
|
||||||
|
return rl.Rectangle.init(rect.x + margin, rect.y + margin, rect.width - 2*margin, rect.height - 2*margin);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insideRect(container: rl.Rectangle, child_size: rl.Vector2) rl.Rectangle {
|
||||||
|
const scale_x = container.width / child_size.x;
|
||||||
|
const scale_y = container.height / child_size.y;
|
||||||
|
|
||||||
|
const min_scale = @min(scale_x, scale_y);
|
||||||
|
return centerRect(container, child_size.scale(min_scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn centerRect(container: rl.Rectangle, child_size: rl.Vector2) rl.Rectangle {
|
||||||
|
return rl.Rectangle{
|
||||||
|
.x = container.x + (container.width - child_size.x)/2,
|
||||||
|
.y = container.y + (container.height - child_size.y)/2,
|
||||||
|
.width = child_size.x,
|
||||||
|
.height = child_size.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn centerOfRect(rect: rl.Rectangle) rl.Vector2 {
|
||||||
|
return rl.Vector2.init(rect.x + rect.width/2, rect.y + rect.height/2);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distanceToLine(lineA: rl.Vector2, lineB: rl.Vector2, point: rl.Vector2) f32 {
|
||||||
|
const ab = lineB.subtract(lineA);
|
||||||
|
const ap = point.subtract(lineA);
|
||||||
|
const proj = ap.dotProduct(ab);
|
||||||
|
const d = proj / ab.lengthSqr();
|
||||||
|
|
||||||
|
if (d <= 0) {
|
||||||
|
return point.distance(lineA);
|
||||||
|
} else if (d >= 1) {
|
||||||
|
return point.distance(lineB);
|
||||||
|
} else {
|
||||||
|
const pointOnLine = lineA.add(ab.scale(d));
|
||||||
|
return point.distance(pointOnLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
defer _ = gpa.deinit();
|
defer _ = gpa.deinit();
|
||||||
_ = allocator;
|
|
||||||
|
|
||||||
rl.setTraceLogLevel(toTraceLogLevel(std.log.default_level));
|
rl.setTraceLogLevel(toTraceLogLevel(std.log.default_level));
|
||||||
|
|
||||||
rl.initWindow(800, 450, "Seam carving");
|
rl.setConfigFlags(.{ .window_resizable = true });
|
||||||
|
rl.initWindow(1024, 720, "Seam carving");
|
||||||
defer rl.closeWindow();
|
defer rl.closeWindow();
|
||||||
|
|
||||||
rl.setTargetFPS(60);
|
rl.setTargetFPS(60);
|
||||||
|
|
||||||
|
const source_image = rl.loadImageFromMemory(".jpg", @embedFile("./assets/example.jpg"));
|
||||||
|
assert(@intFromPtr(source_image.data) != 0);
|
||||||
|
assert(source_image.format == .pixelformat_uncompressed_r8g8b8);
|
||||||
|
defer rl.unloadImage(source_image);
|
||||||
|
|
||||||
|
var image = rl.imageCopy(source_image);
|
||||||
|
defer rl.unloadImage(image);
|
||||||
|
|
||||||
|
const source_texture = rl.loadTextureFromImage(source_image);
|
||||||
|
defer rl.unloadTexture(source_texture);
|
||||||
|
|
||||||
|
var resized_texture = rl.loadTextureFromImage(image);
|
||||||
|
defer rl.unloadTexture(resized_texture);
|
||||||
|
|
||||||
|
var seam_carver = SeamCarver.init(allocator, source_image);
|
||||||
|
defer seam_carver.deinit();
|
||||||
|
|
||||||
|
var resized_width: u32 = @intCast(image.width);
|
||||||
|
var resized_height: u32 = @intCast(image.height);
|
||||||
|
|
||||||
|
var resizing_vertically = false;
|
||||||
|
var resizing_horizontally = false;
|
||||||
|
|
||||||
while (!rl.windowShouldClose()) {
|
while (!rl.windowShouldClose()) {
|
||||||
rl.beginDrawing();
|
rl.beginDrawing();
|
||||||
defer rl.endDrawing();
|
defer rl.endDrawing();
|
||||||
|
|
||||||
rl.clearBackground(rl.Color.white);
|
const window_width: f32 = @floatFromInt(rl.getScreenWidth());
|
||||||
|
const window_height: f32 = @floatFromInt(rl.getScreenHeight());
|
||||||
|
|
||||||
|
const margin = @min(window_width, window_height)*0.02;
|
||||||
|
|
||||||
|
const left_side = insetRect(rl.Rectangle.init(0, 0, window_width/2, window_height), margin);
|
||||||
|
const right_side = insetRect(rl.Rectangle.init(window_width/2, 0, window_width/2, window_height), margin);
|
||||||
|
|
||||||
|
const left_image_rect = insideRect(left_side, .{ .x = @floatFromInt(source_image.width), .y = @floatFromInt(source_image.height) });
|
||||||
|
const right_image_rect = insideRect(right_side, .{ .x = @floatFromInt(source_image.width), .y = @floatFromInt(source_image.height) });
|
||||||
|
|
||||||
|
rl.clearBackground(rl.Color.ray_white);
|
||||||
|
|
||||||
|
const scale = left_image_rect.width / @as(f32, @floatFromInt(source_image.width));
|
||||||
|
|
||||||
|
if (seam_carver.resized_image) |resized_image| {
|
||||||
|
rl.unloadImage(image);
|
||||||
|
image = resized_image;
|
||||||
|
|
||||||
|
rl.unloadTexture(resized_texture);
|
||||||
|
resized_texture = rl.loadTextureFromImage(image);
|
||||||
|
|
||||||
|
seam_carver.resized_image = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.drawTextureEx(
|
||||||
|
source_texture,
|
||||||
|
.{ .x = left_image_rect.x, .y = left_image_rect.y },
|
||||||
|
0,
|
||||||
|
scale,
|
||||||
|
rl.Color.white
|
||||||
|
);
|
||||||
|
|
||||||
|
rl.drawRectangleRec(
|
||||||
|
right_image_rect,
|
||||||
|
rl.Color.light_gray
|
||||||
|
);
|
||||||
|
|
||||||
|
const resized_visual_size = rl.Vector2{
|
||||||
|
.x = scale * @as(f32, @floatFromInt(resized_width)),
|
||||||
|
.y = scale * @as(f32, @floatFromInt(resized_height)),
|
||||||
|
};
|
||||||
|
const resized_rect = centerRect(right_image_rect, resized_visual_size);
|
||||||
|
|
||||||
|
if (seam_carver.work_thread == null) {
|
||||||
|
var pos = rl.Vector2{ .x = resized_rect.x, .y = resized_rect.y };
|
||||||
|
if (resizing_horizontally or resizing_vertically) {
|
||||||
|
pos = rl.Vector2{ .x = right_image_rect.x, .y = right_image_rect.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
const half_image_size = rl.Vector2.init(
|
||||||
|
@as(f32, @floatFromInt(image.width))/2,
|
||||||
|
@as(f32, @floatFromInt(image.height))/2
|
||||||
|
);
|
||||||
|
rl.drawTextureEx(
|
||||||
|
resized_texture,
|
||||||
|
centerOfRect(right_image_rect).subtract(half_image_size.scale(scale)),
|
||||||
|
0,
|
||||||
|
scale,
|
||||||
|
rl.Color.white
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const label: [:0]u8 = try std.fmt.allocPrintZ(allocator, "Resizing {d:.2}%", .{seam_carver.resizing_progress * 100});
|
||||||
|
defer allocator.free(label);
|
||||||
|
|
||||||
|
rl.drawTextEx(
|
||||||
|
rl.getFontDefault(),
|
||||||
|
label,
|
||||||
|
centerOfRect(right_image_rect),
|
||||||
|
30,
|
||||||
|
3,
|
||||||
|
rl.Color.black
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.drawRectangleLinesEx(
|
||||||
|
resized_rect,
|
||||||
|
3,
|
||||||
|
rl.Color.black
|
||||||
|
);
|
||||||
|
|
||||||
|
if (seam_carver.work_thread == null) {
|
||||||
|
const center = centerOfRect(resized_rect);
|
||||||
|
|
||||||
|
const top_left = rl.Vector2.init(resized_rect.x, resized_rect.y);
|
||||||
|
const top_right = rl.Vector2.init(resized_rect.x + resized_rect.width, resized_rect.y);
|
||||||
|
const bottom_left = rl.Vector2.init(resized_rect.x, resized_rect.y + resized_rect.height);
|
||||||
|
const bottom_right = rl.Vector2.init(resized_rect.x + resized_rect.width, resized_rect.y + resized_rect.height);
|
||||||
|
|
||||||
|
const grab_distance = 10;
|
||||||
|
const mouse = rl.getMousePosition();
|
||||||
|
|
||||||
|
if (rl.isMouseButtonDown(.mouse_button_left) and resizing_vertically) {
|
||||||
|
resized_height = @intFromFloat(@abs(center.y - mouse.y)/scale*2);
|
||||||
|
resized_height = @min(resized_height, @as(u32, @intCast(source_image.height)));
|
||||||
|
}
|
||||||
|
if (rl.isMouseButtonDown(.mouse_button_left) and resizing_horizontally) {
|
||||||
|
resized_width = @intFromFloat(@abs(center.x - mouse.x)/scale*2);
|
||||||
|
resized_width = @min(resized_width, @as(u32, @intCast(source_image.width)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceToLine(top_left, top_right, mouse) < grab_distance) {
|
||||||
|
rl.drawLineEx(top_right, top_left, 3, rl.Color.red);
|
||||||
|
if (rl.isMouseButtonPressed(.mouse_button_left)) {
|
||||||
|
resizing_vertically = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (distanceToLine(bottom_left, bottom_right, mouse) < grab_distance) {
|
||||||
|
rl.drawLineEx(bottom_right, bottom_left, 3, rl.Color.red);
|
||||||
|
if (rl.isMouseButtonPressed(.mouse_button_left)) {
|
||||||
|
resizing_vertically = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceToLine(top_left, bottom_left, mouse) < grab_distance) {
|
||||||
|
rl.drawLineEx(top_left, bottom_left, 3, rl.Color.red);
|
||||||
|
if (rl.isMouseButtonPressed(.mouse_button_left)) {
|
||||||
|
resizing_horizontally = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (distanceToLine(bottom_right, top_right, mouse) < grab_distance) {
|
||||||
|
rl.drawLineEx(bottom_right, top_right, 3, rl.Color.red);
|
||||||
|
if (rl.isMouseButtonPressed(.mouse_button_left)) {
|
||||||
|
resizing_horizontally = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rl.isMouseButtonReleased(.mouse_button_left) and (resizing_horizontally or resizing_vertically)) {
|
||||||
|
try seam_carver.startResizing(resized_width, resized_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rl.isMouseButtonReleased(.mouse_button_left)) {
|
||||||
|
resizing_vertically = false;
|
||||||
|
resizing_horizontally = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
354
src/seam-carver.zig
Normal file
354
src/seam-carver.zig
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const rl = @import("raylib");
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const SeamCarver = @This();
|
||||||
|
|
||||||
|
const seam_window_size = 3;
|
||||||
|
comptime {
|
||||||
|
assert(seam_window_size % 2 == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
allocator: Allocator,
|
||||||
|
source_image: rl.Image,
|
||||||
|
|
||||||
|
work_thread: ?std.Thread = null,
|
||||||
|
resized_image: ?rl.Image = null,
|
||||||
|
|
||||||
|
resizing_progress: f32 = 0,
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, source_image: rl.Image) SeamCarver {
|
||||||
|
return SeamCarver{
|
||||||
|
.source_image = source_image,
|
||||||
|
.allocator = allocator
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn startResizing(self: *SeamCarver, new_width: u32, new_height: u32) !void {
|
||||||
|
if (self.work_thread != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thread = try std.Thread.spawn(.{ .allocator = self.allocator }, resizeImageThread, .{self, new_width, new_height});
|
||||||
|
thread.detach(); // TODO: Because of this, thread might leak memory when closing program. Small issue.
|
||||||
|
|
||||||
|
self.work_thread = thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgbToLuminocity(r: u8, g: u8, b: u8) f32 {
|
||||||
|
return 0.2126*@as(f32, @floatFromInt(r)) + 0.7152*@as(f32, @floatFromInt(g)) + 0.0722*@as(f32, @floatFromInt(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn applyLuminance(allocator: Allocator, image: rl.Image) ![]f32 {
|
||||||
|
assert(image.format == .pixelformat_uncompressed_r8g8b8);
|
||||||
|
|
||||||
|
const pixel_count: usize = @intCast(image.width * image.height);
|
||||||
|
const luminance = try allocator.alloc(f32, pixel_count);
|
||||||
|
errdefer allocator.free(luminance);
|
||||||
|
|
||||||
|
const image_data: [*]u8 = @ptrCast(image.data);
|
||||||
|
for (0..pixel_count) |i| {
|
||||||
|
const r = image_data[i*3 + 0];
|
||||||
|
const g = image_data[i*3 + 1];
|
||||||
|
const b = image_data[i*3 + 2];
|
||||||
|
luminance[i] = rgbToLuminocity(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return luminance;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn applySobelPixel(image: []f32, width: u32, height: u32, x: u32, y: u32) f32 {
|
||||||
|
const sobel_center_x = 1;
|
||||||
|
const sobel_center_y = 1;
|
||||||
|
const sobel_size_x = 3;
|
||||||
|
const sobel_size_y = 3;
|
||||||
|
const sobel_x: []const []const f32 = &[_][]const f32{
|
||||||
|
&[_]f32{ -1, 0, 1 },
|
||||||
|
&[_]f32{ -2, 0, 2 },
|
||||||
|
&[_]f32{ -1, 0, 1 },
|
||||||
|
};
|
||||||
|
const sobel_y: []const []const f32 = &[_][]const f32{
|
||||||
|
&[_]f32{ -1, -2, -1 },
|
||||||
|
&[_]f32{ 0, 0, 0 },
|
||||||
|
&[_]f32{ 1, 2, 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
var sum_x: f32 = 0;
|
||||||
|
var sum_y: f32 = 0;
|
||||||
|
|
||||||
|
for (0..sobel_size_y) |oy| {
|
||||||
|
for (0..sobel_size_x) |ox| {
|
||||||
|
var pixel_x: i32 = @as(i32, @intCast(x + ox)) - sobel_center_x;
|
||||||
|
var pixel_y: i32 = @as(i32, @intCast(y + oy)) - sobel_center_y;
|
||||||
|
|
||||||
|
pixel_x = std.math.clamp(pixel_x, 0, @as(i32, @intCast(width))-1);
|
||||||
|
pixel_y = std.math.clamp(pixel_y, 0, @as(i32, @intCast(height))-1);
|
||||||
|
|
||||||
|
const pixel_index = @as(usize, @intCast(pixel_y)) * width + @as(usize, @intCast(pixel_x));
|
||||||
|
|
||||||
|
sum_x += image[pixel_index] * sobel_x[oy][ox];
|
||||||
|
sum_y += image[pixel_index] * sobel_y[oy][ox];
|
||||||
|
|
||||||
|
// sum_x += @as(f32, @floatFromInt(image_data[3*pixel_index + 0])) * sobel_x[oy][ox];
|
||||||
|
// sum_x += @as(f32, @floatFromInt(image_data[3*pixel_index + 1])) * sobel_x[oy][ox];
|
||||||
|
// sum_x += @as(f32, @floatFromInt(image_data[3*pixel_index + 2])) * sobel_x[oy][ox];
|
||||||
|
|
||||||
|
// sum_y += @as(f32, @floatFromInt(image_data[3*pixel_index + 0])) * sobel_y[oy][ox];
|
||||||
|
// sum_y += @as(f32, @floatFromInt(image_data[3*pixel_index + 1])) * sobel_y[oy][ox];
|
||||||
|
// sum_y += @as(f32, @floatFromInt(image_data[3*pixel_index + 2])) * sobel_y[oy][ox];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @sqrt(sum_x*sum_x + sum_y*sum_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn applySobel(allocator: Allocator, image: []f32, width: u32, height: u32) ![]f32 {
|
||||||
|
const image_sobel = try allocator.alloc(f32, width * height);
|
||||||
|
errdefer allocator.free(image_sobel);
|
||||||
|
|
||||||
|
for (0..height) |y| {
|
||||||
|
for (0..width) |x| {
|
||||||
|
image_sobel[y * width + x] = applySobelPixel(image, width, height, @intCast(x), @intCast(y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return image_sobel;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculateEnergy(allocator: Allocator, image: rl.Image) ![]f32 {
|
||||||
|
const luminance = try applyLuminance(allocator, image);
|
||||||
|
defer allocator.free(luminance);
|
||||||
|
|
||||||
|
return try applySobel(allocator, luminance, @intCast(image.width), @intCast(image.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClampedIterator = struct {
|
||||||
|
min: i32,
|
||||||
|
max: i32,
|
||||||
|
|
||||||
|
value: i32,
|
||||||
|
count: usize,
|
||||||
|
|
||||||
|
fn initCentered(min: i32, max: i32, value: i32, radius: u32) ClampedIterator {
|
||||||
|
return ClampedIterator{
|
||||||
|
.min = min,
|
||||||
|
.max = max,
|
||||||
|
.value = value - @as(i32, @intCast(radius / 2)),
|
||||||
|
.count = radius
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(self: *ClampedIterator) ?i32 {
|
||||||
|
if (self.count == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
self.value += 1;
|
||||||
|
self.count -= 1;
|
||||||
|
}
|
||||||
|
return std.math.clamp(self.value, self.min, self.max);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fn removeVerticalSeam(image: *rl.Image, vertical_seam: []u32) void {
|
||||||
|
const image_width: u32 = @intCast(image.width);
|
||||||
|
const image_height: u32 = @intCast(image.height);
|
||||||
|
const image_data: [*]u8 = @ptrCast(image.data);
|
||||||
|
|
||||||
|
assert(image_height == vertical_seam.len);
|
||||||
|
|
||||||
|
var new_index: usize = 0;
|
||||||
|
for (0..image_height) |y| {
|
||||||
|
for (0..image_width) |x| {
|
||||||
|
if (vertical_seam[y] == x) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
image_data[3*new_index+0] = image_data[3*(y * image_width + x)+0];
|
||||||
|
image_data[3*new_index+1] = image_data[3*(y * image_width + x)+1];
|
||||||
|
image_data[3*new_index+2] = image_data[3*(y * image_width + x)+2];
|
||||||
|
new_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
image.width -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findVerticalSeam(result_buffer: []u32, energy_field: []f32, image_width: u32, image_height: u32) []u32 {
|
||||||
|
var seam_x: u32 = 0;
|
||||||
|
for (0..image_width) |x| {
|
||||||
|
if (energy_field[(image_height-1) * image_height + seam_x] > energy_field[(image_height-1) * image_height + x]) {
|
||||||
|
seam_x = @intCast(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var vertical_seam = result_buffer[0..image_height];
|
||||||
|
|
||||||
|
var seam_y: usize = image_height-1;
|
||||||
|
while (true) {
|
||||||
|
vertical_seam[seam_y] = seam_x;
|
||||||
|
|
||||||
|
if (seam_y == 0) break;
|
||||||
|
seam_y -= 1;
|
||||||
|
|
||||||
|
var x_iter = ClampedIterator.initCentered(0, @as(i32, @intCast(image_width))-1, @intCast(seam_x), seam_window_size);
|
||||||
|
while (x_iter.next()) |signed_next_x| {
|
||||||
|
const next_x: usize = @intCast(signed_next_x);
|
||||||
|
const next_energy = energy_field[seam_y * image_width + next_x];
|
||||||
|
const current_energy = energy_field[seam_y * image_width + seam_x];
|
||||||
|
if (current_energy > next_energy) {
|
||||||
|
seam_x = @intCast(next_x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return vertical_seam;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn removeHorizontalSeam(image: *rl.Image, horizontal_seam: []u32) void {
|
||||||
|
const image_width: u32 = @intCast(image.width);
|
||||||
|
const image_height: u32 = @intCast(image.height);
|
||||||
|
const old_image_data: [*]u8 = @ptrCast(image.data);
|
||||||
|
const new_image_data: [*]u8 = @ptrCast(std.c.malloc(image_width * (image_height-1) * 3) orelse @panic("OOM"));
|
||||||
|
|
||||||
|
assert(image_width == horizontal_seam.len);
|
||||||
|
|
||||||
|
for (0..image_width) |x| {
|
||||||
|
var new_y: u32 = 0;
|
||||||
|
for (0..image_height) |y| {
|
||||||
|
if (horizontal_seam[x] == y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
new_image_data[3*(new_y * image_width + x)+0] = old_image_data[3*(y * image_width + x)+0];
|
||||||
|
new_image_data[3*(new_y * image_width + x)+1] = old_image_data[3*(y * image_width + x)+1];
|
||||||
|
new_image_data[3*(new_y * image_width + x)+2] = old_image_data[3*(y * image_width + x)+2];
|
||||||
|
new_y += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
image.height -= 1;
|
||||||
|
image.data = new_image_data;
|
||||||
|
std.c.free(old_image_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findHorizontalSeam(result_buffer: []u32, energy_field: []f32, image_width: u32, image_height: u32) []u32 {
|
||||||
|
var seam_y: u32 = 0;
|
||||||
|
for (0..image_height) |y| {
|
||||||
|
if (energy_field[seam_y * image_height + (image_width-1)] > energy_field[y * image_height + (image_width-1)]) {
|
||||||
|
seam_y = @intCast(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const horizontal_seam = result_buffer[0..image_width];
|
||||||
|
@memset(horizontal_seam, image_width/2);
|
||||||
|
|
||||||
|
var seam_x: usize = image_width-1;
|
||||||
|
while (true) {
|
||||||
|
horizontal_seam[seam_x] = seam_y;
|
||||||
|
|
||||||
|
if (seam_x == 0) break;
|
||||||
|
seam_x -= 1;
|
||||||
|
|
||||||
|
var y_iter = ClampedIterator.initCentered(0, @as(i32, @intCast(image_height))-1, @intCast(seam_y), seam_window_size);
|
||||||
|
while (y_iter.next()) |signed_next_y| {
|
||||||
|
const next_y: usize = @intCast(signed_next_y);
|
||||||
|
const next_energy = energy_field[next_y * image_width + seam_x];
|
||||||
|
const current_energy = energy_field[seam_y * image_width + seam_x];
|
||||||
|
if (current_energy > next_energy) {
|
||||||
|
seam_y = @intCast(next_y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return horizontal_seam;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resizeImageThread(self: *SeamCarver, new_width: u32, new_height: u32) !void {
|
||||||
|
var image = self.source_image.copy();
|
||||||
|
errdefer rl.unloadImage(image);
|
||||||
|
|
||||||
|
self.resizing_progress = 0;
|
||||||
|
|
||||||
|
const horizontal_reductions = @as(u32, @intCast(image.width)) - new_width;
|
||||||
|
const vertical_reductions = @as(u32, @intCast(image.height)) - new_height;
|
||||||
|
const total_progress = @as(f32, @floatFromInt(horizontal_reductions)) + @as(f32, @floatFromInt(vertical_reductions));
|
||||||
|
|
||||||
|
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
const vertical_seam_buffer = try self.allocator.alloc(u32, @intCast(image.height));
|
||||||
|
defer self.allocator.free(vertical_seam_buffer);
|
||||||
|
|
||||||
|
for (0..horizontal_reductions) |iter| {
|
||||||
|
_ = arena.reset(.retain_capacity);
|
||||||
|
|
||||||
|
const image_width: u32 = @intCast(image.width);
|
||||||
|
const image_height: u32 = @intCast(image.height);
|
||||||
|
|
||||||
|
const energy_field = try calculateEnergy(arena.allocator(), image);
|
||||||
|
|
||||||
|
// Calculate top down energy field
|
||||||
|
for (1..image_height) |y| {
|
||||||
|
for (0..image_width) |x| {
|
||||||
|
var min_top_value: f32 = std.math.floatMax(f32);
|
||||||
|
|
||||||
|
var seam_x_iter = ClampedIterator.initCentered(0, @as(i32, @intCast(image_width))-1, @intCast(x), seam_window_size);
|
||||||
|
while (seam_x_iter.next()) |seam_x| {
|
||||||
|
const index = (y - 1) * image_width + @as(usize, @intCast(seam_x));
|
||||||
|
min_top_value = @min(min_top_value, energy_field[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
energy_field[y * image_width + x] += min_top_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vertical_seam = findVerticalSeam(vertical_seam_buffer, energy_field, image_width, image_height);
|
||||||
|
removeVerticalSeam(&image, vertical_seam);
|
||||||
|
|
||||||
|
self.resizing_progress = @as(f32, @floatFromInt(iter)) / total_progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
const horizontal_seam_buffer = try self.allocator.alloc(u32, @intCast(image.width));
|
||||||
|
defer self.allocator.free(horizontal_seam_buffer);
|
||||||
|
|
||||||
|
for (0..vertical_reductions) |iter| {
|
||||||
|
_ = arena.reset(.retain_capacity);
|
||||||
|
|
||||||
|
const image_width: u32 = @intCast(image.width);
|
||||||
|
const image_height: u32 = @intCast(image.height);
|
||||||
|
|
||||||
|
const energy_field = try calculateEnergy(arena.allocator(), image);
|
||||||
|
|
||||||
|
// Calculate left to right energy field
|
||||||
|
for (1..image_width) |x| {
|
||||||
|
for (0..image_height) |y| {
|
||||||
|
var min_left_value: f32 = std.math.floatMax(f32);
|
||||||
|
|
||||||
|
var seam_y_iter = ClampedIterator.initCentered(0, @as(i32, @intCast(image_height))-1, @intCast(y), seam_window_size);
|
||||||
|
while (seam_y_iter.next()) |seam_y| {
|
||||||
|
const index = @as(usize, @intCast(seam_y)) * image_width + (x - 1);
|
||||||
|
min_left_value = @min(min_left_value, energy_field[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
energy_field[y * image_width + x] += min_left_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const horizontal_seam = findHorizontalSeam(horizontal_seam_buffer, energy_field, image_width, image_height);
|
||||||
|
removeHorizontalSeam(&image, horizontal_seam);
|
||||||
|
|
||||||
|
self.resizing_progress = @as(f32, @floatFromInt(horizontal_reductions + iter)) / total_progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.resized_image = image;
|
||||||
|
self.work_thread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: SeamCarver) void {
|
||||||
|
if (self.resized_image) |image| {
|
||||||
|
rl.unloadImage(image);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user