本文章说的VOTT, 版本是2.2.0, 当我接触的时候已经N年没更新了, 估计这种单一的打标签软件不会有太多BUG吧. 这软件说好用也好用, 有些地方也真难用
9 此时应该新的图片已经加载成功了, 然后这里就挺操蛋的, 你要每一张图都浏览一下(按"下"方向快速浏览)(所以这个软件只适用于中小量图片, 大量图片要死人, 或者有其他方法, 暂时我没研究出来, 直接修改.VOTT文件没用), 浏览完后点保存, 并关闭VOTT软件
11 写脚本修改Y.vott, 根据图片名称把旧的asset文件及里面的内容修改成新的, 代码如下(参数自己改), python运行(注意VOTT软件要关闭, 没测不关闭行不行)
import json, os, shutil, time
# ========== 配置(请按需修改) ==========
BASELINE_VOTT = r"D:\B\ObjDetection.vott"
SOURCE_VOTT = r"D:\A\ObjDetection.20250915.vott"
ASSETS_DIR = r"D:\B" # 存放 *-asset.json 的目录
LOG_PATH = r"D:\log.txt" # 日志文件(追加)
MAKE_BACKUP = True # 对被修改的 b-asset.json 做备份(.bak.TIMESTAMP)
# =======================================
def now_ts():
return time.strftime("%Y-%m-%d %H:%M:%S")
def append_log(msg):
line = f"{now_ts()} {msg}\n"
print(line.strip())
with open(LOG_PATH, "a", encoding="utf-8") as lf:
lf.write(line)
def load_json_try(path):
"""尝试若干编码读取 JSON,返回 (obj, encoding)"""
encs = ["utf-8", "utf-8-sig", "latin-1"]
last_exc = None
for e in encs:
try:
with open(path, "r", encoding=e) as f:
return json.load(f), e
except Exception as ex:
last_exc = ex
raise last_exc
def write_json_atomic(path, data, encoding="utf-8"):
tmp = path + ".tmp"
with open(tmp, "w", encoding=encoding) as f:
json.dump(data, f, ensure_ascii=False, indent=2)
os.replace(tmp, path)
def replace_exact_str_in_obj(obj, old, new):
"""递归遍历 JSON 对象,将值完全等于 old 的字符串替换为 new"""
if isinstance(obj, dict):
for k, v in list(obj.items()):
if isinstance(v, str) and v == old:
obj[k] = new
else:
replace_exact_str_in_obj(v, old, new)
elif isinstance(obj, list):
for i, v in enumerate(obj):
if isinstance(v, str) and v == old:
obj[i] = new
else:
replace_exact_str_in_obj(v, old, new)
# 其它类型跳过
def basename_from_asset_info(info):
"""从 asset info(vott 中的 asset 对象)优先取 name,否则从 path 提取 basename"""
if not isinstance(info, dict):
return None
name = info.get("name")
if name:
return os.path.basename(name)
p = info.get("path") or ""
if p.startswith("file:"):
p = p[5:]
return os.path.basename(p) if p else None
def build_asset_maps(vott_path):
data, _enc = load_json_try(vott_path)
assets = data.get("assets", {}) or {}
# 返回: assets_dict, and filename -> list of asset_ids
fname_to_ids = {}
for aid, info in assets.items():
fn = basename_from_asset_info(info)
if fn:
fname_to_ids.setdefault(fn, []).append(aid)
return assets, fname_to_ids
def main():
# 校验路径
if not os.path.isfile(BASELINE_VOTT):
raise SystemExit(f"基准 vott 未找到: {BASELINE_VOTT}")
if not os.path.isfile(SOURCE_VOTT):
raise SystemExit(f"source vott 未找到: {SOURCE_VOTT}")
if not os.path.isdir(ASSETS_DIR):
raise SystemExit(f"assets 目录未找到: {ASSETS_DIR}")
# 初始化日志(如果不存在则创建;按要求为追加模式,这里保留旧日志)
append_log("=== 开始运行 rename_assets_by_vott.py ===")
base_assets, base_fname_map = build_asset_maps(BASELINE_VOTT)
src_assets, src_fname_map = build_asset_maps(SOURCE_VOTT)
append_log(f"基准 vott 中 assets 数量: {len(base_assets)},按文件名分组: {len(base_fname_map)}")
append_log(f"Source vott 中 assets 数量: {len(src_assets)},按文件名分组: {len(src_fname_map)}")
processed = 0
skipped_no_bfile = 0
skipped_existing_a = 0
errors = 0
# 遍历基准 vott 的每个 asset(按 asset id)
for a_id, a_info in base_assets.items():
try:
fn = basename_from_asset_info(a_info)
if not fn:
append_log(f"⚠ 跳过基准 asset {a_id}:无法解析图片名")
continue
a_asset_json = os.path.join(ASSETS_DIR, f"{a_id}-asset.json")
# 步骤1:若 a-asset.json 已存在,跳过
if os.path.exists(a_asset_json):
skipped_existing_a += 1
#append_log(f"跳过(目标已存在): {a_asset_json}")
continue
# 步骤2:在 source vott 中查找所有 b 候选 id
candidate_b_ids = src_fname_map.get(fn, [])
if not candidate_b_ids:
append_log(f"未在 source vott 中找到匹配图片 '{fn}' 的任何 asset id(基准 a={a_id}),记录并跳过")
with open(LOG_PATH, "a", encoding="utf-8") as lf:
lf.write(f"{now_ts()} MISSING_B image='{fn}' baseline_a={a_id} note='no candidates in source vott'\n")
skipped_no_bfile += 1
continue
# 在候选 b 中找第一个存在的 b-asset.json
found_b = None
for b_id in candidate_b_ids:
b_asset_json = os.path.join(ASSETS_DIR, f"{b_id}-asset.json")
if os.path.exists(b_asset_json):
found_b = b_id
found_b_path = b_asset_json
break
if not found_b:
append_log(f"在候选 b 列表中未找到实际存在的 b-asset.json:image='{fn}' candidates={candidate_b_ids} baseline_a={a_id}")
with open(LOG_PATH, "a", encoding="utf-8") as lf:
lf.write(f"{now_ts()} MISSING_B_FILES image='{fn}' baseline_a={a_id} candidates={candidate_b_ids}\n")
skipped_no_bfile += 1
continue
# 步骤3:修改 found_b_path 内容,把 b->a,并写为 a-asset.json
try:
b_json, enc = load_json_try(found_b_path)
except Exception as e:
append_log(f"❌ 读取 b-asset.json 失败: {found_b_path} 错误: {e}")
errors += 1
continue
# 备份 b 文件(可选)
if MAKE_BACKUP:
bak = found_b_path + f".bak.{int(time.time())}"
try:
shutil.copy2(found_b_path, bak)
append_log(f"备份 b 文件: {found_b_path} -> {bak}")
except Exception as e:
append_log(f"⚠ 备份失败: {found_b_path} 错误: {e}")
# 替换 JSON 内容中等于 b 的字符串为 a(精确匹配)
replace_exact_str_in_obj(b_json, found_b, a_id)
# 保证 asset.id 设置为 a_id(如果存在 asset 对象)
if isinstance(b_json, dict):
if "asset" in b_json and isinstance(b_json["asset"], dict):
b_json["asset"]["id"] = a_id
# 同步 baseline 的 name / path(若有)
if a_info.get("name"):
b_json["asset"]["name"] = a_info.get("name")
if a_info.get("path"):
b_json["asset"]["path"] = a_info.get("path")
# 写入 a-asset.json(若存在则先备份)
target_path = os.path.join(ASSETS_DIR, f"{a_id}-asset.json")
try:
if os.path.exists(target_path):
# 不太可能,因为我们刚才检查过不存在,但仍以防万一
bak2 = target_path + f".bak.{int(time.time())}"
shutil.copy2(target_path, bak2)
append_log(f"目标已存在,先备份: {target_path} -> {bak2}")
write_json_atomic(target_path, b_json, encoding=enc or "utf-8")
append_log(f"✅ 写入并生成: {target_path} (来自 {found_b_path},old_b={found_b} -> new_a={a_id})")
except Exception as e:
append_log(f"❌ 写入目标失败: {target_path} 错误: {e}")
errors += 1
continue
# 删除原 b 文件(如果和 target 不同)
try:
if os.path.abspath(found_b_path) != os.path.abspath(target_path):
os.remove(found_b_path)
append_log(f"删除原 b 文件: {found_b_path}")
except Exception as e:
append_log(f"⚠ 无法删除原 b 文件: {found_b_path} 错误: {e}")
processed += 1
except Exception as e:
append_log(f"❌ 处理基准 asset {a_id} 时出错: {e}")
errors += 1
append_log(f"完成:processed={processed}, skipped_existing_a={skipped_existing_a}, skipped_no_bfile={skipped_no_bfile}, errors={errors}")
append_log("=== 脚本结束 ===\n")
if __name__ == "__main__":
main()