Skip to content

roi_calculator

data.transform.roi.roi_calculator

logger module-attribute

logger = getLogger('SaigeResearch')

ROICalculator

Bases: ABC

ROI 좌표 계산을 위한 추상 클래스입니다.

set abstractmethod

set(**kwargs) -> None
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
@abstractmethod
def set(self, **kwargs) -> None:
    pass

__call__ abstractmethod

__call__(image: Union[Image, ndarray], get_intermediate_results: bool = False, warmup: bool = False, **data) -> Dict
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
@abstractmethod
def __call__(
    self,
    image: Union[Image.Image, np.ndarray],
    get_intermediate_results: bool = False,
    warmup: bool = False,
    **data,
) -> Dict:
    pass

RelativeBoxROI

RelativeBoxROI(**kwargs)

Bases: ROICalculator

이미지 크기에 비례하는 상대좌표 박스로 ROI를 계산합니다.

Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def __init__(self, **kwargs) -> None:
    self.set(**kwargs)

set

set(left: float, top: float, right: float, bottom: float) -> None
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def set(self, left: float, top: float, right: float, bottom: float) -> None:
    if not (0.0 <= left < right <= 1.0) or not (0.0 <= top < bottom <= 1.0):
        raise SimpleROIParameterValueError

    self.left = left
    self.top = top
    self.right = right
    self.bottom = bottom

__call__

__call__(image: Union[Image, ndarray], get_intermediate_results: bool = False, warmup: bool = False, **data) -> Dict
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def __call__(
    self,
    image: Union[Image.Image, np.ndarray],
    get_intermediate_results: bool = False,
    warmup: bool = False,
    **data,
) -> Dict:
    w, h = read_image_size(image)

    if self.left == 0.0 and self.top == 0.0 and self.right == 1.0 and self.bottom == 1.0:
        # ROI 좌표가 (0, 0, 1, 1)인 경우, 크롭을 하지 않고 원본 이미지를 그대로 반환합니다.
        return {"roi_coordinates": [0, 0, w, h], "do_crop": False}

    left = int(self.left * w)
    right = max(int(self.right * w), left + 1)
    top = int(self.top * h)
    bottom = max(int(self.bottom * h), top + 1)
    return {"roi_coordinates": [left, top, right, bottom], "do_crop": True}

PixelIntensityROI

PixelIntensityROI(**kwargs)

Bases: ROICalculator

픽셀값이 intensity 범위에 들어오는 픽셀만 필터링한 뒤, 필터링 된 픽셀들의 컨투어를 찾고, 가장 면적이 큰 컨투어를 감싸는 최소 박스로 ROI를 계산합니다.

Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def __init__(self, **kwargs) -> None:
    self.set(**kwargs)

set

set(intensity: Union[Tuple[int, int], List[int]], expansion: int, inversion: bool, offset_left: float, offset_right: float, offset_top: float, offset_bottom: float) -> None
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def set(
    self,
    intensity: Union[Tuple[int, int], List[int]],
    expansion: int,
    inversion: bool,
    offset_left: float,
    offset_right: float,
    offset_top: float,
    offset_bottom: float,
) -> None:
    if (
        not len(intensity) == 2
        or not isinstance(intensity[0], int)
        or not isinstance(intensity[1], int)
        or not (0 <= intensity[0] < intensity[1] <= 255)
    ):
        raise AdvancedROIParameterValueError

    if not (-10 <= expansion <= 10):
        raise AdvancedROIParameterValueError

    for offset in (offset_left, offset_right, offset_top, offset_bottom):
        if not (0 <= offset <= 2):
            raise AdvancedROIParameterValueError
    if not (offset_left + offset_right >= 0.1) or not (offset_top + offset_bottom >= 0.1):
        raise AdvancedROIParameterValueError

    self.intensity = intensity
    self.expansion = expansion
    self.inversion = inversion
    self.offset_left = offset_left
    self.offset_right = offset_right
    self.offset_top = offset_top
    self.offset_bottom = offset_bottom

