Current state

This commit is contained in:
Akumatic 2020-05-15 12:19:47 +02:00
commit ceee58a6bc
12 changed files with 1078 additions and 0 deletions

129
.gitignore vendored Normal file
View File

@ -0,0 +1,129 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-2020 Akumatic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# ExamScan
A Python 3 script to evaluate single and multiple choice exam sheets.
## Example
Input image:
![Example Image](/example/test.png)
Processed image:
![Processed Image](/example/test_eval.png)
## Usage
`main.py (-u URL | -f FILE) -n NUM [-h] [-i IOUT] [-d DOUT] [-c COMP] [-p]`
#### Flags:
##### Required, but mutually exclusive flags:
| Short | Long | Description |
| -- | -- | -- |
| -u URL | --url URL | URL to the image or pdf to be evaluated |
| -f FILE | --file FILE | path to the image or pdf to be evaluated |
##### Required flags:
| Short | Long | Description |
| -- | -- | -- |
| -n NUM | --num NUM | number of answers per question |
##### Optional flags:
| Short | Long | Description |
| -- | -- | -- |
|-h | --help | shows help message and exits |
| -i IOUT | --iout IOUT | path for the output picture to be stored. |
| -d DOUT | --dout DOUT | path for the output data to be stored. |
| -c COMP | --compare COMP | compares the calculated result to a given result |
| -p | --plot | plots every single step |
## Requirements:
| Module | Pip | Git |
| -- | -- | -- |
| OpenCV | [opencv-python](https://pypi.org/project/opencv-python/) | [skvark/opencv-python](https://github.com/skvark/opencv-python) |
| NumPy | [numpy](https://pypi.org/project/numpy/) | [numpy/numpy](https://github.com/numpy/numpy) |
| Matplotlib | [matplotlib](https://pypi.org/project/matplotlib/) | [matplotlib/matplotlib](https://github.com/matplotlib/matplotlib) |
| imutils | [imutils](https://pypi.org/project/imutils/) | [jrosebr1/imutils](https://github.com/jrosebr1/imutils) |
| python-magic | [python-magic](https://pypi.org/project/python-magic/) | [ahupp/python-magic](https://github.com/ahupp/python-magic) |
| pdf2image | [pdf2image](https://pypi.org/project/pdf2image/) | [Belval/pdf2image](https://github.com/Belval/pdf2image) |

BIN
example/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
example/test_eval.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

69
main.py Normal file
View File

@ -0,0 +1,69 @@
#!/usr/bin/python
# SPDX-License-Identifier: MIT
# Copyright (c) 2019 Akumatic
from scan import image, contours, io, eval
if __name__ == "__main__":
# BGR Colors
BGR_R = (0, 0, 255)
BGR_G = (0, 255, 0)
BGR_B = (255, 0, 0)
# Parse cli args
args = io.parse_args()
# Reads File
img = io.read_image(path=args.file, url=args.url)
# image copies for processing
img_orig = img.copy()
img_blur = image.blur(img)
img_thres = image.threshold(img_blur)
img_edges = image.edge_detection(img_thres.copy())
# gather contour data and find boxes. draws boxes
cnts = contours.find_contours(img_edges)
cnts_box = contours.find_boxes(cnts)
img_boxes = image.draw_contours(img_orig.copy(), cnts_box, BGR_G)
# retrieve the center of all contours and
# filters entries too close to each other.
center_boxes = contours.find_center(cnts_box)
avg_radius = int(sum([contours.dist_center_topleft(center[2],
center[:2]) for center in center_boxes]) / len(center_boxes))
contours.filter_centers(center_boxes, avg_radius)
# Sort Box data
#center_boxes = sorted(center_boxes, key=lambda x: (x[1], x[0]))
# draw center of given contours into original picture
img_center_boxes = image.draw_circles(img_orig.copy(), center_boxes, BGR_G)
img_rad = image.draw_circles(img_thres.copy(), center_boxes, BGR_G,
radius=int(avg_radius))
center_eval = [(center[0], center[1], image.ratio_black(img_thres, center[:2],
avg_radius)) for center in center_boxes]
img_eval = image.eval_image(img_orig.copy(), center_eval, avg_radius)
evaluation = eval.evaluate(center_eval, args.num, avg_radius)
print(evaluation)
if args.comp is not None:
result = eval.compare(evaluation, io.load_results(args.comp))
print(result)
# write result to file if optional flag is given
if args.iout is not None:
io.write_image(img_eval, args.iout)
print(f"Stored image to {args.iout}")
if args.dout is not None:
io.store_results(evaluation, args.dout)
print(f"Stored data to {args.dout}")
if args.plot:
io.plot(img_orig, img_blur, img_thres, img_edges,
img_boxes, img_center_boxes, img_rad, img_eval)

0
scan/__init__.py Normal file
View File

117
scan/contours.py Normal file
View File

@ -0,0 +1,117 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2019 Akumatic
import cv2, imutils, numpy
from . import utils
######################
# Contour operations #
######################
def find_contours (
image: numpy.ndarray,
mode: int = cv2.RETR_LIST,
method: int = cv2.CHAIN_APPROX_SIMPLE
) -> list:
""" Find all contours of the filtered image
Args:
image (ndarray):
the filtered image
Returns:
A list containing contour data
"""
cnts = cv2.findContours(image.copy(), mode, method)
return imutils.grab_contours(cnts)
def find_boxes (
contours: list,
thres_area: int = 500,
pad_ratio: float = 0.05
) -> (list, list):
""" Find contours that resemble a box
Args:
contours (list):
a list containing contour data
Returns:
A list containing the box contours
"""
boxes = list()
for c in contours:
area = cv2.contourArea(c)
perimeter = cv2.arcLength(c, True)
shape_factor = utils.circularity(area, perimeter)
if 0.7 < shape_factor < 0.85 and area >= thres_area:
boxes.append(c)
return boxes
def find_center (
contours: list
) -> list:
""" Find the center coordinates of all given contours.
Args:
contours (list):
A list containing contour data
Returns:
A list containing the center coordinates and the contour as tuples
(x, y, contour).
"""
centers = []
for contour in contours:
m = cv2.moments(contour)
try:
x = int(m["m10"] / m["m00"])
y = int(m["m01"] / m["m00"])
centers.append((x, y, contour))
except ZeroDivisionError:
pass
return centers
def filter_centers (
coords: list,
radius: int
):
""" Removes all but one entry in circles given by coordinates and radius
Args:
coords (list):
a list containing tuples of coordinates (x, y)
radius (float):
the radius around a center where no other center should be
"""
a = 0
while a < len(coords):
b = a + 1
while b < len(coords):
if utils.distance(coords[a][:2], coords[b][:2]) <= radius:
del coords[b]
else:
b += 1
a += 1
def dist_center_topleft (
contour: numpy.ndarray,
center: tuple
) -> float:
""" Calculates the distance from the center of a given contour to it's
top left corner
Args:
contour (ndarray):
The contour data
center (tuple):
A tuple containing the center coordinates (x, y)
Returns:
A float with the distance from center to the top left corner as value
"""
x, y, _, _ = cv2.boundingRect(contour)
return utils.distance((x, y), center[:2])

207
scan/eval.py Normal file
View File

@ -0,0 +1,207 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2019 Akumatic
from . import utils
def evaluate (
centers: list,
n: int,
radius: int
) -> list:
""" Evaluates the given data
Args:
centers (list):
contains tuples with coordinates and the black-pixel-ratio
(x, y, ratio)
n (int):
the number of possible answers per question
radius (int):
the radius of the circle around a box
Returns:
A list containings lists with evaluated answers
"""
start_of_file, _ = utils.find_closest_element(centers, (0,0))
# detect distance to next field on the right side
point_1 = start_of_file
point_r = find_next(centers, point_1, 2* radius, "r")
dist_r = utils.distance(point_1[:2], point_r[:2])
# detect distance to next field below
point_d = find_next(centers, point_1, 2 * radius, "d")
dist_d = utils.distance(point_1[:2], point_d[:2])
# detect distance to the next set of answers
for i in range(n - 2):
point_r = find_next(centers, point_r, dist_r, "r")
point_3 = find_next(centers, point_r, 3 * radius, "r", max_dist=dist_r)
if point_3 is not None:
dist_set = utils.distance(start_of_file, point_3)
else:
dist_set = None
answers = eval_set(start_of_file, centers, n, dist_r, dist_d)
if dist_set is not None:
cur = find_next(centers, start_of_file, dist_set, "r", max_dist = radius)
while cur is not None:
answers += eval_set(cur, centers, n, dist_r, dist_d)
cur = find_next(centers, cur, dist_set, "r", max_dist = radius)
return answers
def eval_set (
start: tuple,
centers: list,
n: int,
dist_r: int,
dist_d: int
) -> list:
""" Evaluates a set of answers
Args:
start (tuple):
Containing the coordinates of the top left answer of a set
centers (list):
contains tuples with coordinates and the black-pixel-ratio
(x, y, ratio)
n (int):
the number of possible answers per question
dist_r (int):
the horizontal distance between two columns of answers
dist_d (int):
the vertical distance between two rows of answers
Returns:
A list containing all evaluated data
"""
result = []
result.append(eval_row(start, centers, n, dist_r))
cur = start
while cur != None:
cur = find_next(centers, cur, dist_d, "d", max_dist=dist_r / 2)
if cur is not None:
result.append(eval_row(cur, centers, n, dist_r))
return result
def eval_row (
start: tuple,
centers: int,
n: int,
dist_r: int
) -> list:
""" Evaluates a row of a set of answers with length n
Args:
start (tuple):
Containing the coordinates of the top left answer of a set
centers (list):
contains tuples with coordinates and the black-pixel-ratio
(x, y, ratio)
n (int):
the number of possible answers per question
dist_r (int):
the horizontal distance between two columns of answers
Returns:
A list containing all evaluated answer data from a given row
"""
result = []
result.append(rate_black_pixel_ratio(start[2]))
cur = start
for i in range(n - 1):
cur = find_next(centers, cur, dist_r, "r")
result.append(rate_black_pixel_ratio(cur[2]))
return result
def rate_black_pixel_ratio (
ratio: int,
thres_1: int = 20,
thres_2: int = 50
) -> int:
""" Evaluates the given black-pixel-ratio and returns the detected marking.
Args:
ratio (int):
the ratio of black pixels compared to total pixels
thres_1 (int):
threshold for checked boxes
thres_2 (int):
threshold for corrected boxes
Returns:
-1 if a box is filled
1 if a box is checked
0 if a box is empty
"""
if ratio >= thres_2:
return -1 # corrected box
if ratio >= thres_1:
return 1 # checked
else: # empty box
return 0
def find_next (
centers: list,
cur: tuple,
dist: int,
dir: str,
max_dist: int = None
):
""" Finds the next point in a given direction with a given distance
Args:
centers (list):
contains tuples with coordinates and the black-pixel-ratio
(x, y, ratio)
cur (tuple):
the current point
dist (int):
approximated distance to the next point
dir (str):
direction of the distance. can be either "r" or "d"
max_dist (int):
max distance to be allowe
Returns:
returns the next found point.
if max_dist is given, returns None if distance > max_dist
"""
if dir == "r":
e, d = utils.find_closest_element(centers, (cur[0] + dist, cur[1]))
elif dir == "d":
e, d = utils.find_closest_element(centers, (cur[0], cur[1] + dist))
if max_dist is None or d <= max_dist:
return e
return None
def compare(
data_a: list,
data_b: list
):
""" Compares two sets of evaluated data with the same
Args:
data_a (list):
data set A
data_b (list):
data set B
Returns:
A String "Correct/Total Answers" and the percentage
"""
# asserts the number of questions is the same
assert len(data_a) == len(data_b)
cnt = 0
for i in range(len(data_a)):
if data_a[i] == data_b[i]:
cnt += 1
return (f"{cnt}/{len(data_a)}", cnt / len(data_a) * 100)

143
scan/image.py Normal file
View File

@ -0,0 +1,143 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2019 Akumatic
import cv2, imutils, numpy
def threshold (
image: numpy.ndarray,
threshold: int = 200,
) -> numpy.ndarray:
""" Converts given image to black and white
Args:
image (ndarray):
the image to be processed
threshold (int):
the threshold at which every pixel value should be set to 255
"""
_, img_binary = cv2.threshold(image, threshold, 255, cv2.THRESH_BINARY)
return img_binary
def blur (
image: numpy.ndarray,
kernel_size: int = 7
) -> numpy.ndarray:
""" Filters the given picture by converting it to gray scale and
applying gaussian blur filter
Args:
image (ndarray):
the image to be processed
kernel_size (int):
the size of the kernel for the gaussian blur
Returns:
A ndarray containing the filtered image
"""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
return cv2.GaussianBlur(gray, (kernel_size, kernel_size), 0)
def edge_detection (
image: numpy.ndarray
) -> numpy.ndarray:
""" Applies canny edge detection on a given image
Args:
image (ndarray):
the image to be processed
Returns:
A ndarray containing the image after canny detection
"""
return imutils.auto_canny(image)
def draw_circles (
image: numpy.ndarray,
centers: list,
color: tuple,
radius: int = 3,
thickness: int = 2
) -> numpy.ndarray:
""" Draws the centers of given contures with the given color,
(size, thickness)
Args:
image (ndarray):
the image to be modified
centers (list):
a list containing the center coordinates as tuples (x, y)
color (tuple):
the color coded in BGR color scale
radius (int):
size of the circles to be drawn
thickness (int):
thickness of the borders of all circles to be drawn
"""
for center in centers:
cv2.circle(img=image, center=center[:2], radius=radius,
color=color,thickness=thickness)
return image
def draw_contours (
image: numpy.ndarray,
contours: list,
color: tuple
) -> numpy.ndarray:
""" Draws the given contours in the given picture with the given color.
Args:
image (ndarray):
the image to be modified
contours (list):
A list containing contour data
color (tuple):
the color coded in BGR color scale
Returns:
the modified image as a multidimensional array
"""
img = cv2.drawContours(image, contours, -1, color, 3)
return img
def ratio_black (
image: numpy.ndarray,
center: tuple,
radius: int,
) -> int:
""" Calculates the ratio of black pixels in a given square area.
Args:
image (ndarray):
the image
center (tuple):
radius (int):
Returns:
The ratio of black pixels to all pixels
"""
cnt_black = 0
cnt_pixel = 0
for x in range(center[0] - radius, center[0] + radius):
for y in range(center[1] - radius, center[1] + radius):
if image[y, x] == 0:
cnt_black += 1
cnt_pixel += 1
return int(cnt_black / cnt_pixel * 100)
def eval_image (
image: numpy.ndarray,
data: list,
radius: int
) -> numpy.ndarray:
checked = [d[:2] for d in data if d[2] >= 20]
corrected = [d[:2] for d in data if d[2] >= 50]
draw_circles(image, checked, (0,255,0), radius, thickness=4)
draw_circles(image, corrected, (255,0,0), radius, thickness=4)
return image

280
scan/io.py Normal file
View File

@ -0,0 +1,280 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2019 Akumatic
import cv2, argparse, numpy
##############
# Exceptions #
##############
class UnsupportedFileType(Exception):
pass
class UnsupportedNumberOfPages(Exception):
pass
class MissingArg(Exception):
pass
####################
# Argument Parsing #
####################
def parse_args (
) -> argparse.Namespace:
""" Parser for cli arguments.
Returns:
A Namespace containing all parsed data
"""
# The parser itself
parser = argparse.ArgumentParser(add_help=False)
parser.description = "Evaluates single choice sheets"
# Groups for ordering arguments in help command
grp_req_excl = parser.add_argument_group("required arguments, mutually exclusive")
grp_req = parser.add_argument_group("required arguments")
grp_opt = parser.add_argument_group("optional arguments")
#########################
##### Required Args #####
#########################
# Input path - either an url or a path to a local file
io_grp = grp_req_excl.add_mutually_exclusive_group(required=True)
io_grp.add_argument("-u", "--url", dest="url",
help="URL to the image or pdf to be evaluated.")
io_grp.add_argument("-f", "--file", dest="file",
help="path to the image or pdf to be evaluated.")
# required arg for number of answers each question
grp_req.add_argument("-n", "--num", dest="num", required=True,
type=_arg_int_pos, help="number of answers per question")
#########################
##### Optional Args #####
#########################
# help message. Added manually so it is shown under optional
grp_opt.add_argument("-h", "--help", action="help", help="show this help message and exit")
# path to store the result picture to
grp_opt.add_argument("-i", "--iout", dest="iout",
help="path for the output picture to be stored.")
# path to store the result list to
grp_opt.add_argument("-d", "--dout", dest="dout",
help="path for the output data to be stored.")
# path to compare results generated by the program with data stored in a file
grp_opt.add_argument("-c", "--compare", dest="comp",
help="compares the calculated result to a given result")
# plotting all steps
grp_opt.add_argument("-p", "--plot", dest="plot", action="store_true",
help="plots every single step")
return parser.parse_args()
def _arg_int_pos (
value: str
) -> int:
""" Trying to parse the input argument into a positive value.
Args:
value (str):
The value to be checked
Returns:
The checked and casted value
Raises:
ArgumentTypeError:
Raises an ArgumentTypeError if
- value is not a number
- value is not positive
"""
try:
int_value = int(value)
except ValueError:
raise argparse.ArgumentTypeError(f"{value} is not a number.")
if int_value <= 0:
raise argparse.ArgumentTypeError(f"{value} is not a positive integer.")
return int_value
#################
# File Handling #
#################
def read_image (
path: str = None,
url: str = None
) -> numpy.ndarray:
""" Opens the image if file extension is supported.
Supported file extensions are .jpg, .jpeg, .png and .pdf
Args:
path (String): Path to local file
url (String): URL to file
Returns:
A ndarray containing the image data
"""
# Neither URL nor path provied
if url is None and path is None:
raise MissingArg
# Get data and mime type
if url:
import requests
try:
response = requests.get(url, stream=True).raw
ext = response.headers["Content-Type"].split("; ")[0]
except requests.exceptions.ConnectionError as e:
raise e
else: # path
import magic
try:
with open(path, "rb") as f:
file = f.read()
ext = magic.from_buffer(file, mime=True).split("; ")[0]
except FileNotFoundError as e:
raise e
# If file is image or pdf, parse to cv2
if ext in ("image/png", "image/jpeg"):
if url:
data = numpy.asarray(bytearray(response.read()))
else:
data = numpy.asarray(bytearray(file))
return cv2.imdecode(data, cv2.IMREAD_COLOR)
elif ext in ("application/pdf"):
import pdf2image
if url:
images = pdf2image.convert_from_bytes(response.read())
else:
images = pdf2image.convert_from_bytes(file)
# only pdf with one page are supported
if len(images) != 1:
raise UnsupportedNumberOfPages
data = numpy.asarray(images[0])
return cv2.cvtColor(data, cv2.COLOR_RGB2BGR)
else:
raise UnsupportedFileType(ext)
def write_image (
image: numpy.ndarray,
path: str,
):
""" Stores an image to the given path.
If the directory does not exist, it tries to create it.
Args:
image (ndarray):
the image to be stored
path (String): Path for file to be stored
"""
import os
path_abs = os.path.abspath(path)
path_dir = os.path.dirname(path_abs)
if not os.path.exists(path_dir):
os.makedirs(path_dir)
cv2.imwrite(path_abs, image)
def load_results (
path: str
):
import json
with open(path, "r") as f:
return json.loads(f.read())
def store_results (
data: list,
path: str
):
import json
with open(path, "w+") as f:
f.write(json.dumps(data))
############
# Plotting #
############
def plot (
orig: numpy.ndarray,
blur: numpy.ndarray,
thres: numpy.ndarray,
edges: numpy.ndarray,
boxes: numpy.ndarray,
center_boxes: numpy.ndarray,
checked: numpy.ndarray,
end: numpy.ndarray,
):
""" Plots up to 8 given data.
All Subplot consists of the following:
- plt.subplot => subplot identifier
- plt.xticks, plt.yticks => hide axis labels
- plt.title => Title to be shown
- plt.imshow => shows given image in the subplot
- since opencv image is in BGR, it needs to convert to RGB first
"""
import matplotlib.pyplot as plt
from imutils import opencv2matplotlib
idx = 240
# Subplot y = 1, x = 1
plt.subplot(idx + 1)
plt.xticks([]), plt.yticks([])
plt.title("Original")
plt.imshow(opencv2matplotlib(orig))
# Subplot y = 1, x = 2
plt.subplot(idx + 2)
plt.xticks([]), plt.yticks([])
plt.title("Blur")
plt.imshow(opencv2matplotlib(blur))
# Subplot y = 1, x = 3
plt.subplot(idx + 3)
plt.xticks([]), plt.yticks([])
plt.title("Threshold")
plt.imshow(opencv2matplotlib(thres))
plt.subplot(idx + 4)
plt.xticks([]), plt.yticks([])
plt.title("Canny Edge")
plt.imshow(opencv2matplotlib(edges))
plt.subplot(idx + 5)
plt.xticks([]), plt.yticks([])
plt.title("Boxes")
plt.imshow(opencv2matplotlib(boxes))
plt.subplot(idx + 6)
plt.xticks([]), plt.yticks([])
plt.title("Center of Boxes")
plt.imshow(opencv2matplotlib(center_boxes))
plt.subplot(idx + 7)
plt.xticks([]), plt.yticks([])
plt.title("Areas to be checked")
plt.imshow(opencv2matplotlib(checked))
plt.subplot(idx + 8)
plt.xticks([]), plt.yticks([])
plt.title("Found corrected and checked boxes")
plt.imshow(opencv2matplotlib(end))
plt.show()

65
scan/utils.py Normal file
View File

@ -0,0 +1,65 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2019 Akumatic
import math
def distance (
p: tuple,
q: tuple
) -> float:
""" Calculates direct distance between two points given as tuples.
Args:
p (tuple):
A tuple containing x and y coordinates
q (tuple):
A tuple containing x and y coordinates
Returns:
A float with the distance between given points as value
"""
return math.sqrt((p[0] - q[0])**2 + (p[1] - q[1])**2)
def find_closest_element (
points: list,
point: tuple
) -> tuple:
""" Finds the closes element to a given point
Args:
points (list):
A list containing tuples of coordinates (x, y)
point (tuple):
The (x, y) coordinates of the given point
Returns:
the tuple of the closest point and the distance
between the given and closest point
"""
start, min_dist = None, None
for p in points:
dist = distance(p[:2], point)
if min_dist is None or dist < min_dist:
start, min_dist = p, dist
return start, min_dist
def circularity (
area: float,
perimeter: float
) -> float:
""" Calculates the circularity shape factor with given area and perimeter.
Args:
area (float):
area of a shape
perimeter (float):
length of the perimeter of a shape
Returns:
A float with the circularity shape factor as value
"""
if perimeter == 0:
return 0.0
return (4 * math.pi * area) / (perimeter ** 2)