How to Auto-Deskew Scanned Documents with OpenCV and Python: Step-by-Step Code Example
Scanned documents often contain skewed or crooked images. They do not look good and are not friendly for OCR.
In this article, we are going to use OpenCV and Python to deskew scanned documents based on text lines.
What you’ll build: A Python script that detects and corrects skew in scanned document images using OpenCV contour analysis and affine rotation, plus a browser-based alternative with Dynamic Web TWAIN.
Key Takeaways
- OpenCV can auto-detect document skew angles by analyzing dilated text-line contours with
minAreaRectand taking the median rotation angle. - Affine transformation via
cv2.warpAffinerotates the scanned image to correct skew without cropping content. - Pre-processing steps — grayscale conversion, Gaussian blur, Otsu thresholding, and morphological dilation — are essential to isolate text lines for accurate angle detection.
- For browser-based workflows, Dynamic Web TWAIN provides a built-in
GetSkewAngle+RotateAPI that handles deskewing without custom image processing code.
Common Developer Questions
How do I auto-deskew a scanned document image with OpenCV in Python?
Convert the image to grayscale, threshold and dilate the text lines, measure contour angles with cv2.minAreaRect, and use the median angle as the skew estimate. Then rotate the page back with cv2.warpAffine to produce the deskewed result.
Why is my OpenCV deskew returning the wrong angle, and how do I fix it?
Wrong angles usually come from noisy contours, border artifacts, or keeping near-zero and 90-degree rectangles in the sample set. Cleaning the image first, removing borders, and filtering unusable contour angles before taking the median makes the deskew result much more stable.
What is the easiest way to deskew scanned documents in a web browser with JavaScript?
Use Dynamic Web TWAIN’s built-in GetSkewAngle and Rotate APIs if you want browser-side deskew without rebuilding the image-processing pipeline yourself. That approach skips custom OpenCV code and fits directly into scanning workflows on the web.
Prerequisites
- Python 3.6 or later
- OpenCV installed (
pip install opencv-python) - To try the browser-based approach with Dynamic Web TWAIN, get a 30-day free trial license.
Step-by-Step: Deskew a Scanned Document with OpenCV and Python
We are going to write a Python script to deskew the following sample image.

Step 1: Normalize the Scanned Image for Processing
-
Scanned images are sharp. We can convert the image to grayscale and blur the image first.
img = cv2.imread(path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (9, 9), 0)
-
Resize the image with a fixed height.
resized_height = 480 percent = resized_height / len(img) resized_width = int(percent * len(img[0])) gray = cv2.resize(gray,(resized_width,resized_height))
-
Draw a rectangle around the border to remove border lines.
start_point = (0, 0) end_point = (gray.shape[0], gray.shape[1]) color = (255, 255, 255) thickness = 10 gray = cv2.rectangle(gray, start_point, end_point, color, thickness)
-
Invert the image, since we have to process the text.
gray = cv2.bitwise_not(gray)
-
Run thresholding to get a binary image.
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
-
Dilate the text to make the text lines more obvious.
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 5)) dilate = cv2.dilate(thresh, kernel)
Step 2: Detect the Skew Angle from Text Lines
-
Find all the contours based on the dilated image.
contours, hierarchy = cv2.findContours(dilate, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) -
Use
minAreaRectto get the rotation angles of contours.angles = [] for contour in contours: minAreaRect = cv2.minAreaRect(contour) angle = minAreaRect[-1] if angle != 90.0 and angle != -0.0: #filter out 0 and 90 angles.append(angle) -
Use the median as the skewed angle.
angles.sort() mid_angle = angles[int(len(angles)/2)]
Step 3: Rotate the Image to Correct the Skew
After getting the skewed angle, we can perform affine transformation to get the deskewed image.
if angle > 45: #anti-clockwise
angle = -(90 - angle)
height = original.shape[0]
width = original.shape[1]
m = cv2.getRotationMatrix2D((width / 2, height / 2), angle, 1)
deskewed = cv2.warpAffine(original, m, (width, height), borderValue=(255,255,255))

Deskew Scanned Documents in the Browser with Dynamic Web TWAIN
There are other tools which have the ability to deskew document images. Dynamic Web TWAIN is a JavaScript library to enable document scanning in the browser. It can scan documents from physical scanners via protocols like TWAIN, WIA, SANE and ICA and has a deskew function built-in.
The following is the code snippet to perform deskewing of a scanned document image.
function Deskew(index) {
return new Promise((resolve, reject) => {
DWObject.GetSkewAngle(
index,
function(angle) {
console.log("skew angle: " + angle);
DWObject.Rotate(index, angle, true,
function() {
console.log("Successfully deskewed an image!");
resolve();
},
function(errorCode, errorString) {
console.log(errorString);
reject(errorString);
}
);
},
function(errorCode, errorString) {
console.log(errorString);
reject(errorString);
}
);
})
}
You can use this online demo to have a try. It can also load image or PDF files and save documents in a PDF file.
Common Issues and Edge Cases
- Wrong skew angle on documents with few text lines: If the document has large images, tables, or very few text lines, the contour-based median angle may be unreliable. Filter contours by minimum area (e.g.,
cv2.contourArea(contour) > 100) to exclude noise before calculating the angle. - Black borders appear after rotation:
cv2.warpAffinefills empty pixels withborderValue. If you see black edges, make sure you setborderValue=(255,255,255)for white-background documents. For large skew angles (> 5°), consider cropping the result. - Angle off by 90°: OpenCV’s
minAreaRectreturns angles in the range [−90°, 0°). A nearly-vertical rectangle may report −89° instead of −1°. Theif angle > 45guard in Step 3 handles this, but verify the direction by testing on a known-skewed sample first.