I had an attempt at this using a variation on Fred's (@fmw42) suggestion. I first tried to locate all the white pixels along the image border, by converting to HSV colourspace then finding pixels that are both unsaturated and bright.
I then rotated the resulting image through -5 to +5 degrees in 0.1 degree increments. At each angle of rotation, I ran a SobelY filter looking for horizontal edges. Then I counted the white pixels in each row. Any time I find an orientation that results in a longer horizontal line, I update my best estimate and remember the rotation.
There are many variations possible but this should get you started:
#!/usr/bin/env python3
import cv2
import numpy as np
# Load image
im = cv2.imread('a4e.jpg')
# Find white pixels, i.e. unsaturated and bright
HSV = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
unsat = HSV[:,:,1] < 50
bright= HSV[:,:,2] > 240
white = ((unsat & bright)*255).astype(np.uint8)
cv2.imwrite('DEBUG-white.png', white)
That looks like this:

# Pad with border so it isn't cropped when rotated, get new dimensions
bw = 100
white = cv2.copyMakeBorder(white, bw, bw, bw, bw, borderType= cv2.BORDER_CONSTANT)
w, h = white.shape[:2]
# Find rotation that results in horizontal row with largest number of white pixels
maxOverall = 0
# SobelY horizontal edge kernel
kernel = np.array((
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]), dtype="int")
# Rotate image -5 to +5 degrees in 0.1 degree increments
for angle in [x * 0.1 for x in range(-50, 50)]:
M = cv2.getRotationMatrix2D((h/2,w/2),angle,1)
rotated = cv2.warpAffine(white,M,(h,w))
# Output image for debug purposes
cv2.imwrite(f'DEBUG rotated {angle}.jpg',rotated)
# Filter for horizontal edges
filtered = cv2.filter2D(rotated, -1, kernel)
cv2.imwrite(f'DEBUG rotated {angle} filtered.jpg',filtered)
# Check for maximum white pixels in any row
maxThis = np.amax(np.sum(rotated, axis=1))
if maxThis > maxOverall:
print(f'Angle:{angle}: New longest horizontal row has {maxThis} white pixels')
maxOverall = maxThis
The overall process looks like this:

The output looks like this, which means the detected angle is 0.6 degrees:
Angle:-5.0: New longest horizontal row has 34287 white pixels
Angle:-4.9: New longest horizontal row has 34517 white pixels
Angle:-4.800000000000001: New longest horizontal row has 34809 white pixels
Angle:-4.7: New longest horizontal row has 35191 white pixels
Angle:-4.6000000000000005: New longest horizontal row has 35625 white pixels
Angle:-4.5: New longest horizontal row has 36108 white pixels
Angle:-4.4: New longest horizontal row has 36755 white pixels
Angle:-4.3: New longest horizontal row has 37436 white pixels
Angle:-4.2: New longest horizontal row has 38151 white pixels
Angle:-4.1000000000000005: New longest horizontal row has 38876 white pixels
Angle:-4.0: New longest horizontal row has 39634 white pixels
Angle:-3.9000000000000004: New longest horizontal row has 40414 white pixels
Angle:-3.8000000000000003: New longest horizontal row has 41240 white pixels
Angle:-3.7: New longest horizontal row has 42074 white pixels
Angle:-3.6: New longest horizontal row has 42889 white pixels
Angle:-3.5: New longest horizontal row has 43570 white pixels
Angle:-3.4000000000000004: New longest horizontal row has 44252 white pixels
Angle:-3.3000000000000003: New longest horizontal row has 44902 white pixels
Angle:-3.2: New longest horizontal row has 45776 white pixels
Angle:-3.1: New longest horizontal row has 46620 white pixels
Angle:-3.0: New longest horizontal row has 47414 white pixels
Angle:-2.9000000000000004: New longest horizontal row has 48178 white pixels
Angle:-2.8000000000000003: New longest horizontal row has 48705 white pixels
Angle:-2.7: New longest horizontal row has 49225 white pixels
Angle:-2.6: New longest horizontal row has 49962 white pixels
Angle:-2.5: New longest horizontal row has 51501 white pixels
Angle:-2.4000000000000004: New longest horizontal row has 53217 white pixels
Angle:-2.3000000000000003: New longest horizontal row has 54997 white pixels
Angle:-2.2: New longest horizontal row has 56926 white pixels
Angle:-2.1: New longest horizontal row has 59033 white pixels
Angle:-2.0: New longest horizontal row has 61017 white pixels
Angle:-1.9000000000000001: New longest horizontal row has 62538 white pixels
Angle:-1.8: New longest horizontal row has 63370 white pixels
Angle:-1.7000000000000002: New longest horizontal row has 64144 white pixels
Angle:-1.6: New longest horizontal row has 65685 white pixels
Angle:-1.5: New longest horizontal row has 68510 white pixels
Angle:-1.4000000000000001: New longest horizontal row has 72377 white pixels
Angle:-1.3: New longest horizontal row has 76693 white pixels
Angle:-1.2000000000000002: New longest horizontal row has 80932 white pixels
Angle:-1.1: New longest horizontal row has 84101 white pixels
Angle:-1.0: New longest horizontal row has 86557 white pixels
Angle:-0.9: New longest horizontal row has 90499 white pixels
Angle:-0.8: New longest horizontal row has 97179 white pixels
Angle:-0.7000000000000001: New longest horizontal row has 101430 white pixels
Angle:-0.6000000000000001: New longest horizontal row has 105001 white pixels
Angle:-0.5: New longest horizontal row has 112976 white pixels
Angle:-0.4: New longest horizontal row has 117256 white pixels
Angle:-0.30000000000000004: New longest horizontal row has 131478 white pixels
Angle:-0.2: New longest horizontal row has 141468 white pixels
Angle:-0.1: New longest horizontal row has 164588 white pixels
Angle:0.0: New longest horizontal row has 186150 white pixels
Angle:0.1: New longest horizontal row has 206695 white pixels
Angle:0.2: New longest horizontal row has 230821 white pixels
Angle:0.30000000000000004: New longest horizontal row has 249003 white pixels
Angle:0.4: New longest horizontal row has 258888 white pixels
Angle:0.6000000000000001: New longest horizontal row has 264409 white pixels