I am currently working on a OMR data extraction project where i have to check students OMR sheets. here is my code snippet: import cv2 import numpy as np # read image path = './data/images/5.jpg' img = cv2.imread(path) h, w = img.shape[:2] # resize image img = cv2.resize(img, (w//2, h//2)) img = img[0:h-15, 0:w-5] # threshold on white color lower=(225,225,225) upper=(255,255,255) thresh = cv2.inRange(img, lower, upper) thresh = 255 - thresh imgCanny = cv2.Canny(thresh, 10, 50) # # get contours contoursImage = img.copy() firstOMRBoxImage = img.copy() contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) cv2.drawContours(contoursImage, contours[0], -1, (0,255,0), 2) def rectContour(contours): rectContours = [] for i in contours: area = cv2.contourArea(i) if area > 50: peri = cv2.arcLength(i, True) approx = cv2.approxPolyDP(i, 0.02*peri, True) if (len(approx) == 4): rectContours.append(i) rectContours = sorted(rectContours, key=cv2.contourArea, reverse=True) firstOMRBox = getCornerPoints(rectContours[0]) secondOMRBox = getCornerPoints(rectContours[1]) thirdOMRBox = getCornerPoints(rectContours[3]) fourthOMRBox = getCornerPoints(rectContours[2]) rollNoPoints = getCornerPoints(rectContours[4]) districtPoints = getCornerPoints(rectContours[5]) nameAndDatePoints = getCornerPoints(rectContours[7]) candidateSign = getCornerPoints(rectContours[8]) invigilatorSign = getCornerPoints(rectContours[9]) groupPoints = getCornerPoints(rectContours[10]) classPoints = getCornerPoints(rectContours[12]) if firstOMRBox.size != 0 and secondOMRBox.size != 0: cv2.drawContours(firstOMRBoxImage, firstOMRBox, -1, (0,255,0), 30) cv2.drawContours(firstOMRBoxImage, secondOMRBox, -1, (255,0,0), 30) cv2.drawContours(firstOMRBoxImage, thirdOMRBox, -1, (0,0,255), 30) cv2.drawContours(firstOMRBoxImage, fourthOMRBox, -1, (255,255,0), 30) cv2.drawContours(firstOMRBoxImage, rollNoPoints, -1, (0,255,255), 30) cv2.drawContours(firstOMRBoxImage, districtPoints, -1, (255,0,255), 30) cv2.drawContours(firstOMRBoxImage, nameAndDatePoints, -1, (255,255,255), 30) cv2.drawContours(firstOMRBoxImage, candidateSign, -1, (0,0,0), 30) cv2.drawContours(firstOMRBoxImage, invigilatorSign, -1, (255,255,255), 30) cv2.drawContours(firstOMRBoxImage, groupPoints, -1, (0,0,255), 30) cv2.drawContours(firstOMRBoxImage, classPoints, -1, (255,0,0), 30) firstOMRBox = reorder(firstOMRBox) secondOMRBox = reorder(secondOMRBox) # Get the width and height of the first OMR box # Calculate the width and height of the first OMR box width_omr = np.linalg.norm(firstOMRBox[0][0] - firstOMRBox[1][0]) height_omr = np.linalg.norm(firstOMRBox[0][0] - firstOMRBox[2][0]) # Use the original aspect ratio for the destination points pt1 = np.float32(firstOMRBox) pt2 = np.float32([[0,0],[width_omr,0],[0,height_omr],[width_omr,height_omr]]) matrix = cv2.getPerspectiveTransform(pt1, pt2) imgWarpColoured = cv2.warpPerspective(img, matrix, (int(width_omr), int(height_omr))) # max_side = max(w, h) # pt1 = np.float32(firstOMRBox) # pt2 = np.float32([[0,0],[max_side,0],[0,max_side],[max_side,max_side]]) # matrix = cv2.getPerspectiveTransform(pt1, pt2) # imgWarpColoured = cv2.warpPerspective(img, matrix, (max_side,max_side)) cv2.imwrite('5Wrap_contour.png', imgWarpColoured) # Apply Threshhold # imgWarpGray = cv2.cvtColor(imgWarpColoured, cv2.COLOR_BGR2GRAY) # imgThresh = cv2.threshold(imgWarpGray, 200, 255, cv2.THRESH_BINARY_INV)[1] # cv2.imwrite('6biggest_thresh.png', imgThresh) # print(imgThresh.shape) # x1 = int(w * 0.2) # Start cropping from 70% width # y1 = 0 # Start from the top # x2 = w # End at full width (rightmost) # y2 = h # Full height # imgThresh = imgThresh[y1:y2, x1:x2] # cv2.imwrite('7after_crop.png', imgThresh) # Apply Threshhold imgWarpGray = cv2.cvtColor(imgWarpColoured, cv2.COLOR_BGR2GRAY) imgThresh = cv2.threshold(imgWarpGray, 200, 255, cv2.THRESH_BINARY_INV)[1] cv2.imwrite('6biggest_thresh.png', imgThresh) print(imgThresh.shape) afterContourIMage = imgThresh.copy() grey = cv2.cvtColor(imgWarpColoured, cv2.COLOR_BGR2GRAY) # Find contours contours, hierarchy = cv2.findContours(grey, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) print(len(contours)) cv2.drawContours(afterContourIMage, contours, -1, (0,255,0), 10) cv2.imwrite('7after_contour.png', afterContourIMage) # grey_inverted = cv2.bitwise_not(grey) # cv2.imwrite('7grey_inverted.png', grey_inverted) cv2.threshold(grey, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU, imgThresh) cv2.imwrite('7after_thresh.png', imgThresh) aginAfterContourIMage = imgWarpColoured.copy() # Find contours contours, hierarchy = cv2.findContours(imgThresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) print(len(contours)) cv2.drawContours(aginAfterContourIMage, contours, -1, (0,255,0), 2) cv2.imwrite('7after_contour2.png', aginAfterContourIMage) # Get the current dimensions of imgThresh thresh_h, thresh_w = imgThresh.shape # Now use the dimensions of imgThresh for cropping x1 = int(thresh_w * 0.2) # Start cropping from 20% width y1 = 0 # Start from the top x2 = thresh_w # End at full width y2 = thresh_h # Full height # Make sure our cropping coordinates are valid if x1 < thresh_w and y2 <= thresh_h: imgThresh = imgThresh[y1:y2, x1:x2] cv2.imwrite('7after_crop.png', imgThresh) else: print("Cropping coordinates are out of bounds!") # Use the uncropped version instead cv2.imwrite('7after_crop.png', imgThresh) # biggestThreshContoursImage = imgThresh.copy() # threshContours = cv2.findContours(biggestThreshContoursImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # cv2.drawContours(biggestThreshContoursImage, threshContours[0], -1, (0,255,0), 1) # cv2.imwrite('biggest_thresh_contours.png', biggestThreshContoursImage) splitBoxes(imgThresh) return firstOMRBox def getCornerPoints(cont): peri = cv2.arcLength(cont, True) approx = cv2.approxPolyDP(cont, 0.02*peri, True) return approx def reorder(points): points = points.reshape((4,2)) pointsNew = np.zeros((4,1,2), np.int32) add = points.sum(1) # print(points) # print(add) pointsNew[0] = points[np.argmin(add)] pointsNew[3] = points[np.argmax(add)] diff = np.diff(points, axis=1) pointsNew[1] = points[np.argmin(diff)] pointsNew[2] = points[np.argmax(diff)] # print(pointsNew) return pointsNew def splitBoxes(img): h, w = img.shape[:2] # Make sure height is divisible by 25 new_h = (h // 25) * 25 img = img[:new_h, :] # Crop height to nearest multiple of 25 # Make sure width is divisible by 5 new_w = (w // 5) * 5 img = img[:, :new_w] # Crop width to nearest multiple of 5 rows = np.vsplit(img, 25) # Split into 25 vertical parts cv2.imwrite('8Split_image.png', rows[0]) # Save first row for debugging boxes = [] for i, r in enumerate(rows): cols = np.hsplit(r, 5) # Now width is divisible by 5 for j, box in enumerate(cols): boxes.append(box) # cv2.imwrite(f'Split_image_{i}_{j}.png', box) # Save each box for debugging return boxes # Load image # img = cv2.imread("image.jpg") # Replace with your image path # splitBoxes(img) # Example usage rectContour(contours[0]) # save results cv2.imwrite('1omr_sheet_thresh2.png',thresh) cv2.imwrite('2omr_sheet_canny2.png',imgCanny) cv2.imwrite('3contours2.png',contoursImage) cv2.imwrite('4biggest_contour2.png',firstOMRBoxImage) cv2.waitKey(0) cv2.destroyAllWindows() This is where I'm currently stuck on I have an idea of converting them into grid and somehow defining each of the block into binary. 0 -> white, 1-> black like this. but I don't know how to do that as I'm currently learning about all these stuffs. [After contour image](https://i.sstatic.net/v82NAdjo.png) I've tried to solve it by identifying contours but I didn't succeed as after identifying the contours i don't know how to extract the one's which are rounded and also have fully black color inside the bubble. if i somehow manage to do this I think I can do the rest of the part. Continue reading...