diff --git a/answers.csv b/answers.csv new file mode 100644 index 0000000..22715f4 --- /dev/null +++ b/answers.csv @@ -0,0 +1,41 @@ +Question,Answer +1,A +2,B +3,C +4,A +5,D +6,D +7,E +8,A +9,A +10,D +11,D +12,B +13,A +14,C +15,D +16,E +17,E +18,C +19,A +20,D +21,B +22,D +23,A +24,C +25,D +26,D +27,A +28,E +29,A +30,B +31,A +32,C +33,D +34,E +35,A +36,C +37,B +38,E +39,A +40,D diff --git a/answers_report.csv b/answers_report.csv new file mode 100644 index 0000000..99fab43 --- /dev/null +++ b/answers_report.csv @@ -0,0 +1,41 @@ +Question number,Selected answer,Expected answer,Correct +1,C,A,False +2,B,B,True +3,C,C,True +4,D,A,False +5,A,D,False +6,A,D,False +7,A,E,False +8,B,A,False +9,C,A,False +10,A,D,False +11,C,D,False +12,D,B,False +13,B,A,False +14,D,C,False +15,A,D,False +16,C,E,False +17,C,E,False +18,A,C,False +19,C,A,False +20,B,D,False +21,B,B,True +22,D,D,True +23,C,A,False +24,D,C,False +25,B,D,False +26,A,D,False +27,C,A,False +28,C,E,False +29,B,A,False +30,A,B,False +31,A,A,True +32,B,C,False +33,C,D,False +34,D,E,False +35,D,A,False +36,B,C,False +37,C,B,False +38,A,E,False +39,C,A,False +40,D,D,True diff --git a/arkusz OCR.jpg b/arkusz OCR.jpg new file mode 100644 index 0000000..ea42f54 Binary files /dev/null and b/arkusz OCR.jpg differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..5e9b2bd --- /dev/null +++ b/main.py @@ -0,0 +1,297 @@ +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() diff --git a/odpowiedzi_1.jpg b/odpowiedzi_1.jpg new file mode 100644 index 0000000..d5eeb89 Binary files /dev/null and b/odpowiedzi_1.jpg differ