CLI tool
sbrunner/deskew avatar
sbrunner/deskew

deskew: finding and removing the tilt in a scanned page

Library used to deskew a scanned document

529 stars46 forksPythonMIT

At a glance

What is it?
deskew is a small Python library that estimates the rotation angle of a scanned document and lets you correct it, with a projection-profile approach rather than a neural network, and an angle range deliberately restricted so the tool cannot turn a page sideways. It ships a command line interface, a library function, and a debug-image mode for tuning a detection that does not always land on the right answer.
Who is it for?
deskew fits a document processing pipeline that ingests scanned pages and needs them level before OCR, and that wants a small dependency-light estimator it can tune rather than a model it has to trust. It does not fit a pipeline that must recover a page deliberately scanned at 90 degrees, because the default angle range stops at 45 degrees, and it does not fit a project that expects deskew to correct perspective distortion or cropping, since it only estimates a rotation.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 21 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What skew is, and the range the library will accept

The README opens by defining the terms, and the definition is doing real work in the design.

Skew is measured in degrees. Deskewing is a process whereby skew is removed by rotating an image by the same amount as its skew but in the opposite direction. The result is a horizontally and vertically aligned image where the text runs across the page rather than at an angle.

The constraint that follows is the important one. The return angle is between -45 and 45 degrees, described as being there so as not to arbitrarily change the image orientation. In other words, the estimator will tell you the page is tilted by at most a modest amount, and it will refuse to report a 90 degree rotation as a skew. That is a deliberate limitation rather than a limitation of the method, and it is the difference between a tool you can run unattended on an inbox of scans and one that occasionally hands you a page turned on its side.

There is an escape hatch for the cases where that restriction is wrong. By setting the argument `angle_pm_90` to `True` you get an angle between -90 and 90 degrees. So the default protects you from a page that is upside down, and the flag exists for the case where your corpus genuinely contains pages scanned in portrait and sideways orientations.

The library is MIT licensed, written in Python, the default branch is master, and the last push to the repository was on 2026-10-02. The recent releases are 1.6.1 from 2026-06-03, 1.6.0 from 2026-04-13 and 1.5.3 from 2025-06-27.

Installing it, and the two commands that do the work

Installation is from PyPI with pip, and the README gives both the initial install and the upgrade form.

bash
python3 -m pip install deskew
bash
python3 -m pip install -U deskew

The command line interface has two forms, and the split between them is the first thing to understand. Getting the skew angle is one command.

bash
deskew input.png

And deskewing an image to a new file is the other.

bash
deskew --output output.png input.png

The difference matters because the first prints a number and the second rotates the image. In a batch pipeline you often want the angle rather than the corrected file, because the corrected file depends on how you want to handle the background and the resize, and both of those are decisions the library leaves to you. The CLI writes the corrected image where you ask, and the angle is available on its own when your pipeline wants to make its own decision.

The package dependencies are modest and pinned in a way worth noting. The pyproject.toml requires Python from 3.11 up to but not including 3.15, numpy at 2 or later, and scikit-image pinned to exactly 0.26.0. OpenCV is an optional dependency, opencv-python-headless at 5.0.0.93, and matplotlib is also present in the dependency list. The scikit-image pin is the one to look at: an exact version rather than a range means a pip install of a newer scikit-image into the same environment will either be held back or will need the constraint removed deliberately.

The library API, and why the rotation is not part of it

The library exposes one function that matters, `determine_skew`, and the README is careful to separate detection from correction. You detect, and you rotate yourself, because the rotation is a decision about the image that the library does not make for you.

The scikit-image example is the short version.

python
import numpy as np
from skimage import io
from skimage.color import rgb2gray
from skimage.transform import rotate

from deskew import determine_skew

image = io.imread("input.png")
grayscale = rgb2gray(image)
angle = determine_skew(grayscale)
rotated = rotate(image, angle, resize=True) * 255
io.imsave("output.png", rotated.astype(np.uint8))

The steps are: read the image, convert to grayscale, get the angle, rotate, scale back to 0-255 and save. The `resize=True` argument to scikit-image's rotate is what keeps the rotated image large enough to contain the original content, and the multiplication by 255 is there because that function returns floats.

