|
| 1 | +from abc import ABC, abstractmethod |
| 2 | + |
| 3 | +import tkinter |
| 4 | +from typing import Callable, Optional |
| 5 | + |
| 6 | +import opencsp.common.lib.file.SimpleCsv as scsv |
| 7 | +import opencsp.common.lib.geometry.Pxy as p2 |
| 8 | +import opencsp.common.lib.tool.exception_tools as et |
| 9 | +import opencsp.common.lib.tool.file_tools as ft |
| 10 | +import opencsp.common.lib.tool.log_tools as lt |
| 11 | + |
| 12 | + |
| 13 | +class AbstractImageAnnotation(ABC): |
| 14 | + """Simple annotations that get displayed on top of images.""" |
| 15 | + |
| 16 | + _registered_annotation_classes: set[type["AbstractImageAnnotation"]] = set() |
| 17 | + """ Register of all available simple annotation classes. """ |
| 18 | + |
| 19 | + def __init__(self, is_preview=False): |
| 20 | + """ |
| 21 | + Parameters |
| 22 | + ---------- |
| 23 | + is_preview : bool, optional |
| 24 | + True if this instance is waiting to be finished (such as when |
| 25 | + drawing a line and the mouse button hasn't been released yet). False |
| 26 | + otherwise. By default False. |
| 27 | + """ |
| 28 | + self.is_preview = is_preview |
| 29 | + """ |
| 30 | + True if this instance is waiting to be finished (such as when drawing a |
| 31 | + line and the mouse button hasn't been released yet). False otherwise. |
| 32 | + """ |
| 33 | + self.canvas: tkinter.Canvas = None |
| 34 | + """ The canvas instance on which to draw this instance. """ |
| 35 | + self._canvas_items: list[int] = [] |
| 36 | + """ Handles to the canvas items used to draw this instance. """ |
| 37 | + |
| 38 | + @property |
| 39 | + def canvas_items(self) -> list[int]: |
| 40 | + """List of handles to the graphics items on the canvas.""" |
| 41 | + return self._canvas_items |
| 42 | + |
| 43 | + def clear(self): |
| 44 | + """Removes all graphics representing this instance from the canvas.""" |
| 45 | + for canvas_item in self.canvas_items: |
| 46 | + with et.ignored(Exception): |
| 47 | + self.canvas.delete(canvas_item) |
| 48 | + self.canvas_items.clear() |
| 49 | + |
| 50 | + @classmethod |
| 51 | + def on_mouse_down( |
| 52 | + cls, coord_translator: Callable[[p2.Pxy], p2.Pxy], mouse_down_event: tkinter.Event |
| 53 | + ) -> Optional["AbstractImageAnnotation"]: |
| 54 | + """ |
| 55 | + Creates an instance of this class when the mouse button is pressed. If |
| 56 | + no instance is created, then return None. |
| 57 | +
|
| 58 | + Parameters |
| 59 | + ---------- |
| 60 | + coord_translator : Callable[[p2.Pxy], p2.Pxy] |
| 61 | + Function to translate from event x and y coordinates to image coordinates. |
| 62 | + """ |
| 63 | + return None |
| 64 | + |
| 65 | + @classmethod |
| 66 | + def on_mouse_move( |
| 67 | + cls, |
| 68 | + coord_translator: Callable[[p2.Pxy], p2.Pxy], |
| 69 | + mouse_down_event: tkinter.Event | None, |
| 70 | + mouse_move_event: tkinter.Event, |
| 71 | + ) -> Optional["AbstractImageAnnotation"]: |
| 72 | + """ |
| 73 | + Creates an instance of this class when the mouse is moved. If no |
| 74 | + instance is created, then return None. |
| 75 | +
|
| 76 | + Parameters |
| 77 | + ---------- |
| 78 | + coord_translator : Callable[[p2.Pxy], p2.Pxy] |
| 79 | + Function to translate from event x and y coordinates to image coordinates. |
| 80 | + """ |
| 81 | + return None |
| 82 | + |
| 83 | + @classmethod |
| 84 | + def on_mouse_up( |
| 85 | + cls, |
| 86 | + coord_translator: Callable[[p2.Pxy], p2.Pxy], |
| 87 | + mouse_down_event: tkinter.Event | None, |
| 88 | + mouse_up_event: tkinter.Event, |
| 89 | + ) -> Optional["AbstractImageAnnotation"]: |
| 90 | + """ |
| 91 | + Creates an instance of this class when the mouse button is pressed. If |
| 92 | + no instance is created, then return None. |
| 93 | +
|
| 94 | + Parameters |
| 95 | + ---------- |
| 96 | + coord_translator : Callable[[p2.Pxy], p2.Pxy] |
| 97 | + Function to translate from event x and y coordinates to image coordinates. |
| 98 | + """ |
| 99 | + return None |
| 100 | + |
| 101 | + @staticmethod |
| 102 | + def save_annotations_to_csv(annotations: list["AbstractImageAnnotation"], file_path_name_ext: str, overwrite=False): |
| 103 | + """ |
| 104 | + Saves the given list of simple annotations to the given CSV file using |
| 105 | + each annotations built-in CSV conversion methods. |
| 106 | +
|
| 107 | + Parameters |
| 108 | + ---------- |
| 109 | + annotations : list[AbstractAnnotation] |
| 110 | + The annotations to be saved. |
| 111 | + file_path_name_ext : str |
| 112 | + The CSV file to be saved to. |
| 113 | + overwrite : bool, optional |
| 114 | + True to replace the current contents of the CSV file at |
| 115 | + file_path_name_ext, by default False |
| 116 | +
|
| 117 | + Raises |
| 118 | + ------ |
| 119 | + FileExistsError |
| 120 | + If file_path_name_ext exists and overwrite is False. |
| 121 | + FileNotFoundError |
| 122 | + If the directory of file_path_name_ext doesn't exist. |
| 123 | + """ |
| 124 | + if ft.file_exists(file_path_name_ext): |
| 125 | + if not overwrite: |
| 126 | + raise FileExistsError |
| 127 | + file_path, file_name, file_ext = ft.path_components(file_path_name_ext) |
| 128 | + if not ft.directory_exists(file_path): |
| 129 | + raise FileNotFoundError |
| 130 | + |
| 131 | + # build the list of columns |
| 132 | + columns: list[str] = ["class"] |
| 133 | + for annotation in annotations: |
| 134 | + for aheader in annotation.csv_columns(): |
| 135 | + if aheader not in columns: |
| 136 | + columns.append(aheader) |
| 137 | + header = ",".join(columns) |
| 138 | + |
| 139 | + # add a row for each annotation |
| 140 | + rows: list[str] = [] |
| 141 | + for annotation in annotations: |
| 142 | + row = [""] * len(columns) |
| 143 | + row[0] = annotation.class_descriptor() |
| 144 | + for aheader, sval in zip(annotation.csv_columns(), annotation.csv_values()): |
| 145 | + row[columns.index(aheader)] = sval |
| 146 | + rows.append(",".join(row)) |
| 147 | + |
| 148 | + # save all values to a csv file |
| 149 | + lt.info(f"Saving annotations csv {file_name+file_ext}") |
| 150 | + with open(file_path_name_ext, "w") as fout: |
| 151 | + fout.write(header + "\n") |
| 152 | + for row in rows: |
| 153 | + fout.write(row + "\n") |
| 154 | + |
| 155 | + @staticmethod |
| 156 | + def load_annotations_from_csv(file_path_name_ext: str, is_preview=False) -> list["AbstractImageAnnotation"]: |
| 157 | + """ |
| 158 | + Loads simple annotations from the given CSV file. |
| 159 | +
|
| 160 | + Parameters |
| 161 | + ---------- |
| 162 | + file_path_name_ext: str |
| 163 | + The CSV file to load the annotations from. |
| 164 | +
|
| 165 | + Returns |
| 166 | + ------- |
| 167 | + annotations: list[AbstractAnnotation] |
| 168 | + The loaded annotations. |
| 169 | + """ |
| 170 | + ret: list[AbstractImageAnnotation] = [] |
| 171 | + file_path, file_name, file_ext = ft.path_components(file_path_name_ext) |
| 172 | + |
| 173 | + parser = scsv.SimpleCsv("annotations csv", file_path, file_name + file_ext) |
| 174 | + for row_dict in parser: |
| 175 | + descriptor = row_dict["class"] |
| 176 | + |
| 177 | + for aclass in AbstractImageAnnotation._registered_annotation_classes: |
| 178 | + if aclass.class_descriptor() == descriptor: |
| 179 | + aheaders = aclass.csv_columns() |
| 180 | + svals = [row_dict[aheader] for aheader in aheaders] |
| 181 | + inst = aclass.from_csv(svals, is_preview) |
| 182 | + ret.append(inst) |
| 183 | + break |
| 184 | + |
| 185 | + return ret |
| 186 | + |
| 187 | + @abstractmethod |
| 188 | + def draw(self, coord_translator: Callable[[p2.Pxy], p2.Pxy], canvas: tkinter.Canvas): |
| 189 | + """ |
| 190 | + Adds the graphics to represent this instance to the canvas. Modifies self.canvas_items. |
| 191 | +
|
| 192 | + Implementations of this class should call super().draw(). |
| 193 | +
|
| 194 | + Parameters |
| 195 | + ---------- |
| 196 | + coord_translator : Callable[[p2.Pxy], p2.Pxy] |
| 197 | + Function to translate from image coordinates to screen coordinates. |
| 198 | + canvas : tkinter.Canvas |
| 199 | + The canvas to draw this instance onto. |
| 200 | + """ |
| 201 | + self.clear() |
| 202 | + self.canvas = canvas |
| 203 | + |
| 204 | + @classmethod |
| 205 | + @abstractmethod |
| 206 | + def class_descriptor(self) -> str: |
| 207 | + """A string used to identify this class in an annotations file.""" |
| 208 | + raise NotImplementedError |
| 209 | + |
| 210 | + @classmethod |
| 211 | + @abstractmethod |
| 212 | + def csv_columns(cls) -> list[str]: |
| 213 | + """ |
| 214 | + The names of the columns used to represent this annotation. |
| 215 | +
|
| 216 | + These column names can potentially be shared with other annotations. |
| 217 | + """ |
| 218 | + raise NotImplementedError |
| 219 | + |
| 220 | + @abstractmethod |
| 221 | + def csv_values(self) -> list[str]: |
| 222 | + """Get the values that represent this instance. Should match the order from csv_columns.""" |
| 223 | + raise NotImplementedError |
| 224 | + |
| 225 | + @classmethod |
| 226 | + @abstractmethod |
| 227 | + def from_csv(cls, data: list[str], is_preview=False) -> "AbstractImageAnnotation": |
| 228 | + """Construct an instance of this class from the columns matching the column names for this class.""" |
| 229 | + raise NotImplementedError() |
0 commit comments