```
# clipcrop.py
import os
import sys
from logging import getLogger

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

from CLIP import clip
from configs.path_config import EXAMPLE_IMAGE, OUTPUT_DIR, YOLO_MODELS_DIR
from ultralytics import YOLO  # ← 追加

logger_local = getLogger(__name__)


def download_model(model_url: str, model_name: str, model_dir: str) -> str:
    """
    指定した URL からモデルをダウンロードし、model_dir 内に保存する。
    既に存在する場合はそのパスを返す。
    """
    os.makedirs(model_dir, exist_ok=True)
    model_path = os.path.join(model_dir, model_name)
    if not os.path.exists(model_path):
        logger_local.info(f"Downloading model from {model_url} to {model_path}...")
        try:
            import urllib.request
            urllib.request.urlretrieve(model_url, model_path)
        except Exception:
            logger_local.error(f"Error downloading model.", exc_info=True)
            raise
    return model_path


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):
        # モデル設定 : YOLOv5m6 (v7) を使用
        self.model_name = 'yolov5m6v7.pt'
        self.model_url = 'https://github.com/ultralytics/yolov5/releases/download/v7.0/yolov5m6.pt'
        model_dir = YOLO_MODELS_DIR
        model_path = download_model(self.model_url, self.model_name, model_dir)

        self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        # ultralytics パッケージの YOLO クラスでモデルをロード
        self.model = YOLO(model_path)
        self.model.to(self.device)

        # CLIP は lazy ロード (必要時にロード)
        self.clip = None
        self.preprocess = None

    def get_center(self, image: Image.Image, prompt: str):
        """
        画像とテキストプロンプトに基づき、クロップすべき領域を [x1, x2, y1, y2] 座標で返します。
        1. YOLO により複数候補領域を抽出し、各領域で CLIP による類似度を計算。
        2. 類似度上位 3 件のうち、面積が最も大きい領域を採用。
        3. テンプレートマッチングにより、元画像上でその領域の位置を検出します。
        """
        # ultralytics の YOLO モデルでオブジェクト検出
        results = self.model(image)
        cropped = results.crop(save=False)
        crops = []
        for crop_info in cropped:
            crops.append(Image.fromarray(crop_info["im"]))
        if len(crops) == 0:
            crops = [image]

        # 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]
        # 類似度上位 3 件を選択
        indices = np.argsort(-similarity)[:3]
        max_area = 0
        out_img = None
        for idx in indices:
            im = crops[idx]
            area = im.width * im.height
            if area > max_area:
                max_area = area
                out_img = im
        if out_img is None:
            out_img = image

        # テンプレートマッチングにより、元画像内での領域位置を決定
        res = cv2.matchTemplate(np.array(image), np.array(out_img), cv2.TM_SQDIFF)
        min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
        top_left = min_loc  # TM_SQDIFF の場合は lower 値が最適
        bottom_right = (top_left[0] + out_img.width, top_left[1] + out_img.height)
        return [top_left[0], bottom_right[0], top_left[1], bottom_right[1]]

    def unload(self):
        """
        モデルなどのリソースを CPU に戻して解放します。
        """
        if self.model is not None:
            self.model.to('cpu')
        if self.clip is not None:
            self.clip.to('cpu')
        # preprocess は関数なので特に処理は不要です

    def load(self, device=None):
        """
        指定した device にモデルを再配置します。
        device が指定されなければ、自動判定します。
        """
        if device is None:
            device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        if self.model is not None:
            self.model.to(device)
        if self.clip is not None:
            self.clip.to(device)


# サンプル実行例
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 book"  # クロップしたい対象のプロンプトを設定

    cropclip = CropClip()
    try:
        crop_box = cropclip.get_center(img, prompt)
        # get_center() の返り値は [x1, x2, y1, y2] 形式なので、PIL の crop() 用に (left, top, right, bottom) に変換
        left, right, top, bottom = crop_box
        cropped_img = img.crop((left, top, right, bottom))
        cropped_img.save(image_name)
        logger_local.info(f"Cropped image saved to {image_name}")
    finally:
        cropclip.unload()
```

```
C:\Users\faker\Desktop\lora_test\.venv\Scripts\python.exe C:\Users\faker\Desktop\lora_test\src\utils\clipcrop.py 
2025-06-14 12:07:05,074 __main__:26 download_model [INFO]: Downloading model from https://github.com/ultralytics/yolov5/releases/download/v7.0/yolov5m6.pt to C:\Users\faker\Desktop\lora_test\resource\models\yolov5m6v7.pt...
Traceback (most recent call last):
  File "C:\Users\faker\Desktop\lora_test\src\utils\clipcrop.py", line 170, in <module>
    cropclip = CropClip()
               ^^^^^^^^^^
  File "C:\Users\faker\Desktop\lora_test\src\utils\clipcrop.py", line 75, in __init__
    self.model = YOLO(model_path)
                 ^^^^^^^^^^^^^^^^
  File "C:\Users\faker\Desktop\lora_test\.venv\Lib\site-packages\ultralytics\models\yolo\model.py", line 79, in __init__
    super().__init__(model=model, task=task, verbose=verbose)
  File "C:\Users\faker\Desktop\lora_test\.venv\Lib\site-packages\ultralytics\engine\model.py", line 151, in __init__
    self._load(model, task=task)
  File "C:\Users\faker\Desktop\lora_test\.venv\Lib\site-packages\ultralytics\engine\model.py", line 295, in _load
    self.model, self.ckpt = attempt_load_one_weight(weights)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\faker\Desktop\lora_test\.venv\Lib\site-packages\ultralytics\nn\tasks.py", line 1549, in attempt_load_one_weight
    ckpt, weight = torch_safe_load(weight)  # load ckpt
                   ^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\faker\Desktop\lora_test\.venv\Lib\site-packages\ultralytics\nn\tasks.py", line 1447, in torch_safe_load
    ckpt = torch.load(file, map_location="cpu")
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\faker\Desktop\lora_test\.venv\Lib\site-packages\ultralytics\utils\patches.py", line 117, in torch_load
    return _torch_load(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\faker\Desktop\lora_test\.venv\Lib\site-packages\torch\serialization.py", line 1525, in load
    return _load(
           ^^^^^^
  File "C:\Users\faker\Desktop\lora_test\.venv\Lib\site-packages\torch\serialization.py", line 2114, in _load
    result = unpickler.load()
             ^^^^^^^^^^^^^^^^
  File "C:\Users\faker\Desktop\lora_test\.venv\Lib\site-packages\torch\serialization.py", line 2103, in find_class
    return super().find_class(mod_name, name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: Can't get attribute 'Model' on <module 'models.yolo' (namespace) from ['C:\\Users\\faker\\Desktop\\lora_test\\src\\utils\\models\\yolo']>

Process finished with exit code 1
```

日本語で説明して。