diff --git a/zesje/pregrader.py b/zesje/pregrader.py
index af594222bd87b2da96282bdc4647aed83ec48cfa..0aedfb64ce7e51a0a6fe2d4575e7ee0245b377d4 100644
--- a/zesje/pregrader.py
+++ b/zesje/pregrader.py
@@ -10,6 +10,11 @@
 # coupled feedback cannot be deleted
 
 from zesje.database import db, Exam, Submission, Solution, Problem
+from zesje.scans import find_corner_marker_keypoints
+import numpy as np
+from zesje.images import guess_dpi, get_box
+import cv2
+from PIL import Image
 
 
 def pregrade(exam_token, image):
@@ -50,8 +55,73 @@ def add_feedback_to_solution(submission, page, page_img, corner_keypoints):
                 db.session.commit()
 
 
-def box_is_filled(box, page_img, corner_keypoints):
-    pass
+def box_is_filled(box, page_img, corner_keypoints, marker_margin=72/2.54, threshold=225, cut_padding=0.1, box_size=11):
+    # 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]
+
+    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) < 225)
 
 
 def _locate_checkbox():