このページはまだ翻訳されていません — 英語版を表示しています。

Crop & dust

The Crop tool and the Eraser tool live on the Frame toolbar as two separate panels. The Crop panel handles geometry — aspect ratio, orientation, 90° rotation, fine straighten, and flip. The Eraser panel handles defect removal — manual heal brush, infrared (IR) automatic dust removal, AI inpainting (MI-GAN), and the on-device AI dust & hair detector. Everything described here runs locally on your machine; no image data is sent anywhere.

Crop & transform

The Crop panel opens when you click the Crop tool in the Frame toolbar. The crop box on the image is live: dragging any edge or corner resizes it; dragging inside the box pans it. Holding Shift while dragging locks the aspect ratio. Press Enter (or click outside the box) to commit; press Esc to discard changes.

The Aspect ratio drop-down lets you constrain the crop box to a fixed ratio. Presets are grouped by film format:

  • Original — unconstrained; the box can be any shape (preset id "original", ratio: null).
  • 135 — 36 × 24 (3:2).
  • 120 — 6 × 4.5 (4:3), 6 × 6 (1:1), 6 × 7, 6 × 8, 6 × 9.
  • Large format — 4 × 5, 5 × 7, 8 × 10.
  • Screen · Print — 1 × 1, 16 × 9, 16 × 10, 8.5 × 11.

Selecting a preset snaps the box to that ratio immediately. If you then resize the box freehand, the aspect changes to "custom" (shown in the drop-down only while active). Saved in the CropRect as the aspect field (the preset id string).

The Orientation button (landscape/portrait icon) swaps the width and height of the constrained ratio — toggling a landscape 3:2 box to a portrait 2:3 one, for example. The current orientation is stored as orientation: "landscape" | "portrait" in the CropRect.

The Transform row contains four icon buttons:

  • Rotate left (⌘/Ctrl [) — rotates the image 90° counter-clockwise. Each click increments rot90 by −1 (mod 4); the range is 0–3 clockwise quarter-turns.
  • Rotate right (⌘/Ctrl ]) — rotates 90° clockwise, incrementing rot90 by +1 (mod 4).
  • Flip horizontal — mirrors the image left-to-right, toggling flipH.
  • Flip vertical — mirrors top-to-bottom, toggling flipV.

Flips and rotations interact: the backend applies flips before the quarter-turns (flip_h → flip_v → rot90 clockwise), so flipping a rotated image toggles the correct axis without disturbing the rotation count.

The Straighten slider provides a fine rotation adjustment from −45° to +45° in 0.1° steps, double-clicking the value resets it to 0. This is stored as angle in the CropRect. It is applied as a sub-pixel rotation of the source image around its center (sampled before the crop window) and does not affect the crop box dimensions.

The Reset button clears all crop settings to the full-frame original: rect back to {x:0, y:0, w:1, h:1}, rot90 to 0, flipH/flipV to false, angle to 0, and aspect to "original". It does not affect any other develop parameters.

The complete crop data model (CropRect) written to disk per image contains: rect (a normalized rectangle with x, y, w, h each in 0–1), aspect (preset id string), orientation, rot90 (0–3), flipH (boolean), flipV (boolean), and angle (degrees). Persisted via api.saveCrop().

Under the hood

Orientation transform order (convert.rs): the backend orient pipeline is flip_h → flip_v → rot90 clockwise quarter-turns. On the front end, transforms.ts mirrors this so that the GPU preview overlay matches the rendered output exactly. When rot90 is odd (1 or 3), flipping the displayed view on one axis is equivalent to flipping the pre-rotation image on the other axis, so flipOrient() also negates rot90 mod 4 to keep the displayed crop box aligned: rot90_new = (4 − rot90) % 4, toggling the appropriate flipH/flipV flag.

Normalized rect math: rotateRectCW(r) maps {x, y, w, h} to {x: 1−y−h, y: x, w: h, h: w}, and rotateRectCCW to {x: y, y: 1−x−w, w: h, h: w}. The straighten angle is applied as a rotation of the source image around its center before the crop window is sampled, so the crop box always refers to the post-angle coordinate space. Wire format to the backend uses image_crop: [x, y, w, h] (a normalized 4-tuple), rot90, flip_h, flip_v, and angle.

Manual eraser brush

The Eraser panel opens from the Frame toolbar. The manual brush lets you paint over individual dust spots or scratches and have them healed immediately (or via MI-GAN — see below).

The Brush size slider sets the brush radius normalized to the image width, from 0.005 (very small) to 0.2 (large). The displayed value is the radius as a percentage of image width. You can also scroll the mouse wheel over the image to resize the brush in real time. Each click or drag records a DustStroke: a polyline of normalized points plus the radius r. Press ⌘Z (macOS) to undo the last stroke. Strokes are stored in DustEdits.strokes and persisted via api.saveDust().

In normal mode, each stroke is healed live in the GPU preview as you release the pointer. The inpainted area is blended using a content-aware clone algorithm driven by the surrounding texture.

The Show heal spots checkbox (field showSpots) overlays a small eraser-icon marker at the centroid of every committed stroke, making it easy to see and click-to-delete individual spots. Click a marker to remove that stroke immediately.

The Zoom to area button arms a marquee zoom: drag a rectangle over the image to magnify that region for precise brush work. Once zoomed, the button changes to Reset view to return to the full image.

The Reset button clears all manual strokes (DustEdits.strokes → []) and resets the AI-applied flag. It does not affect IR removal or auto dust settings.

Under the hood

