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)