2019-12-14 15:41:37 +01:00
|
|
|
package nsfw
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2019-12-15 17:19:16 +01:00
|
|
|
"fmt"
|
2019-12-14 15:41:37 +01:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2020-01-02 06:16:58 +01:00
|
|
|
"sync"
|
2019-12-14 15:41:37 +01:00
|
|
|
|
2020-01-12 14:00:56 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/fs"
|
2020-05-03 18:00:50 +02:00
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
2019-12-14 15:41:37 +01:00
|
|
|
tf "github.com/tensorflow/tensorflow/tensorflow/go"
|
|
|
|
"github.com/tensorflow/tensorflow/tensorflow/go/op"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Detector uses TensorFlow to label drawing, hentai, neutral, porn and sexy images.
|
|
|
|
type Detector struct {
|
|
|
|
model *tf.SavedModel
|
|
|
|
modelPath string
|
|
|
|
modelTags []string
|
|
|
|
labels []string
|
2020-01-02 06:16:58 +01:00
|
|
|
mutex sync.Mutex
|
2019-12-14 15:41:37 +01:00
|
|
|
}
|
|
|
|
|
2020-01-09 01:21:09 +01:00
|
|
|
// New returns a new detector instance.
|
|
|
|
func New(modelPath string) *Detector {
|
2019-12-14 15:41:37 +01:00
|
|
|
return &Detector{modelPath: modelPath, modelTags: []string{"serve"}}
|
|
|
|
}
|
|
|
|
|
2020-01-09 01:21:09 +01:00
|
|
|
// File returns matching labels for a jpeg media file.
|
|
|
|
func (t *Detector) File(filename string) (result Labels, err error) {
|
2020-01-12 14:00:56 +01:00
|
|
|
if fs.MimeType(filename) != "image/jpeg" {
|
2020-05-03 18:00:50 +02:00
|
|
|
return result, fmt.Errorf("nsfw: %s is not a jpeg file", txt.Quote(filepath.Base(filename)))
|
2019-12-15 17:19:16 +01:00
|
|
|
}
|
|
|
|
|
2019-12-14 15:41:37 +01:00
|
|
|
imageBuffer, err := ioutil.ReadFile(filename)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return t.Labels(imageBuffer)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Labels returns matching labels for a jpeg media string.
|
|
|
|
func (t *Detector) Labels(img []byte) (result Labels, err error) {
|
|
|
|
if err := t.loadModel(); err != nil {
|
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make tensor
|
2020-07-14 18:00:32 +02:00
|
|
|
tensor, err := createTensorFromImage(img, "jpeg")
|
2019-12-14 15:41:37 +01:00
|
|
|
|
|
|
|
if err != nil {
|
2020-07-14 18:00:32 +02:00
|
|
|
return result, fmt.Errorf("nsfw: %s (create tensor)", err.Error())
|
2019-12-14 15:41:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Run inference
|
|
|
|
output, err := t.model.Session.Run(
|
|
|
|
map[tf.Output]*tf.Tensor{
|
|
|
|
t.model.Graph.Operation("input_tensor").Output(0): tensor,
|
|
|
|
},
|
|
|
|
[]tf.Output{
|
|
|
|
t.model.Graph.Operation("nsfw_cls_model/final_prediction").Output(0),
|
|
|
|
},
|
|
|
|
nil)
|
|
|
|
|
|
|
|
if err != nil {
|
2020-07-14 18:00:32 +02:00
|
|
|
return result, fmt.Errorf("nsfw: %s (run inference)", err.Error())
|
2019-12-14 15:41:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(output) < 1 {
|
2020-07-14 18:00:32 +02:00
|
|
|
return result, fmt.Errorf("nsfw: inference failed, no output")
|
2019-12-14 15:41:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return best labels
|
|
|
|
result = t.getLabels(output[0].Value().([][]float32)[0])
|
|
|
|
|
2020-07-14 18:00:32 +02:00
|
|
|
log.Debugf("nsfw: image classified as %+v", result)
|
2019-12-14 15:41:37 +01:00
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Detector) loadLabels(path string) error {
|
|
|
|
modelLabels := path + "/labels.txt"
|
|
|
|
|
2020-07-14 18:00:32 +02:00
|
|
|
log.Infof("nsfw: loading labels from labels.txt")
|
2019-12-14 15:41:37 +01:00
|
|
|
|
|
|
|
// Load labels
|
|
|
|
f, err := os.Open(modelLabels)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
|
|
|
|
|
|
// Labels are separated by newlines
|
|
|
|
for scanner.Scan() {
|
|
|
|
t.labels = append(t.labels, scanner.Text())
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Detector) loadModel() error {
|
2020-01-02 06:16:58 +01:00
|
|
|
t.mutex.Lock()
|
|
|
|
defer t.mutex.Unlock()
|
|
|
|
|
2019-12-14 15:41:37 +01:00
|
|
|
if t.model != nil {
|
|
|
|
// Already loaded
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-14 18:00:32 +02:00
|
|
|
log.Infof("nsfw: loading model from %s", txt.Quote(filepath.Base(t.modelPath)))
|
2019-12-14 15:41:37 +01:00
|
|
|
|
|
|
|
// Load model
|
|
|
|
model, err := tf.LoadSavedModel(t.modelPath, t.modelTags, nil)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
t.model = model
|
|
|
|
|
|
|
|
return t.loadLabels(t.modelPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Detector) getLabels(p []float32) Labels {
|
|
|
|
return Labels{
|
|
|
|
Drawing: p[0],
|
|
|
|
Hentai: p[1],
|
|
|
|
Neutral: p[2],
|
|
|
|
Porn: p[3],
|
|
|
|
Sexy: p[4],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-14 18:00:32 +02:00
|
|
|
func transformImageGraph(imageFormat string) (graph *tf.Graph, input, output tf.Output, err error) {
|
2019-12-14 15:41:37 +01:00
|
|
|
const (
|
|
|
|
H, W = 224, 224
|
|
|
|
Mean = float32(117)
|
|
|
|
Scale = float32(1)
|
|
|
|
)
|
|
|
|
s := op.NewScope()
|
|
|
|
input = op.Placeholder(s, tf.String)
|
|
|
|
// Decode PNG or JPEG
|
|
|
|
var decode tf.Output
|
|
|
|
if imageFormat == "png" {
|
|
|
|
decode = op.DecodePng(s, input, op.DecodePngChannels(3))
|
|
|
|
} else {
|
|
|
|
decode = op.DecodeJpeg(s, input, op.DecodeJpegChannels(3))
|
|
|
|
}
|
|
|
|
// Div and Sub perform (value-Mean)/Scale for each pixel
|
|
|
|
output = op.Div(s,
|
|
|
|
op.Sub(s,
|
|
|
|
// Resize to 224x224 with bilinear interpolation
|
|
|
|
op.ResizeBilinear(s,
|
|
|
|
// Create a batch containing a single image
|
|
|
|
op.ExpandDims(s,
|
|
|
|
// Use decoded pixel values
|
|
|
|
op.Cast(s, decode, tf.Float),
|
|
|
|
op.Const(s.SubScope("make_batch"), int32(0))),
|
|
|
|
op.Const(s.SubScope("size"), []int32{H, W})),
|
|
|
|
op.Const(s.SubScope("mean"), Mean)),
|
|
|
|
op.Const(s.SubScope("scale"), Scale))
|
|
|
|
graph, err = s.Finalize()
|
|
|
|
return graph, input, output, err
|
|
|
|
}
|
|
|
|
|
2020-07-14 18:00:32 +02:00
|
|
|
func createTensorFromImage(image []byte, imageFormat string) (*tf.Tensor, error) {
|
2019-12-14 15:41:37 +01:00
|
|
|
tensor, err := tf.NewTensor(string(image))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-07-14 18:00:32 +02:00
|
|
|
graph, input, output, err := transformImageGraph(imageFormat)
|
2019-12-14 15:41:37 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
session, err := tf.NewSession(graph, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer session.Close()
|
|
|
|
normalized, err := session.Run(
|
|
|
|
map[tf.Output]*tf.Tensor{input: tensor},
|
|
|
|
[]tf.Output{output},
|
|
|
|
nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return normalized[0], nil
|
|
|
|
}
|