__call__

__call__(image: Union[Image, ndarray], get_intermediate_results: bool = False, warmup: bool = False, **data) -> Dict
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def __call__(
    self,
    image: Union[Image.Image, np.ndarray],
    get_intermediate_results: bool = False,
    warmup: bool = False,
    **data,
) -> Dict:
    w, h = read_image_size(image)

    # PixelIntensityROI의 경우 input image size가 같더라도 output image size가 계속해서 달라질 수 있으며,
    # output image size의 최대 값은 input image size이기 때문에, warmup 시에 input image size를 return 함.
    if warmup:
        output = {"roi_coordinates": [0, 0, w, h], "do_crop": False}
        return output

    # 후속 연산을 위해 grayscale numpy array 이미지로 변환
    filtered_image = to_numpy(image, copy=False)
    filtered_image = convert_image_mode(filtered_image, "L", copy=False)

    # intensity 범위에 들어오는 픽셀을 찾고, 해당 픽셀들을 expand & inverse
    filtered_image = cv2.inRange(filtered_image, lowerb=self.intensity[0], upperb=self.intensity[1])

    expansion_kernel = cv2.getStructuringElement(shape=cv2.MORPH_RECT, ksize=(3, 3), anchor=(1, 1))
    if self.expansion > 0:
        filtered_image = cv2.dilate(
            filtered_image, kernel=expansion_kernel, anchor=(-1, -1), iterations=2 * self.expansion
        )
    elif self.expansion < 0:
        filtered_image = cv2.erode(
            filtered_image, kernel=expansion_kernel, anchor=(-1, -1), iterations=2 * self.expansion
        )

    if self.inversion:
        filtered_image = cv2.bitwise_not(filtered_image)

    contours, _ = cv2.findContours(
        filtered_image, mode=cv2.RETR_LIST, method=cv2.CHAIN_APPROX_SIMPLE
    )
    if contours:
        # contour 중 면적 가장 큰 것 선택 & bounding box 계산
        contour_areas = list(map(cv2.contourArea, contours))
        max_area_contour = contours[np.argmax(contour_areas)]
        left, top, roi_width, roi_height = cv2.boundingRect(max_area_contour)

        # bounding box에 offset 적용 & 이미지 내부로 clip
        roi_center_x = left + (roi_width / 2)
        roi_center_y = top + (roi_height / 2)

        roi_left = roi_center_x - (self.offset_left * roi_width / 2)
        roi_right = roi_center_x + (self.offset_right * roi_width / 2)
        roi_top = roi_center_y - (self.offset_top * roi_height / 2)
        roi_bottom = roi_center_y + (self.offset_bottom * roi_height / 2)

        roi_left = int(np.clip(roi_left, 0, w - 1))
        roi_right = int(np.clip(roi_right, roi_left + 1, w))
        roi_top = int(np.clip(roi_top, 0, h - 1))
        roi_bottom = int(np.clip(roi_bottom, roi_top + 1, h))

        roi_coordinates = [roi_left, roi_top, roi_right, roi_bottom]
    else:
        roi_coordinates = [0, 0, w, h]

    if (
        roi_coordinates[0] == 0
        and roi_coordinates[1] == 0
        and roi_coordinates[2] == w
        and roi_coordinates[3] == h
    ):
        # ROI 좌표가 (0, 0, w, h)인 경우, 크롭을 하지 않고 원본 이미지를 그대로 반환합니다.
        do_crop = False
    else:
        # ROI 좌표가 (0, 0, w, h)가 아닌 경우, 크롭을 수행합니다.
        do_crop = True

    output = {"roi_coordinates": roi_coordinates, "do_crop": do_crop}
    if get_intermediate_results:
        output["filtered_image"] = filtered_image // 255
    return output

AutoRelativeBoxROI

AutoRelativeBoxROI(padding: str = 'medium')

