I am making an automatic exam marking program using Python and OpenCV. The input image is a scanned image of the multiple choice answer sheet (OMR). The goal is to align the image based on the 4 black squares in the 4 corners and then identify the registration number, code, and answers. ⚠️ Problem encountered: When running the find_corners_and_warp function, the program reports the error "❌ Could not find 4 squares in the 4 corners." and alignment cannot be performed. I tried checking the image (link below), and it seems that the detection of contours is not enough 4 squares. Photo link: https://i.sstatic.net/OlYPwjc1.jpg Question: How to be sure to find the correct 4 squares in the 4 corners of the scanned image? Is there a better way? import cv2 import numpy as np # PHẦN I: Đáp án đúng mẫu (A=0, B=1, C=2, D=3) answer_key_part1 = [ 1, 0, 2, 3, 1, 0, 2, 2, 3, 1, 1, 2, 3, 1, 0, 2, 1, 3, 2, 0, 0, 1, 3, 2, 1, 0, 3, 2, 2, 0, 1, 1, 3, 1, 2, 0, 3, 1, 2, 0 ] answer_key_part2 = ["Đúng", "Sai", "Đúng", "Đúng", "Sai", "Sai", "Đúng", "Sai"] answer_key_part3 = ['1,23', '3,14', '0,50', '2,35', '5,00', '0,75'] def find_corners_and_warp(image_path, output_size=(800, 1120)): image = cv2.imread(image_path) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) blurred = cv2.GaussianBlur(gray, (5, 5), 1) edged = cv2.Canny(blurred, 75, 200) # Tìm tất cả contour contours, _ = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # Lọc contour là hình vuông nhỏ (ô vuông góc) square_centers = [] for c in contours: peri = cv2.arcLength(c, True) approx = cv2.approxPolyDP(c, 0.02 * peri, True) area = cv2.contourArea(c) if len(approx) == 4 and 300 < area < 3000: # vừa đủ với ô vuông đen x, y, w, h = cv2.boundingRect(c) cx, cy = x + w // 2, y + h // 2 square_centers.append((cx, cy)) if len(square_centers) < 4: return None, "❌ Không tìm đủ 4 ô vuông ở 4 góc." # Tìm 4 góc theo vị trí h_img, w_img = gray.shape def closest(ref, candidates): return min(candidates, key=lambda pt: (pt[0]-ref[0])**2 + (pt[1]-ref[1])**2) tl = closest((0, 0), square_centers) tr = closest((w_img, 0), square_centers) br = closest((w_img, h_img), square_centers) bl = closest((0, h_img), square_centers) corners = np.array([tl, tr, br, bl], dtype="float32") dst = np.array([ [0, 0], [output_size[0] - 1, 0], [output_size[0] - 1, output_size[1] - 1], [0, output_size[1] - 1] ], dtype="float32") M = cv2.getPerspectiveTransform(corners, dst) warped = cv2.warpPerspective(image, M, output_size) return warped, "✅ Đã căn chỉnh chính xác theo ô vuông góc." def extract_digits(thresh, roi, num_digits=6, num_options=10): x, y, w, h = roi region = thresh[y:y+h, x:x+w] digit_width = w // num_digits extracted_number = "" for i in range(num_digits): digit_img = region[:, i * digit_widthi + 1) * digit_width] cell_height = h // num_options detected_digit = None for j in range(num_options): cell = digit_img[j * cell_heightj + 1) * cell_height, :] if np.count_nonzero(cell == 0) > 200: detected_digit = str(j) break extracted_number += detected_digit if detected_digit else "X" return extracted_number def extract_sbd(thresh): # Cột bên trái (7) trong ảnh: tọa độ x ~ 100, width ~ 160 # Dòng ~ 90–170 roi_sbd = (100, 90, 160, 80) return extract_digits(thresh, roi_sbd, num_digits=6) def extract_ma_de(thresh): # Cột bên phải (8): tọa độ x ~ 600, width ~ 80 roi_ma_de = (600, 90, 80, 80) return extract_digits(thresh, roi_ma_de, num_digits=3) def grade_part1_debug(thresh): roi = thresh[250:980, 100:810] rows, cols = 40, 4 h, w = roi.shape box_h, box_w = h // rows, w // cols selected = [] print(" Bắt đầu chấm PHẦN I (40 câu trắc nghiệm):\n") for r in range(rows): marks = [] for c in range(cols): x1, y1 = c * box_w, r * box_h x2, y2 = (c + 1) * box_w, (r + 1) * box_h box = roi[y1:y2, x1:x2] count = cv2.countNonZero(box) marks.append(count) max_val = max(marks) max_idx = marks.index(max_val) avg_others = (sum(marks) - max_val) / (len(marks) - 1) print(f"Câu {r + 1:02}: A={marks[0]:3} | B={marks[1]:3} | C={marks[2]:3} | D={marks[3]:3} -> ", end="") if max_val < 80 or max_val < avg_others * 1.25: selected.append(-1) print("Không rõ hoặc tô không đủ đậm") else: selected.append(max_idx) print(f"Chọn: {'ABCD'[max_idx]}") score = sum([1 for a, b in zip(selected, answer_key_part1) if a != -1 and a == b]) percent = int(score * 100 / len(answer_key_part1)) print(f"\n✅ PHẦN I Score: {percent}%") print(f" Đáp án đã chọn: {selected}") print(f"ℹ️ Số câu không tô rõ: {selected.count(-1)}") return percent, selected def grade_part2(thresh): roi = thresh[1070:1300, 110:610] rows, cols = 8, 2 h, w = roi.shape box_h, box_w = h // rows, w // cols result = [] for r in range(rows): d_box = roi[r*box_hr+1)*box_h, 0:box_w] s_box = roi[r*box_hr+1)*box_h, box_w:2*box_w] d_val = cv2.countNonZero(d_box) s_val = cv2.countNonZero(s_box) result.append("Đúng" if d_val > s_val else "Sai") score = sum([1 for a, b in zip(result, answer_key_part2) if a == b]) percent = int(score * 100 / len(answer_key_part2)) print(f"✅ PHẦN II Score: {percent}%") print(f" Kết quả: {result}") return percent, result def grade_part3(thresh): roi3 = thresh[1330:1750, 100:730] rows = 11 digits = 4 cols = 6 h, w = roi3.shape box_h = h // rows box_w = w // (cols * digits) answers = [] for c in range(cols): number = "" for d in range(digits): max_val = 0 selected_digit = '' for r in range(rows): x = (c * digits + d) * box_w y = r * box_h box = roi3[y:y+box_h, x:x+box_w] val = cv2.countNonZero(box) if val > max_val: max_val = val selected_digit = ',' if r == 0 else str(r - 1) number += selected_digit answers.append(number) score = sum([1 for a, b in zip(answers, answer_key_part3) if a == b]) percent = int(score * 100 / len(answer_key_part3)) print(f"✅ PHẦN III Score: {percent}%") print(" Số đã tô:", answers) return percent, answers def grade_omr(image_path): warped, msg = find_corners_and_warp(image_path) if warped is None: print("❌", msg) return gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV)[1] sbd = extract_sbd(thresh) made = extract_ma_de(thresh) print(f" Số báo danh: {sbd}") print(f" Mã đề: {made}") print(" Đang chấm bài sau khi đã căn chỉnh...\n") p1_score, _ = grade_part1_debug(thresh) p2_score, _ = grade_part2(thresh) p3_score, _ = grade_part3(thresh) print("\n TỔNG KẾT ĐIỂM:") print(f" PHẦN I: {p1_score}%") print(f"✅ PHẦN II: {p2_score}%") print(f" PHẦN III: {p3_score}%") print(f"\n **Tổng điểm trung bình**: {(p1_score + p2_score + p3_score) // 3}%") if __name__ == "__main__": image_path = "StudentInfo/scanbanchuan.jpg" grade_omr(image_path) Continue reading...