最近發現電腦裡有個資料夾裡面放了一些素材圖,裡面的圖片都沒分類,很雜很亂,但又有幾千張圖,一一人工識別又太耗時間,於是研究了使用AI模型來幫忙分類。


使用環境:

windows 11

python 3.13 (3.12也可以通,3.10~3.13理論上都能通)

顯卡:RTX 3060 12G


安裝ollama

https://ollama.com/

到官網 > Download > Windows版本 > 下載 > 安裝 > 啟動 ollama


安裝必要的包

pip install ollama tqdm json shutil 


需求&解說

我只要簡單分類出真實、動漫,是人物、場景、衣服、其他,是男生、女生。需求可以自行改變提示詞。

一開始我是使用gemma3:4b ,但這個識別能力,實測起來失誤率極高(大概只有1成),描述場景還行,但不知道為何識別反而有點弱。

換成12b可能好一些,但出於vram有限,顯卡算力也不足,使用12b太慢。於是改用qwen2.5vl:7b,成功率應該有9成。

gemma3:4b 大概一秒處理一張

qwen2.5vl:7b 大概兩秒處理一張


程式碼

import ollama
import os
import shutil
import json
from pathlib import Path
from tqdm import tqdm

# --- 1. 設定區 ---
SOURCE_FOLDER = Path(r"F:\project\來源目錄")
DESTINATION_FOLDER = Path(r"F:\project\已分類目錄")
OLLAMA_MODEL = 'qwen2.5vl:7b'  # 使用您下載的視覺模型gemma3:4b qwen2.5vl:7b
SUPPORTED_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.webp', '.bmp', '.gif')

# --- 2. 核心提示詞 (Prompt) ---
# 要求模型一次性回傳所有分類,並以 JSON 格式輸出
CLASSIFICATION_PROMPT = """
請仔細分析這張圖片,並嚴格按照以下 JSON 格式回傳三個屬性:`style`, `subject`, `gender`。

**判斷依據:**

1. **style (風格)**:
    - '真實': 具有照片特徵、真實人物、真實場景、3D 渲染風格。**重要:即使照片內容是 Cosplay、或經過美顏濾鏡處理,只要它本質上是一張照片,就應歸類為'真實'。**
    - '動漫': 手繪風格、2D 動畫風格、漫畫風格、插畫風格。**重要:必須是繪畫或非寫實的數位創作,而不是真人照片。**
   
2.  **subject (主體) - 請依循以下優先級判斷:
    - 人物: 只要畫面中出現清晰可辨的臉孔,且人物佔畫面整體超過三分之一,就應優先判斷為 '人物'。
   -  場景: 圖片主要展示風景、建築、室內外場景 (無明顯人物或人物很小)
    - 衣服: 只有在人物的臉孔被刻意忽略 (例如被裁切掉、模糊或背對鏡頭),或者圖片明顯是電子商務的商品平拍圖時,才判斷為 '衣服'。
   - '其他': 以上都不符合,例如動物、物品、抽象畫等
   
3. **gender (性別)** - 僅當 subject 是 '人物' 時判斷:
   - '女生': 從面部特徵、髮型、體型、服飾判斷為女性
   - '男生': 從面部特徵、髮型、體型、服飾判斷為男性
   - '其他': 無法判斷、多人且性別混合、或非人類角色
   - null: 當 subject 不是 '人物' 時

**重要規則:**
- 必須嚴格選擇指定的值,不要使用其他詞彙
- 如果不確定,請根據圖片中最主要的元素判斷
- 你的回覆只能包含 JSON 物件,不要有任何額外的文字說明

**這裡有兩個範例:**
1. 一張真人穿著動漫角色服裝的女生照片 -> {"style": "真實", "subject": "人物", "gender": "女生"}
2. 一張手繪的動漫女孩畫像 -> {"style": "動漫", "subject": "人物", "gender": "女生"}

**回傳格式:**
{"style": "(風格)", "subject": "(主體)", "gender": "(性別)"}
"""

# --- 3. 同義詞對應表 ---
# 將模型的可能回答,正規化為我們想要的資料夾名稱
SYNONYM_MAPPING = {
    # 風格
    "寫實": "真實", "照片": "真實",
    "動畫": "動漫", "二次元": "動漫",
    # 主體
    "角色": "人物",
    "風景": "場景", "建築": "場景",
    "服裝": "衣服", "穿搭": "衣服",
    # 性別
    "女性": "女生", "女人": "女生", "女孩": "女生",
    "男性": "男生", "男人": "男生", "男孩": "男生"
}


