People: Add face clustering worker #22
Work in progress. No performance optimizations yet.
This commit is contained in:
parent
733b84a03b
commit
1fc4ef123b
|
@ -56,6 +56,7 @@ func main() {
|
|||
commands.IndexCommand,
|
||||
commands.ImportCommand,
|
||||
commands.MomentsCommand,
|
||||
commands.PeopleCommand,
|
||||
commands.OptimizeCommand,
|
||||
commands.PurgeCommand,
|
||||
commands.CleanUpCommand,
|
||||
|
|
2
go.mod
2
go.mod
|
@ -45,6 +45,7 @@ require (
|
|||
github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 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/paulmach/go.geojson v1.4.0
|
||||
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/image v0.0.0-20210628002857-a66eb6448b8d // indirect
|
||||
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
|
||||
gopkg.in/photoprism/go-tz.v2 v2.1.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
|
|
45
go.sum
45
go.sum
|
@ -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.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
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/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/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/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/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/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
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/esimov/pigo v1.4.4 h1:Ab9uYXw0F0Y7OyZQQGwJjktl5LlHdL3ovdXe/T0juK8=
|
||||
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/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k=
|
||||
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.4.0 h1:2OA7MFw38+e9na72T1xgkomPb6GzZzzxvJ5U630FoRM=
|
||||
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/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/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
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/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
|
||||
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/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
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 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
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/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
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/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
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/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
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-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
||||
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-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-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-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-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-20200207192155-f17229e696bd h1:zkO/Lhoka23X63N9OSzpSeROEUQ5ODw47tM3YWjygbs=
|
||||
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-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-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-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/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
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-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-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/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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-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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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/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/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-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-20190311212946-11955173bddd/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-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-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-20191113191852-77e3bb0ad9e7/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-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
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.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
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.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
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/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// MomentsCommand registers the index cli command.
|
||||
// MomentsCommand registers the moments cli command.
|
||||
var MomentsCommand = cli.Command{
|
||||
Name: "moments",
|
||||
Usage: "Creates albums based on popular locations, dates and labels",
|
||||
|
|
48
internal/commands/people.go
Normal file
48
internal/commands/people.go
Normal 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
|
||||
}
|
53
internal/entity/embeddings.go
Normal file
53
internal/entity/embeddings.go
Normal 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
|
||||
}
|
|
@ -30,33 +30,34 @@ type Types map[string]interface{}
|
|||
|
||||
// Entities contains database entities and their table names.
|
||||
var Entities = Types{
|
||||
"errors": &Error{},
|
||||
"addresses": &Address{},
|
||||
"users": &User{},
|
||||
"accounts": &Account{},
|
||||
"folders": &Folder{},
|
||||
"duplicates": &Duplicate{},
|
||||
"files": &File{},
|
||||
"files_share": &FileShare{},
|
||||
"files_sync": &FileSync{},
|
||||
"photos": &Photo{},
|
||||
"details": &Details{},
|
||||
"places": &Place{},
|
||||
"cells": &Cell{},
|
||||
"cameras": &Camera{},
|
||||
"lenses": &Lens{},
|
||||
"countries": &Country{},
|
||||
"albums": &Album{},
|
||||
"photos_albums": &PhotoAlbum{},
|
||||
"labels": &Label{},
|
||||
"categories": &Category{},
|
||||
"photos_labels": &PhotoLabel{},
|
||||
"keywords": &Keyword{},
|
||||
"photos_keywords": &PhotoKeyword{},
|
||||
"passwords": &Password{},
|
||||
"links": &Link{},
|
||||
"markers_dev": &Marker{},
|
||||
"people_dev": &Person{},
|
||||
"errors": &Error{},
|
||||
"addresses": &Address{},
|
||||
"users": &User{},
|
||||
"accounts": &Account{},
|
||||
"folders": &Folder{},
|
||||
"duplicates": &Duplicate{},
|
||||
"files": &File{},
|
||||
"files_share": &FileShare{},
|
||||
"files_sync": &FileSync{},
|
||||
"photos": &Photo{},
|
||||
"details": &Details{},
|
||||
"places": &Place{},
|
||||
"cells": &Cell{},
|
||||
"cameras": &Camera{},
|
||||
"lenses": &Lens{},
|
||||
"countries": &Country{},
|
||||
"albums": &Album{},
|
||||
"photos_albums": &PhotoAlbum{},
|
||||
"labels": &Label{},
|
||||
"categories": &Category{},
|
||||
"photos_labels": &PhotoLabel{},
|
||||
"keywords": &Keyword{},
|
||||
"photos_keywords": &PhotoKeyword{},
|
||||
"passwords": &Password{},
|
||||
"links": &Link{},
|
||||
Marker{}.TableName(): &Marker{},
|
||||
Person{}.TableName(): &Person{},
|
||||
PersonFace{}.TableName(): &PersonFace{},
|
||||
}
|
||||
|
||||
type RowCount struct {
|
||||
|
|
|
@ -21,15 +21,15 @@ const (
|
|||
type Marker struct {
|
||||
ID uint `gorm:"primary_key" json:"ID" 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"`
|
||||
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
|
||||
MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"`
|
||||
MarkerScore int `gorm:"type:SMALLINT" json:"Score" yaml:"Score"`
|
||||
MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"`
|
||||
MarkerLabel string `gorm:"type:VARCHAR(255);" json:"Label" yaml:"Label,omitempty"`
|
||||
MarkerMeta string `gorm:"type:TEXT;" json:"Meta" yaml:"Meta,omitempty"`
|
||||
Embeddings string `gorm:"type:TEXT;" json:"Embeddings" yaml:"Embeddings,omitempty"`
|
||||
MarkerMeta string `gorm:"type:LONGTEXT;" json:"Meta" yaml:"Meta,omitempty"`
|
||||
Embeddings string `gorm:"type:LONGTEXT;" json:"Embeddings" yaml:"Embeddings,omitempty"`
|
||||
X float32 `gorm:"type:FLOAT;" json:"X" yaml:"X,omitempty"`
|
||||
Y float32 `gorm:"type:FLOAT;" json:"Y" yaml:"Y,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 {
|
||||
m := &Marker{
|
||||
FileID: fileUID,
|
||||
RefUID: refUID,
|
||||
Ref: refUID,
|
||||
MarkerSrc: markerSrc,
|
||||
MarkerType: markerType,
|
||||
X: x,
|
||||
|
@ -120,6 +120,22 @@ func (m *Marker) Create() 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.
|
||||
func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
|
||||
const d = 0.07
|
||||
|
@ -146,7 +162,7 @@ func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
|
|||
"MarkerScore": m.MarkerScore,
|
||||
"MarkerMeta": m.MarkerMeta,
|
||||
"Embeddings": m.Embeddings,
|
||||
"RefUID": m.RefUID,
|
||||
"Ref": m.Ref,
|
||||
})
|
||||
|
||||
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
|
@ -15,7 +15,7 @@ func TestNewMarker(t *testing.T) {
|
|||
m := NewMarker(1000000, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 0.308333, 0.206944, 0.355556, 0.355556)
|
||||
assert.IsType(t, &Marker{}, m)
|
||||
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, 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)
|
||||
assert.IsType(t, &Marker{}, m)
|
||||
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, MarkerLabel, m.MarkerType)
|
||||
|
||||
|
|
|
@ -46,14 +46,14 @@ type Person struct {
|
|||
PersonUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
|
||||
PersonSlug string `gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"-"`
|
||||
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"`
|
||||
PersonPrivate bool `json:"Private" yaml:"Private,omitempty"`
|
||||
PersonHidden bool `json:"Hidden" yaml:"Hidden,omitempty"`
|
||||
PersonDescription string `gorm:"type:TEXT;" json:"Description" yaml:"Description,omitempty"`
|
||||
PersonNotes string `gorm:"type:TEXT;" json:"Notes" yaml:"Notes,omitempty"`
|
||||
PersonMeta string `gorm:"type:TEXT;" json:"Meta" yaml:"Meta,omitempty"`
|
||||
Embeddings string `gorm:"type:TEXT;" json:"Embeddings" yaml:"Embeddings,omitempty"`
|
||||
PhotoCount int `gorm:"default:1" json:"PhotoCount" yaml:"-"`
|
||||
PersonMeta string `gorm:"type:LONGTEXT;" json:"Meta" yaml:"Meta,omitempty"`
|
||||
PhotoCount int `gorm:"default:0" json:"PhotoCount" yaml:"-"`
|
||||
BirthYear int `json:"BirthYear" yaml:"BirthYear,omitempty"`
|
||||
BirthMonth int `json:"BirthMonth" yaml:"BirthMonth,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.
|
||||
func NewPerson(name string) *Person {
|
||||
personName := txt.Clip(name, txt.ClipDefault)
|
||||
|
||||
if personName == "" {
|
||||
personName = "Unknown"
|
||||
}
|
||||
|
||||
personName = txt.Title(personName)
|
||||
func NewPerson(personName, personSrc string, photoCount int) *Person {
|
||||
personName = txt.Title(txt.Clip(personName, txt.ClipDefault))
|
||||
personSlug := slug.Make(txt.Clip(personName, txt.ClipSlug))
|
||||
|
||||
result := &Person{
|
||||
PersonSlug: personSlug,
|
||||
PersonName: personName,
|
||||
PhotoCount: 1,
|
||||
PersonSrc: personSrc,
|
||||
PhotoCount: photoCount,
|
||||
}
|
||||
|
||||
return result
|
||||
|
@ -135,7 +130,7 @@ func (m *Person) Delete() 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 {
|
||||
return m.DeletedAt != nil
|
||||
}
|
||||
|
|
90
internal/entity/person_face.go
Normal file
90
internal/entity/person_face.go
Normal 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
|
||||
}
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
func TestNewPerson(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.PersonSlug)
|
||||
})
|
||||
|
@ -17,7 +17,7 @@ func TestNewPerson(t *testing.T) {
|
|||
|
||||
func TestPerson_SetName(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.PersonSlug)
|
||||
|
@ -30,7 +30,7 @@ func TestPerson_SetName(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFirstOrCreatePerson(t *testing.T) {
|
||||
m := NewPerson("Create Me")
|
||||
m := NewPerson("Create Me", SrcAuto, 0)
|
||||
result := FirstOrCreatePerson(m)
|
||||
|
||||
if result == nil {
|
||||
|
@ -43,7 +43,7 @@ func TestFirstOrCreatePerson(t *testing.T) {
|
|||
|
||||
func TestPerson_Save(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := NewPerson("Save Me")
|
||||
m := NewPerson("Save Me", SrcAuto, 0)
|
||||
initialDate := m.UpdatedAt
|
||||
err := m.Save()
|
||||
|
||||
|
@ -60,7 +60,7 @@ func TestPerson_Save(t *testing.T) {
|
|||
|
||||
func TestPerson_Delete(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := NewPerson("Jens Mander")
|
||||
m := NewPerson("Jens Mander", SrcAuto, 0)
|
||||
err := m.Save()
|
||||
assert.False(t, m.Deleted())
|
||||
|
||||
|
@ -114,7 +114,7 @@ func TestPerson_Restore(t *testing.T) {
|
|||
|
||||
func TestFindPerson(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
m := NewPerson("Find Me")
|
||||
m := NewPerson("Find Me", SrcAuto, 0)
|
||||
err := m.Save()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -139,7 +139,7 @@ func TestPerson_Links(t *testing.T) {
|
|||
|
||||
func TestPerson_Update(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 {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -4,7 +4,7 @@ import "github.com/ulule/deepcopier"
|
|||
|
||||
// Marker represents an image marker edit form.
|
||||
type Marker struct {
|
||||
RefUID string `json:"RefUID"`
|
||||
Ref string `json:"Ref"`
|
||||
RefSrc string `json:"RefSrc"`
|
||||
MarkerSrc string `json:"Src"`
|
||||
MarkerType string `json:"Type"`
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
func TestNewMarker(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var m = struct {
|
||||
RefUID string
|
||||
Ref string
|
||||
RefSrc string
|
||||
MarkerSrc string
|
||||
MarkerType string
|
||||
|
@ -17,7 +17,7 @@ func TestNewMarker(t *testing.T) {
|
|||
MarkerInvalid bool
|
||||
MarkerLabel string
|
||||
}{
|
||||
RefUID: "3h59wvth837b5vyiub35",
|
||||
Ref: "3h59wvth837b5vyiub35",
|
||||
RefSrc: "meta",
|
||||
MarkerSrc: "image",
|
||||
MarkerType: "Face",
|
||||
|
@ -32,7 +32,7 @@ func TestNewMarker(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "3h59wvth837b5vyiub35", f.RefUID)
|
||||
assert.Equal(t, "3h59wvth837b5vyiub35", f.Ref)
|
||||
assert.Equal(t, "meta", f.RefSrc)
|
||||
assert.Equal(t, "image", f.MarkerSrc)
|
||||
assert.Equal(t, "Face", f.MarkerType)
|
||||
|
|
|
@ -19,7 +19,7 @@ type Moments struct {
|
|||
conf *config.Config
|
||||
}
|
||||
|
||||
// NewMoments returns a new purge worker.
|
||||
// NewMoments returns a new Moments worker.
|
||||
func NewMoments(conf *config.Config) *Moments {
|
||||
instance := &Moments{
|
||||
conf: conf,
|
||||
|
|
180
internal/photoprism/people.go
Normal file
180
internal/photoprism/people.go
Normal 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()
|
||||
}
|
18
internal/photoprism/people_test.go
Normal file
18
internal/photoprism/people_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -13,3 +13,48 @@ func MarkerByID(id uint) (marker entity.Marker, err error) {
|
|||
|
||||
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
36
internal/query/people.go
Normal 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
|
||||
}
|
19
internal/service/people.go
Normal file
19
internal/service/people.go
Normal 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
|
||||
}
|
|
@ -25,6 +25,7 @@ var services struct {
|
|||
Import *photoprism.Import
|
||||
Index *photoprism.Index
|
||||
Moments *photoprism.Moments
|
||||
People *photoprism.People
|
||||
Purge *photoprism.Purge
|
||||
CleanUp *photoprism.CleanUp
|
||||
Nsfw *nsfw.Detector
|
||||
|
|
Loading…
Reference in a new issue