Moved all assets to assets/ and improved config

This commit is contained in:
Michael Mayer 2018-09-14 12:44:15 +02:00
parent 31562d43cb
commit 609778e7d6
37 changed files with 439 additions and 355 deletions

View file

@ -1,3 +1,11 @@
photos/
assets/photos/*
assets/thumbnails/*
assets/testdata/import
assets/testdata/originals
assets/testdata/thumbnails
frontend/node_modules/*
server/assets/public/build/*
assets/public/build/*
Dockerfile
photoprism
docker-compose*
.travis.yml

19
.gitignore vendored
View file

@ -1,11 +1,17 @@
# Application files and directories
/photoprism
assets/photos/originals/*
assets/photos/import/*
assets/photos/export/*
frontend/node_modules/*
*.log
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
/*.zip
/photoprism
vendor/
# Test binary, build with `go test -c`
*.test
@ -15,11 +21,6 @@ vendor/
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
# Compiled, config and log files
*.log
config.yml
/tweethog
# Generated files
.DS_Store
.DS_Store?
@ -35,6 +36,4 @@ Thumbs.db
.c9revisions
.settings
.swp
.tmp
/photos/
node_modules/
.tmp

View file

@ -102,24 +102,14 @@ ENV PATH $GOBIN:/usr/local/go/bin:$PATH
ENV GO111MODULE on
ENV NODE_ENV production
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" /etc/photoprism /var/photos && chmod -R 777 "$GOPATH"
# Download InceptionV3 model
RUN mkdir -p /model && \
wget "https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip" -O /model/inception.zip && \
unzip /model/inception.zip -d /model && \
chmod -R 777 /model
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
# Set up project directory
WORKDIR "/go/src/github.com/photoprism/photoprism"
COPY . .
RUN cp config.prod.yml /etc/photoprism/config.yml
# Build PhotoPrism
RUN make dep js install
RUN cp -r server/assets /etc/photoprism
RUN make all install
# Expose HTTP port
EXPOSE 80

View file

@ -9,9 +9,15 @@ GOGET=$(GOCMD) get
GOFMT=$(GOCMD) fmt
BINARY_NAME=photoprism
all: dep js build
install:
all: tensorflow-model dep js build
install: install-bin install-assets install-config
install-bin:
$(GOINSTALL) cmd/photoprism/photoprism.go
install-assets:
cp -r assets /var/photoprism
install-config:
mkdir -p /etc/photoprism
cp config.prod.yml /etc/photoprism/config.yml
build:
$(GOBUILD) cmd/photoprism/photoprism.go
js:
@ -26,6 +32,8 @@ test:
clean:
$(GOCLEAN)
rm -f $(BINARY_NAME)
tensorflow-model:
scripts/download-tf-model.sh
image:
docker build . --tag photoprism/photoprism
docker push photoprism/photoprism

View file

@ -21,7 +21,7 @@ Originals are stored in the file system in a structured way for easy backup and
Our goal is to provide the following features (tested as a proof-of-concept):
- High-performance command line tool
- [Web frontend](docs/img/screenshot.jpg)
- [Web frontend](assets/docs/img/screenshot.jpg)
- No proprietary or binary data formats
- Automatic RAW to JPEG conversion
- Duplicate detection (JPEG and RAW can be used simultaneously)
@ -35,7 +35,7 @@ Web Frontend
Open a terminal an type `photoprism start` to start the built-in server. It will listen on port 80 by default.
The UI is based on [Vuetify](https://vuetifyjs.com/en/), a [Material Design](https://material.io/) component framework for Vue.js 2.
![](docs/img/screenshot.jpg)
![](assets/docs/img/screenshot.jpg)
Setup
-----
@ -73,4 +73,4 @@ See [Quick and easy guide for migrating to Go 1.11 modules](https://blog.liquidb
Concept
-------
![](docs/img/concept.jpg)
![](assets/docs/img/concept.jpg)

View file

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 330 KiB

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 510 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

2
assets/tensorflow/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

2
assets/testdata/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

2
assets/thumbnails/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -10,8 +10,6 @@ import (
)
func main() {
conf := photoprism.NewConfig()
app := cli.NewApp()
app.Name = "PhotoPrism"
app.Usage = "Digital Photo Archive"
@ -22,9 +20,7 @@ func main() {
Name: "config",
Usage: "Displays global configuration values",
Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file")))
conf.SetValuesFromCliContext(context)
conf := photoprism.NewConfig(context)
fmt.Printf("NAME VALUE\n")
fmt.Printf("debug %t\n", conf.Debug)
@ -32,12 +28,14 @@ func main() {
fmt.Printf("server-ip %s\n", conf.ServerIP)
fmt.Printf("server-port %d\n", conf.ServerPort)
fmt.Printf("server-mode %s\n", conf.ServerMode)
fmt.Printf("server-assets-path %s\n", conf.ServerAssetsPath)
fmt.Printf("darktable-cli %s\n", conf.DarktableCli)
fmt.Printf("assets-path %s\n", conf.AssetsPath)
fmt.Printf("originals-path %s\n", conf.OriginalsPath)
fmt.Printf("thumbnails-path %s\n", conf.ThumbnailsPath)
fmt.Printf("import-path %s\n", conf.ImportPath)
fmt.Printf("export-path %s\n", conf.ExportPath)
fmt.Printf("darktable-cli %s\n", conf.DarktableCli)
fmt.Printf("database-driver %s\n", conf.DatabaseDriver)
fmt.Printf("database-dsn %s\n", conf.DatabaseDsn)
return nil
},
@ -63,9 +61,7 @@ func main() {
},
},
Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file")))
conf.SetValuesFromCliContext(context)
conf := photoprism.NewConfig(context)
if context.IsSet("server-ip") {
conf.ServerIP = context.String("server-ip")
@ -96,9 +92,7 @@ func main() {
Name: "migrate-db",
Usage: "Automatically migrates / initializes database",
Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file")))
conf.SetValuesFromCliContext(context)
conf := photoprism.NewConfig(context)
fmt.Println("Migrating database...")
@ -113,9 +107,7 @@ func main() {
Name: "import",
Usage: "Imports photos",
Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file")))
conf.SetValuesFromCliContext(context)
conf := photoprism.NewConfig(context)
conf.CreateDirectories()
@ -123,7 +115,9 @@ func main() {
fmt.Printf("Importing photos from %s...\n", conf.ImportPath)
indexer := photoprism.NewIndexer(conf.OriginalsPath, conf.GetDb())
tensorFlow := photoprism.NewTensorFlow(conf.GetTensorFlowModelPath())
indexer := photoprism.NewIndexer(conf.OriginalsPath, tensorFlow, conf.GetDb())
importer := photoprism.NewImporter(conf.OriginalsPath, indexer)
@ -138,9 +132,7 @@ func main() {
Name: "index",
Usage: "Re-indexes all originals",
Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file")))
conf.SetValuesFromCliContext(context)
conf := photoprism.NewConfig(context)
conf.CreateDirectories()
@ -148,7 +140,9 @@ func main() {
fmt.Printf("Indexing photos in %s...\n", conf.OriginalsPath)
indexer := photoprism.NewIndexer(conf.OriginalsPath, conf.GetDb())
tensorFlow := photoprism.NewTensorFlow(conf.GetTensorFlowModelPath())
indexer := photoprism.NewIndexer(conf.OriginalsPath, tensorFlow, conf.GetDb())
indexer.IndexAll()
@ -161,9 +155,7 @@ func main() {
Name: "convert",
Usage: "Converts RAW originals to JPEG",
Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file")))
conf.SetValuesFromCliContext(context)
conf := photoprism.NewConfig(context)
conf.CreateDirectories()
@ -196,9 +188,7 @@ func main() {
},
},
Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file")))
conf.SetValuesFromCliContext(context)
conf := photoprism.NewConfig(context)
conf.CreateDirectories()
@ -247,9 +237,7 @@ func main() {
},
},
Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file")))
conf.SetValuesFromCliContext(context)
conf := photoprism.NewConfig(context)
conf.CreateDirectories()
@ -313,27 +301,27 @@ var globalCliFlags = []cli.Flag{
cli.StringFlag{
Name: "originals-path",
Usage: "originals path",
Value: "/var/photoprism/originals",
Value: "/var/photoprism/photos/originals",
},
cli.StringFlag{
Name: "thumbnails-path",
Usage: "thumbnails path",
Value: "/var/photoprism/thumbnails",
Value: "/var/photoprism/photos/thumbnails",
},
cli.StringFlag{
Name: "import-path",
Usage: "import path",
Value: "/var/photoprism/import",
Value: "/var/photoprism/photos/import",
},
cli.StringFlag{
Name: "export-path",
Usage: "export path",
Value: "/var/photoprism/export",
Value: "/var/photoprism/photos/export",
},
cli.StringFlag{
Name: "server-assets-path",
Usage: "server assets path for templates, js and css",
Value: "/var/photoprism/server",
Name: "assets-path",
Usage: "assets path",
Value: "/var/photoprism",
},
cli.StringFlag{
Name: "database-driver",

View file

@ -1,12 +1,12 @@
debug: true
darktable-cli: /usr/bin/darktable-cli
originals-path: photos/originals
thumbnails-path: photos/thumbnails
import-path: photos/import
export-path: photos/export
assets-path: assets
thumbnails-path: assets/thumbnails
originals-path: assets/photos/originals
import-path: assets/photos/import
export-path: assets/photos/export
server-ip:
server-mode: debug
server-port: 80
server-assets-path: server/assets
database-driver: mysql
database-dsn: photoprism:photoprism@tcp(database:3306)/photoprism?parseTime=true

View file

@ -15,26 +15,30 @@ import (
)
type Config struct {
Debug bool
ConfigFile string
ServerIP string
ServerPort int
ServerMode string
ServerAssetsPath string
DarktableCli string
OriginalsPath string
ThumbnailsPath string
ImportPath string
ExportPath string
DatabaseDriver string
DatabaseDsn string
db *gorm.DB
Debug bool
ConfigFile string
ServerIP string
ServerPort int
ServerMode string
AssetsPath string
ThumbnailsPath string
OriginalsPath string
ImportPath string
ExportPath string
DarktableCli string
DatabaseDriver string
DatabaseDsn string
db *gorm.DB
}
type ConfigValues map[string]interface{}
func NewConfig() *Config {
return &Config{}
func NewConfig(context *cli.Context) *Config {
c := &Config{}
c.SetValuesFromFile(GetExpandedFilename(context.GlobalString("config-file")))
c.SetValuesFromCliContext(context)
return c
}
func (c *Config) SetValuesFromFile(fileName string) error {
@ -62,18 +66,18 @@ func (c *Config) SetValuesFromFile(fileName string) error {
c.ServerMode = serverMode
}
if serverAssetsPath, err := yamlConfig.Get("server-assets-path"); err == nil {
c.ServerAssetsPath = GetExpandedFilename(serverAssetsPath)
}
if originalsPath, err := yamlConfig.Get("originals-path"); err == nil {
c.OriginalsPath = GetExpandedFilename(originalsPath)
if assetsPath, err := yamlConfig.Get("assets-path"); err == nil {
c.AssetsPath = GetExpandedFilename(assetsPath)
}
if thumbnailsPath, err := yamlConfig.Get("thumbnails-path"); err == nil {
c.ThumbnailsPath = GetExpandedFilename(thumbnailsPath)
}
if originalsPath, err := yamlConfig.Get("originals-path"); err == nil {
c.OriginalsPath = GetExpandedFilename(originalsPath)
}
if importPath, err := yamlConfig.Get("import-path"); err == nil {
c.ImportPath = GetExpandedFilename(importPath)
}
@ -102,14 +106,18 @@ func (c *Config) SetValuesFromCliContext(context *cli.Context) error {
c.Debug = context.GlobalBool("debug")
}
if context.GlobalIsSet("originals-path") {
c.OriginalsPath = GetExpandedFilename(context.GlobalString("originals-path"))
if context.GlobalIsSet("assets-path") {
c.AssetsPath = GetExpandedFilename(context.GlobalString("assets-path"))
}
if context.GlobalIsSet("thumbnails-path") {
c.ThumbnailsPath = GetExpandedFilename(context.GlobalString("thumbnails-path"))
}
if context.GlobalIsSet("originals-path") {
c.OriginalsPath = GetExpandedFilename(context.GlobalString("originals-path"))
}
if context.GlobalIsSet("import-path") {
c.ImportPath = GetExpandedFilename(context.GlobalString("import-path"))
}
@ -118,10 +126,6 @@ func (c *Config) SetValuesFromCliContext(context *cli.Context) error {
c.ExportPath = GetExpandedFilename(context.GlobalString("export-path"))
}
if context.GlobalIsSet("server-assets-path") {
c.ServerAssetsPath = GetExpandedFilename(context.GlobalString("server-assets-path"))
}
if context.GlobalIsSet("darktable-cli") {
c.DarktableCli = GetExpandedFilename(context.GlobalString("darktable-cli"))
}
@ -168,6 +172,10 @@ func (c *Config) ConnectToDatabase() error {
return err
}
func (c *Config) GetTensorFlowModelPath() string {
return c.AssetsPath + "/tensorflow"
}
func (c *Config) GetDb() *gorm.DB {
if c.db == nil {
c.ConnectToDatabase()

View file

@ -1,12 +1,12 @@
debug: false
darktable-cli: /Applications/darktable.app/Contents/MacOS/darktable-cli
originals-path: ~/Photos/Originals
thumbnails-path: ~/Photos/Thumbnails
import-path: ~/Photos/Import
export-path: ~/Photos/Export
assets-path: /var/photoprism
thumbnails-path: /var/photoprism/thumbnails
originals-path: ~/Pictures/Originals
import-path: ~/Pictures/Import
export-path: ~/Pictures/Export
server-ip:
server-mode: release
server-port: 8080
server-assets-path: ~/Photos/Server
database-driver: mysql
database-dsn: photoprism:photoprism@tcp(localhost:3306)/photoprism?parseTime=true

View file

@ -1,12 +1,12 @@
debug: false
darktable-cli: /usr/bin/darktable-cli
originals-path: /var/photos/originals
thumbnails-path: /var/photos/thumbnails
import-path: /var/photos/import
export-path: /var/photos/export
assets-path: /var/photoprism
thumbnails-path: /var/photoprism/thumbnails
originals-path: /var/photoprism/photos/originals
import-path: /var/photoprism/photos/import
export-path: /var/photoprism/photos/export
server-ip:
server-mode: release
server-port: 80
server-assets-path: /etc/photoprism/assets
database-driver: mysql
database-dsn: photoprism:photoprism@tcp(database:3306)/photoprism?parseTime=true

View file

@ -1,24 +1,26 @@
package photoprism
import (
"flag"
"fmt"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli"
"os"
"testing"
)
const testDataPath = "testdata"
const testDataPath = "assets/testdata"
const testDataUrl = "https://www.dropbox.com/s/na9p9wwt98l7m5b/import.zip?dl=1"
const testDataHash = "ed3bdb2fe86ea662bc863b63e219b47b8d9a74024757007f7979887d"
var darktableCli = "/usr/bin/darktable-cli"
var testDataZip = GetExpandedFilename(testDataPath + "/import.zip")
var originalsPath = GetExpandedFilename(testDataPath + "/originals")
var assetsPath = GetExpandedFilename("assets")
var thumbnailsPath = GetExpandedFilename(testDataPath + "/thumbnails")
var originalsPath = GetExpandedFilename(testDataPath + "/originals")
var importPath = GetExpandedFilename(testDataPath + "/import")
var exportPath = GetExpandedFilename(testDataPath + "/export")
var serverAssetsPath = GetExpandedFilename("server/assets")
var databaseDriver = "mysql"
var databaseDsn = "photoprism:photoprism@tcp(database:3306)/photoprism?parseTime=true"
@ -66,34 +68,53 @@ func (c *Config) InitializeTestData(t *testing.T) {
func NewTestConfig() *Config {
return &Config{
Debug: false,
DarktableCli: darktableCli,
OriginalsPath: originalsPath,
ThumbnailsPath: thumbnailsPath,
ImportPath: importPath,
ExportPath: exportPath,
ServerAssetsPath: serverAssetsPath,
DatabaseDriver: databaseDriver,
DatabaseDsn: databaseDsn,
Debug: false,
AssetsPath: assetsPath,
ThumbnailsPath: thumbnailsPath,
OriginalsPath: originalsPath,
ImportPath: importPath,
ExportPath: exportPath,
DarktableCli: darktableCli,
DatabaseDriver: databaseDriver,
DatabaseDsn: databaseDsn,
}
}
func getTestCliContext() *cli.Context {
globalSet := flag.NewFlagSet("test", 0)
globalSet.Bool("debug", false, "doc")
globalSet.String("config-file", "config.dev.yml", "doc")
globalSet.String("assets-path", assetsPath, "doc")
globalSet.String("originals-path", originalsPath, "doc")
globalSet.String("darktable-cli ", darktableCli, "doc")
return cli.NewContext(nil, globalSet, nil)
}
func TestNewConfig(t *testing.T) {
c := NewConfig()
context := getTestCliContext()
assert.False(t, context.IsSet("assets-path"))
assert.False(t, context.Bool("debug"))
c := NewConfig(context)
assert.IsType(t, &Config{}, c)
t.Log(c.AssetsPath, c.OriginalsPath, c.DarktableCli)
assert.Equal(t, assetsPath, c.AssetsPath)
assert.True(t, c.Debug)
}
func TestConfig_SetValuesFromFile(t *testing.T) {
c := NewConfig()
c := NewConfig(getTestCliContext())
c.SetValuesFromFile(GetExpandedFilename("config.dev.yml"))
assert.Equal(t, GetExpandedFilename("photos/originals"), c.OriginalsPath)
assert.Equal(t, GetExpandedFilename("photos/thumbnails"), c.ThumbnailsPath)
assert.Equal(t, GetExpandedFilename("photos/import"), c.ImportPath)
assert.Equal(t, GetExpandedFilename("photos/export"), c.ExportPath)
assert.Equal(t, GetExpandedFilename("server/assets"), c.ServerAssetsPath)
assert.Equal(t, GetExpandedFilename("assets"), c.AssetsPath)
assert.Equal(t, GetExpandedFilename("assets/thumbnails"), c.ThumbnailsPath)
assert.Equal(t, GetExpandedFilename("assets/photos/originals"), c.OriginalsPath)
assert.Equal(t, GetExpandedFilename("assets/photos/import"), c.ImportPath)
assert.Equal(t, GetExpandedFilename("assets/photos/export"), c.ExportPath)
assert.Equal(t, databaseDriver, c.DatabaseDriver)
assert.Equal(t, databaseDsn, c.DatabaseDsn)
}

View file

@ -6,7 +6,7 @@ services:
ports:
- 80:80
volumes:
- photo-data:/var/photos
- photo-data:/var/photoprism/photos
database:
image: mysql:latest

View file

@ -7,7 +7,7 @@ const webpack = require('webpack');
const PATHS = {
app: path.join(__dirname, 'src/app.js'),
css: path.join(__dirname, 'css'),
build: path.join(__dirname, '../server/assets/public/build'),
build: path.join(__dirname, '../assets/public/build'),
};
const cssPlugin = new ExtractTextPlugin({

View file

@ -8,7 +8,9 @@ import (
func TestNewImporter(t *testing.T) {
conf := NewTestConfig()
indexer := NewIndexer(conf.OriginalsPath, conf.GetDb())
tensorFlow := NewTensorFlow(conf.GetTensorFlowModelPath())
indexer := NewIndexer(conf.OriginalsPath, tensorFlow, conf.GetDb())
importer := NewImporter(conf.OriginalsPath, indexer)
@ -20,7 +22,9 @@ func TestImporter_ImportPhotosFromDirectory(t *testing.T) {
conf.InitializeTestData(t)
indexer := NewIndexer(conf.OriginalsPath, conf.GetDb())
tensorFlow := NewTensorFlow(conf.GetTensorFlowModelPath())
indexer := NewIndexer(conf.OriginalsPath, tensorFlow, conf.GetDb())
importer := NewImporter(conf.OriginalsPath, indexer)
@ -31,7 +35,9 @@ func TestImporter_GetDestinationFilename(t *testing.T) {
conf := NewTestConfig()
conf.InitializeTestData(t)
indexer := NewIndexer(conf.OriginalsPath, conf.GetDb())
tensorFlow := NewTensorFlow(conf.GetTensorFlowModelPath())
indexer := NewIndexer(conf.OriginalsPath, tensorFlow, conf.GetDb())
importer := NewImporter(conf.OriginalsPath, indexer)

View file

@ -3,8 +3,6 @@ package photoprism
import (
"fmt"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/recognize"
"io/ioutil"
"log"
"os"
"path/filepath"
@ -14,12 +12,14 @@ import (
type Indexer struct {
originalsPath string
tensorFlow *TensorFlow
db *gorm.DB
}
func NewIndexer(originalsPath string, db *gorm.DB) *Indexer {
func NewIndexer(originalsPath string, tensorFlow *TensorFlow, db *gorm.DB) *Indexer {
instance := &Indexer{
originalsPath: originalsPath,
tensorFlow: tensorFlow,
db: db,
}
@ -27,17 +27,15 @@ func NewIndexer(originalsPath string, db *gorm.DB) *Indexer {
}
func (i *Indexer) GetImageTags(jpeg *MediaFile) (results []*Tag) {
if imageBuffer, err := ioutil.ReadFile(jpeg.filename); err == nil {
tags, err := recognize.GetImageTags(string(imageBuffer))
tags, err := i.tensorFlow.GetImageTagsFromFile(jpeg.filename)
if err != nil {
return results
}
if err != nil {
return results
}
for _, tag := range tags {
if tag.Probability > 0.2 { // TODO: Use config variable
results = i.appendTag(results, tag.Label)
}
for _, tag := range tags {
if tag.Probability > 0.15 { // TODO: Use config variable
results = i.appendTag(results, tag.Label)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

View file

@ -1,63 +0,0 @@
package recognize
import (
tf "github.com/tensorflow/tensorflow/tensorflow/go"
"github.com/tensorflow/tensorflow/tensorflow/go/op"
)
func makeTensorFromImage(image string, imageFormat string) (*tf.Tensor, error) {
tensor, err := tf.NewTensor(image)
if err != nil {
return nil, err
}
graph, input, output, err := makeTransformImageGraph(imageFormat)
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
}
// Creates a graph to decode, rezise and normalize an image
func makeTransformImageGraph(imageFormat string) (graph *tf.Graph, input, output tf.Output, err error) {
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
}

View file

@ -1,115 +0,0 @@
package recognize
import (
"bufio"
"errors"
tf "github.com/tensorflow/tensorflow/tensorflow/go"
"io/ioutil"
"log"
"os"
"sort"
)
type ClassifyResult struct {
Filename string `json:"filename"`
Labels []LabelResult `json:"labels"`
}
type LabelResult struct {
Label string `json:"label"`
Probability float32 `json:"probability"`
}
var (
graph *tf.Graph
labels []string
)
func GetImageTags(image string) (result []LabelResult, err error) {
if err := loadModel(); err != nil {
return nil, err
}
// Make tensor
tensor, err := makeTensorFromImage(image, "jpeg")
if err != nil {
return nil, errors.New("invalid image")
}
// Run inference
session, err := tf.NewSession(graph, nil)
if err != nil {
log.Fatal(err)
}
defer session.Close()
output, err := session.Run(
map[tf.Output]*tf.Tensor{
graph.Operation("input").Output(0): tensor,
},
[]tf.Output{
graph.Operation("output").Output(0),
},
nil)
if err != nil {
return nil, errors.New("could not run inference")
}
// Return best labels
return findBestLabels(output[0].Value().([][]float32)[0]), nil
}
func loadModel() error {
// Load inception model
model, err := ioutil.ReadFile("/model/tensorflow_inception_graph.pb")
if err != nil {
return err
}
graph = tf.NewGraph()
if err := graph.Import(model, ""); err != nil {
return err
}
// Load labels
labelsFile, err := os.Open("/model/imagenet_comp_graph_label_strings.txt")
if err != nil {
return err
}
defer labelsFile.Close()
scanner := bufio.NewScanner(labelsFile)
// Labels are separated by newlines
for scanner.Scan() {
labels = append(labels, scanner.Text())
}
if err := scanner.Err(); err != nil {
return err
}
return nil
}
type ByProbability []LabelResult
func (a ByProbability) Len() int { return len(a) }
func (a ByProbability) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByProbability) Less(i, j int) bool { return a[i].Probability > a[j].Probability }
func findBestLabels(probabilities []float32) []LabelResult {
// Make a list of label/probability pairs
var resultLabels []LabelResult
for i, p := range probabilities {
if i >= len(labels) {
break
}
resultLabels = append(resultLabels, LabelResult{Label: labels[i], Probability: p})
}
// Sort by probability
sort.Sort(ByProbability(resultLabels))
// Return top 5 labels
return resultLabels[:5]
}

View file

@ -1,25 +0,0 @@
package recognize
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"testing"
)
func TestGetImageTags(t *testing.T) {
if imageBuffer, err := ioutil.ReadFile("cat.jpg"); err != nil {
t.Error(err)
} else {
result, err := GetImageTags(string(imageBuffer))
assert.NotNil(t, result)
assert.Nil(t, err)
assert.IsType(t, []LabelResult{}, result)
assert.Equal(t, 5, len(result))
assert.Equal(t, "tabby", result[0].Label)
assert.Equal(t, "tiger cat", result[1].Label)
assert.Equal(t, float32(0.23251747), result[1].Probability)
}
}

8
scripts/download-tf-model.sh Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
if [[ ! -e assets/tensorflow/tensorflow_inception_graph.pb ]]; then
wget "https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip" -O assets/tensorflow/inception.zip &&
unzip assets/tensorflow/inception.zip -d assets/tensorflow
else
echo "TensorFlow InceptionV3 model already downloaded."
fi

View file

@ -10,13 +10,13 @@ import (
)
func ConfigureRoutes(app *gin.Engine, conf *photoprism.Config) {
serverAssetsPath := conf.ServerAssetsPath
app.LoadHTMLGlob(serverAssetsPath + "/templates/*")
assetsPath := conf.AssetsPath
app.LoadHTMLGlob(assetsPath + "/templates/*")
app.StaticFile("/favicon.ico", serverAssetsPath + "/favicons/favicon.ico")
app.StaticFile("/favicon.png", serverAssetsPath + "/favicons/favicon.png")
app.StaticFile("/favicon.ico", assetsPath+"/favicons/favicon.ico")
app.StaticFile("/favicon.png", assetsPath+"/favicons/favicon.png")
app.Static("/assets", serverAssetsPath + "/public")
app.Static("/assets", assetsPath+"/public")
// JSON-REST API Version 1
v1 := app.Group("/api/v1")

190
tensorflow.go Normal file
View file

@ -0,0 +1,190 @@
package photoprism
import (
"bufio"
"errors"
tf "github.com/tensorflow/tensorflow/tensorflow/go"
"github.com/tensorflow/tensorflow/tensorflow/go/op"
"io/ioutil"
"log"
"os"
"sort"
)
type TensorFlow struct {
modelPath string
graph *tf.Graph
labels []string
}
func NewTensorFlow(tensorFlowModelPath string) *TensorFlow {
return &TensorFlow{modelPath:tensorFlowModelPath}
}
type TensorFlowLabel struct {
Label string `json:"label"`
Probability float32 `json:"probability"`
}
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 }
func (t *TensorFlow) GetImageTagsFromFile(filename string) (result []TensorFlowLabel, err error) {
imageBuffer, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return t.GetImageTags(string(imageBuffer))
}
func (t *TensorFlow) GetImageTags(image string) (result []TensorFlowLabel, err error) {
if err := t.loadModel(); err != nil {
return nil, err
}
// Make tensor
tensor, err := t.makeTensorFromImage(image, "jpeg")
if err != nil {
return nil, errors.New("invalid image")
}
// Run inference
session, err := tf.NewSession(t.graph, nil)
if err != nil {
log.Fatal(err)
}
defer session.Close()
output, err := session.Run(
map[tf.Output]*tf.Tensor{
t.graph.Operation("input").Output(0): tensor,
},
[]tf.Output{
t.graph.Operation("output").Output(0),
},
nil)
if err != nil {
return nil, errors.New("could not run inference")
}
// Return best labels
return t.findBestLabels(output[0].Value().([][]float32)[0]), nil
}
func (t *TensorFlow) loadModel() error {
if t.graph != nil {
// Already loaded
return nil
}
// Load inception model
model, err := ioutil.ReadFile(t.modelPath + "/tensorflow_inception_graph.pb")
if err != nil {
return err
}
t.graph = tf.NewGraph()
if err := t.graph.Import(model, ""); err != nil {
return err
}
// Load labels
labelsFile, err := os.Open(t.modelPath + "/imagenet_comp_graph_label_strings.txt")
if err != nil {
return err
}
defer labelsFile.Close()
scanner := bufio.NewScanner(labelsFile)
// 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 *TensorFlow) findBestLabels(probabilities []float32) []TensorFlowLabel {
// Make a list of label/probability pairs
var resultLabels []TensorFlowLabel
for i, p := range probabilities {
if i >= len(t.labels) {
break
}
resultLabels = append(resultLabels, TensorFlowLabel{Label: t.labels[i], Probability: p})
}
// Sort by probability
sort.Sort(TensorFlowLabels(resultLabels))
// Return top 5 labels
return resultLabels[:5]
}
func (t *TensorFlow) makeTensorFromImage(image string, imageFormat string) (*tf.Tensor, error) {
tensor, err := tf.NewTensor(image)
if err != nil {
return nil, err
}
graph, input, output, err := t.makeTransformImageGraph(imageFormat)
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
}
// Creates a graph to decode, rezise and normalize an image
func (t *TensorFlow) makeTransformImageGraph(imageFormat string) (graph *tf.Graph, input, output tf.Output, err error) {
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
}

51
tensorflow_test.go Normal file
View file

@ -0,0 +1,51 @@
package photoprism
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"testing"
)
func TestTensorFlow_GetImageTags(t *testing.T) {
conf := NewTestConfig()
conf.InitializeTestData(t)
tensorFlow := NewTensorFlow(conf.GetTensorFlowModelPath())
if imageBuffer, err := ioutil.ReadFile(conf.ImportPath + "/iphone/IMG_6788.JPG"); err != nil {
t.Error(err)
} else {
result, err := tensorFlow.GetImageTags(string(imageBuffer))
assert.NotNil(t, result)
assert.Nil(t, err)
assert.IsType(t, []TensorFlowLabel{}, result)
assert.Equal(t, 5, len(result))
assert.Equal(t, "tabby", result[0].Label)
assert.Equal(t, "tiger cat", result[1].Label)
assert.Equal(t, float32(0.1648176), result[1].Probability)
}
}
func TestTensorFlow_GetImageTagsFromFile(t *testing.T) {
conf := NewTestConfig()
conf.InitializeTestData(t)
tensorFlow := NewTensorFlow(conf.GetTensorFlowModelPath())
result, err := tensorFlow.GetImageTagsFromFile(conf.ImportPath + "/iphone/IMG_6788.JPG")
assert.NotNil(t, result)
assert.Nil(t, err)
assert.IsType(t, []TensorFlowLabel{}, result)
assert.Equal(t, 5, len(result))
assert.Equal(t, "tabby", result[0].Label)
assert.Equal(t, "tiger cat", result[1].Label)
assert.Equal(t, float32(0.1648176), result[1].Probability)
}

View file

@ -46,7 +46,9 @@ func TestCreateThumbnailsFromOriginals(t *testing.T) {
conf.InitializeTestData(t)
indexer := NewIndexer(conf.OriginalsPath, conf.GetDb())
tensorFlow := NewTensorFlow(conf.GetTensorFlowModelPath())
indexer := NewIndexer(conf.OriginalsPath, tensorFlow, conf.GetDb())
importer := NewImporter(conf.OriginalsPath, indexer)

View file

@ -20,7 +20,11 @@ func GetExpandedFilename(filename string) string {
usr, _ := user.Current()
dir := usr.HomeDir
if filename[:2] == "~/" {
if filename == "" {
panic("filename was empty")
}
if len(filename) > 2 && filename[:2] == "~/" {
filename = filepath.Join(dir, filename[2:])
}