Source code for arthropod_describer.common.label_change

import json
import typing
from dataclasses import dataclass, field
from enum import IntEnum
from typing import List, Optional, Tuple, Union, Any

import numpy as np

from arthropod_describer.common.photo import LabelImg


[docs]@dataclass(eq=False) class LabelChange: """Class representing a set of pixels (`coords`) changing their value from `old_label` to `new_label`. coords: the pixels that changed their value new_label: their new value old_label: their previous value label_name: which label image this change pertains to _change_bbox: a bounding box enclosing the pixels (`coords`) """ coords: typing.Tuple[np.ndarray, np.ndarray] new_label: int old_label: int label_name: str _change_bbox: typing.Optional[typing.Tuple[int, int, int, int]] = None
[docs] def swap_labels(self) -> 'LabelChange': return LabelChange(self.coords, self.old_label, self.new_label, self.label_name)
@property def bbox(self) -> typing.Tuple[int, int, int, int]: if self._change_bbox is None: self._change_bbox = (np.min(self.coords[0]), np.max(self.coords[0]), np.min(self.coords[1]), np.max(self.coords[1])) return self._change_bbox
[docs] def local_coords(self, bbox: Optional[Tuple[int, int, int, int]]) -> typing.Tuple[np.ndarray, np.ndarray]: bbox_ = self.bbox if bbox is None else bbox return self.coords[0] - bbox_[0], self.coords[1] - bbox_[2]
[docs] @classmethod def from_dict(cls, obj: typing.Dict[str, Any]) -> 'LabelChange': return LabelChange( (np.array(decode_rle(obj['coords'][0])), np.array(decode_rle(obj['coords'][1]))), obj['new_label'], obj['old_label'], obj['label_name'], obj['_change_bbox'] )
[docs]class DoType(IntEnum): Do = 0, Undo = 1,
[docs]class CommandKind(IntEnum): LabelImgChange = 0, Rot_90_CW = 1, Rot_90_CCW = -1
[docs] def invert(self) -> 'CommandKind': return CommandKind(-1 * self)
[docs]@dataclass(eq=False) class CommandEntry: """A class representing an edit command. change_chain: see `LabelChange` do_type: whether this command is either Undo or Do(Redo) _bbox: a box enclosing all modified pixels update_canvas: whether to update the view source: name of the tool/plugin that is responsible for the change. Will appear in Edit menu as e.g. 'Undo {source}` image_name: name of the photo that was of whose label image was modified label_name: name of the label image that this command pertains to old_approval: what was the approval before this change? new_approval: what is the approval after this change? command_kind: see `CommandKind` """ change_chain: typing.List[LabelChange] = field(default_factory=list) do_type: DoType = DoType.Do _bbox: typing.Optional[typing.Tuple[int, int, int, int]] = None update_canvas: bool = True source: str = '' image_name: str = '' label_name: str = '' old_approval: str = '' new_approval: str = '' command_kind: CommandKind = CommandKind.LabelImgChange
[docs] def add_label_change(self, change: LabelChange): self.change_chain.append(change)
@property def bbox(self) -> typing.Tuple[int, int, int, int]: if self._bbox is None: bbox = list(self.change_chain[0].bbox) for change in self.change_chain[1:]: bbox2 = change.bbox bbox[0] = min(bbox[0], bbox2[0]) bbox[1] = max(bbox[1], bbox2[1]) bbox[2] = min(bbox[2], bbox2[2]) bbox[3] = max(bbox[3], bbox2[3]) self._bbox = tuple(bbox) return self._bbox
[docs] @classmethod def from_dict(cls, obj: typing.Dict[str, Any]) -> 'CommandEntry': return CommandEntry( #[LabelChange.from_dict(lab_ch_dict) for lab_ch_dict in obj['change_chain']], obj['change_chain'], DoType(obj['do_type']), obj['_bbox'], obj['update_canvas'], obj['source'] )
[docs]def ce_object_hook(obj: typing.Dict[str, Any]) -> Union[CommandEntry, LabelChange]: if 'coords' in obj: return LabelChange.from_dict(obj) return CommandEntry.from_dict(obj)
# TODO maybe remove this
[docs]class CommandEntryJsonEncoder(json.JSONEncoder):
[docs] def default(self, o: Any) -> Any: if isinstance(o, LabelChange): return { 'coords': (encode_into_rle(o.coords[0].tolist()), encode_into_rle(o.coords[1].tolist())), 'new_label': o.new_label, 'old_label': o.old_label, 'label_name': o.label_name, '_change_bbox': o._change_bbox } elif isinstance(o, CommandEntry): return { 'change_chain': [self.default(change) for change in o.change_chain], 'do_type': o.do_type, '_bbox': o._bbox, 'update_canvas': o.update_canvas } elif isinstance(o, np.integer): return int(o) else: return super().default(o)
[docs]def encode_into_rle(arr: List[int]) -> List[int]: rle = [arr[0], 1] i = 0 for val in arr[1:]: if val == rle[2 * i]: rle[2 * i + 1] += 1 else: rle.append(val) rle.append(1) i += 1 return rle
[docs]def decode_rle(rle: List[int]) -> List[int]: values = [] for i in range(len(rle) // 2): _vals = [rle[2 * i] for _ in range(2 * i + 1)] values.extend(_vals) return values
[docs]def remove_coords(cmd: CommandEntry) -> Tuple[CommandEntry, List[Tuple[np.ndarray, np.ndarray]]]: coords_list: List[Tuple[np.ndarray, np.ndarray]] = [] for lab_change in cmd.change_chain: coords_list.append(lab_change.coords) lab_change.coords = ([], []) return cmd, coords_list
[docs]def compute_label_difference(old_label: np.ndarray, new_label: np.ndarray) -> np.ndarray: """Returns an image that contains values from `new_label` where `new_label` has different values from `old_label`, otherwise contains `-1`.""" non_equal_mask = old_label != new_label return np.where(non_equal_mask, new_label, -1)
[docs]def label_difference_to_command(label_diff: np.ndarray, label_img: LabelImg, bbox: Optional[Tuple[int, int, int, int]]=None) -> CommandEntry: #label_nd = label_img.label_img #new_labels = np.unique(label_diff)[1:] # filter out the -1 label which is the first on in the returned array #command = CommandEntry() #for label in new_labels: # old_and_new = np.where(label_diff == label, label_nd, -1) # old_labels = np.unique(old_and_new)[1:] # filter out -1 # for old_label in old_labels: # coords = np.nonzero(old_and_new == old_label) # change = LabelChange(coords, label, old_label, label_img.label_type) # command.add_label_change(change) # #return command return CommandEntry(label_difference_to_label_changes(label_diff, label_img, bbox))
[docs]def label_difference_to_label_changes(label_diff: np.ndarray, label_img: LabelImg, bbox: Optional[Tuple[int, int, int, int]] = None) -> List[LabelChange]: """ For a label difference image and a `LabelImg` returns a list of `LabelChange`. What is a label difference image, see the function `compute_label_difference`. """ if bbox is not None: lab_diff = label_diff[bbox[0]:bbox[1], bbox[2]:bbox[3]] lab_nd = label_img.label_image[bbox[0]:bbox[1], bbox[2]:bbox[3]] else: bbox = (0, 0, 0, 0) lab_diff = label_diff lab_nd = label_img.label_image # label_nd = label_img.label_image new_labels = np.unique(lab_diff) # these are all the new labels that were painted in by the user (except -1 which means `no change`) if -1 in new_labels: new_labels = new_labels[1:] # filter out the -1 label which is the first on in the returned array label_changes: List[LabelChange] = [] # now for each `label` in `new_labels` we need to generate one or more `LabelChange` objects # if `label` was painted over a region with a single label, say `1`, we would need to generate only one `LabelChange`, # specifying the coordinates of the pixels painted over and their old label value (`1`) and their new label value (`label`). # The number of `LabelChange` objects we need to create for `label` depends on how many different labels were painted # over by `label`. for label in new_labels: old_and_new = np.where(lab_diff == label, lab_nd, -1) # reveal only those pixels of `label_nd` where `label_diff` is equal to `label` old_labels = np.unique(old_and_new) # Get the all the different labels that were painted over by `label`. if -1 in old_labels: # get rid of -1 (`no change`) old_labels = old_labels[1:] for old_label in old_labels: # here we are actually creating the `LabelChange` objects, the pixels changing their value from `old_label` to `label`. yy, xx = np.nonzero(old_and_new == old_label) # these are the coordinates of the pixels label_changes.append(LabelChange((yy+bbox[0], xx+bbox[2]), label, old_label, label_img.label_semantic)) return label_changes
[docs]def restrict_label_to_mask(label: Union[LabelImg, np.ndarray], mask: Union[LabelImg, np.ndarray]) -> Optional[CommandEntry]: """ Checks whether `label`-s non-zero entries are located entirely within `mask` and if not, returns a CommandEntry which, when performed on `label` will modify `label` so that it is contained inside `mask`. :param label: LabelImg or np.ndarray :param mask: LabelImg or np.ndarray :return: CommandEntry if `label` is not contained within `mask` else None """ label_mask = label.label_image != 0 mask_mask = mask.label_image != 0 within = np.all(np.logical_and(label_mask, mask_mask) == label_mask) if within: return None to_modify = np.logical_xor(label_mask, mask_mask) edit_img = np.where(to_modify, label.label_image, -1) return label_difference_to_command(edit_img, label)
# TODO remove this #def propagate_mask_changes_to(label_img: LabelImg, command: CommandEntry) -> Optional[CommandEntry]: # if label_img.label_type == LabelType.BUG: # return None # # label_nd = label_img.label_image # # label_modifications = {} # # for change in command.change_chain: # if change.new_label > 0: # augmentation of mask, so `label_img` is within the mask # continue # # labels = label_nd[change.coords[0], change.coords[1]] # # for label, y, x in zip(labels, *change.coords): # label_coords_list_tuple = label_modifications.setdefault(label, ([], [])) # label_coords_list_tuple[0].append(y) # label_coords_list_tuple[1].append(x) # label_changes = [LabelChange(coords_list_tuple, 0, label, label_img.label_type) for label, coords_list_tuple in # label_modifications.items()] # return CommandEntry(label_changes, label_type=label_img.label_type)
[docs]def generate_change_command(old_lab_img: LabelImg, new_label: np.ndarray) -> Optional[CommandEntry]: lab_diff = compute_label_difference(old_lab_img.label_image, new_label) return label_difference_to_command(lab_diff, old_lab_img)
[docs]def generate_command_from_coordinates(label_img: LabelImg, coords_x: List[int], coords_y: List[int], new_label: int) -> CommandEntry: lab_img = label_img.label_image values = lab_img[coords_y, coords_x] unique_values = np.unique(values) change_chain: List[LabelChange] = [] for val in unique_values: r, c = coords_y[values == val], coords_x[values == val] lab_change = LabelChange((r, c), new_label=new_label, old_label=val, label_name=label_img.label_semantic) change_chain.append(lab_change) return CommandEntry(change_chain=change_chain)