Skip to content

Instantly share code, notes, and snippets.

@endolith
Last active July 26, 2024 00:06
Show Gist options
  • Save endolith/334196bac1cac45a4893 to your computer and use it in GitHub Desktop.
Save endolith/334196bac1cac45a4893 to your computer and use it in GitHub Desktop.
Detecting rotation and line spacing of image of page of text using Radon transform
"""
Automatically detect rotation and line spacing of an image of text using
Radon transform
If image is rotated by the inverse of the output, the lines will be
horizontal (though they may be upside-down depending on the original image)
It doesn't work with black borders
"""
from skimage.transform import radon
from PIL import Image
from numpy import asarray, mean, array, blackman
import numpy as np
from numpy.fft import rfft
import matplotlib.pyplot as plt
try:
# More accurate peak finding from
# https://gist.github.com/endolith/255291#file-parabolic-py
from parabolic import parabolic
def argmax(x):
return parabolic(x, np.argmax(x))[0]
except ImportError:
from numpy import argmax
def rms_flat(a):
"""
Return the root mean square of all the elements of *a*, flattened out.
"""
return np.sqrt(np.mean(np.abs(a) ** 2))
filename = 'skew-linedetection.png'
# Load file, converting to grayscale
I = asarray(Image.open(filename).convert('L'))
I = I - mean(I) # Demean; make the brightness extend above and below zero
plt.subplot(2, 2, 1)
plt.imshow(I)
# Do the radon transform and display the result
sinogram = radon(I)
plt.subplot(2, 2, 2)
plt.imshow(sinogram.T, aspect='auto')
plt.gray()
# Find the RMS value of each row and find "busiest" rotation,
# where the transform is lined up perfectly with the alternating dark
# text and white lines
r = array([rms_flat(line) for line in sinogram.transpose()])
rotation = argmax(r)
print('Rotation: {:.2f} degrees'.format(90 - rotation))
plt.axhline(rotation, color='r')
# Plot the busy row
row = sinogram[:, rotation]
N = len(row)
plt.subplot(2, 2, 3)
plt.plot(row)
# Take spectrum of busy row and find line spacing
window = blackman(N)
spectrum = rfft(row * window)
plt.plot(row * window)
frequency = argmax(abs(spectrum))
line_spacing = N / frequency # pixels
print('Line spacing: {:.2f} pixels'.format(line_spacing))
plt.subplot(2, 2, 4)
plt.plot(abs(spectrum))
plt.axvline(frequency, color='r')
plt.yscale('log')
plt.show()
@endolith
Copy link
Author

@endolith
Copy link
Author

endolith commented May 24, 2014

http://tpgit.github.io/Leptonica/skew_8c_source.html has a probably faster algorithm that shears and then sums along raster lines, scoring based on "the square of the DIFFERENCE between adjacent line sums, summed over all lines" but would be limited to finding only angles that can be produced by shearing. The rotation method can find any angle, including 90 degrees, but can't distinguish that text is upside-down.

@yasinrawther
Copy link

Hi endolith,

I have tried with my billing image to skew correction. But it is too slow to processing the image.
And it is take around 230 seconds.
How can I resolve this issue?

Thanks

@tangshao0804
Copy link

It's help for me, thank you very much.

@endolith
Copy link
Author

endolith commented May 6, 2019

https://scantailor.org/ can do this very easily

@shyam-yugen
Copy link

rms_flat has been deprecated since 3.1.0 of matplotlib. link here

@ablanco1950
Copy link

Thanks for notice. I use this routine in several projects but without using matplolib, so I think it won't affect me.

@ablanco1950
Copy link

ablanco1950 commented Jul 25, 2023 via email

@endolith
Copy link
Author

@shyam-yugen @ablanco1950

Yep. Please comment on these scipy issues: scipy/scipy#16179 scipy/scipy#16189

@zoldaten
Copy link

zoldaten commented Oct 4, 2023

what to replace rms_flat code ?

@endolith
Copy link
Author

endolith commented Oct 4, 2023

@zoldaten I updated the script. Still works:

Figure_1

@zoldaten
Copy link

zoldaten commented Oct 5, 2023

@endolith thanks !
1.
i see output on skew-linedetection.png:

Rotation: 5.00 degrees
Line spacing: 13.63 pixels

is it correct ?

and sometimes got this:

/home/pi/.local/lib/python3.9/site-packages/skimage/transform/radon_transform.py:75: UserWarning: Radon transform: image must be zero outside the reconstruction circle
  warn('Radon transform: image must be zero outside the '
Rotation: 3.00 degrees
/home/pi/Desktop/rotation_detection.py:69: RuntimeWarning: divide by zero encountered in long_scalars
  line_spacing = N / frequency  # pixels
Line spacing: inf pixels

i saw remark It doesn't work with black borders
what does it mean ? do you have an example image ?

@endolith
Copy link
Author

endolith commented Oct 5, 2023

@zoldaten That's what I get for the example image, yes:

Rotation: 5.00 degrees
Line spacing: 13.63 pixels

@zoldaten
Copy link

zoldaten commented Oct 5, 2023

@endolith
if you dont mind i ll speed up a bit your code.
now i have time 0:00:01.228 sec (with skew-linedetection.png). on raspberry pi.
the bigger pic i use the more inference time. on 2MiB pic i have already 11 sec.
i used cprofile and found that sinogram = radon(I) eats all time.
to speed up it we need smaller image.

so. we need to replace:
I = asarray(Image.open(filename).convert('L'))
with this:

import sys
from PIL.Image import Resampling

I = Image.open(filename).convert('L')
I.thumbnail([sys.maxsize, 480], Resampling.LANCZOS)  #resize image keeping aspect ratio. 480 by example. it may be smaller i think.

now i have on skew-linedetection.png:
0:00:00.739

i didnt tested how the last code works as i need only rotation degrees. and it returns the result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment