```
::::::::: {.document role="main" itemscope="itemscope" itemtype="http://schema.org/Article"}
:::::::: {itemprop="articleBody"}
::::::: {#module-imgutils.preprocess.pillow .section}
[]{#imgutils-preprocess-pillow}

# imgutils.preprocess.pillow[](#module-imgutils.preprocess.pillow "Permalink to this heading"){.headerlink}

This module provides utilities for image processing using the PIL
library, allowing for transformations such as resizing, cropping,
converting to tensor, normalization, and more. It supports both basic
and composite transformations, which can be registered and parsed
dynamically. The module also offers functionality to convert images to
tensors and normalize them, making it suitable for preprocessing tasks
in machine learning applications.

Transforms can be applied individually or composed into sequences using
the PillowCompose class, which applies a list of transforms sequentially
to an image. Each transformation can be registered with decorators to
facilitate dynamic creation and parsing based on configuration
dictionaries or lists, supporting flexible and configurable
preprocessing pipelines.

::: {#register-pillow-transform .section}
## register_pillow_transform[](#register-pillow-transform "Permalink to this heading"){.headerlink}

[[imgutils.preprocess.pillow.]{.pre}]{.sig-prename .descclassname}[[register_pillow_transform]{.pre}]{.sig-name .descname}[(]{.sig-paren}*[[name]{.pre}]{.n}[[:]{.pre}]{.p}[ ]{.w}[[str]{.pre}]{.n}*[)]{.sig-paren}[[[\[source\]]{.pre}]{.viewcode-link}](../../_modules/imgutils/preprocess/pillow.html#register_pillow_transform){.reference .internal}[](#imgutils.preprocess.pillow.register_pillow_transform "Permalink to this definition"){.headerlink}

:   Decorator to register a function as a creator for a specific type of
    Pillow transform.

    Parameters[:]{.colon}

    :   **name** (*str*) -- The name of the transform.
:::

::: {#register-pillow-parse .section}
## register_pillow_parse[](#register-pillow-parse "Permalink to this heading"){.headerlink}

[[imgutils.preprocess.pillow.]{.pre}]{.sig-prename .descclassname}[[register_pillow_parse]{.pre}]{.sig-name .descname}[(]{.sig-paren}*[[name]{.pre}]{.n}[[:]{.pre}]{.p}[ ]{.w}[[str]{.pre}]{.n}*[)]{.sig-paren}[[[\[source\]]{.pre}]{.viewcode-link}](../../_modules/imgutils/preprocess/pillow.html#register_pillow_parse){.reference .internal}[](#imgutils.preprocess.pillow.register_pillow_parse "Permalink to this definition"){.headerlink}

:   Decorator to register a function as a parser for a specific type of
    Pillow transform.

    Parameters[:]{.colon}

    :   **name** (*str*) -- The name of the transform.
:::

::: {#create-pillow-transforms .section}
## create_pillow_transforms[](#create-pillow-transforms "Permalink to this heading"){.headerlink}

[[imgutils.preprocess.pillow.]{.pre}]{.sig-prename .descclassname}[[create_pillow_transforms]{.pre}]{.sig-name .descname}[(]{.sig-paren}*[[tvalue]{.pre}]{.n}[[:]{.pre}]{.p}[ ]{.w}[[list]{.pre}[ ]{.w}[[\|]{.pre}]{.p}[ ]{.w}[dict]{.pre}]{.n}*[)]{.sig-paren}[[[\[source\]]{.pre}]{.viewcode-link}](../../_modules/imgutils/preprocess/pillow.html#create_pillow_transforms){.reference .internal}[](#imgutils.preprocess.pillow.create_pillow_transforms "Permalink to this definition"){.headerlink}

:   Create a transformation or a composition of transformations based on
    the input value.

    Parameters[:]{.colon}

    :   **tvalue** (*Union\[list,* *dict\]*) -- A list or dictionary
        describing the transformation(s).

    Returns[:]{.colon}

    :   A transformation or a composition of transformations.

    Return type[:]{.colon}

    :   Union\[PillowCompose, Any\]

    Raises[:]{.colon}

    :   **TypeError** -- If the input value is not a list or dictionary.

    Example[:]{.colon}

    :   :::: {.doctest .highlight-default .notranslate}
        ::: highlight
            >>> from imgutils.preprocess import create_pillow_transforms
            >>>
            >>> create_pillow_transforms({
            ...     'type': 'resize',
            ...     'size': 384,
            ...     'interpolation': 'bicubic',
            ... })
            PillowResize(size=384, interpolation=bicubic, max_size=None, antialias=True)
            >>> create_pillow_transforms({
            ...     'type': 'resize',
            ...     'size': (224, 256),
            ...     'interpolation': 'bilinear',
            ... })
            PillowResize(size=(224, 256), interpolation=bilinear, max_size=None, antialias=True)
            >>> create_pillow_transforms({'type': 'center_crop', 'size': 224})
            PillowCenterCrop(size=(224, 224))
            >>> create_pillow_transforms({'type': 'to_tensor'})
            PillowToTensor()
            >>> create_pillow_transforms({'type': 'maybe_to_tensor'})
            PillowMaybeToTensor()
            >>> create_pillow_transforms({'type': 'normalize', 'mean': 0.5, 'std': 0.5})
            PillowNormalize(mean=[0.5], std=[0.5])
            >>> create_pillow_transforms({
            ...     'type': 'normalize',
            ...     'mean': [0.485, 0.456, 0.406],
            ...     'std': [0.229, 0.224, 0.225],
            ... })
            PillowNormalize(mean=[0.485 0.456 0.406], std=[0.229 0.224 0.225])
            >>> create_pillow_transforms([
            ...     {'antialias': True,
            ...      'interpolation': 'bicubic',
            ...      'max_size': None,
            ...      'size': 384,
            ...      'type': 'resize'},
            ...     {'size': (224, 224), 'type': 'center_crop'},
            ...     {'type': 'maybe_to_tensor'},
            ...     {'mean': 0.5, 'std': 0.5, 'type': 'normalize'}
            ... ])
            PillowCompose(
                PillowResize(size=384, interpolation=bicubic, max_size=None, antialias=True)
                PillowCenterCrop(size=(224, 224))
                PillowMaybeToTensor()
                PillowNormalize(mean=[0.5], std=[0.5])
            )
        :::
        ::::
:::

::: {#parse-pillow-transforms .section}
## parse_pillow_transforms[](#parse-pillow-transforms "Permalink to this heading"){.headerlink}

[[imgutils.preprocess.pillow.]{.pre}]{.sig-prename .descclassname}[[parse_pillow_transforms]{.pre}]{.sig-name .descname}[(]{.sig-paren}*[[value]{.pre}]{.n}*[)]{.sig-paren}[[[\[source\]]{.pre}]{.viewcode-link}](../../_modules/imgutils/preprocess/pillow.html#parse_pillow_transforms){.reference .internal}[](#imgutils.preprocess.pillow.parse_pillow_transforms "Permalink to this definition"){.headerlink}

:   Parse transformations into a serializable format.

    Parameters[:]{.colon}

    :   **value** (*Any*) -- The transformation or composition to parse.

    Returns[:]{.colon}

    :   A serializable representation of the transformation.

    Return type[:]{.colon}

    :   Union\[list, dict\]

    Raises[:]{.colon}

    :   **TypeError** -- If the transformation cannot be parsed.

    Example[:]{.colon}

    :   :::: {.doctest .highlight-default .notranslate}
        ::: highlight
            >>> from PIL import Image
            >>>
            >>> from imgutils.preprocess import parse_pillow_transforms
            >>> from imgutils.preprocess.pillow import PillowResize, PillowCenterCrop, PillowMaybeToTensor, PillowToTensor,         ...     PillowNormalize
            >>>
            >>> parse_pillow_transforms(PillowResize(
            ...     size=384,
            ...     interpolation=Image.BICUBIC,
            ... ))
            {'type': 'resize', 'size': 384, 'interpolation': 'bicubic', 'max_size': None, 'antialias': True}
            >>> parse_pillow_transforms(PillowResize(
            ...     size=(224, 256),
            ...     interpolation=Image.BILINEAR,
            ... ))
            {'type': 'resize', 'size': (224, 256), 'interpolation': 'bilinear', 'max_size': None, 'antialias': True}
            >>> parse_pillow_transforms(PillowCenterCrop(size=224))
            {'type': 'center_crop', 'size': [224, 224]}
            >>> parse_pillow_transforms(PillowToTensor())
            {'type': 'to_tensor'}
            >>> parse_pillow_transforms(PillowMaybeToTensor())
            {'type': 'maybe_to_tensor'}
            >>> parse_pillow_transforms(PillowNormalize(mean=0.5, std=0.5))
            {'type': 'normalize', 'mean': [0.5], 'std': [0.5]}
            >>> parse_pillow_transforms(PillowNormalize(
            ...     mean=[0.485, 0.456, 0.406],
            ...     std=[0.229, 0.224, 0.225],
            ... ))
            {'type': 'normalize', 'mean': [0.48500001430511475, 0.4560000002384186, 0.4059999883174896], 'std': [0.2290000021457672, 0.2240000069141388, 0.22499999403953552]}
            >>> parse_pillow_transforms(PillowCompose([
            ...     PillowResize(
            ...         size=384,
            ...         interpolation=Image.BICUBIC,
            ...     ),
            ...     PillowCenterCrop(size=224),
            ...     PillowMaybeToTensor(),
            ...     PillowNormalize(mean=0.5, std=0.5),
            ... ]))
            [{'antialias': True,
              'interpolation': 'bicubic',
              'max_size': None,
              'size': 384,
              'type': 'resize'},
             {'size': [224, 224], 'type': 'center_crop'},
             {'type': 'maybe_to_tensor'},
             {'mean': [0.5], 'std': [0.5], 'type': 'normalize'}]
        :::
        ::::
:::
:::::::
::::::::
:::::::::
```

