//! Image processing module
//!
//! This module provides a unified interface to image processing functionality.
//! The main Image struct supports generic pixel types and provides operations for:
//! - Loading and saving images (PNG, JPEG, BMP)
//! - Terminal display with multiple formats (SGR, Braille, Sixel, Kitty)
//! - Geometric transforms (resize, rotate, crop, flip)
//! - Filters (blur, sharpen, edge detection)
//! - Views for zero-copy sub-image operations
//!
//! ## Aliasing contract
//!
//! Input and output images must be fully disjoint, unless a function documents
//! in-place support. Partial overlap is undefined behavior, silently corrupts
//! the output, and is not checked at runtime.

const std = @import("std");
const Allocator = std.mem.Allocator;
const Io = std.Io;
const assert = std.debug.assert;

const Rgb = @import("color.zig").Rgb(u8);
const Rgba = @import("color.zig").Rgba(u8);
const convertColor = @import("color.zig").convertColor;
const Rectangle = @import("geometry.zig").Rectangle;
const Point = @import("geometry/Point.zig").Point;
const codecs = @import("codecs.zig");
const bmp = codecs.bmp;
const gif = codecs.gif;
const jpeg = codecs.jpeg;
const png = codecs.png;
const metrics = @import("image/metrics.zig");
const diff_mod = @import("image/diff.zig");

pub const BorderMode = @import("image/border.zig").BorderMode;
const DisplayFormatter = @import("image/display.zig").DisplayFormatter;
const Edges = @import("image/edges.zig").Edges;
const Enhancement = @import("image/enhancement.zig").Enhancement;
const binary = @import("image/binary.zig");
const meta = @import("meta.zig");
const Transform = @import("image/transforms.zig").Transform;
const interpolation = @import("image/interpolation.zig");
const OrderStatisticBlurOps = @import("image/order_statistic_blur.zig").OrderStatisticBlurOps;

pub const DisplayFormat = @import("image/display.zig").DisplayFormat;
pub const ImageFormat = @import("image/format.zig").ImageFormat;
pub const Interpolation = @import("image/interpolation.zig").Interpolation;
pub const PixelIterator = @import("image/PixelIterator.zig").PixelIterator;
pub const AnimatedImage = @import("image/animated.zig").AnimatedImage;
pub const ShenCastan = @import("image/ShenCastan.zig");
pub const HoughTransform = @import("image/hough.zig").HoughTransform;
pub const Histogram = @import("image/histogram.zig").Histogram;
pub const BinaryKernel = binary.Kernel;
const convolution = @import("image/convolution.zig");
const box_blur = @import("image/box_blur.zig");
pub const MotionBlur = @import("image/motion_blur.zig").MotionBlur;
const MotionBlurOps = @import("image/motion_blur.zig").MotionBlurOps;
pub const Colormap = @import("image/colormaps.zig").Colormap;
const Blending = @import("blending.zig").Blending;
pub const FloodFillOptions = @import("image/flood_fill.zig").FloodFillOptions;

/// Assigns `sample` into `dest`, applying blending when requested and converting
/// between color spaces as needed. `dest` must be a pointer to the pixel to
/// modify. When `sample` is `Rgba` and a blend mode other than `.none` is
/// requested, the destination pixel is converted to `Rgba`, composited, and
/// converted back to the destination type. Otherwise the sample is converted to
/// the target type and stored directly.
pub inline fn assignPixel(dest: anytype, sample: anytype, blend_mode: Blending) void {
    comptime {
        const info = @typeInfo(@TypeOf(dest));
        if (info != .pointer) @compileError("assignPixel expects a pointer destination");
    }
    const DestType = std.meta.Child(@TypeOf(dest));
    const SrcType = @TypeOf(sample);

    if (comptime SrcType == Rgba) {
        if (blend_mode != .none) {
            const rgba_sample: Rgba = sample;
            if (comptime DestType == Rgba) {
                dest.* = dest.blend(rgba_sample, blend_mode);
            } else {
                const dst_rgba = convertColor(Rgba, dest.*);
                const blended = dst_rgba.blend(rgba_sample, blend_mode);
                dest.* = convertColor(DestType, blended);
            }
            return;
        }
    }

    if (comptime SrcType == DestType) {
        dest.* = sample;
    } else {
        dest.* = convertColor(DestType, sample);
    }
}

