weiren119's picture
Feat: app.py
34acdd0
raw
history blame
7.48 kB
#!/usr/bin/env python3
"""
Copyright (c) 2020, Carleton University Biomedical Informatics Collaboratory
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
"""
from typing import Optional, List
import os
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import cv2
from .label import Label
from .line import Line
from .grid import Grid
class Report(object):
def __init__(self, filename: Optional[str] = None, image: Optional[Image.Image] = None):
assert not (filename and image) and bool(filename) != bool(image)
if filename:
self.filename = filename
self.pil_image = Image.open(filename)
else:
self.filename = None
self.pil_image = image
def rescale(self, factor: float) -> "Report":
"""Creates a new Report that has been resized.
Parameters
----------
factor : float
The resize factor.
Returns
-------
Report
A new Report that has be rescaled.
"""
return Report(image=self.pil_image.resize(int(self.pil_image.width * factor), int(self.pil_image.height * factor)))
def rotate(self, angle: float) -> "Report":
"""Creates a new Report that has been rotated.
Parameters
----------
angle : float
The rotation (in degrees) to apply (CCW).
Returns
-------
Report
A new Report that has be rotated.
"""
return Report(image=self.pil_image.rotate(angle, center=(0,0), fillcolor="rgb(255,255,255)"))
def crop(self, x1: int, y1: int, x2: int, y2: int) -> "Report":
"""Creates a new cropped Report.
Parameters
----------
x1: int
The x pixel coordinate of the top-left corner.
y1: int
The y pixel coordinate of the top-left corner.
x2: int
The x pixel coordinate of the bottom-right corner.
y2: int
The y pixel coordinate of the bottom-right corner.
Returns
-------
Report
A new cropped Report.
"""
return Report(image=self.pil_image.crop((x1, y1, x2, y2)))
def show(
self,
labels: Optional[List[Label]] = [],
lines: Optional[List[Line]] = [],
grids: Optional[List[Grid]] = [],
points: Optional[List[tuple]] = [],
title: Optional[str] = None,
filename: Optional[str] = None
):
"""Displays the audiogram on the screen, and saves it to a file
if `filename` parameter is provided.
Parameters
----------
labels : List[Label]
A list of labels to show (default: []).
lines : List[Lines]
A list of lines to show (default: []).
grids : List[Grid]
A list of grids to show (default: []).
points : List[dict]
A list of points of the form { "x": int, "y": int } to show (default: []).
title: str
The title of the plot
filename: Optional[str]
The path to where the image should be save. Image is not saved
if no filename is provided (default: None).
"""
labeled_copy = self.pil_image.copy().convert("RGB")
drawing = ImageDraw.Draw(labeled_copy)
fontpath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "assets", "fonts", "Arial.ttf")
font = None
try:
font = ImageFont.truetype(fontpath, 32)
except:
font = ImageFont.truetype("DejaVuSans.ttf", 32)
for label in labels:
label.draw(drawing)
for line in lines:
line.draw(drawing)
for grid in grids:
grid.draw(drawing)
for point in points:
r = 5 # radius
drawing.ellipse([(point[0] - r, point[1] - r), (point[0] + r, point[1] + r)], fill="rgb(0,0,255)")
if title:
drawing.text((self.pil_image.size[0]/2, 50), title, font=font, align="center", fill="rgb(53,155,232)")
if filename:
labeled_copy.save(filename)
labeled_copy.show()
def save(
self,
filename: str,
labels: Optional[List[Label]] = [],
lines: Optional[List[Line]] = [],
grids: Optional[List[Grid]] = [],
title: Optional[str] = None,
):
"""Saves the report to a file along with annotations for the
provided report elements.
Parameters
----------
filename: str
The path to where the image should be save.
labels : List[Label]
A list of labels to show (default: []).
lines : List[Lines]
A list of lines to show (default: []).
grids : List[Grid]
A list of grids to show (default: []).
title: str
The title of the plot
"""
labeled_copy = self.pil_image.copy().convert("RGB")
drawing = ImageDraw.Draw(labeled_copy)
fontpath = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "assets", "Arial.ttf")
font = None
try:
font = ImageFont.truetype(fontpath, 32)
except:
font = ImageFont.truetype("DejaVuSans.ttf", 32)
for label in labels:
label.draw(drawing)
for line in lines:
line.draw(drawing)
for grid in grids:
grid.draw(drawing)
if title:
drawing.text((self.pil_image.size[0]/2, 50), title, font=font, align="center", fill="rgb(53,155,232)")
labeled_copy.save(filename)
def get_image(self, resize_factor: float = 1) -> Image:
"""Returns a copy of the PIL image representing the report.
Parameters
----------
resize_factor : float
The resize factor of the image sought (default: 1).
Returns
-------
PIL.Image
A copy of the image at the resize factor provided.
"""
return self.pil_image.resize(
(int(self.pil_image.size[0] * resize_factor),
int(self.pil_image.size[1] * resize_factor))
)
def detect_lines(self, threshold=250) -> List[Line]:
"""Detects lines in the report using the Hough Transform.
For details, see: https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_houghlines/py_houghlines.html
Parameters
----------
threshold : int
The threshold above which a line is detected. See documentation for OpenCV's HoughLine function for details.
Returns
-------
List[Line]
A list of lines detected in the report.
"""
gray = np.array(self.get_image())
edges = cv2.Canny(gray, 150, 300, apertureSize = 3)
lines = cv2.HoughLines(edges, 1, np.pi/180, threshold, None, 0, 0)
if lines is None:
return []
lines_list = []
for line in lines:
for l in (line[0],):
rho = l[0]
theta = l[1]
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 1000*(-b))
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
lines_list.append(Line(x1, y1, x2, y2))
return lines_list