12月底时,易班工作站计划推出一个新春集五福活动。类似于“支付宝”的“扫福字、集福卡”,我们要完成一个“扫校徽、集福卡”程序的设计和实现。这篇文章不是一份严谨的技术文档,而是对整个项目分析和实施过程的记录。
该项目当前发布在 Github. 前端: kite-fu,后端: kite-badge、kite-server。
截图
需求分析
技术需求
项目在 小风筝后端 基础上编写,使用小风筝的账户系统和数据库,避免编写重复代码。
前端主流的方案包括 Web、小程序和App。由于开发团队在活动实施时在使用 flutter 框架进行小风筝App的开发,对 flutter 较为熟悉,已经积累了一些框架性的代码,故不考虑小程序而是直接基于 flutter 开发。但考虑到如果做成 app,Apple Appstore 上架审核时长不确定,出现问题的修复周期长,因此前端基于 flutter web 开发。
在选型时,尝试编译了一个 flutter web 的 demo project,生成的源代码在 5M 左右(build 有两种方式,html 和 canvas kit,html 文件大小较小,加载速度快,兼容性好,所以我们没有使用 canvas kit),生成文件的大小和加载速度可以接受。
后端因 python 语言在图像识别领域生态丰富,因此图像识别部分单独使用 python 语言编写。
功能需求
活动仅对校内学生用户开放,即,需要一定的方式验证。由于频繁访问认证服务器容易被学校防火墙屏蔽,因而采用 “统一认证登录” + “身份证号后6位验证” 二选一的方式进行。易班工作站提供全校学生的身份证号信息。登录成功后,系统为每一位用户分配一个唯一 uid
,并生成 JWT token。
依据支付宝的活动功能,用户需要具有基本的:查询已有卡片、扫描校徽功能。扫描结果,应包括:
-
活动未开始或已结束
-
未检测到校徽
-
检测到校徽,抽中福卡并展示
-
检测到校徽,未抽中福卡,给出友好提示,可以给一些小风筝App相关的使用提示
-
达到今日最大次数(可以与“未扫描到卡片”状态合并)
卡片分为“上应福”、“创新福”、“博学福”、“福贵福”和“康宁福”五种。
由于无法估计最终参与的用户数量,且经费有限,需要限制购买奖品的数量(我们没有办法发现金),因此考虑一些控制集齐人数的方法:
-
对每一种卡片设置了不同的概率,可以增加收集难度。并且,对不同学院的用户应用不同卡片概率的设置,并做好宣传,可以增强活动的趣味性。不过活动的策划认为应该在活动初始就公布每一种卡片的获取概率。
-
发布“万能福”(我们称之为“幸运风筝福”)用于提高用户集齐概率。
尽可能实现类似支付宝活动页的扫福体验——实时扫图与识别,不应该要求用户拍照后再上传。
性能需求
项目支撑服务器包括一台单核1G内存和一台单核2G内存的云服务器,在设计时要考虑图像识别达到 5 QPS 的并发数,内存占用和识别速度应在合理范围。
关键技术尝试
项目的主要难点在于:前端实现调用摄像头并具备良好的兼容性,后端实现快速的目标检测。
调用摄像头
Web 上的 Camera API 有两套标准:navigator.getUserMedia
(老方案)和 MediaDevices.getUserMedia
(新方案),Web RTC API 也支持打开摄像头的操作,同时,Flutter 的源中也提供了 camera_web 库。
对几种方案分别测试 iOS(Safari、微信内置浏览器)、Android (华为浏览器、小米浏览器、Firefox、微信内置浏览器),发现不少安卓浏览器仍然只支持旧的 Camera API,而 iOS 平台对新 API 支持较好。flutter 的 camera_web 库疑似使用的是新版 API,对部分安卓浏览器支持不好。
经过测试,最终决定使用 Javascript 封装 navigator.getUserMedia
和 MediaDevices.getUserMedia
两套 API,并通过 frame 嵌入到 flutter 中使用。
校徽检测
后端主要难点在快速目标检测,主要有两套方案:传统的模式匹配与基于深度神经网络的目标检测。因为有同学有 yolo 框架基础,由他完成目标检测相关功能。一开始,我们想使用成熟的产品 Logo 识别库(如 DeepLogo),因为训练麻烦,便改用旷世科技的 yolox 库。
我们依靠易班工作站收集了 69 张(数据集)不同设备拍摄的校徽图案,并标注和检测,在其中,我们发现标注时避开校徽外部的圆形可以提高检测效果。
图:左:标注了完整校徽的识别效果;右:仅标注校徽中间部分的识别效果。实际照片的置信度较截图低。
图:识别校徽的效果。
在识别中,不可避免会出现误报,由于我们要求精度不需要很高,把置信度阈值卡到 93% 左右应该就可以了。
活动模拟
策划同学希望活动时长在 7 天(事实证明时间有点短),每天约能扫中 2 张福卡。初始的福卡分配比例为(40%、30%、20%、5%、5%),但经过模拟后,在每天均参与活动的情况下,7 天集齐的频率仅为 23%。于是调整分配比,最终敲定的比例为:
1 | _DEFAULT_PROBABILITY_LIST = [ |
编写模拟计算 代码,运行一百万次后,结果如下:
策略\集齐率 | 1天 | 2天 | 3天 | 4天 | 5天 | 6天 | 7天 |
---|---|---|---|---|---|---|---|
每天2张 | / | / | 3% | 10% | 18% | 26% | 33% |
每天3张 | / | 3% | 14% | 26% | 37% | 47% | 56% |
每天4张 | / | 10% | 26% | 41% | 53% | 63% | 71% |
每天5张 | 1% | 18% | 37% | 53% | 66% | 74% | 81% |
最终决定每天允许用户最多抽3张,点开公众号文章后可加1次。活动时长为 7 天。
统计与评价
当前的表大致有:
-
fu.scan 用户抽卡记录。card 为 0 表示为抽中,为其他值表示抽中的卡片。
-
user.account 用户表。
这里抛出几个问题。怎样用 SQL 实现:
-
查询集齐 5 种卡片的用户学号
-
在有万能福的情况下,查询集齐卡片的用户学号
-
查询集齐 5 种卡片的用户学号、集齐时间
问题1的思路是,对元组 (uid, card_type)
进行去重,去重后统计每个用户的卡片数量,筛选出集齐了5张卡片的用户。SQL 代码如下:
1 | SELECT a.account FROM ( |
问题2中,有6种卡片,卡片数量为5时即集齐了所有福卡。
问题3没有想到好的SQL实现方式,可能需要结合 PostgreSQL 的 generate_series
函数,选取不同长度的扫描结果,去判断用户是否集齐。包含第一次扫描结果、且长度最短的区间,就是用户整个活动的区间。
由于宣传力度和平台兼容性问题,在 7 天的活动中,共有 314 位用户注册,但只有 231 位用户成功扫过校徽图案。7 天后,仅 21 位用户成功集齐。而项目开始时考虑的“奖品不够”,最终成了笑话。活动期间,从技术上来说,服务器运行总体平稳,识别速度很快(CPU跑,单张图片约70ms),取得了不错的效果。
展望
此处有两个优化的方案:
-
使用客户端(浏览器)进行模型计算。考虑到计算量,这样做是可行的,但模型可能耗费较大的流量。考虑项目规模,这样做也比较安全。
-
使用模式匹配方法效率可能会更高一些。