File size: 7,364 Bytes
80c945e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | // 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..<kh {
for kx in 0..<kw {
for dy in 0..<K {
for dx in 0..<K {
let x = kx * K + dx, y = ky * K + dy
posX[idx] = Int32(x)
posY[idx] = Int32(y)
mask[idx] = 1
let base = idx * D
var o = 0
for py in 0..<P {
let row = sp.baseAddress! + ((y * P + py) * W + x * P) * 3
for e in 0..<(P * 3) {
pp[base + o] = Float16(row[e]) // 0...255, exact
o += 1
}
}
idx += 1
}
}
}
}
}
}
let (cos, sin) = ropeTables(posX: posX, posY: posY)
return VisionInputs(patches: patches, posX: posX, posY: posY, cos: cos, sin: sin, patchMask: mask,
softTokenCount: n, gridRows: ph, gridCols: pw, resizedHeight: H, resizedWidth: W)
}
/// Axial RoPE tables [N, 64]: channels 0..31 rotate by x (16 frequencies, duplicated), 32..63 by y.
/// float32 maths like upstream, cast to fp16.
public static func ropeTables(posX: [Int32], posY: [Int32]) -> (cos: [Float16], sin: [Float16]) {
let F = VisionInputs.headDim / 4 // 16
var inv = [Float](repeating: 0, count: F)
for i in 0..<F { inv[i] = Float(1.0 / pow(VisionInputs.ropeTheta, Double(2 * i) / Double(2 * F))) }
let N = posX.count
var cos = [Float16](repeating: 0, count: N * 64)
var sin = [Float16](repeating: 0, count: N * 64)
for p in 0..<N {
let fx = Float(posX[p]), fy = Float(posY[p])
for i in 0..<F {
let ax = fx * inv[i], ay = fy * inv[i]
let cx = Float16(cosf(ax)), sx = Float16(sinf(ax)), cy = Float16(cosf(ay)), sy = Float16(sinf(ay))
let b = p * 64
cos[b + i] = cx; cos[b + F + i] = cx; cos[b + 2 * F + i] = cy; cos[b + 3 * F + i] = cy
sin[b + i] = sx; sin[b + F + i] = sx; sin[b + 2 * F + i] = sy; sin[b + 3 * F + i] = sy
}
}
return (cos, sin)
}
}
|