The OpenCV example does the same job with more code, and the extra code is exactly what the library is declining to do for you. It computes the new width and height from the sine and cosine of the angle, finds the image centre, builds a rotation matrix with `getRotationMatrix2D`, corrects the translation so the image stays centred, and calls `warpAffine` with a background colour of black.

python
def rotate(image: np.ndarray, angle: float, background: Union[int, Tuple[int, int, int]]) -> np.ndarray:
    old_width, old_height = image.shape[:2]
    angle_radian = math.radians(angle)
    width = abs(np.sin(angle_radian) * old_height) + abs(np.cos(angle_radian) * old_width)
    height = abs(np.sin(angle_radian) * old_width) + abs(np.cos(angle_radian) * old_height)

That helper is a useful thing to copy, because it shows what a correct rotation involves: the output canvas has to grow, the rotation has to be about the centre, and the centre has to be re-registered after the resize. Any of the three done wrongly gives you a cropped or off-centre page.

The background argument is the other decision the library leaves open, and in the example it is a black tuple, so the corners of a rotated page come out black rather than white.

Debug images, and the parameters you tune when the angle is wrong

The README devotes a section to the failure mode, and it is the most practically useful part of the documentation.

If you get a wrong skew angle you can generate debug images, and the section says that can help you tune the skewing detection. The images come from an extra, installed with `pip install deskew[debug_images]`, and they are produced by the function `determine_skew_debug_images`.

The tuning procedure the README gives is ordered, which is worth following exactly. To start the investigation you should first increase the `num_peaks`, whose default is `20`, and use the `determine_skew_debug_images` function. Then you can try to tune `num_peaks`, `angle_pm_90`, `min_angle`, `max_angle`, `min_deviation` and eventually `sigma`.

The ordering says something about how the estimator works. `num_peaks` is the number of candidate angles the projection profile is examined at, so raising it gives a denser search, and the instruction to raise it first says that the default search is the first thing to suspect when the answer is wrong. `min_angle` and `max_angle` bound the search range within the overall limit, and `min_deviation` is the threshold for deciding whether a candidate peak is a real peak or noise, and `sigma` is the smoothing applied to the profile before the peaks are found. The advice to reach `sigma` last tells you the earlier knobs are the ones that usually matter.

The `angle_pm_90` argument appearing in the tuning list as well as the top-level documentation is a reminder that a mis-detection and a wrongly-oriented document look the same from the output.

The README also credits its inspiration, saying it is inspired by Alyn, with a link to the kakul/Alyn repository. The projection-profile approach is the classic method for text skew estimation, and crediting the prior work is the right thing to do.

The toolchain around the library

The repository is a small project with a full set of development scaffolding around it, and the layout shows what the maintainer expects from contributors.

At the top level there is a deskew/ directory for the package itself, a tests/ directory, a doc/ directory holding the example images used in the README, a ci/ directory, and a poetry.lock with a pyproject.toml that is the configuration file rather than a packaging manifest in the usual sense, since the project metadata in it declares the version as 0.0.0 and the actual releases are versioned independently. There is a .python-version file, a .pre-commit-config.yaml, a .prospector.yaml for static analysis, an .editorconfig and a .github/ directory.

The ruff configuration is worth a look because it shows the project's style commitments. The target version is py311, the line length is 110, and the pydocstyle convention is numpy. The lint select list starts empty and is extended with UP, I, S, B and ALL, with a long ignore list, and the per-file ignores for tests relax a large set of rules, which is the normal pattern for a project that wants strict linting in the library and pragmatism in the test suite.

The contributing instructions in the README are two commands.

bash
pip install pre-commit
pre-commit install --allow-missing-config

The `--allow-missing-config` flag matters for contributors on older pre-commit versions, and it tells you the repository expects the hooks to be installed locally rather than run only in CI.

For a library this size, the whole implementation fits in a package directory and a test directory, and the interesting engineering is in the estimator and its parameters rather than in the code structure. That is a good shape for a dependency you would vendor or fork, and a poor shape if you expect a maintained upstream with a long roadmap.

