Though this is old, this can at least help any others who have the same issue. In addition to nathancy's answer, this should allow you to find the very blurry corners with much more accuracy:
Pseudo Code
- Resize if desired, not necessary though
- Convert to grayscale
- Apply blurring or bilateral filtering
- Apply Otsu's threshold to get a binary image
- Find contour that makes up rectangle
- Approximate contour as a rectangle
- Points of approximation are your rectangle's corners!
Code to do so
- Resize:
The function takes the new width and height, so I am just making the image 5 times bigger than it currently is.
img = cv2.resize(img, (img.shape[0] * 5, img.shape[1] * 5))

- Grayscale conversion:
Just converting to grayscale from OpenCV's default BGR colorspace.
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

- Blurring/bilateral filtering:
You could use any number of techniques to soften up this image further, if needed. Maybe a Gaussian blur, or, as nathancy suggested, a bilateral filter, but no need for both.
# choose one, or a different function
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
blurred = cv2.bilateralFilter(gray, 9, 75, 75)

- Otsu's threshold
Using the threshold function, pass 0
and 255
as the arguments for the threshold value and the max value. We pass 0
in because we are using the thresholding technique cv2.THRESH_OTSU
which determines the value for us. This is returned along with the threshold itself, but I just set it to _
because we don't need it.
_, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_OTSU)

- Finding contour
There is a lot more to contours than I will explain here, feel free to checkout docs.
The important things to know for us is that it returns a list of contours along with a hierarchy. We don't need the hierarchy so it is set to _
, and we only need the single contour it finds, so we set contour = contours[0]
.
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contour = contours[0]

- Approximate contour as a rectangle
First we calculate the perimeter of the contour. Then we approximate it with the cv2.approxPolyDP
function, and tell it the maximum distance between the original curve and its approximation with the 0.05 * perimeter
. You may need to play around with the decimal for a better approximation.
The approx
is a numpy array with shape (num_points, 1, 2)
, which in this case is (4, 1, 2)
because it found the 4 corners of the rectangle.
Feel free to read up more in the docs.
perimeter = cv2.arcLength(contour, True)
approx = cv2.approxPolyDP(contour, 0.05 * perimeter, True)
- Find your skewed rectangle!
You're already done! Here's just how you could draw those points. First we draw the circles by looping over them then grabbing the x and y coordinates, and then we draw the rectangle itself.
# drawing points
for point in approx:
x, y = point[0]
cv2.circle(img, (x, y), 3, (0, 255, 0), -1)
# drawing skewed rectangle
cv2.drawContours(img, [approx], -1, (0, 255, 0))

Finished Code
import cv2
img = cv2.imread("rect.png")
img = cv2.resize(img, (img.shape[0] * 5, img.shape[1] * 5))
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.bilateralFilter(gray, 9, 75, 75)
_, thresh = cv2.threshold(blurred, 0, 255, cv2.THRESH_OTSU)
contours, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contour = contours[0]
perimeter = cv2.arcLength(contour, True)
approx = cv2.approxPolyDP(contour, 0.05 * perimeter, True)
for point in approx:
x, y = point[0]
cv2.circle(img, (x, y), 3, (0, 255, 0), -1)
cv2.drawContours(img, [approx], -1, (0, 255, 0))