298 lines
9.1 KiB
Python
298 lines
9.1 KiB
Python
|
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()
|