```
# src/utils/cropper.py
import os
from logging import getLogger

from PIL import Image
from imgutils.detect import detect_person, detection_visualize
from matplotlib import pyplot as plt

from configs.path_config import EXAMPLE_IMAGE, OUTPUT_DIR, IMAGES_DIR

logger_local = getLogger(__name__)


def rectangles_overlap(rect1, rect2):
    """
    2つの矩形が重なっているかを判定します。

    Args:
        rect1, rect2: (x1, y1, x2, y2) 形式の矩形座標

    Returns:
        bool: 重なっている場合True
    """
    x1, y1, x2, y2 = rect1
    x3, y3, x4, y4 = rect2

    # 重なり判定: 片方が他方の右側、左側、上側、下側にある場合は重ならない
    return not (x2 <= x3 or x4 <= x1 or y2 <= y3 or y4 <= y1)


def merge_overlapping_detections(detections):
    """
    重複している検出矩形をマージして、新しい検出として追加します。
    元のdetectionsは削除せず、マージされた検出を追加します。

    Args:
        detections (list): (bbox, label, score) のリスト

    Returns:
        list: 元の検出 + マージされた検出のリスト
    """
    if len(detections) <= 1:
        return detections

    # 各検出にインデックスを付与
    indexed_detections = list(enumerate(detections))

    # 重なっているグループを見つける
    groups = []
    used_indices = set()

    for i, (idx1, (bbox1, label1, score1)) in enumerate(indexed_detections):
        if idx1 in used_indices:
            continue

        # 新しいグループを開始
        current_group = [idx1]
        used_indices.add(idx1)

        # このグループに属する他の検出を探す
        group_changed = True
        while group_changed:
            group_changed = False
            for j, (idx2, (bbox2, label2, score2)) in enumerate(indexed_detections):
                if idx2 in used_indices:
                    continue

                # 現在のグループの任意のメンバーと重なっているかチェック
                for group_idx in current_group:
                    group_bbox = detections[group_idx][0]
                    if rectangles_overlap(group_bbox, bbox2):
                        current_group.append(idx2)
                        used_indices.add(idx2)
                        group_changed = True
                        break

        # グループが複数の検出を含む場合のみ追加
        if len(current_group) > 1:
            groups.append(current_group)

    # 元のdetectionsをコピーして開始
    result_detections = detections.copy()

    # 各グループについてマージされた検出を作成
    for group_indices in groups:
        # グループ内のすべての矩形を包含する最小矩形を計算
        min_x1 = float('inf')
        min_y1 = float('inf')
        max_x2 = float('-inf')
        max_y2 = float('-inf')

        total_score = 0
        labels = []

        for idx in group_indices:
            bbox, label, score = detections[idx]
            x1, y1, x2, y2 = bbox

            min_x1 = min(min_x1, x1)
            min_y1 = min(min_y1, y1)
            max_x2 = max(max_x2, x2)
            max_y2 = max(max_y2, y2)

            total_score += score
            labels.append(label)

        # マージされた検出を作成
        merged_bbox = (min_x1, min_y1, max_x2, max_y2)
        merged_label = f"merged_{len(group_indices)}_persons"  # マージされたことを示すラベル
        merged_score = total_score / len(group_indices)  # 平均スコア

        merged_detection = (merged_bbox, merged_label, merged_score)
        result_detections.append(merged_detection)

        logger_local.info(
            f"Merged {len(group_indices)} overlapping detections into one: "
            f"indices {group_indices}, bbox: {merged_bbox}, avg_score: {merged_score:.2f}"
        )

    return result_detections


def crop_and_save_persons(
        image_path, detections, output_dir='outputs/person_crops',
        min_length=0.3, min_area_ratio=None, merge_overlapping_areas=False,
):
    """
    画像から人物領域を切り出して保存します。
    短辺が指定した最低値を下回る場合、領域を拡大して保存します。

    Args:
        image_path (str): 元画像のパス
        detections (list): detect_person の戻り値 (矩形座標, ラベル, スコア) のリスト
        output_dir (str): 切り出し画像の保存先ディレクトリ
        min_length (int, optional): クロップする領域の短辺の最低値。
                                    指定された場合、短辺がこの値を下回らないように領域を拡大します。
                                    拡大時、画像境界からはみ出す場合は領域をずらして調整します。
                                    Defaults to None.
        min_area_ratio (float, optional): クロップ領域に占める検出された矩形領域の最小比率。
                                          指定された場合、この値を下回る場合はクロップをスキップします。
                                          Defaults to None.
        merge_overlapping_areas (bool, optional): 重複している検出矩形をマージするかどうか。
                                                  Trueの場合、重複している検出を1つにまとめた新しい検出を追加します。
                                                  元の検出は削除されません。
                                                  Defaults to False.
    """
    # 重複している検出をマージ（指定されている場合）
    if merge_overlapping_areas:
        logger_local.info("--- Merging overlapping detections ---")
        detections = merge_overlapping_detections(detections)
        logger_local.info(f"Total detections after merging: {len(detections)}")

    # 出力ディレクトリがなければ作成
    os.makedirs(output_dir, exist_ok=True)

    # 元画像を開く
    with Image.open(image_path) as img:
        img_w, img_h = img.size

        # min_lengthが元画像のサイズより大きい場合は処理不可能なので終了
        if min_length is not None and (min_length > img_w or min_length > img_h):
            logger_local.warning(
                f"min_length ({min_length}) is larger than image dimensions "
                f"({img_w}x{img_h}). Skipping cropping."
            )
            return

        for idx, (bbox, label, score) in enumerate(detections):
            logger_local.info(f"Detected: {label}, bbox: {bbox}, score: {score:.2f}")

            # 元の検出矩形の面積を計算
            orig_x1, orig_y1, orig_x2, orig_y2 = bbox
            orig_width = orig_x2 - orig_x1
            orig_height = orig_y2 - orig_y1
            orig_area = orig_width * orig_height

            crop_bbox = bbox
            # min_lengthが指定されている場合のみ拡大処理を検討
            if min_length is not None:
                x1, y1, x2, y2 = bbox
                w = x2 - x1
                h = y2 - y1

                # 矩形領域の短辺がmin_lengthを下回っている場合、領域を拡大
                if min(w, h) < min_length:
                    logger_local.info(f"  - Original bbox size: {w}x{h}. Expanding to min_length: {min_length}")
                    # 拡大後のサイズを計算
                    new_w = max(w, min_length)
                    new_h = max(h, min_length)

                    # 元のbboxの中心を計算
                    center_x = x1 + w / 2
                    center_y = y1 + h / 2

                    # 中心から拡大した新しい座標を計算
                    new_x1 = center_x - new_w / 2
                    new_y1 = center_y - new_h / 2
                    new_x2 = new_x1 + new_w
                    new_y2 = new_y1 + new_h

                    # 画像領域からはみ出す場合の調整（押し出し処理）
                    # 左へのはみ出し
                    if new_x1 < 0:
                        new_x2 -= new_x1  # new_x2を右にシフト
                        new_x1 = 0
                    # 上へのはみ出し
                    if new_y1 < 0:
                        new_y2 -= new_y1  # new_y2を下にシフト
                        new_y1 = 0

                    # 右へのはみ出し
                    if new_x2 > img_w:
                        new_x1 -= (new_x2 - img_w)  # new_x1を左にシフト
                        new_x2 = img_w
                    # 下へのはみ出し
                    if new_y2 > img_h:
                        new_y1 -= (new_y2 - img_h)  # new_y1を上にシフト
                        new_y2 = img_h

                    # 整数に変換して最終的なクロップ領域とする
                    crop_bbox = tuple(map(int, (new_x1, new_y1, new_x2, new_y2)))
                    logger_local.info(f"  - Adjusted crop_bbox: {crop_bbox}")

            # 最終的なクロップ領域の面積を計算
            final_x1, final_y1, final_x2, final_y2 = crop_bbox
            final_width = final_x2 - final_x1
            final_height = final_y2 - final_y1
            final_area = final_width * final_height

            # 検出矩形の面積比率を計算
            area_ratio = orig_area / final_area
            logger_local.info(f"  - Detection area ratio: {area_ratio:.4f} ({orig_area}/{final_area})")

            # min_area_ratioが指定されており、面積比率が閾値を下回る場合はスキップ
            if min_area_ratio is not None and area_ratio < min_area_ratio:
                logger_local.info(f"  - Skipping crop: area ratio {area_ratio:.4f} is below threshold {min_area_ratio}")
                continue

            # 最終的な領域でクロップ
            cropped = img.crop(crop_bbox)

            # 保存ファイル名例: person_0_0.85.jpg（0番目, スコア0.85）
            # マージされた検出の場合はラベルに"merged"が含まれる
            safe_label = label.replace(' ', '_')  # ファイル名用にスペースをアンダースコアに変更
            save_path = os.path.join(
                output_dir,
                f'{safe_label}_{idx}_{score:.2f}.png'
            )
            cropped.save(save_path)
            logger_local.info(f'Saved: {save_path}')


if __name__ == "__main__":
    from logging.config import dictConfig
    from configs.logging_config import LOGGING_CONFIG

    dictConfig(LOGGING_CONFIG)

    logger_local.info("\n--- Testing with merge_overlapping_areas=True ---")
    dets = detect_person(EXAMPLE_IMAGE)
    crop_and_save_persons(
        EXAMPLE_IMAGE,
        dets,
        OUTPUT_DIR / "detect_merge",
        min_length=1024,
        merge_overlapping_areas=True
    )

    # 結果を可視化
    visualized_image = detection_visualize(EXAMPLE_IMAGE, dets)

    # 画像を表示
    plt.imshow(visualized_image)
    plt.axis('off')
    plt.show()
```

`cropper.py`を`imgutils.preprocess.pillow`で改善できる？