Bases: RelativeBoxROI

RelativeBoxROI class with automatic coordinate calculation.

Monostate pattern applied to prevent ROI coordinate mismatch between train ~ validation dataset.

Attributes:

  • left (float) –

    ROI coordinates.

  • top (float) –

    ROI coordinates.

  • right (float) –

    ROI coordinates.

  • bottom (float) –

    ROI coordinates.

  • is_ready (bol) –

    Whether auto ROI coordinates is set.

  • image_hw (Optional[List[int]]) –

    dataset image size.

  • discard_outer_polygons (bool) –

    Flag for discarding polygons outside of current ROI region.

  • expand_ratio

    Ratio for expanding ROI region. Larger value means more padding.

Example
# in dataset building...
@property
def files(self):
    return self._files

@files.setter
def files(self, value):
    self._files = value

    if isinstance(self.transform.roi_handler.roi_calculator, AutoRelativeBoxROI):
        self.transform.roi_handler.roi_calculator.autoupdate_roi_coordinate(self)
                                                                          # self is dataset
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def __init__(self, padding: str = "medium"):
    self.__dict__ = self.__shared_state

    if padding not in self.REGION_EXPAND_RATIO.keys():
        raise AutoROIParameterValueError(f"AutoROI mode {padding} not available.")

    self.expand_ratio = self.REGION_EXPAND_RATIO[padding]

__shared_state class-attribute instance-attribute

__shared_state = {'_is_ready': False, '_image_hw': None, '_left': -1.0, '_top': -1.0, '_right': -1.0, '_bottom': -1.0}

REGION_EXPAND_RATIO class-attribute instance-attribute

REGION_EXPAND_RATIO = {'small': 0.05, 'medium': 0.1, 'large': 0.2}

__dict__ instance-attribute

__dict__ = __shared_state

expand_ratio instance-attribute

expand_ratio = REGION_EXPAND_RATIO[padding]

is_ready property writable

is_ready

image_hw property writable

image_hw

left property writable

left

top property writable

top

right property writable

right

bottom property writable

bottom

_check_auto_roi_is_ready

_check_auto_roi_is_ready(function)
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def _check_auto_roi_is_ready(function):
    def wrapper(self, *args, **kwargs):
        if self.is_ready:
            return function(self, *args, **kwargs)
        else:
            raise AutoROIParameterRuntimeError(
                "AutoROI not avaliable. Check log and dataset config."
            )

    return wrapper

__call__

__call__(*args, **kwargs)
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
@_check_auto_roi_is_ready
def __call__(self, *args, **kwargs):
    return super().__call__(*args, **kwargs)

autoupdate_roi_coordinate

autoupdate_roi_coordinate(dataset: Dataset) -> bool

Automatically update ROI coordinate using dataset information.

