惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
爱范儿
爱范儿
P
Proofpoint News Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Security Blog
Microsoft Security Blog
小众软件
小众软件
F
Full Disclosure
酷 壳 – CoolShell
酷 壳 – CoolShell
Recent Announcements
Recent Announcements
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
F
Fortinet All Blogs
Google DeepMind News
Google DeepMind News
Jina AI
Jina AI
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
I
Intezer
S
SegmentFault 最新的问题
S
Schneier on Security
V
Vulnerabilities – Threatpost
T
Tenable Blog
P
Privacy & Cybersecurity Law Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Latest news
Latest news
Simon Willison's Weblog
Simon Willison's Weblog
D
DataBreaches.Net
L
LINUX DO - 热门话题
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
Stack Overflow Blog
Stack Overflow Blog
SecWiki News
SecWiki News
H
Hacker News: Front Page
aimingoo的专栏
aimingoo的专栏
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
Threatpost
罗磊的独立博客
L
LangChain Blog
The Last Watchdog
The Last Watchdog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
K
Kaspersky official blog
腾讯CDC
阮一峰的网络日志
阮一峰的网络日志
N
News | PayPal Newsroom
美团技术团队
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
D
Docker
T
The Blog of Author Tim Ferriss
N
Netflix TechBlog - Medium
博客园 - 【当耐特】
Cyberwarzone
Cyberwarzone

博客园 - 黑暗之眼

VS2022 过期 删除MYSQL教程 安装mysql 64位 Smartform给文本绑定值 ABAP的smartform赋值 查源代码中指定字符串 Excel取消保护密码 修改机器名后无法使用复制 COCOS2DX2.2.2 创建CCEditBox输入框架实现文本及密码输入 SSMS 2008R2没有智能感知方法解决 SQL SERVER 强制排序规则查询 清除SQL Server执行计划 Cocos2D-x搭建新环境注意事项 Cocos2D 指定文件夹创建项目 Cocos2D创建项目 DVDRW光驱无法读DVD刻录盘 Android导入Cocos2D的Sample项目 监控SQL Server的job执行情况 动态调用Web Service
VOTT项目迁移
黑暗之眼 · 2025-09-16 · via 博客园 - 黑暗之眼

本文章说的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()