// Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../../../LICENSE). // // Ports the logic of the Gemma 4 image processor (image_processing_gemma4.py in Hugging Face transformers 5.19, // Copyright 2026 The HuggingFace Team, Apache-2.0); see ../../../NOTICE. // // Host-side preparation of the `vision_2520` inputs: target size (aspect-preserving, multiples of 48 px, at most // 2520 patches of 16x16), the exact resize, 16x16x3 patches of INTEGER pixel values 0..255 (exact in Float16; the // graph computes P - 127.5 with upstream's 2/255 scale folded into its first projection) in CELL-MAJOR order (the // 9 patches of each 3x3 pooling cell contiguous, cells row-major), (x, y) patch positions, the axial-RoPE cos/sin // tables and the patch mask. Mirrors python/eg2_image.py. import Foundation public struct VisionInputs: Sendable { public static let patch = 16 public static let pool = 3 public static let cellPx = 48 public static let maxSoftTokens = 280 public static let maxPatches = 2520 public static let patchDim = 768 // 16 * 16 * 3 public static let headDim = 64 public static let ropeTheta = 100.0 public var patches: [Float16] // [2520 * 768], pixel values 0..255, zero padded public var posX: [Int32] // [2520], 0 for padding public var posY: [Int32] // [2520] public var cos: [Float16] // [2520 * 64] public var sin: [Float16] // [2520 * 64] public var patchMask: [Float16] // [2520], 1 = patch, 0 = padding public var softTokenCount: Int // n: valid rows of the vision output public var gridRows: Int // patch rows public var gridCols: Int // patch cols public var resizedHeight: Int public var resizedWidth: Int } public enum ImagePreprocessor { /// Gemma4 `get_aspect_ratio_preserving_size` (height, width), verbatim logic. public static func targetSize(height: Int, width: Int, maxPatches: Int = VisionInputs.maxPatches) throws -> (height: Int, width: Int) { let patch = VisionInputs.patch, cell = VisionInputs.cellPx let targetPx = Double(maxPatches * patch * patch) let factor = (targetPx / Double(height * width)).squareRoot() var th = Int((factor * Double(height) / Double(cell)).rounded(.down)) * cell var tw = Int((factor * Double(width) / Double(cell)).rounded(.down)) * cell let maxSide = (maxPatches / (VisionInputs.pool * VisionInputs.pool)) * cell if th == 0 && tw == 0 { throw EmbedderError(description: "image would resize to 0x0") } if th == 0 { th = cell tw = min(Int((Double(width) / Double(height)).rounded(.down)) * cell, maxSide) } else if tw == 0 { tw = cell th = min(Int((Double(height) / Double(width)).rounded(.down)) * cell, maxSide) } if Double(th * tw) > targetPx { throw EmbedderError(description: "size exceeds the patch budget") } return (th, tw) } /// Resized 8-bit RGB (interleaved, [height][width][3]) of a decoded image, exactly as the upstream processor /// produces it (bicubic antialiased on uint8, width pass then height pass). public static func resized(_ image: DecodedImage) throws -> DecodedImage { let (th, tw) = try targetSize(height: image.height, width: image.width) let px = ImageResampler.resize(image.rgb, width: image.width, height: image.height, channels: 3, toWidth: tw, toHeight: th) return DecodedImage(width: tw, height: th, rgb: px) } /// Decode (no EXIF orientation unless asked, alpha dropped, no colour management) + resize + patches. public static func prepare(url: URL, applyEXIFOrientation: Bool = false) throws -> VisionInputs { try prepare(try ImageDecoder.decodeRGB(url: url, applyEXIFOrientation: applyEXIFOrientation)) } public static func prepare(_ decoded: DecodedImage) throws -> VisionInputs { let r = try resized(decoded) return patchify(resizedRGB: r.rgb, width: r.width, height: r.height) } /// `rgb`: [height][width][3] uint8 with height, width multiples of 48. public static func patchify(resizedRGB rgb: [UInt8], width W: Int, height H: Int) -> VisionInputs { let P = VisionInputs.patch, K = VisionInputs.pool, N = VisionInputs.maxPatches, D = VisionInputs.patchDim let ph = H / P, pw = W / P let kh = ph / K, kw = pw / K let n = kh * kw var patches = [Float16](repeating: 0, count: N * D) var posX = [Int32](repeating: 0, count: N) var posY = [Int32](repeating: 0, count: N) var mask = [Float16](repeating: 0, count: N) var idx = 0 rgb.withUnsafeBufferPointer { sp in patches.withUnsafeMutableBufferPointer { pp in for ky in 0.. (cos: [Float16], sin: [Float16]) { let F = VisionInputs.headDim / 4 // 16 var inv = [Float](repeating: 0, count: F) for i in 0..