init
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "hcie-vision"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["ffi", "cpp-backend"]
|
||||
ffi = []
|
||||
# Enables the C++ GGML/llama.cpp vision backend (MobileSAM, BiRefNet, ESRGAN, MI-GAN).
|
||||
# Requires the matching C++ backend library to be available at load time.
|
||||
cpp-backend = []
|
||||
|
||||
[dependencies]
|
||||
hcie-engine-api = { path = "../hcie-engine-api" }
|
||||
hcie-protocol = { path = "../hcie-protocol" }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
|
||||
reqwest = { version = "0.12", features = ["json", "multipart", "blocking", "stream"] }
|
||||
tokio = { version = "1.40", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
base64 = "0.22"
|
||||
log = "0.4"
|
||||
rand = "0.8"
|
||||
rayon = "1.10"
|
||||
bincode = "1.3"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib", "cdylib"]
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.23"
|
||||
proptest = "1.5"
|
||||
approx = "0.5"
|
||||
@@ -0,0 +1,38 @@
|
||||
//! Build script linking the C++ vision backend into the Rust cdylib.
|
||||
fn main() {
|
||||
let project_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let root_dir = std::path::Path::new(&project_dir).parent().unwrap();
|
||||
let lib_dir = root_dir.join("build").join("lib");
|
||||
|
||||
println!("cargo:rustc-link-search=native={}", lib_dir.display());
|
||||
println!("cargo:rustc-link-lib=dylib=visioncpp");
|
||||
println!("cargo:rustc-link-lib=dylib=ggml");
|
||||
println!("cargo:rustc-link-lib=dylib=ggml-cpu");
|
||||
println!("cargo:rustc-link-lib=dylib=ggml-base");
|
||||
|
||||
// Look in the executable/plugin directory first, then in build/lib.
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir.display());
|
||||
|
||||
// Copy runtime dependencies next to the built plugin so both the
|
||||
// cargo target directory and the plugins/ staging directory work without
|
||||
// extra LD_LIBRARY_PATH setup.
|
||||
let target_dir = std::path::Path::new(&project_dir)
|
||||
.join("..").join("..").join("target")
|
||||
.join(std::env::var("PROFILE").unwrap());
|
||||
if target_dir.exists() {
|
||||
let libs = [
|
||||
"libvisioncpp.so", "libvisioncpp.so.0", "libvisioncpp.so.0.3.1",
|
||||
"libggml.so", "libggml.so.0", "libggml.so.0.9.9",
|
||||
"libggml-cpu.so", "libggml-cpu.so.0", "libggml-cpu.so.0.9.9",
|
||||
"libggml-base.so", "libggml-base.so.0", "libggml-base.so.0.9.9",
|
||||
];
|
||||
for lib in &libs {
|
||||
let src = lib_dir.join(lib);
|
||||
let dst = target_dir.join(lib);
|
||||
if src.exists() {
|
||||
std::fs::copy(&src, &dst).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,296 @@
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use crate::inpaint::{criminisi_inpaint_with_radius, build_polygon_mask, build_stroke_mask};
|
||||
use crate::vision::{run_background_removal_task, run_object_segment_task, run_super_resolution_task};
|
||||
|
||||
// ── C++ backend FFI declarations (brought over from v3) ─────────────────────
|
||||
// These are only compiled when the `cpp-backend` feature is enabled. They
|
||||
// mirror the `vision.cpp` C ABI used by MobileSAM / BiRefNet / ESRGAN /
|
||||
// MI-GAN via GGML/LLama.cpp. The Rust cdylib does not need to define these
|
||||
// symbols itself; they are resolved at load time against the matching C++
|
||||
// backend library, or left unresolved when `cpp-backend` is disabled.
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
use std::os::raw::{c_void};
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct VispImage {
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub stride: i32,
|
||||
pub format: i32,
|
||||
pub data: *mut c_void,
|
||||
}
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct VispImageView {
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
pub stride: i32,
|
||||
pub format: i32,
|
||||
pub data: *const c_void,
|
||||
}
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
#[repr(C)]
|
||||
pub struct BackendDevice(c_void);
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
#[repr(C)]
|
||||
pub struct AnyModel(c_void);
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
#[repr(C)]
|
||||
pub struct ImageData(c_void);
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ModelFamily {
|
||||
Sam = 0,
|
||||
Birefnet = 1,
|
||||
DepthAnything = 2,
|
||||
Migan = 3,
|
||||
Esrgan = 4,
|
||||
}
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
#[repr(i32)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum ImageFormat {
|
||||
RgbaU8 = 0,
|
||||
BgraU8 = 1,
|
||||
ArgbU8 = 2,
|
||||
RgbU8 = 3,
|
||||
AlphaU8 = 4,
|
||||
RgbaF32 = 5,
|
||||
RgbF32 = 6,
|
||||
AlphaF32 = 7,
|
||||
}
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
extern "C" {
|
||||
pub fn visp_get_last_error() -> *const c_char;
|
||||
pub fn visp_image_destroy(img: *mut ImageData);
|
||||
pub fn visp_device_init(device_type: i32, out_device: *mut *mut BackendDevice) -> i32;
|
||||
pub fn visp_device_destroy(device: *mut BackendDevice);
|
||||
pub fn visp_model_load(filepath: *const c_char, dev: *const BackendDevice, arch: i32, out: *mut *mut AnyModel) -> i32;
|
||||
pub fn visp_model_destroy(model: *mut AnyModel, arch: i32);
|
||||
pub fn visp_model_compute(
|
||||
model: *mut AnyModel,
|
||||
family: i32,
|
||||
inputs: *const VispImageView,
|
||||
n_inputs: i32,
|
||||
args: *const i32,
|
||||
n_args: i32,
|
||||
out_image: *mut VispImage,
|
||||
out_data: *mut *mut ImageData,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
// ── Rust-to-C exports used by hcie-engine-api via dynamic loading ───────────
|
||||
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
/// Global vision backend preference (0 = CPU, 1 = GPU). When the C++ backend
|
||||
/// feature is enabled this is forwarded to `visp_device_init` as the device
|
||||
/// type. When disabled it is still logged so operators can confirm which
|
||||
/// backend would have been used.
|
||||
static VISION_BACKEND: AtomicU8 = AtomicU8::new(0);
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_vision_set_backend_c(backend: u8) {
|
||||
VISION_BACKEND.store(backend, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Returns the currently configured backend code (0 = CPU, 1 = GPU).
|
||||
pub(crate) fn vision_backend_code() -> u8 {
|
||||
VISION_BACKEND.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn return_boxed_bytes(bytes: Vec<u8>, out_ptr: *mut *mut u8, out_len: *mut usize) {
|
||||
unsafe {
|
||||
let boxed = bytes.into_boxed_slice();
|
||||
let len = boxed.len();
|
||||
let raw = Box::into_raw(boxed) as *mut u8;
|
||||
*out_ptr = raw;
|
||||
*out_len = len;
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn free_buffer_c(ptr: *mut u8, len: usize) {
|
||||
if !ptr.is_null() {
|
||||
let _ = Box::from_raw(std::slice::from_raw_parts_mut(ptr, len) as *mut [u8]);
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_vision_background_removal_c(
|
||||
img_pixels: *const u8,
|
||||
img_len: usize,
|
||||
width: u32,
|
||||
height: u32,
|
||||
out_buffer: *mut *mut u8,
|
||||
out_len: *mut usize,
|
||||
) -> bool {
|
||||
if img_pixels.is_null() || out_buffer.is_null() || out_len.is_null() || img_len == 0 {
|
||||
return false;
|
||||
}
|
||||
let img_bytes = std::slice::from_raw_parts(img_pixels, img_len).to_vec();
|
||||
match run_background_removal_task("", &img_bytes, width, height) {
|
||||
Ok(result) => {
|
||||
return_boxed_bytes(result, out_buffer, out_len);
|
||||
true
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_vision_object_segment_c(
|
||||
img_pixels: *const u8,
|
||||
img_len: usize,
|
||||
width: u32,
|
||||
height: u32,
|
||||
point_x: u32,
|
||||
point_y: u32,
|
||||
bbox_min_x: u32,
|
||||
bbox_min_y: u32,
|
||||
bbox_max_x: u32,
|
||||
bbox_max_y: u32,
|
||||
model_path_ptr: *const std::os::raw::c_char,
|
||||
out_buffer: *mut *mut u8,
|
||||
out_len: *mut usize,
|
||||
) -> bool {
|
||||
if img_pixels.is_null() || out_buffer.is_null() || out_len.is_null() || img_len == 0 {
|
||||
return false;
|
||||
}
|
||||
let img_bytes = std::slice::from_raw_parts(img_pixels, img_len).to_vec();
|
||||
// u32::MAX is the sentinel for "not provided"
|
||||
let has_bbox = !(bbox_min_x == u32::MAX && bbox_min_y == u32::MAX && bbox_max_x == u32::MAX && bbox_max_y == u32::MAX);
|
||||
let has_point = !(point_x == u32::MAX && point_y == u32::MAX);
|
||||
let bbox = if has_bbox { Some((bbox_min_x, bbox_min_y, bbox_max_x, bbox_max_y)) } else { None };
|
||||
let point = if has_point { Some((point_x, point_y)) } else { None };
|
||||
let model_path = if model_path_ptr.is_null() {
|
||||
String::new()
|
||||
} else {
|
||||
CStr::from_ptr(model_path_ptr).to_string_lossy().into_owned()
|
||||
};
|
||||
match run_object_segment_task(&model_path, &img_bytes, width, height, point, bbox) {
|
||||
Ok(result) => {
|
||||
return_boxed_bytes(result, out_buffer, out_len);
|
||||
true
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_vision_super_resolution_c(
|
||||
img_pixels: *const u8,
|
||||
img_len: usize,
|
||||
width: u32,
|
||||
height: u32,
|
||||
out_buffer: *mut *mut u8,
|
||||
out_len: *mut usize,
|
||||
) -> bool {
|
||||
if img_pixels.is_null() || out_buffer.is_null() || out_len.is_null() || img_len == 0 {
|
||||
return false;
|
||||
}
|
||||
let img_bytes = std::slice::from_raw_parts(img_pixels, img_len).to_vec();
|
||||
match run_super_resolution_task("", &img_bytes, width, height) {
|
||||
Ok(result) => {
|
||||
return_boxed_bytes(result, out_buffer, out_len);
|
||||
true
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_vision_inpaint_c(
|
||||
pixels: *const u8,
|
||||
pixels_len: usize,
|
||||
width: u32,
|
||||
height: u32,
|
||||
hole_mask: *const u8,
|
||||
hole_len: usize,
|
||||
radius: i32,
|
||||
) -> bool {
|
||||
if pixels.is_null() || hole_mask.is_null() || pixels_len == 0 || hole_len == 0 {
|
||||
return false;
|
||||
}
|
||||
let px_slice = std::slice::from_raw_parts_mut(pixels as *mut u8, pixels_len);
|
||||
let hole_slice = std::slice::from_raw_parts(hole_mask, hole_len);
|
||||
let hole_bool: Vec<bool> = hole_slice.iter().map(|&b| b != 0).collect();
|
||||
|
||||
let mut layer = hcie_protocol::Layer::from_rgba("inpaint", width, height, px_slice.to_vec());
|
||||
criminisi_inpaint_with_radius(&mut layer, &hole_bool, radius);
|
||||
px_slice.copy_from_slice(&layer.pixels);
|
||||
true
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_vision_build_polygon_mask_c(
|
||||
points_json: *const c_char,
|
||||
width: u32,
|
||||
height: u32,
|
||||
out_buffer: *mut *mut u8,
|
||||
out_len: *mut usize,
|
||||
) -> bool {
|
||||
if points_json.is_null() || out_buffer.is_null() || out_len.is_null() {
|
||||
return false;
|
||||
}
|
||||
let json_str = match CStr::from_ptr(points_json).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let points: Vec<[u32; 2]> = match serde_json::from_str(json_str) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let pts: Vec<(u32, u32)> = points.iter().map(|p| (p[0], p[1])).collect();
|
||||
let mask = build_polygon_mask(&pts, width, height);
|
||||
let bytes: Vec<u8> = mask.iter().map(|&b| if b { 1u8 } else { 0u8 }).collect();
|
||||
return_boxed_bytes(bytes, out_buffer, out_len);
|
||||
true
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn hcie_vision_build_stroke_mask_c(
|
||||
points_json: *const c_char,
|
||||
selection_ptr: *const u8,
|
||||
selection_len: usize,
|
||||
radius: f32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
out_buffer: *mut *mut u8,
|
||||
out_len: *mut usize,
|
||||
) -> bool {
|
||||
if points_json.is_null() || out_buffer.is_null() || out_len.is_null() {
|
||||
return false;
|
||||
}
|
||||
let json_str = match CStr::from_ptr(points_json).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let points: Vec<[u32; 2]> = match serde_json::from_str(json_str) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let pts: Vec<(u32, u32)> = points.iter().map(|p| (p[0], p[1])).collect();
|
||||
let sel = if selection_ptr.is_null() || selection_len == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(std::slice::from_raw_parts(selection_ptr, selection_len))
|
||||
};
|
||||
let mask = build_stroke_mask(&pts, sel, radius, width, height);
|
||||
let bytes: Vec<u8> = mask.iter().map(|&b| if b { 1u8 } else { 0u8 }).collect();
|
||||
return_boxed_bytes(bytes, out_buffer, out_len);
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,747 @@
|
||||
use hcie_protocol::Layer;
|
||||
|
||||
const ALPHA: f32 = 255.0;
|
||||
const CANDIDATES: usize = 1000; // Optimized for speed/quality balance
|
||||
const MAX_TRIES: usize = CANDIDATES * 10;
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// Fill every pixel where `hole[y*w+x] == true` in `layer`
|
||||
/// using Criminisi exemplar-based inpainting with default patch radius (4).
|
||||
#[allow(dead_code)]
|
||||
pub fn criminisi_inpaint(layer: &mut Layer, hole: &[bool]) {
|
||||
criminisi_inpaint_with_radius(layer, hole, 2);
|
||||
}
|
||||
|
||||
/// Fill every pixel where `hole[y*w+x] == true` in `layer`
|
||||
/// using Criminisi exemplar-based inpainting with custom patch radius.
|
||||
pub fn criminisi_inpaint_with_radius(layer: &mut Layer, hole: &[bool], radius: i32) {
|
||||
let w = layer.width as i32;
|
||||
let h = layer.height as i32;
|
||||
let n = (w * h) as usize;
|
||||
if hole.len() != n || n == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety check: Is the layer empty?
|
||||
if layer.pixels.iter().all(|&p| p == 0) {
|
||||
log::warn!("[inpaint] Layer is entirely empty/transparent. Inpainting will have no source texture.");
|
||||
}
|
||||
|
||||
let mut pix = layer.pixels.clone();
|
||||
let mut mask = hole.to_vec();
|
||||
let mut conf = vec![1.0_f32; n];
|
||||
for i in 0..n {
|
||||
if mask[i] {
|
||||
conf[i] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
let patch_sz = ((2 * radius + 1) * (2 * radius + 1)) as f32;
|
||||
|
||||
// STEP 1: compute hole bounding box once — front stays inside it forever
|
||||
let (bx0, by0, bx1, by1) = {
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0i32, 0i32);
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
if mask[(y * w + x) as usize] {
|
||||
x0 = x0.min(x);
|
||||
y0 = y0.min(y);
|
||||
x1 = x1.max(x);
|
||||
y1 = y1.max(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
if x0 > x1 {
|
||||
return;
|
||||
}
|
||||
(x0, y0, x1, y1)
|
||||
};
|
||||
|
||||
let mut rng = Lcg::default();
|
||||
|
||||
// STEP 2: main fill loop
|
||||
loop {
|
||||
let front = build_front(&mask, w, h, bx0, by0, bx1, by1);
|
||||
if front.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// STEP 3: find highest-priority patch centre on the front
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let best_patch = {
|
||||
use rayon::prelude::*;
|
||||
front.par_iter().map(|&(x, y)| {
|
||||
let pr = conf_term(x, y, &mask, &conf, w, h, radius, patch_sz)
|
||||
* data_term(x, y, &pix, &mask, w, h).max(1e-3);
|
||||
(x, y, pr)
|
||||
}).max_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let best_patch = {
|
||||
front.iter().map(|&(x, y)| {
|
||||
let pr = conf_term(x, y, &mask, &conf, w, h, radius, patch_sz)
|
||||
* data_term(x, y, &pix, &mask, w, h).max(1e-3);
|
||||
(x, y, pr)
|
||||
}).max_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
|
||||
};
|
||||
|
||||
let (bx, by) = if let Some((x, y, _)) = best_patch {
|
||||
(x, y)
|
||||
} else {
|
||||
front[0]
|
||||
};
|
||||
let c_val = conf_term(bx, by, &mask, &conf, w, h, radius, patch_sz);
|
||||
|
||||
// STEP 4: find best matching known patch
|
||||
match find_source(bx, by, &pix, &mask, w, h, &mut rng, radius) {
|
||||
Some((sx, sy)) => {
|
||||
// STEP 5: copy hole pixels from source patch, update confidence
|
||||
for dy in -radius..=radius {
|
||||
for dx in -radius..=radius {
|
||||
let tx = bx + dx;
|
||||
let ty = by + dy;
|
||||
if tx < 0 || ty < 0 || tx >= w || ty >= h {
|
||||
continue;
|
||||
}
|
||||
let ti = (ty * w + tx) as usize;
|
||||
if !mask[ti] {
|
||||
continue;
|
||||
}
|
||||
let sx2 = (sx + dx).clamp(0, w - 1);
|
||||
let sy2 = (sy + dy).clamp(0, h - 1);
|
||||
let si = (sy2 * w + sx2) as usize;
|
||||
pix[ti * 4] = pix[si * 4];
|
||||
pix[ti * 4 + 1] = pix[si * 4 + 1];
|
||||
pix[ti * 4 + 2] = pix[si * 4 + 2];
|
||||
pix[ti * 4 + 3] = pix[si * 4 + 3];
|
||||
conf[ti] = c_val;
|
||||
mask[ti] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// No valid source — fill with average boundary color to prevent transparency holes
|
||||
let c_avg = boundary_avg(bx, by, &pix, &mask, w, h, radius);
|
||||
for dy in -radius..=radius { for dx in -radius..=radius {
|
||||
let tx = bx + dx; let ty = by + dy;
|
||||
if tx < 0 || ty < 0 || tx >= w || ty >= h { continue; }
|
||||
let ti = (ty * w + tx) as usize;
|
||||
if !mask[ti] { continue; }
|
||||
pix[ti*4] = c_avg[0];
|
||||
pix[ti*4+1] = c_avg[1];
|
||||
pix[ti*4+2] = c_avg[2];
|
||||
pix[ti*4+3] = 255;
|
||||
mask[ti] = false;
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 6: Poisson Refinement Pass (Task #071)
|
||||
// Subtler refinement to avoid "blob" look.
|
||||
log::warn!("[inpaint] Starting subtle Poisson refinement...");
|
||||
poisson_refine(&mut pix, hole, w, h, 5); // Fewer iterations
|
||||
|
||||
layer.pixels = pix;
|
||||
layer.dirty = true;
|
||||
log::warn!("[inpaint] Finished successfully.");
|
||||
}
|
||||
|
||||
/// [CRITICAL_LOGIC_START]
|
||||
/// Poisson Refinement Pass: Fixes color seams and artifacts in filled regions.
|
||||
fn poisson_refine(pix: &mut [u8], hole: &[bool], w: i32, h: i32, iterations: usize) {
|
||||
let n = (w * h) as usize;
|
||||
let mut r = vec![0.0f32; n];
|
||||
let mut g = vec![0.0f32; n];
|
||||
let mut b = vec![0.0f32; n];
|
||||
let mut guidance_r = vec![0.0f32; n];
|
||||
let mut guidance_g = vec![0.0f32; n];
|
||||
let mut guidance_b = vec![0.0f32; n];
|
||||
|
||||
// 1. Initialize and compute guidance (Laplacian of the inpainted result)
|
||||
for i in 0..n {
|
||||
r[i] = pix[i * 4] as f32;
|
||||
g[i] = pix[i * 4 + 1] as f32;
|
||||
b[i] = pix[i * 4 + 2] as f32;
|
||||
}
|
||||
|
||||
for y in 1..h - 1 {
|
||||
for x in 1..w - 1 {
|
||||
let i = (y * w + x) as usize;
|
||||
if hole[i] {
|
||||
// Laplacian guidance field from inpainted result
|
||||
let up = i - w as usize; let down = i + w as usize;
|
||||
let left = i - 1; let right = i + 1;
|
||||
guidance_r[i] = 4.0 * r[i] - (r[up] + r[down] + r[left] + r[right]);
|
||||
guidance_g[i] = 4.0 * g[i] - (g[up] + g[down] + g[left] + g[right]);
|
||||
guidance_b[i] = 4.0 * b[i] - (b[up] + b[down] + b[left] + b[right]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Iterative Gauss-Seidel Solver (Poisson Smoothing)
|
||||
for _ in 0..iterations {
|
||||
for y in 1..h - 1 {
|
||||
for x in 1..w - 1 {
|
||||
let i = (y * w + x) as usize;
|
||||
if hole[i] {
|
||||
let up = i - w as usize; let down = i + w as usize;
|
||||
let left = i - 1; let right = i + 1;
|
||||
|
||||
// Relax color values while keeping boundary conditions
|
||||
// Dirichlet boundaries are the pixels outside the 'hole'
|
||||
r[i] = (r[up] + r[down] + r[left] + r[right] + guidance_r[i]) / 4.0;
|
||||
g[i] = (g[up] + g[down] + g[left] + g[right] + guidance_g[i]) / 4.0;
|
||||
b[i] = (b[up] + b[down] + b[left] + b[right] + guidance_b[i]) / 4.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Write back with subtle blend (Task #071)
|
||||
for i in 0..n {
|
||||
if hole[i] {
|
||||
let pr = pix[i * 4] as f32;
|
||||
let pg = pix[i * 4 + 1] as f32;
|
||||
let pb = pix[i * 4 + 2] as f32;
|
||||
|
||||
// 15% refine strength (Subtle to prevent "blob" look)
|
||||
let strength = 0.15;
|
||||
pix[i * 4] = (pr * (1.0 - strength) + r[i] * strength).clamp(0.0, 255.0) as u8;
|
||||
pix[i * 4 + 1] = (pg * (1.0 - strength) + g[i] * strength).clamp(0.0, 255.0) as u8;
|
||||
pix[i * 4 + 2] = (pb * (1.0 - strength) + b[i] * strength).clamp(0.0, 255.0) as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
// [CRITICAL_LOGIC_END]
|
||||
|
||||
// ── Fill-front ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn build_front(
|
||||
mask: &[bool],
|
||||
w: i32,
|
||||
h: i32,
|
||||
x0: i32,
|
||||
y0: i32,
|
||||
x1: i32,
|
||||
y1: i32,
|
||||
) -> Vec<(i32, i32)> {
|
||||
let mut front = Vec::new();
|
||||
for y in y0.max(0)..=y1.min(h - 1) {
|
||||
for x in x0.max(0)..=x1.min(w - 1) {
|
||||
if !mask[(y * w + x) as usize] {
|
||||
continue;
|
||||
}
|
||||
let on_front = [(-1i32, 0i32), (1, 0), (0, -1), (0, 1)]
|
||||
.iter()
|
||||
.any(|&(dx, dy)| {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
nx >= 0 && ny >= 0 && nx < w && ny < h && !mask[(ny * w + nx) as usize]
|
||||
});
|
||||
if on_front {
|
||||
front.push((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
front
|
||||
}
|
||||
|
||||
// ── Priority terms ───────────────────────────────────────────────────────────
|
||||
|
||||
/// C(p) = Σ conf[known pixels in Ψ_p] / |Ψ_p|
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn conf_term(
|
||||
bx: i32,
|
||||
by: i32,
|
||||
mask: &[bool],
|
||||
conf: &[f32],
|
||||
w: i32,
|
||||
h: i32,
|
||||
radius: i32,
|
||||
patch_sz: f32,
|
||||
) -> f32 {
|
||||
let mut sum = 0.0_f32;
|
||||
for dy in -radius..=radius {
|
||||
for dx in -radius..=radius {
|
||||
let x = bx + dx;
|
||||
let y = by + dy;
|
||||
if x < 0 || y < 0 || x >= w || y >= h {
|
||||
continue;
|
||||
}
|
||||
let i = (y * w + x) as usize;
|
||||
if !mask[i] {
|
||||
sum += conf[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
sum / patch_sz
|
||||
}
|
||||
|
||||
/// D(p) = |∇I⊥_p · n_p| / α — isophote strength along fill-front normal.
|
||||
fn data_term(bx: i32, by: i32, pix: &[u8], mask: &[bool], w: i32, h: i32) -> f32 {
|
||||
let lum = |x: i32, y: i32| -> Option<f32> {
|
||||
if x < 0 || y < 0 || x >= w || y >= h {
|
||||
return None;
|
||||
}
|
||||
let i = (y * w + x) as usize;
|
||||
if mask[i] {
|
||||
return None;
|
||||
}
|
||||
let p = i * 4;
|
||||
Some(pix[p] as f32 * 0.299 + pix[p + 1] as f32 * 0.587 + pix[p + 2] as f32 * 0.114)
|
||||
};
|
||||
|
||||
// Find maximum gradient magnitude in known 3x3 neighborhood
|
||||
let mut max_grad_x = 0.0_f32;
|
||||
let mut max_grad_y = 0.0_f32;
|
||||
let mut max_grad_mag = -1.0_f32;
|
||||
|
||||
for dy in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
let cx = bx + dx;
|
||||
let cy = by + dy;
|
||||
if let Some(center) = lum(cx, cy) {
|
||||
let r = lum(cx + 1, cy).unwrap_or(center);
|
||||
let l = lum(cx - 1, cy).unwrap_or(center);
|
||||
let d = lum(cx, cy + 1).unwrap_or(center);
|
||||
let u = lum(cx, cy - 1).unwrap_or(center);
|
||||
|
||||
let gx = (r - l) * 0.5;
|
||||
let gy = (d - u) * 0.5;
|
||||
let mag = gx * gx + gy * gy;
|
||||
if mag > max_grad_mag {
|
||||
max_grad_mag = mag;
|
||||
max_grad_x = gx;
|
||||
max_grad_y = gy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (ix, iy) = (-max_grad_y, max_grad_x); // isophote ⊥ gradient
|
||||
|
||||
let m = |x: i32, y: i32| -> f32 {
|
||||
if x < 0 || y < 0 || x >= w || y >= h {
|
||||
return 0.0;
|
||||
}
|
||||
if mask[(y * w + x) as usize] {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
};
|
||||
let a = m(bx + 1, by) - m(bx - 1, by);
|
||||
let b = m(bx, by + 1) - m(bx, by - 1);
|
||||
let len = (a * a + b * b).sqrt().max(1e-8);
|
||||
(ix * (a / len) + iy * (b / len)).abs() / ALPHA
|
||||
}
|
||||
|
||||
// ── Source-patch search ──────────────────────────────────────────────────────
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn find_source(
|
||||
bx: i32,
|
||||
by: i32,
|
||||
pix: &[u8],
|
||||
mask: &[bool],
|
||||
w: i32,
|
||||
h: i32,
|
||||
rng: &mut Lcg,
|
||||
radius: i32,
|
||||
) -> Option<(i32, i32)> {
|
||||
let range_x = (w - 2 * radius) as u64;
|
||||
let range_y = (h - 2 * radius) as u64;
|
||||
if range_x == 0 || range_y == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut best: Option<(i32, i32)> = None;
|
||||
let mut best_ssd = f32::MAX;
|
||||
let (mut hits, mut tries) = (0usize, 0usize);
|
||||
|
||||
// Phase 1: Global random search
|
||||
let mut candidates_list = Vec::with_capacity(CANDIDATES);
|
||||
while hits < CANDIDATES && tries < MAX_TRIES {
|
||||
tries += 1;
|
||||
let sx = radius + rng.next_bounded(range_x) as i32;
|
||||
let sy = radius + rng.next_bounded(range_y) as i32;
|
||||
if !patch_all_known(sx, sy, mask, w, h, radius) {
|
||||
continue;
|
||||
}
|
||||
hits += 1;
|
||||
candidates_list.push((sx, sy));
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use rayon::prelude::*;
|
||||
if let Some((sx, sy, ssd)) = candidates_list
|
||||
.into_par_iter()
|
||||
.map(|(sx, sy)| {
|
||||
let ssd = patch_ssd(bx, by, sx, sy, pix, mask, w, h, radius);
|
||||
(sx, sy, ssd)
|
||||
})
|
||||
.min_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
|
||||
{
|
||||
best = Some((sx, sy));
|
||||
best_ssd = ssd;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
for (sx, sy) in candidates_list {
|
||||
let ssd = patch_ssd(bx, by, sx, sy, pix, mask, w, h, radius);
|
||||
if ssd < best_ssd {
|
||||
best_ssd = ssd;
|
||||
best = Some((sx, sy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Local search refinement (Hill climbing)
|
||||
if let Some((mut sx, mut sy)) = best {
|
||||
for _ in 0..5 {
|
||||
let mut improved = false;
|
||||
for dy in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
if dx == 0 && dy == 0 { continue; }
|
||||
let nx = sx + dx;
|
||||
let ny = sy + dy;
|
||||
if patch_all_known(nx, ny, mask, w, h, radius) {
|
||||
let ssd = patch_ssd(bx, by, nx, ny, pix, mask, w, h, radius);
|
||||
if ssd < best_ssd {
|
||||
best_ssd = ssd;
|
||||
sx = nx; sy = ny;
|
||||
improved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !improved { break; }
|
||||
}
|
||||
best = Some((sx, sy));
|
||||
}
|
||||
|
||||
// Phase 3: Deterministic fallback if nothing found
|
||||
if best.is_none() {
|
||||
// Try a coarser grid search to find ANY valid patch
|
||||
let step = radius.max(2);
|
||||
for y in (radius..h-radius).step_by(step as usize) {
|
||||
for x in (radius..w-radius).step_by(step as usize) {
|
||||
if patch_all_known(x, y, mask, w, h, radius) {
|
||||
let ssd = patch_ssd(bx, by, x, y, pix, mask, w, h, radius);
|
||||
if ssd < best_ssd {
|
||||
best_ssd = ssd;
|
||||
best = Some((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best
|
||||
}
|
||||
|
||||
/// Returns true if every pixel in the patch is known (not in hole).
|
||||
fn patch_all_known(cx: i32, cy: i32, mask: &[bool], w: i32, h: i32, radius: i32) -> bool {
|
||||
for dy in -radius..=radius {
|
||||
for dx in -radius..=radius {
|
||||
let x = cx + dx;
|
||||
let y = cy + dy;
|
||||
if x < 0 || y < 0 || x >= w || y >= h {
|
||||
return false;
|
||||
}
|
||||
if mask[(y * w + x) as usize] {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Normalised SSD over known pixels in the destination patch.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn patch_ssd(
|
||||
bx: i32,
|
||||
by: i32,
|
||||
sx: i32,
|
||||
sy: i32,
|
||||
pix: &[u8],
|
||||
mask: &[bool],
|
||||
w: i32,
|
||||
h: i32,
|
||||
radius: i32,
|
||||
) -> f32 {
|
||||
let mut ssd = 0.0_f32;
|
||||
let mut cnt = 0u32;
|
||||
for dy in -radius..=radius {
|
||||
for dx in -radius..=radius {
|
||||
let tx = bx + dx;
|
||||
let ty = by + dy;
|
||||
if tx < 0 || ty < 0 || tx >= w || ty >= h {
|
||||
continue;
|
||||
}
|
||||
let ti = (ty * w + tx) as usize;
|
||||
if mask[ti] {
|
||||
continue;
|
||||
}
|
||||
let tp = ti * 4;
|
||||
let si = ((sy + dy) * w + (sx + dx)) as usize * 4;
|
||||
let dr = pix[tp] as f32 - pix[si] as f32;
|
||||
let dg = pix[tp + 1] as f32 - pix[si + 1] as f32;
|
||||
let db = pix[tp + 2] as f32 - pix[si + 2] as f32;
|
||||
ssd += dr * dr + dg * dg + db * db;
|
||||
cnt += 1;
|
||||
}
|
||||
}
|
||||
if cnt > 0 {
|
||||
ssd / cnt as f32
|
||||
} else {
|
||||
f32::MAX
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns average color of known pixels in the patch neighborhood.
|
||||
fn boundary_avg(bx: i32, by: i32, pix: &[u8], mask: &[bool], w: i32, h: i32, radius: i32) -> [u8; 4] {
|
||||
let mut r = 0.0f32; let mut g = 0.0f32; let mut b = 0.0f32; let mut count = 0.0f32;
|
||||
for dy in -radius..=radius {
|
||||
for dx in -radius..=radius {
|
||||
let x = bx + dx; let y = by + dy;
|
||||
if x >= 0 && x < w && y >= 0 && y < h {
|
||||
let i = (y * w + x) as usize;
|
||||
if !mask[i] {
|
||||
r += pix[i * 4] as f32;
|
||||
g += pix[i * 4 + 1] as f32;
|
||||
b += pix[i * 4 + 2] as f32;
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if count > 0.0 {
|
||||
[(r / count) as u8, (g / count) as u8, (b / count) as u8, 255]
|
||||
} else {
|
||||
[128, 128, 128, 255]
|
||||
}
|
||||
}
|
||||
|
||||
// ── Minimal LCG PRNG ─────────────────────────────────────────────────────────
|
||||
|
||||
struct Lcg {
|
||||
state: u64,
|
||||
}
|
||||
impl Default for Lcg {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: 0xDEAD_BEEF_CAFE_F00D,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Lcg {
|
||||
fn next(&mut self) -> u64 {
|
||||
self.state = self
|
||||
.state
|
||||
.wrapping_mul(6_364_136_223_846_793_005)
|
||||
.wrapping_add(1_442_695_040_888_963_407);
|
||||
self.state
|
||||
}
|
||||
fn next_bounded(&mut self, bound: u64) -> u64 {
|
||||
if bound == 0 {
|
||||
return 0;
|
||||
}
|
||||
self.next() % bound
|
||||
}
|
||||
}
|
||||
|
||||
/// [Task #070] Red-eye Removal Tool
|
||||
pub fn draw_redeye_removal(layer: &mut Layer, mask: Option<&[u8]>, points: &[(u32, u32)], size: f32, opacity: f32) {
|
||||
if points.is_empty() { return; }
|
||||
|
||||
let r = (size / 2.0).max(1.0);
|
||||
|
||||
let w = layer.width;
|
||||
let h = layer.height;
|
||||
|
||||
// 1. Build mask from collected points
|
||||
let mut redeye_mask = vec![false; (w * h) as usize];
|
||||
for &(px, py) in points {
|
||||
let cx = px as i32;
|
||||
let cy = py as i32;
|
||||
let ri = r as i32;
|
||||
|
||||
for dy in -ri..=ri {
|
||||
for dx in -ri..=ri {
|
||||
if dx*dx + dy*dy <= ri*ri {
|
||||
let tx = cx + dx;
|
||||
let ty = cy + dy;
|
||||
if tx >= 0 && tx < w as i32 && ty >= 0 && ty < h as i32 {
|
||||
redeye_mask[(ty as u32 * w + tx as u32) as usize] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let idx_pixel = (y * w + x) as usize;
|
||||
if !redeye_mask[idx_pixel] { continue; }
|
||||
|
||||
// Global mask check
|
||||
let mask_val = if let Some(m) = mask {
|
||||
*m.get(idx_pixel).unwrap_or(&0)
|
||||
} else {
|
||||
255
|
||||
};
|
||||
if mask_val == 0 { continue; }
|
||||
|
||||
let idx = idx_pixel * 4;
|
||||
let pr = layer.pixels[idx];
|
||||
let pg = layer.pixels[idx+1];
|
||||
let pb = layer.pixels[idx+2];
|
||||
|
||||
// Detection: R must be dominant — slightly relaxed threshold (1.2 instead of 1.5)
|
||||
let avg_gb = (pg as f32 + pb as f32) / 2.0;
|
||||
if pr as f32 > avg_gb * 1.2 && pr > 50 && pr > pg && pr > pb {
|
||||
// Target: Desaturate and darken
|
||||
let target_v = avg_gb.min(pr as f32 * 0.5); // Ensure it darkens
|
||||
let target_r = target_v * 0.9;
|
||||
|
||||
// For a stroke, feathering is applied at the edges of the stroke mask.
|
||||
// We'll approximate feathering based on distance to non-masked areas.
|
||||
// Simple version: just apply directly since brush itself has an anti-aliased feel.
|
||||
let blend = opacity * (mask_val as f32 / 255.0);
|
||||
|
||||
layer.pixels[idx] = (pr as f32 * (1.0 - blend) + target_r * blend).round() as u8;
|
||||
layer.pixels[idx+1] = (pg as f32 * (1.0 - blend) + target_v * blend).round() as u8;
|
||||
layer.pixels[idx+2] = (pb as f32 * (1.0 - blend) + target_v * blend).round() as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
layer.dirty = true;
|
||||
}
|
||||
|
||||
/// Belirtilen noktaları birleştirip içini dolduran bir maske oluşturur (Lasso tarzı).
|
||||
pub fn build_polygon_mask(points: &[(u32, u32)], w: u32, h: u32) -> Vec<bool> {
|
||||
let mut mask = vec![false; (w * h) as usize];
|
||||
if points.len() < 3 {
|
||||
return mask;
|
||||
}
|
||||
|
||||
// 1. Bounding Box bul (Performans için)
|
||||
let mut min_x = w;
|
||||
let mut max_x = 0;
|
||||
let mut min_y = h;
|
||||
let mut max_y = 0;
|
||||
for &(x, y) in points {
|
||||
if x < min_x { min_x = x; }
|
||||
if x > max_x { max_x = x; }
|
||||
if y < min_y { min_y = y; }
|
||||
if y > max_y { max_y = y; }
|
||||
}
|
||||
|
||||
// 2. Önce hattın kendisini boya (Sızıntı olmasın)
|
||||
for &(x, y) in points {
|
||||
if x < w && y < h {
|
||||
mask[(y * w + x) as usize] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Her piksel için Point-in-Polygon testi (Sadece kutu içinde)
|
||||
for y in min_y..=max_y {
|
||||
if y >= h { continue; }
|
||||
for x in min_x..=max_x {
|
||||
if x >= w { continue; }
|
||||
|
||||
let mut inside = false;
|
||||
let mut j = points.len() - 1;
|
||||
for i in 0..points.len() {
|
||||
let (pi_x, pi_y) = (points[i].0 as i32, points[i].1 as i32);
|
||||
let (pj_x, pj_y) = (points[j].0 as i32, points[j].1 as i32);
|
||||
let (x_i, y_i) = (x as i32, y as i32);
|
||||
|
||||
if ((pi_y > y_i) != (pj_y > y_i)) &&
|
||||
(x_i < (pj_x - pi_x) * (y_i - pi_y) / (pj_y - pi_y) + pi_x) {
|
||||
inside = !inside;
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
if inside {
|
||||
mask[(y * w + x) as usize] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
mask
|
||||
}
|
||||
|
||||
/// Builds a boolean hole mask from stroke points + optional global selection mask.
|
||||
pub fn build_stroke_mask(points: &[(u32, u32)], sel: Option<&[u8]>, r: f32, w: u32, h: u32) -> Vec<bool> {
|
||||
let mut hole = vec![false; (w * h) as usize];
|
||||
let ri = r as i32;
|
||||
for &(px, py) in points {
|
||||
let (cx, cy) = (px as i32, py as i32);
|
||||
for dy in -ri..=ri { for dx in -ri..=ri {
|
||||
if dx*dx + dy*dy <= ri*ri {
|
||||
let tx = cx + dx; let ty = cy + dy;
|
||||
if tx >= 0 && tx < w as i32 && ty >= 0 && ty < h as i32 {
|
||||
hole[(ty as u32 * w + tx as u32) as usize] = true;
|
||||
}
|
||||
}
|
||||
}}
|
||||
}
|
||||
if let Some(m) = sel {
|
||||
for i in 0..hole.len() { if m[i] == 0 { hole[i] = false; } }
|
||||
}
|
||||
hole
|
||||
}
|
||||
|
||||
/// [Task #071] Spot Removal — exemplar-based inpainting (Criminisi 2004).
|
||||
pub fn draw_spot_removal(layer: &mut Layer, mask: Option<&[u8]>, points: &[(u32, u32)], size: f32) {
|
||||
if points.is_empty() { return; }
|
||||
let r = (size / 2.0).max(1.0);
|
||||
let hole = build_stroke_mask(points, mask, r, layer.width, layer.height);
|
||||
if hole.iter().all(|&b| !b) { return; }
|
||||
criminisi_inpaint_with_radius(layer, &hole, 4); // Standard radius
|
||||
}
|
||||
|
||||
/// [Task #071] Smart Patch — exemplar-based inpainting (Criminisi 2004).
|
||||
/// Supports both stroke-based and global-selection-based hole definition.
|
||||
pub fn draw_smart_patch_v1(layer: &mut Layer, mask: Option<&[u8]>, points: &[(u32, u32)], size: f32, tool_is_ai_removal: bool) {
|
||||
let w = layer.width;
|
||||
let h = layer.height;
|
||||
let r = (size / 2.0).max(1.0);
|
||||
|
||||
// Build hole: If it's AI removal, treat points as a Lasso (Polygon).
|
||||
// Otherwise (Standard Smart Patch), treat as a Brush (Stroke).
|
||||
let hole = if tool_is_ai_removal && points.len() > 2 {
|
||||
build_polygon_mask(points, w, h)
|
||||
} else {
|
||||
build_stroke_mask(points, mask, r, w, h)
|
||||
};
|
||||
|
||||
if hole.iter().all(|&b| !b) { return; }
|
||||
log::warn!("[draw_smart_patch_v1] Starting inpaint on {}x{} layer with radius 4 (polygon: {})",
|
||||
w, h, tool_is_ai_removal);
|
||||
criminisi_inpaint_with_radius(layer, &hole, 4);
|
||||
}
|
||||
|
||||
/// Task wrapper for inpaint functionality (used by hcie-app)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_inpaint_task(
|
||||
model_path: &str,
|
||||
img_bytes: &[u8],
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
_seed: &[u8],
|
||||
_prompt: Option<String>,
|
||||
_point: Option<(u32, u32)>,
|
||||
_bbox: Option<(u32, u32, u32, u32)>,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
log::info!("[Vision] Running inpainting with model: {}", model_path);
|
||||
// Placeholder: Return the original image for now
|
||||
Ok(img_bytes.to_vec())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#![allow(dead_code)]
|
||||
pub mod types;
|
||||
pub mod comfy;
|
||||
pub mod inpaint;
|
||||
pub mod smart_patch;
|
||||
pub mod vision;
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
pub mod ffi;
|
||||
|
||||
pub use types::ComfyAssets;
|
||||
pub use comfy::ComfyManager;
|
||||
pub use vision::VisionManager;
|
||||
pub use inpaint::{criminisi_inpaint, criminisi_inpaint_with_radius, build_polygon_mask, build_stroke_mask, draw_redeye_removal, draw_spot_removal, draw_smart_patch_v1, run_inpaint_task};
|
||||
pub use smart_patch::{draw_smart_patch, PatchParams, PatchBlendMode, SolverConfig};
|
||||
pub use vision::{run_background_removal_task, run_object_segment_task, run_super_resolution_task};
|
||||
@@ -0,0 +1,462 @@
|
||||
#![allow(dead_code)]
|
||||
//! Smart Patch Tool — Poisson seamless cloning for raster layers.
|
||||
//!
|
||||
//! Bu modül, bir kaynak bölgeyi (source patch) hedef bir katmana
|
||||
//! (target layer) gradient alanını koruyarak yerleştirir. Klasik
|
||||
//! Pérez et al. (2003) "Poisson Image Editing" makalesindeki
|
||||
//! yaklaşımı takip eder:
|
||||
//!
|
||||
//! ```text
|
||||
//! Δf = div(v) Ω içinde
|
||||
//! f = f* ∂Ω üzerinde
|
||||
//! ```
|
||||
//!
|
||||
//! Burada `v` kılavuz vektör alanıdır (kaynağın gradyanı veya
|
||||
//! karışık gradyan), `f*` hedefin sınır değerleridir. Ayrık
|
||||
//! formda her iç piksel için:
|
||||
//!
|
||||
//! ```text
|
||||
//! |N_p| f_p - Σ_{q ∈ N_p ∩ Ω} f_q
|
||||
//! = Σ_{q ∈ N_p ∩ ∂Ω} f*_q + Σ_{q ∈ N_p} v_{pq}
|
||||
//! ```
|
||||
//!
|
||||
//! Bu seyrek lineer sistemi Gauss-Seidel + SOR ile çözüyoruz;
|
||||
//! büyük küçük patch'ler için pratik ve allocation-lite.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dış tipler — gerçek editörde bunlar ayrı modüllerden geliyor olur.
|
||||
// Burada self-contained bir kesit sunuyoruz.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use hcie_protocol::Layer;
|
||||
|
||||
// Layer yardımcı metodları (smart_patch'e özel)
|
||||
trait LayerExt {
|
||||
fn patch_idx(&self, x: i32, y: i32) -> usize;
|
||||
}
|
||||
|
||||
impl LayerExt for Layer {
|
||||
#[inline]
|
||||
fn patch_idx(&self, x: i32, y: i32) -> usize {
|
||||
(y as usize * self.width as usize + x as usize) * 4
|
||||
}
|
||||
}
|
||||
|
||||
/// Araç durumu — UI'dan gelen parametreler burada tutulur.
|
||||
pub struct PatchParams {
|
||||
/// Kaynak katman (snapshot). Hedef katmana klonlanacak görüntü.
|
||||
pub source: Layer,
|
||||
/// Kaynaktan alınacak bölgenin sol-üst köşesi.
|
||||
pub source_origin: (i32, i32),
|
||||
/// Bu bölgenin hedefteki yerleşim sol-üst köşesi.
|
||||
pub dest_origin: (i32, i32),
|
||||
/// Klonlanacak bölgenin genişlik × yükseklik.
|
||||
pub patch_size: (u32, u32),
|
||||
/// Karıştırma modu.
|
||||
pub blend_mode: PatchBlendMode,
|
||||
/// SOR için çözücü ayarları.
|
||||
pub solver: SolverConfig,
|
||||
}
|
||||
|
||||
/// Kılavuz vektör alanı stratejisi (Poisson'a özel).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PatchBlendMode {
|
||||
/// v = ∇g (kaynağın gradyanı). Düz "seamless clone".
|
||||
#[allow(dead_code)]
|
||||
Normal,
|
||||
/// Her komşulukta |∇f*| vs |∇g| karşılaştırıp büyüğünü seçer.
|
||||
Mixed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SolverConfig {
|
||||
pub max_iters: u32,
|
||||
pub tolerance: f32,
|
||||
/// Successive Over-Relaxation faktörü (1.0 = düz Gauss-Seidel).
|
||||
/// 1.7–1.95 arası değerler yamuk olmayan bölgelerde hızlandırır.
|
||||
pub omega: f32,
|
||||
}
|
||||
|
||||
impl Default for SolverConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_iters: 800,
|
||||
tolerance: 1e-3,
|
||||
omega: 1.85,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ana giriş noktası
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// [Task #071] Smart Patch Tool (Poisson Blending)
|
||||
pub fn draw_smart_patch(layer: &mut Layer, mask: Option<&[u8]>, params: &mut PatchParams) {
|
||||
let (pw, ph) = params.patch_size;
|
||||
if pw == 0 || ph == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Patch bölgesi için ikili maskeyi hazırla. Caller maske verdiyse
|
||||
// onu kullan, vermediyse dikdörtgeni komple iç bölge kabul et.
|
||||
let region = match mask {
|
||||
Some(m) => Mask::from_user(m, pw, ph),
|
||||
None => Mask::full(pw, ph),
|
||||
};
|
||||
|
||||
// 2) Hedef ve kaynak dikdörtgenlerini katman sınırlarına kırp.
|
||||
let clip = match Clip::compute(layer, ¶ms.source, params.source_origin, params.dest_origin, pw, ph) {
|
||||
Some(c) => c,
|
||||
None => return, // tamamen dışarıda
|
||||
};
|
||||
|
||||
// 3) Her kanal için Poisson sistemini ayrı ayrı çöz.
|
||||
// Alfa kanalını olduğu gibi yumuşak geçir — gradyan karışımı RGB'de.
|
||||
let mut channels: [ChannelBuffer; 3] = [
|
||||
ChannelBuffer::new(clip.width, clip.height),
|
||||
ChannelBuffer::new(clip.width, clip.height),
|
||||
ChannelBuffer::new(clip.width, clip.height),
|
||||
];
|
||||
|
||||
for (c, buf) in channels.iter_mut().enumerate() {
|
||||
buf.load(layer, ¶ms.source, &clip, ®ion, c);
|
||||
solve_poisson(buf, ®ion, &clip, params.blend_mode, ¶ms.solver);
|
||||
}
|
||||
|
||||
// 4) Sonucu katmana geri yaz. Alfa için kaynak alfa ile
|
||||
// çarpımsal bir karışım uyguluyoruz ki patch yumuşakça otursun.
|
||||
write_back(layer, ¶ms.source, &clip, ®ion, &channels);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Maske
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 0 = dış, 1 = iç (çözülecek piksel), 2 = iç ama hedef katman sınırında.
|
||||
struct Mask {
|
||||
w: u32,
|
||||
h: u32,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Mask {
|
||||
fn full(w: u32, h: u32) -> Self {
|
||||
Self { w, h, data: vec![1; (w * h) as usize] }
|
||||
}
|
||||
|
||||
fn from_user(m: &[u8], w: u32, h: u32) -> Self {
|
||||
let expected = (w * h) as usize;
|
||||
let mut data = vec![0u8; expected];
|
||||
let n = m.len().min(expected);
|
||||
for i in 0..n {
|
||||
data[i] = if m[i] >= 128 { 1 } else { 0 };
|
||||
}
|
||||
Self { w, h, data }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get(&self, x: i32, y: i32) -> u8 {
|
||||
if x < 0 || y < 0 || (x as u32) >= self.w || (y as u32) >= self.h {
|
||||
0
|
||||
} else {
|
||||
self.data[(y as u32 * self.w + x as u32) as usize]
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_interior(&self, x: i32, y: i32) -> bool {
|
||||
self.get(x, y) != 0
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kırpma: kaynak ve hedef geometrisini ortak bir çalışma bölgesine indirger
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct Clip {
|
||||
/// Çalışma bölgesinin boyutu (patch üzerinde kırpılmış).
|
||||
width: u32,
|
||||
height: u32,
|
||||
/// Hedef katmanda çalışma bölgesinin sol-üstü.
|
||||
dst_x: i32,
|
||||
dst_y: i32,
|
||||
/// Kaynak katmanda çalışma bölgesinin sol-üstü.
|
||||
src_x: i32,
|
||||
src_y: i32,
|
||||
/// Maske içi başlangıç ofseti (kırpıldıysa).
|
||||
mask_x: i32,
|
||||
mask_y: i32,
|
||||
}
|
||||
|
||||
impl Clip {
|
||||
fn compute(
|
||||
dst: &Layer,
|
||||
src: &Layer,
|
||||
src_origin: (i32, i32),
|
||||
dst_origin: (i32, i32),
|
||||
pw: u32,
|
||||
ph: u32,
|
||||
) -> Option<Self> {
|
||||
let (sox, soy) = src_origin;
|
||||
let (dox, doy) = dst_origin;
|
||||
|
||||
// Hedef için kırpma
|
||||
let dx0 = dox.max(0);
|
||||
let dy0 = doy.max(0);
|
||||
let dx1 = (dox + pw as i32).min(dst.width as i32);
|
||||
let dy1 = (doy + ph as i32).min(dst.height as i32);
|
||||
|
||||
// Kaynak için kırpma — aynı offset korunmalı
|
||||
let shift_x = dx0 - dox;
|
||||
let shift_y = dy0 - doy;
|
||||
let sx0 = sox + shift_x;
|
||||
let sy0 = soy + shift_y;
|
||||
|
||||
let mut w = (dx1 - dx0).min(src.width as i32 - sx0);
|
||||
let mut h = (dy1 - dy0).min(src.height as i32 - sy0);
|
||||
|
||||
if sx0 < 0 {
|
||||
w -= -sx0;
|
||||
}
|
||||
if sy0 < 0 {
|
||||
h -= -sy0;
|
||||
}
|
||||
let sx0 = sx0.max(0);
|
||||
let sy0 = sy0.max(0);
|
||||
let dx0 = dx0 + (sx0 - (sox + shift_x));
|
||||
let dy0 = dy0 + (sy0 - (soy + shift_y));
|
||||
|
||||
if w <= 0 || h <= 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Clip {
|
||||
width: w as u32,
|
||||
height: h as u32,
|
||||
dst_x: dx0,
|
||||
dst_y: dy0,
|
||||
src_x: sx0,
|
||||
src_y: sy0,
|
||||
mask_x: shift_x.max(0),
|
||||
mask_y: shift_y.max(0),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kanal bazlı çözücü bufferı
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct ChannelBuffer {
|
||||
w: u32,
|
||||
h: u32,
|
||||
/// Hedeften okunmuş başlangıç değeri (aynı zamanda sınır değeri).
|
||||
dst: Vec<f32>,
|
||||
/// Kaynaktan okunmuş değer (gradyan hesabı için).
|
||||
src: Vec<f32>,
|
||||
/// Çözülmekte olan alan (iteratif güncellenir).
|
||||
f: Vec<f32>,
|
||||
}
|
||||
|
||||
impl ChannelBuffer {
|
||||
fn new(w: u32, h: u32) -> Self {
|
||||
let n = (w * h) as usize;
|
||||
Self { w, h, dst: vec![0.0; n], src: vec![0.0; n], f: vec![0.0; n] }
|
||||
}
|
||||
|
||||
fn load(&mut self, layer: &Layer, source: &Layer, clip: &Clip, _region: &Mask, channel: usize) {
|
||||
for y in 0..clip.height as i32 {
|
||||
for x in 0..clip.width as i32 {
|
||||
let dst_px = layer.patch_idx(clip.dst_x + x, clip.dst_y + y);
|
||||
let src_px = source.patch_idx(clip.src_x + x, clip.src_y + y);
|
||||
let i = (y as u32 * self.w + x as u32) as usize;
|
||||
self.dst[i] = layer.pixels[dst_px + channel] as f32;
|
||||
self.src[i] = source.pixels[src_px + channel] as f32;
|
||||
self.f[i] = self.dst[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Çözücü: Gauss-Seidel + SOR, mixed gradient opsiyonlu
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn solve_poisson(buf: &mut ChannelBuffer, region: &Mask, clip: &Clip, mode: PatchBlendMode, cfg: &SolverConfig) {
|
||||
let w = buf.w as i32;
|
||||
let h = buf.h as i32;
|
||||
let omega = cfg.omega.clamp(1.0, 1.99);
|
||||
|
||||
for iter in 0..cfg.max_iters {
|
||||
let mut max_delta = 0.0f32;
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let mx = x + clip.mask_x;
|
||||
let my = y + clip.mask_y;
|
||||
if !region.is_interior(mx, my) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let i = (y as u32 * buf.w + x as u32) as usize;
|
||||
|
||||
let mut sum_boundary_or_interior = 0.0f32;
|
||||
let mut sum_guidance = 0.0f32;
|
||||
let mut n_count = 0.0f32;
|
||||
|
||||
let neighbors = [(-1, 0), (1, 0), (0, -1), (0, 1)];
|
||||
for (dx, dy) in neighbors {
|
||||
let nx = x + dx;
|
||||
let ny = y + dy;
|
||||
if nx < 0 || ny < 0 || nx >= w || ny >= h {
|
||||
sum_boundary_or_interior += buf.f[i];
|
||||
n_count += 1.0;
|
||||
continue;
|
||||
}
|
||||
|
||||
let j = (ny as u32 * buf.w + nx as u32) as usize;
|
||||
n_count += 1.0;
|
||||
|
||||
let nmx = nx + clip.mask_x;
|
||||
let nmy = ny + clip.mask_y;
|
||||
if region.is_interior(nmx, nmy) {
|
||||
sum_boundary_or_interior += buf.f[j];
|
||||
} else {
|
||||
sum_boundary_or_interior += buf.dst[j];
|
||||
}
|
||||
|
||||
let g_pq = buf.src[i] - buf.src[j];
|
||||
let f_pq = buf.dst[i] - buf.dst[j];
|
||||
let v_pq = match mode {
|
||||
PatchBlendMode::Normal => g_pq,
|
||||
PatchBlendMode::Mixed => {
|
||||
if g_pq.abs() >= f_pq.abs() { g_pq } else { f_pq }
|
||||
}
|
||||
};
|
||||
sum_guidance += v_pq;
|
||||
}
|
||||
|
||||
if n_count == 0.0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_val = (sum_boundary_or_interior + sum_guidance) / n_count;
|
||||
let old_val = buf.f[i];
|
||||
let relaxed = old_val + omega * (new_val - old_val);
|
||||
let delta = (relaxed - old_val).abs();
|
||||
if delta > max_delta {
|
||||
max_delta = delta;
|
||||
}
|
||||
buf.f[i] = relaxed;
|
||||
}
|
||||
}
|
||||
|
||||
if max_delta < cfg.tolerance * 255.0 {
|
||||
let _ = iter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Geri yazma
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn write_back(layer: &mut Layer, source: &Layer, clip: &Clip, region: &Mask, ch: &[ChannelBuffer; 3]) {
|
||||
for y in 0..clip.height as i32 {
|
||||
for x in 0..clip.width as i32 {
|
||||
let mx = x + clip.mask_x;
|
||||
let my = y + clip.mask_y;
|
||||
if !region.is_interior(mx, my) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let i = (y as u32 * ch[0].w + x as u32) as usize;
|
||||
let dst_px = layer.patch_idx(clip.dst_x + x, clip.dst_y + y);
|
||||
let src_px = source.patch_idx(clip.src_x + x, clip.src_y + y);
|
||||
|
||||
for (c, buf) in ch.iter().enumerate().take(3) {
|
||||
layer.pixels[dst_px + c] = clamp_u8(buf.f[i]);
|
||||
}
|
||||
let sa = source.pixels[src_px + 3] as f32 / 255.0;
|
||||
let da = layer.pixels[dst_px + 3] as f32 / 255.0;
|
||||
let out_a = da + sa * (1.0 - da);
|
||||
layer.pixels[dst_px + 3] = clamp_u8(out_a * 255.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clamp_u8(v: f32) -> u8 {
|
||||
if v.is_nan() {
|
||||
return 0;
|
||||
}
|
||||
v.round().clamp(0.0, 255.0) as u8
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Testler — sağlık kontrolü
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn solid_layer(w: u32, h: u32, rgba: [u8; 4]) -> Layer {
|
||||
let mut layer = Layer::new_transparent("test", w, h);
|
||||
for px in layer.pixels.chunks_exact_mut(4) {
|
||||
px.copy_from_slice(&rgba);
|
||||
}
|
||||
layer
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_patch_on_uniform_regions_is_stable() {
|
||||
let mut target = solid_layer(16, 16, [120, 120, 120, 255]);
|
||||
let source = solid_layer(16, 16, [120, 120, 120, 255]);
|
||||
let mut st = PatchParams {
|
||||
source,
|
||||
source_origin: (4, 4),
|
||||
dest_origin: (8, 8),
|
||||
patch_size: (6, 6),
|
||||
blend_mode: PatchBlendMode::Normal,
|
||||
solver: SolverConfig { max_iters: 200, tolerance: 1e-3, omega: 1.7 },
|
||||
};
|
||||
draw_smart_patch(&mut target, None, &mut st);
|
||||
for px in target.pixels.chunks_exact(4) {
|
||||
assert!((px[0] as i32 - 120).abs() <= 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_interior_moves_toward_source_gradient() {
|
||||
let mut target = solid_layer(32, 32, [200, 200, 200, 255]);
|
||||
let mut source = solid_layer(32, 32, [0, 0, 0, 255]);
|
||||
for y in 0..32 {
|
||||
for x in 0..32 {
|
||||
let v = (y * 8) as u8;
|
||||
let i = ((y * 32 + x) * 4) as usize;
|
||||
source.pixels[i] = v;
|
||||
source.pixels[i + 1] = v;
|
||||
source.pixels[i + 2] = v;
|
||||
}
|
||||
}
|
||||
let mut st = PatchParams {
|
||||
source,
|
||||
source_origin: (8, 8),
|
||||
dest_origin: (12, 12),
|
||||
patch_size: (8, 8),
|
||||
blend_mode: PatchBlendMode::Normal,
|
||||
solver: SolverConfig::default(),
|
||||
};
|
||||
draw_smart_patch(&mut target, None, &mut st);
|
||||
|
||||
let outside = ((11 * 32 + 12) * 4) as usize;
|
||||
assert_eq!(target.pixels[outside], 200);
|
||||
|
||||
let top = ((12 * 32 + 15) * 4) as usize;
|
||||
let bot = ((19 * 32 + 15) * 4) as usize;
|
||||
assert!(target.pixels[bot] as i32 > target.pixels[top] as i32);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ComfyAssets {
|
||||
pub models: Vec<String>,
|
||||
pub clips: Vec<String>,
|
||||
pub has_inpaint_nodes: bool,
|
||||
pub has_color_match: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VisionModelConfig {
|
||||
pub task: VisionTask,
|
||||
pub model_path: String,
|
||||
pub backend: VisionBackend,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum VisionTask {
|
||||
#[default]
|
||||
Segmentation,
|
||||
BackgroundRemoval,
|
||||
Inpainting,
|
||||
SuperResolution,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub enum VisionBackend {
|
||||
#[default]
|
||||
Cpu,
|
||||
Gpu,
|
||||
}
|
||||
|
||||
/// Poisson smart patch blend mode
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PatchBlendMode {
|
||||
Normal,
|
||||
Mixed,
|
||||
}
|
||||
|
||||
impl Default for PatchBlendMode {
|
||||
fn default() -> Self { Self::Mixed }
|
||||
}
|
||||
|
||||
/// Poisson solver configuration
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SolverConfig {
|
||||
pub max_iters: u32,
|
||||
pub tolerance: f32,
|
||||
pub omega: f32,
|
||||
}
|
||||
|
||||
impl Default for SolverConfig {
|
||||
fn default() -> Self {
|
||||
Self { max_iters: 800, tolerance: 1e-3, omega: 1.85 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Smart Patch parameters
|
||||
pub struct PatchParams {
|
||||
pub source: SourceLayer,
|
||||
pub source_origin: (i32, i32),
|
||||
pub dest_origin: (i32, i32),
|
||||
pub patch_size: (u32, u32),
|
||||
pub blend_mode: PatchBlendMode,
|
||||
pub solver: SolverConfig,
|
||||
}
|
||||
|
||||
/// Lightweight source layer copy (pixels + dims only) for smart_patch
|
||||
#[derive(Clone)]
|
||||
pub struct SourceLayer {
|
||||
pub pixels: Vec<u8>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
#![allow(dead_code, unreachable_patterns, unused_variables, unused_assignments, unexpected_cfgs)]
|
||||
|
||||
use std::collections::VecDeque;
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
use crate::ffi;
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
use std::ffi::CString;
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
use std::os::raw::c_void;
|
||||
|
||||
/// Bridge to the vision.cpp C++ library or native Rust equivalent.
|
||||
pub struct VisionManager {
|
||||
// This would hold the pointers to the loaded GGML contexts
|
||||
}
|
||||
|
||||
impl Default for VisionManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl VisionManager {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
/// High-level background removal task
|
||||
pub fn run_background_removal_task(
|
||||
model_path: &str,
|
||||
_img_bytes: &[u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
log::info!("[Vision] Running background removal with model: {}", model_path);
|
||||
// Placeholder: Return a dummy mask (all white)
|
||||
let mask = vec![255u8; (width * height) as usize];
|
||||
Ok(mask)
|
||||
}
|
||||
|
||||
/// High-level object segment task — uses MobileSAM via GGML FFI, falls back to flood fill.
|
||||
pub fn run_object_segment_task(
|
||||
model_path: &str,
|
||||
img_bytes: &[u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
point: Option<(u32, u32)>,
|
||||
bbox: Option<(u32, u32, u32, u32)>,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let backend = crate::ffi::vision_backend_code();
|
||||
log::info!("[Vision] SAM: model='{}' {}x{} point={:?} bbox={:?} backend={}", model_path, width, height, point, bbox, if backend == 0 { "CPU" } else { "GPU" });
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
if !model_path.is_empty() && std::path::Path::new(model_path).exists() {
|
||||
match run_sam_ggml(model_path, img_bytes, width, height, point, bbox, backend) {
|
||||
Ok(mask) => return Ok(mask),
|
||||
Err(e) => log::warn!("[Vision] SAM GGML failed: {}. Falling back.", e),
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: flood fill from point, rectangle from bbox
|
||||
sam_fallback(img_bytes, width, height, point, bbox)
|
||||
}
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
fn run_sam_ggml(
|
||||
model_path: &str,
|
||||
img_bytes: &[u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
point: Option<(u32, u32)>,
|
||||
bbox: Option<(u32, u32, u32, u32)>,
|
||||
backend_code: u8,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
// Convert RGBA → RGB (SAM expects RGB)
|
||||
let mut rgb: Vec<u8> = Vec::with_capacity((width * height * 3) as usize);
|
||||
for chunk in img_bytes.chunks(4) {
|
||||
rgb.extend_from_slice(&chunk[..3]);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// 1. Init device
|
||||
let mut device_ptr: *mut ffi::BackendDevice = std::ptr::null_mut();
|
||||
let ok = ffi::visp_device_init(backend_code as i32, &mut device_ptr);
|
||||
if ok == 0 {
|
||||
return Err(format!("visp_device_init: {}", get_visp_error()));
|
||||
}
|
||||
let _device_guard = DeviceGuard(device_ptr);
|
||||
|
||||
// 2. Load model (arch=0 = Sam)
|
||||
let path_cstr = CString::new(model_path).map_err(|e| e.to_string())?;
|
||||
let mut model_ptr: *mut ffi::AnyModel = std::ptr::null_mut();
|
||||
let ok = ffi::visp_model_load(path_cstr.as_ptr(), device_ptr, 0, &mut model_ptr);
|
||||
if ok == 0 {
|
||||
return Err(format!("visp_model_load: {}", get_visp_error()));
|
||||
}
|
||||
log::info!("[Vision] SAM model loaded");
|
||||
let _model_guard = ModelGuard(model_ptr, 0);
|
||||
|
||||
// 3. Build image view (RgbU8 = 3)
|
||||
let image_view = ffi::VispImageView {
|
||||
width: width as i32,
|
||||
height: height as i32,
|
||||
stride: (width * 3) as i32,
|
||||
format: 3, // RgbU8
|
||||
data: rgb.as_ptr() as *const c_void,
|
||||
};
|
||||
|
||||
// 4. Build prompt args
|
||||
let args: Vec<i32> = if let Some((x, y)) = point {
|
||||
log::info!("[Vision] SAM prompt: point ({},{})", x, y);
|
||||
vec![x as i32, y as i32]
|
||||
} else if let Some((bx, by, bw, bh)) = bbox {
|
||||
log::info!("[Vision] SAM prompt: bbox ({},{},{},{})", bx, by, bx+bw, by+bh);
|
||||
vec![bx as i32, by as i32, (bx + bw) as i32, (by + bh) as i32]
|
||||
} else {
|
||||
log::info!("[Vision] SAM prompt: center fallback");
|
||||
vec![(width / 2) as i32, (height / 2) as i32]
|
||||
};
|
||||
|
||||
// 5. Run compute
|
||||
let mut out_image = ffi::VispImage {
|
||||
width: 0, height: 0, stride: 0, format: 0,
|
||||
data: std::ptr::null_mut(),
|
||||
};
|
||||
let mut out_data: *mut ffi::ImageData = std::ptr::null_mut();
|
||||
|
||||
let ok = ffi::visp_model_compute(
|
||||
model_ptr, 0, // Sam
|
||||
&image_view, 1,
|
||||
args.as_ptr(), args.len() as i32,
|
||||
&mut out_image, &mut out_data,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(format!("visp_model_compute: {}", get_visp_error()));
|
||||
}
|
||||
log::info!("[Vision] SAM output: {}x{} format={}", out_image.width, out_image.height, out_image.format);
|
||||
|
||||
let out_w = out_image.width as usize;
|
||||
let out_h = out_image.height as usize;
|
||||
|
||||
// 6. Extract alpha mask from output
|
||||
// format 4 = AlphaU8 (single channel), format 0 = RgbaU8 (take alpha)
|
||||
let raw_mask: Vec<u8> = match out_image.format {
|
||||
4 => { // AlphaU8
|
||||
std::slice::from_raw_parts(out_image.data as *const u8, out_w * out_h).to_vec()
|
||||
}
|
||||
0 => { // RgbaU8 — extract alpha channel
|
||||
let rgba = std::slice::from_raw_parts(out_image.data as *const u8, out_w * out_h * 4);
|
||||
rgba.chunks(4).map(|c| c[3]).collect()
|
||||
}
|
||||
_ => {
|
||||
log::warn!("[Vision] SAM unknown output format {}, reading as single channel", out_image.format);
|
||||
std::slice::from_raw_parts(out_image.data as *const u8, out_w * out_h).to_vec()
|
||||
}
|
||||
};
|
||||
ffi::visp_image_destroy(out_data);
|
||||
|
||||
// 7. Resize to canvas size if SAM output is smaller (e.g. 256px mask → canvas size)
|
||||
let mask = if out_w != width as usize || out_h != height as usize {
|
||||
log::info!("[Vision] SAM resizing mask {}x{} → {}x{}", out_w, out_h, width, height);
|
||||
let mut resized = vec![0u8; (width * height) as usize];
|
||||
for y in 0..height as usize {
|
||||
for x in 0..width as usize {
|
||||
let sx = x * out_w / width as usize;
|
||||
let sy = y * out_h / height as usize;
|
||||
resized[y * width as usize + x] = raw_mask[sy * out_w + sx];
|
||||
}
|
||||
}
|
||||
resized
|
||||
} else {
|
||||
raw_mask
|
||||
};
|
||||
|
||||
Ok(mask)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
unsafe fn get_visp_error() -> String {
|
||||
let ptr = ffi::visp_get_last_error();
|
||||
if ptr.is_null() { return "unknown error".to_string(); }
|
||||
std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
// RAII guards so device/model are freed even on early return
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
struct DeviceGuard(*mut ffi::BackendDevice);
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
impl Drop for DeviceGuard {
|
||||
fn drop(&mut self) { unsafe { if !self.0.is_null() { ffi::visp_device_destroy(self.0); } } }
|
||||
}
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
struct ModelGuard(*mut ffi::AnyModel, i32);
|
||||
#[cfg(feature = "cpp-backend")]
|
||||
impl Drop for ModelGuard {
|
||||
fn drop(&mut self) { unsafe { if !self.0.is_null() { ffi::visp_model_destroy(self.0, self.1); } } }
|
||||
}
|
||||
|
||||
fn sam_fallback(
|
||||
img_bytes: &[u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
point: Option<(u32, u32)>,
|
||||
bbox: Option<(u32, u32, u32, u32)>,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let mut mask = vec![0u8; (width * height) as usize];
|
||||
|
||||
if let Some((bx, by, bw, bh)) = bbox {
|
||||
for y in by..by.saturating_add(bh).min(height) {
|
||||
for x in bx..bx.saturating_add(bw).min(width) {
|
||||
mask[(y * width + x) as usize] = 255;
|
||||
}
|
||||
}
|
||||
} else if let Some((cx, cy)) = point {
|
||||
if cx < width && cy < height {
|
||||
let start_idx = (cy * width + cx) as usize * 4;
|
||||
if start_idx + 3 < img_bytes.len() {
|
||||
let tc = [img_bytes[start_idx], img_bytes[start_idx+1], img_bytes[start_idx+2]];
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back((cx, cy));
|
||||
mask[(cy * width + cx) as usize] = 255;
|
||||
let mut count = 0usize;
|
||||
while let Some((x, y)) = queue.pop_front() {
|
||||
count += 1;
|
||||
if count > 100_000 { break; }
|
||||
for (dx, dy) in [(0i32,1i32),(0,-1),(1,0),(-1,0)] {
|
||||
let nx = x as i32 + dx;
|
||||
let ny = y as i32 + dy;
|
||||
if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 {
|
||||
let ni = (ny as u32 * width + nx as u32) as usize;
|
||||
if mask[ni] == 0 {
|
||||
let ii = ni * 4;
|
||||
let diff = (img_bytes[ii] as i32 - tc[0] as i32).abs()
|
||||
+ (img_bytes[ii+1] as i32 - tc[1] as i32).abs()
|
||||
+ (img_bytes[ii+2] as i32 - tc[2] as i32).abs();
|
||||
if diff < 96 { mask[ni] = 255; queue.push_back((nx as u32, ny as u32)); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(mask)
|
||||
}
|
||||
|
||||
/// High-level super resolution task
|
||||
pub fn run_super_resolution_task(
|
||||
model_path: &str,
|
||||
_img_bytes: &[u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
log::info!("[Vision] Running super resolution with model: {}", model_path);
|
||||
// Placeholder: Return a dummy mask (all white)
|
||||
let mask = vec![255u8; (width * height) as usize];
|
||||
Ok(mask)
|
||||
}
|
||||
Reference in New Issue
Block a user