Repository navigation
feat(cv): add MI-GAN image inpainting - #1573
Conversation
Native crop, paste and blendMasked ops (windowed copies, and a masked composite with the mask eroded and Gaussian-feathered so the seam blends), and on top of them createInpainter / useInpainter: run a MI-GAN style model (RGB in [0, 1] plus a 1 = known / 0 = hole mask) on the square crop around the painted region, then blend the output back into the full-resolution frame, following upstream's MIGAN_Pipeline. The public mask marks the hole with any non-zero pixel; the inversion to the graph's convention happens inside the task. An empty mask returns the input unchanged.
models.inpainting.MIGAN with the Places2 512 and 256 checkpoints and the FFHQ faces checkpoint, fp16 only, on XNNPACK, Core ML and Vulkan, plus the matching inpainting feature in the native libs map.
Camera integration moves to 12 so the task pages stay together.
Pick a photo, paint the mask with a brush over the viewport, run the model and compare with the original. The gesture-handler ScrollView lets the brush win over scrolling only above the photo. app.json declares NSLocalNetworkUsageDescription so a dev build on a physical iPhone can reach Metro and local model files.
The square the inpainting model runs on was found by scanning the mask pixel by pixel in JavaScript, which took 140 ms at 1080p and 750 ms at 12 MP on a Galaxy S23, more than the model itself. Add a `maskBounds` native op, the bounding box of a mask's non-zero pixels as an inclusive `xyxy` box or `null`, and make `computeInpaintingCrop` use it. The function keeps taking an image buffer, converted to grayscale the way `inpaint` does, and now also takes a tensor so the task scans the mask it has already uploaded. The scan drops to single milliseconds and the 12 MP end-to-end time from 1120 ms to 395 ms.
# Conflicts: # .cspell-wordlist.txt # packages/react-native-executorch/src/index.ts # packages/react-native-executorch/src/models.ts
This comment was marked as resolved.
This comment was marked as resolved.
….10.7 The model table on the inpainting guide linked to per-variant anchors on the generated `models` page, which only exists down to the family level, so the docs build failed on broken anchors. Link the three rows to `#inpaintingmigan` like the other nested families do. Pin `nativeLibsVersion` to 0.10.7, the current libs release.
|
@borys3kk I uploaded the models on HF: https://huggingface.co/software-mansion/react-native-executorch-migan/tree/main, please wire them and check if everything works correctly and the speed you measured is the same. Also please update PR description. |
…askBounds Per review: the model sees the whole photo resized to its input, and the output is resized back and composited with the native blendMasked. The mask is area-resized so thin strokes survive the downscale. Cropping around the hole is left to the caller.
The inpainting blend eroded the hole by 1 px, so any stroke thinner than 3 px eroded to nothing and inpaint() silently returned the input. With the full-frame pipeline the fill is upsampled and its edge already soft, so the MI-GAN presets drop the erosion, and blendMasked falls back to the un-eroded region when shrink would empty it. The Gaussian feather also spread the weight about 1 px outside the mask, contradicting the promise that pixels outside it come back unchanged; the weight is now zeroed outside the mask so the ramp runs inward only. Docs: an empty mask still runs the model.
Per review: inpaint() takes a 'gray' mask only (MaskBuffer) and rejects other formats with INVALID_ARGUMENT instead of converting them, which drops a per-call tensor. The mask chain scales 0 -> 1 and non-zero -> < 1 in one normalize and thresholds just under 1, so the second normalize and one scratch tensor go away; the final copy stays as the batch reshape. The example's MaskCanvas reads the strokes back as Gray_8 (red channel of the RGBA read-back as a fallback).
Per review: the frame-sized buffers are an explicit auxTensors list released in finally, as in paddleOcr, instead of a scratch helper that grew a list behind the chain.
Per review: InpainterOptions now extends ImagePreprocessorOptions the way StyleTransferOptions and DepthEstimatorOptions do (resizeMode fixed to 'stretch', normalizeOpts and outNormalizeOpts exposed), so a model with a different input normalization can be wired without library changes. The hand-rolled image chain and its four tensors go away; the mask keeps its own area-resize and threshold path.
Per review, matching normalizeOpts and outNormalizeOpts next to it.
|
Addressed the review in five commits (3120d76 … 28b136d), see the inline replies. On top of that, an end-to-end run on an iPhone 17 against the published HF models (all six iOS variants, eight real photos incl. two 12 MP, plus synthetic edge cases) turned up two problems in the blend, both fixed in 3120d76:
Also in the docs: an empty mask returns the input unchanged but still runs the model (the early return went away with the crop). Whole |
msluszniak
left a comment
There was a problem hiding this comment.
PR description: inpainter.test.ts has 12 tests now (it says 17), and the end-to-end numbers from your comment could go there too.
An inward-only feather leaves a 1 px stroke 60% original, and at the photo's resolution the 5-tap ramp hides no seam anyway: the fill is already soft from upsampling. Presets use feather: 0; docs match.
…onverting An RGBA input was already the frame, yet a second full-resolution RGBA buffer (48 MB at 12 MP) was allocated and never written.
|
PR description updated: 12 tests, and the |
…as a conditional entry Same shape as the other CV tasks; the RGBA buffer is still only allocated when the input needs converting.
Description
Adds image inpainting on MI-GAN: paint a mask over a photo and the model fills the region with plausible content (object removal, cleanup).
cvop:blendMasked(masked composite, with optional erosion and inward feathering of the mask; the presets use neither, since the upsampled fill is already soft), with host C++ tests.createInpainter/useInpainter. The task resizes the whole frame to the model, runsforward, resizes the output back and blends it into the full-resolution frame through the mask. Cropping around the hole is left to the caller. The public mask marks the hole with any non-zero pixel; the inversion to the graph's1 = knownconvention happens inside the task. An empty mask returns the input unchanged.models.inpainting.MIGANwithSIZE_512,SIZE_256(Places2) andFACES_256(FFHQ), fp16 only, on XNNPACK, Core ML and Vulkan. Newinpaintingfeature in the native libs map.MaskCanvas), result/original toggle.app.jsongainsNSLocalNetworkUsageDescriptionso a dev build on a physical iPhone can reach Metro and local files.The exports come from export-scripts MR !31 and are published to
software-mansion/react-native-executorch-miganatv0.11.0. Native libs are pinned to0.10.7, which the XNNPACK exports need.Introduces a breaking change?
Type of change
Tested on
Testing instructions
apps/computer-vision→ Image Inpainting.Library:
yarn workspace react-native-executorch test(12 new tests ininpainter.test.ts),scripts/run-native-tests.sh -R CvOpsTest(needs OpenCV 4; Homebrew'sopencvis 5.x and does not link against the vendored 4.x headers, useopencv@4withOpenCV_DIR).Measured with the published
v0.11.0files and native libs 0.10.7,forwardonly, medians after warm-up:forwardFaces 256 runs at the same speed as 256. Whole
inpaint()with MI-GAN 512 on a 708x424 photo: ~35 ms Core ML / ~89 ms XNNPACK on the iPhone 17, ~200-260 ms Vulkan / ~260-300 ms XNNPACK on the S23. At 12 MP the full-resolution resize and blend dominate: 100-250 ms on the iPhone, 0.4-1.6 s on the S23 with a large run-to-run spread.End-to-end on both devices (all six variants per platform, eight real photos incl. two at 12 MP, synthetic edge cases down to 1x1 and a single-pixel hole, RGB vs RGBA input, sync vs async, two calls in flight, dispose): pixels outside the mask come back unchanged on every case, outputs are deterministic, and inside the mask they are within 1-9 levels (mean) of upstream's reference results on the
places2_512examples. Vulkan and XNNPACK outputs on the S23 differ by about one level on average (fp16 on GPU vs CPU). With upstream'ssheep_mask.pngexample the output matches upstream'ssheep_out.png.Screenshots
(to be added)
Related issues
#1373
Checklist
Additional notes
@shopify/react-native-skiadeprecatesSkPath.moveTo/lineTo; the mask component usesPathBuilder.cspellonPATH(node_modules/.bin) and all workspaces built (yarn prepare), otherwisespell-check-otherandtypesfail for reasons unrelated to the change.