Portrait-Infer / ensemble_results.py
chris-123's picture
Upload ensemble_results.py
11d7bc2 verified
Raw
History Blame Contribute Delete
10 kB
import os
import re
import json
import glob
import copy
from collections import Counter, defaultdict
from statistics import median
CRITERIA = [
"Color Harmony",
"Visual Style Consistency",
"Sharpness",
"Light and Shadow Modeling",
"Creativity and Originality",
"Exposure Control",
"Application of Classical Composition Principles",
"Depth of Field and Layering",
"Visual Center Stability",
"Visual Flow Guidance",
"Structural Support Stability",
"Appropriateness of Negative Space",
"Subject Integrity",
]
# LEVEL_ORDER = {"Poor": 0, "Medium": 1, "Good": 2}
# LEVEL_INV = {0: "Poor", 1: "Medium", 2: "Good"}
LEVEL_ORDER = {"Poor": 0, "Medium": 1, "Good": 2}
LEVEL_INV = {0: "A", 1: "B", 2: "C"}
def normalize_level(x):
if not isinstance(x, str):
return None
x = x.strip().lower()
mp = {
"poor": "Poor",
"medium": "Medium",
"good": "Good",
}
return mp.get(x)
def basename_from_item(item):
img_path = item.get("images", [{}])[0].get("path", "")
return os.path.basename(img_path)
def parse_response_raw(resp):
"""
支持:
- "{\"total_score\": 84}"
- "41"
- "{\"criteria\": {...}}"
- "[\"Medium\", ...]"
- "{\"answer\": \"C\"}"
"""
if isinstance(resp, (dict, list, int, float)):
return resp
if not isinstance(resp, str):
return None
s = resp.strip()
# 纯数字分数
if re.fullmatch(r"-?\d+(\.\d+)?", s):
return float(s)
# 标准 JSON
try:
return json.loads(s)
except Exception:
pass
# 兜底:提取 {...}
m = re.search(r"\{.*\}", s, flags=re.S)
if m:
try:
return json.loads(m.group(0))
except Exception:
pass
# 兜底:提取 [...]
m = re.search(r"\[.*\]", s, flags=re.S)
if m:
try:
return json.loads(m.group(0))
except Exception:
pass
return None
def iter_json_or_jsonl(path):
with open(path, "r", encoding="utf-8") as f:
text = f.read().strip()
if not text:
return []
try:
obj = json.loads(text)
if isinstance(obj, list):
return obj
if isinstance(obj, dict):
return [obj]
except Exception:
pass
rows = []
for line in text.splitlines():
line = line.strip()
if line:
rows.append(json.loads(line))
return rows
def read_all_files(folder, recursive=True):
pattern = "**/*.json*" if recursive else "*.json*"
files = sorted(glob.glob(os.path.join(folder, pattern), recursive=recursive))
rows = []
for fp in files:
try:
rows.extend(iter_json_or_jsonl(fp))
except Exception as e:
print(f"[WARN] failed to read {fp}: {e}")
return rows
def parse_score_folder(folder):
pred = defaultdict(list)
for item in read_all_files(folder):
name = basename_from_item(item)
data = parse_response_raw(item.get("response", ""))
score = None
if isinstance(data, dict):
score = data.get("total_score")
elif isinstance(data, (int, float)):
score = data
elif isinstance(data, str):
if re.fullmatch(r"-?\d+(\.\d+)?", data.strip()):
score = float(data.strip())
if name and score is not None:
try:
score = float(score)
score = max(0, min(100, score))
pred[name].append(score)
except Exception:
pass
return pred
def parse_level_folder(folder):
pred = defaultdict(lambda: defaultdict(list))
for item in read_all_files(folder):
name = basename_from_item(item)
data = parse_response_raw(item.get("response", ""))
if not name:
continue
# 格式1:{"criteria": {"Color Harmony": "Good", ...}}
if isinstance(data, dict) and isinstance(data.get("criteria"), dict):
criteria = data["criteria"]
for c in CRITERIA:
lv = normalize_level(criteria.get(c))
if lv:
pred[name][c].append(lv)
# 格式2:["Medium", "Medium", ..., 共13个]
elif isinstance(data, list):
for c, lv_raw in zip(CRITERIA, data):
lv = normalize_level(lv_raw)
if lv:
pred[name][c].append(lv)
return pred
def parse_reason_folder(folder):
pred = defaultdict(list)
for item in read_all_files(folder):
name = basename_from_item(item)
data = parse_response_raw(item.get("response", ""))
ans = None
if isinstance(data, dict):
ans = data.get("answer")
elif isinstance(data, str):
ans = data
if name and isinstance(ans, str):
ans = ans.strip().upper()
if ans in {"A", "B", "C", "D"}:
pred[name].append(ans)
return pred
def majority_vote(values, default=None):
values = [v for v in values if v is not None]
if not values:
return default
cnt = Counter(values)
# 平票时按第一次出现顺序
return max(cnt.keys(), key=lambda x: (cnt[x], -values.index(x)))
def ensemble_scores(score_dicts, method="mean"):
merged = defaultdict(list)
# print("merged is", merged)
for d in score_dicts:
for name, scores in d.items():
merged[name].extend(scores)
out = {}
for name, scores in merged.items():
if method == "median":
val = median(scores)
else:
val = sum(scores) / len(scores)
# print("scores are", name, scores, val)
out[name] = int(round(max(0, min(100, val))))
# out[name] = int(round(val))
return out
#####################
LEVEL_SCORE = {
"Poor": 2.5,
"Medium": 6.0,
"Good": 8.5,
}
def score_to_level(score):
if 0 <= score < 5:
return "A"
elif 5 <= score < 7:
return "B"
elif 7 <= score <= 10:
return "C"
else:
# 兜底,防止异常值
score = max(0, min(10, score))
if score < 5:
return "A"
elif score < 7:
return "B"
return "C"
def ensemble_levels(level_dicts, method="score_mean"):
merged = defaultdict(lambda: defaultdict(list))
for d in level_dicts:
for name, cd in d.items():
for c, levels in cd.items():
merged[name][c].extend(levels)
out = defaultdict(dict)
for name, cd in merged.items():
for c in CRITERIA:
vals = cd.get(c, [])
if not vals:
continue
if method == "score_mean":
nums = [LEVEL_SCORE[v] for v in vals if v in LEVEL_SCORE]
if nums:
avg_score = sum(nums) / len(nums)
out[name][c] = score_to_level(avg_score)
elif method == "vote":
out[name][c] = majority_vote(vals, default="Medium")
elif method == "ordinal_mean":
nums = [LEVEL_ORDER[v] for v in vals if v in LEVEL_ORDER]
if nums:
out[name][c] = LEVEL_INV[int(round(sum(nums) / len(nums)))]
return out
######################
def ensemble_answers(reason_dicts):
merged = defaultdict(list)
for d in reason_dicts:
for name, answers in d.items():
merged[name].extend(answers)
return {
name: majority_vote(answers, default="A")
for name, answers in merged.items()
}
def build_submission(
template_path,
score_model_folders,
level_model_folders,
reason_model_folders,
output_path,
score_method="mean",
level_method="vote",
):
score_dicts = [parse_score_folder(p) for p in score_model_folders]
level_dicts = [parse_level_folder(p) for p in level_model_folders]
reason_dicts = [parse_reason_folder(p) for p in reason_model_folders]
score_ens = ensemble_scores(score_dicts, method=score_method)
level_ens = ensemble_levels(level_dicts, method=level_method)
answer_ens = ensemble_answers(reason_dicts)
with open(template_path, "r", encoding="utf-8") as f:
result = json.load(f)
missing_score = 0
missing_level = 0
missing_answer = 0
for item in result:
name = item["image_path"]
if name in score_ens:
item["total_score"] = score_ens[name]
else:
missing_score += 1
for c in CRITERIA:
if name in level_ens and c in level_ens[name]:
item["criteria"][c]["level"] = level_ens[name][c]
else:
missing_level += 1
if name in answer_ens:
item["answer"] = answer_ens[name]
else:
missing_answer += 1
with open(output_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"Saved to: {output_path}")
print(f"Images: {len(result)}")
print(f"Missing score images: {missing_score}")
print(f"Missing level fields: {missing_level}")
print(f"Missing answer images: {missing_answer}")
if __name__ == "__main__":
# from pathlib import Path
# ROOT = Path("/mnt/shared-storage-user/zhuxiaorong/liyunhao_data/my_code/cvpr26_challenge")
TEMPLATE_PATH = "track_1_test_demo.json"
OUTPUT_PATH = "./track_1_test.json"
SCORE_MODEL_FOLDERS = [
"./result-score"
]
LEVEL_MODEL_FOLDERS = [
"./result-level"
]
REASON_MODEL_FOLDERS = [
"./result-reason"
]
build_submission(
template_path=TEMPLATE_PATH,
score_model_folders=SCORE_MODEL_FOLDERS,
level_model_folders=LEVEL_MODEL_FOLDERS,
reason_model_folders=REASON_MODEL_FOLDERS,
output_path=OUTPUT_PATH,
score_method="mean",
level_method="score_mean",
)