#!/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 List import PIL.ImageDraw import numpy as np class Line(object): def __init__(self, x1, y1, x2, y2, color="rgb(255,0,0)", label=None): self.p1 = { "x": x1, "y": y1 } self.p2 = { "x": x2, "y": y2 } self.color = color self.label = label def get_angle(self) -> float: """Returns the angle of the line in degrees in the range [0, 180[. Returns ------- float The angle of the line in degrees. """ dx = self.p2["x"] - self.p1["x"] dy = self.p2["y"] - self.p1["y"] angle = np.degrees(np.arctan2(abs(dy), abs(dx))) if self.p2["y"] - self.p1["y"] < 0: angle = 180 - angle return angle def has_a_perpendicular_line(self, lines: List["Line"], tolerance: float = 1) -> bool: """Checks if a line has at least one perpendicular line among a list of lines. Parameters ---------- lines : List[Line] A list of lines. tolerance : float A difference of `tolerance` from a 90 degrees angle between the two lines is still considered perpendicular. Returns ------- bool `True` if the line has a perpendicular line in the list, `False` otherwise. """ assert tolerance >= 0 line_angle1 = self.get_angle() for other_line in lines: line_angle2 = other_line.get_angle() angle = abs(line_angle1 - line_angle2) if abs(angle - 90) < tolerance: return True return False def get_x(self) -> int: """Return the middle x pixel coordinate of a vertical line. Only applicable to vertical lines. Returns ------- int The middle x coordinate of a vertical line. """ assert self.is_vertical() return int((self.p1["x"] + self.p2["x"]) / 2) def get_y(self) -> int: """Return the middle y pixel coordinate of a horizontal line. Only applicable to horizontal lines. Returns ------- int The middle y coordinate of a horizontal line. """ assert self.is_horizontal() return (self.p1["y"] + self.p2["y"]) / 2 def is_vertical(self, tolerance: float = 1) -> bool: """Returns true if the line is vertical. Parameters ---------- tolerance : float A deviation of `tolerance` degrees from the vertical line is still considered vertical. Returns ------- bool True if the line is vertical, False otherwise. """ assert tolerance >= 0 angle = self.get_angle() return angle <= (-90 + tolerance) or angle >= (90 - tolerance) def is_horizontal(self, tolerance: float = 1) -> bool: """Returns true if the line is horizontal. Parameters ---------- tolerance : float A deviation of `tolerance` degrees from the horizontal line is still considered horizontal. Returns ------- bool True if the line is horiontal, False otherwise. """ assert tolerance >= 0 angle = self.get_angle() return angle >= -tolerance and angle <= tolerance def crosses_label(self, labels: List["Label"]) -> bool: """Checks if the line intersects one of the labels passed. Parameters ---------- labels : List[Label] A list of labels that the line could possibly cross. Returns ------- bool True if the line crosses at least one Label among the ones provided, False otherwise. """ for label in labels: if self.is_vertical(): x = (self.p1["x"] + self.p2["x"]) / 2 if x >= label.p1["x"] and x <= label.p2["x"]: return True elif self.is_horizontal(): y = (self.p1["y"] + self.p2["y"]) / 2 if y >= label.p1["y"] and y <= label.p2["y"]: return True return False def draw(self, canvas: PIL.ImageDraw): """Draws the line on the canvas (image) passed. Parameters ---------- canvas : PIL.ImageDraw The PIL.ImageDraw on which the line is to be displayed. """ canvas.line([ (self.p1["x"], self.p1["y"]), (self.p2["x"], self.p2["y"]) ], width=3, fill=self.color) if self.label: canvas.text((self.p1["x"] + 5, self.p1["y"]), str(self.label), fill=self.color)