最近發現電腦裡有個資料夾裡面放了一些素材圖,裡面的圖片都沒分類,很雜很亂,但又有幾千張圖,一一人工識別又太耗時間,於是研究了使用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
若有業務合作需求,可寫信至: [email protected]
創業、網站經營相關內容未來將發布在 小易創業筆記