import gradio as gr from ultralytics import YOLO from PIL import Image, ImageDraw, ImageFont import numpy as np model = YOLO("model.pt") from PIL import Image, ImageDraw, ImageFont import numpy as np def predict(img, target_class, dot_color, text_color_choice, dot_size, font_size): path = img.split("\\")[-1].split(".")[0] print("path", path) results = model.predict(source=img, save=False, show_labels=False, show_conf=False) if not isinstance(results, (list, tuple)): results = [results] # 顏色設定 if dot_color == "紅色": dot_rgb = (255, 0, 0) else: dot_rgb = (57, 255, 20) # 螢光綠 if text_color_choice == "黑色": text_rgb = (0, 0, 0) else: text_rgb = (255, 255, 255) # 大小設定 try: r = int(dot_size) except: r = 10 if r < 2: r = 2 try: fsize = int(font_size) except: fsize = 36 if fsize < 8: fsize = 8 count = 0 annot_img = None annot_img_numbered = None # 轉換成 YOLO 類別索引 if target_class == "膠囊": class_idx = 0 else: class_idx = 1 for i in results: # 過濾出該類別的框 selected_boxes = [] if hasattr(i.boxes, "cls"): for b, c in zip(i.boxes.xyxy, i.boxes.cls): if int(c) == class_idx: selected_boxes.append(b) count = len(selected_boxes) img_pil = Image.fromarray(i.orig_img[..., ::-1]) img_pil_numbered = img_pil.copy() draw = ImageDraw.Draw(img_pil) draw_num = ImageDraw.Draw(img_pil_numbered) try: font = ImageFont.truetype("DejaVuSans.ttf", fsize) except: try: font = ImageFont.truetype("arial.ttf", fsize) except: font = ImageFont.load_default() for idx, box in enumerate(selected_boxes, start=1): x1, y1, x2, y2 = box[:4].tolist() cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 # 第一張:只有圓點 draw.ellipse((cx - r, cy - r, cx + r, cy + r), fill=dot_rgb, outline=None) # 第二張:圓點 + 編號 draw_num.ellipse((cx - r, cy - r, cx + r, cy + r), fill=dot_rgb, outline=None) # 最後一顆改紅字 num_color = (255, 0, 0) if idx == count else text_rgb draw_num.text((cx + r + 4, cy - r), str(idx), fill=num_color, font=font) annot_img = np.array(img_pil) annot_img_numbered = np.array(img_pil_numbered) return str(count), annot_img, annot_img_numbered # 自定義 CSS 樣式(背景改為白色、簡潔風格) custom_css = """ .gradio-container { background: #ffffff; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } /* 主標題 */ .main-header { text-align: center; color: #333333; font-size: 2.2em; font-weight: bold; margin-bottom: 5px; } /* 副標題 */ .sub-header { text-align: center; color: #555555; font-size: 1.1em; margin-bottom: 20px; font-weight: 300; } /* 卡片容器 */ .card-container { background: #f9f9f9; border-radius: 12px; padding: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); margin: 15px; } /* 輸入區 */ .input-section { background: #ffffff; border-radius: 10px; padding: 20px; margin-bottom: 15px; border: 1px solid #e0e0e0; } /* 輸出區 */ .output-section { background: #ffffff; border-radius: 10px; padding: 20px; border: 1px solid #e0e0e0; } /* 按鈕 */ .predict-btn { background: #4285f4 !important; border: none !important; border-radius: 20px !important; padding: 12px 32px !important; color: white !important; font-size: 16px !important; font-weight: 600 !important; transition: background 0.2s ease !important; cursor: pointer !important; } .predict-btn:hover { background: #3367d6 !important; } /* 圖片上傳 */ .image-upload { border: 2px dashed #cccccc !important; border-radius: 10px !important; background: #fcfcfc !important; padding: 15px !important; } .image-upload:hover { border-color: #bbbbbb !important; background: #f5f5f5 !important; } /* 數量文字 */ .count-output { font-size: 22px !important; font-weight: 600 !important; color: #222222 !important; text-align: center !important; background: #ffffff !important; border: 1px solid #e0e0e0 !important; border-radius: 8px !important; padding: 12px !important; } /* 結果圖 */ .result-image { border-radius: 8px !important; box-shadow: 0 6px 18px rgba(0,0,0,0.04) !important; border: 1px solid #e0e0e0 !important; } /* 響應式 */ @media (max-width: 768px) { .main-header { font-size: 1.8em; } .predict-btn { width: 100% !important; margin-top: 15px !important; } } """ with gr.Blocks( title="Pill Counter", theme=gr.themes.Soft( primary_hue="blue", secondary_hue="green", neutral_hue="gray", font=gr.themes.GoogleFont("Noto Sans TC") ), css=custom_css ) as demo: gr.HTML("""
Pill Counter
""") with gr.Row(): with gr.Column(scale=1): gr.HTML('

📤 上傳藥物圖片

') img = gr.Image( type="filepath", format=["jpg", "png"], height=450, width=800, label="請選擇或拖放圖片", elem_classes=["image-upload"] ) target_class = gr.Radio( choices=["膠囊", "錠劑"], value="膠囊", label="要計算的類別", info="選擇只計算錠劑或膠囊的總數" ) gr.HTML('
') button = gr.Button( "開始計算", variant="primary", elem_classes=["predict-btn"] ) gr.HTML('
支援 JPG/PNG,建議 640×640 以上
') gr.HTML('') dot_color = gr.Radio( choices=["紅色", "螢光綠"], value="紅色", label="圓點顏色", info="選擇藥錠中心圓點顏色" ) text_color_choice = gr.Radio( choices=["黑色", "白色"], value="黑色", label="文字顏色", info="選擇編號文字顏色" ) dot_size = gr.Textbox( label="圓點大小", value="10", info="設定圓點半徑大小(建議範圍 5~20)" ) font_size = gr.Textbox( label="字體大小", value="36", info="設定編號文字大小(建議範圍 20~60)" ) with gr.Column(scale=1): gr.HTML('

📊 檢測結果

') data_output = gr.Textbox( interactive=False, elem_classes=["count-output"] ) gr.HTML('
') img_output = gr.Image( type="numpy", elem_classes=["result-image"] ) gr.HTML('
') img_output_numbered = gr.Image( type="numpy", elem_classes=["result-image"] ) gr.HTML('') gr.HTML("""
使用步驟:
  1. 上傳清晰的圖片,保持純色背景(白色或黑色),勿有反光
  2. 點擊「開始計算」按鈕
  3. 查看標註後圖片及藥錠總數
""") button.click( fn=predict, inputs=[img, target_class, dot_color, text_color_choice, dot_size, font_size], outputs=[data_output, img_output, img_output_numbered] ) if __name__ == "__main__": demo.launch()