Coding 的痕迹

一位互联网奔跑者的网上日记

0%

"小风筝" 集五福

12月底时,易班工作站计划推出一个新春集五福活动。类似于“支付宝”的“扫福字、集福卡”,我们要完成一个“扫校徽、集福卡”程序的设计和实现。这篇文章不是一份严谨的技术文档,而是对整个项目分析和实施过程的记录。

该项目当前发布在 Github. 前端: kite-fu,后端: kite-badgekite-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.getUserMediaMediaDevices.getUserMedia 两套 API,并通过 frame 嵌入到 flutter 中使用。

校徽检测

后端主要难点在快速目标检测,主要有两套方案:传统的模式匹配与基于深度神经网络的目标检测。因为有同学有 yolo 框架基础,由他完成目标检测相关功能。一开始,我们想使用成熟的产品 Logo 识别库(如 DeepLogo),因为训练麻烦,便改用旷世科技的 yolox 库。

我们依靠易班工作站收集了 69 张(数据集)不同设备拍摄的校徽图案,并标注和检测,在其中,我们发现标注时避开校徽外部的圆形可以提高检测效果。

:左:标注了完整校徽的识别效果;右:仅标注校徽中间部分的识别效果。实际照片的置信度较截图低。

:识别校徽的效果。

在识别中,不可避免会出现误报,由于我们要求精度不需要很高,把置信度阈值卡到 93% 左右应该就可以了。

活动模拟

策划同学希望活动时长在 7 天(事实证明时间有点短),每天约能扫中 2 张福卡。初始的福卡分配比例为(40%、30%、20%、5%、5%),但经过模拟后,在每天均参与活动的情况下,7 天集齐的频率仅为 23%。于是调整分配比,最终敲定的比例为:

1
2
3
4
5
6
7
_DEFAULT_PROBABILITY_LIST = [
0.40, # 创新福
0.30, # 博学福
0.15, # 富贵福
0.10, # 康宁福
0.05, # 上应福
]

编写模拟计算 代码,运行一百万次后,结果如下:

策略\集齐率 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 实现:

  1. 查询集齐 5 种卡片的用户学号

  2. 在有万能福的情况下,查询集齐卡片的用户学号

  3. 查询集齐 5 种卡片的用户学号、集齐时间

问题1的思路是,对元组 (uid, card_type) 进行去重,去重后统计每个用户的卡片数量,筛选出集齐了5张卡片的用户。SQL 代码如下:

1
2
3
4
5
6
7
8
SELECT a.account FROM (
SELECT uid, COUNT(card) AS count
FROM
(SELECT DISTINCT uid, card FROM fu.scan WHERE card != 0) log
GROUP BY uid
ORDER BY count DESC) s,
"user".account AS a
WHERE count = 5 AND s.uid = a.uid;

问题2中,有6种卡片,卡片数量为5时即集齐了所有福卡。

问题3没有想到好的SQL实现方式,可能需要结合 PostgreSQL 的 generate_series 函数,选取不同长度的扫描结果,去判断用户是否集齐。包含第一次扫描结果、且长度最短的区间,就是用户整个活动的区间。


由于宣传力度和平台兼容性问题,在 7 天的活动中,共有 314 位用户注册,但只有 231 位用户成功扫过校徽图案。7 天后,仅 21 位用户成功集齐。而项目开始时考虑的“奖品不够”,最终成了笑话。活动期间,从技术上来说,服务器运行总体平稳,识别速度很快(CPU跑,单张图片约70ms),取得了不错的效果。

展望

此处有两个优化的方案:

  1. 使用客户端(浏览器)进行模型计算。考虑到计算量,这样做是可行的,但模型可能耗费较大的流量。考虑项目规模,这样做也比较安全。

  2. 使用模式匹配方法效率可能会更高一些。

欢迎关注我的其它发布渠道