Skip to content

dataclass

Module diagram

classDiagram
  class dataclass {
  }
  class box {
  }
  class segment {
  }

data.dataclass

box

segment

Segment

Segment(bounding_box: Optional[SegmentBox] = None, bitmap: Optional[ndarray] = None, contours: Optional[Contours] = None, class_index: Optional[int] = None)

Segment 타입을 정의합니다. (= SegmentedObject) Segmentation 라벨링 혹은 모델의 예측 결과로 나오는 연결된 픽셀 덩어리이며, 1개의 outer polygon과 여러개의 inner polygon (도넛 형태인 경우) 으로 구성된 오브젝트입니다.

해당 오브젝트를 표현하는 방식은 2가지가 존재하며, segment를 이용해 어떤 연산을 수행하는가에 따라 다른 표현 방식이 필요합니다. 1. bounding_box & bitmap: SegmentBox & np.ndarray 2. contours: Sequence[np.ndarray]

Segment 오브젝트를 생성하기 위해서는 1가지 표현의 데이터만 필요하고, 다른 표현이 필요한 연산의 경우 lazy 한 방식으로 해당 표현을 계산합니다.

Segment 오브젝트 생성 시 bounding_box, bitmap 과 contours가 모두 주어진 경우, 서로 일치하는지 확인하지 않습니다.

Segment 오브젝트 생성 시 bounding_box, bitmap 표현이 주어진 경우 모든 픽셀이 연결되어 있는지 확인하지 않습니다.

Parameters:

  • bounding_box (Optional[SegmentBox], default: None ) –

    description. Defaults to None.

  • bitmap (Optional[Bitmap], default: None ) –

    description. Defaults to None.

  • contours (Optional[Contours], default: None ) –

    description. Defaults to None.

  • class_index (Optional[int], default: None ) –

    description. Defaults to None.

Source code in SaigeToolkit/data/dataclass/segment.py
def __init__(
    self,
    bounding_box: Optional[SegmentBox] = None,
    bitmap: Optional[np.ndarray] = None,
    contours: Optional[Contours] = None,
    class_index: Optional[int] = None,
) -> None:
    if contours is None:
        assert (
            bitmap is not None and bounding_box is not None
        ), "Either `bitmap` + `bounding_box` or `contours` must be given"
    self._bounding_box = bounding_box
    self._bitmap = bitmap
    self._contours = contours
    self.class_index = class_index

get_bounding_box_from_contours

get_bounding_box_from_contours(contours: Contours) -> SegmentBox

contours의 bounding box를 구합니다.

Source code in SaigeToolkit/data/dataclass/segment.py
def get_bounding_box_from_contours(contours: Contours) -> SegmentBox:
    """contours의 bounding box를 구합니다."""
    left, top, width, height = cv2.boundingRect(contours[0])
    return SegmentBox.from_xywh(left, top, width, height)

convert_contours_to_box_and_bitmap

convert_contours_to_box_and_bitmap(contours: Contours, bounding_box: Optional[SegmentBox] = None) -> Tuple[SegmentBox, Bitmap]

contours를 bounding_box 와 bitmap representation으로 변환합니다

Source code in SaigeToolkit/data/dataclass/segment.py
def convert_contours_to_box_and_bitmap(
    contours: Contours,
    bounding_box: Optional[SegmentBox] = None,
) -> Tuple[SegmentBox, Bitmap]:
    """contours를 bounding_box 와 bitmap representation으로 변환합니다"""
    if bounding_box is None:
        bounding_box = get_bounding_box_from_contours(contours)
    bitmap = cv2.drawContours(
        np.zeros((bounding_box.height, bounding_box.width), dtype=np.uint8),
        contours=contours,
        contourIdx=-1,
        color=BITMAP_PIXEL_VALUE,
        thickness=-1,
        offset=[-bounding_box.left, -bounding_box.top],
    )
    return bounding_box, bitmap

convert_box_and_bitmap_to_contours

convert_box_and_bitmap_to_contours(bitmap: Bitmap, bounding_box: Optional[SegmentBox] = None) -> Contours

bounding_box 와 bitmap을 contours representation으로 변환합니다

