Architectural debt addressed in one pass:
- CropViewer drops its five local refs (rotationDeg, cropLeft/Top/Right
/Bottom) in favour of computed views over `store.cropRotate`. Drag and
rotation handlers write straight to the store via thin setters; the
store is now the canonical source of truth. localStorage is flushed on
commit boundaries (drag end, rotation change, unmount, Next), not on
every pointermove.
- `ImagePreTransform` deleted from `src/types/index.ts`. CorrectedImage-
Viewer now takes `crop?: { state: CropRotateState; srcW; srcH }` and
derives the pixel-space affine internally via a memoised `computed`.
Cuts the second reactive ref in MeasureViewer and removes the
redundant "type that exists only to ferry derived numbers across a
prop boundary."
- `crop-transform.ts` is now pure geometry. The DOM-bound
`renderRotatedCropped` moved to a new `src/lib/crop-render.ts`. The
pure module is now safe to import from a worker / test without a
canvas mock.
Bug fix shipped in the same commit because it fell out of the same
analysis:
- Zoom (wheel + pinch) anchored on a measurement-space point returned
by `screenToImg`, but `viewOffset = screen - bitmap * scale` expects a
bitmap-space point. With identity crop the two were equal, so the bug
was invisible until the user cropped — at that point each zoom step
shifted the image far off-screen ("canvas goes blank/black"). Pan was
fine because it never converted screen↔image. Fix: a tiny
`screenToBitmap` helper used by `onWheel` and the pinch branch of
`onTouchMove`. Measurement placement / drag still go through the
full `screenToImg` so coords stay in the canonical frame.
Skwik
Client-side image deskewing tool. Upload a photo taken at an angle, place reference measurements on known objects, and get a perspective-corrected output with real-world scale.
Everything runs in the browser -- no server, no uploads.
Example
![]() Before — angled shot |
![]() After — corrected perspective |
![]() Measured — annotations baked in |
|
How it works
- Upload a JPG or HEIC image (HEIC is converted automatically). Past uploads are cached and reopen straight into Measure with their datums and annotations restored.
- Review EXIF data -- camera, lens, focal length.
- Place datums on the image -- rectangles, lines, or ellipses (circles) with known real-world dimensions. Each datum carries a 1--5 confidence score. One datum can be flagged as the world-axis reference to fix the output orientation.
- Deskew -- OpenCV.js computes a perspective transform and produces a corrected image at a configurable px/mm scale, with per-datum residuals and a global RMS error reported.
- Measure -- annotate the corrected image with lines, rectangles, ellipses, circles, and angles in real-world units. Export the bare PNG, or the image with measurements baked in (full resolution or current viewport).
The algorithm
The pipeline picks a primary datum that fixes the output gauge (orientation, position, scale): any datum the user explicitly flagged as the world-axis reference, otherwise the highest-confidence rectangle, then ellipse, then line. The primary's known dimensions are mapped onto an axis-aligned output frame via cv.getPerspectiveTransform -- that's the warm-start homography.
That homography is then refined by an alternating-minimization loop around cv.findHomography (which internally runs DLT + Levenberg-Marquardt). On every outer pass, each datum is turned into output-space point correspondences using its shape constraint:
- Rectangle -- Procrustes-fit an ideal
w × hrectangle to the current projection of the user-marked corners. - Line -- preserve the projected midpoint and direction, rescale to the expected length.
- Ellipse -- sample 12 points along the user ellipse, project them, then radially snap to a circle of the expected diameter centred on the projected user-marked centre. Forces circles to stay circular under perspective.
Confidence scores (1--5) are realised as integer replication of correspondences -- cv.findHomography has no native weighting. The primary additionally gets a ×3 gauge boost so its anchors don't drift while secondary datums are being satisfied. The loop runs until the homography stops moving (max-entry relative delta below 1e-6) or 30 iterations, with period-2 oscillation detection logging a warning if the alternating minimization fails to converge.
A single cv.warpPerspective with Lanczos resampling produces the corrected image at the requested px/mm scale; output bounds are derived by projecting the source image corners through the final homography and clamped to a 12288 px maximum dimension to keep WASM heap usage bounded.
Per-datum residuals are reported alongside the result: edge length error and corner perpendicularity for rectangles, length error for lines, and isotropy / skew / equivalent-diameter error from the projected conic for ellipses, plus a global RMS percentage across all of them.
Quick start
pnpm install
pnpm dev
Open http://localhost:5173.
Build
pnpm build # type-check + production build
pnpm preview # serve the build locally
Lint & format
pnpm lint # eslint (strict TS + Vue)
pnpm lint:fix # auto-fix
pnpm format # prettier
pnpm type-check # vue-tsc
Stack
| Layer | Tech |
|---|---|
| Framework | Vue 3 + TypeScript (strict) |
| Build | Vite |
| Components | shadcn-vue + Tailwind CSS v4 |
| Canvas | Konva.js + vue-konva |
| CV | OpenCV.js 4.12 (WASM) |
| HEIC | heic-to |
| EXIF | exifr |
| State | Pinia |
Datum presets
Rectangles: A3, A4, A5, A6, 15×10 cm. Circles: €1, €2, US 25¢, UK 1p, CD. Custom dimensions supported on every type; lines accept any length.
How Skwik compares
There are plenty of tools that do part of what Skwik does, but none that combine everything:
| Tool | Client-side | Multi-datum weighting | Real-world mm scale | Measurement tools | Scale bar export |
|---|---|---|---|---|---|
| Skwik | ✅ | ✅ | ✅ | ✅ | ✅ |
| MYOG Perspective Correction | ✅ | ❌ | ✅ | ❌ | ❌ |
| PerspectiveFix | ✅ | ❌ | ❌ | ❌ | ❌ |
| PicFix.pro | ✅ | ❌ | ❌ | ❌ | ❌ |
| ImageOnline Perspective | ✅ | ❌ | ❌ | ❌ | ❌ |
| Toolschimp Image Measure | ✅ | ❌ | ✅ | ✅ | ❌ |
| Aspose Deskew | ❌ | ❌ | ❌ | ❌ | ❌ |
Most deskew tools just pull 4 corners to a rectangle without any real-world dimensions -- the output has no scale. Most measurement tools calibrate against a single reference and don't correct perspective. Skwik uses multiple weighted datums (rectangles, lines, and ellipses, each with a confidence score) to solve both problems in one pass, and lets you measure distances or export with a scale bar on the corrected image.
License
GNU General Public License v3.0


