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