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).
This commit is contained in:
Michael Mayer 2019-04-26 02:22:53 +02:00
parent 94205f0113
commit ffc64cceb0
13 changed files with 217 additions and 132 deletions

View file

@ -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

View file

@ -63,15 +63,9 @@
></v-text-field>
<v-spacer></v-spacer>
<v-text-field
v-model="vibrantColor"
label="Vibrant color"
placeholder="white"
></v-text-field>
<v-spacer></v-spacer>
<v-text-field
v-model="mutedColor"
label="Muted color"
placeholder="blue"
v-model="color"
label="Color"
placeholder="unknown"
></v-text-field>
<v-spacer></v-spacer>
@ -229,6 +223,7 @@
'activator': null,
'attach': null,
'colors': ['green', 'purple', 'indigo', 'primary', 'success', 'orange'],
'color': '',
'editing': null,
'index': -1,
'items': [

View file

@ -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' },

View file

@ -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;

14
go.mod
View file

@ -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

29
go.sum
View file

@ -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=

View file

@ -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

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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 {

View file

@ -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)