People: Add face clustering worker #22

Work in progress. No performance optimizations yet.
This commit is contained in:
Michael Mayer 2021-08-12 04:54:20 +02:00
parent 733b84a03b
commit 1fc4ef123b
22 changed files with 637 additions and 65 deletions

View file

@ -56,6 +56,7 @@ func main() {
commands.IndexCommand, commands.IndexCommand,
commands.ImportCommand, commands.ImportCommand,
commands.MomentsCommand, commands.MomentsCommand,
commands.PeopleCommand,
commands.OptimizeCommand, commands.OptimizeCommand,
commands.PurgeCommand, commands.PurgeCommand,
commands.CleanUpCommand, commands.CleanUpCommand,

2
go.mod
View file

@ -45,6 +45,7 @@ require (
github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/mpraski/clusters v0.0.0-20171016094157-18104487c312
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/paulmach/go.geojson v1.4.0 github.com/paulmach/go.geojson v1.4.0
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
@ -64,6 +65,7 @@ require (
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d
gonum.org/v1/gonum v0.9.3 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/photoprism/go-tz.v2 v2.1.1 gopkg.in/photoprism/go-tz.v2 v2.1.1
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0

45
go.sum
View file

@ -15,12 +15,15 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@ -81,6 +84,7 @@ github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DP
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/esimov/pigo v1.4.4 h1:Ab9uYXw0F0Y7OyZQQGwJjktl5LlHdL3ovdXe/T0juK8= github.com/esimov/pigo v1.4.4 h1:Ab9uYXw0F0Y7OyZQQGwJjktl5LlHdL3ovdXe/T0juK8=
github.com/esimov/pigo v1.4.4/go.mod h1:SGkOUpm4wlEmQQJKlaymAkThY8/8iP+XE0gFo7g8G6w= github.com/esimov/pigo v1.4.4/go.mod h1:SGkOUpm4wlEmQQJKlaymAkThY8/8iP+XE0gFo7g8G6w=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k= github.com/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k=
github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc= github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc=
@ -94,8 +98,13 @@ github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.4.0 h1:2OA7MFw38+e9na72T1xgkomPb6GzZzzxvJ5U630FoRM= github.com/go-errors/errors v1.4.0 h1:2OA7MFw38+e9na72T1xgkomPb6GzZzzxvJ5U630FoRM=
github.com/go-errors/errors v1.4.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.4.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
@ -175,6 +184,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
@ -225,12 +236,18 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mpraski/clusters v0.0.0-20171016094157-18104487c312 h1:XDW24M0xpJ83twch860OuhzUPKWfVOg2qoDBtYOo+UY=
github.com/mpraski/clusters v0.0.0-20171016094157-18104487c312/go.mod h1:1wDbOlBLClLuyu3ggcgsE1QGcWd1/LywIS9JymHVgZg=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY= github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY=
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs= github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -239,6 +256,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
@ -294,18 +312,30 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd h1:zkO/Lhoka23X63N9OSzpSeROEUQ5ODw47tM3YWjygbs=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -344,8 +374,6 @@ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -378,6 +406,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
@ -389,12 +418,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@ -407,6 +439,7 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -422,6 +455,13 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@ -480,5 +520,6 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View file

@ -9,7 +9,7 @@ import (
"github.com/urfave/cli" "github.com/urfave/cli"
) )
// MomentsCommand registers the index cli command. // MomentsCommand registers the moments cli command.
var MomentsCommand = cli.Command{ var MomentsCommand = cli.Command{
Name: "moments", Name: "moments",
Usage: "Creates albums based on popular locations, dates and labels", Usage: "Creates albums based on popular locations, dates and labels",

View file

@ -0,0 +1,48 @@
package commands
import (
"context"
"time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
"github.com/urfave/cli"
)
// PeopleCommand registers the people cli command.
var PeopleCommand = cli.Command{
Name: "people",
Usage: "Performs face clustering and recognition",
Action: peopleAction,
}
// peopleAction performs face clustering and recognition.
func peopleAction(ctx *cli.Context) error {
start := time.Now()
conf := config.NewConfig(ctx)
service.SetConfig(conf)
_, cancel := context.WithCancel(context.Background())
defer cancel()
if err := conf.Init(); err != nil {
return err
}
conf.InitDb()
people := service.People()
if err := people.Start(); err != nil {
return err
} else {
elapsed := time.Since(start)
log.Infof("completed in %s", elapsed)
}
conf.Shutdown()
return nil
}

View file

@ -0,0 +1,53 @@
package entity
import (
"encoding/json"
"strings"
)
type Embedding = []float64
// Embeddings represents marker face embeddings.
type Embeddings = []Embedding
// EmbeddingsMidpoint returns the embeddings vector midpoint.
func EmbeddingsMidpoint(m Embeddings) (result Embedding) {
for i, emb := range m {
if i == 0 {
result = emb
continue
}
for j, val := range result {
result[j] = (val + emb[j]) / 2
}
}
return result
}
// UnmarshalEmbeddings parses face embedding JSON.
func UnmarshalEmbeddings(s string) (result Embeddings) {
if !strings.HasPrefix(s, "[[") {
return nil
}
if err := json.Unmarshal([]byte(s), &result); err != nil {
log.Errorf("faces: %s", err)
}
return result
}
// UnmarshalEmbedding parses a single face embedding JSON.
func UnmarshalEmbedding(s string) (result Embedding) {
if !strings.HasPrefix(s, "[") {
return nil
}
if err := json.Unmarshal([]byte(s), &result); err != nil {
log.Errorf("faces: %s", err)
}
return result
}

View file

@ -30,33 +30,34 @@ type Types map[string]interface{}
// Entities contains database entities and their table names. // Entities contains database entities and their table names.
var Entities = Types{ var Entities = Types{
"errors": &Error{}, "errors": &Error{},
"addresses": &Address{}, "addresses": &Address{},
"users": &User{}, "users": &User{},
"accounts": &Account{}, "accounts": &Account{},
"folders": &Folder{}, "folders": &Folder{},
"duplicates": &Duplicate{}, "duplicates": &Duplicate{},
"files": &File{}, "files": &File{},
"files_share": &FileShare{}, "files_share": &FileShare{},
"files_sync": &FileSync{}, "files_sync": &FileSync{},
"photos": &Photo{}, "photos": &Photo{},
"details": &Details{}, "details": &Details{},
"places": &Place{}, "places": &Place{},
"cells": &Cell{}, "cells": &Cell{},
"cameras": &Camera{}, "cameras": &Camera{},
"lenses": &Lens{}, "lenses": &Lens{},
"countries": &Country{}, "countries": &Country{},
"albums": &Album{}, "albums": &Album{},
"photos_albums": &PhotoAlbum{}, "photos_albums": &PhotoAlbum{},
"labels": &Label{}, "labels": &Label{},
"categories": &Category{}, "categories": &Category{},
"photos_labels": &PhotoLabel{}, "photos_labels": &PhotoLabel{},
"keywords": &Keyword{}, "keywords": &Keyword{},
"photos_keywords": &PhotoKeyword{}, "photos_keywords": &PhotoKeyword{},
"passwords": &Password{}, "passwords": &Password{},
"links": &Link{}, "links": &Link{},
"markers_dev": &Marker{}, Marker{}.TableName(): &Marker{},
"people_dev": &Person{}, Person{}.TableName(): &Person{},
PersonFace{}.TableName(): &PersonFace{},
} }
type RowCount struct { type RowCount struct {

View file

@ -21,15 +21,15 @@ const (
type Marker struct { type Marker struct {
ID uint `gorm:"primary_key" json:"ID" yaml:"-"` ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
FileID uint `gorm:"index;" json:"-" yaml:"-"` FileID uint `gorm:"index;" json:"-" yaml:"-"`
RefUID string `gorm:"type:VARBINARY(42);index;" json:"RefUID" yaml:"RefUID,omitempty"` Ref string `gorm:"type:VARBINARY(42);index;" json:"Ref" yaml:"Ref,omitempty"`
RefSrc string `gorm:"type:VARBINARY(8);default:'';" json:"RefSrc" yaml:"RefSrc,omitempty"` RefSrc string `gorm:"type:VARBINARY(8);default:'';" json:"RefSrc" yaml:"RefSrc,omitempty"`
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"` MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"` MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"`
MarkerScore int `gorm:"type:SMALLINT" json:"Score" yaml:"Score"` MarkerScore int `gorm:"type:SMALLINT" json:"Score" yaml:"Score"`
MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"` MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"`
MarkerLabel string `gorm:"type:VARCHAR(255);" json:"Label" yaml:"Label,omitempty"` MarkerLabel string `gorm:"type:VARCHAR(255);" json:"Label" yaml:"Label,omitempty"`
MarkerMeta string `gorm:"type:TEXT;" json:"Meta" yaml:"Meta,omitempty"` MarkerMeta string `gorm:"type:LONGTEXT;" json:"Meta" yaml:"Meta,omitempty"`
Embeddings string `gorm:"type:TEXT;" json:"Embeddings" yaml:"Embeddings,omitempty"` Embeddings string `gorm:"type:LONGTEXT;" json:"Embeddings" yaml:"Embeddings,omitempty"`
X float32 `gorm:"type:FLOAT;" json:"X" yaml:"X,omitempty"` X float32 `gorm:"type:FLOAT;" json:"X" yaml:"X,omitempty"`
Y float32 `gorm:"type:FLOAT;" json:"Y" yaml:"Y,omitempty"` Y float32 `gorm:"type:FLOAT;" json:"Y" yaml:"Y,omitempty"`
W float32 `gorm:"type:FLOAT;" json:"W" yaml:"W,omitempty"` W float32 `gorm:"type:FLOAT;" json:"W" yaml:"W,omitempty"`
@ -50,7 +50,7 @@ func (Marker) TableName() string {
func NewMarker(fileUID uint, refUID, markerSrc, markerType string, x, y, w, h float32) *Marker { func NewMarker(fileUID uint, refUID, markerSrc, markerType string, x, y, w, h float32) *Marker {
m := &Marker{ m := &Marker{
FileID: fileUID, FileID: fileUID,
RefUID: refUID, Ref: refUID,
MarkerSrc: markerSrc, MarkerSrc: markerSrc,
MarkerType: markerType, MarkerType: markerType,
X: x, X: x,
@ -120,6 +120,22 @@ func (m *Marker) Create() error {
return Db().Create(m).Error return Db().Create(m).Error
} }
// UnmarshalEmbeddings parses face embedding JSON strings.
func (m *Marker) UnmarshalEmbeddings() (result Embeddings) {
return UnmarshalEmbeddings(m.Embeddings)
}
// FindMarker returns an existing row if exists.
func FindMarker(id uint) *Marker {
result := Marker{}
if err := Db().Where("id = ?", id).First(&result).Error; err == nil {
return &result
}
return nil
}
// UpdateOrCreateMarker updates a marker in the database or creates a new one if needed. // UpdateOrCreateMarker updates a marker in the database or creates a new one if needed.
func UpdateOrCreateMarker(m *Marker) (*Marker, error) { func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
const d = 0.07 const d = 0.07
@ -146,7 +162,7 @@ func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
"MarkerScore": m.MarkerScore, "MarkerScore": m.MarkerScore,
"MarkerMeta": m.MarkerMeta, "MarkerMeta": m.MarkerMeta,
"Embeddings": m.Embeddings, "Embeddings": m.Embeddings,
"RefUID": m.RefUID, "Ref": m.Ref,
}) })
log.Debugf("faces: updated existing marker %d for file %d", result.ID, result.FileID) log.Debugf("faces: updated existing marker %d for file %d", result.ID, result.FileID)

File diff suppressed because one or more lines are too long

View file

@ -15,7 +15,7 @@ func TestNewMarker(t *testing.T) {
m := NewMarker(1000000, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556) m := NewMarker(1000000, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
assert.IsType(t, &Marker{}, m) assert.IsType(t, &Marker{}, m)
assert.Equal(t, uint(1000000), m.FileID) assert.Equal(t, uint(1000000), m.FileID)
assert.Equal(t, "lt9k3pw1wowuy3c3", m.RefUID) assert.Equal(t, "lt9k3pw1wowuy3c3", m.Ref)
assert.Equal(t, SrcImage, m.MarkerSrc) assert.Equal(t, SrcImage, m.MarkerSrc)
assert.Equal(t, MarkerLabel, m.MarkerType) assert.Equal(t, MarkerLabel, m.MarkerType)
} }
@ -25,7 +25,7 @@ func TestUpdateOrCreateMarker(t *testing.T) {
m := NewMarker(1000000, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556) m := NewMarker(1000000, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
assert.IsType(t, &Marker{}, m) assert.IsType(t, &Marker{}, m)
assert.Equal(t, uint(1000000), m.FileID) assert.Equal(t, uint(1000000), m.FileID)
assert.Equal(t, "lt9k3pw1wowuy3c3", m.RefUID) assert.Equal(t, "lt9k3pw1wowuy3c3", m.Ref)
assert.Equal(t, SrcImage, m.MarkerSrc) assert.Equal(t, SrcImage, m.MarkerSrc)
assert.Equal(t, MarkerLabel, m.MarkerType) assert.Equal(t, MarkerLabel, m.MarkerType)

View file

@ -46,14 +46,14 @@ type Person struct {
PersonUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"` PersonUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
PersonSlug string `gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"-"` PersonSlug string `gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"-"`
PersonName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name"` PersonName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name"`
PersonSrc string `gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src"`
PersonFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"` PersonFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
PersonPrivate bool `json:"Private" yaml:"Private,omitempty"` PersonPrivate bool `json:"Private" yaml:"Private,omitempty"`
PersonHidden bool `json:"Hidden" yaml:"Hidden,omitempty"` PersonHidden bool `json:"Hidden" yaml:"Hidden,omitempty"`
PersonDescription string `gorm:"type:TEXT;" json:"Description" yaml:"Description,omitempty"` PersonDescription string `gorm:"type:TEXT;" json:"Description" yaml:"Description,omitempty"`
PersonNotes string `gorm:"type:TEXT;" json:"Notes" yaml:"Notes,omitempty"` PersonNotes string `gorm:"type:TEXT;" json:"Notes" yaml:"Notes,omitempty"`
PersonMeta string `gorm:"type:TEXT;" json:"Meta" yaml:"Meta,omitempty"` PersonMeta string `gorm:"type:LONGTEXT;" json:"Meta" yaml:"Meta,omitempty"`
Embeddings string `gorm:"type:TEXT;" json:"Embeddings" yaml:"Embeddings,omitempty"` PhotoCount int `gorm:"default:0" json:"PhotoCount" yaml:"-"`
PhotoCount int `gorm:"default:1" json:"PhotoCount" yaml:"-"`
BirthYear int `json:"BirthYear" yaml:"BirthYear,omitempty"` BirthYear int `json:"BirthYear" yaml:"BirthYear,omitempty"`
BirthMonth int `json:"BirthMonth" yaml:"BirthMonth,omitempty"` BirthMonth int `json:"BirthMonth" yaml:"BirthMonth,omitempty"`
BirthDay int `json:"BirthDay" yaml:"BirthDay,omitempty"` BirthDay int `json:"BirthDay" yaml:"BirthDay,omitempty"`
@ -95,20 +95,15 @@ func (m *Person) BeforeCreate(scope *gorm.Scope) error {
} }
// NewPerson returns a new person. // NewPerson returns a new person.
func NewPerson(name string) *Person { func NewPerson(personName, personSrc string, photoCount int) *Person {
personName := txt.Clip(name, txt.ClipDefault) personName = txt.Title(txt.Clip(personName, txt.ClipDefault))
if personName == "" {
personName = "Unknown"
}
personName = txt.Title(personName)
personSlug := slug.Make(txt.Clip(personName, txt.ClipSlug)) personSlug := slug.Make(txt.Clip(personName, txt.ClipSlug))
result := &Person{ result := &Person{
PersonSlug: personSlug, PersonSlug: personSlug,
PersonName: personName, PersonName: personName,
PhotoCount: 1, PersonSrc: personSrc,
PhotoCount: photoCount,
} }
return result return result
@ -135,7 +130,7 @@ func (m *Person) Delete() error {
return Db().Delete(m).Error return Db().Delete(m).Error
} }
// Deleted returns true if the label is deleted. // Deleted returns true if the person is deleted.
func (m *Person) Deleted() bool { func (m *Person) Deleted() bool {
return m.DeletedAt != nil return m.DeletedAt != nil
} }

View file

@ -0,0 +1,90 @@
package entity
import (
"crypto/sha1"
"fmt"
"time"
)
type PeopleFaces []PersonFace
// PersonFace represents the face of a Person.
type PersonFace struct {
ID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
PersonUID string `gorm:"type:VARBINARY(42);index;" json:"PersonUID" yaml:"PersonUID"`
FaceSrc string `gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src"`
Embedding string `gorm:"type:LONGTEXT;" json:"Embedding" yaml:"Embedding,omitempty"`
PhotoCount int `gorm:"default:0" json:"PhotoCount" yaml:"-"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
}
// TableName returns the entity database table name.
func (PersonFace) TableName() string {
return "people_faces_dev"
}
/*
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *PersonFace) BeforeCreate(scope *gorm.Scope) error {
return scope.SetColumn("ID")
}*/
// NewPersonFace returns a new face.
func NewPersonFace(personUID, faceSrc, embedding string, photoCount int) *PersonFace {
result := &PersonFace{
ID: fmt.Sprintf("%x", sha1.Sum([]byte(embedding))),
PersonUID: personUID,
FaceSrc: faceSrc,
Embedding: embedding,
PhotoCount: photoCount,
}
return result
}
// UnmarshalEmbedding parses the face embedding JSON string.
func (m *PersonFace) UnmarshalEmbedding() (result Embedding) {
return UnmarshalEmbedding(m.Embedding)
}
// Save updates the existing or inserts a new face.
func (m *PersonFace) Save() error {
peopleMutex.Lock()
defer peopleMutex.Unlock()
return Db().Save(m).Error
}
// Create inserts the face to the database.
func (m *PersonFace) Create() error {
peopleMutex.Lock()
defer peopleMutex.Unlock()
return Db().Create(m).Error
}
// Delete removes the face from the database.
func (m *PersonFace) Delete() error {
return Db().Delete(m).Error
}
// Deleted returns true if the face is deleted.
func (m *PersonFace) Deleted() bool {
return m.DeletedAt != nil
}
// Restore restores the face in the database.
func (m *PersonFace) Restore() error {
if m.Deleted() {
return UnscopedDb().Model(m).Update("DeletedAt", nil).Error
}
return nil
}
// Update a face property in the database.
func (m *PersonFace) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
}

View file

@ -9,7 +9,7 @@ import (
func TestNewPerson(t *testing.T) { func TestNewPerson(t *testing.T) {
t.Run("Jens_Mander", func(t *testing.T) { t.Run("Jens_Mander", func(t *testing.T) {
m := NewPerson("Jens Mander") m := NewPerson("Jens Mander", SrcAuto, 0)
assert.Equal(t, "Jens Mander", m.PersonName) assert.Equal(t, "Jens Mander", m.PersonName)
assert.Equal(t, "jens-mander", m.PersonSlug) assert.Equal(t, "jens-mander", m.PersonSlug)
}) })
@ -17,7 +17,7 @@ func TestNewPerson(t *testing.T) {
func TestPerson_SetName(t *testing.T) { func TestPerson_SetName(t *testing.T) {
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
m := NewPerson("Jens Mander") m := NewPerson("Jens Mander", SrcAuto, 0)
assert.Equal(t, "Jens Mander", m.PersonName) assert.Equal(t, "Jens Mander", m.PersonName)
assert.Equal(t, "jens-mander", m.PersonSlug) assert.Equal(t, "jens-mander", m.PersonSlug)
@ -30,7 +30,7 @@ func TestPerson_SetName(t *testing.T) {
} }
func TestFirstOrCreatePerson(t *testing.T) { func TestFirstOrCreatePerson(t *testing.T) {
m := NewPerson("Create Me") m := NewPerson("Create Me", SrcAuto, 0)
result := FirstOrCreatePerson(m) result := FirstOrCreatePerson(m)
if result == nil { if result == nil {
@ -43,7 +43,7 @@ func TestFirstOrCreatePerson(t *testing.T) {
func TestPerson_Save(t *testing.T) { func TestPerson_Save(t *testing.T) {
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
m := NewPerson("Save Me") m := NewPerson("Save Me", SrcAuto, 0)
initialDate := m.UpdatedAt initialDate := m.UpdatedAt
err := m.Save() err := m.Save()
@ -60,7 +60,7 @@ func TestPerson_Save(t *testing.T) {
func TestPerson_Delete(t *testing.T) { func TestPerson_Delete(t *testing.T) {
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
m := NewPerson("Jens Mander") m := NewPerson("Jens Mander", SrcAuto, 0)
err := m.Save() err := m.Save()
assert.False(t, m.Deleted()) assert.False(t, m.Deleted())
@ -114,7 +114,7 @@ func TestPerson_Restore(t *testing.T) {
func TestFindPerson(t *testing.T) { func TestFindPerson(t *testing.T) {
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
m := NewPerson("Find Me") m := NewPerson("Find Me", SrcAuto, 0)
err := m.Save() err := m.Save()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -139,7 +139,7 @@ func TestPerson_Links(t *testing.T) {
func TestPerson_Update(t *testing.T) { func TestPerson_Update(t *testing.T) {
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
m := NewPerson("Update Me") m := NewPerson("Update Me", SrcAuto, 0)
if err := m.Save(); err != nil { if err := m.Save(); err != nil {
t.Fatal(err) t.Fatal(err)

View file

@ -4,7 +4,7 @@ import "github.com/ulule/deepcopier"
// Marker represents an image marker edit form. // Marker represents an image marker edit form.
type Marker struct { type Marker struct {
RefUID string `json:"RefUID"` Ref string `json:"Ref"`
RefSrc string `json:"RefSrc"` RefSrc string `json:"RefSrc"`
MarkerSrc string `json:"Src"` MarkerSrc string `json:"Src"`
MarkerType string `json:"Type"` MarkerType string `json:"Type"`

View file

@ -9,7 +9,7 @@ import (
func TestNewMarker(t *testing.T) { func TestNewMarker(t *testing.T) {
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
var m = struct { var m = struct {
RefUID string Ref string
RefSrc string RefSrc string
MarkerSrc string MarkerSrc string
MarkerType string MarkerType string
@ -17,7 +17,7 @@ func TestNewMarker(t *testing.T) {
MarkerInvalid bool MarkerInvalid bool
MarkerLabel string MarkerLabel string
}{ }{
RefUID: "3h59wvth837b5vyiub35", Ref: "3h59wvth837b5vyiub35",
RefSrc: "meta", RefSrc: "meta",
MarkerSrc: "image", MarkerSrc: "image",
MarkerType: "Face", MarkerType: "Face",
@ -32,7 +32,7 @@ func TestNewMarker(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
assert.Equal(t, "3h59wvth837b5vyiub35", f.RefUID) assert.Equal(t, "3h59wvth837b5vyiub35", f.Ref)
assert.Equal(t, "meta", f.RefSrc) assert.Equal(t, "meta", f.RefSrc)
assert.Equal(t, "image", f.MarkerSrc) assert.Equal(t, "image", f.MarkerSrc)
assert.Equal(t, "Face", f.MarkerType) assert.Equal(t, "Face", f.MarkerType)

View file

@ -19,7 +19,7 @@ type Moments struct {
conf *config.Config conf *config.Config
} }
// NewMoments returns a new purge worker. // NewMoments returns a new Moments worker.
func NewMoments(conf *config.Config) *Moments { func NewMoments(conf *config.Config) *Moments {
instance := &Moments{ instance := &Moments{
conf: conf, conf: conf,

View file

@ -0,0 +1,180 @@
package photoprism
import (
"encoding/json"
"fmt"
"runtime/debug"
"time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/query"
"github.com/mpraski/clusters"
)
// People represents a worker that clusters face embeddings to search for individual people.
type People struct {
conf *config.Config
}
// NewPeople returns a new People worker.
func NewPeople(conf *config.Config) *People {
instance := &People{
conf: conf,
}
return instance
}
// Start clusters face embeddings to search for individual people.
func (m *People) Start() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("people: %s (panic)\nstack: %s", r, debug.Stack())
log.Error(err)
}
}()
if err := mutex.MainWorker.Start(); err != nil {
return err
}
defer mutex.MainWorker.Stop()
embeddings, err := query.Embeddings()
if err != nil {
return err
}
if len(embeddings) == 0 {
log.Infof("people: no faces detected")
return nil
}
// see https://fse.studenttheses.ub.rug.nl/18064/1/Report_research_internship.pdf
c, e := clusters.DBSCAN(1, 0.42, 1, clusters.EuclideanDistance)
if e != nil {
return e
}
if err := c.Learn(embeddings); err != nil {
log.Errorf("people: %s", err)
}
sizes := c.Sizes()
log.Infof("people: found %d faces from %d people", len(embeddings), len(sizes))
faceClusters := make([]entity.Embeddings, len(sizes))
for i, _ := range sizes {
faceClusters[i] = entity.Embeddings{}
}
guesses := c.Guesses()
for index, number := range guesses {
if number < 1 {
continue
}
faceClusters[number-1] = append(faceClusters[number-1], embeddings[index])
}
for _, clusterEmb := range faceClusters {
if emb, err := json.Marshal(entity.EmbeddingsMidpoint(clusterEmb)); err != nil {
log.Errorf("people: %s", err)
} else if f := entity.NewPersonFace("", entity.SrcImage, string(emb), len(clusterEmb)); f == nil {
log.Errorf("people: face should not be nil - bug?")
} else if err := f.Save(); err != nil {
log.Errorf("people: %s while saving face", err)
}
}
if err := query.PurgeUnknownFaces(); err != nil {
log.Errorf("people: %s", err)
}
peopleFaces, err := query.PeopleFaces()
if err != nil {
return err
}
faceMap := make(map[string]entity.Embedding)
for _, f := range peopleFaces {
var id string
if f.PersonUID != "" {
id = f.PersonUID
} else {
id = f.ID
}
faceMap[id] = f.UnmarshalEmbedding()
}
limit := 500
offset := 0
for {
markers, err := query.Markers(limit, offset, entity.MarkerFace, true, false)
if err != nil {
return err
}
if len(markers) == 0 {
break
}
for _, marker := range markers {
if mutex.MainWorker.Canceled() {
return fmt.Errorf("people: worker canceled")
}
if _, ok := faceMap[marker.Ref]; ok {
continue
}
var ref string
var dist float64
for _, e1 := range marker.UnmarshalEmbeddings() {
for id, e2 := range faceMap {
if d := clusters.EuclideanDistance(e1, e2); ref == "" || d < dist {
ref = id
dist = d
}
}
}
if marker.Ref == ref {
continue
}
if err := marker.Update("Ref", ref); err != nil {
log.Errorf("people: %s while saving marker", err)
} else {
log.Debugf("people: marker %d ref %s", marker.ID, ref)
}
}
offset += limit
time.Sleep(50 * time.Millisecond)
}
return nil
}
// Cancel stops the current operation.
func (m *People) Cancel() {
mutex.MainWorker.Cancel()
}

View file

@ -0,0 +1,18 @@
package photoprism
import (
"testing"
"github.com/photoprism/photoprism/internal/config"
)
func TestPeople_Start(t *testing.T) {
conf := config.TestConfig()
m := NewPeople(conf)
err := m.Start()
if err != nil {
t.Fatal(err)
}
}

View file

@ -13,3 +13,48 @@ func MarkerByID(id uint) (marker entity.Marker, err error) {
return marker, nil return marker, nil
} }
// Markers finds a list of file markers filtered by type, embeddings, and sorted by id.
func Markers(limit, offset int, markerType string, embeddings, noRef bool) (result entity.Markers, err error) {
stmt := Db()
if markerType != "" {
stmt = stmt.Where("marker_type = ?", markerType)
}
if embeddings {
stmt = stmt.Where("embeddings <> ''")
}
if noRef {
stmt = stmt.Where("ref = ''")
}
stmt = stmt.Order("id").Limit(limit).Offset(offset)
err = stmt.Find(&result).Error
return result, err
}
// Embeddings finds all face embeddings.
func Embeddings() (result entity.Embeddings, err error) {
var col []string
stmt := Db().
Model(&entity.Marker{}).
Where("marker_type = ?", entity.MarkerFace).
Where("embeddings <> ''").
Order("id")
if err := stmt.Pluck("embeddings", &col).Error; err != nil {
return result, err
}
for _, embeddingsJson := range col {
if embeddings := entity.UnmarshalEmbeddings(embeddingsJson); len(embeddings) > 0 {
result = append(result, embeddings...)
}
}
return result, nil
}

36
internal/query/people.go Normal file
View file

@ -0,0 +1,36 @@
package query
import (
"github.com/photoprism/photoprism/internal/entity"
)
// People finds a list of people.
func People(limit, offset int, embeddings bool) (result entity.People, err error) {
stmt := Db()
if embeddings {
stmt = stmt.Where("embeddings <> ''")
}
stmt = stmt.Order("id").Limit(limit).Offset(offset)
err = stmt.Find(&result).Error
return result, err
}
// PeopleFaces finds a list of faces.
func PeopleFaces() (result entity.PeopleFaces, err error) {
stmt := Db().
Order("id")
err = stmt.Find(&result).Error
return result, err
}
// PurgeUnknownFaces removes unknown faces from the index.
func PurgeUnknownFaces() error {
return UnscopedDb().Delete(
entity.PersonFace{},
"face_src = ? AND person_uid = '' AND updated_at < ?", entity.SrcImage, entity.Yesterday()).Error
}

View file

@ -0,0 +1,19 @@
package service
import (
"sync"
"github.com/photoprism/photoprism/internal/photoprism"
)
var oncePeople sync.Once
func initPeople() {
services.People = photoprism.NewPeople(Config())
}
func People() *photoprism.People {
oncePeople.Do(initPeople)
return services.People
}

View file

@ -25,6 +25,7 @@ var services struct {
Import *photoprism.Import Import *photoprism.Import
Index *photoprism.Index Index *photoprism.Index
Moments *photoprism.Moments Moments *photoprism.Moments
People *photoprism.People
Purge *photoprism.Purge Purge *photoprism.Purge
CleanUp *photoprism.CleanUp CleanUp *photoprism.CleanUp
Nsfw *nsfw.Detector Nsfw *nsfw.Detector