Where deskew is the wrong tool

Three cases point elsewhere. The first is a corpus with genuinely rotated pages. The default return angle is limited to between -45 and 45 degrees precisely so the tool does not arbitrarily change the image orientation, and a scanner that feeds pages in at 90 degrees will get an angle near the boundary or a confident wrong answer, and you will need `angle_pm_90` to see the real orientation at all.

The second is a pipeline that needs perspective correction, not rotation. A page photographed with a phone at an angle has keystone distortion, where the top edge is wider than the bottom edge, and a rotation cannot fix that. Deskew estimates a single angle, applies a rotation, and leaves the trapezoid alone. If the input is a camera capture rather than a flatbed or an ADF scan, you need a document rectification step first, and deskew is not it.

The third is a case where a wrong angle is unacceptable. The README's own framing is that you might get a wrong skew angle and should generate debug images to tune the detection, and there is no confidence score in the output that would let a pipeline decide when to trust the estimate. The projection-profile method also assumes a page with enough horizontal text structure to produce a usable profile, so a page that is mostly a photograph, a chart, or a sparse form may give a technically valid but meaningless angle. A modern layout analysis or OCR-based orientation classifier is the right tool when the page content is not dominated by lines of text.

The alternative worth comparing is a deep model that predicts page orientation directly, and the difference in approach is where the trade sits. A learned classifier gives a discrete orientation and a confidence, and it handles non-text pages and skew beyond 45 degrees, at the cost of a model download, a dependency on a runtime, and a black box that cannot be tuned with six named parameters. Deskew gives you a continuous angle, a small dependency footprint, and six knobs you can adjust with visual feedback from the debug images. For a pipeline that ingests clean flatbed scans and wants the page level, the classical method with a tuning loop is the smaller and more predictable dependency, and the README's tuning instructions are the evidence that it is meant to be adjusted rather than trusted blindly.

Editorial conclusion

deskew fits a document processing pipeline that ingests scanned pages and needs them level before OCR, and that wants a small dependency-light estimator it can tune rather than a model it has to trust. It does not fit a pipeline that must recover a page deliberately scanned at 90 degrees, because the default angle range stops at 45 degrees, and it does not fit a project that expects deskew to correct perspective distortion or cropping, since it only estimates a rotation. Before you adopt it, run it on a handful of your own worst scans with the debug images enabled, because the README says a wrong skew angle is the failure mode to expect, and check that the version of scikit-image your environment has matches the pinned 0.26.0.

Frequently asked questions

What does the deskew library do?

It estimates the skew angle of a scanned document and lets you remove it by rotating the image by the same amount in the opposite direction. The returned angle is between -45 and 45 degrees by default so the tool does not arbitrarily change the image orientation, and setting angle_pm_90 to True widens the range to -90 and 90 degrees.

How do I install deskew and deskew an image from the command line?

Install it with python3 -m pip install deskew, and upgrade with python3 -m pip install -U deskew. To deskew a file run deskew --output output.png input.png, and to only get the angle run deskew input.png.

How do I use deskew as a Python library?

Read the image, convert it to grayscale, pass it to determine_skew, and rotate it yourself with scikit-image's rotate or OpenCV's warpAffine. The library deliberately leaves the rotation and the background colour to the caller, since the scikit-image example uses resize=True and the OpenCV example passes a black background tuple.

What should I do when deskew returns the wrong angle?

Install the debug images extra with pip install deskew[debug_images] and use the determine_skew_debug_images function. The README says to first increase num_peaks, whose default is 20, and then to tune num_peaks, angle_pm_90, min_angle, max_angle, min_deviation and eventually sigma.

What are deskew's dependencies and version requirements?

The pyproject.toml requires Python from 3.11 up to but not including 3.15, numpy at 2 or later, scikit-image pinned to 0.26.0, and matplotlib, with opencv-python-headless at 5.0.0.93 as an optional dependency. The project is MIT licensed and its recent releases include 1.6.1 from 2026-06-03.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. sbrunner/deskew on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sbrunner-deskew.svg)](https://hysenlabs.com/projects/sbrunner-deskew)

Was this useful?