#!/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