From ffc64cceb087fb003de7453a95aa5c395c002e32 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Fri, 26 Apr 2019 02:22:53 +0200 Subject: [PATCH] Use 3x3 thumbnail for color indexing #7 Other implementations were unstable due to the use of random numbers. This seems to be fast and also enables us to search specific parts of an image. 16 colors are indexed (Material Design). --- Makefile | 3 +- frontend/src/app/pages/photosEdit.vue | 13 +- frontend/src/app/routes.js | 6 +- frontend/src/model/photo.js | 11 ++ go.mod | 14 +- go.sum | 29 ++-- internal/models/photo.go | 3 +- internal/photoprism/colors.go | 203 ++++++++++++++++-------- internal/photoprism/colors_slow_test.go | 25 +-- internal/photoprism/colors_test.go | 24 +-- internal/photoprism/indexer.go | 11 +- internal/photoprism/search.go | 5 +- internal/photoprism/tensorflow.go | 2 +- 13 files changed, 217 insertions(+), 132 deletions(-) diff --git a/Makefile b/Makefile index 532713dd2..b9add47eb 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,6 @@ test-coverage: go test -tags=slow -timeout 30m -coverprofile=coverage.txt -covermode=atomic -v ./internal/... go tool cover -html=coverage.txt -o coverage.html clean: - go clean rm -f $(BINARY_NAME) download: scripts/download-inception.sh @@ -68,6 +67,8 @@ fmt: go fmt ./internal/... ./cmd/... dep: go build -v ./... +tidy: + go mod tidy upgrade: go mod tidy go get -u diff --git a/frontend/src/app/pages/photosEdit.vue b/frontend/src/app/pages/photosEdit.vue index d3735ced4..dc07d1128 100644 --- a/frontend/src/app/pages/photosEdit.vue +++ b/frontend/src/app/pages/photosEdit.vue @@ -63,15 +63,9 @@ > - - @@ -229,6 +223,7 @@ 'activator': null, 'attach': null, 'colors': ['green', 'purple', 'indigo', 'primary', 'success', 'orange'], + 'color': '', 'editing': null, 'index': -1, 'items': [ diff --git a/frontend/src/app/routes.js b/frontend/src/app/routes.js index 37707443a..c45c44a37 100644 --- a/frontend/src/app/routes.js +++ b/frontend/src/app/routes.js @@ -21,10 +21,10 @@ export default [ { name: 'Favorites', path: '/favorites', component: Todo }, { name: 'Places', path: '/places', component: Todo }, { name: 'Albums', path: '/albums', component: Albums }, - { name: 'Albums', path: '/albums2', component: Albums2 }, + { name: 'Albums2', path: '/albums2', component: Albums2 }, { name: 'Import', path: '/import', component: Import }, - { name: 'Import', path: '/import2', component: Import2 }, - { name: 'Import', path: '/import3', component: Import3 }, + { name: 'Import2', path: '/import2', component: Import2 }, + { name: 'Import3', path: '/import3', component: Import3 }, { name: 'Export', path: '/export', component: Export }, { name: 'Settings', path: '/settings', component: Settings }, { path: '*', redirect: '/photos' }, diff --git a/frontend/src/model/photo.js b/frontend/src/model/photo.js index 7955ee11d..11993cfc7 100644 --- a/frontend/src/model/photo.js +++ b/frontend/src/model/photo.js @@ -10,6 +10,17 @@ class Photo extends Abstract { return this.ID; } + getColor() { + switch (this.PhotoColor) { + case 'brown': + case 'black': + case 'white': + case 'grey': + return 'grey lighten-2'; + default: + return this.PhotoColor + ' lighten-4'; + } + } getColors() { return this.PhotoColors; diff --git a/go.mod b/go.mod index f11941b5c..bd6dadaaf 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/photoprism/photoprism require ( cloud.google.com/go v0.34.0 // indirect - github.com/EdlinOrg/prominentcolor v0.0.0-20180211183425-27c67d28df53 - github.com/RobCherry/vibrant v0.0.0-20160904011657-0680b8cf1c89 github.com/araddon/dateparse v0.0.0-20181123171228-21df004e09ca github.com/bamiaux/rez v0.0.0-20170731184118-29f4463c688b // indirect github.com/blacktear23/go-proxyprotocol v0.0.0-20180807104634-af7a81e8dd0d // indirect @@ -47,8 +45,6 @@ require ( github.com/modern-go/reflect2 v1.0.1 // indirect github.com/montanaflynn/stats v0.0.0-20181214052348-945b007cb92f // indirect github.com/myesui/uuid v1.0.0 // indirect - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/oliamb/cutter v0.2.2 // indirect github.com/onsi/gomega v1.4.3 // indirect github.com/opentracing/opentracing-go v1.0.2 github.com/pingcap/errors v0.11.0 @@ -76,10 +72,12 @@ require ( go.uber.org/atomic v1.3.2 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.9.1 // indirect - golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect - golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b - golang.org/x/net v0.0.0-20181217023233-e147a9138326 // indirect - golang.org/x/sys v0.0.0-20181217223516-dcdaa6325bcb // indirect + golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d // indirect + golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b // indirect + golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 // indirect + golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect + golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 // indirect + golang.org/x/text v0.3.1 // indirect golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect google.golang.org/appengine v1.3.0 // indirect google.golang.org/genproto v0.0.0-20181218023534-67d6565462c5 // indirect diff --git a/go.sum b/go.sum index 8acf92ca6..1ddb785fc 100644 --- a/go.sum +++ b/go.sum @@ -3,10 +3,6 @@ cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/EdlinOrg/prominentcolor v0.0.0-20180211183425-27c67d28df53 h1:l+L2QDgQqz1rdaCRg1ncdFQeEWEbjsymnZRNDS/XHSo= -github.com/EdlinOrg/prominentcolor v0.0.0-20180211183425-27c67d28df53/go.mod h1:mYmDsxfcmBz6izH/SqtSzfsUiZdPNPpPgUPKCZq70KQ= -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/apache/thrift v0.0.0-20161221203622-b2a4d4ae21c7 h1:Fv9bK1Q+ly/ROk4aJsVMeuIwPel4bEnD8EPiI91nZMg= github.com/apache/thrift v0.0.0-20161221203622-b2a4d4ae21c7/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/araddon/dateparse v0.0.0-20181123171228-21df004e09ca h1:7tLEgJZb8/+TI8fLso4lINkuSOI4DqQYwhFB+nRH7RQ= @@ -156,14 +152,10 @@ github.com/montanaflynn/stats v0.0.0-20181214052348-945b007cb92f h1:r//C+RGlxxi1 github.com/montanaflynn/stats v0.0.0-20181214052348-945b007cb92f/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/myesui/uuid v1.0.0 h1:xCBmH4l5KuvLYc5L7AS7SZg9/jKdIFubM7OVoLqaQUI= github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/ngaut/pools v0.0.0-20180318154953-b7bc8c42aac7 h1:7KAv7KMGTTqSmYZtNdcNTgsos+vFzULLwyElndwn+5c= github.com/ngaut/pools v0.0.0-20180318154953-b7bc8c42aac7/go.mod h1:iWMfgwqYW+e8n5lC/jjNEhwcjbRDpl5NT7n2h+4UNcI= github.com/ngaut/sync2 v0.0.0-20141008032647-7a24ed77b2ef h1:K0Fn+DoFqNqktdZtdV3bPQ/0cuYh2H4rkg0tytX/07k= github.com/ngaut/sync2 v0.0.0-20141008032647-7a24ed77b2ef/go.mod h1:7WjlapSfwQyo6LNmIvEWzsW1hbBQfpUO4JWnuQRmva8= -github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k= -github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -258,8 +250,9 @@ go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180503215945-1f94bef427e3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d h1:adrbvkTDn9rGnXg2IJDKozEpXXLZN89pdIA+Syt4/u0= +golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM= golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -269,23 +262,31 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181217023233-e147a9138326 h1:iCzOf0xz39Tstp+Tu/WwyGjUXCk34QhQORRxBeXXTA4= -golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 h1:FP8hkuE6yUEaJnK7O2eTuejKWwW+Rhfj80dQ2JcKxCU= +golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181217223516-dcdaa6325bcb h1:zzdd4xkMwu/GRxhSUJaCPh4/jil9kAbsU7AUmXboO+A= -golang.org/x/sys v0.0.0-20181217223516-dcdaa6325bcb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0 h1:V+O002es++Mnym06Rj/S6Fl7VCsgRBgVDGb/NoZVHUg= +golang.org/x/sys v0.0.0-20190424175732-18eb32c0e2f0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1 h1:nsUiJHvm6yOoRozW9Tz0siNk9sHieLzR+w814Ihse3A= +golang.org/x/text v0.3.1/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/internal/models/photo.go b/internal/models/photo.go index da06e685d..7a2935eb4 100644 --- a/internal/models/photo.go +++ b/internal/models/photo.go @@ -17,8 +17,7 @@ type Photo struct { PhotoNotes string `gorm:"type:text;"` PhotoArtist string PhotoColors string - PhotoVibrantColor string - PhotoMutedColor string + PhotoColor string PhotoCanonicalName string PhotoFavorite bool PhotoLat float64 diff --git a/internal/photoprism/colors.go b/internal/photoprism/colors.go index 5c05b64be..6ad3ffb2e 100644 --- a/internal/photoprism/colors.go +++ b/internal/photoprism/colors.go @@ -2,95 +2,172 @@ package photoprism import ( "fmt" - "github.com/EdlinOrg/prominentcolor" - "github.com/RobCherry/vibrant" - "github.com/lucasb-eyer/go-colorful" - "image" "image/color" "log" - "os" - "sort" + + "github.com/disintegration/imaging" + "github.com/lucasb-eyer/go-colorful" ) -var colorMap = map[string]color.RGBA{ - "red": {0xf4, 0x43, 0x36, 0xff}, - "pink": {0xe9, 0x1e, 0x63, 0xff}, - "purple": {0x9c, 0x27, 0xb0, 0xff}, - "indigo": {0x3F, 0x51, 0xB5, 0xff}, - "blue": {0x21, 0x96, 0xF3, 0xff}, - "cyan": {0x00, 0xBC, 0xD4, 0xff}, - "teal": {0x00, 0x96, 0x88, 0xff}, - "green": {0x4C, 0xAF, 0x50, 0xff}, - "lime": {0xCD, 0xDC, 0x39, 0xff}, - "yellow": {0xFF, 0xEB, 0x3B, 0xff}, - "amber": {0xFF, 0xC1, 0x07, 0xff}, - "orange": {0xFF, 0x98, 0x00, 0xff}, - "brown": {0x79, 0x55, 0x48, 0xff}, - "grey": {0x9E, 0x9E, 0x9E, 0xff}, - "white": {0x00, 0x00, 0x00, 0xff}, - "black": {0xFF, 0xFF, 0xFF, 0xff}, +type MaterialColor uint16 +type MaterialColors []MaterialColor + +const ColorSampleSize = 3 + +const ( + Black MaterialColor = iota + Brown + Grey + White + Purple + Indigo + Blue + Cyan + Teal + Green + Lime + Yellow + Amber + Orange + Red + Pink +) + +var materialColorNames = map[MaterialColor]string{ + Black: "black", // 0 + Brown: "brown", // 1 + Grey: "grey", // 2 + White: "white", // 3 + Purple: "purple", // 4 + Indigo: "indigo", // 5 + Blue: "blue", // 6 + Cyan: "cyan", // 7 + Teal: "teal", // 8 + Green: "green", // 9 + Lime: "lime", // A + Yellow: "yellow", // B + Amber: "amber", // C + Orange: "orange", // D + Red: "red", // E + Pink: "pink", // F } -func deduplicate(s []string) []string { - seen := make(map[string]struct{}, len(s)) - j := 0 - for _, v := range s { - if _, ok := seen[v]; ok { - continue - } - seen[v] = struct{}{} - s[j] = v - j++ +var materialColorWeight = map[MaterialColor]uint16{ + Black: 2, + Brown: 1, + Grey: 2, + White: 2, + Purple: 5, + Indigo: 3, + Blue: 3, + Cyan: 4, + Teal: 4, + Green: 3, + Lime: 5, + Yellow: 5, + Amber: 5, + Orange: 5, + Red: 5, + Pink: 5, +} + +func (c MaterialColor) Name() string { + return materialColorNames[c] +} + +func (c MaterialColor) Hex() string { + return fmt.Sprintf("%X", c) +} + +func (c MaterialColors) Hex() (result string) { + for _, materialColor := range c { + result += materialColor.Hex() } - return s[:j] + + return result } -func getColorNames(actualColor colorful.Color) (result []string) { - var maxDistance = 0.27 +var materialColorMap = map[color.RGBA]MaterialColor{ + {0x00, 0x00, 0x00, 0xff}: Black, + {0x79, 0x55, 0x48, 0xff}: Brown, + {0x9E, 0x9E, 0x9E, 0xff}: Grey, + {0xFF, 0xFF, 0xFF, 0xff}: White, + {0x9c, 0x27, 0xb0, 0xff}: Purple, + {0x3F, 0x51, 0xB5, 0xff}: Indigo, + {0x21, 0x96, 0xF3, 0xff}: Blue, + {0x00, 0xBC, 0xD4, 0xff}: Cyan, + {0x00, 0x96, 0x88, 0xff}: Teal, + {0x4C, 0xAF, 0x50, 0xff}: Green, + {0xCD, 0xDC, 0x39, 0xff}: Lime, + {0xFF, 0xEB, 0x3B, 0xff}: Yellow, + {0xFF, 0xC1, 0x07, 0xff}: Amber, + {0xFF, 0x98, 0x00, 0xff}: Orange, + {0xf4, 0x43, 0x36, 0xff}: Red, + {0xe9, 0x1e, 0x63, 0xff}: Pink, +} - for colorName, colorRGBA := range colorMap { +func colorfulToMaterialColor(actualColor colorful.Color) (result MaterialColor) { + var distance = 1.0 + + for colorRGBA, materialColor := range materialColorMap { colorColorful, _ := colorful.MakeColor(colorRGBA) currentDistance := colorColorful.DistanceLab(actualColor) - if maxDistance >= currentDistance { - result = append(result, fmt.Sprintf("%s", colorName)) + if distance >= currentDistance { + distance = currentDistance + result = materialColor } } return result } -// GetColors returns color information for a given mediafiles. -func (m *MediaFile) GetColors() (colors []string, vibrantHex string, mutedHex string) { - file, _ := os.Open(m.filename) +// Colors returns color information for a media file. +func (m *MediaFile) Colors() (colors MaterialColors, mainColor MaterialColor, err error) { + jpeg, err := m.GetJpeg() - defer file.Close() + if err != nil { + log.Printf("can't find jpeg: %s", err.Error()) - decodedImage, _, _ := image.Decode(file) - - palette := vibrant.NewPaletteBuilder(decodedImage).Generate() - - if vibrantSwatch := palette.VibrantSwatch(); vibrantSwatch != nil { - color, _ := colorful.MakeColor(vibrantSwatch.Color()) - vibrantHex = color.Hex() + return colors, mainColor, err } - if mutedSwatch := palette.MutedSwatch(); mutedSwatch != nil { - color, _ := colorful.MakeColor(mutedSwatch.Color()) - mutedHex = color.Hex() + img, err := imaging.Open(jpeg.GetFilename(), imaging.AutoOrientation(true)) + + if err != nil { + log.Printf("can't open jpeg: %s", err.Error()) + + return colors, mainColor, err } - centroids, err := prominentcolor.KmeansWithAll(5, decodedImage, prominentcolor.ArgumentDefault|prominentcolor.ArgumentNoCropping, prominentcolor.DefaultSize, prominentcolor.GetDefaultMasks()) - if err == nil { - for _, centroid := range centroids { - colorfulColor, _ := colorful.MakeColor(color.RGBA{R: uint8(centroid.Color.R), G: uint8(centroid.Color.G), B: uint8(centroid.Color.B), A: 0xff}) - colors = append(colors, getColorNames(colorfulColor)...) + img = imaging.Resize(img, ColorSampleSize, ColorSampleSize, imaging.Box) + + bounds := img.Bounds() + width, height := bounds.Max.X, bounds.Max.Y + + colorCount := make(map[MaterialColor]uint16) + var mainColorCount uint16 + + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + r, g, b, a := img.At(x, y).RGBA() + rgbColor, _ := colorful.MakeColor(color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}) + materialColor := colorfulToMaterialColor(rgbColor) + colors = append(colors, materialColor) + + if _, ok := colorCount[materialColor]; ok == true { + colorCount[materialColor] += materialColorWeight[materialColor] + } else { + colorCount[materialColor] = materialColorWeight[materialColor] + } + + if colorCount[materialColor] > mainColorCount { + mainColorCount = colorCount[materialColor] + mainColor = materialColor + } + } - colors = deduplicate(colors) - sort.Strings(colors) - } else { - log.Printf("Unable to detect most dominent color in image: %s", err) } - return colors, vibrantHex, mutedHex + return colors, mainColor, nil } diff --git a/internal/photoprism/colors_slow_test.go b/internal/photoprism/colors_slow_test.go index 73e14476f..9d701e7b0 100644 --- a/internal/photoprism/colors_slow_test.go +++ b/internal/photoprism/colors_slow_test.go @@ -15,26 +15,29 @@ func TestMediaFile_GetColors_Slow(t *testing.T) { conf.InitializeTestData(t) if mediaFile2, err := NewMediaFile(conf.ImportPath() + "/iphone/IMG_6788.JPG"); err == nil { + colors, main, err := mediaFile2.Colors() - names, vibrantHex, mutedHex := mediaFile2.GetColors() + t.Log(colors, main, err) - t.Log(names, vibrantHex, mutedHex) - - assert.Equal(t, "#3d85c3", vibrantHex) - assert.Equal(t, "#988570", mutedHex) - assert.Equal(t, []string([]string{"black", "brown", "grey", "white"}), names); + assert.Nil(t, err) + assert.IsType(t, MaterialColors{}, colors) + assert.Equal(t, "grey", main.Name()) + assert.Equal(t, MaterialColors{0x2, 0x1, 0x2, 0x1, 0x1, 0x1, 0x2, 0x1, 0x2}, colors) } else { t.Error(err) } if mediaFile3, err := NewMediaFile(conf.ImportPath() + "/raw/20140717_154212_1EC48F8489.jpg"); err == nil { + colors, main, err := mediaFile3.Colors() - names, vibrantHex, mutedHex := mediaFile3.GetColors() + t.Log(colors, main, err) + + assert.Nil(t, err) + assert.IsType(t, MaterialColors{}, colors) + assert.Equal(t, "grey", main.Name()) + + assert.Equal(t, MaterialColors{0x3, 0x2, 0x2, 0x1, 0x2, 0x2, 0x2, 0x2, 0x1}, colors) - t.Log(names, vibrantHex, mutedHex) - assert.Equal(t, []string([]string{"black", "brown", "grey"}), names); - assert.Equal(t, "#d5d437", vibrantHex) - assert.Equal(t, "#a69f55", mutedHex) } else { t.Error(err) } diff --git a/internal/photoprism/colors_test.go b/internal/photoprism/colors_test.go index 21885b6e9..7b42d5de0 100644 --- a/internal/photoprism/colors_test.go +++ b/internal/photoprism/colors_test.go @@ -13,27 +13,27 @@ func TestMediaFile_GetColors(t *testing.T) { conf.InitializeTestData(t) if mediaFile1, err := NewMediaFile(conf.ImportPath() + "/dog.jpg"); err == nil { - names, vibrantHex, mutedHex := mediaFile1.GetColors() + colors, main, err := mediaFile1.Colors() - t.Log(names, vibrantHex, mutedHex) + t.Log(colors, main, err) - assert.IsType(t, []string{}, names) - assert.Equal(t, "#e0ed21", vibrantHex) - assert.Equal(t, "#977d67", mutedHex) - assert.Equal(t, []string([]string{"black", "brown", "grey", "white"}), names); + assert.Nil(t, err) + assert.IsType(t, MaterialColors{}, colors) + assert.Equal(t, "grey", main.Name()) + assert.Equal(t, MaterialColors{0x1, 0x2, 0x1, 0x2, 0x2, 0x1, 0x1, 0x1, 0x0}, colors) } else { t.Error(err) } if mediaFile2, err := NewMediaFile(conf.ImportPath() + "/ape.jpeg"); err == nil { - names, vibrantHex, mutedHex := mediaFile2.GetColors() + colors, main, err := mediaFile2.Colors() - t.Log(names, vibrantHex, mutedHex) + t.Log(colors, main, err) - assert.IsType(t, []string{}, names) - assert.Equal(t, "#97c84a", vibrantHex) - assert.Equal(t, "#6c9a68", mutedHex) - assert.Equal(t, []string([]string{"grey", "teal", "white"}), names); + assert.Nil(t, err) + assert.IsType(t, MaterialColors{}, colors) + assert.Equal(t, "teal", main.Name()) + assert.Equal(t, MaterialColors{0x8, 0x8, 0x2, 0x8, 0x2, 0x1, 0x8, 0x1, 0x2}, colors) } else { t.Error(err) } diff --git a/internal/photoprism/indexer.go b/internal/photoprism/indexer.go index 29ea55d73..4542c4e69 100644 --- a/internal/photoprism/indexer.go +++ b/internal/photoprism/indexer.go @@ -76,7 +76,6 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string { var photo models.Photo var file, primaryFile models.File var isPrimary = false - var colorNames []string var tags []*models.Tag canonicalName := mediaFile.GetCanonicalNameFromFile() @@ -95,9 +94,10 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string { } // PhotoColors - colorNames, photo.PhotoVibrantColor, photo.PhotoMutedColor = jpeg.GetColors() + photoColors, photoColor, _ := jpeg.Colors() - photo.PhotoColors = strings.Join(colorNames, ", ") + photo.PhotoColor = photoColor.Name() + photo.PhotoColors = photoColors.Hex() // Tags (TensorFlow) tags = i.getImageTags(jpeg) @@ -163,9 +163,10 @@ func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string { } else if time.Now().Sub(photo.UpdatedAt).Minutes() > 10 { // If updated more than 10 minutes ago if jpeg, err := mediaFile.GetJpeg(); err == nil { // PhotoColors - colorNames, photo.PhotoVibrantColor, photo.PhotoMutedColor = jpeg.GetColors() + photoColors, photoColor, _ := jpeg.Colors() - photo.PhotoColors = strings.Join(colorNames, ", ") + photo.PhotoColor = photoColor.Name() + photo.PhotoColors = photoColors.Hex() photo.Camera = models.NewCamera(mediaFile.GetCameraModel(), mediaFile.GetCameraMake()).FirstOrCreate(i.db) photo.Lens = models.NewLens(mediaFile.GetLensModel(), mediaFile.GetLensMake()).FirstOrCreate(i.db) diff --git a/internal/photoprism/search.go b/internal/photoprism/search.go index fe5ef319b..525f76b46 100644 --- a/internal/photoprism/search.go +++ b/internal/photoprism/search.go @@ -33,8 +33,7 @@ type PhotoSearchResult struct { PhotoArtist string PhotoKeywords string PhotoColors string - PhotoVibrantColor string - PhotoMutedColor string + PhotoColor string PhotoCanonicalName string PhotoLat float64 PhotoLong float64 @@ -118,7 +117,7 @@ func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, error) if form.Query != "" { likeString := "%" + strings.ToLower(form.Query) + "%" - q = q.Where("tags.tag_label LIKE ? OR LOWER(photo_title) LIKE ? OR LOWER(photo_colors) LIKE ?", likeString, likeString, likeString) + q = q.Where("tags.tag_label LIKE ? OR LOWER(photo_title) LIKE ? OR LOWER(photo_color) LIKE ?", likeString, likeString, likeString) } if form.CameraID > 0 { diff --git a/internal/photoprism/tensorflow.go b/internal/photoprism/tensorflow.go index ce57b4863..096b51f70 100644 --- a/internal/photoprism/tensorflow.go +++ b/internal/photoprism/tensorflow.go @@ -163,7 +163,7 @@ func (t *TensorFlow) makeTensorFromImage(image string, imageFormat string) (*tf. // Creates a graph to decode, resize and normalize an image func (t *TensorFlow) makeTransformImageGraph(imageFormat string) ( - graph *tf.Graph, input, output tf.Output, err error) { + graph *tf.Graph, input, output tf.Output, err error) { const ( H, W = 224, 224 Mean = float32(117)