Question: I have an image that i would like to do an edge detection and draw a bounding box to it, my problem is my python code does not draw the bounding box and im not sure if its because it was not able to detect any objects in it or im just drawing the rectangle wrong.  Here is my attempt import cv2 import numpy as np img = cv2.imread("image1.jpg") (B, G, R) = cv2.split(img) img = cv2.Canny(B, 20, 100) # Blue channel gives the best box so far # img = cv2.Canny(R, 20, 100) # img = cv2.Canny(R, 20, 100) ret,thresh = cv2.threshold(img,20,100,0) contours,hierarchy = cv2.findContours(thresh, 1, 2) cnt = contours[0] M = cv2.moments(cnt) for c in contours: rect = cv2.minAreaRect(c) box = cv2.boxPoints(rect) box = np.intp(box) img = cv2.drawContours(img,[box],0,(0,0,255),2) # display the image with bounding rectangle drawn on it # cv2.namedWindow('Bounding Rectangle', cv2.WINDOW_KEEPRATIO) cv2.imshow('Bounding Rectangle', img) cv2.waitKey(0) cv2.destroyAllWindows()  This produces this image  and I am expecting an image that is something like this: 

Answer: My approach:  optional: correct white balance so that the background loses its tint convert to color space with saturation channel, threshold that contours, minAreaRect  This approach detects all four colored squares. # you can skip this step by defining `balanced = im * np.float32(1/255)` # gray = im.mean(axis=(0,1), dtype=np.float32) # gray world gray = np.median(im, axis=(0,1)).astype(np.float32) # majority vote print(gray) # [137. 127. 140.] balanced = (im / gray) * 0.8 # some moderate scaling so it's not "overexposed"   # transform into color space that has a "saturation" axis hsv = cv.cvtColor(balanced, cv.COLOR_BGR2HSV) H,S,V = cv.split(hsv) # squares nicely visible in S   for comparison, the saturation channel of the source, without white balance:  # Otsu finds threshold level automatically (_, mask) = cv.threshold((S * 255).astype(np.uint8), 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU) # To remove minor debris/noise. Increase iterations as needed. mask = cv.morphologyEx(mask, cv.MORPH_OPEN, kernel=None, iterations=5)   (contours, _) = cv.findContours(mask, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) hull = cv.convexHull(np.concatenate(contours)) rrect = cv.minAreaRect(hull)  # visualization canvas = im.copy() cv.drawContours(image=canvas, contours=contours, contourIdx=-1, color=(255,255,0), thickness=7) # cv.drawContours(image=canvas, contours=[hull], contourIdx=-1, color=(255,255,0), thickness=7) rrect_points = cv.boxPoints(rrect).round().astype(int) cv.polylines(canvas, [rrect_points], isClosed=True, color=(0,255,255), thickness=3)    The code in one piece, minus some imshow/imwrite: import numpy as np import cv2 as cv im = cv.imread("picture.jpg") # gray = im.mean(axis=(0,1), dtype=np.float32) # gray world gray = np.median(im, axis=(0,1)).astype(np.float32) # majority vote print(gray) # [137. 127. 140.] balanced = (im / gray) * 0.8 # some moderate scaling so it's not "overexposed" # transform into color space that has a "saturation" axis hsv = cv.cvtColor(balanced, cv.COLOR_BGR2HSV) H,S,V = cv.split(hsv) # squares nicely visible in S # Otsu finds threshold level automatically (_, mask) = cv.threshold((S * 255).astype(np.uint8), 0, 255, cv.THRESH_BINARY | cv.THRESH_OTSU) # To remove minor debris/noise. Increase iterations as needed. mask = cv.morphologyEx(mask, cv.MORPH_OPEN, kernel=None, iterations=5) (contours, _) = cv.findContours(mask, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) hull = cv.convexHull(np.concatenate(contours)) rrect = cv.minAreaRect(hull) # visualization canvas = im.copy() cv.drawContours(image=canvas, contours=contours, contourIdx=-1, color=(255,255,0), thickness=7) # cv.drawContours(image=canvas, contours=[hull], contourIdx=-1, color=(255,255,0), thickness=7) rrect_points = cv.boxPoints(rrect).round().astype(int) cv.polylines(canvas, [rrect_points], isClosed=True, color=(0,255,255), thickness=3)