--- title: node+nsfwjs搭建图片鉴黄服务 id: '363' cover: '/images/nsfwjs_logo.jpeg' categories: - 分享 tags: - node - JavaScript - nsfwjs - egg date: 2021-05-10 14:25:23 --- 去年由于业务的需要,对图片的内容安全做一定的监控,简单的说就是图片鉴黄,由于早前就体验过nsfwjs的强大,所以第一时间就想到用它搭建一个图片鉴黄的服务。 翻看了github上[nsfwjs的文档](https://github.com/infinitered/nsfwjs),也有node相关的例子,原本会以为比较顺利的可以搭建完成,没想到后面还是经历了一波三折,所以也就有了这篇文章,记录一下自己的心路历程。 # 1. 服务框架搭建 node框架选择很多,我这里选了egg,因为egg集成了很多丰富的能力,简单省事,并且图片鉴黄,必定会有上传,egg也很好的集成了上传模块,简单配置一下即可放心使用。 egg的搭建以及上传的配置,就不展开了,参考官方文档即可。 [egg官方文档](https://eggjs.org/zh-cn/tutorials/index.html) [egg上传](https://eggjs.org/zh-cn/basics/controller.html#%E8%8E%B7%E5%8F%96%E4%B8%8A%E4%BC%A0%E7%9A%84%E6%96%87%E4%BB%B6) *node版本建议选用 14.x 及以上版本,反正我在centOS 7.x 环境下吃过亏了。* # 2. 安装 ``` sh npm install nsfwjs npm install @tensorflow/tfjs-node ``` # 3. 加载模型 官方文档给出的例子如下: ``` js const axios = require('axios') const tf = require('@tensorflow/tfjs-node') const nsfw = require('nsfwjs') async function fn() { const pic = await axios.get(`link-to-picture`, { responseType: 'arraybuffer', }) const model = await nsfw.load() // 加载模型 const image = await tf.node.decodeImage(pic.data, 3) const predictions = await model.classify(image) image.dispose() console.log(predictions) } fn() ``` nsfw.load() 不传参数的话,默认加载一个官方的模型地址,模型的大小大概几十上百兆,加载的过程会比较慢,如果你的服务部署到非互联网环境,那就直接启动不了了,所以把模型放到本地服务才是比较好的解决方案。 先去[nsfw_model](https://github.com/GantMan/nsfw_model/releases/tag/1.1.0)下载模型到本地(选择第一个nsfw_mobilenet_v2_140_224.zip 即可),然后解压zip包。web_model文件夹里面就是我们需要的模型了。 ![web_model](/images/WX20210510-155205.png) 然后把web_model里面的文件,放到egg项目下的app/model目录下,然后在项目根目录下增加一个app.js文件,内容如下: ``` js const tf = require('@tensorflow/tfjs-node') const nsfw = require('nsfwjs') class AppBootHook { constructor(app) { this.app = app; } async didLoad() { console.log('nsfwModel 加载中···'); this.app.nsfwModel = await nsfw.load('file://./app/model/', { type: 'graph' }); console.log('nsfwModel 已经加州完毕!'); } } module.exports = AppBootHook; ``` *因为用的是graph model,所以type传'graph'* # 4. 图片鉴定 在app.js里面已经加载了模型,并且挂载到app.nsfwModel上,所以只要在需要的时候直接使用即可。 router和controller部分直接跳过,这部分是定义一个上传的路由,然后上传成功后获取上传的图片文件。与nsfwjs的使用没有太大关系,直接跳到获取到图片文件后使用nsfwjs鉴定。 ``` js const Service = require('egg').Service; const tf = require('@tensorflow/tfjs-node'); const { Image } = require('image-js'); // 官方到例子用'jpeg-js',自己测试png格式会报错,所以改用'image-js' const contentBoundary = require('../util/contentBoundary'); const convert = async img => { const image = await Image.load(img.filepath); const numChannels = 3; const numPixels = image.width * image.height; const values = new Int32Array(numPixels * numChannels); for (let i = 0; i < numPixels; i++) { for (let c = 0; c < numChannels; ++c) { values[i * numChannels + c] = image.data[i * 4 + c]; } } return tf.tensor3d(values, [image.height, image.width, numChannels], 'int32'); }; class ContentCheckService extends Service { async contentCheck(img) { // img 为上传的file try { const image = await convert(img); const predictions = await this.app.nsfwModel.classify(image); // 图片鉴定 console.log('Predictions: ', predictions); const isSafeContent = contentBoundary(predictions); return isSafeContent; } catch (err) { throw new Error(err); } } } module.exports = ContentCheckService; ``` ``` js // ../util/contentBoundary.js const safeContent = ['Drawing', 'Neutral']; // 设置图片内容安全的类型 const contentBoundary = predictions => { let safeProbability = 0; for (let index = 0; index < predictions.length; index++) { const item = predictions[index]; const className = item.className; const probability = item.probability; if (safeContent.includes(className)) { safeProbability += probability; } } return safeProbability > 0.5; }; module.exports = contentBoundary; ``` # 5. 测试 服务部署好后测试一下。 图一: ![web_model](/images/N8co23tdCfLVHk2m.png) 测试结果: ![web_model](/images/4666be0b-43c7-472b-9adc-8364d461bc4d.png) check返回true,表示该图是安全的。 图二: ![web_model](/images/ksdfdksfdsddacsfsvsd.jpeg) 测试结果: ![web_model](/images/WX20210510-164355.png) check返回false,表示该图是不安全,可能涉黄。 *因为我在代码里设置里Drawing和Neutral类型的才是安全的图片,Hentai、Porn、Sexy类型都认为是不安全,可能涉黄* node+nsfwjs搭建图片鉴黄服务的大概思路就是这样,期间经历过不少曲折,就不一一赘述了。