Commit a512b2d2 authored by Anton Akhmerov's avatar Anton Akhmerov
Browse files

Merge branch '443-improve-binary-threshold' into 'master'

Improve threshold everywhere for processing scans

Closes #435 and #443

See merge request zesje/zesje!302
parents 2576d761 e9fbded5
......@@ -88,3 +88,17 @@ def test_is_misaligned(config_app, coords, student_misaligned, reference):
width=coords[2], height=coords[2])
assert pregrader.is_problem_misaligned(problem, student_misaligned, reference)
def test_threshold(config_app, datadir):
dir = os.path.join(datadir, 'thresholds')
files = os.listdir(dir)
for filename in files:
img = Image.open(os.path.join(dir, filename))
problem = Problem(name='Problem')
problem.widget = ProblemWidget(x=0, y=0, width=img.size[0], height=img.size[1])
data = np.array(img)
reference = np.full_like(data, 255)
assert not pregrader.is_blank(problem, data, reference)
......@@ -45,3 +45,10 @@ MIN_ANSWER_SIZE_MM2 = 4
SQLALCHEMY_TRACK_MODIFICATIONS = False # Suppress future deprecation warning
ZIP_MIME_TYPES = ['application/zip', 'application/octet-stream', 'application/x-zip-compressed', 'multipart/x-zip']
# threshold for converting color images to binary
THRESHOLD_STUDENT_ID = 210
THRESHOLD_BLANK = 210
THRESHOLD_CORNER_MARKER = 175
THRESHOLD_MCQ = 175
THRESHOLD_MISALIGMENT = 175
......@@ -67,7 +67,12 @@ def widget_area(problem):
return widget_area_in
def covers(cover_img, to_cover_img, padding_pixels=0, threshold=0, kernel_size=9):
def covers(cover_img,
to_cover_img,
padding_pixels=0,
threshold=0,
binary_threshold=150,
kernel_size=9):
"""Check if an image covers another image
First, both images are converted to binary. Then, all the content
......@@ -86,14 +91,16 @@ def covers(cover_img, to_cover_img, padding_pixels=0, threshold=0, kernel_size=9
The amount of padding to remove before checking if it is covered
threshold: int
The amount of pixels that are allowed to not be covered
binary_threshold: int, between 0 and 255
The value used to convert grayscale images to binary
kernel_size: int
The diameter in pixels of the kernel that is used to thicken the lines
"""
cover = cv2.cvtColor(cover_img, cv2.COLOR_BGR2GRAY)
_, cover_bin = cv2.threshold(cover, 150, 255, cv2.THRESH_BINARY)
_, cover_bin = cv2.threshold(cover, binary_threshold, 255, cv2.THRESH_BINARY)
to_cover = cv2.cvtColor(to_cover_img, cv2.COLOR_BGR2GRAY)
_, to_cover_bin = cv2.threshold(to_cover, 150, 255, cv2.THRESH_BINARY)
_, to_cover_bin = cv2.threshold(to_cover, binary_threshold, 255, cv2.THRESH_BINARY)
kernel = np.ones((kernel_size, kernel_size), dtype=np.uint8)
cover_thick = cv2.erode(cover_bin, kernel, iterations=1)
......@@ -135,6 +142,9 @@ def is_misaligned(area_inch, img, reference, padding_inch=0.2):
img_cropped = get_box(img, area_inch, padding=padding_inch)
reference_cropped = get_box(reference, area_inch, padding=padding_inch)
binary_threshold = current_app.config['THRESHOLD_MISALIGMENT']
return not covers(img_cropped, reference_cropped,
padding_pixels=padding_pixels,
binary_threshold=binary_threshold,
kernel_size=kernel_size)
......@@ -81,13 +81,14 @@ def grade_mcq(sol, page_img):
A numpy array of the image scan
"""
box_size = current_app.config['CHECKBOX_SIZE']
binary_threshold = current_app.config['THRESHOLD_MCQ']
problem = sol.problem
mc_filled_counter = 0
for mc_option in problem.mc_options:
box = (mc_option.x, mc_option.y)
if box_is_filled(box, page_img, box_size=box_size):
if box_is_filled(box, page_img, binary_threshold=binary_threshold, box_size=box_size):
feedback = mc_option.feedback
sol.feedback.append(feedback)
mc_filled_counter += 1
......@@ -198,16 +199,24 @@ def is_blank(problem, page_img, reference_img):
min_answer_area_pixels = int(current_app.config['MIN_ANSWER_SIZE_MM2'] * dpi**2 / (mm_per_inch)**2)
binary_threshold = current_app.config['THRESHOLD_BLANK']
student = get_box(page_img, widget_area_in, padding=padding_inch)
reference = get_box(reference_img, widget_area_in, padding=padding_inch)
return covers(reference, student,
padding_pixels=padding_pixels,
kernel_size=kernel_size,
threshold=min_answer_area_pixels)
threshold=min_answer_area_pixels,
binary_threshold=binary_threshold)
def box_is_filled(box, page_img, threshold=225, cut_padding=0.05, box_size=9):
def box_is_filled(box,
page_img,
threshold=225,
binary_threshold=160,
cut_padding=0.05,
box_size=9):
"""
A function that finds the checkbox in a general area and then checks if it is filled in.
......@@ -220,6 +229,8 @@ def box_is_filled(box, page_img, threshold=225, cut_padding=0.05, box_size=9):
threshold: int
the threshold needed for a checkbox to be considered marked range is between 0 (fully black)
and 255 (absolutely white).
binary_threshold: int, between 0 and 255
The value used to convert grayscale images to binary
cut_padding: float
The extra padding when retrieving an area where the checkbox is in inches.
box_size: int
......@@ -239,7 +250,7 @@ def box_is_filled(box, page_img, threshold=225, cut_padding=0.05, box_size=9):
cut_im = get_box(page_img, coords, padding=cut_padding)
gray_im = cv2.cvtColor(cut_im, cv2.COLOR_BGR2GRAY)
_, bin_im = cv2.threshold(gray_im, 160, 255, cv2.THRESH_BINARY)
_, bin_im = cv2.threshold(gray_im, binary_threshold, 255, cv2.THRESH_BINARY)
h_bin, w_bin, *_ = bin_im.shape
mask = np.zeros((h_bin+2, w_bin+2), np.uint8)
......
......@@ -455,7 +455,8 @@ def get_student_number(image, student_id_widget_coords):
widget_image = get_box(image, student_id_widget_coords_inch, padding=0.0)
_, thresholded = cv2.threshold(widget_image, 150, 255, cv2.THRESH_BINARY)
threshold = current_app.config['THRESHOLD_STUDENT_ID']
_, thresholded = cv2.threshold(widget_image, threshold, 255, cv2.THRESH_BINARY)
box_size = current_app.config['ID_GRID_BOX_SIZE'] / inch * dpi
margin = current_app.config['ID_GRID_MARGIN'] / inch * dpi
......@@ -539,6 +540,8 @@ def find_corner_marker_keypoints(image_array, corner_sizes=[0.125, 0.25, 0.5]):
marker_area = marker_length * marker_width * 2
marker_area_min = max(marker_length * (marker_width - 1) * 2, 0) # One pixel thinner due to possible aliasing
binary_threshold = current_app.config['THRESHOLD_CORNER_MARKER']
corner_points = []
top_bottom = (True, False)
......@@ -553,7 +556,7 @@ def find_corner_marker_keypoints(image_array, corner_sizes=[0.125, 0.25, 0.5]):
w_slice = slice(0, int(w*corner_size)) if is_left else slice(int(w*(1-corner_size)), None)
gray_im = cv2.cvtColor(image_array[h_slice, w_slice], cv2.COLOR_BGR2GRAY)
_, inv_im = cv2.threshold(gray_im, 175, 255, cv2.THRESH_BINARY_INV)
_, inv_im = cv2.threshold(gray_im, binary_threshold, 255, cv2.THRESH_BINARY_INV)
ret, labels = cv2.connectedComponents(inv_im)
for label in range(1, ret):
new_img = (labels == label)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment