import cv2 import np import argparse import csv class AnswersChecker: def __init__(self, answers_file): self.path = answers_file self.answers = [] self.correct_answers = [] self.incorrect_answers = [] self.answered_questions = [] self.total_questions = 0 self.score_percent = 0 def load_answers_from_csv(self): file = open(self.path) reader = csv.DictReader(file) for row in reader: self.answers.append([row["Question"], row["Answer"]]) def compare(self, input_answers): self.answered_questions = input_answers self.correct_answers = [element for element in self.answers if element in input_answers] self.incorrect_answers = [element for element in self.answered_questions if element not in self.correct_answers] def get_percentage(self): return (len(self.correct_answers) / len(self.answers)) * 100 def get_report(self): output = "Total number of questions: " + str(len(self.answers)) + "\n" output += "Total number of questions answered: " + str(len(self.answered_questions)) + "\n" output += "Correct answers: " + str(len(self.correct_answers)) + "\n" output += "Percentage of correct answers:" + str(self.get_percentage()) + "%" return output def get_incorrect_answers(self): output = "Incorrect answers:" + "\n" for i in self.incorrect_answers: output += str(i[0]) + ": " + str(i[1]) + "\n" return output def get_correct_answers(self): output = "Correct answers:" + "\n" for i in self.correct_answers: output += str(i[0]) + ": " + str(i[1]) + "\n" return output def has_passed(self, passmark_points = 0, passmark_percentage = 0): if passmark_points != 0: if len(self.correct_answers) >= passmark_points: return True else: return False if passmark_percentage != 0: if self.get_percentage() >= passmark_percentage: return True else: return False def write_report_to_csv(self): try: path_first = self.path.split(".")[0] csv_writer = CsvWriter(path_first + "_report.csv") csv_writer.write_report(self.answered_questions, self.answers) except: pass class CsvWriter: def __init__(self, output_file_path): self.path = output_file_path self.file = open(self.path, 'w', newline='') def write_report(self, selected_answers, model_answers): column_names = ["Question number", "Selected answer", "Expected answer", "Correct"] csv_writer = csv.DictWriter(self.file, fieldnames=column_names) csv_writer.writeheader() for i in range(1, len(model_answers)+1): try: iscorrect = str(selected_answers[i-1]) == str(model_answers[i-1]) csv_writer.writerow({"Question number": str(i), "Selected answer": selected_answers[i-1][1], "Expected answer": model_answers[i-1][1], "Correct": str(iscorrect)}) except: pass class Selection: def __init__(self, x, y, selection: ""): self.x = x self.y = y self.selection = selection self.question_number = 0 def get_answer(self): return str(self.question_number) + ": " + str(self.selection) # Default values if no input file is provided ANSWERS_OPTIONS = 4 SUBSECTIONS = 2 DEFAULT_FILE = "wypelniony_arkusz_poprawne_odp.jpg" DEFAULT_ANSWERS_FILE = "answers.csv" SELECTED_INTENSITY_THRESHOLD = 100 DEFAULT_PASSMARK_PERCENTAGE = 50 DEFAULT_PASSMARK_POINTS = 20 # mapping the integer values to capital letters ANSWERS_CHOICES = {0: "A", 1: "B", 2: "C", 3: "D", 4: "E", 5: "F"} X_OFFSET = 2 Y_OFFSET = 2 MIN_OPTIONS = 3 if __name__ == "__main__": parser = argparse.ArgumentParser(description='Specify the input arguments.') percentage_passmark = 0 points_passmark = 0 options = 0 passmark_selected = False parser.add_argument('file', help='The image to be processed', nargs="?") parser.add_argument('-percent', type=int, help='The image to be processed', nargs="?") parser.add_argument('-passmark', type=int, help='The image to be processed', nargs="?") parser.add_argument('-options', type=int, help='The image to be processed', nargs="?") args = parser.parse_args() answers_path = "" if args.file is not None: answers_path = args.file else: answers_path = DEFAULT_FILE if args.percent is not None: percentage_passmark = args.percent else: percentage_passmark = DEFAULT_PASSMARK_PERCENTAGE if args.options is not None: options = args.options else: options = ANSWERS_OPTIONS if args.passmark is not None: points_passmark = args.passmark passmark_selected = True else: points_passmark = DEFAULT_PASSMARK_POINTS img = cv2.imread(answers_path, 0) gray_img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) gray_img = cv2.bitwise_not(gray_img) img = cv2.bitwise_not(img) # Detect circles circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1.1, 5, param1=90, param2=30, minRadius=12, maxRadius=20) circles = circles[0] # Sort the detected circles along x-axis x_sorted = circles[np.argsort(circles[:, 0]), :] index = -1 sorted_list = [] previous = 0 # Assign the circles to columns based on their X-coordinate for s in x_sorted: if s[0] > previous + X_OFFSET: index += 1 sorted_list.append([]) sorted_list[index].append(s) previous = s[0] sorted_list_filtered = [] # Accept columns above a given value for i in sorted_list: if len(i) > MIN_OPTIONS: sorted_list_filtered.append(i) selected_circles = [] question_number = 0 sorted_list2 = [] number_of_columns = int(len(sorted_list_filtered)/SUBSECTIONS) start = 0 end = options for i in range(number_of_columns): sorted_list2.append(sorted_list_filtered[start:end]) start = end end += end column_number = 0 question_per_column = 0 # Get the expected number of questions per column try: columns_number = [] for i in range(0, len(sorted_list2[0])): columns_number.append(len(sorted_list2[0][i])) question_per_column = max(columns_number) except: pass average_y_distance = 0 # Calculate average y-axis distance between points for i in range(0, len(sorted_list2[0])): if len(sorted_list2[0][i]) == question_per_column: np_ar = np.asarray(sorted_list2[0][i]) diff = np.diff(np_ar, axis = 1) average_y_distance = np.mean(diff) break for a in sorted_list2: index = 0 for l in a: l = np.asarray(l) l = l[l[:, 1].argsort()] question_number = 0 + column_number*question_per_column for i in l: cv2.circle(gray_img, (i[0], i[1]), i[2], (0, 255, 0), 2) avg = gray_img[int(i[1])-int(i[2]):int(i[1])+int(i[2])+1, int(i[0])-int(i[2]):int(i[0])+int(i[2])+1] question_number += 1 if np.mean(avg) > SELECTED_INTENSITY_THRESHOLD: cv2.circle(gray_img, (i[0], i[1]), 2, (0, 0, 255), 3) selection = Selection(int(i[0]), int(i[1]), ANSWERS_CHOICES[index]) selection.question_number = question_number selected_circles.append(selection) index += 1 column_number += 1 # Sort selected answers by the question number selected_circles.sort(key=lambda x: x.question_number, reverse=False) answers_checker = AnswersChecker(DEFAULT_ANSWERS_FILE) answers_checker.load_answers_from_csv() answers_list = [] for i in selected_circles: q_number = i.question_number q_ans = i.selection answers_list.append([str(q_number), q_ans]) answers_checker.compare(answers_list) # Print the entire report print(answers_checker.get_report()) print("\n") # Get incorrect answers print(answers_checker.get_incorrect_answers()) # Get correct answers print(answers_checker.get_correct_answers()) # Save the answers to CSV answers_checker.write_report_to_csv() print("\nList of answers") for circle in selected_circles: print(circle.get_answer()) print("-"*10) # Passed? if passmark_selected: print("Has the person passed the assessment?", answers_checker.has_passed(passmark_points=points_passmark)) else: print("Has the person passed the assessment?", answers_checker.has_passed(passmark_percentage=percentage_passmark)) cv2.imshow('Detected answers', gray_img) cv2.waitKey(0) cv2.destroyAllWindows()