VOTT项目迁移

 

本文章说的VOTT, 版本是2.2.0,  当我接触的时候已经N年没更新了, 估计这种单一的打标签软件不会有太多BUG吧. 这软件说好用也好用, 有些地方也真难用

这个软件是没有项目的导入导出的功能(导出是指生成数据集, 而不是项目的导出), 我问了gpt半天, 再加上自己摸索, 终于找到一个算是可行的办法

闲话不说了, 下面记录一下迁移步骤

1  把旧项目COPY到新的电脑(路径暂定为A)(包括.vott项目文件, 暂定为X.vott)

2  新电脑指定一个文件夹当做目标文件夹(暂定为B)

3  删除新电脑的C:\Users\[你的用户名]\AppData\Roaming\vott\Local Storage\leveldb 文件夹中的所有文件

4  打开VOTT, 配置左下角的配置, 添加Security Token并保存

5 在新电脑新建一个项目, 然后保存

6 上一步会生成一个.vott文件(暂定为Y.vott), 编辑它, 把X.vott中的tag中的部分的内容COPY过来, 并保存

7 然后把旧项目的图片(路径A中的)全部COPY到新电脑的文件夹B(我不确定这里能不能同时copy 所有的asset文件)

8 重新打开VOTT, 打开本地项目(注意, 要用打开本地项目打开 ".VOTT"文件, 不要用右上角的"最近项目"打开)

9 此时应该新的图片已经加载成功了, 然后这里就挺操蛋的, 你要每一张图都浏览一下(按"下"方向快速浏览)(所以这个软件只适用于中小量图片, 大量图片要死人, 或者有其他方法, 暂时我没研究出来, 直接修改.VOTT文件没用), 浏览完后点保存, 并关闭VOTT软件

10 把所有asset文件copy到文件夹B(不确定能不能放在第7步, 如果能, 此步骤忽略)

11 写脚本修改Y.vott, 根据图片名称把旧的asset文件及里面的内容修改成新的, 代码如下(参数自己改), python运行(注意VOTT软件要关闭, 没测不关闭行不行)

12 运行完后, 再用vott软件打开项目(注意, 要用打开本地项目打开 ".VOTT"文件, 不要用右上角的"最近项目"打开)

13 好像差不多了, 因为不是第一时间写的, 有些可能忘了, 有问题再研究吧(不过估计短期内不会再整了)

 


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()

 

posted on 2025-09-16 10:56  黑暗之眼  阅读(10)  评论(0)    收藏  举报