去年由于业务的需要,对图片的内容安全做一定的监控,简单的说就是图片鉴黄,由于早前就体验过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

然后把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'); // 官方到例子用'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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ../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

测试结果:

web_model

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

图二:

web_model

测试结果:

web_model

check返回false,表示该图是不安全,可能涉黄。
因为我在代码里设置里Drawing和Neutral类型的才是安全的图片,Hentai、Porn、Sexy类型都认为是不安全,可能涉黄

node+nsfwjs搭建图片鉴黄服务的大概思路就是这样,期间经历过不少曲折,就不一一赘述了。