diff --git a/ai/.gitignore b/ai/.gitignore index 89bf445fc4a046cfffe2ac5ba2910f89be8c1d5d..65029931b77a1c341e376b5b7be6834ff8f360e5 100644 --- a/ai/.gitignore +++ b/ai/.gitignore @@ -1,2 +1,3 @@ .idea -src/output.pdf \ No newline at end of file +src/output.pdf +generated_pdfs/* \ No newline at end of file diff --git a/ai/src/bubble_sheet_generator.py b/ai/src/bubble_sheet_generator.py index a383a8fd9d6bf3de968b0c5fb964c649dca743f2..4eb852c8db970d36871fd93d270c3623c41c857b 100644 --- a/ai/src/bubble_sheet_generator.py +++ b/ai/src/bubble_sheet_generator.py @@ -1,3 +1,5 @@ +import time + import numpy as np import matplotlib.pyplot as plt import matplotlib.patches as patches @@ -7,79 +9,64 @@ from ai.src.utils import load_config question_number = 1 -def draw_rect(ax, config, rect_x, rect_y, rect_type="answer_rect", gray_columns=False, last_rect_q=None): +def draw_header(ax, rect_x, rect_y, config): """ - Draws a rectangle including circles to be filled in the final bubble sheet + Draw the header of the bubble sheet + :param ax: The axis to draw the rectangle on + :param rect_x: The x-coordinate of the rectangle top left corner + :param rect_y: The y-coordinate of the rectangle top left corner + :param config: Configuration dictionary + """ + PIXELS_PER_INCH = 96 + # Configuration + header_title = config["header"]["title"] + header_date = config["header"]["date"] + header_name = config["header"]["name"] + font_size = config["header"]["font_size"] + font_size_relative = font_size / (ax.figure.get_figheight() * PIXELS_PER_INCH) + font = config["header"]["font"] + text_color = config["colors"]["main_color"] + + # Draw the header + rect_y -= font_size_relative + rect_x -= font_size_relative + ax.text(rect_x, rect_y, header_title, ha='left', va='bottom', fontsize=font_size+5, fontname=font, color=text_color, weight='bold') + font_size_offset = font_size_relative * 2 + ax.text(rect_x, rect_y - font_size_offset, header_date, ha='left', va='bottom', fontsize=font_size, fontname=font, + color=text_color) + font_size_offset = font_size_relative * 4 + ax.text(rect_x, rect_y - font_size_offset, header_name, ha='left', va='bottom', fontsize=font_size, fontname=font, + color=text_color) + + +def gray_out(ax, config, rect_x, rect_y, rect_type="answer_rect", gray_columns=False): + """ + Gray out every other column or row :param ax: The axis to draw the rectangle on :param config: Configuration dictionary :param rect_x: The x-coordinate of the rectangle top left corner :param rect_y: The y-coordinate of the rectangle top left corner :param rect_type: Type of the rectangle (Student ID or Answers) :param gray_columns: If true every other column will be grayed out, otherwise every other row - :param last_rect_q: Number of questions in the last rectangle """ - # Set colors from the config - rect_color = config["colors"]["main_color"] + # Configuration off_color = config["colors"]["off_color"] - text_color = config["colors"]["text_color"] - # Set the width of line of the rectangle rect_width = config["rect_settings"]["rect_line_width"] - # Set the width and height of the rectangle and the grid inside width = config[rect_type]["width"] height = config[rect_type]["height"] - grid_x = config[rect_type]["grid"]["cols"] - grid_y = config[rect_type]["grid"]["rows"] - - # Set the labels and offsets and font sizes from the config - label = config[rect_type]["label"]["main"] - q_label = config[rect_type]["label"]["rows"] - a_label = config[rect_type]["label"]["cols"] - - label_offset = config[rect_type]["label_offset"]["main"] - q_offset = config[rect_type]["label_offset"]["rows"] - a_offset = config[rect_type]["label_offset"]["cols"] - - label_font_size = config[rect_type]["label_font_size"]["main"] - q_label_fontsize = config[rect_type]["label_font_size"]["rows"] - a_label_fontsize = config[rect_type]["label_font_size"]["cols"] - - # Set up the question label and answer label correctly - if rect_type == "answer_rect": - global question_number # Global question number counter - - # If this is the last rectangle, that could have less questions - if last_rect_q is not None: - q_label = [str(i + question_number) for i in range(last_rect_q)] - question_number += last_rect_q - # Else normal rectangle - else: - q_label = [str(i + question_number) for i in range(grid_y)] - question_number += grid_y - - # If the answer labels are alphabetic use ABCD... else if numeric use 1234... - if a_label == "alphabetic": - a_label = [chr(65 + i) for i in range(grid_x)] - elif a_label == "numeric": - a_label = [str(i + 1) for i in range(grid_x)] - - # Set up the student ID label correctly - if rect_type == "student_id_rect": - q_label = [str(i) for i in range(grid_y)] - # Rounded corners rectangle - round_rect = patches.FancyBboxPatch((rect_x, rect_y), width, height, edgecolor=rect_color, facecolor="none", - linewidth=rect_width, boxstyle="round,pad=0.01") - ax.add_patch(round_rect) + cols = config[rect_type]["grid"]["cols"] + rows = config[rect_type]["grid"]["rows"] # Width and Height of grid cell - grid_width = width / grid_x - grid_height = height / grid_y + grid_width = width / cols + grid_height = height / rows # Gray out every other column if gray_columns: - for i in range(grid_x): + for i in range(cols): x = rect_x + grid_width * i if i % 2 == 0: rect = patches.Rectangle((x, rect_y), grid_width, height, edgecolor=off_color, @@ -87,16 +74,42 @@ def draw_rect(ax, config, rect_x, rect_y, rect_type="answer_rect", gray_columns= ax.add_patch(rect) # Gray out every other row else: - for i in range(grid_y): + for i in range(rows): y = rect_y + grid_height * i if i % 2 == 0: rect = patches.Rectangle((rect_x, y), width, grid_height, edgecolor=off_color, facecolor=off_color, linewidth=rect_width) ax.add_patch(rect) - # Draw the bubbles - for i in range(grid_x): - for j in range(grid_y): + +def draw_bubbles(ax, config, rect_x, rect_y, rect_type, last_rect_q, student_id): + """ + Draw the bubbles in the rectangle + :param ax: The axis to draw the rectangle on + :param config: Configuration dictionary + :param rect_x: The x-coordinate of the rectangle top left corner + :param rect_y: The y-coordinate of the rectangle top left corner + :param rect_type: Type of the rectangle (Student ID or Answers) + :param last_rect_q: Number of questions in the last rectangle + :param student_id: Student ID + """ + # Configuration + rect_color = config["colors"]["main_color"] + + rect_width = config["rect_settings"]["rect_line_width"] + + width = config[rect_type]["width"] + height = config[rect_type]["height"] + + cols = config[rect_type]["grid"]["cols"] + rows = config[rect_type]["grid"]["rows"] + + # Width and Height of grid cell + grid_width = width / cols + grid_height = height / rows + + for i in range(cols): + for j in range(rows): # Skip the bubbles if this is the last rectangle and the question number is less than the last_rect_q if last_rect_q is not None and j < last_rect_q: continue @@ -104,12 +117,97 @@ def draw_rect(ax, config, rect_x, rect_y, rect_type="answer_rect", gray_columns= # Calculate the position of the bubble x = rect_x + grid_width * i y = rect_y + grid_height * j + + # If this is the student ID rectangle, fill the bubbles according to the student ID + face_color = "none" + if rect_type == "student_id_rect": + y = rect_y + height - (grid_height * (j + 1)) + if student_id[i] == str(j): + face_color = rect_color + # Draw the bubble circle = patches.Circle((x + grid_width / 2, y + grid_height / 2), grid_width / 3, - edgecolor=rect_color, facecolor="none", linewidth=rect_width) + edgecolor=rect_color, facecolor=face_color, linewidth=rect_width) ax.add_patch(circle) - # Draw the big label (if it is set in the config) + +def setup_labels(config, rect_type, last_rect_q=None): + """ + Set up the question label and answer label correctly + :param config: Configuration dictionary + :param rect_type: Type of the rectangle (Student ID or Answers) + :param last_rect_q: Number of questions in the last rectangle + :return: Question label and Answer label + """ + # Configuration + cols = config[rect_type]["grid"]["cols"] + rows = config[rect_type]["grid"]["rows"] + + q_label = config[rect_type]["label"]["rows"] + a_label = config[rect_type]["label"]["cols"] + + # Set up the question label and answer label correctly + if rect_type == "answer_rect": + global question_number # Global question number counter + + # If this is the last rectangle, that could have less questions + if last_rect_q is not None: + q_label = [str(i + question_number) for i in range(last_rect_q)] + question_number += last_rect_q + # Else normal rectangle + else: + q_label = [str(i + question_number) for i in range(rows)] + question_number += rows + + # If the answer labels are alphabetic use ABCD... else if numeric use 1234... + if a_label == "alphabetic": + a_label = [chr(65 + i) for i in range(cols)] + elif a_label == "numeric": + a_label = [str(i + 1) for i in range(cols)] + + # Set up the student ID label correctly + if rect_type == "student_id_rect": + q_label = [str(i) for i in range(rows)] + + return q_label, a_label + + +def draw_labels(ax, config, rect_x, rect_y, rect_type, last_rect_q=None): + """ + Draw the labels of questions and answers + :param ax: The axis to draw the rectangle on + :param config: Configuration dictionary + :param rect_x: The x-coordinate of the rectangle top left corner + :param rect_y: The y-coordinate of the rectangle top left corner + :param rect_type: Type of the rectangle (Student ID or Answers) + :param last_rect_q: Number of questions in the last rectangle + """ + # Configuration + text_color = config["colors"]["text_color"] + + width = config[rect_type]["width"] + height = config[rect_type]["height"] + + label = config[rect_type]["label"]["main"] + + cols = config[rect_type]["grid"]["cols"] + rows = config[rect_type]["grid"]["rows"] + + label_offset = config[rect_type]["label_offset"]["main"] + q_offset = config[rect_type]["label_offset"]["rows"] + a_offset = config[rect_type]["label_offset"]["cols"] + + label_font_size = config[rect_type]["label_font_size"]["main"] + q_label_fontsize = config[rect_type]["label_font_size"]["rows"] + a_label_fontsize = config[rect_type]["label_font_size"]["cols"] + + # Set up the question label and answer label correctly (or student ID label) + q_label, a_label = setup_labels(config, rect_type, last_rect_q=last_rect_q) + + # Width and Height of grid cell + grid_width = width / cols + grid_height = height / rows + if label != "": ax.text(rect_x + width / 2, rect_y + height + label_offset, label, ha='center', va='center', fontsize=label_font_size) @@ -125,9 +223,45 @@ def draw_rect(ax, config, rect_x, rect_y, rect_type="answer_rect", gray_columns= ha='center', va='center', fontsize=q_label_fontsize, color=text_color) +def draw_rect(ax, config, rect_x, rect_y, rect_type="answer_rect", gray_columns=False, last_rect_q=None, student_id=0): + """ + Draws a rectangle including circles to be filled in the final bubble sheet + :param ax: The axis to draw the rectangle on + :param config: Configuration dictionary + :param rect_x: The x-coordinate of the rectangle top left corner + :param rect_y: The y-coordinate of the rectangle top left corner + :param rect_type: Type of the rectangle (Student ID or Answers) + :param gray_columns: If true every other column will be grayed out, otherwise every other row + :param last_rect_q: Number of questions in the last rectangle + :param student_id: Student ID + """ + # Configuration + rect_color = config["colors"]["main_color"] + + rect_width = config["rect_settings"]["rect_line_width"] + + width = config[rect_type]["width"] + height = config[rect_type]["height"] + + # Rounded corners rectangle + round_rect = patches.FancyBboxPatch((rect_x, rect_y), width, height, edgecolor=rect_color, facecolor="none", + linewidth=rect_width, boxstyle="round,pad=0.01") + ax.add_patch(round_rect) + + # Gray out every other column or row + gray_out(ax, config, rect_x, rect_y, rect_type=rect_type, gray_columns=gray_columns) + + # Draw the bubbles + draw_bubbles(ax, config, rect_x, rect_y, rect_type, last_rect_q, str(student_id).zfill(4)) + + # Draw labels + draw_labels(ax, config, rect_x, rect_y, rect_type, last_rect_q=last_rect_q) + + def generate_bubble_sheet(student_id): """ Main function to generate the bubble sheet + :param student_id: Student ID (number from 0 to 9999) """ # Load the configuration file config = load_config() @@ -142,7 +276,10 @@ def generate_bubble_sheet(student_id): # Define the Student ID field x = config["student_id_rect"]["x"] y = config["student_id_rect"]["y"] - draw_rect(ax, config, x, y, rect_type="student_id_rect", gray_columns=True) + draw_rect(ax, config, x, y, rect_type="student_id_rect", gray_columns=True, student_id=student_id) + + # Draw the header + draw_header(ax, x, 1 - y, config) # Offset between rectangles offset_between_rect = config["rect_settings"]["rect_space_between"] diff --git a/ai/src/config.json b/ai/src/config.json index cd185397fe89b57512a2f1205cdf749fe314f383..3669a340419329f7fb3d2daf77a96cd22acb3ccd 100644 --- a/ai/src/config.json +++ b/ai/src/config.json @@ -5,9 +5,11 @@ "text_color": "gray" }, "header": { - "title": "Multiple Choice Answer Sheet", - "date": "Date: ", - "signature": "Signature: " + "font_size": 15, + "font": "Arial", + "title": "Prvnà pÃsemná práce z pÅ™edmÄ›tu KIV/ADT", + "date": "Datum: ", + "name": "Jméno: " }, "rect_settings": { "rect_line_width": 2, @@ -23,17 +25,17 @@ "cols": 4 }, "label": { - "main": "Student ID", + "main": "ID Studenta", "rows": "", "cols": "" }, "label_offset": { - "main": 0.05, + "main": 0.04, "rows": 0.04, "cols": 0.05 }, "label_font_size": { - "main": 20, + "main": 15, "rows": 12, "cols": 12 } diff --git a/ai/src/preprocessor.py b/ai/src/preprocessor.py index b683ca8ea4cdfc3608bafeb3b097d5b2bc9283f6..2644a9d7581b3ac93e219dc721bedf933249a018 100644 --- a/ai/src/preprocessor.py +++ b/ai/src/preprocessor.py @@ -6,7 +6,7 @@ import skimage.filters.thresholding as th import pythreshold.utils as putils import imutils.contours -IMG_FOLDER = "tsp_zaznamove_archy" +IMG_FOLDER = "../tsp_zaznamove_archy" INPUT_FILE = "naskenovany_vyplneny.pdf" diff --git a/ai/src/tsp_zaznamove_archy/naskenovany_prazdny.pdf b/ai/tsp_zaznamove_archy/naskenovany_prazdny.pdf similarity index 100% rename from ai/src/tsp_zaznamove_archy/naskenovany_prazdny.pdf rename to ai/tsp_zaznamove_archy/naskenovany_prazdny.pdf diff --git a/ai/src/tsp_zaznamove_archy/naskenovany_vyplneny.pdf b/ai/tsp_zaznamove_archy/naskenovany_vyplneny.pdf similarity index 100% rename from ai/src/tsp_zaznamove_archy/naskenovany_vyplneny.pdf rename to ai/tsp_zaznamove_archy/naskenovany_vyplneny.pdf diff --git a/ai/src/tsp_zaznamove_archy/vygenerovany.pdf b/ai/tsp_zaznamove_archy/vygenerovany.pdf similarity index 100% rename from ai/src/tsp_zaznamove_archy/vygenerovany.pdf rename to ai/tsp_zaznamove_archy/vygenerovany.pdf