```
# clipcrop.py
from logging import getLogger

import cv2
import numpy as np
import torch
from PIL import Image
from ultralytics import YOLO

from CLIP import clip
from configs.path_config import EXAMPLE_IMAGE, OUTPUT_DIR, YOLO_MODELS_DIR

logger_local = getLogger(__name__)


def clip_boxes(boxes, shape):
    """
    画像サイズ (高さ, 幅) に合わせて、ボックスの座標 (x1, y1, x2, y2) をクリップします。
    """
    if isinstance(boxes, torch.Tensor):
        boxes[:, 0].clamp_(0, shape[1])
        boxes[:, 1].clamp_(0, shape[0])
        boxes[:, 2].clamp_(0, shape[1])
        boxes[:, 3].clamp_(0, shape[0])
    else:
        boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, shape[1])
        boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, shape[0])


def find_position(parent: Image.Image, child: Image.Image):
    """
    cv2.matchTemplate を用いて、parent 画像内から child の位置を求め、
    child の中心座標 (center_x, center_y) を返します。
    """
    w = child.width
    h = child.height
    res = cv2.matchTemplate(np.array(parent), np.array(child), cv2.TM_CCOEFF_NORMED)
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
    top_left = max_loc
    center_x = top_left[0] + (w / 2)
    center_y = top_left[1] + (h / 2)
    return center_x, center_y


class CropClip:
    def __init__(self):
        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.model = None
        self.clip = None
        self.preprocess = None

    def __enter__(self):
        """
        コンテキストマネージャの開始時に呼ばれる。
        モデル等をデバイスにロードする。
        """
        self._load()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """
        コンテキストマネージャの終了時に呼ばれる。
        モデル等のリソースを解放する。
        """
        self._unload()

    def get_center(self, image: Image.Image, prompt: str):
        """
        画像とテキストプロンプトに基づき、クロップすべき領域を [x1, x2, y1, y2] 座標で返します。
        """
        # ultralytics の YOLO モデルでオブジェクト検出
        results = self.model(image)
        crops = []
        crop_boxes = []
        for result in results:
            if result.boxes is not None:
                for box in result.boxes:
                    x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
                    logger_local.info(f"YOLO検出: x1={x1}, y1={y1}, x2={x2}, y2={y2}")
                    crop = image.crop((x1, y1, x2, y2))
                    crops.append(crop)
                    crop_boxes.append((x1, y1, x2, y2))
        if len(crops) == 0:
            crops = [image]
            crop_boxes = [(0, 0, image.width, image.height)]

        # CLIP モデルの lazy ロード
        if self.clip is None or self.preprocess is None:
            self.clip, self.preprocess = clip.load("ViT-B/32", device=self.device)

        # 各クロップ候補を前処理し、画像特徴量を計算
        images_tensor = torch.stack([self.preprocess(im) for im in crops]).to(self.device)
        with torch.no_grad():
            image_features = self.clip.encode_image(images_tensor)
            image_features /= image_features.norm(dim=-1, keepdim=True)

        # プロンプトをエンコードしてテキスト特徴量を取得
        with torch.no_grad():
            text_inputs = clip.tokenize(prompt).to(self.device)
            text_features = self.clip.encode_text(text_inputs)
            text_features /= text_features.norm(dim=-1, keepdim=True)

        # テキストと画像の類似度を計算
        similarity = (text_features.cpu().numpy() @ image_features.cpu().numpy().T)[0]
        logger_local.info(f"CLIP類似度: {similarity}")

        # 類似度最大のインデックスを取得
        best_idx = np.argmax(similarity)
        out_img = crops[best_idx]
        out_idx = best_idx
        logger_local.info(f"採用crop index: {out_idx}, crop_box={crop_boxes[out_idx]}")

        # find_positionを使ってテンプレートマッチングによる位置を取得
        center_x, center_y = find_position(image, out_img)
        left = int(center_x - out_img.width / 2)
        top = int(center_y - out_img.height / 2)
        right = left + out_img.width
        bottom = top + out_img.height
        logger_local.info(
            f"テンプレートマッチング(find_position): left={left}, top={top}, right={right}, bottom={bottom}")

        return [left, top, right, bottom]

    def _load(self, device=None):
        """
        指定した device にモデルを再配置します。
        device が指定されなければ、自動判定します。
        """
        if device is None:
            device = self.device
        # YOLOロード
        self.model = YOLO(YOLO_MODELS_DIR / 'yolov5su.pt')
        self.model.to(device)
        # CLIPロード
        self.clip, self.preprocess = clip.load("ViT-B/32", device=device)

    def _unload(self):
        """
        モデルなどのリソースを CPU に戻して解放します。
        """
        if self.model is not None:
            self.model.to('cpu')
        if self.clip is not None:
            self.clip.to('cpu')
        torch.cuda.empty_cache()


# サンプル実行例
if __name__ == "__main__":
    from logging.config import dictConfig
    from configs.logging_config import LOGGING_CONFIG

    dictConfig(LOGGING_CONFIG)

    img = Image.open(EXAMPLE_IMAGE)
    image_name = OUTPUT_DIR / "output_test.png"
    prompt = "a girl"  # クロップしたい対象のプロンプトを設定

    with CropClip() as cropclip:
        crop_box = cropclip.get_center(img, prompt)
        left, top, right, bottom = crop_box
        logger_local.info(f"Cropping with: left={left}, top={top}, right={right}, bottom={bottom}")
        cropped_img = img.crop((left, top, right, bottom))
        cropped_img.save(image_name)
```

CripClopがインスタンス化時にmin_lengthとmax_lengthを受け取るようにして、そのアスペクト比を超えないようにしながら、関心領域の全体を含むようにクロップして。通常、min_length=1024, max_length=1344。さらに必要ならmin_lengthとmax_lengthを満たすように画像を縮小を満たすようにリサイズしてから返すようにして。 これで、クラスを利用する側はほとんどなにも追加の処理が必要なくなる。できる？