一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。
二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。
三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
102 lines
4.9 KiB
Python
102 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""按原始坐标比较截图,不缩放候选图,不把误差分数当成视觉验收。"""
|
|
from __future__ import annotations
|
|
import argparse
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
import re
|
|
from PIL import Image, ImageChops, ImageEnhance, ImageStat
|
|
|
|
|
|
def safe_name(value):
|
|
if not re.fullmatch(r'[\w.-]+', value) or '..' in value:
|
|
raise ValueError('名称只能包含文字、数字、短横线、下划线或单个点')
|
|
return value
|
|
|
|
|
|
def rms(diff):
|
|
stat = ImageStat.Stat(diff)
|
|
return math.sqrt(sum(value * value for value in stat.rms) / len(stat.rms))
|
|
|
|
|
|
def make_heatmap(diff):
|
|
gray = ImageEnhance.Contrast(diff.convert('L')).enhance(2.2)
|
|
gray = ImageEnhance.Brightness(gray).enhance(1.5)
|
|
heat = Image.new('RGB', diff.size, (255, 255, 255))
|
|
heat.paste(Image.new('RGB', diff.size, (255, 55, 55)), mask=gray)
|
|
return heat
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--reference', type=Path, required=True)
|
|
parser.add_argument('--candidate', type=Path, required=True)
|
|
parser.add_argument('--out-dir', type=Path, required=True)
|
|
parser.add_argument('--prefix', default='comparison')
|
|
parser.add_argument('--clip', action='append', default=[], help='名称:x,y,w,h;可重复')
|
|
parser.add_argument('--allow-height-difference', action='store_true', help='长页诊断:保留高度差,只比较共同坐标,不表示页面完整')
|
|
args = parser.parse_args()
|
|
reference = Image.open(args.reference).convert('RGB')
|
|
candidate = Image.open(args.candidate).convert('RGB')
|
|
if reference.width != candidate.width:
|
|
parser.error('截图宽度不一致;请先校准视口、DPR及截图缩放方式。不会自动缩放。')
|
|
if reference.height != candidate.height and not args.allow_height_difference:
|
|
parser.error('截图高度不一致;先修正截图方式。长页诊断可显式使用 --allow-height-difference。')
|
|
clips = []
|
|
names = {'full', 'overlap', 'reference-tail', 'candidate-tail'}
|
|
try:
|
|
safe_name(args.prefix)
|
|
for raw in args.clip:
|
|
name, numbers = raw.split(':', 1)
|
|
safe_name(name)
|
|
x, y, w, h = map(int, numbers.split(','))
|
|
if name in names or min(x, y) < 0 or min(w, h) <= 0 or x+w > reference.width or y+h > reference.height:
|
|
raise ValueError('分区重名、尺寸无效或超出参考图')
|
|
if y+h > candidate.height and not args.allow_height_difference:
|
|
raise ValueError('分区超出候选图')
|
|
names.add(name)
|
|
clips.append((name, (x, y, x+w, y+h)))
|
|
except ValueError as error:
|
|
parser.error(str(error))
|
|
args.out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def compare_region(name, ref, cand):
|
|
diff = ImageChops.difference(ref, cand)
|
|
paths = {suffix: args.out_dir / f'{args.prefix}-{name}-{suffix}.png' for suffix in ('candidate', 'diff', 'heatmap')}
|
|
cand.save(paths['candidate'])
|
|
diff.save(paths['diff'])
|
|
make_heatmap(diff).save(paths['heatmap'])
|
|
return {'name': name, 'size': ref.size, 'rms_diff': round(rms(diff), 3),
|
|
'mean_abs_diff': round(sum(ImageStat.Stat(diff).mean)/3, 3),
|
|
**{key: str(value) for key, value in paths.items()}}
|
|
|
|
height = min(reference.height, candidate.height)
|
|
equal = reference.size == candidate.size
|
|
box = (0, 0, reference.width, height)
|
|
regions = [compare_region('full' if equal else 'overlap', reference.crop(box), candidate.crop(box))]
|
|
tails = {}
|
|
for label, image in [('reference', reference), ('candidate', candidate)]:
|
|
if image.height > height:
|
|
path = args.out_dir / f'{args.prefix}-{label}-tail.png'
|
|
image.crop((0, height, image.width, image.height)).save(path)
|
|
tails[label] = str(path)
|
|
for name, box in clips:
|
|
if box[3] > candidate.height:
|
|
regions.append({'name': name, 'status': '候选截图缺少完整分区,未计分', 'box': box})
|
|
else:
|
|
regions.append(compare_region(name, reference.crop(box), candidate.crop(box)))
|
|
metrics = {'reference': str(args.reference), 'candidate': str(args.candidate),
|
|
'reference_size': reference.size, 'candidate_size_original': candidate.size,
|
|
'candidate_size_compared': [candidate.width, height], 'resized': False,
|
|
'geometry_match': equal, 'height_delta': candidate.height-reference.height,
|
|
'unmatched_tails': tails, 'regions': regions,
|
|
'note': '误差不等于还原度;字体、摄影和位置分别判断。尺寸一致也不代表视觉通过。'}
|
|
(args.out_dir / f'{args.prefix}-metrics.json').write_text(json.dumps(metrics, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
print(json.dumps(metrics, ensure_ascii=False, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|