Add M9-M14: UI redesign, plugins, macros, 3D viz, binary/segmentation ops, Metal GPU, scientific LUTs

M9: TailwindCSS + shadcn/ui frontend redesign
M10: Dynamic plugin system with shared library loading
M11: Macro recording, playback, and batch processing
M12: 3D volume visualization, marching cubes, STL/OBJ export, rivol:// protocol
M13: Additional GPU shaders, enhanced auto-threshold, merge channels
M14: Binary image processing (EDT, watershed, skeleton, connected components,
     voronoi), segmentation & analysis (particles, colocalization, find maxima),
     math/noise/filter/transform ops (60+ total), scientific LUTs (Fire, Ice,
     Spectrum, Jet, Phase, HiLo + .lut file I/O), Apple Metal GPU optimization
     (metal-only feature, Apple Silicon detection, lower dispatch threshold),
     native Metal compute crate (ri-metal with MSL shaders via objc2-metal),
     7 new WGSL GPU shaders (bilateral, variance, mean, math_ops, outline,
     minmax_filter, affine transform). 25 crates, 162 tests, 75+ IPC commands.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Omar Sobh
2026-03-10 07:41:56 -07:00
co-authored by Claude Opus 4.6
parent 1889715f3c
commit 0fd39d7fd3
135 changed files with 19456 additions and 1905 deletions
+72
View File
@@ -0,0 +1,72 @@
use ri_core::TypedBuffer;
/// Mean (box) filter: replace each pixel with the average of its neighborhood.
pub fn mean_filter(buffer: &TypedBuffer, w: u32, h: u32, radius: u32) -> TypedBuffer {
let w = w as usize;
let h = h as usize;
let r = radius as i32;
let input: Vec<f32> = match buffer {
TypedBuffer::U8(d) => d.iter().map(|&v| v as f32).collect(),
TypedBuffer::U16(d) => d.iter().map(|&v| v as f32).collect(),
TypedBuffer::F32(d) => d.clone(),
};
let mut output = vec![0.0f32; w * h];
for y in 0..h {
for x in 0..w {
let mut sum = 0.0f64;
let mut count = 0u32;
for dy in -r..=r {
for dx in -r..=r {
let nx = x as i32 + dx;
let ny = y as i32 + dy;
if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
sum += input[ny as usize * w + nx as usize] as f64;
count += 1;
}
}
}
output[y * w + x] = (sum / count as f64) as f32;
}
}
match buffer {
TypedBuffer::U8(_) => TypedBuffer::U8(
output.iter().map(|&v| v.round().clamp(0.0, 255.0) as u8).collect(),
),
TypedBuffer::U16(_) => TypedBuffer::U16(
output.iter().map(|&v| v.round().clamp(0.0, 65535.0) as u16).collect(),
),
TypedBuffer::F32(_) => TypedBuffer::F32(output),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mean_filter_smooths() {
// 5x5 image with a spike in center
let mut data = vec![0u8; 25];
data[12] = 100; // center
let buf = TypedBuffer::U8(data);
let result = mean_filter(&buf, 5, 5, 1);
let s = result.as_u8_slice().unwrap();
// Center should be reduced (averaged with 8 zero neighbors + itself)
assert!(s[12] < 100);
assert!(s[12] > 0);
}
#[test]
fn constant_image_unchanged() {
let buf = TypedBuffer::U8(vec![50; 25]);
let result = mean_filter(&buf, 5, 5, 1);
let s = result.as_u8_slice().unwrap();
assert!(s.iter().all(|&v| v == 50));
}
}