def get_image_files(folder_path):
    """取得資料夾中所有支援的圖片檔案路徑"""
    files = []
    for ext in SUPPORTED_EXTENSIONS:
        files.extend(folder_path.glob(f'*{ext}'))
    return files

def normalize_classification(category, value):
    """使用同義詞表來正規化分類結果"""
    if value is None:
        return None
    # 檢查值是否在同義詞表的 key 中,如果是,就回傳對應的 value
    return SYNONYM_MAPPING.get(value, value)

def ask_ollama_for_json(image_path):
    """
    向 Ollama 發送請求,並期望得到一個 JSON 回應
    """
    try:
        response = ollama.chat(
            model=OLLAMA_MODEL,
            messages=[
                {
                    'role': 'user',
                    'content': CLASSIFICATION_PROMPT,
                    'images': [str(image_path)]
                }
            ],
            options={
                "temperature": 0.1  # 降低溫度以提升分類準確度
            }
        )
        content = response['message']['content']
       
        # 清理模型可能回傳的 markdown 語法 ```json ... ```
        if content.startswith("```json"):
            content = content.strip("```json").strip("`").strip()

        # 解析 JSON
        return json.loads(content)
       
    except json.JSONDecodeError:
        print(f"\n[警告] 模型未回傳有效的 JSON。回應: {content}")
        return None
    except Exception as e:
        print(f"\n[錯誤] 與 Ollama 溝通時發生錯誤: {e}")
        return None

def main():
    """主執行函數"""
    print("--- Ollama 圖片分類器 v2 (高效版) ---")
   
    if not SOURCE_FOLDER.exists():
        print(f"[錯誤] 來源資料夾不存在: {SOURCE_FOLDER}")
        return
       
    DESTINATION_FOLDER.mkdir(exist_ok=True)
   
    image_files = get_image_files(SOURCE_FOLDER)
   
    if not image_files:
        print(f"在 {SOURCE_FOLDER} 中找不到任何支援的圖片檔案。")
        return
       
    print(f"找到 {len(image_files)} 張圖片,準備開始分類...")
   
    for image_path in tqdm(image_files, desc="整體進度"):
        class_data = ask_ollama_for_json(image_path)
        print(f"檔名: {image_path.name}")
        print(f"分類結果: {class_data}")
        if not class_data:
            target_dir = DESTINATION_FOLDER / "分類失敗"
            target_dir.mkdir(exist_ok=True)
            shutil.move(str(image_path), str(target_dir / image_path.name))
            continue
           
        # 從 JSON 中提取並正規化分類結果
        style = normalize_classification('style', class_data.get('style'))
        subject = normalize_classification('subject', class_data.get('subject'))
        gender = normalize_classification('gender', class_data.get('gender'))

        # 根據分類結果建立路徑
        path_parts = [style, subject]
        if subject == '人物' and gender:
            path_parts.append(gender)
           
        # 過濾掉 None 的值,並建立目標資料夾
        final_path_parts = [part for part in path_parts if part]
        if not final_path_parts:
            final_path_parts.append("未分類")

        target_dir = DESTINATION_FOLDER.joinpath(*final_path_parts)
        target_dir.mkdir(parents=True, exist_ok=True)
       
        # 移動檔案
        shutil.move(str(image_path), str(target_dir / image_path.name))

    print("\n--- 分類完成! ---")

if __name__ == "__main__":
    main()



一度還嘗試了 Janus-Pro-1B 但也是識別率超差,但它在描述圖片時又挺精準的,因此放棄它。它的部屬也相對的難

#janus pro預安裝(得先再虛擬環境) ,不能直接用pip安裝,pip的包不對
pip install git+https://github.com/huggingface/transformers.git
pip install git+https://github.com/deepseek-ai/Janus.git
#安裝pytorch2.6+cu121
pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu121


程式碼

import os
import shutil
import json
import torch
from PIL import Image
from pathlib import Path
from tqdm import tqdm

# --- 1. 核心函式庫導入 ---
# 因為 janus 已被正確安裝到虛擬環境中,所以可以直接導入
try:
    from transformers import AutoModelForCausalLM
    from janus.models import MultiModalityCausalLM, VLChatProcessor
    from janus.utils.io import load_pil_images
