Skip to content

Commit

Permalink
Switch to using three-point change of basis
Browse files Browse the repository at this point in the history
  • Loading branch information
iansan5653 committed Nov 5, 2019
1 parent 73b99e3 commit 089d98e
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 82 deletions.
13 changes: 9 additions & 4 deletions code/bubble_sheet_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@

folders_prompt = user_interface.MainWindow()
input_folder = folders_prompt.input_folder
image_paths = file_handling.filter_images(file_handling.list_file_paths(input_folder))
image_paths = file_handling.filter_images(
file_handling.list_file_paths(input_folder))
output_folder = folders_prompt.output_folder
multi_answers_as_f = folders_prompt.multi_answers_as_f
keys_file = folders_prompt.keys_file
arrangement_file = folders_prompt.arrangement_file

progress = user_interface.ProgressTracker(folders_prompt.root, len(image_paths))
progress = user_interface.ProgressTracker(folders_prompt.root,
len(image_paths))

for image_path in image_paths:
progress.set_status(f"Processing '{image_path.name}'.")
Expand All @@ -32,7 +34,9 @@
try:
corners = corner_finding.find_corner_marks(prepared_image)
except corner_finding.CornerFindingError:
progress.set_status(f"Error with '{image_path.name}': couldn't find corners. Skipping...")
progress.set_status(
f"Error with '{image_path.name}': couldn't find corners. Skipping..."
)
time.sleep(1)
continue

Expand Down Expand Up @@ -78,7 +82,8 @@
scores.save(output_folder / "scores.csv")
success_string += "✔️ All scored results processed and saved to 'scores.csv'."
if arrangement_file:
data_exporting.save_reordered_version(scores, arrangement_file, output_folder / "reordered.csv")
data_exporting.save_reordered_version(scores, arrangement_file,
output_folder / "reordered.csv")
success_string += "✔️ Reordered results saved to 'reordered.csv'."
else:
success_string += "No exam keys were found, so no scoring was performed."
Expand Down
53 changes: 17 additions & 36 deletions code/corner_finding.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import math
import typing

import numpy as np
Expand Down Expand Up @@ -126,12 +125,10 @@ def find_corner_marks(image: np.ndarray) -> geometry_utils.Polygon:
except WrongShapeError:
continue

[a, b] = l_mark.polygon[0:2]
rotation = math.atan2(b.y - a.y, b.x - a.x) - math.radians(90)
to_new_basis, from_new_basis = geometry_utils.create_change_of_basis(
a, rotation)
nominal_to_right_side = 49.5 * l_mark.unit_length
nominal_to_bottom = -(66.5 * l_mark.unit_length)
to_new_basis, _ = geometry_utils.create_change_of_basis(
l_mark.polygon[0], l_mark.polygon[5], l_mark.polygon[4])
nominal_to_right_side = 50 - 0.5
nominal_to_bottom = ((64 - 0.5) / 2)
tolerance = 0.1 * nominal_to_right_side

top_right_squares = []
Expand All @@ -147,22 +144,19 @@ def find_corner_marks(image: np.ndarray) -> geometry_utils.Polygon:
centroid_new_basis = to_new_basis(centroid)

if math_utils.is_within_tolerance(
centroid_new_basis.x, -0.5 * l_mark.unit_length,
centroid_new_basis.x, nominal_to_right_side,
tolerance) and math_utils.is_within_tolerance(
centroid_new_basis.y, nominal_to_right_side,
tolerance):
centroid_new_basis.y, 0.5, tolerance):
top_right_squares.append(square)
elif math_utils.is_within_tolerance(
centroid_new_basis.x, nominal_to_bottom,
centroid_new_basis.x, 0.5,
tolerance) and math_utils.is_within_tolerance(
centroid_new_basis.y, -0.5 * l_mark.unit_length,
tolerance):
centroid_new_basis.y, nominal_to_bottom, tolerance):
bottom_left_squares.append(square)
elif math_utils.is_within_tolerance(
centroid_new_basis.x, nominal_to_bottom,
centroid_new_basis.x, nominal_to_right_side,
tolerance) and math_utils.is_within_tolerance(
centroid_new_basis.y, nominal_to_right_side,
tolerance):
centroid_new_basis.y, nominal_to_bottom, tolerance):
bottom_right_squares.append(square)

