| 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: "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) |
|
|
| |
| 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 |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
|
|
| 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) |
|
|
| |
|
|
| out[name] = int(round(max(0, min(100, 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__": |
|
|
| |
| |
|
|
|
|
| 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", |
| ) |
|
|