Skip to main content

lama/
lib.rs

1use hf_hub::api::sync::Api;
2use image::{DynamicImage, GenericImageView};
3use ort::{inputs, session::Session, value::TensorRef};
4use std::cmp::{max, min};
5
6#[derive(Debug)]
7pub struct Lama {
8    model: Session,
9}
10
11impl Lama {
12    pub fn new() -> anyhow::Result<Self> {
13        let api = Api::new()?;
14        let repo = api.model("mayocream/lama-manga-onnx".to_string());
15        let model_path = repo.get("lama-manga.onnx")?;
16
17        let model = Session::builder()?
18            .with_optimization_level(ort::session::builder::GraphOptimizationLevel::Level3)?
19            .commit_from_file(model_path)?;
20
21        Ok(Lama { model })
22    }
23
24    pub fn inference(
25        &mut self,
26        image: &DynamicImage,
27        mask: &DynamicImage,
28    ) -> anyhow::Result<DynamicImage> {
29        // Use tiled inference universally for quality and scalability.
30        // Defaults: 512 tile, 128 overlap for smooth seams.
31        self.inference_tiled(image, mask, 512, 128)
32    }
33
34    /// Inpaint an image using tiled inference with multiresolution-style blending.
35    ///
36    /// - `tile_size`: size of model input tiles (typically 512 for LaMa).
37    /// - `overlap`: pixels of overlap between neighboring tiles (e.g., 128).
38    ///
39    /// The final result preserves original pixels outside the mask and blends
40    /// inpainted tiles smoothly inside the masked regions.
41    pub fn inference_tiled(
42        &mut self,
43        image: &DynamicImage,
44        mask: &DynamicImage,
45        tile_size: u32,
46        overlap: u32,
47    ) -> anyhow::Result<DynamicImage> {
48        let (w, h) = image.dimensions();
49        let tile = max(32, tile_size); // guard against tiny tiles
50        let ovl = min(overlap, tile.saturating_sub(1));
51        let stride = tile.saturating_sub(ovl);
52
53        // Accumulators for weighted blending
54        let mut acc_r = vec![0f32; (w * h) as usize];
55        let mut acc_g = vec![0f32; (w * h) as usize];
56        let mut acc_b = vec![0f32; (w * h) as usize];
57        let mut acc_w = vec![0f32; (w * h) as usize];
58
59        // Convert inputs to RGB/Gray for faster pixel access
60        let img_rgb = image.to_rgb8();
61        // Interpret mask: >0 means inpaint region
62        let mask_luma = mask.to_luma8();
63
64        // Iterate tiles
65        let mut y0 = 0u32;
66        while y0 < h {
67            let mut x0 = 0u32;
68            while x0 < w {
69                let x1 = min(x0 + tile, w);
70                let y1 = min(y0 + tile, h);
71                let eff_w = x1 - x0;
72                let eff_h = y1 - y0;
73
74                // Skip tiles with no masked pixels in effective region
75                let mut any_masked = false;
76                'mask_check: for yy in 0..eff_h {
77                    for xx in 0..eff_w {
78                        if mask_luma.get_pixel(x0 + xx, y0 + yy)[0] > 0 {
79                            any_masked = true;
80                            break 'mask_check;
81                        }
82                    }
83                }
84                if !any_masked {
85                    x0 = x0.saturating_add(stride).min(w);
86                    continue;
87                }
88
89                // Build 512x512 (or tile x tile) reflected-padded tiles for image & mask
90                let (tile_img, tile_mask) =
91                    extract_reflect_padded_tile(&img_rgb, &mask_luma, x0, y0, eff_w, eff_h, tile);
92
93                // Run model on tile
94                let tile_out = self.infer_tile_512(&tile_img, &tile_mask)?; // RGB tile x tile
95
96                // Extract effective region (top-left eff_w x eff_h)
97                let mut tile_out_crop = image::RgbImage::new(eff_w, eff_h);
98                for yy in 0..eff_h {
99                    for xx in 0..eff_w {
100                        tile_out_crop.put_pixel(xx, yy, *tile_out.get_pixel(xx, yy));
101                    }
102                }
103
104                // Compute blending weights for this tile (raised-cosine over overlap)
105                let weights = make_tile_weights(eff_w, eff_h, ovl);
106
107                // Multiply weights by mask>0 to ensure we only blend inpaint areas
108                // (softening via raised-cosine already smooths across tiles)
109                // Accumulate
110                for yy in 0..eff_h {
111                    for xx in 0..eff_w {
112                        let global_x = x0 + xx;
113                        let global_y = y0 + yy;
114                        let idx = (global_y * w + global_x) as usize;
115
116                        let m = if mask_luma.get_pixel(global_x, global_y)[0] > 0 {
117                            1.0f32
118                        } else {
119                            0.0f32
120                        };
121
122                        if m == 0.0 {
123                            continue;
124                        }
125
126                        let wgt = weights[(yy * eff_w + xx) as usize] * m;
127                        if wgt <= 0.0 {
128                            continue;
129                        }
130
131                        let p = tile_out_crop.get_pixel(xx, yy);
132                        acc_r[idx] += p[0] as f32 * wgt;
133                        acc_g[idx] += p[1] as f32 * wgt;
134                        acc_b[idx] += p[2] as f32 * wgt;
135                        acc_w[idx] += wgt;
136                    }
137                }
138
139                x0 = x0.saturating_add(stride).min(w);
140            }
141            y0 = y0.saturating_add(stride).min(h);
142        }
143
144        // Compose final image: use original outside mask, blended result inside
145        let mut out = img_rgb.clone();
146        for y in 0..h {
147            for x in 0..w {
148                let idx = (y * w + x) as usize;
149                if mask_luma.get_pixel(x, y)[0] == 0 {
150                    continue; // keep original
151                }
152                let wsum = acc_w[idx];
153                if wsum > 0.0 {
154                    let r = (acc_r[idx] / wsum).clamp(0.0, 255.0) as u8;
155                    let g = (acc_g[idx] / wsum).clamp(0.0, 255.0) as u8;
156                    let b = (acc_b[idx] / wsum).clamp(0.0, 255.0) as u8;
157                    out.put_pixel(x, y, image::Rgb([r, g, b]));
158                }
159            }
160        }
161
162        Ok(DynamicImage::ImageRgb8(out))
163    }
164}
165
166/// Extract a tile of size (tile x tile) using reflection padding as needed from (x0..x0+eff_w, y0..y0+eff_h).
167fn extract_reflect_padded_tile(
168    img: &image::RgbImage,
169    mask: &image::GrayImage,
170    x0: u32,
171    y0: u32,
172    eff_w: u32,
173    eff_h: u32,
174    tile: u32,
175) -> (image::RgbImage, image::GrayImage) {
176    let mut out_img = image::RgbImage::new(tile, tile);
177    let mut out_msk = image::GrayImage::new(tile, tile);
178
179    // copy valid region to top-left
180    for yy in 0..eff_h {
181        for xx in 0..eff_w {
182            let src_x = x0 + xx;
183            let src_y = y0 + yy;
184            out_img.put_pixel(xx, yy, *img.get_pixel(src_x, src_y));
185            out_msk.put_pixel(xx, yy, *mask.get_pixel(src_x, src_y));
186        }
187    }
188
189    // reflect-pad on right
190    for yy in 0..eff_h {
191        for xx in eff_w..tile {
192            let rx = if eff_w == 0 {
193                0
194            } else {
195                eff_w - 1 - ((xx - eff_w) % eff_w)
196            };
197            let p = *out_img.get_pixel(rx, yy);
198            let m = *out_msk.get_pixel(rx, yy);
199            out_img.put_pixel(xx, yy, p);
200            out_msk.put_pixel(xx, yy, m);
201        }
202    }
203    // reflect-pad on bottom
204    for yy in eff_h..tile {
205        let sy = if eff_h == 0 {
206            0
207        } else {
208            eff_h - 1 - ((yy - eff_h) % eff_h)
209        };
210        for xx in 0..tile {
211            let p = *out_img.get_pixel(xx, sy);
212            let m = *out_msk.get_pixel(xx, sy);
213            out_img.put_pixel(xx, yy, p);
214            out_msk.put_pixel(xx, yy, m);
215        }
216    }
217
218    (out_img, out_msk)
219}
220
221/// Raised-cosine feathering weights within a tile effective region.
222/// Weight = 1 in the center, smoothly drops to 0 across an overlap/2 band near borders.
223fn make_tile_weights(w: u32, h: u32, overlap: u32) -> Vec<f32> {
224    use std::f32::consts::PI;
225    let mut weights = vec![1.0f32; (w * h) as usize];
226    let half = (overlap as f32) / 2.0;
227    if overlap == 0 {
228        return weights;
229    }
230
231    for y in 0..h {
232        for x in 0..w {
233            let dx = min(x, w - 1 - x) as f32;
234            let dy = min(y, h - 1 - y) as f32;
235            let d = dx.min(dy);
236            let wxy = if d >= half || half <= 1e-3 {
237                1.0
238            } else {
239                // raised cosine from 0 at border to 1 at distance >= half
240                let t = (d / half).clamp(0.0, 1.0);
241                0.5 * (1.0 - (PI * (1.0 - t)).cos())
242            };
243            weights[(y * w + x) as usize] = wxy;
244        }
245    }
246    weights
247}
248
249impl Lama {
250    /// Run a single-tile inference assuming a square tile of size 512 (or arbitrary tile size equal on both dims)
251    /// No resizing/aspect logic, inputs must match model size.
252    fn infer_tile_512(
253        &mut self,
254        tile_img: &image::RgbImage,
255        tile_mask: &image::GrayImage,
256    ) -> anyhow::Result<image::RgbImage> {
257        let (tw, th) = tile_img.dimensions();
258        // Model is trained for 512x512; allow other sizes if the model supports dynamic shapes.
259        let w = tw as usize;
260        let h = th as usize;
261
262        let mut image_data = ndarray::Array::zeros((1, 3, h, w));
263        for y in 0..th {
264            for x in 0..tw {
265                let p = tile_img.get_pixel(x, y);
266                let fx = x as usize;
267                let fy = y as usize;
268                image_data[[0, 0, fy, fx]] = (p[0] as f32) / 255.0;
269                image_data[[0, 1, fy, fx]] = (p[1] as f32) / 255.0;
270                image_data[[0, 2, fy, fx]] = (p[2] as f32) / 255.0;
271            }
272        }
273
274        let mut mask_data = ndarray::Array::zeros((1, 1, h, w));
275        for y in 0..th {
276            for x in 0..tw {
277                let m = tile_mask.get_pixel(x, y)[0];
278                let fx = x as usize;
279                let fy = y as usize;
280                mask_data[[0, 0, fy, fx]] = if m > 0 { 1.0f32 } else { 0.0f32 };
281            }
282        }
283
284        let inputs = inputs![
285            "image" => TensorRef::from_array_view(image_data.view())?,
286            "mask" => TensorRef::from_array_view(mask_data.view())?,
287        ];
288        let outputs = self.model.run(inputs)?;
289        let output = outputs["output"].try_extract_array::<f32>()?;
290        let output = output.view();
291
292        let mut out_img = image::RgbImage::new(tw, th);
293        for y in 0..th {
294            for x in 0..tw {
295                let r = (output[[0, 0, y as usize, x as usize]] * 255.0).clamp(0.0, 255.0) as u8;
296                let g = (output[[0, 1, y as usize, x as usize]] * 255.0).clamp(0.0, 255.0) as u8;
297                let b = (output[[0, 2, y as usize, x as usize]] * 255.0).clamp(0.0, 255.0) as u8;
298                out_img.put_pixel(x, y, image::Rgb([r, g, b]));
299            }
300        }
301        Ok(out_img)
302    }
303}