except ImportError as e:
    print("錯誤:無法導入必要的函式庫。")
    print("請確認您已在正確的虛擬環境中,並已從 GitHub 安裝 Janus 函式庫。")
    print(f"詳細錯誤: {e}")
    exit()


# --- 2. 設定區 ---
# 使用 Hugging Face Hub 的模型 ID
MODEL_ID = "deepseek-ai/Janus-Pro-1B" 

# 圖片來源與目標資料夾
SOURCE_FOLDER = Path(r"F:\project\來源目錄")
DESTINATION_FOLDER = Path(r"F:\project\已分類目錄")
SUPPORTED_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.webp', '.bmp', '.gif')

# 分類用的提示詞 (Prompt)
CLASSIFICATION_PROMPT = """
請仔細分析這張圖片,並嚴格按照以下 JSON 格式回傳三個屬性:`style`, `subject`, `gender`。

**判斷依據:**

1. **style (風格)**:
    - '真實': 具有照片特徵、真實人物、真實場景、3D 渲染風格。**重要:即使照片內容是 Cosplay、或經過美顏濾鏡處理,只要它本質上是一張照片,就應歸類為'真實'。**
    - '動漫': 手繪風格、2D 動畫風格、漫畫風格、插畫風格。**重要:必須是繪畫或非寫實的數位創作,而不是真人照片。**
   
2.  **subject (主體) - 請依循以下優先級判斷:
    - 人物: 只要畫面中出現清晰可辨的臉孔,且人物佔畫面整體超過三分之一,就應優先判斷為 '人物'。
   -  場景: 圖片主要展示風景、建築、室內外場景 (無明顯人物或人物很小)
    - 衣服: 只有在人物的臉孔被刻意忽略 (例如被裁切掉、模糊或背對鏡頭),或者圖片明顯是電子商務的商品平拍圖時,才判斷為 '衣服'。
   - '其他': 以上都不符合,例如動物、物品、抽象畫等
   
3. **gender (性別)** - 僅當 subject 是 '人物' 時判斷:
   - '女生': 從面部特徵、髮型、體型、服飾判斷為女性
   - '男生': 從面部特徵、髮型、體型、服飾判斷為男性
   - '其他': 無法判斷、多人且性別混合、或非人類角色
   - null: 當 subject 不是 '人物' 時

**重要規則:**
- 必須嚴格選擇指定的值,不要使用其他詞彙
- 如果不確定,請根據圖片中最主要的元素判斷
- 你的回覆只能包含 JSON 物件,不要有任何額外的文字說明

**這裡有兩個範例:**
1. 一張真人穿著動漫角色服裝的女生照片 -> {"style": "真實", "subject": "人物", "gender": "女生"}
2. 一張手繪的動漫女孩畫像 -> {"style": "動漫", "subject": "人物", "gender": "女生"}

**回傳格式:**
{"style": "(風格)", "subject": "(主體)", "gender": "(性別)"}
"""

# 同義詞對應表
SYNONYM_MAPPING = {
    "寫實": "真實", "照片": "真實",
    "動畫": "動漫", "二次元": "動漫", "动漫": "動漫",
    "角色": "人物",
    "風景": "場景", "建築": "場景",
    "服裝": "衣服", "穿搭": "衣服",
    "女性": "女生", "女人": "女生", "女孩": "女生",
    "男性": "男生", "男人": "男生", "男孩": "男生"
}

# --- 3. 核心功能函數 ---

def get_image_files(folder_path):
    """取得資料夾中所有支援的圖片檔案路徑"""
    files = []
    for ext in SUPPORTED_EXTENSIONS:
        files.extend(folder_path.glob(f'*{ext.lower()}'))
        files.extend(folder_path.glob(f'*{ext.upper()}'))
    # 去重
    return list(set(files))

def normalize_classification(value):
    """使用同義詞表來正規化分類結果"""
    if value is None:
        return None
    return SYNONYM_MAPPING.get(value, value)

