
点检要求拍照,这事儿几乎所有厂都在做。麻烦在于照片什么都不说:一张模腔的照片,看不出是哪台机、哪一天、谁拍的。手机相册里翻出来的旧图,跟今天拍的放在一起,人眼分不出。
我们这边出过一次:一台压机的点检记录连着两周都有照片,看着挺规范。后来设备漏油停机,翻记录才发现那两周的照片是同一张,被重复上传了十四次。点检员图省事,一次拍了反复用。
这类问题靠人盯不住。我们的解法是把水印从客户端搬到服务端:照片上传后,由服务端拿服务器时间、设备档案里的安装位置、当次的点检任务,压一条水印进图里,同时把原图哈希入库。前端不参与水印生成,也就没有改的余地。
移动端拍照的默认行为是调起系统相机,拍完存相册。上传时如果给的是"选择图片"入口,用户就能从相册里翻。翻出来的图可能是上周的,也可能是别人发群里的。
更省事的做法是根本不拍,把同事发来的图存下来,第二天接着用。这类行为不是恶意,是嫌麻烦——点检点有二十几个,跑一圈四十分钟,少拍几张能省十分钟。
还有一个入口常被忽视:聊天工具。点检员之间建个群,谁路过顺手拍一张发群里,别人转手存下来上传。图是真的,时间不是。这种情况水印也救不了,因为图本身是当天拍的,只是不是他拍的、不是在那个点拍的。要挡住它,只能靠照片与任务绑定——任务里没这个点,图就传不上去。
很多表单工具支持在手机上加水印,时间、地点、姓名都能印上去。问题是这些都运行在用户的设备上:手机时间可以改,位置可以伪造,水印是先画好再上传的,图片本身没有留下任何可校验的痕迹。
有人在手机上把系统时间往前调一天,拍完再调回来,水印上就是昨天的日期。这种操作不需要技术,设置里点两下就行。所以客户端水印的作用是"提醒",不是"证明"。
一张能当证据的点检照片,要能回答三个问题:什么时间拍的、在哪台设备上拍的、拍的是哪个点检项。三个要素里任何一个来自客户端,整张图的可信度就打折。
我们的做法是三个全部由服务端给:时间取服务器接收时刻(同时保留设备采集时刻做对照),设备取点检任务绑定的设备编码,点检项取任务明细里的项目编号。客户端只负责传图和任务号,别的一概不传。
链路是五步:接收原图、算哈希、纠正方向并缩放、压水印、归档。五步同步做完,返回带水印的图给前端预览,原图另存一份。
顺序不能换。哈希要在任何处理之前算,算的是原图的字节;先缩放再算哈希,算出来的是处理后图片的指纹,跟原图对不上,校验就失效了。同理,水印要压在缩放之后的图上,否则水印会跟着图一起被缩,字变得看不清。
from io import BytesIO
import hashlib
from PIL import Image, ImageDraw, ImageFont, ImageOps
FONT_PATH = "assets/fonts/mono-cjk.ttf"
LINE_H = 34
PAD = 16
MAX_SIDE = 1600
def stamp(raw, ctx):
digest = hashlib.sha256(raw).hexdigest()
img = Image.open(BytesIO(raw))
img = ImageOps.exif_transpose(img).convert("RGB")
img.thumbnail((MAX_SIDE, MAX_SIDE), Image.LANCZOS)
lines = [
"设备 %s %s" % (ctx["device_code"], ctx["device_name"]),
"项目 %s" % ctx["item_name"],
"时间 %s" % ctx["shot_at"].strftime("%Y-%m-%d %H:%M:%S"),
"人员 %s 校验 %s" % (ctx["emp_no"], digest[:10]),
]
band_h = LINE_H * len(lines) + PAD * 2
band = Image.new("RGBA", (img.width, band_h), (0, 0, 0, 140))
draw = ImageDraw.Draw(band)
font = ImageFont.truetype(FONT_PATH, 22)
for i, text in enumerate(lines):
draw.text((PAD, PAD + i * LINE_H), text, font=font,
fill=(255, 255, 255, 255))
img.paste(band, (0, img.height - band_h), band)
out = BytesIO()
img.save(out, "JPEG", quality=86, optimize=True)
return out.getvalue(), digest代码里有几个刻意的选择。exif_transpose 必须先调,手机竖拍的照片在像素层面是横的,靠 EXIF 里的方向标记来显示,不转正就压水印,字会横着印在竖图上。
缩放到长边不超过一千六,是为了存储和传输。点检照片不需要原始分辨率,一千六足够看清油渍和裂纹,单张从四兆压到四百多千字节,一个月的存储量差十倍。
水印条压在底部,用整条半透明黑底加白字。试过在角落描边写字,遇到浅色背景看不清;也试过印在中间,挡住了要看的部位。底部整条牺牲一点画面,换来的是任何背景下都读得清。
返回的 digest 存入照片记录表,字段建唯一索引。有人拿同一张图重复上传,哈希一样,插入时直接冲突,前端提示"该照片已存在"。
这就解决了重复用图的问题。点检员想省事,翻出昨天的图再传一次,系统不认。当然他还可以重新拍一张空机器的照片,这挡不住,但那是另一类问题,靠抽查和现场核对去管,不该由技术全包。
水印里的校验码是哈希前四位到十位,印在图上。现场用手机看照片,扫一眼校验码,再跟系统里的记录对一下,就知道这张图是不是系统出的。
系统里对外的所有入口——列表、详情、导出、报表附件——都只给带水印的图。原图单独存,权限只开给质量部和设备科,调阅要留记录。
这条比任何加密都有效。图能随便下载,水印再漂亮也能被裁掉;图只有带水印的版本流出去,裁掉水印就等于没有图。
导出的 Excel 报表里挂的缩略图也走同一条链路。报表是外发的东西,一旦有人把原图塞进报表,前面的管控就白做了。我们在这块吃过一次小亏:报表模板里有个插图位,原本指向原图目录,测试时没发现,直到一次外发才被同事提醒。后来把所有出图入口统一收敛到一个服务方法,谁也别想绕过。
处理链路里特意没有覆盖原图。原图是证据,任何修改都会让哈希失效。带水印的图是展示件,丢了可以重新生成。
存储上分两个目录,按年月分文件夹,文件名用任务号加工序号。不用原始文件名——手机传来的名字多是随机串,重名概率不低,覆盖过一次就找不回来了。
允许补拍,但补拍的照片单独标记。判定规则是:拍摄时刻与任务计划时刻相差超过两小时,就算补拍,记录里标黄,班组长能看到。
补拍不是错,漏了就该补。要防的是用补拍掩盖没去现场。所以补拍照片除了常规水印,还会多一行"补拍"字样,谁看都知道这张不是当场拍的。
两小时这个阈值是按班次长度定的,不是拍脑袋。一个班八小时,正常点检间隔两到四小时,超过两小时还没传,基本就是漏了再回头补。阈值写在配置里,不同车间可以不一样,夜班可以放宽到三小时。
上线四个月,点检照片重复率从百分之十几降到零,哈希唯一索引挡住的重复提交有两千多次。照片平均体积从三点八兆降到四百多千字节,四个月省下的存储够用两年。
补拍比例稳定在百分之六上下,集中在夜班和后半夜。这个数字后来被用来调整点检频次——有两个点的补拍率超过三成,实地看下来确实偏远,后来把这两个点并到了相邻的巡检路线上。
以前客户投诉批次问题,要翻点检记录证明当时检查过,翻出来的是没有水印的照片,客户不认。现在导出带水印的图,时间、设备、项目、人都在图上,还带校验码,沟通成本降了很多。
这块我们放在搭贝里做任务派发和照片归档,水印这一段是自己写的一个小服务。
有一次印象比较深:客户质疑某批产品的外观检验没做,我们从系统里导出那台设备当天的十二张照片,时间连续,水印清晰,客户当场就不问了。这种时候技术的价值很具体,就是省掉一场扯皮。
问:服务端打水印,图片多的时候会不会拖慢上传?
答:单张处理在两百毫秒上下,主要耗时在解码和编码。并发高的时候把处理放进队列异步做,前端先拿到回执,几秒后刷新出带水印的图。别在上传接口里同步做,一百个人同时传会把接口拖死。
问:哈希唯一索引会不会误伤正常照片?
答:概率极低。两张不同时间拍的照片,字节完全一致基本不可能。真出现冲突,说明确实是同一张图。要注意的是旋转、裁剪后的图哈希会变,所以校验只能挡原样重传,挡不住改过的图,这部分靠水印和抽查补。
问:手机没有网络时拍的照片,时间怎么算?
答:设备采集时刻照存,服务端接收时刻另存一列,水印上印采集时刻并标注"离线"。判定补拍时用采集时刻,不用接收时刻,否则一整批离线照片都会被标成补拍。
问:水印印在底部,会不会挡住设备铭牌或者刻度?
答:会,所以要给拍照留构图余量。界面上加一个取景框提示,告诉用户底部一条会被水印覆盖,把要看的部位放在框内。另外留一个"重新构图"入口,压完发现挡住了可以重拍,不必勉强用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。