if len(top_right_squares) == 0 or len(bottom_left_squares) == 0 or len(
Expand All @@ -172,26 +166,13 @@ def find_corner_marks(image: np.ndarray) -> geometry_utils.Polygon:
# TODO: When multiple, either progressively decrease tolerance or
# choose closest to centroid

top_right_square = [
to_new_basis(p) for p in top_right_squares[0].polygon
]
bottom_left_square = [
to_new_basis(p) for p in bottom_left_squares[0].polygon
]
bottom_right_square = [
to_new_basis(p) for p in bottom_right_squares[0].polygon
]

top_left_corner = a
top_right_corner = from_new_basis(
geometry_utils.get_corner(top_right_square,
geometry_utils.Corner.TR))
bottom_left_corner = from_new_basis(
geometry_utils.get_corner(bottom_left_square,
geometry_utils.Corner.BL))
bottom_right_corner = from_new_basis(
geometry_utils.get_corner(bottom_right_square,
geometry_utils.Corner.BR))
top_left_corner = l_mark.polygon[0]
top_right_corner = geometry_utils.get_corner(
top_right_squares[0].polygon, geometry_utils.Corner.TR)
bottom_right_corner = geometry_utils.get_corner(
bottom_right_squares[0].polygon, geometry_utils.Corner.BR)
bottom_left_corner = geometry_utils.get_corner(
bottom_left_squares[0].polygon, geometry_utils.Corner.BL)

return [
top_left_corner, top_right_corner, bottom_right_corner,
Expand Down
61 changes: 39 additions & 22 deletions code/geometry_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,33 +193,50 @@ def extend_ray(a: Point, b: Point, distance: float):


def create_change_of_basis(
new_origin: Point,
theta: float) -> typing.Tuple[typing.Callable[[Point], Point], typing.
Callable[[Point], Point]]:
"""Returns functions that will convert points from the current coordinate
system to a new one where the origin is translated to `new_origin` and the
axis are rotated `theta` radians CCW.
origin: Point, bottom_left: Point, bottom_right: Point
) -> typing.Tuple[typing.Callable[[Point], Point], typing.
Callable[[Point], Point]]:
"""Returns functions that will convert points to/from the current coordinate
system to a new one where the passed `origin` point becomes `0,0`, the
`bottom_left` point becomes `0,1`, and the `bottom_right` point becomes `1,1`.
Returns:
A tuple where the first element is a function that converts
points to the new system, and the second is a function that converts
points _to_ the new system, and the second is a function that converts
them back.
"""
origin = Point(0, 0)
target_origin = Point(0, 0)
target_bl = Point(0, 1)
target_br = Point(1, 1)
target_matrix = np.array([[target_origin.x], [target_bl.x], [target_br.x],
[target_origin.y], [target_bl.y], [target_br.y]],
float)

from_matrix = np.array([[origin.x, origin.y, 1, 0, 0, 0],
[bottom_left.x, bottom_left.y, 1, 0, 0, 0],
[bottom_right.x, bottom_right.y, 1, 0, 0, 0],
[0, 0, 0, origin.x, origin.y, 1],
[0, 0, 0, bottom_left.x, bottom_left.y, 1],
[0, 0, 0, bottom_right.x, bottom_right.y, 1]],
float)

result = np.matmul(np.linalg.inv(from_matrix), target_matrix)
transformation_matrix = np.array([[result[0][0], result[1][0]],
[result[3][0], result[4][0]]])
transformation_matrix_inv = np.linalg.inv(transformation_matrix)
rotation_matrix = np.array([[result[2][0]], [result[5][0]]])

def to_basis(point: Point) -> Point:
translated = Point(point.x - new_origin.x, point.y - new_origin.y)
r = calc_2d_dist(origin, translated)
phi = math.atan2(translated.y, translated.x)
to_phi = phi - theta
return Point(r * math.cos(to_phi), r * math.sin(to_phi))
point_vector = np.array([[point.x], [point.y]], float)
result = np.matmul(transformation_matrix,
point_vector) + rotation_matrix
return Point(result[0][0], result[1][0])

def from_basis(point: Point) -> Point:
r = calc_2d_dist(origin, point)
phi = math.atan2(point.y, point.x)
to_phi = phi + theta
rotated = Point(r * math.cos(to_phi), r * math.sin(to_phi))
return Point(rotated.x + new_origin.x, rotated.y + new_origin.y)
point_vector = np.array([[point.x], [point.y]], float)
result = np.matmul(transformation_matrix_inv,
(point_vector - rotation_matrix))
return Point(result[0][0], result[1][0])

return to_basis, from_basis

Expand Down Expand Up @@ -248,16 +265,16 @@ def get_corner(square: Polygon, corner: Corner) -> Point:
"""Gets the point representing the given corner of the square. Square should
be pretty close to vertical - horizontal. """
xs = [p.x for p in square]
highest_xs = list_utils.find_greatest_value_indexes(xs, 2)
highest_xs = sorted(list_utils.find_greatest_value_indexes(xs, 2))
side_points = [
p for i, p in enumerate(square)
if (corner.value[0] == 1 and i in highest_xs) or (
corner.value[0] == 0 and i not in highest_xs)
if (corner.value[1] == 1 and i in highest_xs) or (
corner.value[1] == 0 and i not in highest_xs)
]
side_ys = [p.y for p in side_points]
[highest_y] = list_utils.find_greatest_value_indexes(side_ys, 1)
corner_point = side_points[highest_y] if (
corner.value[1] == 1) else side_points[list_utils.next_index(
corner.value[0] == 0) else side_points[list_utils.next_index(
side_points, highest_y)]
return corner_point

Expand Down
32 changes: 12 additions & 20 deletions code/grid_reading.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Functions for establishing and reading the grid."""

import abc
import math
import typing

import numpy as np
Expand All @@ -10,12 +9,10 @@
import geometry_utils
import grid_info
import image_utils

""" Percent fill past which a grid cell is considered filled."""
# This was found by averaging the empty fill percents of all bubbles and adding
# 10% to that number.
GRID_CELL_FILL_THRESHOLD = 0.6

GRID_CELL_FILL_THRESHOLD = 0.59
""" The fraction cropped from each cell (the percentage of the box around each
cell that is empty space)"""
GRID_CELL_CROP_FRACTION = 0.4
Expand All @@ -41,17 +38,11 @@ def __init__(self, corners: geometry_utils.Polygon, horizontal_cells: int,
self.corners = corners
self.horizontal_cells = horizontal_cells
self.vertical_cells = vertical_cells
[a, b] = corners[0:2]
theta = math.atan2(b.y - a.y, b.x - a.x)
self._to_grid_basis, self._from_grid_basis = geometry_utils.create_change_of_basis(
corners[0], theta)

corners_in_basis = [self._to_grid_basis(c) for c in corners]
self.width = corners_in_basis[1].x
self.height = corners_in_basis[3].y
corners[0], corners[3], corners[2])

self.horizontal_cell_size = self.width / self.horizontal_cells
self.vertical_cell_size = self.height / self.vertical_cells
self.horizontal_cell_size = 1 / self.horizontal_cells
self.vertical_cell_size = 1 / self.vertical_cells

self.image = image

Expand Down Expand Up @@ -211,17 +202,16 @@ def read_value(self) -> typing.List[typing.List[str]]:
return typing.cast(typing.List[typing.List[str]], super().read_value())


def get_group_from_info(info: grid_info.GridGroupInfo, grid: Grid) -> _GridFieldGroup:
def get_group_from_info(info: grid_info.GridGroupInfo,
grid: Grid) -> _GridFieldGroup:
if info.fields_type is grid_info.FieldType.LETTER:
return LetterGridFieldGroup(grid, info.horizontal_start,
info.vertical_start, info.num_fields,
info.field_length,
info.field_orientation)
info.field_length, info.field_orientation)
else:
return NumberGridFieldGroup(grid, info.horizontal_start,
info.vertical_start, info.num_fields,
info.field_length,
info.field_orientation)
info.field_length, info.field_orientation)


def read_field(
Expand All @@ -235,7 +225,8 @@ def read_answer(
question: int, grid: Grid
) -> typing.List[typing.Union[typing.List[str], typing.List[int]]]:
"""Shortcut to read a field given just the key for it and the grid object."""
return get_group_from_info(grid_info.questions_info[question], grid).read_value()
return get_group_from_info(grid_info.questions_info[question],
grid).read_value()


def field_group_to_string(
Expand All @@ -258,7 +249,8 @@ def read_field_as_string(field: grid_info.Field, grid: Grid) -> str:
return field_group_to_string(read_field(field, grid))


def read_answer_as_string(question: int, grid: Grid, multi_answers_as_f: bool) -> str:
def read_answer_as_string(question: int, grid: Grid,
multi_answers_as_f: bool) -> str:
"""Shortcut to read a question's answer and format it as a string, given
just the question number and the grid object. """
answer = field_group_to_string(read_answer(question, grid))
Expand Down

0 comments on commit 089d98e

Please sign in to comment.