def ask_janus_for_json(model, processor, image_path, question):
    """
    使用 Janus-Pro-1B 分析圖片,並回傳解析後的 JSON。
    這是被替換掉的核心「大腦」。
    """
    try:
        pil_image = Image.open(image_path).convert("RGB")
    except Exception as e:
        print(f"\n[錯誤] 無法開啟圖片 {image_path.name}: {e}")
        return None

    conversation = [
        {"role": "<|User|>", "content": f"<image_placeholder>\n{question}", "images": [str(image_path)]},
        {"role": "<|Assistant|>", "content": ""},
    ]

    pil_images = load_pil_images(conversation)
    prepare_inputs = processor(
        conversations=conversation, images=pil_images, force_batchify=True
    ).to(model.device)

    inputs_embeds = model.prepare_inputs_embeds(**prepare_inputs)

    outputs = model.language_model.generate(
        inputs_embeds=inputs_embeds,
        attention_mask=prepare_inputs.attention_mask,
        pad_token_id=processor.tokenizer.eos_token_id,
        bos_token_id=processor.tokenizer.bos_token_id,
        eos_token_id=processor.tokenizer.eos_token_id,
        max_new_tokens=128,
         do_sample=True, # <--- 改為 True
        temperature=0.1, # <--- 改為 0.1
        top_p=0.95, # <--- 增加 top_p 參數
        use_cache=True,
    )

    answer_text = processor.tokenizer.decode(outputs[0].cpu().tolist(), skip_special_tokens=True)
   
    try:
        if "```json" in answer_text:
            answer_text = answer_text.split("```json")[1].split("```")[0]
        answer_text = answer_text.strip()
        return json.loads(answer_text)
    except (json.JSONDecodeError, IndexError) as e:
        print(f"\n[警告] 無法解析來自模型的 JSON 回應。錯誤: {e}\n回應內容: '{answer_text}'")
        return None

def main():
    """主執行函數"""
    print("--- Janus-Pro 圖片分類器 (整合版) ---")
   
    if not SOURCE_FOLDER.exists():
        print(f"[錯誤] 來源資料夾不存在: {SOURCE_FOLDER}")
        return
       
    DESTINATION_FOLDER.mkdir(exist_ok=True)
   
    # --- 模型載入 (一次性) ---
    print(f"正在從 Hugging Face Hub 載入模型 '{MODEL_ID}'...")
    device = "cuda" if torch.cuda.is_available() else "cpu"
    dtype = torch.bfloat16 if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float16
   
    processor = VLChatProcessor.from_pretrained(MODEL_ID)
    model = AutoModelForCausalLM.from_pretrained(
        MODEL_ID, trust_remote_code=True,use_safetensors=True 
    ).to(dtype).to(device).eval()
    print(f"模型已成功載入到 {device}。")
    # --- 模型載入完成 ---

    image_files = get_image_files(SOURCE_FOLDER)
   
    if not image_files:
        print(f"在 {SOURCE_FOLDER} 中找不到任何支援的圖片檔案。")
        return
       
    print(f"找到 {len(image_files)} 張圖片,準備開始分類...")
   
    for image_path in tqdm(image_files, desc="整體進度"):
        # --- 呼叫新的分類函數 ---
        class_data = ask_janus_for_json(model, processor, image_path, CLASSIFICATION_PROMPT)
        # 增加印出檔名與分類結果
        print(f"檔名: {image_path.name}")
        print(f"分類結果: {class_data}")
        target_dir = DESTINATION_FOLDER / "分類失敗"

        if class_data and isinstance(class_data, dict):
            style = normalize_classification(class_data.get('style'))
            subject = normalize_classification(class_data.get('subject'))
            gender = normalize_classification(class_data.get('gender'))

            path_parts = [style, subject]
            if subject == '人物' and gender:
                path_parts.append(gender)
           
            final_path_parts = [part for part in path_parts if part]
            if final_path_parts:
                target_dir = DESTINATION_FOLDER.joinpath(*final_path_parts)
       
        try:
            target_dir.mkdir(parents=True, exist_ok=True)
            shutil.move(str(image_path), str(target_dir / image_path.name))
        except Exception as e:
            print(f"\n[錯誤] 移動檔案 {image_path.name} 失敗: {e}")

    print("\n--- 分類完成! ---")

if __name__ == "__main__":
    main()



文章轉載或引用,請先告知並保留原文出處與連結!!(單純分享或非營利的只需保留原文出處,不用告知)

原文連結:
https://blog.aidec.tw/post/python-ollama-image-category
若有業務合作需求,可寫信至: opweb666@gmail.com
創業、網站經營相關內容未來將發布在 小易創業筆記