from pathlib import Path
from typing import Dict
import typing
import cv2
import numpy as np
from PIL import Image
from PySide2.QtGui import QImage
from skimage import io
from arthropod_describer.common.label_image import LabelImgInfo, LabelImg
from arthropod_describer.common.photo import Photo, Subscriber, UpdateContext
from arthropod_describer.common.units import Value
from arthropod_describer.common.utils import ScaleSetting
[docs]class LocalPhoto(Photo):
def __init__(self, folder: Path, img_name: str, lbl_image_info: Dict[str, LabelImgInfo], subs: Subscriber):
self._tags: typing.Set[str] = set()
self._dirty_flag: bool = False
self._image: typing.Optional[np.ndarray] = None
self._image_path = folder / img_name
self._bug_bbox: typing.Optional[typing.Tuple[int, int, int, int]] = None
self._label_images: Dict[str, LabelImg] = {}
self._label_image_info: Dict[str, LabelImgInfo] = lbl_image_info
#self._label_image_types = lbl_image_types
self._scale: typing.Optional[Value] = None
self._scale_setting: typing.Optional[ScaleSetting] = ScaleSetting()
self._lab_approvals: Dict[str, typing.Optional[str]] = {lbl_name: None for lbl_name in lbl_image_info.keys()}
with Image.open(self._image_path) as im:
self._image_size = im.size
self._np_size = self._image_size[::-1]
self.format = im.format
# create the label images
for lbl_name in self._label_image_info.keys():
lbl_img = self[lbl_name]
self.__subscriber: Subscriber = subs
self._thumbnail: typing.Optional[QImage] = None
@property
def image(self) -> np.ndarray:
if self._image is None:
#self._image = io.imread(str(self._image_path))
with Image.open(self._image_path) as im:
self._image = np.asarray(im)
# This is a workaround around RGBA images
if self._image.shape[2] > 3:
self._image = self._image[:, :, :3]
return self._image
@property
def image_name(self) -> str:
return self._image_path.name
@property
def image_path(self) -> Path:
return self._image_path
@image_path.setter
def image_path(self, path: Path):
raise NotImplementedError('Changing path is not implemented')
@property
def image_size(self) -> typing.Tuple[int, int]:
return self._image_size
def __getitem__(self, lab_name: str) -> typing.Optional[LabelImg]:
if lab_name not in self._label_images:
lab_fname = self._image_path.name + '.tif'
self._label_images[lab_name] = LabelImg.create2(self._image_path.parent.parent / lab_name / lab_fname,
self.image_size, label_info=self.label_image_info[lab_name],
label_name=lab_name)
lab = self._label_images[lab_name]
return lab
@property
def bug_bbox(self) -> typing.Optional[typing.Tuple[int, int, int, int]]:
return self._bug_bbox
[docs] def recompute_bbox(self):
pass
#coords = np.nonzero(self._regions_image.label_img)
#if len(coords[0]) == 0:
# top, left = 0, 0
# bottom, right = self.regions_image.label_img.shape
#else:
# top, left = np.min(coords[0]), np.min(coords[1])
# bottom, right = np.max(coords[0]), np.max(coords[1])
#self._bug_bbox = [left, top, right, bottom]
@property
def label_images_(self) -> Dict[str, LabelImg]:
return self._label_images
@property
def label_image_info(self) -> Dict[str, LabelImgInfo]:
return self._label_image_info
@property
def image_scale(self) -> typing.Optional[Value]:
return self._scale_setting.scale
@image_scale.setter
def image_scale(self, scale: typing.Optional[Value]):
self._scale_setting.scale = scale
@property
def scale_setting(self) -> typing.Optional[ScaleSetting]:
return self._scale_setting
@scale_setting.setter
def scale_setting(self, setting: typing.Optional[ScaleSetting]):
self._scale_setting = setting
self._subscriber.notify(self.image_name, UpdateContext.Photo, {'type': 'image_scale'})
@property
def approved(self) -> Dict[str, typing.Optional[str]]:
return self._lab_approvals
[docs] def rotate(self, ccw: bool):
loaded = self._image is not None
if not loaded:
self._image = io.imread(str(self._image_path))
if self.scale_setting is not None and self.scale_setting.scale_line is not None:
mid = (round(self.image_size[0] * 0.5), round(self.image_size[1] * 0.5))
self.scale_setting.scale_line.rotate(ccw, mid)
self._image = cv2.rotate(self._image, cv2.ROTATE_90_COUNTERCLOCKWISE if ccw else cv2.ROTATE_90_CLOCKWISE) #skimage.transform.rotate(self._image, 90 * (-1 if ccw else 1), order=2)
self._image = np.ascontiguousarray(self._image, dtype=self._image.dtype)
self._dirty_flag = True
self._np_size = self._image.shape[:2]
self._image_size = self._np_size[::-1]
for lab_img in self._label_images.values():
lab_img.rotate(ccw)
if not loaded:
self.save()
self._image = None
self.save()
self.__subscriber.notify(self.image_name, UpdateContext.Photo, {'operation':
'rot_90_ccw' if ccw else 'rot_90_cw'})
[docs] def resize(self, factor: float):
loaded = self._image is not None
print(f'resizing with factor {factor}')
# TODO adapt measurements, or at least signal that the measurements are not up-to-date anymore!
if self._scale_setting is not None and self._scale_setting.scale is not None:
# print(f'changing scale from {self._scale} to {self._scale * factor}')
self._scale_setting.scale *= factor
if not loaded:
self._image = io.imread(str(self._image_path))
self._dirty_flag = True
im = Image.fromarray(self._image)
print(f'old size is {self._image_size}')
size = (int(round(factor * self._image_size[0])),
int(round(factor * self._image_size[1])))
self._image_size = size
print(f'new size if {self._image_size}')
self._np_size = self._image_size[::-1]
im = im.resize(self._image_size, resample=2)
self._image = np.asarray(im)
for lbl_img in self._label_images.values():
lbl_img.resize(factor)
if not loaded:
self.save()
self._image = None
if self.scale_setting is not None and self.scale_setting.scale_line is not None:
mid = (round(self.image_size[0] * 0.5), round(self.image_size[1] * 0.5))
self.scale_setting.scale_line.scale(factor, (0, 0))
self.__subscriber.notify(self.image_name, UpdateContext.Photo,
{'operation': 'resize',
'factor': factor})
[docs] def save(self):
if self.has_unsaved_changes:
if self._dirty_flag:
if self._image is not None:
#im = Image.fromarray(self._image)
#im.save(self._image_path)
if self.format != 'TIFF':
bgr = cv2.cvtColor(self._image, cv2.COLOR_BGR2RGB)
cv2.imwrite(str(self._image_path), bgr)
else:
im = Image.fromarray(self._image)
im.save(self._image_path)
for lab_img in self._label_images.values():
lab_img.save()
self._dirty_flag = False
[docs] def unload(self):
self.save()
self._image = None
for lab_img in self._label_images.values():
lab_img.unload()
[docs] def has_segmentation_for(self, label_name: str) -> bool:
return self._label_images[label_name].is_segmented
@property
def has_unsaved_changes(self) -> bool:
return self._dirty_flag or any([lab.has_unsaved_changed for lab in self._label_images.values()])
@property
def _subscriber(self) -> Subscriber:
return self.__subscriber
@property
def tags(self) -> typing.Set[str]:
return self._tags
@tags.setter
def tags(self, _tags: typing.Set[str]):
self._tags = {tag for tag in _tags if not tag.isspace() and len(tag) > 0}
self._subscriber.notify(self.image_name, UpdateContext.Photo,
{'tags': {
'added': list(self._tags),
'removed': []
}})
[docs] def add_tag(self, tag: str):
if tag in self._tags or len(tag) == 0 or tag.isspace():
return
self._tags.add(tag)
self._subscriber.notify(self.image_name, UpdateContext.Photo,
{'tags': {
'added': [tag],
'removed': []
}})
[docs] def remove_tag(self, tag: str):
if tag not in self._tags:
return
self._tags.remove(tag)
self._subscriber.notify(self.image_name, UpdateContext.Photo,
{'tags': {
'added': [],
'removed': [tag]
}})
[docs] def toggle_tag(self, tag: str, enabled: bool):
if enabled:
self.add_tag(tag)
else:
self.remove_tag(tag)
@property
def thumbnail(self) -> typing.Optional[QImage]:
return self._thumbnail
@thumbnail.setter
def thumbnail(self, thumbnail: typing.Optional[QImage]):
self._thumbnail = thumbnail
# TODO fire signal to notify of thumbnail change