最近發現電腦裡有個資料夾裡面放了一些素材圖,裡面的圖片都沒分類,很雜很亂,但又有幾千張圖,一一人工識別又太耗時間,於是研究了使用AI模型來幫忙分類。
使用環境:
windows 11
python 3.13 (3.12也可以通,3.10~3.13理論上都能通)
顯卡:RTX 3060 12G
安裝ollama
到官網 > 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
創業、網站經營相關內容未來將發布在 小易創業筆記