Source code in SaigeToolkit/data/dataclass/segment.py
def convert_box_and_bitmap_to_contours(
    bitmap: Bitmap,
    bounding_box: Optional[SegmentBox] = None,
) -> Contours:
    """bounding_box 와 bitmap을 contours representation으로 변환합니다"""
    polygons, hierarchy = cv2.findContours(bitmap, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
    if not polygons:
        raise SegmentValueError
    parents = hierarchy[0, :, -1]
    outers = np.where(parents == -1)[0]
    if len(outers) != 1:
        raise SegmentValueError
    outer_idx = outers[0]
    contours = [polygons[outer_idx]]
    for idx_inner in np.where(parents == outer_idx)[0]:
        contours.append(polygons[idx_inner])
    if bounding_box is not None:
        contours = [contour + (bounding_box.left, bounding_box.top) for contour in contours]
    return contours

compute_box_intersection

compute_box_intersection(box1: SegmentBox, box2: SegmentBox) -> Optional[SegmentBox]

intersecting box를 계산합니다. 겹치지 않는 경우 None

Source code in SaigeToolkit/data/dataclass/segment.py
def compute_box_intersection(box1: SegmentBox, box2: SegmentBox) -> Optional[SegmentBox]:
    """intersecting box를 계산합니다. 겹치지 않는 경우 None"""
    left = max(box1.left, box2.left)
    top = max(box1.top, box2.top)
    right = min(box1.right, box2.right)
    bottom = min(box1.bottom, box2.bottom)
    intersection = SegmentBox.from_xyxy(left, top, right, bottom)
    if intersection.width <= 0 or intersection.height <= 0:
        return None
    else:
        return intersection

to_segments

to_segments(segments: List[Union[Segment, Dict]]) -> List[Segment]

List[Union[Segment, Dict]]를을 List[Segment]들로 변환합니다

Source code in SaigeToolkit/data/dataclass/segment.py
def to_segments(segments: List[Union[Segment, Dict]]) -> List[Segment]:
    """List[Union[Segment, Dict]]를을 List[Segment]들로 변환합니다"""
    return [Segment.from_any(segment) for segment in segments]

merge_segments

merge_segments(segments: List[Segment]) -> Segment

여러 Segment들을 하나의 Segment로 합칩니다. (union)

Source code in SaigeToolkit/data/dataclass/segment.py
def merge_segments(segments: List[Segment]) -> Segment:
    """여러 Segment들을 하나의 Segment로 합칩니다. (union)"""
    if not segments:
        raise ValueError
    left = min(segment.bounding_box.left for segment in segments)
    top = min(segment.bounding_box.top for segment in segments)
    right = max(segment.bounding_box.right for segment in segments)
    bottom = max(segment.bounding_box.bottom for segment in segments)
    box = SegmentBox.from_xyxy(left, top, right, bottom)
    bitmap = np.zeros((box.height, box.width), dtype=np.uint8)
    for segment in segments:
        offset_x = segment.bounding_box.left - left
        offset_y = segment.bounding_box.top - top
        bitmap[
            offset_y : offset_y + segment.bounding_box.height,
            offset_x : offset_x + segment.bounding_box.width,
        ][segment.bitmap > 0] = BITMAP_PIXEL_VALUE
    return Segment(bounding_box=box, bitmap=bitmap, class_index=segments[0].class_index)

compute_contours_area_exact

compute_contours_area_exact(contours: Contours) -> int

bounding box 크기의 이미지에 contour를 그린 뒤 픽셀 개수 카운트

Source code in SaigeToolkit/data/dataclass/segment.py
def compute_contours_area_exact(contours: Contours) -> int:
    """bounding box 크기의 이미지에 contour를 그린 뒤 픽셀 개수 카운트"""
    _, bitmap = convert_contours_to_box_and_bitmap(contours=contours)
    return int(np.sum(bitmap) / BITMAP_PIXEL_VALUE)

compute_contours_area_fast

compute_contours_area_fast(contours: Contours) -> int

cv2.contourArea() 로 contour 면적 계산: contour 테두리를 0.5 픽셀 제외하고 계산되는 듯. average error: 17.08%

Source code in SaigeToolkit/data/dataclass/segment.py
def compute_contours_area_fast(contours: Contours) -> int:
    """cv2.contourArea() 로 contour 면적 계산: contour 테두리를 0.5 픽셀 제외하고 계산되는 듯.
    average error: 17.08%
    """
    outer = contours[0]
    area = cv2.contourArea(outer)
    for inner in contours[1:]:
        area -= cv2.contourArea(inner)
    return max(int(area), 1)

compute_contours_area_fast_plus

compute_contours_area_fast_plus(contours: Contours) -> int

cv2.contourArea() 로 contour 면적 계산, outer의 경우 cv2.arcLength() 더해줌 average error: 1.50%

Source code in SaigeToolkit/data/dataclass/segment.py
def compute_contours_area_fast_plus(contours: Contours) -> int:
    """cv2.contourArea() 로 contour 면적 계산, outer의 경우 cv2.arcLength() 더해줌
    average error: 1.50%

    """
    outer = contours[0]
    area = cv2.contourArea(outer) + 0.5 * cv2.arcLength(outer, closed=True)
    for inner in contours[1:]:
        area -= cv2.contourArea(inner)
    return max(int(area), 1)

compute_contours_area_fast_plusplus

compute_contours_area_fast_plusplus(contours: Contours) -> int

cv2.contourArea() + cv2.arcLength() 로 contour 면적 계산 average error: 1.58%

Source code in SaigeToolkit/data/dataclass/segment.py
def compute_contours_area_fast_plusplus(contours: Contours) -> int:
    """cv2.contourArea() + cv2.arcLength() 로 contour 면적 계산
    average error: 1.58%
    """
    outer = contours[0]
    area = cv2.contourArea(outer) + 0.5 * cv2.arcLength(outer, closed=True)
    for inner in contours[1:]:
        area -= cv2.contourArea(inner) + 0.5 * cv2.arcLength(inner, closed=True)
    return max(int(area), 1)

compute_contours_area

compute_contours_area(contours: Contours, method: str) -> int

contours의 면적을 계산합니다. 참고: https://www.notion.so/65e5a56b2b8744bea087b1a0d2f9bfbf

Source code in SaigeToolkit/data/dataclass/segment.py
def compute_contours_area(contours: Contours, method: str) -> int:
    """contours의 면적을 계산합니다.
    참고: https://www.notion.so/65e5a56b2b8744bea087b1a0d2f9bfbf
    """
    if method not in contours_area_methods:
        raise KeyError
    return contours_area_methods[method](contours)

compute_segment_intersection_area

compute_segment_intersection_area(segment1: Segment, segment2: Segment) -> int

두 Segment 사이의 겹치는 영역 넓이를 계산합니다. bounding_box를 이용해 겹치는 박스를 먼저 계산한 뒤 해당 박스만 잘라서 겹치는 픽셀 수를 셉니다.

Source code in SaigeToolkit/data/dataclass/segment.py
def compute_segment_intersection_area(segment1: Segment, segment2: Segment) -> int:
    """두 Segment 사이의 겹치는 영역 넓이를 계산합니다.
    bounding_box를 이용해 겹치는 박스를 먼저 계산한 뒤 해당 박스만 잘라서 겹치는 픽셀 수를 셉니다.
    """
    intersection_box = compute_box_intersection(segment1.bounding_box, segment2.bounding_box)
    if intersection_box is None:
        return 0

    left = intersection_box.left - segment1.bounding_box.left
    top = intersection_box.top - segment1.bounding_box.top
    bitmap1_intersection = segment1.bitmap[
        top : top + intersection_box.height,
        left : left + intersection_box.width,
    ]

    left = intersection_box.left - segment2.bounding_box.left
    top = intersection_box.top - segment2.bounding_box.top
    bitmap2_intersection = segment2.bitmap[
        top : top + intersection_box.height,
        left : left + intersection_box.width,
    ]
    return int(np.sum(bitmap1_intersection * bitmap2_intersection))

compute_segment_score

compute_segment_score(segment: Segment, scoremap: ndarray, method: str = 'mean', return_float: bool = False) -> Union[float, int]

scoremap 중 segment 영역의 score를 대표하는 값을 계산합니다.

Source code in SaigeToolkit/data/dataclass/segment.py
def compute_segment_score(
    segment: Segment,
    scoremap: np.ndarray,
    method: str = "mean",
    return_float: bool = False,
) -> Union[float, int]:
    """scoremap 중 segment 영역의 score를 대표하는 값을 계산합니다."""
    bbox = segment.bounding_box
    scoremap_crop = scoremap[bbox.top : bbox.top + bbox.height, bbox.left : bbox.left + bbox.width]
    scoremap_masked = scoremap_crop[segment.bitmap > 0]

    if method == "mean":
        score = scoremap_masked.mean()
    elif method == "max":
        score = scoremap_masked.max()
    else:
        raise ValueError

    if (not return_float) and np.issubdtype(scoremap.dtype, np.integer):
        return int(score)
    else:
        return float(score)

compute_segment_properties_and_apply_threshold

compute_segment_properties_and_apply_threshold(segment: Segment, calc_area_and_apply_threshold: bool = False, area_method: str = 'fast_plus', area_threshold: int = 0, calc_score_and_apply_threshold: bool = False, scoremap: Optional[ndarray] = None, score_method: str = 'mean', score_threshold: Union[float, int] = 0, score_as_float: bool = False) -> Optional[Dict]

API 계산 결과로 필요한 Segment의 property 들을 계산하고 threshold를 적용합니다. threshold에 걸리지 않는 경우 각 property들의 Dict를, threshold 에 걸리는 경우 None을 리턴합니다. 계산 로직은 다음과 순서로 적용됩니다.

  1. calc_object_area_and_apply_threshold=True 인 경우
    1. area 계산
    2. area_threshold 적용
  2. calc_object_score_and_apply_threshold=True 인 경우
    1. score 계산
    2. score_threshold 적용
  3. 아래 항목들 계산
    1. bounding_box
    2. bounding_rotated_box
    3. fitted_ellipse

Parameters:

Returns:

  • Optional[Dict]

    Optional[Dict]: segment property dictionary. threshold에 걸려서 필터링 된 경우 None

Source code in SaigeToolkit/data/dataclass/segment.py
def compute_segment_properties_and_apply_threshold(
    segment: Segment,
    calc_area_and_apply_threshold: bool = False,
    area_method: str = "fast_plus",
    area_threshold: int = 0,
    calc_score_and_apply_threshold: bool = False,
    scoremap: Optional[np.ndarray] = None,
    score_method: str = "mean",
    score_threshold: Union[float, int] = 0,
    score_as_float: bool = False,
) -> Optional[Dict]:
    """API 계산 결과로 필요한 Segment의 property 들을 계산하고 threshold를 적용합니다.
    threshold에 걸리지 않는 경우 각 property들의 Dict를, threshold 에 걸리는 경우 None을 리턴합니다.
    계산 로직은 다음과 순서로 적용됩니다.

    1. `calc_object_area_and_apply_threshold=True` 인 경우
        1. area 계산
        2. area_threshold 적용
    2. `calc_object_score_and_apply_threshold=True` 인 경우
        1. score 계산
        2. score_threshold 적용
    3. 아래 항목들 계산
        1. bounding_box
        2. bounding_rotated_box
        3. fitted_ellipse

    Args:
        segment (Segment): segment

    Returns:
        Optional[Dict]: segment property dictionary. threshold에 걸려서 필터링 된 경우 None
    """
    properties = {}

    # area
    if calc_area_and_apply_threshold:
        area = compute_contours_area(contours=segment.contours, method=area_method)
        if area < area_threshold:
            return None
        properties["area"] = area

    # score
    if calc_score_and_apply_threshold:
        assert scoremap is not None
        score = compute_segment_score(
            segment=segment,
            scoremap=scoremap,
            method=score_method,
            return_float=score_as_float,
        )
        if score < score_threshold:
            return None
        properties["score"] = score

    # default properties
    if segment.class_index is not None:
        properties["class_index"] = segment.class_index
    properties["contours"] = segment.contours
    outer = segment.contours[0]

    # bounding_box: v1 legacy logic
    bbox = segment.bounding_box
    properties["bounding_box"] = [bbox.left, bbox.top, bbox.width, bbox.height]

    # bounding_rot_box: v1 legacy logic
    ((center_x, center_y), (width, height), angle) = cv2.minAreaRect(outer)
    rot_box_points = cv2.boxPoints(((center_x, center_y), (width, height), angle)).tolist()
    bounding_rot_box = [[center_x, center_y], [width, height], angle, rot_box_points]
    properties["bounding_rot_box"] = bounding_rot_box

    # fitted_ellipse: v1 legacy logic
    fitted_ellipse = bounding_rot_box[:3]
    if len(outer) >= 5:
        ((center_x, center_y), (width, height), angle) = cv2.fitEllipse(outer)
        if width > 1e-5 and height > 1e-5:
            fitted_ellipse = [[center_x, center_y], [width, height], angle]
    properties["fitted_ellipse"] = fitted_ellipse

    return properties