2018-09-14 10:44:15 +00:00
|
|
|
package photoprism
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2019-04-30 11:17:01 +00:00
|
|
|
"bytes"
|
2018-09-14 10:44:15 +00:00
|
|
|
"errors"
|
2019-04-30 11:17:01 +00:00
|
|
|
"image"
|
2018-09-14 10:44:15 +00:00
|
|
|
"io/ioutil"
|
2019-04-30 11:17:01 +00:00
|
|
|
"math"
|
2018-09-14 10:44:15 +00:00
|
|
|
"os"
|
|
|
|
"sort"
|
2018-10-31 06:14:33 +00:00
|
|
|
|
2019-04-30 11:17:01 +00:00
|
|
|
"github.com/disintegration/imaging"
|
2019-05-04 15:34:51 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2018-10-31 06:14:33 +00:00
|
|
|
tf "github.com/tensorflow/tensorflow/tensorflow/go"
|
2018-09-14 10:44:15 +00:00
|
|
|
)
|
|
|
|
|
2018-11-01 16:01:45 +00:00
|
|
|
// TensorFlow if a tensorflow wrapper given a graph, labels and a modelPath.
|
2018-09-14 10:44:15 +00:00
|
|
|
type TensorFlow struct {
|
|
|
|
modelPath string
|
2019-04-30 11:17:01 +00:00
|
|
|
model *tf.SavedModel
|
2018-09-17 16:40:57 +00:00
|
|
|
labels []string
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|
|
|
|
|
2018-11-01 16:01:45 +00:00
|
|
|
// NewTensorFlow returns a new TensorFlow.
|
2018-09-14 10:44:15 +00:00
|
|
|
func NewTensorFlow(tensorFlowModelPath string) *TensorFlow {
|
2018-09-17 16:40:57 +00:00
|
|
|
return &TensorFlow{modelPath: tensorFlowModelPath}
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|
|
|
|
|
2018-11-01 16:01:45 +00:00
|
|
|
// TensorFlowLabel defines a Json struct with label and probability.
|
2018-09-14 10:44:15 +00:00
|
|
|
type TensorFlowLabel struct {
|
|
|
|
Label string `json:"label"`
|
|
|
|
Probability float32 `json:"probability"`
|
|
|
|
}
|
|
|
|
|
2019-04-30 11:17:01 +00:00
|
|
|
func (a *TensorFlowLabel) Percent() int {
|
|
|
|
return int(math.Round(float64(a.Probability * 100)))
|
|
|
|
}
|
|
|
|
|
2018-11-01 16:01:45 +00:00
|
|
|
// TensorFlowLabels is a slice of tensorflow labels.
|
2018-09-14 10:44:15 +00:00
|
|
|
type TensorFlowLabels []TensorFlowLabel
|
|
|
|
|
|
|
|
func (a TensorFlowLabels) Len() int { return len(a) }
|
|
|
|
func (a TensorFlowLabels) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
func (a TensorFlowLabels) Less(i, j int) bool { return a[i].Probability > a[j].Probability }
|
|
|
|
|
2018-12-21 00:55:45 +00:00
|
|
|
// GetImageTagsFromFile returns tags for a jpeg image file.
|
2018-09-14 10:44:15 +00:00
|
|
|
func (t *TensorFlow) GetImageTagsFromFile(filename string) (result []TensorFlowLabel, err error) {
|
|
|
|
imageBuffer, err := ioutil.ReadFile(filename)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-04-30 11:17:01 +00:00
|
|
|
return t.GetImageTags(imageBuffer)
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|
|
|
|
|
2018-12-21 00:55:45 +00:00
|
|
|
// GetImageTags returns tags for a jpeg image string.
|
2019-04-30 11:17:01 +00:00
|
|
|
func (t *TensorFlow) GetImageTags(img []byte) (result []TensorFlowLabel, err error) {
|
2018-09-14 10:44:15 +00:00
|
|
|
if err := t.loadModel(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make tensor
|
2019-04-30 11:17:01 +00:00
|
|
|
tensor, err := t.makeTensorFromImage(img, "jpeg")
|
2018-09-14 10:44:15 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.New("invalid image")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run inference
|
2019-04-30 11:17:01 +00:00
|
|
|
output, err := t.model.Session.Run(
|
2018-09-14 10:44:15 +00:00
|
|
|
map[tf.Output]*tf.Tensor{
|
2019-04-30 11:17:01 +00:00
|
|
|
t.model.Graph.Operation("input_1").Output(0): tensor,
|
2018-09-14 10:44:15 +00:00
|
|
|
},
|
|
|
|
[]tf.Output{
|
2019-04-30 11:17:01 +00:00
|
|
|
t.model.Graph.Operation("predictions/Softmax").Output(0),
|
2018-09-14 10:44:15 +00:00
|
|
|
},
|
|
|
|
nil)
|
|
|
|
|
|
|
|
if err != nil {
|
2019-04-30 11:17:01 +00:00
|
|
|
return result, errors.New("could not run inference")
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|
|
|
|
|
2019-04-30 11:17:01 +00:00
|
|
|
if len(output) < 1 {
|
|
|
|
return result, errors.New("result is empty")
|
|
|
|
}
|
|
|
|
|
2018-09-14 10:44:15 +00:00
|
|
|
// Return best labels
|
2019-05-04 15:34:51 +00:00
|
|
|
result = t.findBestLabels(output[0].Value().([][]float32)[0])
|
|
|
|
|
|
|
|
log.Debugf("labels: %v", result)
|
|
|
|
|
|
|
|
return result, nil
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (t *TensorFlow) loadModel() error {
|
2019-05-04 15:34:51 +00:00
|
|
|
if t.model != nil {
|
2018-09-14 10:44:15 +00:00
|
|
|
// Already loaded
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-05-04 15:34:51 +00:00
|
|
|
savedModel := t.modelPath + "/nasnet"
|
|
|
|
modelLabels := savedModel + "/labels.txt"
|
|
|
|
|
|
|
|
log.Infof("loading image classification model from \"%s\"", savedModel)
|
|
|
|
|
2019-04-30 11:17:01 +00:00
|
|
|
// Load model
|
2019-05-04 15:34:51 +00:00
|
|
|
model, err := tf.LoadSavedModel(savedModel, []string{"photoprism"}, nil)
|
2019-04-30 11:17:01 +00:00
|
|
|
|
2018-09-14 10:44:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-04-30 11:17:01 +00:00
|
|
|
|
|
|
|
t.model = model
|
2018-09-14 10:44:15 +00:00
|
|
|
|
2019-05-04 15:34:51 +00:00
|
|
|
log.Infof("loading classification labels from \"%s\"", modelLabels)
|
|
|
|
|
2018-09-14 10:44:15 +00:00
|
|
|
// Load labels
|
2019-05-04 15:34:51 +00:00
|
|
|
f, err := os.Open(modelLabels)
|
|
|
|
|
2018-09-14 10:44:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-04-30 11:17:01 +00:00
|
|
|
|
2019-05-04 15:34:51 +00:00
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(f)
|
2018-09-14 10:44:15 +00:00
|
|
|
|
|
|
|
// Labels are separated by newlines
|
|
|
|
for scanner.Scan() {
|
|
|
|
t.labels = append(t.labels, scanner.Text())
|
|
|
|
}
|
2019-05-04 15:34:51 +00:00
|
|
|
|
2018-09-14 10:44:15 +00:00
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-05-04 15:34:51 +00:00
|
|
|
|
2018-09-14 10:44:15 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *TensorFlow) findBestLabels(probabilities []float32) []TensorFlowLabel {
|
|
|
|
// Make a list of label/probability pairs
|
2019-05-04 15:34:51 +00:00
|
|
|
var result []TensorFlowLabel
|
2018-09-14 10:44:15 +00:00
|
|
|
for i, p := range probabilities {
|
|
|
|
if i >= len(t.labels) {
|
|
|
|
break
|
|
|
|
}
|
2019-05-04 15:34:51 +00:00
|
|
|
|
2019-05-06 21:18:10 +00:00
|
|
|
if p < 0.08 {
|
|
|
|
continue
|
|
|
|
}
|
2019-05-04 15:34:51 +00:00
|
|
|
|
|
|
|
result = append(result, TensorFlowLabel{Label: t.labels[i], Probability: p})
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Sort by probability
|
2019-05-04 15:34:51 +00:00
|
|
|
sort.Sort(TensorFlowLabels(result))
|
|
|
|
|
|
|
|
l := len(result)
|
2018-09-14 10:44:15 +00:00
|
|
|
|
2019-05-04 15:34:51 +00:00
|
|
|
if l >= 5 {
|
|
|
|
return result[:5]
|
|
|
|
} else {
|
|
|
|
return result[:l]
|
|
|
|
}
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|
|
|
|
|
2019-04-30 11:17:01 +00:00
|
|
|
func (t *TensorFlow) makeTensorFromImage(image []byte, imageFormat string) (*tf.Tensor, error) {
|
2019-05-01 12:54:11 +00:00
|
|
|
img, err := imaging.Decode(bytes.NewReader(image), imaging.AutoOrientation(true))
|
2019-04-30 11:17:01 +00:00
|
|
|
|
2018-09-14 10:44:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-04-30 11:17:01 +00:00
|
|
|
|
|
|
|
width, height := 224, 224
|
|
|
|
|
2019-05-13 16:01:50 +00:00
|
|
|
img = imaging.Fill(img, width, height, imaging.Center, imaging.Lanczos)
|
2019-04-30 11:17:01 +00:00
|
|
|
|
|
|
|
return imageToTensorTF(img, width, height)
|
|
|
|
}
|
|
|
|
|
|
|
|
func imageToTensorTF(img image.Image, imageHeight, imageWidth int) (*tf.Tensor, error) {
|
|
|
|
var tfImage [1][][][3]float32
|
|
|
|
|
|
|
|
for j := 0; j < imageHeight; j++ {
|
|
|
|
tfImage[0] = append(tfImage[0], make([][3]float32, imageWidth))
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|
2019-04-30 11:17:01 +00:00
|
|
|
|
|
|
|
for i := 0; i < imageWidth; i++ {
|
|
|
|
for j := 0; j < imageHeight; j++ {
|
|
|
|
r, g, b, _ := img.At(i, j).RGBA()
|
|
|
|
tfImage[0][j][i][0] = convertTF(r)
|
|
|
|
tfImage[0][j][i][1] = convertTF(g)
|
|
|
|
tfImage[0][j][i][2] = convertTF(b)
|
|
|
|
}
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|
2019-04-30 11:17:01 +00:00
|
|
|
|
|
|
|
return tf.NewTensor(tfImage)
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|
|
|
|
|
2019-04-30 11:17:01 +00:00
|
|
|
func convertTF(value uint32) float32 {
|
|
|
|
return (float32(value>>8) - float32(127.5)) / float32(127.5)
|
2018-09-14 10:44:15 +00:00
|
|
|
}
|