/// A simple image struct that encapsulates the size and the data.
pub fn Image(comptime T: type) type {
    return struct {
        rows: u32,
        cols: u32,
        data: []T,
        stride: usize,

        const Self = @This();

        /// Integral image operations for fast box filtering and region sums.
        pub const Integral = @import("image/integral.zig").Integral(T);

        /// Creates an empty image with zero dimensions, used as a placeholder for output parameters.
        /// When passed to functions like `rotateFrom()`, `blurBox()`, etc., the function will
        /// automatically allocate and size the image appropriately. This eliminates the need
        /// to pre-allocate or guess output dimensions.
        ///
        /// Example usage:
        /// ```zig
        /// var rotated: Image(u8) = .empty;
        /// try image.rotateFrom(allocator, center, angle, &rotated); // Auto-sizes to optimal dimensions
        /// defer rotated.deinit(allocator);
        /// ```
        pub const empty: Self = .{ .rows = 0, .cols = 0, .data = &[_]T{}, .stride = 0 };

        /// Constructs an image of rows and cols size allocating its own memory.
        /// The image owns the memory and deinit should be called to free it.
        pub fn init(allocator: Allocator, rows: u32, cols: u32) !Image(T) {
            const pixel_count = try std.math.mul(usize, rows, cols);
            return .{
                .rows = rows,
                .cols = cols,
                .data = try allocator.alloc(T, pixel_count),
                .stride = cols,
            };
        }

        /// Constructs an image with the same dimensions as the reference image.
        /// The image owns the memory and deinit should be called to free it.
        ///
        /// Example usage:
        /// ```zig
        /// var blurred = try Image(u8).initLike(allocator, original);
        /// defer blurred.deinit(allocator);
        /// try original.gaussianBlur(1.4, blurred);
        /// ```
        pub fn initLike(allocator: Allocator, reference: anytype) !Image(T) {
            const RefType = @TypeOf(reference);
            comptime assert(@hasField(RefType, "rows"));
            comptime assert(@hasField(RefType, "cols"));
            return init(allocator, reference.rows, reference.cols);
        }

        /// Frees the image's backing buffer and resets it to `empty`. Only call this on images
        /// returned by an allocating constructor (`init`, `initLike`, `dupe`, `scale`, `rotate`,
        /// `crop`, `load`, `loadFromBytes`, `convert`, `applyColormap`). Do not call on views or
        /// images created via `initFromSlice` / `initFromBytes`; those do not own their storage.
        /// Idempotent: safe to call on an already-deinit'd or default-constructed image.
        pub fn deinit(self: *Self, allocator: Allocator) void {
            allocator.free(self.data);
            self.* = .empty;
        }

        /// Constructs an image of rows and cols size from an existing slice.
        pub fn initFromSlice(rows: u32, cols: u32, data: []T) Image(T) {
            const expected_len = std.math.mul(usize, rows, cols) catch @panic("Image.initFromSlice overflow");
            assert(data.len >= expected_len);
            return .{
                .rows = rows,
                .cols = cols,
                .data = data[0..expected_len],
                .stride = cols,
            };
        }

        /// Constructs an image of `rows` and `cols` size by reinterpreting the provided slice of `bytes` as a slice of `T`.
        /// The length of the `bytes` slice must be exactly `rows * cols * @sizeOf(T)`.
        pub fn initFromBytes(rows: u32, cols: u32, bytes: []u8) Image(T) {
            const expected_len = std.math.mul(usize, rows, cols) catch @panic("Image.initFromBytes overflow");
            const expected_bytes = std.math.mul(usize, expected_len, @sizeOf(T)) catch @panic("Image.initFromBytes overflow");
            assert(expected_bytes == bytes.len);
            return .{
                .rows = rows,
                .cols = cols,
                .data = @as([*]T, @ptrCast(@alignCast(bytes.ptr)))[0 .. bytes.len / @sizeOf(T)],
                .stride = cols,
            };
        }

        /// Fills the entire image with a solid value.
        pub fn fill(self: Self, value: T) void {
            if (self.isContiguous()) {
                @memset(self.data, value);
            } else {
                for (0..self.rows) |r| {
                    const start = r * self.stride;
                    @memset(self.data[start .. start + self.cols], value);
                }
            }
        }

        /// Sets the border outside `rect` to `value` (rect is clipped to bounds).
        /// Efficiently fills only the top/bottom bands and left/right bands per row.
        pub fn setBorder(self: Self, rect: Rectangle(u32), value: T) void {
            const bounds = self.getRectangle();
            const inner = bounds.intersect(rect) orelse {
                self.fill(value);
                return;
            };

            var r: usize = 0;
            while (r < inner.t) : (r += 1) {
                const start = r * self.stride;
                @memset(self.data[start .. start + self.cols], value);
            }

            r = inner.t;
            while (r < inner.b) : (r += 1) {
                const row_start = r * self.stride;
                if (inner.l > 0) {
                    @memset(self.data[row_start .. row_start + inner.l], value);
                }
                if (inner.r < self.cols) {
                    @memset(self.data[row_start + inner.r .. row_start + self.cols], value);
                }
            }

            r = inner.b;
            while (r < self.rows) : (r += 1) {
                const start = r * self.stride;
                @memset(self.data[start .. start + self.cols], value);
            }
        }

        /// Returns the image data reinterpreted as a slice of bytes.
        /// Note: The image should not be a view; this is enforced by an assertion.
        pub fn asBytes(self: Self) []u8 {
            assert(self.rows * self.cols == self.data.len);
            assert(self.isContiguous());
            return @as([*]u8, @ptrCast(@alignCast(self.data.ptr)))[0 .. self.data.len * @sizeOf(T)];
        }

        /// Loads an image from a file with automatic format detection.
        /// Detects format based on file header signatures and calls the appropriate loader.
        ///
        /// Example usage:
        /// ```zig
        /// var img = try Image(Rgb).load(io, allocator, "photo.jpg");
        /// defer img.deinit(allocator);
        /// ```
        pub fn load(io: Io, allocator: Allocator, file_path: []const u8) !Self {
            const image_format = try ImageFormat.detectFromPath(io, file_path) orelse return error.UnsupportedImageFormat;
            return switch (image_format) {
                .png => png.load(T, io, allocator, file_path, .{}),
                .jpeg => jpeg.load(T, io, allocator, file_path, .{}),
                .bmp => bmp.load(T, io, allocator, file_path, .{}),
                .gif => gif.load(T, io, allocator, file_path, .{}),
            };
        }

        /// Loads an image from an in-memory byte buffer with automatic format detection.
        /// This is useful when image data comes from network streams or preloaded assets.
        ///
        /// Example usage:
        /// ```zig
        /// const bytes = try fetchNetworkImage();
        /// var img: Image(Rgb) = try .loadFromBytes(allocator, bytes);
        /// defer img.deinit(allocator);
        /// ```
        pub fn loadFromBytes(allocator: Allocator, data: []const u8) !Self {
            const image_format = ImageFormat.detectFromBytes(data) orelse return error.UnsupportedImageFormat;
            return switch (image_format) {
                .png => png.loadFromBytes(T, allocator, data, .{}),
                .jpeg => jpeg.loadFromBytes(T, allocator, data, .{}),
                .bmp => bmp.loadFromBytes(T, allocator, data, .{}),
                .gif => gif.loadFromBytes(T, allocator, data, .{}),
            };
        }

        /// Saves the image to a file. Format is selected from the file extension:
        /// `.png`, `.jpg`/`.jpeg`, `.bmp`, or `.gif` (case-insensitive).
        /// Returns `error.UnsupportedImageFormat` for any other extension.
        pub fn save(self: Self, io: Io, allocator: Allocator, file_path: []const u8) !void {
            const fmt = ImageFormat.fromExtension(file_path) orelse return error.UnsupportedImageFormat;
            return switch (fmt) {
                .png => png.save(T, io, allocator, self, file_path),
                .jpeg => jpeg.save(T, io, allocator, self, file_path),
                .bmp => bmp.save(T, io, allocator, self, file_path),
                .gif => gif.save(T, io, allocator, self, file_path),
            };
        }

        /// Returns the total number of pixels in the image (rows * cols).
        pub inline fn size(self: Self) usize {
            return @as(usize, self.rows) * @as(usize, self.cols);
        }

        /// Returns the number of channels or depth of this image type.
        pub fn channels() u32 {
            return comptime switch (@typeInfo(T)) {
                .int, .float => 1,
                .@"struct" => |info| info.field_names.len,
                .array => |info| info.len,
                else => @compileError("Image(" ++ @typeName(T) ++ ") is unsupported."),
            };
        }

        /// Returns true if and only if `self` and `other` have the same number of rows and columns.
        /// It does not compare pixel data or types.
        pub fn hasSameShape(self: Self, other: anytype) bool {
            return self.rows == other.rows and self.cols == other.cols;
        }

        /// Returns the bounding rectangle for the current image.
        pub fn getRectangle(self: Self) Rectangle(u32) {
            return .{ .l = 0, .t = 0, .r = self.cols, .b = self.rows };
        }

        /// Returns the center point of the image as a Point(2, f32).
        /// This is commonly used as the rotation center for image rotation.
        ///
        /// Example usage:
        /// ```zig
        /// try image.rotate(allocator, image.getCenter(), angle, &rotated);
        /// ```
        pub fn getCenter(self: Self) Point(2, f32) {
            return .init(.{
                @as(f32, @floatFromInt(self.cols)) / 2.0,
                @as(f32, @floatFromInt(self.rows)) / 2.0,
            });
        }

        /// Returns an image view with boundaries defined by `rect` within the image boundaries.
        /// The returned image references the memory of `self`, so there are no allocations
        /// or copies.
        pub fn view(self: Self, rect: Rectangle(u32)) Image(T) {
            const clipped = self.getRectangle().intersect(rect) orelse {
                return Self.empty;
            };
            if (clipped.isEmpty()) {
                return Self.empty;
            }

            const rows = clipped.height();
            const cols = clipped.width();
            const start = @as(usize, clipped.t) * self.stride + @as(usize, clipped.l);
            const end = @as(usize, clipped.b - 1) * self.stride + @as(usize, clipped.r);
            return .{
                .rows = rows,
                .cols = cols,
                .data = self.data[start..end],
                .stride = self.stride,
            };
        }

        /// Returns true if the image data is stored contiguously in memory.
        /// This is determined by checking if the `cols` field equals the `stride` field.
        /// When false, there is padding between rows.
        pub fn isContiguous(self: Self) bool {
            return self.cols == self.stride;
        }

        /// Creates a duplicate of the image with newly allocated memory.
        /// Correctly handles views by copying only the visible data.
        ///
        /// Example usage:
        /// ```zig
        /// var duped = try image.dupe(allocator);
        /// defer duped.deinit(allocator);
        /// ```
        pub fn dupe(self: Self, allocator: Allocator) !Self {
            const result: Self = try .init(allocator, self.rows, self.cols);
            self.copy(result);
            return result;
        }

        /// Copies image data from `self` to `dst`, correctly handling views.
        /// If src and dst are the same object, does nothing (no-op).
        pub fn copy(self: Self, dst: Self) void {
            assert(self.hasSameShape(dst));
            if (self.data.ptr == dst.data.ptr) {
                return;
            }
            if (self.isContiguous() and dst.isContiguous()) {
                @memcpy(dst.data, self.data);
            } else {
                for (0..self.rows) |r| {
                    const src_row_start = r * self.stride;
                    const dst_row_start = r * dst.stride;
                    @memcpy(
                        dst.data[dst_row_start .. dst_row_start + self.cols],
                        self.data[src_row_start .. src_row_start + self.cols],
                    );
                }
            }
        }

        /// Converts the image to a different pixel type, writing into a pre-allocated output image.
        /// The output image `out` must have the same dimensions as `self`.
        pub fn convertInto(self: Self, comptime TargetType: type, out: Image(TargetType)) void {
            assert(self.hasSameShape(out));
            if (comptime T == TargetType) {
                self.copy(out);
            } else {
                for (0..self.rows) |r| {
                    for (0..self.cols) |c| {
                        out.at(r, c).* = convertColor(TargetType, self.at(r, c).*);
                    }
                }
            }
        }

        /// Converts the image to a different pixel type.
        /// Allocates a new image with the target pixel type and converts each pixel using the color conversion system.
        ///
        /// Example usage:
        /// ```zig
        /// var rgba_image: Image(Rgba) = ...;
        /// var gray_image = try rgba_image.convert(allocator, u8);
        /// defer gray_image.deinit(allocator);
        /// ```
        pub fn convert(self: Self, allocator: Allocator, comptime TargetType: type) !Image(TargetType) {
            const result = try Image(TargetType).init(allocator, self.rows, self.cols);
            self.convertInto(TargetType, result);
            return result;
        }

        /// Returns the value at position row, col.  It assumes the coordinates are in bounds and
        /// triggers safety-checked undefined behavior when they aren't.
        pub inline fn at(self: Self, row: usize, col: usize) *T {
            assert(row < self.rows);
            assert(col < self.cols);
            return &self.data[row * self.stride + col];
        }

        /// Returns the optional value at row, col in the image.
        pub fn atOrNull(self: Self, row: i64, col: i64) ?*T {
            const irows: isize = @intCast(self.rows);
            const icols: isize = @intCast(self.cols);
            if (row < 0 or col < 0 or row >= irows or col >= icols) {
                return null;
            } else {
                return self.at(@intCast(row), @intCast(col));
            }
        }

        /// Creates a formatter for terminal display with custom options.
        /// Provides fine-grained control over output format, palette modes, and dithering.
        ///
        /// Display modes:
        /// - `.sgr`: Uses SGR (Select Graphic Rendition) with Unicode half-block characters (requires monospace font with U+2580 support)
        /// - `.braille`: Uses Braille patterns for 2x4 resolution (requires Unicode Braille support U+2800-U+28FF; dots binarized by `threshold`, optionally tinted with truecolor or a quantized palette)
        /// - `.sixel`: Uses the sixel graphics protocol if supported
        /// - `.kitty`: Uses the kitty graphics protocol if supported
        /// - `.iterm2`: Uses the iTerm2 inline image protocol if supported
        /// - `.auto`: Automatically selects best available format: kitty -> iterm2 -> sixel -> sgr
        ///
        /// Example:
        /// ```zig
        /// const img = try Image(Rgb).load(io, allocator, "test.png");
        /// std.debug.print("{f}", .{img.display(io, .sgr)});           // SGR with unicode half blocks
        /// std.debug.print("{f}", .{img.display(io, .{ .braille = .{ .threshold = 0.5 } })}); // 2x4 braille, truecolor tint
        /// std.debug.print("{f}", .{img.display(io, .{ .sixel = .{ .palette_mode = .adaptive } })});
        /// std.debug.print("{f}", .{img.display(io, .{ .kitty = .default })});  // Kitty graphics protocol
        /// ```
        pub fn display(self: *const Self, io: Io, display_format: DisplayFormat) DisplayFormatter(T) {
            return .{
                .image = self,
                .display_format = display_format,
                .io = io,
            };
        }

        /// Displays the image information: color type, rows and cols.
        pub fn format(self: Self, writer: *Io.Writer) Io.Writer.Error!void {
            const type_name: []const u8 = @typeName(T);
            if (std.mem.lastIndexOfScalar(u8, type_name, '.')) |pos| {
                try writer.print("Image({s}){{ .rows = {d}, .cols = {d} }}", .{ type_name[pos + 1 ..], self.rows, self.cols });
            } else {
                try writer.print("Image({s}){{ .rows = {d}, .cols = {d} }}", .{ type_name, self.rows, self.cols });
            }
        }

        /// Flips an image from left to right (mirror effect).
        pub fn flipLeftRight(self: Self) void {
            return Transform(T).flipLeftRight(self);
        }

        /// Flips an image from top to bottom (upside down effect).
        pub fn flipTopBottom(self: Self) void {
            return Transform(T).flipTopBottom(self);
        }

        /// Inverts the colors of an image in-place.
        /// For grayscale (u8): inverts as 255 - value
        /// For RGB colors: inverts each channel as 255 - channel
        /// For RGBA colors: inverts RGB channels but preserves alpha
        pub fn invert(self: Self) void {
            if (T == u8) {
                for (0..self.rows) |r| {
                    for (0..self.cols) |c| {
                        const pixel = self.at(r, c);
                        pixel.* = 255 - pixel.*;
                    }
                }
            } else if (@hasDecl(T, "invert")) {
                for (0..self.rows) |r| {
                    for (0..self.cols) |c| {
                        const pixel = self.at(r, c);
                        pixel.* = pixel.invert();
                    }
                }
            } else {
                @compileError("invert() requires pixel types with an invert() method or u8 grayscale pixels");
            }
        }

        /// Performs interpolation at position x, y using the specified method.
        /// Returns `null` if the coordinates are outside valid bounds for the chosen method.
        pub fn interpolate(self: Self, x: f32, y: f32, method: Interpolation, border: BorderMode) ?T {
            return interpolation.interpolate(T, self, x, y, method, border);
        }

        /// Resizes an image to fit in out, using the specified interpolation method.
        /// The output image must have the desired dimensions pre-allocated.
        /// Note: allocator is used for temporary buffers during RGB/RGBA channel processing.
        pub fn resize(self: Self, out: Self, allocator: Allocator, method: Interpolation) void {
            interpolation.resize(T, self, out, allocator, method);
        }

        /// Scales the image by the given factor using the specified interpolation method.
        /// A factor > 1.0 enlarges the image, < 1.0 shrinks it.
        /// The caller is responsible for calling deinit() on the returned image.
        pub fn scale(self: Self, allocator: Allocator, factor: f32, method: Interpolation) !Self {
            if (factor <= 0) return error.InvalidScaleFactor;

            const new_rows: u32 = @round(@as(f32, @floatFromInt(self.rows)) * factor);
            const new_cols: u32 = @round(@as(f32, @floatFromInt(self.cols)) * factor);

            if (new_rows == 0 or new_cols == 0) return error.InvalidDimensions;

            const scaled: Self = try .init(allocator, new_rows, new_cols);
            self.resize(scaled, allocator, method);
            return scaled;
        }

        /// Resizes an image to fit within the output dimensions while preserving aspect ratio.
        /// The image is centered with black/zero padding around it (letterboxing).
        /// Returns a rectangle describing the area containing the actual image content.
        pub fn letterbox(self: Self, out: Self, allocator: Allocator, method: Interpolation) Rectangle(u32) {
            return Transform(T).letterbox(self, out, allocator, method);
        }

        /// Rotates the image by `angle` (radians) around its center, returning a new image sized
        /// to fit the rotated content. Caller must `deinit` the result.
        ///
        /// Example:
        /// ```zig
        /// var rotated = try image.rotate(allocator, std.math.pi / 4.0, .bilinear, .zero);
        /// defer rotated.deinit(allocator);
        /// ```
        pub fn rotate(self: Self, allocator: Allocator, angle: f32, method: Interpolation, border: BorderMode) !Self {
            return Transform(T).rotate(self, allocator, angle, method, border);
        }

        /// Rotates the image into the pre-allocated `out`, centered with `border` padding for any
        /// uncovered area. Use `rotateBounds(angle)` to size `out` if you want no clipping.
        pub fn rotateInto(self: Self, out: Self, angle: f32, method: Interpolation, border: BorderMode) void {
            return Transform(T).rotateInto(self, out, angle, method, border);
        }

        /// Computes the output dimensions needed to contain `self` rotated by `angle` (radians)
        /// without clipping.
        pub fn rotateBounds(self: Self, angle: f32) struct { rows: u32, cols: u32 } {
            return Transform(T).rotateBounds(self, angle);
        }

        /// Crops a rectangular region from the image. Coordinates are rounded; out-of-bounds areas
        /// are filled with zeroed pixels (e.g., black/transparent). Caller must `deinit` the result.
        ///
        /// Example:
        /// ```zig
        /// var chip = try image.crop(allocator, .{ .l = 10, .t = 10, .r = 100, .b = 100 });
        /// defer chip.deinit(allocator);
        /// ```
        pub fn crop(self: Self, allocator: Allocator, rectangle: Rectangle(f32)) !Self {
            return Transform(T).crop(self, allocator, rectangle);
        }

        /// Extracts a rotated rectangular region (defined in source coordinates) and resamples it
        /// to fill the pre-allocated `out` image. `angle` is in radians, counter-clockwise around
        /// the rect center.
        ///
        /// Notes:
        /// - Out-of-bounds samples are filled with zeroed pixels (e.g., black/transparent).
        /// - `out` can be a view; strides are respected via `at()` accessors.
        pub fn extract(self: Self, out: Self, rect: Rectangle(f32), angle: f32, method: Interpolation, border: BorderMode) void {
            return Transform(T).extract(self, out, rect, angle, method, border);
        }

        /// Inserts `source` into `self` at the destination rectangle, with optional rotation
        /// (radians, counter-clockwise around the rect center). Complement of `extract`.
        ///
        /// Notes:
        /// - The source image is scaled to fit the destination rectangle.
        /// - For Image(Rgba) sources, alpha blending is applied using the specified blend mode.
        /// - When the source is not RGBA, pixels are copied directly.
        /// - Pixels outside the source bounds are not modified in self.
        /// - This method mutates self in-place.
        pub fn insert(self: *Self, source: anytype, rect: Rectangle(f32), angle: f32, method: Interpolation, blend_mode: Blending) void {
            return Transform(T).insert(self, source, rect, angle, method, blend_mode);
        }

        /// Warps the image through a Similarity, Affine, or Projective `transform`, sampling each
        /// destination pixel from the corresponding source location. `out` must be pre-allocated
        /// to the desired output shape.
        ///
        /// Example:
        /// ```zig
        /// const transform: SimilarityTransform(T) = try .init(from_points, to_points);
        /// const warped: Image(T) = try .init(allocator, 512, 512);
        /// defer warped.deinit(allocator);
        /// image.warp(warped, transform, .bilinear);
        /// ```
        pub fn warp(self: Self, out: Self, transform: anytype, method: Interpolation) void {
            return Transform(T).warp(self, out, transform, method);
        }

        /// Computes the integral image, also known as a summed-area table (SAT), of `self`.
        /// For multi-channel images (e.g., structs like `Rgba`), it computes a per-channel
        /// integral image, storing one f32 plane per channel in `planes`.
        pub fn integral(self: Self, allocator: Allocator, planes: *Self.Integral.Planes) !void {
            return Self.Integral.compute(self, allocator, planes);
        }

        /// Computes a blurred version of `self` using a box blur. The `radius` parameter
        /// determines the size of the box window. The output image must be pre-allocated
        /// with the same dimensions as the input.
        pub fn boxBlur(self: Self, out: Self, allocator: Allocator, radius: u32) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            if (radius == 0) {
                self.copy(out);
                return;
            }

            try box_blur.boxBlur(T, self, out, allocator, radius);
        }

        /// Applies a median blur using a square window with the given radius.
        /// Radius specifies half the window size; window size = `radius * 2 + 1`.
        /// The output image must be pre-allocated with the same dimensions as the input.
        pub fn medianBlur(self: Self, out: Self, allocator: Allocator, radius: usize) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            try OrderStatisticBlurOps(T).medianBlur(self, out, allocator, radius);
        }

        /// Applies a percentile blur (order-statistic filter) with the given percentile fraction.
        /// Percentile must be in the range [0, 1]; 0.5 corresponds to a median blur.
        ///
        /// Useful when you want fine-grained control over which ranked pixel is kept from the
        /// neighborhood. For example, `percentile = 0.1` can suppress bright outliers while
        /// retaining much of the local structure.
        ///
        /// ```zig
        /// var robust = try Image(u8).initLike(allocator, image);
        /// defer robust.deinit(allocator);
        /// try image.percentileBlur(robust, allocator, 2, 0.1, .mirror);
        /// ```
        pub fn percentileBlur(
            self: Self,
            out: Self,
            allocator: Allocator,
            radius: usize,
            percentile: f64,
            border: BorderMode,
        ) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            try OrderStatisticBlurOps(T).percentileBlur(self, out, allocator, radius, percentile, border);
        }

        /// Applies a minimum blur (percentile zero) over a square window with the given radius.
        ///
        /// This is the morphological *erosion* operator – great for removing "salt" noise or
        /// shrinking bright speckles while leaving darker structures intact.
        ///
        /// ```zig
        /// var denoised = try Image(u8).initLike(allocator, image);
        /// defer denoised.deinit(allocator);
        /// try image.minBlur(denoised, allocator, 1, .mirror);
        /// ```
        pub fn minBlur(
            self: Self,
            out: Self,
            allocator: Allocator,
            radius: usize,
            border: BorderMode,
        ) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            try OrderStatisticBlurOps(T).minBlur(self, out, allocator, radius, border);
        }

        /// Applies a maximum blur (percentile one) over a square window with the given radius.
        ///
        /// Equivalent to morphological *dilation*. It can fill in small gaps or expand highlights,
        /// which is helpful for creating masks or closing thin cracks.
        ///
        /// ```zig
        /// var mask = try Image(u8).initLike(allocator, image);
        /// defer mask.deinit(allocator);
        /// try image.maxBlur(mask, allocator, 2, .mirror);
        /// ```
        pub fn maxBlur(
            self: Self,
            out: Self,
            allocator: Allocator,
            radius: usize,
            border: BorderMode,
        ) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            try OrderStatisticBlurOps(T).maxBlur(self, out, allocator, radius, border);
        }

        /// Applies a midpoint blur that averages the minimum and maximum values within the window.
        ///
        /// Midpoint filtering reduces random impulse noise while retaining thin edges.
        /// Think of it as a compromise between min and max filters.
        ///
        /// ```zig
        /// var softened = try Image(u8).initLike(allocator, image);
        /// defer softened.deinit(allocator);
        /// try image.midpointBlur(softened, allocator, 1, .mirror);
        /// ```
        pub fn midpointBlur(
            self: Self,
            out: Self,
            allocator: Allocator,
            radius: usize,
            border: BorderMode,
        ) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            try OrderStatisticBlurOps(T).midpointBlur(self, out, allocator, radius, border);
        }

        /// Applies an alpha-trimmed mean blur, discarding a fraction of the lowest and highest pixels.
        /// `trim_fraction` must be in [0, 0.5).
        ///
        /// This filter is useful when you want the smoothness of an average but need robustness to
        /// extremes (for example, sensor hot pixels or specular highlights). Trimming 10% from each
        /// tail delivers a strong denoise without smearing edges.
        ///
        /// ```zig
        /// var robust_mean = try Image(Rgba).initLike(allocator, color_image);
        /// defer robust_mean.deinit(allocator);
        /// try color_image.alphaTrimmedMeanBlur(robust_mean, allocator, 2, 0.1, .mirror);
        /// ```
        pub fn alphaTrimmedMeanBlur(
            self: Self,
            out: Self,
            allocator: Allocator,
            radius: usize,
            trim_fraction: f64,
            border: BorderMode,
        ) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            try OrderStatisticBlurOps(T).alphaTrimmedMeanBlur(self, out, allocator, radius, trim_fraction, border);
        }

        /// Computes a sharpened version of `self` by enhancing edges using the formula
        /// `sharpened = 2 * original - blurred`, where `blurred` is a box-blurred version
        /// of the original image. The `radius` parameter controls the size of the blur.
        /// The output image must be pre-allocated with the same dimensions as the input.
        pub fn sharpen(self: Self, out: Self, allocator: Allocator, radius: usize) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            if (radius == 0) {
                self.copy(out);
                return;
            }

            try box_blur.sharpen(T, self, out, allocator, radius);
        }

        /// Stretches the intensity range so the darkest/brightest pixels map to 0/255, modifying
        /// the image in place. `cutoff` is the fraction of pixels [0, 0.5] to ignore from each
        /// end of the histogram (helps reject outliers).
        pub fn autocontrast(self: Self, cutoff: f32) !void {
            return Enhancement(T).autocontrast(self, cutoff);
        }

        /// Equalizes the histogram of an image to improve contrast.
        ///
        /// This function redistributes pixel intensities to achieve a more uniform histogram,
        /// which typically enhances contrast in images with poor contrast or uneven lighting.
        /// The technique maps the cumulative distribution function (CDF) of pixel values to
        /// create a more even spread of intensities across the full range.
        ///
        /// For color images (RGB/RGBA), each channel is equalized independently.
        ///
        /// Example usage:
        /// ```zig
        /// var img = try Image(u8).load(io, allocator, "low_contrast.png");
        /// var equalized = try img.equalize(allocator);
        /// defer equalized.deinit(allocator);
        /// ```
        /// Equalizes the histogram to improve contrast. Modifies in-place.
        pub fn equalize(self: Self) void {
            return Enhancement(T).equalize(self);
        }

        /// Fills a contiguous region of pixels starting from `start_row` and `start_col`
        /// that have a similar color/intensity (within `options.threshold` distance) to either
        /// the seed pixel or the parent pixel, replacing them with `fill_value`.
        pub fn floodFill(
            self: Self,
            allocator: Allocator,
            start_row: u32,
            start_col: u32,
            fill_value: T,
            options: FloodFillOptions,
        ) !void {
            return @import("image/flood_fill.zig").floodFill(T, self, allocator, start_row, start_col, fill_value, options);
        }

        /// Computes Otsu's threshold and produces a binary image.
        /// Returns the threshold value that maximizes between-class variance.
        /// The output image must be pre-allocated with the same dimensions as the input.
        pub fn thresholdOtsu(self: Self, out: Image(u8), allocator: Allocator) !u8 {
            if (comptime T != u8) {
                @compileError("thresholdOtsu is only available for Image(u8)");
            }
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            return binary.Binary.thresholdOtsu(self, out, allocator);
        }

        /// Applies adaptive mean thresholding using a square window defined by `radius`.
        /// Each pixel is compared against the mean of its local neighborhood minus `c`.
        /// The output image must be pre-allocated with the same dimensions as the input.
        pub fn thresholdAdaptiveMean(self: Self, out: Image(u8), allocator: Allocator, radius: usize, c: f32) !void {
            if (comptime T != u8) {
                @compileError("thresholdAdaptiveMean is only available for Image(u8)");
            }
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            return binary.Binary.thresholdAdaptiveMean(self, out, allocator, radius, c);
        }

        /// Performs binary dilation using the provided structuring element.
        /// The output image must be pre-allocated with the same dimensions as the input.
        pub fn dilateBinary(self: Self, out: Image(u8), allocator: Allocator, kernel: BinaryKernel, iterations: usize) !void {
            if (comptime T != u8) {
                @compileError("dilateBinary is only available for Image(u8)");
            }
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            try binary.Binary.dilate(self, out, allocator, kernel, iterations);
        }

        /// Performs binary erosion using the provided structuring element.
        /// The output image must be pre-allocated with the same dimensions as the input.
        pub fn erodeBinary(self: Self, out: Image(u8), allocator: Allocator, kernel: BinaryKernel, iterations: usize) !void {
            if (comptime T != u8) {
                @compileError("erodeBinary is only available for Image(u8)");
            }
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            try binary.Binary.erode(self, out, allocator, kernel, iterations);
        }

        /// Performs a binary opening (erosion followed by dilation).
        /// The output image must be pre-allocated with the same dimensions as the input.
        pub fn openBinary(self: Self, out: Image(u8), allocator: Allocator, kernel: BinaryKernel, iterations: usize) !void {
            if (comptime T != u8) {
                @compileError("openBinary is only available for Image(u8)");
            }
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            try binary.Binary.open(self, out, allocator, kernel, iterations);
        }

        /// Performs a binary closing (dilation followed by erosion).
        /// The output image must be pre-allocated with the same dimensions as the input.
        pub fn closeBinary(self: Self, out: Image(u8), allocator: Allocator, kernel: BinaryKernel, iterations: usize) !void {
            if (comptime T != u8) {
                @compileError("closeBinary is only available for Image(u8)");
            }
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            try binary.Binary.close(self, out, allocator, kernel, iterations);
        }

        /// Applies a 2D convolution with the given kernel to the image.
        pub fn convolve(
            self: Self,
            /// The output image (must be pre-allocated with same dimensions).
            out: Self,
            allocator: Allocator,
            /// A 2D array representing the convolution kernel.
            kernel: anytype,
            /// How to handle pixels at the image borders.
            border: BorderMode,
        ) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            return convolution.convolve(T, self, out, allocator, kernel, border);
        }

        /// Performs separable convolution using two 1D kernels (horizontal and vertical).
        /// This is much more efficient for separable filters like Gaussian blur.
        pub fn convolveSeparable(
            self: Self,
            /// The output image (must be pre-allocated with same dimensions).
            out: Self,
            allocator: Allocator,
            /// Horizontal (column) kernel.
            kernel_x: []const f32,
            /// Vertical (row) kernel.
            kernel_y: []const f32,
            /// How to handle image borders.
            border: BorderMode,
        ) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            return convolution.convolveSeparable(T, self, out, allocator, kernel_x, kernel_y, border);
        }

        /// Applies Gaussian blur to the image using separable convolution.
        pub fn gaussianBlur(
            self: Self,
            /// The output blurred image (must be pre-allocated with same dimensions).
            out: Self,
            allocator: Allocator,
            /// Standard deviation of the Gaussian kernel.
            sigma: f32,
        ) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            // sigma == 0 means no blur; just copy input to output
            if (sigma == 0) {
                self.copy(out);
                return;
            }
            if (sigma < 0) return error.InvalidSigma;

            // Calculate kernel size (3 sigma on each side)
            const radius: usize = @ceil(3.0 * sigma);
            const kernel_size = 2 * radius + 1;

            // Generate 1D Gaussian kernel
            var kernel = try allocator.alloc(f32, kernel_size);
            defer allocator.free(kernel);

            var sum: f32 = 0;
            for (0..kernel_size) |i| {
                const x = @as(f32, @floatFromInt(i)) - @as(f32, @floatFromInt(radius));
                kernel[i] = @exp(-(x * x) / (2.0 * sigma * sigma));
                sum += kernel[i];
            }

            // Normalize kernel
            for (kernel) |*k| {
                k.* /= sum;
            }

            // Apply separable convolution
            try convolution.convolveSeparable(T, self, out, allocator, kernel, kernel, .mirror);
        }

        /// Applies the Sobel filter to `self` to perform edge detection.
        /// The output is a grayscale image representing the magnitude of gradients at each pixel.
        /// The output image must be pre-allocated with the same dimensions as the input.
        pub fn sobel(
            self: Self,
            /// Output image that will be filled with the Sobel magnitude image.
            out: Image(u8),
            allocator: Allocator,
        ) !void {
            if (self.rows != out.rows or self.cols != out.cols) {
                return error.DimensionMismatch;
            }
            return Edges(T).sobel(self, out, allocator);
        }

        /// Applies the Shen-Castan edge detection algorithm using the Infinite Symmetric
        /// Exponential Filter (ISEF). This algorithm provides superior edge localization
        /// and noise handling compared to traditional methods.
        /// The output image must be pre-allocated with the same dimensions as the input.
        pub fn shenCastan(
            self: Self,
            /// Output edge map as binary image (0 or 255).
            out: Image(u8),
            allocator: Allocator,
            /// Shen-Castan options (smoothing, thresholds, thinning, hysteresis).
            opts: ShenCastan,
        ) !void {
            if (self.rows != out.rows or self.cols != out.cols) {
                return error.DimensionMismatch;
            }
            return Edges(T).shenCastan(self, out, allocator, opts);
        }

        /// Applies the Canny edge detection algorithm, a classic multi-stage edge detector.
        /// This algorithm produces thin, well-localized edges with good noise suppression.
        ///
        /// The Canny algorithm consists of five main steps:
        /// 1. Gaussian smoothing to reduce noise
        /// 2. Gradient computation using Sobel operators
        /// 3. Non-maximum suppression to thin edges
        /// 4. Double thresholding to classify strong and weak edges
        /// 5. Edge tracking by hysteresis to link edges
        ///
        /// Note: `high_threshold` should be 2-3x larger than `low_threshold` for best results.
        ///
        /// Example:
        /// ```zig
        /// var edges = try Image(u8).initLike(allocator, image);
        /// defer edges.deinit(allocator);
        /// try image.canny(edges, allocator, 1.4, 50, 150);
        /// ```
        pub fn canny(
            self: Self,
            /// Output edge map as binary image (0 or 255).
            out: Image(u8),
            allocator: Allocator,
            /// Standard deviation for Gaussian blur (typical: 1.0-2.0).
            sigma: f32,
            /// Lower threshold for hysteresis (0-255).
            low_threshold: f32,
            /// Upper threshold for hysteresis (0-255).
            high_threshold: f32,
        ) !void {
            if (self.rows != out.rows or self.cols != out.cols) {
                return error.DimensionMismatch;
            }
            return Edges(T).canny(self, out, allocator, sigma, low_threshold, high_threshold);
        }

        /// Applies motion blur effect to the image.
        /// Supports linear motion blur (camera/object movement) and radial blur (zoom/spin effects).
        /// The output image must be pre-allocated with the same dimensions as the input.
        ///
        /// Example usage:
        /// ```zig
        /// var out = try Image(Rgb).initLike(allocator, image);
        /// defer out.deinit(allocator);
        ///
        /// // Linear motion blur
        /// try image.motionBlur(out, allocator, .{ .linear = .{ .angle = 0, .distance = 30 }});
        /// ```
        pub fn motionBlur(
            self: Self,
            /// Output image containing the motion blurred result.
            out: Self,
            allocator: Allocator,
            /// Type and parameters of motion blur to apply.
            motion: MotionBlur,
        ) !void {
            if (!self.hasSameShape(out)) {
                return error.DimensionMismatch;
            }
            switch (motion) {
                .linear => |params| try MotionBlurOps(T).linear(self, out, allocator, params.angle, params.distance),
                .radial_zoom => |params| try MotionBlurOps(T).radial(self, out, allocator, params.center_x, params.center_y, params.strength, .zoom),
                .radial_spin => |params| try MotionBlurOps(T).radial(self, out, allocator, params.center_x, params.center_y, params.strength, .spin),
            }
        }

        /// Calculates the Peak Signal-to-Noise Ratio (PSNR) between two images.
        /// PSNR is a measure of image fidelity, with higher values indicating better quality.
        /// Returns `inf` when images are identical (mean squared error = 0).
        ///
        /// Returns `error.DimensionMismatch` if the images have different dimensions.
        ///
        /// This wrapper is type-agnostic and works with any pixel type:
        /// - Scalars (u8, f32, etc.)
        /// - Structs (Rgb, Rgba, etc.)
        /// - Arrays ([3]u8, [4]f32, etc.)
        pub fn psnr(self: Self, other: Self) !f64 {
            return metrics.psnr(T, self, other);
        }

        /// Calculates the Structural Similarity Index (SSIM) between two images.
        /// SSIM is a perceptual metric that measures structural similarity, with values in [0, 1].
        /// 1.0 = identical images, 0.0 = completely different.
        ///
        /// This is more perceptually meaningful than PSNR for image quality assessment.
        /// Uses an 11x11 Gaussian window with σ=1.5, as recommended in the original paper.
        ///
        /// Reference: Wang et al., "Image Quality Assessment: From Error Visibility to Structural Similarity",
        /// IEEE Transactions on Image Processing, 2004.
        ///
        /// ## Implementation Notes:
        /// - For RGB/RGBA pixels: converts to luminance using Rec. 709 weights (ignores alpha)
        /// - For grayscale pixels: uses pixel value directly
        /// - For float pixels: assumes normalized [0, 1] range
        /// - Uses "valid" windowing: drops 5-pixel border (no padding/reflection)
        ///
        /// Returns an error if the images have different dimensions or are too small (< 11x11).
        pub fn ssim(self: Self, other: Self) !f64 {
            return metrics.ssim(T, self, other);
        }

        /// Options for computing image differences.
        pub const DiffOptions = diff_mod.DiffOptions;

        /// Result of a difference operation.
        pub const DiffResult = diff_mod.DiffResult;

        /// Computes the difference between `self` and `other` per pixel/channel.
        /// The result is stored in `out`, which must have the same dimensions.
        /// Applies scaling, thresholding, and visualization options in a single pass.
        pub fn diff(self: Self, out: Self, other: Self, opts: DiffOptions) !DiffResult {
            return diff_mod.compute(T, self, out, other, opts);
        }

        /// Computes the mean absolute pixel error normalized by the maximum channel value
        /// (e.g. 255 for `u8`). Requires both images to share the same dimensions.
        pub fn meanPixelError(self: Self, other: Self) !f64 {
            return metrics.meanPixelError(T, self, other);
        }

        pub fn pixels(self: Self) PixelIterator(T) {
            return .{
                .data = self.data,
                .cols = self.cols,
                .stride = self.stride,
                .rows = self.rows,
            };
        }

        /// Computes a histogram of the image pixel values.
        /// Supported types: u8, Rgb, Rgba
        /// Returns a Histogram struct with channel-specific bins.
        pub fn histogram(self: Self) Histogram(T) {
            var hist: Histogram(T) = .init();

            var iter = self.pixels();
            while (iter.next()) |pixel| {
                switch (T) {
                    u8 => {
                        hist.values[pixel.*] += 1;
                    },
                    Rgb => {
                        hist.r[pixel.r] += 1;
                        hist.g[pixel.g] += 1;
                        hist.b[pixel.b] += 1;
                    },
                    Rgba => {
                        hist.r[pixel.r] += 1;
                        hist.g[pixel.g] += 1;
                        hist.b[pixel.b] += 1;
                        hist.a[pixel.a] += 1;
                    },
                    else => @compileError("histogram() only supports u8, Rgb, and Rgba types"),
                }
            }
            return hist;
        }

        /// Applies a colormap to the image, converting it to an RGB image.
        /// If min_val or max_val are null in the map configuration, they are computed from the image content.
        /// Uses luminance for multi-channel input images.
        pub fn applyColormap(
            self: Self,
            allocator: Allocator,
            map: Colormap,
        ) !Image(Rgb) {
            const colormaps = @import("image/colormaps.zig");

            // Determine range
            var min_val: f64 = 0;
            var max_val: f64 = 0;

            const range_opts = switch (map) {
                inline else => |r| r,
            };

            if (range_opts.min == null or range_opts.max == null) {
                // Find min/max
                var min_v: f64 = std.math.floatMax(f64);
                var max_v: f64 = -std.math.floatMax(f64);

                // For empty image, use 0-1 range to avoid issues
                if (self.size() == 0) {
                    min_v = 0;
                    max_v = 1;
                } else {
                    for (0..self.rows) |r| {
                        for (0..self.cols) |c| {
                            const val = if (comptime meta.isScalar(T)) meta.as(f64, self.at(r, c).*) else convertColor(f64, self.at(r, c).*);
                            min_v = @min(min_v, val);
                            max_v = @max(max_v, val);
                        }
                    }
                }
                min_val = range_opts.min orelse min_v;
                max_val = range_opts.max orelse max_v;
            } else {
                min_val = range_opts.min.?;
                max_val = range_opts.max.?;
            }

            // Ensure max >= min to avoid division by zero
            if (max_val <= min_val) {
                max_val = min_val + 1.0;
            }

            var out: Image(Rgb) = try .init(allocator, self.rows, self.cols);

            for (0..self.rows) |r| {
                for (0..self.cols) |c| {
                    const val = if (comptime meta.isScalar(T)) meta.as(f64, self.at(r, c).*) else convertColor(f64, self.at(r, c).*);
                    const color = switch (map) {
                        inline else => |_, tag| @field(colormaps, @tagName(tag))(val, min_val, max_val),
                    };
                    out.at(r, c).* = color;
                }
            }
            return out;
        }
    };
}

// Run all tests
test {
    _ = @import("image/PixelIterator.zig");
    _ = @import("image/animated.zig");
    _ = @import("image/format.zig");
    _ = @import("image/display.zig");
    _ = @import("image/tests/integral.zig");
    _ = @import("image/tests/filters.zig");
    _ = @import("image/tests/transforms.zig");
    _ = @import("image/tests/display.zig");
    _ = @import("image/tests/interpolation.zig");
    _ = @import("image/tests/resize.zig");
    _ = @import("image/tests/psnr.zig");
    _ = @import("image/tests/shen_castan.zig");
    _ = @import("image/tests/binary.zig");
    _ = @import("image/hough.zig");
    _ = @import("image/colormaps.zig");
    _ = @import("image/border.zig");
    _ = @import("image/convolution.zig");
    _ = @import("image/order_statistic_blur.zig");
    _ = @import("image/tests/flood_fill.zig");
}
