去年由于业务的需要,对图片的内容安全做一定的监控,简单的说就是图片鉴黄,由于早前就体验过nsfwjs的强大,所以第一时间就想到用它搭建一个图片鉴黄的服务。
翻看了github上nsfwjs的文档,也有node相关的例子,原本会以为比较顺利的可以搭建完成,没想到后面还是经历了一波三折,所以也就有了这篇文章,记录一下自己的心路历程。
1. 服务框架搭建
node框架选择很多,我这里选了egg,因为egg集成了很多丰富的能力,简单省事,并且图片鉴黄,必定会有上传,egg也很好的集成了上传模块,简单配置一下即可放心使用。
egg的搭建以及上传的配置,就不展开了,参考官方文档即可。
egg官方文档
egg上传
node版本建议选用 14.x 及以上版本,反正我在centOS 7.x 环境下吃过亏了。
2. 安装
1 2
| npm install nsfwjs npm install @tensorflow/tfjs-node
|
3. 加载模型
官方文档给出的例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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下载模型到本地(选择第一个nsfw_mobilenet_v2_140_224.zip
即可),然后解压zip包。web_model文件夹里面就是我们需要的模型了。

然后把web_model里面的文件,放到egg项目下的app/model目录下,然后在项目根目录下增加一个app.js文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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鉴定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| const Service = require('egg').Service; const tf = require('@tensorflow/tfjs-node'); const { Image } = require('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) { 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;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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. 测试
服务部署好后测试一下。
图一:

测试结果:

check返回true,表示该图是安全的。
图二:

测试结果:

check返回false,表示该图是不安全,可能涉黄。
因为我在代码里设置里Drawing和Neutral类型的才是安全的图片,Hentai、Porn、Sexy类型都认为是不安全,可能涉黄
node+nsfwjs搭建图片鉴黄服务的大概思路就是这样,期间经历过不少曲折,就不一一赘述了。