Fixes mirrored slice exports by making orientation explicit: - PhotoSlice.swapped flag persists a user left/right swap - swap_left_right() mirrors [TL,TR,BR,BL] -> [TR,TL,BL,BR] - normalize_top_left() honors the flag so squarize/reset_rotation no longer overwrite manual orientation - Swap L/R toolbar button wired through canvas state + controller - Color-code left (orange) vs right (cyan) corners and edges
6.7 KiB
ScanSlicer (fork) — Project Guide
Purpose of this file: give any LLM working in this repo enough context to be effective without re-reading everything. Keep it current as the code changes.
What this project is
ScanSlicer is a desktop Qt application (Python/PySide6) that detects multiple photos in a flatbed scan image, lets the user correct each box manually, then exports perspective-corrected crops. Inspired by PhotoSlicer (Francesco Vannini). Runs on Linux over Wayland or X11.
This repo is a local fork of macrodev8/ScanSlicer (upstream owner inactive). License: GNU GPL v3 (must be preserved). Attribution: keep upstream credit and the "inspired by PhotoSlicer" note.
Goals of this fork
- Fix the mirrored-export orientation bug and add an explicit left/right orientation workflow (implemented, see below). This is the primary patch.
- Keep the app usable on the maintainer's own photo scans; afterwards only minimal maintenance is expected.
- Push to the maintainer's Forgejo instance at
code.dgerdesmann.de(no GitHub fork). Keep licensing, attribution, and repository description proper.
The maintainer is primarily an R user and new to Python/Git. Changes should be small, self-checked, clean, and secure; do not assume the maintainer can vet Python code.
The orientation fix (implemented)
The bug: exported slices could come out mirrored (left/right swapped). Root
cause is PhotoSlice.normalize_top_left() (in slices.py), which re-derives
"top" from geometry — it picks a top-left corner and resolves winding so edge
0→1 is the "most horizontal" top edge. That inference is ambiguous, and because
squarize() and reset_rotation() both re-run normalize_top_left() at the
end, any manual correction was silently overwritten.
The fix adds an explicit left/right orientation workflow:
PhotoSlice.swapped— persistent flag (defaultFalse): "user has explicitly mirrored left/right".PhotoSlice.swap_left_right()— togglesswappedand reindexes the corners[TL,TR,BR,BL] -> [TR,TL,BL,BR](i.e.bbox[[1,0,3,2]]), a pure left/right mirror that keeps the top (edge 0→1) and bottom edges in place. NOTE: the earlier[0,3,2,1]reindex was wrong — it rotated the quad 90°.normalize_top_left()applies the same[1,0,3,2]reindex whenswappedis true (after its geometric inference), so manual orientation survivessquarize()/reset_rotation().- "Swap L/R" toolbar button (
ui_builders_preview.py) ->CanvasStateMixin.swap_active_slice_left_right()->ScanSlicerCanvasActionsMixin.swap_active_box_left_right(). - Visual indication in
CanvasRenderMixin.paintEvent()(canvas_render.py): corners color-coded left = orange, right = cyan; edges colored red = top (0→1), green = right (1→2), orange = bottom (2→3), and the left edge (3→0) follows lock state (blue locked / lightgreen unlocked).
Key implementation facts
PhotoSlice.bboxis a 4×2 numpy array in the order[top-left, top-right, bottom-right, bottom-left].Autoslicer.save_slice()(inautoslicer.py) builds the perspective transform assuming the[TL,TR,BR,BL]order. Do NOT change that contract without updating every consumer.PhotoSlice.squarize()andreset_rotation()callnormalize_top_left()at the end; theswappedflag makes that safe for manual orientation.
Repository structure
scanslicer/ application source (package)
entry.py CLI parsing, Wayland/X11 backend selection
main.py QtScanSlicer main window, theme, app bootstrap
controller*.py UI action handlers (status, canvas, detection,
image I/O, params)
ui_builders*.py widget construction (controls, preview, shared)
canvas*.py interactive canvas (interaction/render/state mixins)
slices.py PhotoSlice geometry model + polys_iou
autoslicer.py detection/export engine wrapper (save_slice)
autoslicer_detection.py detection pipelines, contour→box extraction
autoslicer_feedback.py auto-tune profiles, scoring, feedback persistence
params.py parameter model, defaults, presets
packaging/appimage/ AppImage recipe + build scripts (preferred deploy)
Dockerfile container runtime image
docker-compose.yml GUI container wiring
pyproject.toml package metadata + dependencies (uv.lock pinned)
docs/ user-guide, architecture, parameters, autotune,
runtime-config, appimage, development
Architecture in brief
QtScanSlicer(main.py) is aQMainWindowcomposed from UI builder mixins and controller mixins.QtSlicingCanvas(canvas.py) = interaction + render + state mixins; holds the image, zoom/pan state, and the list ofPhotoSliceobjects.- Background work (detection, auto-tune) runs on a single-worker
QThreadPool; results are delivered back to the UI via Qt signals. - Export flow:
save_all()(controller_image_io.py) loops over locked slices and callsAutoslicer.save_slice(bbox, path, params)for each.
Run / build
uv sync --frozen
uv run scanslicer /path/to/scans # or: python -m scanslicer
- AppImage (preferred):
bash packaging/appimage/build-appimage.sh - AppImage via Docker:
bash packaging/appimage/build-appimage-docker.sh - Docker runtime:
docker compose up --build - Dependencies (from
pyproject.toml): PySide6, opencv-python, numpy, Pillow. - Python >= 3.10; dev env is Python 3.12 in
.venv.
AppImage build: Docker context gotcha
The maintainer's Docker points at Docker Desktop (desktop-linux context). Its
bind-mount filesystem forbids chown, but appimage-builder needs root
ownership while extracting .deb packages, so the build fails with
dpkg-deb ... Cannot change ownership to uid 0, gid 0: Operation not permitted.
Workaround: run the Dockerized build against the native daemon (or switch
context with docker context use default):
DOCKER_HOST=unix:///var/run/docker.sock bash packaging/appimage/build-appimage-docker.sh
Output lands in appimage/dist/ScanSlicer-<version>-x86_64.AppImage (the
appimage/ dir is git-ignored).
Config / persistence
Runtime files live in ~/.config/scanslicer/ (or $XDG_CONFIG_HOME/scanslicer/)
by default: defaults.json, presets.json, autotune-feedback.json,
status.log. Paths are overridable via SCANSLICER_*_PATH env vars. In the
Docker setup these land in ./.scanslicer/ on the host.
Notes
- No automated test suite exists; validate changes by running the app on sample scans (startup, detection, save, auto-tune).