Indexer now automatically sets title, keywords and detected colors

This commit is contained in:
Michael Mayer 2018-09-11 10:56:52 +02:00
parent 7dbbc64506
commit 3d23056851
15 changed files with 172 additions and 33 deletions

View file

@ -7,7 +7,7 @@ before_script:
- docker-compose -f docker-compose.travis.yml up -d
script:
- docker-compose exec photoprism make deps migrate-db js test
- docker-compose exec photoprism make dep migrate-db js test
after_script:
- docker-compose -f docker-compose.travis.yml down

View file

@ -9,7 +9,7 @@ GOGET=$(GOCMD) get
GOFMT=$(GOCMD) fmt
BINARY_NAME=photoprism
all: deps js build
all: dep js build
install:
$(GOINSTALL) cmd/photoprism/photoprism.go
build:
@ -29,9 +29,9 @@ clean:
image:
docker build . --tag photoprism/photoprism
docker push photoprism/photoprism
format:
fmt:
$(GOFMT) ./...
deps:
dep:
$(GOBUILD) -v ./...
upgrade:
$(GOGET) -u

View file

@ -1,4 +1,5 @@
![PhotoPrism](docs/img/logo.png)
PhotoPrism - Long-Term Digital Photo Archive
============================================
[![Powered By](https://img.shields.io/badge/powered%20by-Go,%20TensorFlow%20%26%20Vuetify-blue.svg)][powered by]
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)][license]

View file

@ -14,7 +14,7 @@ func main() {
app := cli.NewApp()
app.Name = "PhotoPrism"
app.Usage = "Digital Photo Archive"
app.Usage = "Long-Term Digital Photo Archive"
app.Version = "0.2.0"
app.Flags = globalCliFlags
app.Commands = []cli.Command{
@ -97,6 +97,8 @@ func main() {
conf.CreateDirectories()
conf.MigrateDb()
fmt.Printf("Importing photos from %s...\n", conf.ImportPath)
indexer := photoprism.NewIndexer(conf.OriginalsPath, conf.GetDb())
@ -120,6 +122,8 @@ func main() {
conf.CreateDirectories()
conf.MigrateDb()
fmt.Printf("Indexing photos in %s...\n", conf.OriginalsPath)
indexer := photoprism.NewIndexer(conf.OriginalsPath, conf.GetDb())

47
colors.go Normal file
View file

@ -0,0 +1,47 @@
package photoprism
import (
"github.com/RobCherry/vibrant"
"github.com/lucasb-eyer/go-colorful"
"golang.org/x/image/colornames"
"image"
"os"
)
func getColorNames(actualColor colorful.Color) (result []string) {
var maxDistance = 0.22
for colorName, colorRGBA := range colornames.Map {
colorColorful, _ := colorful.MakeColor(colorRGBA)
currentDistance := colorColorful.DistanceRgb(actualColor)
if maxDistance >= currentDistance {
result = append(result, colorName)
}
}
return result
}
func (m *MediaFile) GetColors() (colors []string, vibrantHex string, mutedHex string) {
file, _ := os.Open(m.filename)
defer file.Close()
decodedImage, _, _ := image.Decode(file)
palette := vibrant.NewPaletteBuilder(decodedImage).Generate()
if vibrantSwatch := palette.VibrantSwatch(); vibrantSwatch != nil {
color, _ := colorful.MakeColor(vibrantSwatch.Color())
colors = append(colors, getColorNames(color)...)
vibrantHex = color.Hex()
}
if mutedSwatch := palette.MutedSwatch(); mutedSwatch != nil {
color, _ := colorful.MakeColor(mutedSwatch.Color())
colors = append(colors, getColorNames(color)...)
mutedHex = color.Hex()
}
return colors, vibrantHex, mutedHex
}

40
colors_test.go Normal file
View file

@ -0,0 +1,40 @@
package photoprism
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestMediaFile_GetColors(t *testing.T) {
conf := NewTestConfig()
conf.InitializeTestData(t)
mediaFile1 := NewMediaFile(conf.ImportPath + "/dog.jpg")
names, vibrantHex, mutedHex := mediaFile1.GetColors()
t.Log(names, vibrantHex, mutedHex)
assert.IsType(t, []string{}, names)
assert.Equal(t, "#e0ed21", vibrantHex)
assert.Equal(t, "#977d67", mutedHex)
mediaFile2 := NewMediaFile(conf.ImportPath + "/iphone/IMG_6788.JPG")
names, vibrantHex, mutedHex = mediaFile2.GetColors()
t.Log(names, vibrantHex, mutedHex)
assert.Equal(t, "#3d85c3", vibrantHex)
assert.Equal(t, "#988570", mutedHex)
mediaFile3 := NewMediaFile(conf.ImportPath + "/raw/20140717_154212_1EC48F8489.jpg")
names, vibrantHex, mutedHex = mediaFile3.GetColors()
t.Log(names, vibrantHex, mutedHex)
assert.Equal(t, "#d5d437", vibrantHex)
assert.Equal(t, "#a69f55", mutedHex)
}

View file

@ -8,6 +8,7 @@ type File struct {
gorm.Model
Photo *Photo
PhotoID uint
PrimaryFile bool
Filename string
FileType string `gorm:"type:varchar(30)"`
MimeType string `gorm:"type:varchar(50)"`

2
go.mod
View file

@ -2,6 +2,7 @@ module github.com/photoprism/photoprism
require (
cloud.google.com/go v0.27.0
github.com/RobCherry/vibrant v0.0.0-20160904011657-0680b8cf1c89
github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e
github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b
github.com/brett-lempereur/ish v0.0.0-20161214150457-bbdc45bcf55d
@ -18,6 +19,7 @@ require (
github.com/json-iterator/go v1.1.5
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28
github.com/lib/pq v1.0.0
github.com/lucasb-eyer/go-colorful v0.0.0-20180709185858-c7842319cf3a
github.com/mattn/go-isatty v0.0.4
github.com/mattn/go-sqlite3 v1.9.0
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd

4
go.sum
View file

@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.27.0 h1:Xa8ZWro6QYKOwDKtxfKsiE0ea2jD39nx32RxtF5RjYE=
cloud.google.com/go v0.27.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/RobCherry/vibrant v0.0.0-20160904011657-0680b8cf1c89 h1:k8/G7/7+vhkmphbzRSHulomGLxKJnM6Dp5NJ2HePGwY=
github.com/RobCherry/vibrant v0.0.0-20160904011657-0680b8cf1c89/go.mod h1:xu1tbmzBGes+jcIUU9yATLxmOoxdCZT0hUp5HY1c6/A=
github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e h1:s05JG2GwtJMHaPcXDpo4V35TFgyYZzNsmBlSkHPEbeg=
github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI=
github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b h1:5Ci5wpOL75rYF6RQGRoqhEAU6xLJ6n/D4SckXX1yB74=
@ -43,6 +45,8 @@ github.com/lib/pq v0.0.0-20180523175426-90697d60dd84 h1:it29sI2IM490luSc3RAhp5Wu
github.com/lib/pq v0.0.0-20180523175426-90697d60dd84/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lucasb-eyer/go-colorful v0.0.0-20180709185858-c7842319cf3a h1:B2QfFRl5yGVGGcyEVFzfdXlC1BBvszsIAsCeef2oD0k=
github.com/lucasb-eyer/go-colorful v0.0.0-20180709185858-c7842319cf3a/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=

View file

@ -1,6 +1,7 @@
package photoprism
import (
"fmt"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/recognize"
"io/ioutil"
@ -33,7 +34,7 @@ func (i *Indexer) GetImageTags(jpeg *MediaFile) (result []Tag) {
}
for _, tag := range tags {
if tag.Probability > 0.2 {
if tag.Probability > 0.2 { // TODO: Use config variable
var tagModel Tag
if res := i.db.First(&tagModel, "label = ?", tag.Label); res.Error != nil {
@ -50,7 +51,10 @@ func (i *Indexer) GetImageTags(jpeg *MediaFile) (result []Tag) {
func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) {
var photo Photo
var file File
var file, primaryFile File
var isPrimary = false
var colorNames []string
var keywords []string
canonicalName := mediaFile.GetCanonicalNameFromFile()
fileHash := mediaFile.GetHash()
@ -64,22 +68,53 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) {
if exifData, err := jpeg.GetExifData(); err == nil {
photo.Lat = exifData.Lat
photo.Long = exifData.Long
photo.Artist = exifData.Artist
}
colorNames, photo.VibrantColor, photo.MutedColor = jpeg.GetColors()
photo.ColorNames = strings.Join(colorNames, ", ")
photo.Tags = i.GetImageTags(jpeg)
for _, tag := range photo.Tags {
keywords = append(keywords, tag.Label)
}
}
if location, err := mediaFile.GetLocation(); err == nil {
i.db.FirstOrCreate(location, "id = ?", location.ID)
photo.Location = location
keywords = append(keywords, location.City, location.County, location.Country, location.LocationCategory)
if location.Name != "" {
photo.Title = fmt.Sprintf("%s / %s / %s", location.Name, location.Country, mediaFile.GetDateCreated().Format("2006"))
keywords = append(keywords, location.Name)
} else if location.City != "" {
photo.Title = fmt.Sprintf("%s / %s / %s", location.City, location.Country, mediaFile.GetDateCreated().Format("2006"))
} else if location.County != "" {
photo.Title = fmt.Sprintf("%s / %s / %s", location.County, location.Country, mediaFile.GetDateCreated().Format("2006"))
}
if location.LocationType != "" {
keywords = append(keywords, location.LocationType)
}
}
if photo.Title == "" {
if len(photo.Tags) > 0 {
photo.Title = fmt.Sprintf("%s / %s", strings.Title(photo.Tags[0].Label), mediaFile.GetDateCreated().Format("2006"))
} else {
photo.Title = fmt.Sprintf("Unknown / %s", mediaFile.GetDateCreated().Format("2006"))
}
}
photo.Keywords = strings.ToLower(strings.Join(keywords, ", "))
photo.Camera = NewCamera(mediaFile.GetCameraModel()).FirstOrCreate(i.db)
photo.TakenAt = mediaFile.GetDateCreated()
photo.CanonicalName = canonicalName
photo.Files = []File{}
photo.Albums = []Album{}
photo.Author = ""
photo.Favorite = false
photo.Private = true
@ -88,8 +123,13 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) {
i.db.Create(&photo)
}
if result := i.db.Where("file_type = 'jpg' AND primary_file = 1 AND photo_id = ?", photo.ID).First(&primaryFile); result.Error != nil {
isPrimary = mediaFile.GetType() == FileTypeJpeg
}
if result := i.db.First(&file, "hash = ?", fileHash); result.Error != nil {
file.PhotoID = photo.ID
file.PrimaryFile = isPrimary
file.Filename = mediaFile.GetFilename()
file.Hash = fileHash
file.FileType = mediaFile.GetType()

View file

@ -14,6 +14,7 @@ type Location struct {
DisplayName string
Lat float64
Long float64
Name string
City string
Postcode string
County string
@ -39,6 +40,7 @@ type OpenstreetmapLocation struct {
PlaceId string `json:"place_id"`
Lat string `json:"lat"`
Lon string `json:"lon"`
Name string `json:"name"`
Category string `json:"category"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
@ -89,6 +91,7 @@ func (m *MediaFile) GetLocation() (*Location, error) {
location.Long = lon
}
location.Name = openstreetmapLocation.Name
location.Postcode = openstreetmapLocation.Address.Postcode
location.County = openstreetmapLocation.Address.County
location.State = openstreetmapLocation.Address.State
@ -96,7 +99,10 @@ func (m *MediaFile) GetLocation() (*Location, error) {
location.CountryCode = openstreetmapLocation.Address.CountryCode
location.DisplayName = openstreetmapLocation.DisplayName
location.LocationCategory = openstreetmapLocation.Category
location.LocationType = openstreetmapLocation.Type
if openstreetmapLocation.Type != "yes" && openstreetmapLocation.Type != "unclassified" {
location.LocationType = openstreetmapLocation.Type
}
m.location = location

View file

@ -10,6 +10,7 @@ import (
type ExifData struct {
DateTime time.Time
Artist string
CameraModel string
UniqueID string
Lat float64
@ -51,6 +52,10 @@ func (m *MediaFile) GetExifData() (*ExifData, error) {
return m.exifData, err
}
if artist, err := x.Get(exif.Artist); err == nil {
m.exifData.Artist = strings.Replace(artist.String(), "\"", "", -1)
}
if camModel, err := x.Get(exif.Model); err == nil {
m.exifData.CameraModel = strings.Replace(camModel.String(), "\"", "", -1)
}

View file

@ -9,6 +9,12 @@ type Photo struct {
gorm.Model
Title string
Description string `gorm:"type:text;"`
Artist string
Keywords string
TextContent string `gorm:"type:text;"`
ColorNames string
VibrantColor string
MutedColor string
TakenAt time.Time
CanonicalName string
PerceptualHash string
@ -17,7 +23,6 @@ type Photo struct {
Albums []Album `gorm:"many2many:album_photos;"`
Camera *Camera
CameraID uint
Author string
Lat float64
Long float64
Location *Location

View file

@ -21,9 +21,11 @@ func NewQuery(originalsPath string, db *gorm.DB) *Search {
func (s *Search) FindPhotos(query string, count int, offset int) (photos []Photo) {
q := s.db.Preload("Tags").Preload("Files").Preload("Location").Preload("Albums")
q = q.Joins("JOIN files ON files.photo_id = photos.id AND files.primary_file")
if query != "" {
q = q.Joins("JOIN photo_tags ON photo_tags.photo_id=photos.id")
q = q.Joins("JOIN tags ON photo_tags.tag_id=tags.id")
q = q.Joins("JOIN photo_tags ON photo_tags.photo_id = photos.id")
q = q.Joins("JOIN tags ON photo_tags.tag_id = tags.id")
q = q.Where("tags.label LIKE ?", "%"+query+"%")
}

View file

@ -3,7 +3,6 @@ package photoprism
import (
"fmt"
"github.com/disintegration/imaging"
"image"
"log"
"os"
"path/filepath"
@ -61,30 +60,15 @@ func (m *MediaFile) GetThumbnail(path string, size int) (result *MediaFile, err
return m.CreateThumbnail(thumbnailFilename, size)
}
func (m *MediaFile) fixImageOrientation(img image.Image) image.Image {
switch orientation := m.GetOrientation(); orientation {
case 3:
img = imaging.Rotate180(img)
case 6:
img = imaging.Rotate270(img)
case 8:
img = imaging.Rotate90(img)
}
return img
}
// Resize preserving the aspect ratio
func (m *MediaFile) CreateThumbnail(filename string, size int) (result *MediaFile, err error) {
img, err := imaging.Open(m.filename)
img, err := imaging.Open(m.filename, imaging.AutoOrientation(true))
if err != nil {
log.Printf("open failed: %s", err.Error())
return nil, err
}
img = m.fixImageOrientation(img)
img = imaging.Fit(img, size, size, imaging.Lanczos)
err = imaging.Save(img, filename)
@ -118,15 +102,13 @@ func (m *MediaFile) GetSquareThumbnail(path string, size int) (result *MediaFile
// Resize and crop to square format
func (m *MediaFile) CreateSquareThumbnail(filename string, size int) (result *MediaFile, err error) {
img, err := imaging.Open(m.filename)
img, err := imaging.Open(m.filename, imaging.AutoOrientation(true))
if err != nil {
log.Printf("open failed: %s", err.Error())
return nil, err
}
img = m.fixImageOrientation(img)
img = imaging.Fill(img, size, size, imaging.Center, imaging.Lanczos)
err = imaging.Save(img, filename)