Stroke model (dust.ts): a DustStroke is { points: DustPoint[], r: number } where each DustPoint is { x: number, y: number } in the displayed image's normalized coordinate space (0–1 in both axes), and r is the brush radius normalized to the displayed image width. The centroid of a stroke (strokeCentroid()) is the arithmetic mean of all points — this is the anchor for the heal-spot marker. Wire format to the backend: each stroke is serialized as { points: [[x,y], …], r } (2-tuples, not objects) via wireDust() in api.ts. Individual spot deletion uses removeStrokeAt(dust, i), which filters the strokes array by index and clears aiApplied so any pending MI-GAN mask must be re-applied. Screen radius in pixels = r × imageWidthPx × zoomFactor.

AI fill (MI-GAN)

Enabling the AI fill (MI-GAN) checkbox switches the brush into mask mode. Strokes are no longer healed live: instead they appear as a semi-transparent red overlay on the image, showing exactly which areas will be inpainted. When you are satisfied with the mask, click AI erase to run MI-GAN — a machine-inpainting model that reconstructs the masked pixels from surrounding context. The model runs on-device; no image data leaves your computer.

The AI erase button is enabled only when there are uncommitted strokes and the current mask has not yet been applied. It shows a spinner while the model is running and is labelled "Erasing…" during processing. After a successful run, aiApplied is set to true and the button becomes inactive. Adding or removing any stroke clears aiApplied and re-enables the button so the new mask can be applied. Undo (⌘Z) removes the last stroke and also clears the applied flag.

Under the hood

State machine (dust.ts): DustEdits.brushMigan governs whether the GPU preview draws live heals or a mask overlay. addStroke() and undoStroke() both reset aiApplied = false, ensuring the mask is never shown as "done" after a change. The "AI erase" dispatch fires an aiErase event that the parent view handles by invoking the MI-GAN backend command with the current stroke list. setBrushMigan() also clears aiApplied when the mode is toggled.

Infrared (IR) dust removal

Some dedicated film scanners capture a fourth channel alongside R, G, B using near-infrared light. Because silver-halide grain is nearly transparent to infrared while dust and scratches scatter it strongly, this channel provides a near-perfect dust mask at no cost to image quality.

When the loaded scan contains an IR plane, the Remove dust (IR) toggle is enabled. Clicking it turns IR removal on or off; the current state is shown as "On" or "Off" badge next to the label. When IR removal is active, the Sensitivity slider (0–100, default 50) controls how aggressively the IR channel is used as a mask. Higher values remove more — including finer dust — at the risk of clipping real detail at the grain level; lower values are more conservative. Both fields are stored in DustEdits.irRemoval as enabled and sensitivity, and are passed to the backend render as the irRemoval parameter.

When the scan has no IR channel, the button is disabled and labelled with a "soon" badge (the tooltip reads "Requires an infrared scan channel"). IR removal is scanner-dependent: Nikon Coolscan and Minolta Scan Multi scanners with ICE support produce IR-capable TIFF files; most flatbed and DSLR scans do not.

Under the hood

Wire format (api.ts): IR state is passed to every render call via the irRemoval: IrRemoval parameter ({ enabled: boolean, sensitivity: number }). The backend uses the IR plane as an alpha mask for the heal composite, blending the reconstructed (dust-free) pixels with the original according to the IR signal strength scaled by the sensitivity value. The hasIr prop on EraserPanel is set by the parent view after probing the loaded image's channel count.

AI dust & hair removal (auto)

The AI Dust & Hair Removal sub-panel runs an on-device machine-learning detector to find dust spots and hairs across the whole image automatically, then inpaints them using the same MI-GAN model as the manual AI fill. Everything runs locally — models are downloaded once and stored on your machine; no image is uploaded.

The first time you open this panel, OpenEnlarge checks whether the AI models are installed. If not, it shows a download prompt with the approximate model size in MB. Click Download AI models to fetch them; a progress bar tracks the download. Once installed, the panel shows a Sensitivity slider (0–100, default 50) and the Detect & remove button.

The Sensitivity slider controls how aggressively the detector flags pixels as defects. At higher values, smaller and fainter spots are detected; at lower values only prominent defects are flagged. The slider commits on pointer release (not while dragging), triggering a re-run of the detection and inpainting. Sensitivity is stored in DustEdits.autoDust.sensitivity.

Clicking Detect & remove runs the full detector + inpainting pipeline on the current image. The button shows a spinner and is disabled while processing. After completion, a toast reports how many distinct spots were removed (or "no dust found" if the image was clean). The toggle state DustEdits.autoDust.enabled controls whether the auto-heal is displayed on the main preview. Individual auto-detected spots that you want to keep can be excluded by clicking them in the "Show heal spots" overlay — those exclusions are stored in DustEdits.autoDustExclusions.

Under the hood

API (api.ts): api.autodustStatus() returns { installed: boolean, downloadBytes: number }. api.downloadAutodust() triggers the model download; progress is streamed via the autodust://download-progress Tauri event ({ received, total } bytes). After a detection run the backend emits autodust://result with { count: number } — the count of distinct spots removed. The autoDustInstalled store (store.ts) caches the installed state across panel mounts so the download prompt does not re-appear. Auto-dust state in DustEdits: autoDust: { enabled: boolean, sensitivity: number }; exclusion points: autoDustExclusions: DustPoint[] (each a normalized { x, y } centroid added via addExclusion()).

Next: AI tools →

OpenEnlarge — オープンソースのフィルムスキャン編集 · MITライセンス