mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Indexer now automatically sets title, keywords and detected colors
This commit is contained in:
parent
7dbbc64506
commit
3d23056851
15 changed files with 172 additions and 33 deletions
|
|
@ -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
|
||||
6
Makefile
6
Makefile
|
|
@ -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
|
||||
|
|
@ -1,4 +1,5 @@
|
|||

|
||||
PhotoPrism - Long-Term Digital Photo Archive
|
||||
============================================
|
||||
|
||||
[][powered by]
|
||||
[][license]
|
||||
|
|
|
|||
|
|
@ -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
47
colors.go
Normal 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
40
colors_test.go
Normal 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)
|
||||
}
|
||||
1
file.go
1
file.go
|
|
@ -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
2
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
46
indexer.go
46
indexer.go
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
7
photo.go
7
photo.go
|
|
@ -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
|
||||
|
|
|
|||
6
query.go
6
query.go
|
|
@ -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+"%")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue