Moved all assets to assets/ and improved config
|
@ -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
|
@ -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
|
14
Dockerfile
|
@ -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
|
||||
|
|
12
Makefile
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 330 KiB After Width: | Height: | Size: 330 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 510 KiB After Width: | Height: | Size: 510 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
2
assets/tensorflow/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
2
assets/testdata/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
2
assets/thumbnails/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
|
@ -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",
|
||||
|
|
|
@ -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
|
64
config.go
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ services:
|
|||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- photo-data:/var/photos
|
||||
- photo-data:/var/photoprism/photos
|
||||
|
||||
database:
|
||||
image: mysql:latest
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
22
indexer.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Before Width: | Height: | Size: 130 KiB |
|
@ -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
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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
|
@ -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
|
|
@ -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
|
@ -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
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
6
util.go
|
@ -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:])
|
||||
}
|
||||
|
||||
|
|