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

View file

@ -102,24 +102,14 @@ ENV PATH $GOBIN:/usr/local/go/bin:$PATH
ENV GO111MODULE on ENV GO111MODULE on
ENV NODE_ENV production ENV NODE_ENV production
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" /etc/photoprism /var/photos && chmod -R 777 "$GOPATH" RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && 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
# Set up project directory # Set up project directory
WORKDIR "/go/src/github.com/photoprism/photoprism" WORKDIR "/go/src/github.com/photoprism/photoprism"
COPY . . COPY . .
RUN cp config.prod.yml /etc/photoprism/config.yml
# Build PhotoPrism # Build PhotoPrism
RUN make dep js install RUN make all install
RUN cp -r server/assets /etc/photoprism
# Expose HTTP port # Expose HTTP port
EXPOSE 80 EXPOSE 80

View file

@ -9,9 +9,15 @@ GOGET=$(GOCMD) get
GOFMT=$(GOCMD) fmt GOFMT=$(GOCMD) fmt
BINARY_NAME=photoprism BINARY_NAME=photoprism
all: dep js build all: tensorflow-model dep js build
install: install: install-bin install-assets install-config
install-bin:
$(GOINSTALL) cmd/photoprism/photoprism.go $(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: build:
$(GOBUILD) cmd/photoprism/photoprism.go $(GOBUILD) cmd/photoprism/photoprism.go
js: js:
@ -26,6 +32,8 @@ test:
clean: clean:
$(GOCLEAN) $(GOCLEAN)
rm -f $(BINARY_NAME) rm -f $(BINARY_NAME)
tensorflow-model:
scripts/download-tf-model.sh
image: image:
docker build . --tag photoprism/photoprism docker build . --tag photoprism/photoprism
docker push 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): Our goal is to provide the following features (tested as a proof-of-concept):
- High-performance command line tool - High-performance command line tool
- [Web frontend](docs/img/screenshot.jpg) - [Web frontend](assets/docs/img/screenshot.jpg)
- No proprietary or binary data formats - No proprietary or binary data formats
- Automatic RAW to JPEG conversion - Automatic RAW to JPEG conversion
- Duplicate detection (JPEG and RAW can be used simultaneously) - 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. 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. 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 Setup
----- -----
@ -73,4 +73,4 @@ See [Quick and easy guide for migrating to Go 1.11 modules](https://blog.liquidb
Concept 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() { func main() {
conf := photoprism.NewConfig()
app := cli.NewApp() app := cli.NewApp()
app.Name = "PhotoPrism" app.Name = "PhotoPrism"
app.Usage = "Digital Photo Archive" app.Usage = "Digital Photo Archive"
@ -22,9 +20,7 @@ func main() {
Name: "config", Name: "config",
Usage: "Displays global configuration values", Usage: "Displays global configuration values",
Action: func(context *cli.Context) error { Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file"))) conf := photoprism.NewConfig(context)
conf.SetValuesFromCliContext(context)
fmt.Printf("NAME VALUE\n") fmt.Printf("NAME VALUE\n")
fmt.Printf("debug %t\n", conf.Debug) fmt.Printf("debug %t\n", conf.Debug)
@ -32,12 +28,14 @@ func main() {
fmt.Printf("server-ip %s\n", conf.ServerIP) fmt.Printf("server-ip %s\n", conf.ServerIP)
fmt.Printf("server-port %d\n", conf.ServerPort) fmt.Printf("server-port %d\n", conf.ServerPort)
fmt.Printf("server-mode %s\n", conf.ServerMode) fmt.Printf("server-mode %s\n", conf.ServerMode)
fmt.Printf("server-assets-path %s\n", conf.ServerAssetsPath) fmt.Printf("assets-path %s\n", conf.AssetsPath)
fmt.Printf("darktable-cli %s\n", conf.DarktableCli)
fmt.Printf("originals-path %s\n", conf.OriginalsPath) fmt.Printf("originals-path %s\n", conf.OriginalsPath)
fmt.Printf("thumbnails-path %s\n", conf.ThumbnailsPath) fmt.Printf("thumbnails-path %s\n", conf.ThumbnailsPath)
fmt.Printf("import-path %s\n", conf.ImportPath) fmt.Printf("import-path %s\n", conf.ImportPath)
fmt.Printf("export-path %s\n", conf.ExportPath) 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 return nil
}, },
@ -63,9 +61,7 @@ func main() {
}, },
}, },
Action: func(context *cli.Context) error { Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file"))) conf := photoprism.NewConfig(context)
conf.SetValuesFromCliContext(context)
if context.IsSet("server-ip") { if context.IsSet("server-ip") {
conf.ServerIP = context.String("server-ip") conf.ServerIP = context.String("server-ip")
@ -96,9 +92,7 @@ func main() {
Name: "migrate-db", Name: "migrate-db",
Usage: "Automatically migrates / initializes database", Usage: "Automatically migrates / initializes database",
Action: func(context *cli.Context) error { Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file"))) conf := photoprism.NewConfig(context)
conf.SetValuesFromCliContext(context)
fmt.Println("Migrating database...") fmt.Println("Migrating database...")
@ -113,9 +107,7 @@ func main() {
Name: "import", Name: "import",
Usage: "Imports photos", Usage: "Imports photos",
Action: func(context *cli.Context) error { Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file"))) conf := photoprism.NewConfig(context)
conf.SetValuesFromCliContext(context)
conf.CreateDirectories() conf.CreateDirectories()
@ -123,7 +115,9 @@ func main() {
fmt.Printf("Importing photos from %s...\n", conf.ImportPath) 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) importer := photoprism.NewImporter(conf.OriginalsPath, indexer)
@ -138,9 +132,7 @@ func main() {
Name: "index", Name: "index",
Usage: "Re-indexes all originals", Usage: "Re-indexes all originals",
Action: func(context *cli.Context) error { Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file"))) conf := photoprism.NewConfig(context)
conf.SetValuesFromCliContext(context)
conf.CreateDirectories() conf.CreateDirectories()
@ -148,7 +140,9 @@ func main() {
fmt.Printf("Indexing photos in %s...\n", conf.OriginalsPath) 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() indexer.IndexAll()
@ -161,9 +155,7 @@ func main() {
Name: "convert", Name: "convert",
Usage: "Converts RAW originals to JPEG", Usage: "Converts RAW originals to JPEG",
Action: func(context *cli.Context) error { Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file"))) conf := photoprism.NewConfig(context)
conf.SetValuesFromCliContext(context)
conf.CreateDirectories() conf.CreateDirectories()
@ -196,9 +188,7 @@ func main() {
}, },
}, },
Action: func(context *cli.Context) error { Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file"))) conf := photoprism.NewConfig(context)
conf.SetValuesFromCliContext(context)
conf.CreateDirectories() conf.CreateDirectories()
@ -247,9 +237,7 @@ func main() {
}, },
}, },
Action: func(context *cli.Context) error { Action: func(context *cli.Context) error {
conf.SetValuesFromFile(photoprism.GetExpandedFilename(context.GlobalString("config-file"))) conf := photoprism.NewConfig(context)
conf.SetValuesFromCliContext(context)
conf.CreateDirectories() conf.CreateDirectories()
@ -313,27 +301,27 @@ var globalCliFlags = []cli.Flag{
cli.StringFlag{ cli.StringFlag{
Name: "originals-path", Name: "originals-path",
Usage: "originals path", Usage: "originals path",
Value: "/var/photoprism/originals", Value: "/var/photoprism/photos/originals",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "thumbnails-path", Name: "thumbnails-path",
Usage: "thumbnails path", Usage: "thumbnails path",
Value: "/var/photoprism/thumbnails", Value: "/var/photoprism/photos/thumbnails",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "import-path", Name: "import-path",
Usage: "import path", Usage: "import path",
Value: "/var/photoprism/import", Value: "/var/photoprism/photos/import",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "export-path", Name: "export-path",
Usage: "export path", Usage: "export path",
Value: "/var/photoprism/export", Value: "/var/photoprism/photos/export",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "server-assets-path", Name: "assets-path",
Usage: "server assets path for templates, js and css", Usage: "assets path",
Value: "/var/photoprism/server", Value: "/var/photoprism",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "database-driver", Name: "database-driver",

View file

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

View file

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

View file

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

View file

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

View file

@ -1,24 +1,26 @@
package photoprism package photoprism
import ( import (
"flag"
"fmt" "fmt"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli"
"os" "os"
"testing" "testing"
) )
const testDataPath = "testdata" const testDataPath = "assets/testdata"
const testDataUrl = "https://www.dropbox.com/s/na9p9wwt98l7m5b/import.zip?dl=1" const testDataUrl = "https://www.dropbox.com/s/na9p9wwt98l7m5b/import.zip?dl=1"
const testDataHash = "ed3bdb2fe86ea662bc863b63e219b47b8d9a74024757007f7979887d" const testDataHash = "ed3bdb2fe86ea662bc863b63e219b47b8d9a74024757007f7979887d"
var darktableCli = "/usr/bin/darktable-cli" var darktableCli = "/usr/bin/darktable-cli"
var testDataZip = GetExpandedFilename(testDataPath + "/import.zip") var testDataZip = GetExpandedFilename(testDataPath + "/import.zip")
var originalsPath = GetExpandedFilename(testDataPath + "/originals") var assetsPath = GetExpandedFilename("assets")
var thumbnailsPath = GetExpandedFilename(testDataPath + "/thumbnails") var thumbnailsPath = GetExpandedFilename(testDataPath + "/thumbnails")
var originalsPath = GetExpandedFilename(testDataPath + "/originals")
var importPath = GetExpandedFilename(testDataPath + "/import") var importPath = GetExpandedFilename(testDataPath + "/import")
var exportPath = GetExpandedFilename(testDataPath + "/export") var exportPath = GetExpandedFilename(testDataPath + "/export")
var serverAssetsPath = GetExpandedFilename("server/assets")
var databaseDriver = "mysql" var databaseDriver = "mysql"
var databaseDsn = "photoprism:photoprism@tcp(database:3306)/photoprism?parseTime=true" var databaseDsn = "photoprism:photoprism@tcp(database:3306)/photoprism?parseTime=true"
@ -66,34 +68,53 @@ func (c *Config) InitializeTestData(t *testing.T) {
func NewTestConfig() *Config { func NewTestConfig() *Config {
return &Config{ return &Config{
Debug: false, Debug: false,
DarktableCli: darktableCli, AssetsPath: assetsPath,
OriginalsPath: originalsPath, ThumbnailsPath: thumbnailsPath,
ThumbnailsPath: thumbnailsPath, OriginalsPath: originalsPath,
ImportPath: importPath, ImportPath: importPath,
ExportPath: exportPath, ExportPath: exportPath,
ServerAssetsPath: serverAssetsPath, DarktableCli: darktableCli,
DatabaseDriver: databaseDriver, DatabaseDriver: databaseDriver,
DatabaseDsn: databaseDsn, 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) { 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) 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) { func TestConfig_SetValuesFromFile(t *testing.T) {
c := NewConfig() c := NewConfig(getTestCliContext())
c.SetValuesFromFile(GetExpandedFilename("config.dev.yml")) c.SetValuesFromFile(GetExpandedFilename("config.dev.yml"))
assert.Equal(t, GetExpandedFilename("photos/originals"), c.OriginalsPath) assert.Equal(t, GetExpandedFilename("assets"), c.AssetsPath)
assert.Equal(t, GetExpandedFilename("photos/thumbnails"), c.ThumbnailsPath) assert.Equal(t, GetExpandedFilename("assets/thumbnails"), c.ThumbnailsPath)
assert.Equal(t, GetExpandedFilename("photos/import"), c.ImportPath) assert.Equal(t, GetExpandedFilename("assets/photos/originals"), c.OriginalsPath)
assert.Equal(t, GetExpandedFilename("photos/export"), c.ExportPath) assert.Equal(t, GetExpandedFilename("assets/photos/import"), c.ImportPath)
assert.Equal(t, GetExpandedFilename("server/assets"), c.ServerAssetsPath) assert.Equal(t, GetExpandedFilename("assets/photos/export"), c.ExportPath)
assert.Equal(t, databaseDriver, c.DatabaseDriver) assert.Equal(t, databaseDriver, c.DatabaseDriver)
assert.Equal(t, databaseDsn, c.DatabaseDsn) assert.Equal(t, databaseDsn, c.DatabaseDsn)
} }

View file

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

View file

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

View file

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

View file

@ -3,8 +3,6 @@ package photoprism
import ( import (
"fmt" "fmt"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/recognize"
"io/ioutil"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@ -14,12 +12,14 @@ import (
type Indexer struct { type Indexer struct {
originalsPath string originalsPath string
tensorFlow *TensorFlow
db *gorm.DB db *gorm.DB
} }
func NewIndexer(originalsPath string, db *gorm.DB) *Indexer { func NewIndexer(originalsPath string, tensorFlow *TensorFlow, db *gorm.DB) *Indexer {
instance := &Indexer{ instance := &Indexer{
originalsPath: originalsPath, originalsPath: originalsPath,
tensorFlow: tensorFlow,
db: db, db: db,
} }
@ -27,17 +27,15 @@ func NewIndexer(originalsPath string, db *gorm.DB) *Indexer {
} }
func (i *Indexer) GetImageTags(jpeg *MediaFile) (results []*Tag) { func (i *Indexer) GetImageTags(jpeg *MediaFile) (results []*Tag) {
if imageBuffer, err := ioutil.ReadFile(jpeg.filename); err == nil { tags, err := i.tensorFlow.GetImageTagsFromFile(jpeg.filename)
tags, err := recognize.GetImageTags(string(imageBuffer))
if err != nil { if err != nil {
return results return results
} }
for _, tag := range tags { for _, tag := range tags {
if tag.Probability > 0.2 { // TODO: Use config variable if tag.Probability > 0.15 { // TODO: Use config variable
results = i.appendTag(results, tag.Label) 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) { func ConfigureRoutes(app *gin.Engine, conf *photoprism.Config) {
serverAssetsPath := conf.ServerAssetsPath assetsPath := conf.AssetsPath
app.LoadHTMLGlob(serverAssetsPath + "/templates/*") app.LoadHTMLGlob(assetsPath + "/templates/*")
app.StaticFile("/favicon.ico", serverAssetsPath + "/favicons/favicon.ico") app.StaticFile("/favicon.ico", assetsPath+"/favicons/favicon.ico")
app.StaticFile("/favicon.png", serverAssetsPath + "/favicons/favicon.png") app.StaticFile("/favicon.png", assetsPath+"/favicons/favicon.png")
app.Static("/assets", serverAssetsPath + "/public") app.Static("/assets", assetsPath+"/public")
// JSON-REST API Version 1 // JSON-REST API Version 1
v1 := app.Group("/api/v1") 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) 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) importer := NewImporter(conf.OriginalsPath, indexer)

View file

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