diff --git a/client/views/Exam.jsx b/client/views/Exam.jsx index 3ccbc0087586e63a07fe50a09b7b2900dded0730..85737fb3b67934e7777f872f1bc5a1f0763100c5 100644 --- a/client/views/Exam.jsx +++ b/client/views/Exam.jsx @@ -42,11 +42,7 @@ class Exams extends React.Component { page: problem.page, name: problem.name, graded: problem.graded, - mc_options: problem.mc_options.map((option) => { - option.widget.x -= 7 - option.widget.y -= 21 - return option - }), + mc_options: problem.mc_options, isMCQ: problem.mc_options && problem.mc_options.length !== 0 // is the problem a mc question - used to display PanelMCQ } } @@ -365,8 +361,8 @@ class Exams extends React.Component { 'feedback_id': null, 'widget': { 'name': 'mc_option_' + labels[index], - 'x': xPos + 7, - 'y': yPos + 21, + 'x': xPos, + 'y': yPos, 'type': 'mcq_widget' } } @@ -379,8 +375,6 @@ class Exams extends React.Component { formData.append('label', data.label) api.put('mult-choice/', formData).then(result => { data.id = result.mult_choice_id - data.widget.x -= 7 - data.widget.y -= 21 this.createNewMCOWidget(problemWidget, data) this.generateAnswerBoxes(problemWidget, labels, index + 1, xPos + 24, yPos) }).catch(err => { diff --git a/client/views/ExamEditor.jsx b/client/views/ExamEditor.jsx index 311cadafb250703af69f983cae03a8e88438ea5e..15781e4c19b4aa826c3fb22667c62393a41cfeb9 100644 --- a/client/views/ExamEditor.jsx +++ b/client/views/ExamEditor.jsx @@ -187,27 +187,6 @@ class ExamEditor extends React.Component { this.props.updateExam() }) } - - updateState = (widget, data) => { - this.props.updateMCWidgetPosition(widget, { - x: Math.round(data.x), - y: Math.round(data.y) - }) - } - - updateMCOPosition = (widget, data) => { - this.updateState(widget, data) - - widget.problem.mc_options.forEach( - (option, i) => { - let newData = { - x: Math.round(data.x) + i * 24 + 7, - y: Math.round(data.y) + 21 - } - this.updateWidgetPositionDB(option, newData) - }) - } - /** * This function renders a group of options into one draggable widget * @returns {*} @@ -217,9 +196,6 @@ class ExamEditor extends React.Component { let height = 38 let enableResizing = false const isSelected = widget.id === this.props.selectedWidgetId - let xPos = widget.problem.mc_options[0].widget.x - let yPos = widget.problem.mc_options[0].widget.y - return ( <ResizeAndDrag key={'widget_mc_' + widget.id} @@ -237,8 +213,8 @@ class ExamEditor extends React.Component { topRight: enableResizing }} position={{ - x: xPos, - y: yPos + x: widget.problem.mc_options[0].widget.x, + y: widget.problem.mc_options[0].widget.y }} size={{ width: width, @@ -248,7 +224,19 @@ class ExamEditor extends React.Component { this.props.selectWidget(widget.id) }} onDragStop={(e, data) => { - this.updateMCOPosition(widget, data) + this.props.updateMCWidgetPosition(widget, { + x: Math.round(data.x), + y: Math.round(data.y) + }) + + widget.problem.mc_options.forEach( + (option, i) => { + let newData = { + x: Math.round(data.x), + y: Math.round(data.y) + i * 24 + } + this.updateWidgetPositionDB(option, newData) + }) }} > <div className={isSelected ? 'mcq-widget widget selected' : 'mcq-widget widget '}> @@ -330,28 +318,6 @@ class ExamEditor extends React.Component { onDragStart={() => { this.props.selectWidget(widget.id) }} - onDrag={(e, data) => { - if (widget.problem.mc_options.length > 0) { - let xPos = widget.problem.mc_options[0].widget.x - let yPos = widget.problem.mc_options[0].widget.y - let width = 24 * widget.problem.mc_options.length - let height = 38 - - if (xPos < data.x) { - xPos = data.x - } else if (xPos + width > data.x + widget.width) { - xPos = data.x + widget.width - width - } - - if (yPos < data.y) { - yPos = data.y - } else if (yPos + height > data.y + widget.height) { - yPos = data.y + widget.height - height - } - - this.updateState(widget, { x: xPos, y: yPos }) - } - }} onDragStop={(e, data) => { this.props.updateWidget(widget.id, { x: { $set: Math.round(data.x) }, @@ -361,26 +327,7 @@ class ExamEditor extends React.Component { x: Math.round(data.x), y: Math.round(data.y) }).then(() => { - if (widget.problem.mc_options.length > 0) { - let xPos = widget.problem.mc_options[0].widget.x - let yPos = widget.problem.mc_options[0].widget.y - let width = 24 * widget.problem.mc_options.length - let height = 38 - - if (xPos < data.x) { - xPos = data.x - } else if (xPos + width > data.x + widget.width) { - xPos = data.x + widget.width - width - } - - if (yPos < data.y) { - yPos = data.y - } else if (yPos + height > data.y + widget.height) { - yPos = data.y + widget.height - height - } - - this.updateMCOPosition(widget, { x: xPos, y: yPos }) - } + // ok }).catch(err => { console.log(err) // update to try and get a consistent state diff --git a/tests/data/cornermarkers/a4-3-markers.png b/tests/data/cornermarkers/a4-3-markers.png deleted file mode 100644 index 32bdd11a2fbf87fed48625c3a12f0e609e2088a6..0000000000000000000000000000000000000000 Binary files a/tests/data/cornermarkers/a4-3-markers.png and /dev/null differ diff --git a/tests/data/cornermarkers/a4-rotated-3-markers.png b/tests/data/cornermarkers/a4-rotated-3-markers.png deleted file mode 100644 index d32cbacac4d4a2d8a327736ce8967623f8aca22d..0000000000000000000000000000000000000000 Binary files a/tests/data/cornermarkers/a4-rotated-3-markers.png and /dev/null differ diff --git a/tests/data/cornermarkers/a4-rotated.png b/tests/data/cornermarkers/a4-rotated.png deleted file mode 100644 index 8dbf8630a73b83a71dcc36eb687933632657f5aa..0000000000000000000000000000000000000000 Binary files a/tests/data/cornermarkers/a4-rotated.png and /dev/null differ diff --git a/tests/test_three_corners.py b/tests/test_three_corners.py deleted file mode 100644 index 1169be12790279fffebab2502cbea72e01f7117f..0000000000000000000000000000000000000000 --- a/tests/test_three_corners.py +++ /dev/null @@ -1,57 +0,0 @@ -import cv2 -import os -import numpy as np - -from zesje.images import fix_corner_markers -from zesje.scans import find_corner_marker_keypoints - - -def test_three_straight_corners_1(): - shape = (240, 200, 3) - corner_markers = [(50, 50), (120, 50), (50, 200)] - - top_left, corner_markers = fix_corner_markers(corner_markers, shape) - - assert (120, 200) in corner_markers - assert top_left == (50, 50) - - -def test_three_straight_corners_2(): - shape = (240, 200, 3) - corner_markers = [(120, 50), (50, 200), (120, 200)] - - top_left, corner_markers = fix_corner_markers(corner_markers, shape) - - assert (50, 50) in corner_markers - assert top_left == (50, 50) - - -def test_pdf(datadir): - # Max deviation of inferred corner marker and actual location - epsilon = 2 - - # Scan rotated image with 4 corner markers - image_filename1 = 'a4-rotated.png' - image_path = os.path.join(datadir, 'cornermarkers', image_filename1) - page_img = cv2.imread(image_path) - - corners1 = find_corner_marker_keypoints(page_img) - - # Scan the same image with 3 corner markers - image_filename2 = 'a4-rotated-3-markers.png' - image_path = os.path.join(datadir, 'cornermarkers', image_filename2) - page_img = cv2.imread(image_path) - - corners2 = find_corner_marker_keypoints(page_img) - - # Get marker that was removed - diff = [corner for corner in corners1 if corner not in corners2] - diff_marker = min(diff) - - _, fixed_corners2 = fix_corner_markers(corners2, page_img.shape) - added_marker = [corner for corner in fixed_corners2 if corner not in corners2][0] - - # Check if 'inferred' corner marker is not too far away - dist = np.linalg.norm(np.subtract(added_marker, diff_marker)) - - assert dist < epsilon diff --git a/zesje/api/exams.py b/zesje/api/exams.py index a1c449a05bd1ad5d5a1f50a75c7e3698d66114e5..c88db56497c2e223b93a3a6ce9b2e9574b0e72f1 100644 --- a/zesje/api/exams.py +++ b/zesje/api/exams.py @@ -47,7 +47,8 @@ def get_cb_data_for_exam(exam): cb_data = [] for problem in exam.problems: page = problem.widget.page - cb_data += [(cb.x, cb.y, page, cb.label) for cb in problem.mc_options] + if page: + cb_data += [(cb.x, cb.y, page, cb.label) for cb in problem.mc_options] return cb_data diff --git a/zesje/api/mult_choice.py b/zesje/api/mult_choice.py index 29eaa61dfa5363f18a0a7afb91d78893da1a678d..8a1b2c9ffd72ba1445b18a029efd6c62503626b8 100644 --- a/zesje/api/mult_choice.py +++ b/zesje/api/mult_choice.py @@ -64,8 +64,8 @@ class MultipleChoice(Resource): mc_type = 'mcq_widget' if not id: - # Insert new empty feedback option that links to the same problem, with the label as name - new_feedback_option = FeedbackOption(problem_id=problem_id, text=label) + # Insert new empty feedback option that links to the same problem + new_feedback_option = FeedbackOption(problem_id=problem_id, text='') db.session.add(new_feedback_option) db.session.commit() diff --git a/zesje/images.py b/zesje/images.py index 331a7a087864bf90d6d99f4e34aafb8110f75d34..0e2167f278b7d4e9e407d317c16cbd7eed00ee45 100644 --- a/zesje/images.py +++ b/zesje/images.py @@ -2,8 +2,6 @@ import numpy as np -from operator import sub, add - def guess_dpi(image_array): h, *_ = image_array.shape @@ -38,63 +36,6 @@ def get_box(image_array, box, padding=0.3): return image_array[top:bottom, left:right] -def fix_corner_markers(corner_keypoints, shape): - """ - Corrects the list of corner markers if only three corner markers are found. - This function raises if less than three corner markers are detected. - - Parameters - ---------- - corner_keypoints : - List of corner marker locations as tuples - shape : - Shape of the image in (x, y, dim) - - Returns - ------- - corner_keypoints : - A list of four corner markers. - top_left : tuple - Coordinates of the top left corner marker - """ - - if len(corner_keypoints) == 4 or len(corner_keypoints) < 3: - raise RuntimeError("Fewer then 3 corner markers found") - - x_sep = shape[1] / 2 - y_sep = shape[0] / 2 - - top_left = [(x, y) for x, y in corner_keypoints if x < x_sep and y < y_sep] - bottom_left = [(x, y) for x, y in corner_keypoints if x < x_sep and y > y_sep] - top_right = [(x, y) for x, y in corner_keypoints if x > x_sep and y < y_sep] - bottom_right = [(x, y) for x, y in corner_keypoints if x > x_sep and y > y_sep] - - missing_point = () - - if not top_left: - # Top left point is missing - (dx, dy) = tuple(map(sub, top_right[0], bottom_right[0])) - missing_point = tuple(map(add, bottom_left[0], (dx, dy))) - top_left = [missing_point] - - elif not bottom_left: - # Bottom left point is missing - (dx, dy) = tuple(map(sub, top_right[0], bottom_right[0])) - missing_point = tuple(map(sub, top_left[0], (dx, dy))) - - elif not top_right: - # Top right point is missing - (dx, dy) = tuple(map(sub, top_left[0], bottom_left[0])) - missing_point = tuple(map(add, bottom_right[0], (dx, dy))) - - elif not bottom_right: - # bottom right - (dx, dy) = tuple(map(sub, top_left[0], bottom_left[0])) - missing_point = tuple(map(sub, top_right[0], (dx, dy))) - - return top_left[0], corner_keypoints + [missing_point] - - def box_is_filled(image_array, box_coords, padding=0.3, threshold=150, pixels=False): """ Determines if a box is filled diff --git a/zesje/pdf_generation.py b/zesje/pdf_generation.py index 09bf9641bd13e631eb02a163f69ef2959b7955d6..0c0b8c5ae143e7ff7004adf57aaf353fc96ad565 100644 --- a/zesje/pdf_generation.py +++ b/zesje/pdf_generation.py @@ -162,27 +162,25 @@ def generate_checkbox(canvas, x, y, label): canvas : reportlab canvas object x : int - the x coordinate of the top left corner of the box in points (pt) + the x coordinate of the top left corner of the box in pixels y : int - the y coordinate of the top left corner of the box in points (pt) + the y coordinate of the top left corner of the box in pixels label: str A string representing the label that is drawn on top of the box, will only take the first character """ fontsize = 11 # Size of font margin = 5 # Margin between elements and sides - markboxsize = fontsize - 2 # Size of checkboxes boxes - x_label = x + 1 # location of the label - y_label = y + margin # remove fontsize from the y label since we draw from the bottom left up - box_y = y - markboxsize # remove the markboxsize because the y is the coord of the top - # and reportlab prints from the bottom + markboxsize = fontsize - 2 # Size of student number boxes + x_label = x + 1 + y_label = y + margin + fontsize # check that there is a label to print if (label and not (len(label) == 0)): canvas.setFont('Helvetica', fontsize) canvas.drawString(x_label, y_label, label[0]) - canvas.rect(x, box_y, markboxsize, markboxsize) + canvas.rect(x, y, markboxsize, markboxsize) def generate_datamatrix(exam_id, page_num, copy_num): @@ -275,8 +273,6 @@ def _generate_overlay(canv, pagesize, exam_id, copy_num, num_pages, id_grid_x, index = 0 max_index = len(cb_data) cb_data = sorted(cb_data, key=lambda tup: tup[2]) - # invert the y axis - cb_data = [(cb[0], pagesize[1] - cb[1], cb[2], cb[3]) for cb in cb_data] else: index = 0 max_index = 0 diff --git a/zesje/pregrader.py b/zesje/pregrader.py deleted file mode 100644 index 4954bc8a7d2083f782ce0c03e42c74284e2e12f7..0000000000000000000000000000000000000000 --- a/zesje/pregrader.py +++ /dev/null @@ -1,135 +0,0 @@ -import cv2 -import numpy as np - -from zesje.database import db, Solution -from zesje.images import guess_dpi, get_box, fix_corner_markers - - -def add_feedback_to_solution(sub, exam, page, page_img, corner_keypoints): - """ - Adds the multiple choice options that are identified as marked as a feedback option to a solution - - Parameters - ------ - sub : Submission - the current submission - exam : Exam - the current exam - page_img : Image - image of the page - corner_keypoints : array - locations of the corner keypoints as (x, y) tuples - """ - problems_on_page = [problem for problem in exam.problems if problem.widget.page == page] - - top_left_point, fixed_corner_keypoints = fix_corner_markers(corner_keypoints, page_img.shape) - - for problem in problems_on_page: - sol = Solution.query.filter(Solution.problem_id == problem.id, Solution.submission_id == sub.id).one_or_none() - - for mc_option in problem.mc_options: - box = (mc_option.x, mc_option.y) - - if box_is_filled(box, page_img, top_left_point): - feedback = mc_option.feedback - sol.feedback.append(feedback) - db.session.commit() - - -def box_is_filled(box, page_img, corner_keypoints, marker_margin=72/2.54, threshold=225, cut_padding=0.1, box_size=11): - """ - A function that finds the checkbox in a general area and then checks if it is filled in. - - Params - ------ - box: (int, int) - The coordinates of the top left (x,y) of the checkbox in points. - page_img: np.array - A numpy array of the image scan - corner_keypoints: (float,float) - The x coordinate of the left markers and the y coordinate of the top markers, - used as point of reference since scans can deviate from the original. - (x,y) are both in pixels. - marker_margin: float - The margin between the corner markers and the edge of a page when generated. - threshold: int - the threshold needed for a checkbox to be considered marked range is between 0 (fully black) - and 255 (absolutely white). - cut_padding: float - The extra padding when retrieving an area where the checkbox is in inches. - box_size: int - the size of the checkbox in points. - - Output - ------ - True if the box is marked, else False. - """ - - # shouldn't be needed, but some images are drawn a bit weirdly - y_shift = 5 - # create an array with y top, y bottom, x left and x right. use the marker margin to allign to the page. - coords = np.asarray([box[1] - marker_margin + y_shift, box[1] + box_size - marker_margin + y_shift, - box[0] - marker_margin, box[0] + box_size - marker_margin])/72 - - # add the actually margin from the scan to corner markers to the coords in inches - dpi = guess_dpi(page_img) - coords[0] = coords[0] + corner_keypoints[1]/dpi - coords[1] = coords[1] + corner_keypoints[1]/dpi - coords[2] = coords[2] + corner_keypoints[0]/dpi - coords[3] = coords[3] + corner_keypoints[0]/dpi - - # get the box where we think the box is - cut_im = get_box(page_img, coords, padding=cut_padding) - - # convert to grayscale - gray_im = cv2.cvtColor(cut_im, cv2.COLOR_BGR2GRAY) - # apply threshold to only have black or white - _, bin_im = cv2.threshold(gray_im, 150, 255, cv2.THRESH_BINARY) - - h_bin, w_bin, *_ = bin_im.shape - # create a mask that gets applied when floodfill the white - mask = np.zeros((h_bin+2, w_bin+2), np.uint8) - flood_im = bin_im.copy() - # fill the image from the top left - cv2.floodFill(flood_im, mask, (0, 0), 0) - # fill it from the bottom right just in case the top left doesn't cover all the white - cv2.floodFill(flood_im, mask, (h_bin-2, w_bin-2), 0) - - # find white parts - coords = cv2.findNonZero(flood_im) - # Find a bounding box of the white parts - x, y, w, h = cv2.boundingRect(coords) - # cut the image to this box - res_rect = bin_im[y:y+h, x:x+w] - - # the size in pixels we expect the drawn box to - box_size_px = box_size*dpi / 72 - - # if the rectangle is bigger (higher) than expected, cut the image up a bit - if h > 1.5 * box_size_px: - print("in h resize") - y_partition = 0.333 - # try getting another bounding box on bottom 2/3 of the screen - coords2 = cv2.findNonZero(flood_im[y + int(y_partition * h): y + h, x: x+w]) - x2, y2, w2, h2 = cv2.boundingRect(coords2) - # add these coords to create a new bounding box we are looking at - new_y = y+y2 + int(y_partition * h) - new_x = x + x2 - res_rect = bin_im[new_y:new_y + h2, new_x:new_x + w2] - - else: - new_x, new_y, w2, h2 = x, y, w, h - - # do the same for width - if w2 > 1.5 * box_size_px: - # usually the checkbox is somewhere in the bottom left of the bounding box - coords3 = cv2.findNonZero(flood_im[new_y: new_y + h2, new_x: new_x + int(0.66 * w2)]) - x3, y3, w3, h3 = cv2.boundingRect(coords3) - res_rect = bin_im[new_y + y3: new_y + y3 + h3, new_x + x3: new_x + x3 + w3] - - # if the found box is smaller than a certain threshold - # it means that we only found a little bit of white and the box is filled - res_x, res_y, *_ = res_rect.shape - if res_x < 0.333 * box_size_px or res_y < 0.333 * box_size_px: - return True - return np.average(res_rect) < threshold diff --git a/zesje/scans.py b/zesje/scans.py index 4d3e1c5b963ef2b3432ad95c8a62c712e550d2ac..bcbde9f68e30f93eea06f2403cc5d0a35844f6ce 100644 --- a/zesje/scans.py +++ b/zesje/scans.py @@ -5,7 +5,6 @@ import os from collections import namedtuple, Counter from io import BytesIO import signal -import traceback import cv2 import numpy as np @@ -18,7 +17,7 @@ from .database import db, Scan, Exam, Page, Student, Submission, Solution, ExamW from .datamatrix import decode_raw_datamatrix from .images import guess_dpi, get_box from .factory import make_celery -from .pregrader import add_feedback_to_solution + ExtractedBarcode = namedtuple('ExtractedBarcode', ['token', 'copy', 'page']) @@ -55,7 +54,7 @@ def process_pdf(scan_id): # TODO: When #182 is implemented, properly separate user-facing # messages (written to DB) from developer-facing messages, # which should be written into the log. - write_pdf_status(scan_id, 'error', f"Unexpected error: {error}\n Traceback:\n" + traceback.format_exc()) + write_pdf_status(scan_id, 'error', "Unexpected error: " + str(error)) def _process_pdf(scan_id, app_config): @@ -92,8 +91,8 @@ def _process_pdf(scan_id, app_config): print(description) failures.append(page) except Exception as e: - report_error(f'Error processing page {e}.\nTraceback:\n{traceback.format_exc()}') - raise + report_error(f'Error processing page {page}: {e}') + return except Exception as e: report_error(f"Failed to read pdf: {e}") raise @@ -338,13 +337,7 @@ def process_page(image_data, exam_config, output_dir=None, strict=False): else: return True, "Testing, image not saved and database not updated." - sub, exam = update_database(image_path, barcode) - - try: - add_feedback_to_solution(sub, exam, barcode.page, image_array, corner_keypoints) - except RuntimeError as e: - if strict: - return False, str(e) + update_database(image_path, barcode) if barcode.page == 0: description = guess_student( @@ -392,12 +385,8 @@ def update_database(image_path, barcode): Returns ------- - sub, exam where - - sub : Submission - the current submission - exam : Exam - the current exam + signature_validated : bool + If the corresponding submission has a validated signature. """ exam = Exam.query.filter(Exam.token == barcode.token).first() if exam is None: @@ -417,8 +406,6 @@ def update_database(image_path, barcode): db.session.commit() - return sub, exam - def decode_barcode(image, exam_config): """Extract a barcode from a PIL Image."""