Returns success flag.

Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def autoupdate_roi_coordinate(self, dataset: Dataset) -> bool:
    """Automatically update ROI coordinate using dataset information.

    Returns success flag.
    """

    # XXX: only called once
    if self.is_ready:
        logger.info("ROI coordinate already set. Update only once")
        return True

    marginal_label_region_xyxy = np.array([np.inf, np.inf, 0, 0])

    for data_idx in range(len(dataset)):
        file = dataset.files[data_idx]
        data_dict = dataset.load_image(file)
        data_dict.update(dataset.load_label(file))

        data_image: Union[Image.Image, np.ndarray] = data_dict["image"]
        current_img_hw = list(read_image_size(data_image))[::-1]

        if self.image_hw:
            if self.image_hw != current_img_hw:
                logger.warn("Variation in image size detected.. autoROI update aborted.")
                logger.warn(
                    f"Expected image size: {self.image_hw} / Given image size: {current_img_hw}"
                )
                return False

        self.image_hw = current_img_hw

        for polygon in data_dict["polygons"]:
            x_min, x_max = np.min(polygon[:, 0]), np.max(polygon[:, 0])
            y_min, y_max = np.min(polygon[:, 1]), np.max(polygon[:, 1])

            if x_min < marginal_label_region_xyxy[0]:
                marginal_label_region_xyxy[0] = x_min

            if y_min < marginal_label_region_xyxy[1]:
                marginal_label_region_xyxy[1] = y_min

            if x_max > marginal_label_region_xyxy[2]:
                marginal_label_region_xyxy[2] = x_max

            if y_max > marginal_label_region_xyxy[3]:
                marginal_label_region_xyxy[3] = y_max

    if not _check_region_xyxy_is_valid(marginal_label_region_xyxy):
        logger.warning(
            "AutoROI setting update failed. Needs at least one training data & valid polygon."
        )
        return False

    marginal_label_region_xyxy = _expand_region_xyxy(marginal_label_region_xyxy, self.expand_ratio)
    marginal_label_region_xyxy = _fit_region_into_img_size(
        marginal_label_region_xyxy,
        self.image_hw,
    )

    self.left = marginal_label_region_xyxy[0] / self.image_hw[1]
    self.top = marginal_label_region_xyxy[1] / self.image_hw[0]
    self.right = marginal_label_region_xyxy[2] / self.image_hw[1]
    self.bottom = marginal_label_region_xyxy[3] / self.image_hw[0]

    self.is_ready = True
    marginal_label_region_ratio = [self.left, self.top, self.right, self.bottom]
    logger.info(f"AutoROI setting success. ROI coordinates: {marginal_label_region_ratio}")

    return True

read_image_size

read_image_size(image: Union[Image, Tensor, ndarray]) -> ImageSizeType

image size: (W, H)

Source code in SaigeToolkit/data/transform/image_function.py
def read_image_size(image: Union[Image.Image, torch.Tensor, np.ndarray]) -> ImageSizeType:
    """image size: (W, H)"""
    if isinstance(image, Image.Image):
        return image.size
    elif isinstance(image, torch.Tensor) and image.ndim in (2, 3, 4):  # HW, CHW, BCHW
        return (image.shape[-1], image.shape[-2])
    elif isinstance(image, np.ndarray) and image.ndim in (2, 3):  # HW, HWC
        return (image.shape[1], image.shape[0])
    else:
        raise NotImplementedError

to_numpy

to_numpy(image: Union[Image, Tensor, ndarray], copy: bool = False) -> ndarray
Source code in SaigeToolkit/data/transform/image_function.py
def to_numpy(image: Union[Image.Image, torch.Tensor, np.ndarray], copy: bool = False) -> np.ndarray:
    if isinstance(image, Image.Image):
        image = np.array(image)  # Image.Image -> np.ndarray
    elif isinstance(image, torch.Tensor):
        assert image.ndim in [3, 4]  # BCHW / CHW

        if image.ndim == 4:  # if BCHW
            assert image.shape[0] == 1  # Batch size must be 1
            image = image.squeeze(0)  # BCHW -> CHW

        image = image.permute(1, 2, 0)  # CHW -> HWC

        if image.shape[2] == 1:  # if gray image
            image = image.squeeze(2)  # HWC -> HW

        image = image.detach().cpu().numpy()  # torch.Tensor, HW(C) -> np.ndarray, HW(C)
    elif isinstance(image, np.ndarray):
        if copy:
            image = image.copy()
    else:
        raise NotImplementedError

    # gray: HW / rgb & rgba: HWC
    return image

convert_image_mode

convert_image_mode(image: Union[Image, ndarray], mode: str, copy: bool = False) -> Union[Image, ndarray]

convert image mode

Parameters:

  • image (Union[Image, ndarray]) –

    image

  • mode (str) –

    "RGB" or "L"

  • copy (bool, default: False ) –

    to copy data. Defaults to False.

Returns:

  • Union[Image, ndarray]

    Union[Image.Image, np.ndarray]: converted image

Note

RGB -> L 변환 시 Image.Image와 np.ndarray 연산 결과가 다를 수 있음. (PIL과 cv2 에서 변환식은 L = R * 299/1000 + G * 587/1000 + B * 114/1000 로 동일하나 소숫점 처리 방식이 다름)

Source code in SaigeToolkit/data/transform/image_function.py
def convert_image_mode(
    image: Union[Image.Image, np.ndarray], mode: str, copy: bool = False
) -> Union[Image.Image, np.ndarray]:
    """convert image mode

    Args:
        image (Union[Image.Image, np.ndarray]): image
        mode (str): "RGB" or "L"
        copy (bool, optional): to copy data. Defaults to False.

    Returns:
        Union[Image.Image, np.ndarray]: converted image

    Note:
        RGB -> L 변환 시 Image.Image와 np.ndarray 연산 결과가 다를 수 있음.
        (PIL과 cv2 에서 변환식은 L = R * 299/1000 + G * 587/1000 + B * 114/1000 로 동일하나 소숫점 처리 방식이 다름)
    """
    if mode not in IMAGE_MODES:
        raise NotImplementedError

    if isinstance(image, Image.Image):
        if image.mode != mode:
            image = image.convert(mode)
        elif copy:
            image = image.copy()
    elif isinstance(image, np.ndarray):
        # check original mode & regularize (RGB: HW3 / L: HW)
        if image.ndim == 3:
            if image.shape[2] == 1:
                original_mode = "L"
                image = image[:, :, 0]  # HW1 -> HW (no copy)
            elif image.shape[2] == 2:
                original_mode = "L"
                image = image[:, :, 0]  # LA(HW2) -> L(HW) (no copy)
            elif image.shape[2] == 3:
                original_mode = "RGB"
            elif image.shape[2] == 4:
                original_mode = "RGB"
                image = image[:, :, :3]  # RGBA -> RGB (no copy)
            else:
                raise NotImplementedError
        elif image.ndim == 2:
            original_mode = "L"
        else:
            raise NotImplementedError
        # convert
        if original_mode == "RGB" and mode == "L":
            image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
        elif original_mode == "L" and mode == "RGB":
            image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
        elif copy:
            image = image.copy()
    else:
        raise NotImplementedError
    return image

_expand_region_xyxy

_expand_region_xyxy(region_xyxy: ndarray, expand_ratio: float) -> ndarray
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def _expand_region_xyxy(region_xyxy: np.ndarray, expand_ratio: float) -> np.ndarray:
    min_loc = region_xyxy[:2]
    max_loc = region_xyxy[2:]
    w, h = max_loc - min_loc
    offset_w: float = w * expand_ratio / 2
    offset_h: float = h * expand_ratio / 2
    offset = np.array([-offset_w, -offset_h, offset_w, offset_h])
    new_region_xyxy = (region_xyxy + offset).astype(int)
    return new_region_xyxy

_fit_region_into_img_size

_fit_region_into_img_size(region_xyxy: ndarray, img_shape_hw: Tuple[int, int]) -> ndarray
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def _fit_region_into_img_size(region_xyxy: np.ndarray, img_shape_hw: Tuple[int, int]) -> np.ndarray:
    H, W = img_shape_hw
    clipped_region_xyxy = region_xyxy.copy()
    clipped_region_xyxy[[0, 2]] = np.clip(region_xyxy[[0, 2]], 0, W)
    clipped_region_xyxy[[1, 3]] = np.clip(region_xyxy[[1, 3]], 0, H)
    return clipped_region_xyxy

_check_region_xyxy_is_valid

_check_region_xyxy_is_valid(region_xyxy: ndarray) -> bool
Source code in SaigeToolkit/data/transform/roi/roi_calculator.py
def _check_region_xyxy_is_valid(region_xyxy: np.ndarray) -> bool:
    return region_xyxy[0] < region_xyxy[2] and region_xyxy[1] < region_xyxy[3]