diff --git a/cmd/photoprism/photoprism.go b/cmd/photoprism/photoprism.go index d7e3db745..33e4eb6b0 100644 --- a/cmd/photoprism/photoprism.go +++ b/cmd/photoprism/photoprism.go @@ -56,6 +56,7 @@ func main() { commands.IndexCommand, commands.ImportCommand, commands.MomentsCommand, + commands.PeopleCommand, commands.OptimizeCommand, commands.PurgeCommand, commands.CleanUpCommand, diff --git a/go.mod b/go.mod index 0c12d08e1..2a4f99409 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index d2aad222d..0a7a5f153 100644 --- a/go.sum +++ b/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= diff --git a/internal/commands/moments.go b/internal/commands/moments.go index e6c8e0186..31d55b6f8 100644 --- a/internal/commands/moments.go +++ b/internal/commands/moments.go @@ -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", diff --git a/internal/commands/people.go b/internal/commands/people.go new file mode 100644 index 000000000..a81513117 --- /dev/null +++ b/internal/commands/people.go @@ -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 +} diff --git a/internal/entity/embeddings.go b/internal/entity/embeddings.go new file mode 100644 index 000000000..1bf87f1b2 --- /dev/null +++ b/internal/entity/embeddings.go @@ -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 +} diff --git a/internal/entity/entity.go b/internal/entity/entity.go index b5317b77c..9e3a176a5 100644 --- a/internal/entity/entity.go +++ b/internal/entity/entity.go @@ -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 { diff --git a/internal/entity/marker.go b/internal/entity/marker.go index 4d3248664..a9fc66242 100644 --- a/internal/entity/marker.go +++ b/internal/entity/marker.go @@ -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) diff --git a/internal/entity/marker_fixtures.go b/internal/entity/marker_fixtures.go index 51af4feb5..7e81a3d4a 100644 --- a/internal/entity/marker_fixtures.go +++ b/internal/entity/marker_fixtures.go @@ -21,7 +21,7 @@ func (m MarkerMap) Pointer(name string) *Marker { var MarkerFixtures = MarkerMap{ "1000003-1": Marker{ FileID: 1000003, - RefUID: "lt9k3pw1wowuy3c3", + Ref: "lt9k3pw1wowuy3c3", MarkerSrc: SrcImage, MarkerType: MarkerLabel, X: 0.308333, @@ -31,7 +31,7 @@ var MarkerFixtures = MarkerMap{ }, "1000003-2": Marker{ FileID: 1000003, - RefUID: "", + Ref: "", MarkerLabel: "Unknown", MarkerSrc: SrcImage, MarkerType: MarkerLabel, @@ -42,7 +42,7 @@ var MarkerFixtures = MarkerMap{ }, "1000003-3": Marker{ FileID: 1000003, - RefUID: "", + Ref: "", MarkerSrc: SrcImage, MarkerType: MarkerLabel, MarkerLabel: "Center", @@ -51,6 +51,32 @@ var MarkerFixtures = MarkerMap{ W: 0, H: 0, }, + "1000003-4": Marker{ + FileID: 1000003, + Ref: "", + MarkerSrc: SrcImage, + MarkerType: MarkerFace, + MarkerLabel: "Jens Mander", + MarkerMeta: "[{\"name\":\"lp46\",\"x\":-0.08359375,\"y\":-0.027083334,\"h\":0.044791665,\"w\":0.03359375},{\"name\":\"lp46_v\",\"x\":0.08671875,\"y\":-0.009375,\"h\":0.045833334,\"w\":0.034375},{\"name\":\"lp44\",\"x\":-0.0546875,\"y\":-0.048958335,\"h\":0.044791665,\"w\":0.03359375},{\"name\":\"lp44_v\",\"x\":0.06328125,\"y\":-0.033333335,\"h\":0.045833334,\"w\":0.034375},{\"name\":\"lp42\",\"x\":-0.021875,\"y\":-0.03125,\"h\":0.044791665,\"w\":0.03359375},{\"name\":\"lp42_v\",\"x\":0.03203125,\"y\":-0.025,\"h\":0.045833334,\"w\":0.034375},{\"name\":\"lp38\",\"x\":-0.0265625,\"h\":0.044791665,\"w\":0.03359375},{\"name\":\"lp38_v\",\"x\":0.03125,\"y\":0.0052083335,\"h\":0.044791665,\"w\":0.03359375},{\"name\":\"lp312\",\"x\":-0.06796875,\"y\":-0.008333334,\"h\":0.045833334,\"w\":0.034375},{\"name\":\"lp312_v\",\"x\":0.06953125,\"y\":0.008333334,\"h\":0.044791665,\"w\":0.03359375},{\"name\":\"mouth_lp93\",\"x\":-0.00703125,\"y\":0.09375,\"h\":0.044791665,\"w\":0.03359375},{\"name\":\"mouth_lp84\",\"x\":-0.04921875,\"y\":0.128125,\"h\":0.045833334,\"w\":0.034375},{\"name\":\"mouth_lp82\",\"x\":-0.01328125,\"y\":0.16145833,\"h\":0.045833334,\"w\":0.034375},{\"name\":\"mouth_lp81\",\"x\":-0.0078125,\"y\":0.13333334,\"h\":0.044791665,\"w\":0.03359375},{\"name\":\"lp84\",\"x\":0.034375,\"y\":0.14479166,\"h\":0.044791665,\"w\":0.03359375},{\"name\":\"eye_l\",\"x\":-0.0484375,\"y\":-0.004166667,\"h\":0.030208332,\"w\":0.02265625},{\"name\":\"eye_r\",\"x\":0.0484375,\"y\":0.0052083335,\"h\":0.030208332,\"w\":0.02265625}]", + Embeddings: "[[0.019231493,-0.028809275,-0.083006255,-0.015598502,0.08550906,0.001886255,0.09019353,0.07551488,-0.011814484,0.03680722,-0.08332401,0.014950869,0.055766843,-0.0073407963,-0.0552993,0.05847239,0.026178556,-0.045643847,0.012550894,-0.012022383,-0.0040185535,0.00023647904,-0.01580684,-0.008704283,0.04575994,-0.046812356,0.056398943,-0.026091263,0.059522673,-0.044217024,0.014755385,-0.01350486,-0.049488768,-0.03139871,-0.006726978,-0.02347069,-0.0059445584,-0.004682308,-0.057403754,-0.06537466,0.013326172,-0.009667708,0.022370687,0.0037015954,0.03744496,0.052890837,-0.0077360696,-0.049944617,-0.03868134,0.001521219,0.03840492,-0.10928545,0.023024736,-0.055707198,-0.13260484,0.009903039,-0.04250921,-0.0040567834,0.03343564,-0.01785736,0.0043026204,-0.031062575,-0.0019649328,0.06487235,-0.14464019,-0.017717961,-0.0033534314,0.029505186,0.008849258,-0.0026131037,-0.06479913,-0.111862205,0.05469594,0.049985956,-0.00067700783,0.003068928,0.0018148758,0.0073374105,0.025748348,-0.0424614,0.062650666,0.058194485,-0.04309207,-0.020790769,-0.030982763,0.008360668,0.01289988,-0.019662105,0.0122521445,-0.00342255,-0.056044392,0.034414552,0.04604621,0.0074918787,0.033526078,-0.036619328,-0.047758896,-0.032501936,-0.08068566,-0.02964604,0.04137439,0.06888022,0.04018322,0.0023792675,-0.026837967,-0.049688686,-0.057930365,-0.064863205,0.004485477,-0.026958624,-0.025907256,0.009216111,-0.014622554,-0.037213538,0.078393415,-0.054682203,-0.009757617,0.03503295,0.027951613,-0.0014038666,0.06851987,0.020453943,-0.00996363,-0.12156495,-0.017301193,-0.032558206,0.07816977,0.029640608,0.03150378,0.047710016,-0.009032255,0.013137696,-0.056541067,-0.0075266357,0.007981631,-0.025004586,0.030189874,-0.007878598,0.03279407,0.059711676,-0.003458093,0.01758815,-0.010460049,0.012645757,0.006580964,-0.019848932,0.02724201,0.001597322,0.0998137,-0.03012735,0.011577833,0.0028034616,0.029448563,-0.011648439,0.05161503,-0.028912308,-0.07435124,-0.0033618107,0.030619241,0.04033193,-0.0049585714,-0.016229536,0.05426284,-0.031509526,0.014953531,-0.025750436,0.000924462,-0.008974987,0.03417905,0.0011432022,0.007551494,0.001544253,0.016510574,0.044189014,0.054653972,0.0811879,-0.0036358214,0.035907324,0.047312576,-0.0068604983,-0.029337948,0.027281888,-0.08003064,-0.054304074,0.0073654098,0.019096468,0.004265153,-0.03670545,0.0049256124,-0.00017001168,0.04502574,-0.00074317795,0.024450991,-0.052183975,0.15243798,-0.010411264,-0.016080985,0.017625313,0.022576308,-0.053998422,0.0023750497,-0.057889163,0.056488033,-0.025041293,0.011416959,0.08541408,0.08386572,0.0396809,0.0920811,-0.048865005,-0.026257817,0.032705985,-0.053586084,0.032277677,0.07027237,0.016478207,-0.08976395,-0.006354579,-0.038090594,0.024028458,0.013410826,0.08724634,-0.028266877,0.07450935,0.03037237,-0.018165091,-0.029061696,-0.0006272013,-0.005710233,0.037058197,-0.085284434,-0.04267883,0.029252399,-0.033832725,-0.07989745,0.025662571,0.019242961,-0.10760675,0.0021055646,0.0021679339,0.0382236,0.01998776,-0.040100973,-0.041129857,0.0025446033,-0.03981458,-0.031129645,-0.061818115,-0.031583495,0.08146585,-0.042177603,0.05506262,-0.045823902,-0.031090494,-0.04478884,-0.06997962,-0.0024934823,0.0020288285,-0.074601755,-0.029107075,-0.03953502,0.0551208,-0.05685647,-0.0010502128,0.04371499,-0.031777892,0.030995583,-0.056923013,0.04047056,-0.05058555,-0.020007737,-0.0034168877,0.040992126,-0.0065717557,-0.060389657,-0.015070164,0.0300555,-0.0049498156,0.035197794,-0.010181344,-0.01548949,0.09214268,0.06594641,-0.04095856,0.031043377,0.016846823,0.011824804,0.010486359,0.006952729,-0.0423542,-0.038642637,0.037917823,-0.042705245,0.002531653,0.049575835,-0.008132699,0.060207773,0.050799962,0.029537434,-0.011404661,0.07556842,0.044129632,-0.025151744,-0.04391785,-0.057073284,0.066147126,-0.03833207,-0.11469866,0.018607577,0.03958792,0.005069568,-0.0022620235,-0.06963503,-0.027407918,-0.01658652,-0.08928287,0.04213307,0.0653322,0.070556566,-0.08890351,0.05253341,-0.03952343,-0.03277939,-0.07603576,0.023622843,0.01869451,0.0012659469,-0.016868249,-0.048114307,-0.12678534,-0.023940234,0.03518056,0.030265998,-0.027942419,-0.030615386,-0.027904578,0.04184872,-0.06871633,0.018810445,-0.0050423085,0.01902196,0.036709674,0.05114107,-0.008238005,-0.033530205,0.009237725,-0.019793255,-0.011098562,-0.040874466,0.028055958,0.052516278,0.002427102,-0.018204305,-0.029648917,-0.071319304,-0.0468887,0.061347924,0.031454504,-0.031552624,-0.08102158,0.074395515,-0.048840746,0.030817837,0.07313089,0.062438965,-0.00383427,0.05508265,-0.0077238525,0.026816545,-0.050590646,-0.02294128,0.0017293892,-0.018405396,0.03635994,-0.0025451558,-0.005002223,-0.059726283,0.008561757,-0.05424462,-0.009393793,-0.040830627,0.02030258,0.003973316,0.020119876,-0.017317869,0.01453977,-0.025234057,0.072000384,-0.0413773,-0.111050114,0.06299958,0.00016407811,-0.050199028,-0.013726295,0.0100843245,0.022901619,0.056635447,0.038121402,0.024791898,-0.011317786,-0.059741378,-0.0004985886,-0.0129778,0.04761788,0.018754557,0.035193384,-0.021030819,0.042050865,0.08013355,-0.042123444,0.065898865,0.03751851,-0.049639545,-0.03844303,0.006924326,0.022647297,-0.008048924,0.015995622,-0.033804823,0.007260562,-0.046132274,-0.0064429105,0.031663842,-0.006572463,-0.06781134,0.013187006,0.013765757,-0.03454214,-0.015666338,-0.0023530773,-0.07751217,-0.008714079,-0.010440672,0.026577305,0.066843055,-0.00440322,-0.0071120844,0.008546605,0.087584786,-0.027872834,-0.043215286,-0.0657154,0.042205643,0.003758622,-0.029912153,0.020607445,-0.0034470167,0.040666997,0.081489064,-0.044269655,-0.006542095,-0.054021314,-0.029242665,-0.027248744,-0.02227558,0.082040176,-0.030761424,-0.023510806,-0.06973938,-0.0032560013,0.055813447,0.03283226,0.068810284,0.029060816,-0.03417918,0.004436392,-0.018858656,-0.0056046643,-0.034613956,-0.024074866,0.029658342,-0.023564866,-0.011503043,0.04425076,-0.017220033,0.094600976,0.0664404,0.0784834,0.0034832316,-0.056852203,-0.012945193,-0.050754715,-0.05909069,-0.046398517,0.024399279,-0.02930913,-0.10395105,-0.011431423,0.028347071,0.010558335,0.027873065,-0.109235674,0.032155566,-0.06912663,0.010209797,-0.038717333,0.014557831,-0.07610044,0.024598178,0.016510325,0.04507311,-0.056756962,0.013758646,-0.06844432,-0.006438681,0.090832904,0.051732734,-0.011350843,-0.025747566,-0.043756638,-0.028601611,0.0338011]]", + X: 0.6, + Y: 0.7, + W: 0.2, + H: 0.05, + }, + "1000003-5": Marker{ + FileID: 1000003, + Ref: "", + MarkerSrc: SrcImage, + MarkerType: MarkerFace, + MarkerLabel: "Corn McCornface", + MarkerMeta: "[{\"name\":\"lp46\",\"x\":-0.10546875,\"y\":-0.045898438,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"lp46_v\",\"x\":0.11328125,\"y\":0.0126953125,\"h\":0.034179688,\"w\":0.045572918},{\"name\":\"lp44\",\"x\":-0.053385418,\"y\":-0.0546875,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"lp44_v\",\"x\":0.09375,\"y\":-0.0078125,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"lp42\",\"x\":-0.015625,\"y\":-0.030273438,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"lp42_v\",\"x\":0.0546875,\"y\":-0.0087890625,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"lp38\",\"x\":-0.033854168,\"y\":-0.0087890625,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"lp38_v\",\"x\":0.037760418,\"y\":0.01171875,\"h\":0.032226562,\"w\":0.04296875},{\"name\":\"lp312\",\"x\":-0.091145836,\"y\":-0.02734375,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"lp312_v\",\"x\":0.08984375,\"y\":0.021484375,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"mouth_lp93\",\"x\":-0.026041666,\"y\":0.07714844,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"mouth_lp84\",\"x\":-0.102864586,\"y\":0.08496094,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"mouth_lp82\",\"x\":-0.05859375,\"y\":0.12109375,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"mouth_lp81\",\"x\":-0.045572918,\"y\":0.10058594,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"lp84\",\"x\":-0.0065104165,\"y\":0.1171875,\"h\":0.033203125,\"w\":0.044270832},{\"name\":\"eye_l\",\"x\":-0.059895832,\"y\":-0.015625,\"h\":0.022460938,\"w\":0.029947916},{\"name\":\"eye_r\",\"x\":0.059895832,\"y\":0.015625,\"h\":0.022460938,\"w\":0.029947916}]", + Embeddings: "[[-0.01598889,-0.013085627,-0.091266885,0.01090832,0.059863407,-0.018543985,0.06513644,0.072262324,-0.0024608355,0.023480255,-0.034981046,0.028824307,0.031145066,0.020279909,-0.038303424,0.063686624,0.00016155555,-0.040199693,-0.004059554,-0.038183372,0.010284749,0.0393553,-0.013954249,0.014198846,0.047331642,-0.07308497,0.008753774,-0.044199772,0.0351775,-0.009616884,-0.011886778,-0.03133512,-0.008360827,-0.0021931753,-0.0031518617,-0.0007841898,0.0012749507,-0.013682331,-0.0093261255,-0.0646958,0.028137255,-0.051393177,-0.010831488,-0.0019370695,0.026701374,0.08734394,-0.03148508,-0.072140485,-0.008645494,0.03283726,0.025486251,-0.14762828,0.0016828487,-0.049219336,-0.090523295,-0.017858343,-0.0433293,-0.03822806,0.03775215,-0.030626448,-0.005236273,-0.025029438,0.011486794,0.08866543,-0.11626933,-0.012919138,0.011206989,0.029296853,0.029712738,-0.0035943172,-0.0625837,-0.08751456,0.06506425,0.08434424,0.018379156,0.006281598,-0.019832052,0.013404299,0.050819624,-0.0025536602,0.108513094,0.043542076,-0.03385126,-0.013718123,-0.020935653,0.026902547,0.023695294,-0.032848295,0.02122507,-0.06577069,-0.049782418,0.07434279,0.011499832,0.0274455,0.023498816,-0.0024784799,-0.0408338,-0.01835984,-0.07471391,-0.020153865,0.020164149,0.08315784,0.057026222,0.0064989394,-0.030934982,-0.068170995,-0.0447578,-0.08320161,-0.02340753,-0.052578453,-0.014655025,0.011221938,0.020638045,-0.046642996,0.07793852,-0.07613048,-0.0052055195,0.050180323,-0.008933726,-0.006827808,0.046653792,0.046993263,-0.022871166,-0.10476393,-0.019975707,-0.013084017,0.054878037,0.016760936,0.0547656,0.016288247,0.029425414,-0.0039762836,-0.06857062,0.011283167,0.031562343,-0.0301557,0.018833537,0.014527415,0.014445754,0.08944574,-0.0201444,0.07525237,0.00338875,-0.009128363,0.03199888,0.003338322,-0.034410495,0.036284383,0.061515816,-0.04700179,-0.0037704427,-0.014624933,0.00053029,-0.040128306,0.033271074,-0.034320436,-0.087529205,-0.015765699,-0.042281955,0.026603937,-0.03505665,0.0045038764,0.07849869,-0.020796757,-0.027909378,-0.030466244,-0.022126308,0.032068476,0.017038053,0.011572472,-0.023514347,-0.009388009,0.0471232,0.079254,0.038409125,0.09174638,-0.00019651726,0.031568147,0.061691687,0.0035580387,0.01834219,-0.034832783,-0.02472014,-0.0043187817,-0.012822156,-0.006406414,0.04589425,-0.029704314,0.0122809885,-0.003854935,0.034970216,-0.015017675,0.060136527,-0.0018005831,0.122445226,-0.02466541,-0.01479218,0.021981824,0.008459089,-0.024648905,-0.005501727,-0.03819929,0.09229076,-0.00051632634,0.026605012,0.07888228,0.108206324,0.052503213,0.11041478,-0.051830698,0.0057799574,0.06611321,-0.07650234,0.05879657,0.052348997,0.019547585,-0.07638257,-0.044862173,-0.043846298,-0.021011176,-0.01881608,0.107294865,-0.022127088,0.067301944,0.06469245,-0.00538971,-0.014842585,0.008351809,0.004484374,0.040771488,-0.10075861,-0.037090372,0.010828192,-0.014120988,-0.106951684,0.017808948,0.0014778537,-0.052681025,-0.005136359,0.03752494,0.021728097,0.054375395,-0.024591682,-0.028040286,-0.00003123716,-0.009580712,-0.01999454,-0.036310453,0.012808869,0.033562016,-0.048961494,0.049189627,-0.075262696,-0.025279071,-0.017653963,-0.07986348,-0.0058788625,-0.007859457,-0.07787707,-0.0141451955,-0.027267313,0.034545522,-0.066852376,-0.033797234,0.00662154,-0.0042986833,0.06258031,-0.027749743,0.035639953,-0.0860917,-0.021690408,-0.041379962,0.058219258,0.036375474,-0.028576316,0.009679853,0.0540403,-0.00078071933,0.046852123,-0.009272538,0.00014093994,0.08755382,0.029109005,-0.051301096,0.05482741,0.0515714,0.00093064347,-0.0030184423,0.006834386,0.018765705,-0.05297967,0.003963442,0.0058909976,0.0015918964,0.0059644254,-0.010626185,0.016774561,0.019620024,0.012779505,-0.0037311793,0.06090134,0.030533332,-0.0013029866,0.0078083063,-0.07804226,0.055505097,-0.0323863,-0.13843039,0.017691568,0.015463283,-0.0054733357,0.029853273,0.044509612,-0.017058503,-0.009644833,-0.08004177,0.018354492,0.08402592,0.08028074,-0.033064563,0.0071564782,-0.026635528,-0.020762388,-0.09037636,0.07283039,-0.0013951861,0.023556605,-0.025109852,-0.01911825,-0.072672606,0.023367604,0.0066802157,-0.029526467,-0.005831557,-0.056150723,-0.036622144,0.014390345,-0.06641906,0.027680077,-0.0027364918,0.07881769,0.0032321527,0.06692333,-0.023430921,-0.0156005705,-0.012797314,-0.06341876,-0.0075710113,-0.08883436,0.018736875,0.0515824,-0.03050761,-0.038169485,-0.019994874,-0.037897438,0.0030616417,0.016518306,0.017313045,-0.040592685,-0.121986456,0.036341745,-0.055392407,0.06128348,0.09128614,0.085432775,-0.04018598,0.020804984,0.022338325,-0.047893576,-0.04436214,0.010663377,-0.01539266,0.001970492,-0.02548427,-0.0010024207,0.012728738,-0.03458635,0.000458029,-0.07648158,0.017893706,-0.03620278,0.012510285,0.042211026,0.029836254,-0.01023813,-0.014964832,-0.036710255,0.072146155,-0.032623224,-0.054371897,0.059095327,-0.026043909,-0.026475005,-0.03756759,-0.0033844158,0.01665272,0.055144988,0.020163653,0.010291277,0.016448587,-0.08021163,-0.00711534,0.0014388722,0.047059905,0.055735916,0.04716966,0.002357553,0.028611615,0.053914364,-0.022333615,0.01227299,-0.006376796,-0.020971471,-0.031313244,0.004976049,0.027839795,0.032628387,0.014910606,-0.019821445,-0.023739582,-0.061071664,-0.03563204,0.04504174,-0.043931577,-0.09461471,0.035146907,0.018801821,-0.023486922,-0.014275421,-0.04465509,-0.02062559,0.0049236626,-0.018532282,0.0329802,0.08521481,-0.025042368,-0.0031619244,-0.012923802,0.062199675,0.008717194,-0.06812108,-0.059829578,0.012077271,0.04268468,-0.029709337,0.058341388,-0.023695359,0.026195621,0.089156136,-0.05210246,-0.006754805,-0.08180936,-0.016863013,0.01305001,-0.06608525,0.060481545,-0.008385101,-0.022109058,-0.060189016,0.029254012,0.030882897,-0.009892429,0.04769517,0.010766996,-0.032422658,-0.010280704,-0.035042368,-0.046891414,-0.021463934,-0.022753375,0.011999883,-0.027142627,-0.00023438907,0.016639061,-0.022108192,0.06750844,0.031127596,0.062595494,0.035271265,-0.00848079,0.0054323506,-0.038877886,-0.09272652,-0.028250217,-0.008661228,-0.08010225,-0.066937625,-0.022481173,0.023471648,0.0064566983,-0.0010692414,-0.11465305,0.041438285,-0.08444645,0.004585747,-0.028296055,0.04911384,-0.0049776305,0.043981623,-0.010605184,0.07764796,-0.022333274,0.050281037,-0.06543124,-0.00412494,0.08933364,0.043259624,0.0007392553,-0.0055968673,-0.041995358,-0.01275004,0.017657083]]", + X: 0.2, + Y: 0.3, + W: 0.1, + H: 0.1, + }, } // CreateMarkerFixtures inserts known entities into the database for testing. diff --git a/internal/entity/marker_test.go b/internal/entity/marker_test.go index d89d4f763..7c57703b7 100644 --- a/internal/entity/marker_test.go +++ b/internal/entity/marker_test.go @@ -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) diff --git a/internal/entity/person.go b/internal/entity/person.go index a282c790e..39f400cba 100644 --- a/internal/entity/person.go +++ b/internal/entity/person.go @@ -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 } diff --git a/internal/entity/person_face.go b/internal/entity/person_face.go new file mode 100644 index 000000000..39df59b60 --- /dev/null +++ b/internal/entity/person_face.go @@ -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 +} diff --git a/internal/entity/person_test.go b/internal/entity/person_test.go index dc1c18fc0..2fc80b7b9 100644 --- a/internal/entity/person_test.go +++ b/internal/entity/person_test.go @@ -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) diff --git a/internal/form/marker.go b/internal/form/marker.go index cd846b2cf..839d50f1f 100644 --- a/internal/form/marker.go +++ b/internal/form/marker.go @@ -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"` diff --git a/internal/form/marker_test.go b/internal/form/marker_test.go index 6c01e25b5..5d8f2654d 100644 --- a/internal/form/marker_test.go +++ b/internal/form/marker_test.go @@ -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) diff --git a/internal/photoprism/moments.go b/internal/photoprism/moments.go index 6675ccc1a..61db4e6ad 100644 --- a/internal/photoprism/moments.go +++ b/internal/photoprism/moments.go @@ -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, diff --git a/internal/photoprism/people.go b/internal/photoprism/people.go new file mode 100644 index 000000000..131bb32c9 --- /dev/null +++ b/internal/photoprism/people.go @@ -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() +} diff --git a/internal/photoprism/people_test.go b/internal/photoprism/people_test.go new file mode 100644 index 000000000..c942fd3d3 --- /dev/null +++ b/internal/photoprism/people_test.go @@ -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) + } +} diff --git a/internal/query/markers.go b/internal/query/markers.go index 5b4cad397..4043cff06 100644 --- a/internal/query/markers.go +++ b/internal/query/markers.go @@ -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 +} diff --git a/internal/query/people.go b/internal/query/people.go new file mode 100644 index 000000000..3a1a92344 --- /dev/null +++ b/internal/query/people.go @@ -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 +} diff --git a/internal/service/people.go b/internal/service/people.go new file mode 100644 index 000000000..c43558d9e --- /dev/null +++ b/internal/service/people.go @@ -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 +} diff --git a/internal/service/service.go b/internal/service/service.go index c0d708cd3..8c5572826 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -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