File size: 13,076 Bytes
53ccd32 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | # -*- coding: utf-8 -*-
"""
使用方法:
python rule_19_vllm.py \
--input_dir "/path/to/your/images" \
--model_path "/path/to/your/Qwen2.5-VL-7B-Instruct"
"""
import os
import re
import json
import argparse
import multiprocessing
from pathlib import Path
from typing import Dict, List, Optional, Any
from tqdm import tqdm
from PIL import Image, ImageFile
from transformers import AutoProcessor
from vllm import LLM, SamplingParams
os.environ['VLLM_WORKER_MULTIPROC_METHOD'] = 'spawn'
ImageFile.LOAD_TRUNCATED_IMAGES = True
# 支持的图片格式
IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
# ==========================================
# 【提示词工程】
# ==========================================
SYS_PROMPT_TEXT ="""You are a highly critical Senior Art Director. Your job is to flag "Low-Quality / Amateur" advertising designs.
You have ZERO TOLERANCE for "Cheap Ad Styles" (often called "Niu Pi Xian" in Chinese context).
INPUT: One image and one natural-language question about design aesthetic and text harmony.
YOUR TASK:
1. Determine if the image is a **VIOLATION** (Unsuitable) or **SAFE** (Suitable) based on the criteria below.
2. Output a JSON object containing a rigorous Chain-of-Thought ("think") and a precise classification label ("answer").
OUTPUT FORMAT:
Return EXACTLY two blocks, no extra text:
<think>Detailed reasoning evaluating font effects, background integration, and aesthetic consistency against the 'cheap design' criteria...</think><answer>{"Answer": "<Suitable OR Unsuitable>", "Answer type": "Text-Design Harmony"}</answer>
=========================================
STRICT VIOLATION CRITERIA (If ANY match -> Unsuitable)
=========================================
1. **The "WordArt" Effect (廉价特效):**
- **Bad Strokes:** Text uses heavy, amateurish strokes (thick white/colored outlines) that look jagged or pixelated.
- **Fake 3D/Metal:** Outdated "Pseudo-3D" gradients (e.g., shiny gold/silver metal textures) that clash with a flat background.
- **Cheap Glow:** Aggressive "Outer Glow" (neon glow) that makes the text look blurry or radioactive.
- **Distortion:** Text is unprofessionally stretched, squeezed, or distorted strictly to fit a space.
2. **Visual Clutter & Conflict (背景冲突与拼贴感):**
- **Legibility Loss:** Text is placed directly on top of a "Busy Photograph" (leaves, city streets, crowds) without a sufficient background mask, making it hard to read.
- **Color Vibration:** Text color aggressively vibrates against the background (e.g., bright red text directly on bright green).
- **Patchwork Style:** The text background looks like a "sticker" arbitrarily pasted onto a photo, completely ignoring the photo's lighting and perspective.
3. **Inconsistent Aesthetic (风格割裂):**
- Foreground graphic elements (e.g., a cartoon/gaming style "Button" or "Banner") are superimposed on a realistic, high-res nature/human photograph. They do not belong in the same visual world.
=========================================
CRITERIA FOR 'SUITABLE' (NON-VIOLATION / GOOD DESIGN)
=========================================
1. **Clean Professionalism:** Professional typography with no cheap text effects (e.g., simple text like "xx折扣" is perfectly fine if the font is clean).
2. **Proper Integration:** Text placed on a solid, clean color background, or properly masked on a complex background.
3. **Cohesive Art Direction:** Clean, flat vector art that matches its surroundings visually.
=========================================
DECISION LOGIC
=========================================
- **Unsuitable**: If the design looks cheap, messy, outdated, features "WordArt" effects, or feels like a patched-together "Niu Pi Xian" ad.
- **Suitable**: If the design is clean, professional, and visually harmonious.
"""
SYS_PROMPT_TEXT="""You are a highly critical "Senior Art Director." Your goal is to evaluate the "Professional Polish" of splash ads. You have [ZERO TOLERANCE] for raw, unprocessed photos that look like amateur snapshots.
INPUT:
One image and one natural-language question about professional polish quality.
YOUR TASK:
1. Determine if the image is a **VIOLATION** (Unsuitable) or **SAFE** (Suitable) based on the criteria below.
2. Output a JSON object containing a rigorous Chain-of-Thought ("think") and a precise classification label ("answer").
OUTPUT FORMAT:
Return EXACTLY two blocks, no extra text:
<think>
Detailed reasoning evaluating lighting, color grading, and depth of field against the "passerby snapshot" criteria...
</think>
<answer>
{"Answer": "<Suitable OR Unsuitable>", "Answer type": "Professional Polish"}
</answer>
=========================================
STRICT VIOLATION CRITERIA (If ANY match -> Unsuitable)
=========================================
1. **Lack of Professional Post-Processing:**
- The image appears to be a "Raw Photo" directly from a camera/phone without professional retouching.
2. **The "Amateur Snapshot" Aesthetic:**
- The image looks like something a "passerby" could easily capture. It lacks the sophisticated framing, high-end texture, and artistic polish required for premium advertising.
3. **Absence of Value Conveyance:**
- The image is visually "flat" and fails to evoke a sense of high quality. It does not use professional polish techniques to guide the viewer's emotions.
=========================================
CRITERIA FOR 'SUITABLE' (NON-VIOLATION / PREMIUM TEXTURE)
=========================================
1. **Professional Post-Processing:**
- **Masterful Retouching:** The image shows clear evidence of professional professional polish (not straight-out-of-camera).
2. **The "High-End" Aesthetic:**
- **Professionalism:** The image features a look that cannot be easily replicated by a passerby.
- **Superior Texture:** Displays deliberate set design and artistic polish.
4. **Media Exemption:** Film stills or variety show photography are always classified as SAFE (Suitable).
=========================================
DECISION LOGIC
=========================================
- **Unsuitable**: If the image looks like an unprocessed, amateur snapshot with flat lighting and no professional polish polish.
- **Suitable**: If the image shows professional professional polish, or is a professional film/variety show still.
"""
def collect_images(input_dir: Path) -> List[Dict[str, str]]:
if not input_dir.exists():
raise FileNotFoundError(f"Input directory not found: {input_dir}")
files = [p for p in input_dir.iterdir() if p.is_file() and p.suffix.lower() in IMG_EXTS]
files.sort()
print(f"[Info] Found {len(files)} images in {input_dir}")
return [{"path": str(p), "filename": p.name} for p in files]
def parse_llm_output(text: str) -> Dict[str, Any]:
default_res = {
"label": "Parse Error",
"think": "No reasoning found",
"raw": text
}
if not text:
return default_res
think_match = re.search(r'<think>(.*?)</think>', text, re.DOTALL)
think_content = think_match.group(1).strip() if think_match else ""
answer_match = re.search(r'<answer>(.*?)</answer>', text, re.DOTALL)
extracted_label = "Parse Error"
if answer_match:
json_str = answer_match.group(1).strip()
try:
data = json.loads(json_str)
raw_ans = data.get("Answer", "")
if "unsuitable" in raw_ans.lower():
extracted_label = "Unsuitable"
elif "suitable" in raw_ans.lower():
extracted_label = "Suitable"
else:
extracted_label = raw_ans
except json.JSONDecodeError:
if "Unsuitable" in json_str:
extracted_label = "Unsuitable"
elif "Suitable" in json_str:
extracted_label = "Suitable"
else:
if "Unsuitable" in text:
extracted_label = "Unsuitable"
elif "Suitable" in text:
extracted_label = "Suitable"
return {
"label": extracted_label,
"think": think_content,
"raw": text
}
def prepare_vllm_inputs(batch_meta: List[Dict], processor) -> List[Dict]:
vllm_inputs = []
user_query = "Analyze this image against the design rules and return the JSON decision."
for item in batch_meta:
img_path = item["path"]
try:
image_obj = Image.open(img_path).convert("RGB")
messages = [
{"role": "system", "content": [{"type": "text", "text": SYS_PROMPT_TEXT}]},
{"role": "user", "content": [
{"type": "image", "image": img_path},
{"type": "text", "text": user_query}
]}
]
prompt_text = processor.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
vllm_inputs.append({
"prompt": prompt_text,
"multi_modal_data": {"image": image_obj}
})
except Exception as e:
print(f"[Warning] Failed to load {img_path}: {e}")
vllm_inputs.append(None)
return vllm_inputs
def main():
parser = argparse.ArgumentParser(description="AI Visual Comfort Auditor")
parser.add_argument("--input_dir", type=str, required=True, help="Folder containing images to check")
parser.add_argument("--model_path", type=str, required=True, help="Path to local Qwen-VL model")
parser.add_argument("--batch_size", type=int, default=512, help="Inference batch size")
parser.add_argument("--tp_size", type=int, default=2, help="Tensor Parallel size")
args = parser.parse_args()
input_path = Path(args.input_dir)
meta_data = collect_images(input_path)
if not meta_data:
print("[Info] No images found. Exiting.")
return
print(f"\n[Init] Loading Model: {args.model_path}")
llm = LLM(
model=args.model_path,
tokenizer=args.model_path,
trust_remote_code=True,
tensor_parallel_size=args.tp_size,
gpu_memory_utilization=0.90,
max_model_len=8192,
enforce_eager=True,
limit_mm_per_prompt={"image": 1}
)
processor = AutoProcessor.from_pretrained(args.model_path, trust_remote_code=True)
sampling_params = SamplingParams(
temperature=0.7,
max_tokens=1024,
top_p=0.9
)
results = []
print(f"\n[Run] Starting Inference on {len(meta_data)} images...")
for i in tqdm(range(0, len(meta_data), args.batch_size), desc="Processing Batches"):
batch_meta = meta_data[i : i + args.batch_size]
batch_inputs = prepare_vllm_inputs(batch_meta, processor)
valid_inputs = [inp for inp in batch_inputs if inp is not None]
valid_indices = [idx for idx, inp in enumerate(batch_inputs) if inp is not None]
if not valid_inputs:
continue
outputs = llm.generate(valid_inputs, sampling_params=sampling_params, use_tqdm=False)
for local_idx, out in enumerate(outputs):
original_meta = batch_meta[valid_indices[local_idx]]
generated_text = out.outputs[0].text
parsed = parse_llm_output(generated_text)
results.append({
"filename": original_meta["filename"],
"path": original_meta["path"],
"label": parsed["label"], # Suitable / Unsuitable
"think": parsed["think"],
"raw_output": generated_text
})
total = len(results)
unsuitable_count = sum(1 for r in results if r["label"] == "Unsuitable")
suitable_count = sum(1 for r in results if r["label"] == "Suitable")
error_count = total - unsuitable_count - suitable_count
unsuitable_rate = (unsuitable_count / total * 100) if total > 0 else 0
suitable_rate = (suitable_count / total * 100) if total > 0 else 0
print("\n" + "="*60)
print(f"AUDIT REPORT FOR: {input_path.name}")
print("="*60)
print(f"{'Total Images':<25}: {total}")
print("-" * 60)
print(f"{'UNSUITABLE (Violation)':<25}: {unsuitable_count} ({unsuitable_rate:.2f}%)")
print(f"{'SUITABLE (Safe)':<25}: {suitable_count} ({suitable_rate:.2f}%)")
print(f"{'Parse Errors':<25}: {error_count}")
print("="*60)
output_file = input_path / f"audit_result_{input_path.name}.json"
try:
with open(output_file, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\n[Done] Detailed JSON report saved to:\n-> {output_file}")
except Exception as e:
print(f"[Error] Could not save JSON: {e}")
if __name__ == "__main__":
try:
multiprocessing.set_start_method('spawn', force=True)
except RuntimeError:
pass
main() |