deskew — 2D geometry op

• Data kinds: image → image

• Call: fullseye.apply(img, "deskew", a=0.5, b=0.5) (the 2-D model is one image plus two scalar knobs a,b∈[0,1])

deskew: input → output

*The figure is the actual output on a synthetic 128×128 input. Left: input, right: output. Point clouds are drawn as a top-down scatter (brightness = z), 1-D series as a line plot, volumes as the maximum-intensity projection along z, videos as the middle frame, complex images as magnitude; return values that are not pictures are shown as the values themselves.*

Sweeping knob a (0.1 / 0.5 / 0.9, the other knob at its default):

▸ deskew: knob a sweep (docs site)

Sweeping knob b (0.1 / 0.5 / 0.9, the other knob at its default):

▸ deskew: knob b sweep (docs site)

Stages (the ops that come before → this op, left to right):

▸ deskew: stages (docs site)

On other images (synthetic scene / photo / coins. Top row: inputs, bottom row: their outputs. Knobs at default):

▸ deskew: other inputs (docs site)

*No colour (H,W,3) input is shown: this op treats the colour channel as a third spatial axis (it crosses channels). Call it per channel.*

Usage

Measures the tilt and straightens it (deskew). The angle is not given as a knob but estimated from the image.

a sweeps the search range ±(2 + 43a)° (±2° at a=0, ±45° at a=1). b sweeps the refinement step 1/(1 + round(9b))° (1° steps at b=0, 0.1° steps at b=1; the finer, the slower ― this is where the trade-off lies).

The criterion is the sum of squared differences of the horizontal projection profile. When rotating by some angle makes the lines align, the curve of intensity collapsed along the row direction rises and falls most sharply. It searches coarsely in 1° steps and refines the best ±1° with the step of b. On ties it takes the smaller angle, so an image without structure or an empty frame falls to 0° (= returns the input as is).

• The search runs on a copy decimated to 256 pixels on the long side (decimation does not change the angle). Only the final 1 rotation is done at full size. Even the widest search on 512x512 measured 121 ms.

• The score is taken only from the central square after rotation (side = min(H,W)/sqrt(2)) ― a region that stays inside the frame at any angle, so the way the corners are filled does not mix into the score.

• Outside the frame is filled with the median of the border (mode="constant"). The existing rotate_image fills by mirroring, so straightening a form makes mirrored characters appear in the four corners (its docstring itself admits it is "unsuitable where you want to fill outside the frame with the background colour"). Being able to pass what was straightened directly to OCR or cropping is what this op adds.

Measured accuracy and limits (synthetic page with 14-pixel line spacing and 384-pixel width): with b=1 (0.1° steps), 1 / 2 / 3.7 / 5 / 12 / 20° all give an error of 0.00°. However, tilts below 0.6° return 0 ― even across the full width of the page the line offset does not reach 1 pixel, and the sharpness of the projection is buried in the rounding of the interpolation. To measure finer, make the input larger (the criterion is proportional to the width).

Prior work: measuring tilt with projection profiles is classic; Postl (1986) used the variance and Baird (1987) the sum of squares + a coarse-to-fine angle search as the criterion. This op is in Baird's lineage; what is new is not the method but the handling of the border (below).

★Previously the difference criterion was off by +3.0 degrees. The cause has been established. The preliminary measurement of 2026-09-17, for ground truths −7/−3/+3/+7 degrees, gave the variance criterion an error of 0.0 degrees while the difference criterion was off by +3.0 degrees on all 4, and the cause was recorded as probably the border artefacts produced by ndimage.rotate, though "whether mode="constant" removes them is unverified". The cause was not the fill method but the region looked at — taking the score only from the inscribed square after rotation makes the difference criterion too give an error of 0.00 degrees at all of 1/2/3.7/5/12/20 degrees (measured).

The value range stays that of the input (neither linear interpolation nor the border median widens the range). ★Empty frames (value range below 1e-9) and inputs smaller than 8 pixels are returned as they are. ★It must not be split into tiles ― the angle is 1 value determined from the whole image; measuring per tile would rotate each tile by a different angle (registered with the reason in scale.py).

Detailed usage guide

• gallery2d_geometry family guide

References (sample data, literature)

• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).

• Operator provenance and references — the sources of the research/methods this op family came from.

• The canonical algorithm (author, year) and its uses are named in the family usage guide above.

Try it in Studio

The program below has been verified to run (same input as the figure). In Studio's help this block becomes buttons that load and run it on the spot.

rotate_img 0.58 0.50
deskew 0.50 0.50

▸ Load this pipeline  ·  Load & run

Runnable examples (verified samples that actually call this op)

• gallery2d_geometry — py -3.11 examples/gallery2d_geometry.py

Ops the type connects to (they accept image as input)

identity · gaussian · mean_box · bilateral · unsharp · median · min_filter · max_filter

Same category (geometry)

rotate_img · rescale_img · affine_warp · sk_swirl · mirror_image · transpose_region · rotate_image · zoom_image_factor


*Provenance: ops.py — 2D operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*

© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.