A desktop tool for bi-temporal remote-sensing image change inspection — flicker comparison, 256-color mask annotation, and offline AI-assisted selection.
English | 简体中文
Annotating bi-temporal change-detection data means flipping back and forth between two phases of large orthoimages, spotting what changed, and painting per-class masks. Generic image editors don't fit this loop: they lack phase-synced comparison, and they don't tie mask pixels to a label color system. DT-Change-Inspector was built for exactly this loop and is used in daily annotation production.
Everything runs fully offline — images never leave the machine.
Bi-temporal comparison
- A/B phases side by side, or rapid flicker at 5 speeds (150 / 250 / 350 / 600 / 1000 ms)
- Swap phase order per image (
Space/Tab), or physically swap the A/B files on disk (X) to turn "disappeared" into "appeared" - Pan / zoom with middle-drag and wheel; reset with
R
Annotation
- 256 labels, each with its own color, name, and a custom hotkey (single keys or combos like
Ctrl+Alt+1) - Polygon annotation (
Q) with guide line and edge snapping; Enter or right-click to commit,Backspaceto remove the last point - Connected-region delete by color (
E), plusShift+click to erase with a polygon - Labels live in the per-user config directory (
mask_labels.json); the bundled file is only the first-run seed, so upgrades do not overwrite custom labels
AI-assisted selection (MobileSAM, offline)
- Bundled MobileSAM ONNX models (encoder + quantized decoder) run locally via
onnxruntime-node— no Python, no GPU, no upload Fto start: left-click inside the target as a positive point,Shift+click / right-click as negative points to trim the candidate,Enterto write it into the current label color- The first click computes the image embedding; follow-up point refinements are much faster
- SAM outlines what the clicks suggest — whether it is a real temporal change stays a human decision
Safe data workflow
Deletemoves the current A/B/Mask group intodeleted_pairs/(recycle bin);Urestores it, including after an application restartCtrl+Zundoes the latest mask edit on the current image within the current session; when no mask edit is available it falls back to restoring the last deleted groupShift+Deleteremoves permanently with confirmation- Masks use the same stem as the A/B pair and are written as lossless PNG; legacy JPG/BMP masks are converted to PNG on their first edit
Production workflow
- Dataset-level review state in
review_state.json; the status bar shows annotated/reviewed counts;Njumps to the next unprocessed image,Mmarks or clears “reviewed / unchanged” - Load summary reports A/B/Mask file counts and unmatched files; the sibling Mask folder is auto-detected; recent datasets and drag-and-drop dataset roots are supported
- Label configurations can be imported/exported; changing a label color warns that existing masks are not recolored
- Current-mask statistics break pixels down by label and export to CSV, with off-palette colors listed separately
- Window bounds are remembered; the tray menu opens the local log directory
Built for 4K imagery
- Rapid page-through renders only the frame you stop on, renders are cancelable, and interaction uses a lightweight preview that snaps back to full resolution when you pause
- Mask fill and red-outline layers are cached per view, so flicker, opacity changes and mask toggling reuse them instead of reprocessing every pixel
- Recently decoded images use a 384 MB byte-budget LRU (preview bitmaps included), so memory stays bounded on large datasets
- Mask edits use a main-process raw-mask cache and bounding-box compositing; large connected-region deletes run asynchronously
- Canvas renders at the device pixel ratio for crisp imagery on 125%/150%/200% Windows scaling
- SAM previews and IPC use the model-resolution mask instead of expanding to the source size
dataset/
├─ A/ # phase-1 images
├─ B/ # phase-2 images (same filenames as A/)
├─ Mask/ # output masks (same stem as A/B, lossless PNG)
├─ review_state.json # reviewed/unchanged state (optional, generated)
└─ deleted_pairs/ # recycle folder + manifest.json (generated)
Supported formats: png, jpg, jpeg, tif, tiff, bmp, webp.
Existing Mask folders and mask_labels.json from older versions work as-is.
When A and B have different pixel dimensions, annotation is restricted to phase A to prevent silent mask-grid distortion.
- A label's color is its class value: annotation pixels are written in the label's exact RGB.
- New masks are always written as lossless PNG, even when the source pair is
.jpg(tile.jpg→Mask/tile.png). - A legacy
.jpg/.jpeg/.bmpmask is copied into a lossless PNG on its first edit; the original file is kept. - If an existing mask has a different size than phase A, the app asks for confirmation before nearest-neighbor rescaling it to the A grid.
- Background is always pure black
(0, 0, 0). - The red outline shown in the UI is view-only and never written to the mask.
- Any non-black color can be used as a label, including pure white.
- Legacy JPEG masks may contain palette drift from earlier lossy writes. Audit them with
pnpm mask:convert <MaskDir> --dry-run, then run the same command without--dry-runto rewrite them as exact-color PNG.
Grab the latest installer (NSIS, ~141 MB with models bundled) from the Releases page and follow the wizard. The app maximizes on first launch and then remembers its window bounds. Closing the window hides it to the system tray — right-click the tray icon to quit for real.
# Electron client
pnpm install
# Python side (legacy tkinter viewer / mask_core.py)
pip install -r requirements.txt
# Run
pnpm startpnpm test # Node unit tests
pnpm test:smoke # in-app self test
pnpm mask:convert <MaskDir> --dry-run # audit mask palette
pnpm mask:convert <MaskDir> # convert legacy JPG/BMP masks to PNG
pnpm build # Windows NSIS installer -> release/- Launch and pick the A, B and Mask directories.
- Flicker (
B) or compare (V) to locate a change. - Switch to the phase where the target is visible (
Space/Tab) — B phase for additions, A phase for removals — and pressF. - Click inside the target, refine with negative clicks if needed,
Enterto commit with the current label. - Page through with
A/Dand repeat. Toggle mask visibility withSto review.
| Key | Action |
|---|---|
A / ← |
Previous image |
D / → |
Next image (rapid paging renders only the final frame) |
N |
Next unannotated/unreviewed image |
M |
Mark or clear the current image's review state |
Space / Tab |
Toggle A/B phase |
B |
Flicker A/B |
V |
Side-by-side comparison |
X |
Swap A/B files on disk (press again to revert) |
S |
Show / hide mask |
C |
Label manager |
Q |
Polygon annotation |
E |
Delete annotation (click a connected region; Shift+click erases with a polygon) |
F |
AI selection (MobileSAM) |
Delete |
Move group to deleted_pairs/ |
Shift+Delete |
Delete permanently (confirmed) |
U |
Undo the last delete |
Ctrl+Z |
Undo the latest mask edit in this session; fall back to undoing a delete |
R / 0 |
Reset view |
+ / - / wheel |
Zoom |
| Middle-drag | Pan |
Enter |
Commit annotation |
Backspace |
Remove last point |
Esc |
Cancel current operation |
Per-label hotkeys are recorded in the label manager (C) and work during polygon editing without losing painted points.
├─ electron/ # Electron app
│ ├─ main.js # main process: windows, tray, dialogs, mask IO
│ ├─ core/ # pairs, labels, shortcuts, mask-service, review, logger, window state
│ ├─ models/ # MobileSAM ONNX weights + license (committed)
│ └─ tests/ # node --test unit tests
├─ .github/workflows/ # CI: unit tests + Electron self test
├─ scripts/convert-masks.js # batch palette audit / legacy mask conversion
├─ pair_change_viewer.py # legacy tkinter version (v2.x), kept for reference
├─ mask_core.py # shared mask logic for the Python side (pytest)
├─ mask_labels.json # default 256-label palette
├─ .requirements/ # per-feature requirement docs, one folder per change
└─ release/ # build output (not committed)
Electron 43 · onnxruntime-node 1.29 · sharp 0.35 · Node unit tests + pytest · electron-builder / NSIS
Code is released under the MIT License. The bundled MobileSAM weights follow their own license — see electron/models/LICENSE-MobileSAM-in-the-Browser.txt. MobileSAM: ChaoningZhang/MobileSAM.
