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:
parent
94205f0113
commit
ffc64cceb0
13 changed files with 217 additions and 132 deletions
3
Makefile
3
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
|
||||
|
|
|
@ -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': [
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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
14
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
|
||||
|
|
29
go.sum
29
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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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++
|
||||
}
|
||||
return s[: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 getColorNames(actualColor colorful.Color) (result []string) {
|
||||
var maxDistance = 0.27
|
||||
func (c MaterialColor) Name() string {
|
||||
return materialColorNames[c]
|
||||
}
|
||||
|
||||
for colorName, colorRGBA := range colorMap {
|
||||
func (c MaterialColor) Hex() string {
|
||||
return fmt.Sprintf("%X", c)
|
||||
}
|
||||
|
||||
func (c MaterialColors) Hex() (result string) {
|
||||
for _, materialColor := range c {
|
||||
result += materialColor.Hex()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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)...)
|
||||
}
|
||||
colors = deduplicate(colors)
|
||||
sort.Strings(colors)
|
||||
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 {
|
||||
log.Printf("Unable to detect most dominent color in image: %s", err)
|
||||
colorCount[materialColor] = materialColorWeight[materialColor]
|
||||
}
|
||||
|
||||
return colors, vibrantHex, mutedHex
|
||||
if colorCount[materialColor] > mainColorCount {
|
||||
mainColorCount = colorCount[materialColor]
|
||||
mainColor = materialColor
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return colors, mainColor, nil
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue