Skip to content

roi

Module diagram

classDiagram
  class roi {
  }
  class api {
  }
  class roi_calculator {
  }
  class roi_handler {
  }
  roi --> api
  roi --> roi_handler
  api --> roi_handler
  roi_handler --> roi_calculator

data.transform.roi

ROI 좌표 계산 및 ROI 크롭, blind_mask 적용을 수행하는 ROIHandler 클래스를 제공합니다. Set-ROI 기능을 위한 API 클래스인 ROIHandlerAPI도 제공합니다.

ROIHandler

ROIHandler(**kwargs)

ROI 기능을 수행합니다. 이미지에 대한 ROI 좌표를 계산해 크롭하고, 크롭된 이미지에 blind_mask를 적용해 마스크 영역의 픽셀 값을 0으로 치환합니다.

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

roi_calculator_types class-attribute instance-attribute

roi_calculator_types = {'simple': RelativeBoxROI, 'advanced': PixelIntensityROI, 'auto': AutoRelativeBoxROI}

roi_calculator instance-attribute

roi_calculator: ROICalculator

set

set(mode: str, blind_mask: Union[None, ndarray, str], image_only: bool = False, discard_outer_polygons: bool = False, det_blind_mask_threshold: float = 0.5, **kwargs)
Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def set(
    self,
    mode: str,
    blind_mask: Union[None, np.ndarray, str],
    image_only: bool = False,
    discard_outer_polygons: bool = False,
    det_blind_mask_threshold: float = 0.5,
    **kwargs,
):
    if mode not in self.roi_calculator_types:
        raise ROIModeError

    # mode 변경된 경우 roi_calculator 인스턴스 새로 생성, 그렇지 않은 경우 set 호출
    if not hasattr(self, "mode") or self.mode != mode:
        self.mode = mode
        self.roi_calculator = self.roi_calculator_types[mode](**kwargs)
    else:
        self.roi_calculator.set(**kwargs)

    if isinstance(blind_mask, str):
        blind_mask = np.array(Image.open(blind_mask))
    if blind_mask is not None and (blind_mask.dtype != np.uint8 or blind_mask.ndim != 2):
        raise ROIBlindMaskValueError
    self.blind_mask = blind_mask

    self.image_only = image_only

    self.discard_outer_polygons = discard_outer_polygons

    self.det_blind_mask_threshold = det_blind_mask_threshold

apply_crop

apply_crop(image: Union[Image, ndarray, List[Union[Image, ndarray]]], return_revert_params: bool = False, warmup: bool = False, **data) -> Union[Dict, Tuple[Dict, Dict]]
Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def apply_crop(
    self,
    image: Union[Image.Image, np.ndarray, List[Union[Image.Image, np.ndarray]]],
    return_revert_params: bool = False,
    warmup: bool = False,
    **data,
) -> Union[Dict, Tuple[Dict, Dict]]:
    multipage = isinstance(image, list)

    roi_info = self.roi_calculator(
        image=image[0] if multipage else image,
        get_intermediate_results=False,
        warmup=warmup,
        **data,
    )
    roi_coordinates = roi_info["roi_coordinates"]  # [left, top, right, bottom]

    if multipage:
        image_size_before_roi = read_image_size(image[0])
        cropped_image = [crop(image_i, roi_coordinates) for image_i in image]
        image_size_after_roi = read_image_size(cropped_image[0])
    else:
        image_size_before_roi = read_image_size(image)
        cropped_image = crop(image, roi_coordinates)
        image_size_after_roi = read_image_size(cropped_image)

    data["image"] = cropped_image

    revert_params = {
        "image_size_before_roi": image_size_before_roi,
        "roi_coordinates": roi_coordinates,
        "image_size_after_roi": image_size_after_roi,
    }

    if self.image_only:
        return (data, revert_params) if return_revert_params else data

    # 라벨 크롭 & 마스킹
    if "mask" in data:
        data["mask"] = crop(data["mask"], roi_coordinates)

    if "bboxes" in data:
        data["bboxes"] = crop_box(data["bboxes"], roi_coordinates)

    if "polygons" in data:
        # NOTE: polygons에는 blind_mask가 적용되지 않습니다.
        new_polygons = []
        new_data = {key: [] for key in ["strings", "ignore"] if key in data}
        # TODO: add any keys to be updated which should have same length with polygons.

        for idx, polygon in enumerate(data["polygons"]):
            if self.discard_outer_polygons and not self._check_polygon_within_box(
                polygon, roi_coordinates
            ):
                continue

            new_polygons.append(polygon)
            for k, v in new_data.items():
                v.append(data[k][idx])

        new_polygons = translate_polygon(
            polygons=new_polygons,
            offset=roi_coordinates[:2],
        )

        data.update({"polygons": new_polygons, **new_data})

    return (data, revert_params) if return_revert_params else data

apply_mask

apply_mask(image: Union[Image, ndarray, List], **data)
Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def apply_mask(
    self,
    image: Union[Image.Image, np.ndarray, List],
    **data,
):
    if self.blind_mask is None:
        data["image"] = image
        return data

    multipage = isinstance(image, List)

    # cv2 resize: 약간 부정확하지만 빠름. 여기서는 아주 정확할 필요없음.
    image_size = read_image_size(image[0] if multipage else image)
    blind_mask = cv2.resize(self.blind_mask, dsize=image_size, interpolation=cv2.INTER_NEAREST)
    bool_mask = blind_mask > 0

    if multipage:
        data["image"] = [fill_pixels_with_mask(image_i, bool_mask, value=0) for image_i in image]
    else:
        data["image"] = fill_pixels_with_mask(image, bool_mask, value=0)

    if self.image_only:
        return data

    # 라벨 크롭 & 마스킹
    if "mask" in data:
        data["mask"] = fill_pixels_with_mask(data["mask"], bool_mask, value=0)

    if "bboxes" in data:
        indices_alive = []

        if len(data["bboxes"]):
            xyxy_bboxes = data["bboxes"].convert_coordinate("xyxy")

            for i, bbox in enumerate(xyxy_bboxes):
                x0, y0, x1, y1 = bbox

                label_area = (x1 - x0) * (y1 - y0)
                overlapping_area = np.sum(blind_mask[int(y0) : int(y1), int(x0) : int(x1)])

                if overlapping_area / label_area < self.det_blind_mask_threshold:
                    indices_alive.append(i)

        data["bboxes"] = data["bboxes"][indices_alive]

        if "labels" in data:
            data["labels"] = data["labels"][indices_alive]

    return data

__call__

__call__(return_revert_params: bool = False, warmup: bool = False, **data) -> Union[Dict, Tuple[Dict, Dict]]
Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def __call__(
    self,
    return_revert_params: bool = False,
    warmup: bool = False,
    **data,
) -> Union[Dict, Tuple[Dict, Dict]]:
    if return_revert_params:
        data, revert_params = self.apply_crop(return_revert_params=True, warmup=warmup, **data)
        data = self.apply_mask(**data)
        return data, revert_params
    else:
        data = self.apply_crop(return_revert_params=False, warmup=warmup, **data)
        data = self.apply_mask(**data)
        return data

_check_polygon_within_box

_check_polygon_within_box(polygon: List[List[float]], roi_coordinates: List[int]) -> bool
Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def _check_polygon_within_box(self, polygon: List[List[float]], roi_coordinates: List[int]) -> bool:
    left, top, right, bottom = roi_coordinates

    def check_x_cord(p):
        return p >= left and p < right

    def check_y_cord(p):
        return p >= top and p < bottom

    def check_point_within_box(point_xy):
        x, y = point_xy
        return check_x_cord(x) and check_y_cord(y)

    return all(check_point_within_box(p) for p in polygon)

ROIHandlerAPI

ROIHandlerAPI(**kwargs)

Set-ROI 기능을 위한 API입니다.

Usage
# 핸들러 빌드. 아래는 simple mode의 예제 config이며, 자세한 설명은 ROIHandlerAPI.set() 함수 참고.
config = {
    "mode": "simple",
    "left": 0.0,
    "top": 0.0,
    "right": 1.0,
    "bottom": 1.0,
    "blind_mask": None,
}
error, roi_hander = ROIHandlerAPI.build(config)

# image에 대한 roi 계산. 리턴 결과 설명은 ROIHandlerAPI.apply() 함수 참고.
image = np.zeros((100, 100, 3), dtype=np.unit8)
error, roi_results = roi_handler.apply(image)

# roi 파라미터 변경
config["left"] = 0.1
error, _  = roi_handler.set(config)

# 변경된 파라미터로 roi 다시 계산
error, roi_results = roi_handler.apply(image)
Source code in SaigeToolkit/data/transform/roi/api.py
def __init__(self, **kwargs) -> None:
    self.handler = ROIHandler(**kwargs)

handler instance-attribute

handler = ROIHandler(**kwargs)

build classmethod

build(config: Dict) -> ROIHandlerAPI

ROIHandlerAPI를 빌드합니다. Args: config (Dict): ROIHandlerAPI.set의 파라미터와 동일합니다.

Returns:

Source code in SaigeToolkit/data/transform/roi/api.py
@classmethod
@error_handler
def build(cls, config: Dict) -> ROIHandlerAPI:
    """ROIHandlerAPI를 빌드합니다.
    Args:
        config (Dict): ROIHandlerAPI.set의 파라미터와 동일합니다.

    Returns:
        ROIHandlerAPI: 빌드된 ROIHandlerAPI
    """
    return cls(**config)

set

set(config: Dict) -> None

ROI 파라미터를 변경합니다.

Parameters:

  • config (Dict) –

    ROI 파라미터의 dict입니다. mode에 따라 다른 파라미터를 가집니다.

    # simple mode:
    {
        "mode": "simple",  # Simple ROI 모드.
        "left": float,  # ROI의 왼쪽 경계. [0.0, 1.0) 범위의 실수.
        "top": float,  # ROI의 위쪽 경계. [0.0, 1.0) 범위의 실수.
        "right": float,  # ROI의 오른쪽 경계. (right, 1.0] 범위의 실수.
        "bottom": float,  # ROI의 아래쪽 경계. (top, 1.0] 범위의 실수.
        "blind_mask": Optional[np.ndarray],  # blind mask 이미지. None인 경우 blind 적용 안함.
                                             # np.ndarray인 경우 (dtype=uint8, shape=(H_roi, W_roi))이며 픽셀 값은 0 또는 1.
                                             # mask의 값이 1인 영역이 학습/검사 시 마스킹됩니다.
                                             # polygons에는 blind_mask가 적용되지 않습니다.
    }
    # advanced mode:
    {
        "mode": "advanced",  # Advanced ROI 모드.
        "intensity": List[int],  # 필터링할 [최소, 최대] 픽셀값 범위. 각 값은 [0, 255] 범위의 정수.
        "expansion": int,  # 필터링된 픽셀 영역에 대한 확장/축소 정도. [-10, 10] 범위의 정수.
        "inversion": bool,  # True인 경우 필터링된 픽셀 영역을 반전.
        "offset_left": float,  # ROI 박스의 왼쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
        "offset_right": float,  # ROI 박스의 오른쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
        "offset_top": float,  # ROI 박스의 위쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
        "offset_bottom": float,  # ROI 박스의 아래쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
        "blind_mask": Optional[np.ndarray],  # blind mask 이미지. None인 경우 blind 적용 안함.
                                             # np.ndarray인 경우 (dtype=uint8, shape=(H_roi, W_roi))이며 픽셀 값은 0 또는 1.
                                             # mask의 값이 1인 영역이 학습/검사 시 마스킹됩니다.
                                             # polygons에는 blind_mask가 적용되지 않습니다.
    }
    

Source code in SaigeToolkit/data/transform/roi/api.py
@error_handler
def set(self, config: Dict) -> None:
    """ROI 파라미터를 변경합니다.

    Args:
        config (Dict): ROI 파라미터의 dict입니다. mode에 따라 다른 파라미터를 가집니다.
            ```python
            # simple mode:
            {
                "mode": "simple",  # Simple ROI 모드.
                "left": float,  # ROI의 왼쪽 경계. [0.0, 1.0) 범위의 실수.
                "top": float,  # ROI의 위쪽 경계. [0.0, 1.0) 범위의 실수.
                "right": float,  # ROI의 오른쪽 경계. (right, 1.0] 범위의 실수.
                "bottom": float,  # ROI의 아래쪽 경계. (top, 1.0] 범위의 실수.
                "blind_mask": Optional[np.ndarray],  # blind mask 이미지. None인 경우 blind 적용 안함.
                                                     # np.ndarray인 경우 (dtype=uint8, shape=(H_roi, W_roi))이며 픽셀 값은 0 또는 1.
                                                     # mask의 값이 1인 영역이 학습/검사 시 마스킹됩니다.
                                                     # polygons에는 blind_mask가 적용되지 않습니다.
            }
            # advanced mode:
            {
                "mode": "advanced",  # Advanced ROI 모드.
                "intensity": List[int],  # 필터링할 [최소, 최대] 픽셀값 범위. 각 값은 [0, 255] 범위의 정수.
                "expansion": int,  # 필터링된 픽셀 영역에 대한 확장/축소 정도. [-10, 10] 범위의 정수.
                "inversion": bool,  # True인 경우 필터링된 픽셀 영역을 반전.
                "offset_left": float,  # ROI 박스의 왼쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
                "offset_right": float,  # ROI 박스의 오른쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
                "offset_top": float,  # ROI 박스의 위쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
                "offset_bottom": float,  # ROI 박스의 아래쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
                "blind_mask": Optional[np.ndarray],  # blind mask 이미지. None인 경우 blind 적용 안함.
                                                     # np.ndarray인 경우 (dtype=uint8, shape=(H_roi, W_roi))이며 픽셀 값은 0 또는 1.
                                                     # mask의 값이 1인 영역이 학습/검사 시 마스킹됩니다.
                                                     # polygons에는 blind_mask가 적용되지 않습니다.
            }
            ```
    """
    self.handler.set(**config)

apply

apply(image: ndarray) -> Dict

image에 대한 ROI 좌표 및 기타 결과를 계산합니다.

Parameters:

  • image (ndarray) –

    ROI를 적용할 입력 이미지.

Returns:

  • Dict ( Dict ) –

    ROI 계산 결과 dict 입니다. mode에 따라 다른 결과 값들을 가집니다.

    # simple mode:
    {
        "roi_coordinates": List[int],  # [left, top, right, bottom].
    }
    # advanced mode:
    {
        "roi_coordinates": List[int],  # [left, top, right, bottom].
        "filtered_image": np.ndarray(uint8, shape=(H, W)),  # intensity, expansion, inversion이 적용된 중간 결과 이미지 입니다. (픽셀값: 0 or 1)
    }
    

Source code in SaigeToolkit/data/transform/roi/api.py
@error_handler
def apply(self, image: np.ndarray) -> Dict:
    """`image`에 대한 ROI 좌표 및 기타 결과를 계산합니다.

    Args:
        image (np.ndarray): ROI를 적용할 입력 이미지.

    Returns:
        Dict: ROI 계산 결과 dict 입니다. mode에 따라 다른 결과 값들을 가집니다.
            ```python
            # simple mode:
            {
                "roi_coordinates": List[int],  # [left, top, right, bottom].
            }
            # advanced mode:
            {
                "roi_coordinates": List[int],  # [left, top, right, bottom].
                "filtered_image": np.ndarray(uint8, shape=(H, W)),  # intensity, expansion, inversion이 적용된 중간 결과 이미지 입니다. (픽셀값: 0 or 1)
            }
            ```
    """
    return self.handler.roi_calculator(image=image, get_intermediate_results=True)

api

error_handler module-attribute

error_handler = get_error_handler(SaigeToolkitError)

ROIHandlerAPI

ROIHandlerAPI(**kwargs)

Set-ROI 기능을 위한 API입니다.

Usage
# 핸들러 빌드. 아래는 simple mode의 예제 config이며, 자세한 설명은 ROIHandlerAPI.set() 함수 참고.
config = {
    "mode": "simple",
    "left": 0.0,
    "top": 0.0,
    "right": 1.0,
    "bottom": 1.0,
    "blind_mask": None,
}
error, roi_hander = ROIHandlerAPI.build(config)

# image에 대한 roi 계산. 리턴 결과 설명은 ROIHandlerAPI.apply() 함수 참고.
image = np.zeros((100, 100, 3), dtype=np.unit8)
error, roi_results = roi_handler.apply(image)

# roi 파라미터 변경
config["left"] = 0.1
error, _  = roi_handler.set(config)

# 변경된 파라미터로 roi 다시 계산
error, roi_results = roi_handler.apply(image)
Source code in SaigeToolkit/data/transform/roi/api.py
def __init__(self, **kwargs) -> None:
    self.handler = ROIHandler(**kwargs)
handler instance-attribute
handler = ROIHandler(**kwargs)
build classmethod
build(config: Dict) -> ROIHandlerAPI

ROIHandlerAPI를 빌드합니다. Args: config (Dict): ROIHandlerAPI.set의 파라미터와 동일합니다.

Returns:

Source code in SaigeToolkit/data/transform/roi/api.py
@classmethod
@error_handler
def build(cls, config: Dict) -> ROIHandlerAPI:
    """ROIHandlerAPI를 빌드합니다.
    Args:
        config (Dict): ROIHandlerAPI.set의 파라미터와 동일합니다.

    Returns:
        ROIHandlerAPI: 빌드된 ROIHandlerAPI
    """
    return cls(**config)
set
set(config: Dict) -> None

ROI 파라미터를 변경합니다.

Parameters:

  • config (Dict) –

    ROI 파라미터의 dict입니다. mode에 따라 다른 파라미터를 가집니다.

    # simple mode:
    {
        "mode": "simple",  # Simple ROI 모드.
        "left": float,  # ROI의 왼쪽 경계. [0.0, 1.0) 범위의 실수.
        "top": float,  # ROI의 위쪽 경계. [0.0, 1.0) 범위의 실수.
        "right": float,  # ROI의 오른쪽 경계. (right, 1.0] 범위의 실수.
        "bottom": float,  # ROI의 아래쪽 경계. (top, 1.0] 범위의 실수.
        "blind_mask": Optional[np.ndarray],  # blind mask 이미지. None인 경우 blind 적용 안함.
                                             # np.ndarray인 경우 (dtype=uint8, shape=(H_roi, W_roi))이며 픽셀 값은 0 또는 1.
                                             # mask의 값이 1인 영역이 학습/검사 시 마스킹됩니다.
                                             # polygons에는 blind_mask가 적용되지 않습니다.
    }
    # advanced mode:
    {
        "mode": "advanced",  # Advanced ROI 모드.
        "intensity": List[int],  # 필터링할 [최소, 최대] 픽셀값 범위. 각 값은 [0, 255] 범위의 정수.
        "expansion": int,  # 필터링된 픽셀 영역에 대한 확장/축소 정도. [-10, 10] 범위의 정수.
        "inversion": bool,  # True인 경우 필터링된 픽셀 영역을 반전.
        "offset_left": float,  # ROI 박스의 왼쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
        "offset_right": float,  # ROI 박스의 오른쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
        "offset_top": float,  # ROI 박스의 위쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
        "offset_bottom": float,  # ROI 박스의 아래쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
        "blind_mask": Optional[np.ndarray],  # blind mask 이미지. None인 경우 blind 적용 안함.
                                             # np.ndarray인 경우 (dtype=uint8, shape=(H_roi, W_roi))이며 픽셀 값은 0 또는 1.
                                             # mask의 값이 1인 영역이 학습/검사 시 마스킹됩니다.
                                             # polygons에는 blind_mask가 적용되지 않습니다.
    }
    

Source code in SaigeToolkit/data/transform/roi/api.py
@error_handler
def set(self, config: Dict) -> None:
    """ROI 파라미터를 변경합니다.

    Args:
        config (Dict): ROI 파라미터의 dict입니다. mode에 따라 다른 파라미터를 가집니다.
            ```python
            # simple mode:
            {
                "mode": "simple",  # Simple ROI 모드.
                "left": float,  # ROI의 왼쪽 경계. [0.0, 1.0) 범위의 실수.
                "top": float,  # ROI의 위쪽 경계. [0.0, 1.0) 범위의 실수.
                "right": float,  # ROI의 오른쪽 경계. (right, 1.0] 범위의 실수.
                "bottom": float,  # ROI의 아래쪽 경계. (top, 1.0] 범위의 실수.
                "blind_mask": Optional[np.ndarray],  # blind mask 이미지. None인 경우 blind 적용 안함.
                                                     # np.ndarray인 경우 (dtype=uint8, shape=(H_roi, W_roi))이며 픽셀 값은 0 또는 1.
                                                     # mask의 값이 1인 영역이 학습/검사 시 마스킹됩니다.
                                                     # polygons에는 blind_mask가 적용되지 않습니다.
            }
            # advanced mode:
            {
                "mode": "advanced",  # Advanced ROI 모드.
                "intensity": List[int],  # 필터링할 [최소, 최대] 픽셀값 범위. 각 값은 [0, 255] 범위의 정수.
                "expansion": int,  # 필터링된 픽셀 영역에 대한 확장/축소 정도. [-10, 10] 범위의 정수.
                "inversion": bool,  # True인 경우 필터링된 픽셀 영역을 반전.
                "offset_left": float,  # ROI 박스의 왼쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
                "offset_right": float,  # ROI 박스의 오른쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
                "offset_top": float,  # ROI 박스의 위쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
                "offset_bottom": float,  # ROI 박스의 아래쪽 사이즈. [0.0, 2.0] 범위의 실수 이며, 1인 경우 기본 크기.
                "blind_mask": Optional[np.ndarray],  # blind mask 이미지. None인 경우 blind 적용 안함.
                                                     # np.ndarray인 경우 (dtype=uint8, shape=(H_roi, W_roi))이며 픽셀 값은 0 또는 1.
                                                     # mask의 값이 1인 영역이 학습/검사 시 마스킹됩니다.
                                                     # polygons에는 blind_mask가 적용되지 않습니다.
            }
            ```
    """
    self.handler.set(**config)
apply
apply(image: ndarray) -> Dict

image에 대한 ROI 좌표 및 기타 결과를 계산합니다.

Parameters:

  • image (ndarray) –

    ROI를 적용할 입력 이미지.

Returns:

  • Dict ( Dict ) –

    ROI 계산 결과 dict 입니다. mode에 따라 다른 결과 값들을 가집니다.

    # simple mode:
    {
        "roi_coordinates": List[int],  # [left, top, right, bottom].
    }
    # advanced mode:
    {
        "roi_coordinates": List[int],  # [left, top, right, bottom].
        "filtered_image": np.ndarray(uint8, shape=(H, W)),  # intensity, expansion, inversion이 적용된 중간 결과 이미지 입니다. (픽셀값: 0 or 1)
    }
    

Source code in SaigeToolkit/data/transform/roi/api.py
@error_handler
def apply(self, image: np.ndarray) -> Dict:
    """`image`에 대한 ROI 좌표 및 기타 결과를 계산합니다.

    Args:
        image (np.ndarray): ROI를 적용할 입력 이미지.

    Returns:
        Dict: ROI 계산 결과 dict 입니다. mode에 따라 다른 결과 값들을 가집니다.
            ```python
            # simple mode:
            {
                "roi_coordinates": List[int],  # [left, top, right, bottom].
            }
            # advanced mode:
            {
                "roi_coordinates": List[int],  # [left, top, right, bottom].
                "filtered_image": np.ndarray(uint8, shape=(H, W)),  # intensity, expansion, inversion이 적용된 중간 결과 이미지 입니다. (픽셀값: 0 or 1)
            }
            ```
    """
    return self.handler.roi_calculator(image=image, get_intermediate_results=True)

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)
    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]}

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]}
        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]

    output = {"roi_coordinates": roi_coordinates}
    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

_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]

roi_handler

ROIHandler

ROIHandler(**kwargs)

ROI 기능을 수행합니다. 이미지에 대한 ROI 좌표를 계산해 크롭하고, 크롭된 이미지에 blind_mask를 적용해 마스크 영역의 픽셀 값을 0으로 치환합니다.

Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def __init__(self, **kwargs) -> None:
    self.set(**kwargs)
roi_calculator_types class-attribute instance-attribute
roi_calculator_types = {'simple': RelativeBoxROI, 'advanced': PixelIntensityROI, 'auto': AutoRelativeBoxROI}
roi_calculator instance-attribute
roi_calculator: ROICalculator
set
set(mode: str, blind_mask: Union[None, ndarray, str], image_only: bool = False, discard_outer_polygons: bool = False, det_blind_mask_threshold: float = 0.5, **kwargs)
Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def set(
    self,
    mode: str,
    blind_mask: Union[None, np.ndarray, str],
    image_only: bool = False,
    discard_outer_polygons: bool = False,
    det_blind_mask_threshold: float = 0.5,
    **kwargs,
):
    if mode not in self.roi_calculator_types:
        raise ROIModeError

    # mode 변경된 경우 roi_calculator 인스턴스 새로 생성, 그렇지 않은 경우 set 호출
    if not hasattr(self, "mode") or self.mode != mode:
        self.mode = mode
        self.roi_calculator = self.roi_calculator_types[mode](**kwargs)
    else:
        self.roi_calculator.set(**kwargs)

    if isinstance(blind_mask, str):
        blind_mask = np.array(Image.open(blind_mask))
    if blind_mask is not None and (blind_mask.dtype != np.uint8 or blind_mask.ndim != 2):
        raise ROIBlindMaskValueError
    self.blind_mask = blind_mask

    self.image_only = image_only

    self.discard_outer_polygons = discard_outer_polygons

    self.det_blind_mask_threshold = det_blind_mask_threshold
apply_crop
apply_crop(image: Union[Image, ndarray, List[Union[Image, ndarray]]], return_revert_params: bool = False, warmup: bool = False, **data) -> Union[Dict, Tuple[Dict, Dict]]
Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def apply_crop(
    self,
    image: Union[Image.Image, np.ndarray, List[Union[Image.Image, np.ndarray]]],
    return_revert_params: bool = False,
    warmup: bool = False,
    **data,
) -> Union[Dict, Tuple[Dict, Dict]]:
    multipage = isinstance(image, list)

    roi_info = self.roi_calculator(
        image=image[0] if multipage else image,
        get_intermediate_results=False,
        warmup=warmup,
        **data,
    )
    roi_coordinates = roi_info["roi_coordinates"]  # [left, top, right, bottom]

    if multipage:
        image_size_before_roi = read_image_size(image[0])
        cropped_image = [crop(image_i, roi_coordinates) for image_i in image]
        image_size_after_roi = read_image_size(cropped_image[0])
    else:
        image_size_before_roi = read_image_size(image)
        cropped_image = crop(image, roi_coordinates)
        image_size_after_roi = read_image_size(cropped_image)

    data["image"] = cropped_image

    revert_params = {
        "image_size_before_roi": image_size_before_roi,
        "roi_coordinates": roi_coordinates,
        "image_size_after_roi": image_size_after_roi,
    }

    if self.image_only:
        return (data, revert_params) if return_revert_params else data

    # 라벨 크롭 & 마스킹
    if "mask" in data:
        data["mask"] = crop(data["mask"], roi_coordinates)

    if "bboxes" in data:
        data["bboxes"] = crop_box(data["bboxes"], roi_coordinates)

    if "polygons" in data:
        # NOTE: polygons에는 blind_mask가 적용되지 않습니다.
        new_polygons = []
        new_data = {key: [] for key in ["strings", "ignore"] if key in data}
        # TODO: add any keys to be updated which should have same length with polygons.

        for idx, polygon in enumerate(data["polygons"]):
            if self.discard_outer_polygons and not self._check_polygon_within_box(
                polygon, roi_coordinates
            ):
                continue

            new_polygons.append(polygon)
            for k, v in new_data.items():
                v.append(data[k][idx])

        new_polygons = translate_polygon(
            polygons=new_polygons,
            offset=roi_coordinates[:2],
        )

        data.update({"polygons": new_polygons, **new_data})

    return (data, revert_params) if return_revert_params else data
apply_mask
apply_mask(image: Union[Image, ndarray, List], **data)
Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def apply_mask(
    self,
    image: Union[Image.Image, np.ndarray, List],
    **data,
):
    if self.blind_mask is None:
        data["image"] = image
        return data

    multipage = isinstance(image, List)

    # cv2 resize: 약간 부정확하지만 빠름. 여기서는 아주 정확할 필요없음.
    image_size = read_image_size(image[0] if multipage else image)
    blind_mask = cv2.resize(self.blind_mask, dsize=image_size, interpolation=cv2.INTER_NEAREST)
    bool_mask = blind_mask > 0

    if multipage:
        data["image"] = [fill_pixels_with_mask(image_i, bool_mask, value=0) for image_i in image]
    else:
        data["image"] = fill_pixels_with_mask(image, bool_mask, value=0)

    if self.image_only:
        return data

    # 라벨 크롭 & 마스킹
    if "mask" in data:
        data["mask"] = fill_pixels_with_mask(data["mask"], bool_mask, value=0)

    if "bboxes" in data:
        indices_alive = []

        if len(data["bboxes"]):
            xyxy_bboxes = data["bboxes"].convert_coordinate("xyxy")

            for i, bbox in enumerate(xyxy_bboxes):
                x0, y0, x1, y1 = bbox

                label_area = (x1 - x0) * (y1 - y0)
                overlapping_area = np.sum(blind_mask[int(y0) : int(y1), int(x0) : int(x1)])

                if overlapping_area / label_area < self.det_blind_mask_threshold:
                    indices_alive.append(i)

        data["bboxes"] = data["bboxes"][indices_alive]

        if "labels" in data:
            data["labels"] = data["labels"][indices_alive]

    return data
__call__
__call__(return_revert_params: bool = False, warmup: bool = False, **data) -> Union[Dict, Tuple[Dict, Dict]]
Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def __call__(
    self,
    return_revert_params: bool = False,
    warmup: bool = False,
    **data,
) -> Union[Dict, Tuple[Dict, Dict]]:
    if return_revert_params:
        data, revert_params = self.apply_crop(return_revert_params=True, warmup=warmup, **data)
        data = self.apply_mask(**data)
        return data, revert_params
    else:
        data = self.apply_crop(return_revert_params=False, warmup=warmup, **data)
        data = self.apply_mask(**data)
        return data
_check_polygon_within_box
_check_polygon_within_box(polygon: List[List[float]], roi_coordinates: List[int]) -> bool
Source code in SaigeToolkit/data/transform/roi/roi_handler.py
def _check_polygon_within_box(self, polygon: List[List[float]], roi_coordinates: List[int]) -> bool:
    left, top, right, bottom = roi_coordinates

    def check_x_cord(p):
        return p >= left and p < right

    def check_y_cord(p):
        return p >= top and p < bottom

    def check_point_within_box(point_xy):
        x, y = point_xy
        return check_x_cord(x) and check_y_cord(y)

    return all(check_point_within_box(p) for p in polygon)