```
# src/utils/cropper.py
import os

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


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.
    """
    # 出力ディレクトリがなければ作成
    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):
            print(
                f"Warning: 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):
            print(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:
                    print(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)))
                    print(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
            print(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:
                print(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）
            save_path = os.path.join(
                output_dir,
                f'person_{idx}_{score:.2f}.jpg'
            )
            cropped.save(save_path)
            print(f'Saved: {save_path}')


if __name__ == "__main__":
    # 元の挙動のテスト
    dets = detect_person(EXAMPLE_IMAGE)
    print("--- Testing original behavior ---")
    crop_and_save_persons(
        EXAMPLE_IMAGE,
        dets,
        OUTPUT_DIR / "detect_original",
        min_length=1024
    )

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

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

merge_overlapping_areasを追加して。
処理前に、detectionsの中に矩形領域が重なっているものがある場合、その重なっているものすべてを１つの検出領域にまとめてdetectionsの中に加える。この処理は汎用的なので別のメソッドに実装して、それを内部から呼んで。
detectionsはマージしてもdetectionsの中から取り除かない。つまり、この引数をTrueにしても出力される画像が減ることはない。