import os import time import cv2 import numpy as np from ultralytics import YOLO from pathlib import Path from PIL import Image, ImageDraw, ImageFont from core.accel import resolve_yolo_weights # ==================== 【配置区域】 ==================== _ROOT = Path(__file__).resolve().parents[1] class Config: # 你的Plate检测模型路径(相对项目根目录);若存在 best_openvino_model 会自动优先加载 MODEL_PATH = str(_ROOT / "best.pt") CONF_THRESHOLD = 0.90 IOU_THRESHOLD = 0.45 # 显式 imgsz,便于 OpenVINO / CPU 控制耗时;可用环境变量 DISH_YOLO_IMGSZ 覆盖 IMGSZ = int(os.environ.get("DISH_YOLO_IMGSZ", "640")) BOX_THICKNESS = 2 FONT_SIZE = 22 # 输出目录 OUTPUT_DIR = r"detect" Cutting = r"cutting" GENERATE_VISUALIZATION = True # 是否生成可视化图 # 只有 Plate 一个类别 LABEL_NAMES = {0: "Plate"} COLORS = {'Plate': (0, 255, 0)} # ======================================================== class YOLOv11PlateExtractor: def __init__(self, config): self.config = config weights = resolve_yolo_weights(config.MODEL_PATH) self.model = YOLO(weights) self.weights_path = weights print(f"✅ Plate 模型加载成功: {weights}") def predict(self, source): """source 可为图片路径或 BGR ndarray。""" return self.model.predict( source=source, conf=self.config.CONF_THRESHOLD, iou=self.config.IOU_THRESHOLD, imgsz=getattr(self.config, "IMGSZ", 640), verbose=False, )[0] def crop_plate(self, image, results): cropped_regions = [] if results.boxes is not None: boxes = results.boxes.xyxy.cpu().numpy() confs = results.boxes.conf.cpu().numpy() h, w = image.shape[:2] for box, conf in zip(boxes, confs): x1, y1, x2, y2 = map(int, box) x1 = max(0, x1) y1 = max(0, y1) x2 = min(w, x2) y2 = min(h, y2) if x2 > x1 and y2 > y1: crop = image[y1:y2, x1:x2] if crop.size > 0: cropped_regions.append({ "image": crop.copy(), "conf": float(conf), "box": [x1, y1, x2, y2], }) return cropped_regions # 标注默认:大库红框 + 白字;今日菜单由 style 覆盖为绿框绿字 LABEL_BOX_COLOR = (229, 57, 53) LABEL_CHIP_BG = (229, 57, 53, 220) LABEL_CHIP_TEXT = (255, 255, 255, 255) def _load_font(self, size=None): size = size or self.config.FONT_SIZE for font_name in ("simhei.ttf", "simsun.ttc", "msyh.ttc", "msyh.ttf"): try: return ImageFont.truetype(font_name, size) except Exception: continue return ImageFont.load_default() def _draw_box_and_chip(self, base_img, box, text, font, style=None): """描边框 + 标签芯片;style 可覆盖颜色(今日菜单绿 / 大库红 / 未知橙)。""" style = style or {} outline = style.get("box") or (229, 57, 53, 180) chip_bg = style.get("chip_bg") or self.LABEL_CHIP_BG chip_fg = style.get("chip_fg") or self.LABEL_CHIP_TEXT thickness = int(style.get("box_width") or self.config.BOX_THICKNESS or 2) x1, y1, x2, y2 = map(int, box) rgba = base_img.convert("RGBA") overlay = Image.new("RGBA", rgba.size, (0, 0, 0, 0)) odraw = ImageDraw.Draw(overlay) for t in range(max(1, thickness)): odraw.rectangle( [(x1 - t, y1 - t), (x2 + t, y2 + t)], outline=outline, ) try: bbox = font.getbbox(str(text)) tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] except Exception: tw, th = len(str(text)) * 10, 16 pad_x, pad_y = 8, 4 chip_h = th + pad_y * 2 chip_w = max(tw + pad_x * 2, chip_h) chip_y = y1 - chip_h - 4 if y1 - chip_h - 4 > 0 else y1 + 4 chip_x = x1 # 圆形编号更贴近参考图 if str(text).isdigit() and len(str(text)) <= 2: r = max(chip_h, 22) // 2 + 2 cx, cy = x1 + r + 2, max(y1 - r - 4, r + 2) odraw.ellipse([(cx - r, cy - r), (cx + r, cy + r)], fill=chip_bg) odraw.text((cx, cy), str(text), fill=chip_fg, font=font, anchor="mm") else: odraw.rectangle( [(chip_x, chip_y), (chip_x + chip_w, chip_y + chip_h)], fill=chip_bg, ) odraw.text((chip_x + pad_x, chip_y + pad_y), str(text), fill=chip_fg, font=font) return Image.alpha_composite(rgba, overlay).convert("RGB") def visualize_results(self, image, results, save_path, labels=None): """ 在检测框上绘制标签。 labels 为 None 时画 Plate+置信度;传入列表时画菜品名称(认菜不认盘展示)。 """ img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) font = self._load_font() if results.boxes is not None: boxes = results.boxes.xyxy.cpu().numpy() confs = results.boxes.conf.cpu().numpy() for i, (box, conf) in enumerate(zip(boxes, confs)): if labels is not None and i < len(labels): text = str(labels[i]) else: text = f"Plate {conf:.2f}" img = self._draw_box_and_chip(img, box, text, font) os.makedirs(os.path.dirname(save_path) or ".", exist_ok=True) img.save(save_path) return save_path def visualize_with_labels(self, image_or_path, boxes, labels, save_path, styles=None): """ 在原图上按框绘制菜品名称(不依赖 YOLO results)。 :param image_or_path: BGR ndarray 或图片路径 :param boxes: [[x1,y1,x2,y2], ...] :param labels: 与 boxes 对齐的菜品名称列表 :param styles: 可选,与 boxes 对齐的颜色样式 dict 列表 """ if isinstance(image_or_path, str): image = cv2.imread(image_or_path) if image is None: raise ValueError(f"无法读取图片: {image_or_path}") else: image = image_or_path img = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) font = self._load_font() styles = styles or [] for i, (box, label) in enumerate(zip(boxes, labels)): style = styles[i] if i < len(styles) else None img = self._draw_box_and_chip(img, box, str(label), font, style=style) os.makedirs(os.path.dirname(save_path) or ".", exist_ok=True) img.save(save_path) return save_path def _load_bgr(self, image_or_path): if isinstance(image_or_path, np.ndarray): if image_or_path.size == 0: return None return image_or_path if isinstance(image_or_path, (str, Path)): image = cv2.imread(str(image_or_path)) return image return None def process_image( self, image_or_path, stem=None, save_crops=True, visualize=None, ): """ 统一检测入口:支持路径或 BGR ndarray。 :return: (visualize_save_path, crop_items, boxes) crop_items: [{"image": ndarray, "path": str|None, "conf": float}, ...] """ image = self._load_bgr(image_or_path) if image is None: print(f"❌ 读取失败: {image_or_path}") return None, [], [] if stem is None: if isinstance(image_or_path, (str, Path)): stem = Path(image_or_path).stem else: stem = f"frame_{int(time.time() * 1000)}" # 内存直传,避免 YOLO 再读盘 results = self.predict(image) cropped_regions = self.crop_plate(image, results) do_viz = self.config.GENERATE_VISUALIZATION if visualize is None else bool(visualize) visualize_save_path = None if do_viz: os.makedirs(self.config.OUTPUT_DIR, exist_ok=True) visualize_save_path = self.visualize_results( image, results, os.path.join(self.config.OUTPUT_DIR, f"{stem}.jpg"), ) crop_items = [] boxes = [] if save_crops: os.makedirs(self.config.Cutting, exist_ok=True) for i, region in enumerate(cropped_regions): img_crop = region["image"] path = None if save_crops: filename = f"{stem}_Plate_{i + 1}.jpeg" path = os.path.join(self.config.Cutting, filename) cv2.imwrite(path, img_crop) crop_items.append({ "image": img_crop, "path": path, "conf": region.get("conf"), }) boxes.append(region["box"]) print(f"✅ {stem} → 裁剪完成,共 {len(cropped_regions)} 张") return visualize_save_path, crop_items, boxes def process_single_image(self, image_path): """ 处理单张图片:检测餐盘、裁剪、生成初始可视化图。 :return: (visualize_save_path, cropped_paths, boxes) boxes 与 cropped_paths 一一对应,供后续标注菜品名 """ viz, crop_items, boxes = self.process_image( image_path, save_crops=True, visualize=True, ) cropped_paths = [c["path"] for c in crop_items if c.get("path")] return viz, cropped_paths, boxes def main(): cfg = Config() app = YOLOv11PlateExtractor(cfg) img_path = r"C:\Users\王含波\Desktop\文件\classfiy\20260422095806.jpg" visualize_save_path, cropped_file_paths, boxes = app.process_single_image(img_path) print("\n📦 裁剪后的图片路径:") print(cropped_file_paths) print("boxes:", boxes) print("viz:", visualize_save_path) if __name__ == "__main__": main()