重点是每张卡问一次,而不是每帧问一次
卡牌识别早就能用了。把一个好模型对准一张集换式卡牌,名称、系列和编号大约半秒就回来了。它在视频上才会崩:不是技术上崩,而是纯粹的数量问题。
视频的大部分是重复。一张卡举四秒,按每秒十五帧算就是六十张同一张卡的照片;向识别端点问六十次,得到的是六十遍同样的答案。每一条朴素的流处理流水线都在干这件事,所以直播实时分析才会被认为是只有资金雄厚的平台才玩得起的东西。
cardstream 的存在就是为了消灭这种重复。它是一台架在你的视频和识别调用之间的小型状态机,几乎完全由它自己就能算出来的信号搭成:画面静止了吗?这和刚才是同一张卡吗?画面里到底有没有卡?只有一张真正新的卡静止下来时才发出调用,其他时候一律不发。其余所有处理都跑在你已有的硬件上。
每张卡调用一次,而不是每帧一次
一小时直播,每秒十五帧(这是项目验证过的帧率上限),镜头前大约出现一百张不同的卡。分析完全相同的视频,有两种方式:
次识别调用:每帧一次,其中绝大多数只是在重复一个刚刚已经有了答案的问题。
次识别调用:每张不同的卡一次,帧率多高、每张卡举多久都不影响。
调用次数少了大约 540 倍,而且这个差距不是数字游戏,而是整个设计本身。把卡拿稳不动,计数不会涨;把摄像头帧率调高,计数也不会涨。调用次数跟着你展示的卡走,而不是跟着你直播的时长走。
谁在让它持续运转
由真正做识别服务的人开发
cardstream 出自 Ximilar 收藏品识别背后的团队。它调用的识别端点就是我们在生产环境里运行的那些,所以客户端和服务端由同一批人维护。
全部公开开发
整个包都在 GitHub 上:状态机、两种部署形态和浏览器界面。决策逻辑的任何部分都没有藏在一个你无法查看的服务后面。
无需网络即可测试
整个测试套件离线运行,用替身代替模型文件和 HTTP 调用。在一台没有 API 密钥的笔记本上 fork 这个仓库,你依然能知道自己有没有弄坏什么。
代码不允许忘记的事
-
每张卡调用一次,而不是每帧一次
每一个设计决定都从这里出发。只要一个信号能在本地算出来,它就一定跑在那唯一一次离开本机的调用之前。
-
决策逻辑只有一份
"什么时候调用"只存在于一个模块里。不会散落在驱动、传输层和界面中,让三个人用三种方式各改一遍:就一个文件,一口气就能读完。
-
驱动保持轻薄
调度、日志和 I/O 归驱动管;什么算一张新卡归引擎管。换一种帧的到达方式,绝不会悄悄改变调用发出的时机。
-
没有任何东西阻塞帧循环
解码、检测、HTTP 和磁盘全部在循环之外运行。一次慢识别只会丢一帧,绝不会让你的直播延迟越积越多。
-
不锁定任何供应商,包括我们自己
识别调用是一个小接口后面可替换的一步。把它指向完全不同的地方,所有闸门、节流和缓存都照常工作。
-
默认自托管
你的硬件、你的密钥、你的视频。在客户端模式下,离开本机的只有每张不同的卡的一张裁切图,除非你选择把这场直播保存到你自己的 Ximilar 账号。
卡牌教给我们的事
其中一些足够反直觉,所以我们把它写在下一个人能找到的地方:仓库里,紧挨着它所约束的代码。
好心的提示可能会拉低准确率
告诉端点这张卡属于哪个游戏,会关掉它自己的文字系统分类器,转而默认拉丁字母,于是一张日文卡会悄无声息地匹配到英文版。这是实测出来的,不是猜的,所以只指定游戏而不指定文字系统会被当作错误处理。
稍微宽松一点的裁切匹配得更好
紧贴卡边的裁切看起来很对,效果却更差。送出去的裁切图故意留了边距,因为一点上下文比干净的边缘更管用。
恢复比假装没事更好
一次调用挂起超过超时时间,流水线会自行解开卡住的状态继续前进,而不是等一个可能永远不会来的结果。这个局限写在仓库里,而不是被粉饰过去。
读它、跑它、提补丁
仓库是公开的:一个可安装的 Python 包,带相互独立的 extras,加一个浏览器界面。定位器和嵌入模型的权重是我们自己训练的,随发行版以 Apache-2.0 发布。欢迎 issue 和 pull request,尤其欢迎来自真实直播的反馈,那能暴露出任何测试环境都发现不了的问题。
识别本身是 Ximilar 的端点,将来也一直是,因为那是背后有卡牌数据库的部分。它周围的一切,你都可以随意修改。
开源。自托管。你的直播,你的技术栈。
没有中间平台,没有按席位收费的许可:接入 Ximilar API 或你自己的识别系统,cardstream 对每张不同的卡只调用一次,而不是每帧一次。无论你是在 Whatnot 上拆盒、做 Fanatics Live 式的直播秀,还是在自己的直播电商环境里开播,今晚就能用手头现有的硬件跑起 cardstream。