People: Improve Facial Recognition Accuracy, Command, and UI #22

Work in progress. Performed refactoring along the way.
This commit is contained in:
Michael Mayer 2021-08-13 20:04:59 +02:00
parent 9c99c35db1
commit a974b3a7ea
26 changed files with 417 additions and 226 deletions

View file

@ -31,6 +31,7 @@ package main
import (
"os"
"path/filepath"
"github.com/photoprism/photoprism/internal/commands"
"github.com/photoprism/photoprism/internal/config"
@ -44,6 +45,7 @@ var log = event.Log
func main() {
app := cli.NewApp()
app.Name = "PhotoPrism"
app.HelpName = filepath.Base(os.Args[0])
app.Usage = "Browse Your Life in Pictures"
app.Version = version
app.Copyright = "(c) 2018-2021 Michael Mayer <hello@photoprism.org>"
@ -56,7 +58,7 @@ func main() {
commands.IndexCommand,
commands.ImportCommand,
commands.MomentsCommand,
commands.PeopleCommand,
commands.FacesCommand,
commands.OptimizeCommand,
commands.PurgeCommand,
commands.CleanUpCommand,

View file

@ -1670,9 +1670,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz",
"integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==",
"version": "13.11.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz",
"integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==",
"dependencies": {
"type-fest": "^0.20.2"
},
@ -1895,9 +1895,9 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
},
"node_modules/@types/node": {
"version": "16.4.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.14.tgz",
"integrity": "sha512-GZpnVRNtv7sHDXIFncsERt+qvj4rzAgRQtnvzk3Z7OVNtThD2dHXYCMDNc80D5mv4JE278qo8biZCwcmkbdpqw=="
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz",
"integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw=="
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
@ -2758,12 +2758,13 @@
}
},
"node_modules/axios-mock-adapter": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz",
"integrity": "sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==",
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.20.0.tgz",
"integrity": "sha512-shZRhTjLP0WWdcvHKf3rH3iW9deb3UdKbdnKUoHmmsnBhVXN3sjPJM6ZvQ2r/ywgvBVQrMnjrSyQab60G1sr2w==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"is-buffer": "^2.0.3"
"is-blob": "^2.1.0",
"is-buffer": "^2.0.5"
},
"peerDependencies": {
"axios": ">= 0.9.0"
@ -3209,9 +3210,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001249",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz",
"integrity": "sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw==",
"version": "1.0.30001251",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
@ -4740,9 +4741,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.3.802",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.802.tgz",
"integrity": "sha512-dXB0SGSypfm3iEDxrb5n/IVKeX4uuTnFHdve7v+yKJqNpEP0D4mjFJ8e1znmSR+OOVlVC+kDO6f2kAkTFXvJBg=="
"version": "1.3.805",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.805.tgz",
"integrity": "sha512-uUJF59M6pNSRHQaXwdkaNB4BhSQ9lldRdG1qCjlrAFkynPGDc5wPoUcYEQQeQGmKyAWJPvGkYAWmtVrxWmDAkg=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@ -5723,9 +5724,9 @@
}
},
"node_modules/eslint/node_modules/globals": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz",
"integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==",
"version": "13.11.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz",
"integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==",
"dependencies": {
"type-fest": "^0.20.2"
},
@ -7450,9 +7451,12 @@
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"node_modules/is-bigint": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.3.tgz",
"integrity": "sha512-ZU538ajmYJmzysE5yU4Y7uIrPQ2j704u+hXFiIPQExpqzzUbpe5jCPdTfmz7jXRxZdvjY3KZ3ZNenoXQovX+Dg==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
"integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
"dependencies": {
"has-bigints": "^1.0.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -7469,6 +7473,17 @@
"node": ">=0.10.0"
}
},
"node_modules/is-blob": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-boolean-object": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@ -12202,9 +12217,9 @@
}
},
"node_modules/protocol-buffers-schema": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz",
"integrity": "sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw=="
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.2.tgz",
"integrity": "sha512-LPzSaBYp/TcbuSlpGwqT5jR9kvJ3Zp5ic2N5c2ybx6XB/lSfEHq2D7ja8AgoxHoMD91wXFALJoXsvshKPuXyew=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
@ -14393,9 +14408,9 @@
}
},
"node_modules/tar": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.7.tgz",
"integrity": "sha512-PBoRkOJU0X3lejJ8GaRCsobjXTgFofRDSPdSUhRSdlwJfifRlQBwGXitDItdGFu0/h0XDMCkig0RN1iT7DBxhA==",
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz",
"integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
@ -14690,9 +14705,9 @@
}
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/tsscmp": {
"version": "1.0.6",
@ -17059,9 +17074,9 @@
},
"dependencies": {
"globals": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz",
"integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==",
"version": "13.11.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz",
"integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==",
"requires": {
"type-fest": "^0.20.2"
}
@ -17250,9 +17265,9 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
},
"@types/node": {
"version": "16.4.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.14.tgz",
"integrity": "sha512-GZpnVRNtv7sHDXIFncsERt+qvj4rzAgRQtnvzk3Z7OVNtThD2dHXYCMDNc80D5mv4JE278qo8biZCwcmkbdpqw=="
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz",
"integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw=="
},
"@types/parse-json": {
"version": "4.0.0",
@ -17931,12 +17946,13 @@
}
},
"axios-mock-adapter": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz",
"integrity": "sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==",
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.20.0.tgz",
"integrity": "sha512-shZRhTjLP0WWdcvHKf3rH3iW9deb3UdKbdnKUoHmmsnBhVXN3sjPJM6ZvQ2r/ywgvBVQrMnjrSyQab60G1sr2w==",
"requires": {
"fast-deep-equal": "^3.1.3",
"is-buffer": "^2.0.3"
"is-blob": "^2.1.0",
"is-buffer": "^2.0.5"
}
},
"babel-loader": {
@ -18287,9 +18303,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001249",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz",
"integrity": "sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw=="
"version": "1.0.30001251",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A=="
},
"chai": {
"version": "4.3.4",
@ -19432,9 +19448,9 @@
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
},
"electron-to-chromium": {
"version": "1.3.802",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.802.tgz",
"integrity": "sha512-dXB0SGSypfm3iEDxrb5n/IVKeX4uuTnFHdve7v+yKJqNpEP0D4mjFJ8e1znmSR+OOVlVC+kDO6f2kAkTFXvJBg=="
"version": "1.3.805",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.805.tgz",
"integrity": "sha512-uUJF59M6pNSRHQaXwdkaNB4BhSQ9lldRdG1qCjlrAFkynPGDc5wPoUcYEQQeQGmKyAWJPvGkYAWmtVrxWmDAkg=="
},
"emoji-regex": {
"version": "8.0.0",
@ -19739,9 +19755,9 @@
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"globals": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz",
"integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==",
"version": "13.11.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz",
"integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==",
"requires": {
"type-fest": "^0.20.2"
}
@ -21482,9 +21498,12 @@
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"is-bigint": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.3.tgz",
"integrity": "sha512-ZU538ajmYJmzysE5yU4Y7uIrPQ2j704u+hXFiIPQExpqzzUbpe5jCPdTfmz7jXRxZdvjY3KZ3ZNenoXQovX+Dg=="
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
"integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
"requires": {
"has-bigints": "^1.0.1"
}
},
"is-binary-path": {
"version": "1.0.1",
@ -21495,6 +21514,11 @@
"binary-extensions": "^1.0.0"
}
},
"is-blob": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw=="
},
"is-boolean-object": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
@ -24917,9 +24941,9 @@
}
},
"protocol-buffers-schema": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz",
"integrity": "sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw=="
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.5.2.tgz",
"integrity": "sha512-LPzSaBYp/TcbuSlpGwqT5jR9kvJ3Zp5ic2N5c2ybx6XB/lSfEHq2D7ja8AgoxHoMD91wXFALJoXsvshKPuXyew=="
},
"proxy-addr": {
"version": "2.0.7",
@ -26691,9 +26715,9 @@
"integrity": "sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw=="
},
"tar": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.7.tgz",
"integrity": "sha512-PBoRkOJU0X3lejJ8GaRCsobjXTgFofRDSPdSUhRSdlwJfifRlQBwGXitDItdGFu0/h0XDMCkig0RN1iT7DBxhA==",
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz",
"integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==",
"requires": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
@ -26924,9 +26948,9 @@
}
},
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"tsscmp": {
"version": "1.0.6",

View file

@ -29,17 +29,17 @@
<v-card-actions class="card-details pa-0">
<v-layout v-if="marker.Score < 30" row wrap align-center>
<v-flex xs6 class="text-xs-center pa-1">
<v-btn color="accent lighten-2"
small depressed dark block :round="false"
<v-flex xs6 class="text-xs-center pa-0">
<v-btn color="transparent"
large depressed block :round="false"
class="action-archive text-xs-center"
:title="$gettext('Reject')" @click.stop="reject(marker)">
<v-icon dark>clear</v-icon>
</v-btn>
</v-flex>
<v-flex xs6 class="text-xs-center pa-1">
<v-btn color="accent lighten-2"
small depressed dark block :round="false"
<v-flex xs6 class="text-xs-center pa-0">
<v-btn color="transparent"
large depressed block :round="false"
class="action-approve text-xs-center"
:title="$gettext('Approve')" @click.stop="confirm(marker)">
<v-icon dark>check</v-icon>
@ -47,15 +47,15 @@
</v-flex>
</v-layout>
<v-layout v-else row wrap align-center>
<v-flex xs12 class="text-xs-left pa-1">
<v-flex xs12 class="text-xs-left pa-0">
<v-text-field
v-model="marker.Label"
:rules="[textRule]"
color="secondary-dark"
browser-autocomplete="off"
class="input-name pa-0 ma-1"
class="input-name pa-0 ma-0"
hide-details
single-line
solo-inverted
clearable
@click:clear="clearName(marker)"
@change="updateName(marker)"

1
go.mod
View file

@ -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/montanaflynn/stats v0.6.6 // 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

2
go.sum
View file

@ -236,6 +236,8 @@ 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/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ=
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
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=

View file

@ -45,13 +45,13 @@ func cleanUpAction(ctx *cli.Context) error {
log.Infof("cleanup: read-only mode enabled")
}
cleanUp := service.CleanUp()
w := service.CleanUp()
opt := photoprism.CleanUpOptions{
Dry: ctx.Bool("dry"),
}
if thumbs, orphans, err := cleanUp.Start(opt); err != nil {
if thumbs, orphans, err := w.Start(opt); err != nil {
return err
} else {
elapsed := time.Since(start)

View file

@ -50,9 +50,9 @@ func convertAction(ctx *cli.Context) error {
log.Infof("converting originals in %s", txt.Quote(convertPath))
convert := service.Convert()
w := service.Convert()
if err := convert.Start(convertPath); err != nil {
if err := w.Start(convertPath); err != nil {
log.Error(err)
}

View file

@ -63,10 +63,10 @@ func copyAction(ctx *cli.Context) error {
log.Infof("copying media files from %s to %s", sourcePath, conf.OriginalsPath())
imp := service.Import()
w := service.Import()
opt := photoprism.ImportOptionsCopy(sourcePath)
imp.Start(opt)
w.Start(opt)
elapsed := time.Since(start)

View file

@ -0,0 +1,90 @@
package commands
import (
"context"
"time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
"github.com/urfave/cli"
)
// FacesCommand registers the faces cli command.
var FacesCommand = cli.Command{
Name: "faces",
Usage: "Runs facial recognition sub-commands",
Subcommands: []cli.Command{
{
Name: "stats",
Usage: "Shows stats on face embeddings",
Action: facesStatsAction,
},
{
Name: "index",
Usage: "Performs face clustering and recognition",
Action: facesIndexAction,
},
},
}
// facesStatsAction shows stats on face embeddings.
func facesStatsAction(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()
w := service.Faces()
if err := w.Analyze(); err != nil {
return err
} else {
elapsed := time.Since(start)
log.Infof("completed in %s", elapsed)
}
conf.Shutdown()
return nil
}
// facesIndexAction performs face clustering and recognition.
func facesIndexAction(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()
w := service.Faces()
if err := w.Start(); err != nil {
return err
} else {
elapsed := time.Since(start)
log.Infof("completed in %s", elapsed)
}
conf.Shutdown()
return nil
}

View file

@ -63,10 +63,10 @@ func importAction(ctx *cli.Context) error {
log.Infof("moving media files from %s to %s", sourcePath, conf.OriginalsPath())
imp := service.Import()
w := service.Import()
opt := photoprism.ImportOptionsMove(sourcePath)
imp.Start(opt)
w.Start(opt)
elapsed := time.Since(start)

View file

@ -6,6 +6,8 @@ import (
"strings"
"time"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
@ -62,38 +64,40 @@ func indexAction(ctx *cli.Context) error {
log.Infof("index: read-only mode enabled")
}
ind := service.Index()
var indexed fs.Done
indOpt := photoprism.IndexOptions{
Path: subPath,
Rescan: ctx.Bool("all"),
Convert: conf.Settings().Index.Convert && conf.SidecarWritable(),
Stack: true,
if w := service.Index(); w != nil {
opt := photoprism.IndexOptions{
Path: subPath,
Rescan: ctx.Bool("all"),
Convert: conf.Settings().Index.Convert && conf.SidecarWritable(),
Stack: true,
}
indexed = w.Start(opt)
}
indexed := ind.Start(indOpt)
if w := service.Purge(); w != nil {
opt := photoprism.PurgeOptions{
Path: subPath,
Ignore: indexed,
}
prg := service.Purge()
prgOpt := photoprism.PurgeOptions{
Path: subPath,
Ignore: indexed,
}
if files, photos, err := prg.Start(prgOpt); err != nil {
log.Error(err)
} else if len(files) > 0 || len(photos) > 0 {
log.Infof("purge: removed %d files and %d photos", len(files), len(photos))
if files, photos, err := w.Start(opt); err != nil {
log.Error(err)
} else if len(files) > 0 || len(photos) > 0 {
log.Infof("purge: removed %d files and %d photos", len(files), len(photos))
}
}
if ctx.Bool("cleanup") {
cleanUp := service.CleanUp()
w := service.CleanUp()
opt := photoprism.CleanUpOptions{
Dry: false,
}
if thumbs, orphans, err := cleanUp.Start(opt); err != nil {
if thumbs, orphans, err := w.Start(opt); err != nil {
return err
} else {
log.Infof("cleanup: removed %d index entries and %d orphan thumbnails", orphans, thumbs)

View file

@ -36,9 +36,9 @@ func momentsAction(ctx *cli.Context) error {
log.Infof("moments: read-only mode enabled")
}
moments := service.Moments()
w := service.Moments()
if err := moments.Start(); err != nil {
if err := w.Start(); err != nil {
return err
} else {
elapsed := time.Since(start)

View file

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

View file

@ -62,7 +62,7 @@ func purgeAction(ctx *cli.Context) error {
log.Infof("purge: read-only mode enabled")
}
prg := service.Purge()
w := service.Purge()
opt := photoprism.PurgeOptions{
Path: subPath,
@ -70,7 +70,7 @@ func purgeAction(ctx *cli.Context) error {
Hard: ctx.Bool("hard"),
}
if files, photos, err := prg.Start(opt); err != nil {
if files, photos, err := w.Start(opt); err != nil {
return err
} else {
elapsed := time.Since(start)

View file

@ -125,7 +125,7 @@ func resetAction(ctx *cli.Context) error {
log.Infof("removed files in %s", time.Since(start))
} else {
log.Infof("no backup files found")
log.Infof("no metadata backups found for removal")
}
} else {
log.Infof("keeping backup files")
@ -160,7 +160,7 @@ func resetAction(ctx *cli.Context) error {
log.Infof("removed files in %s", time.Since(start))
} else {
log.Infof("no backup files found")
log.Infof("no album backups found for removal")
}
} else {
log.Infof("keeping backup files")

View file

@ -93,8 +93,6 @@ func Detect(fileName string) (faces Faces, err error) {
return faces, fmt.Errorf("faces: file '%s' not found", txt.Quote(filepath.Base(fileName)))
}
log.Infof("faces: analyzing %s", txt.Quote(filepath.Base(fileName)))
det, params, err := fd.Detect(fileName)
if err != nil {

View file

@ -116,7 +116,7 @@ func (t *Net) getFaceCrop(fileName, fileHash string, f Point) (img image.Image,
} else if img, err := imaging.Open(cacheFile); err != nil {
log.Errorf("faces: failed loading cached crop %s", filepath.Base(cacheFile))
} else {
log.Debugf("faces: found cached crop %s", filepath.Base(cacheFile))
log.Debugf("faces: using cached crop %s", filepath.Base(cacheFile))
return img, nil
}

View file

@ -6,43 +6,156 @@ import (
"runtime/debug"
"time"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/montanaflynn/stats"
"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/photoprism/photoprism/pkg/txt"
"github.com/mpraski/clusters"
)
// People represents a worker for face clustering and recognition.
type People struct {
// Faces represents a worker for face clustering and recognition.
type Faces struct {
conf *config.Config
}
// NewPeople returns a new People worker.
func NewPeople(conf *config.Config) *People {
instance := &People{
// NewFaces returns a new Faces worker.
func NewFaces(conf *config.Config) *Faces {
instance := &Faces{
conf: conf,
}
return instance
}
// Analyze face embeddings.
func (w *Faces) Analyze() (err error) {
if embeddings, err := query.Embeddings(true); err != nil {
return err
} else if len(embeddings) == 0 {
log.Infof("faces: no embeddings found")
} else {
c := len(embeddings)
log.Debugf("faces: found %d embeddings to analyze", c)
distMin := make([]float64, c)
distMax := make([]float64, c)
for i := 0; i < c; i++ {
min := -1.0
max := -1.0
for j := 0; j < c; j++ {
if i == j {
continue
}
d := clusters.EuclideanDistance(embeddings[i], embeddings[j])
if min < 0 || d < min {
min = d
}
if max < 0 || d > max {
max = d
}
}
distMin[i] = min
distMax[i] = max
}
minMedian, _ := stats.Median(distMin)
minMin, _ := stats.Min(distMin)
minMax, _ := stats.Max(distMin)
log.Infof("faces: min Ø %f < median %f < %f", minMin, minMedian, minMax)
maxMedian, _ := stats.Median(distMax)
maxMin, _ := stats.Min(distMax)
maxMax, _ := stats.Max(distMax)
log.Infof("faces: max Ø %f < median %f < %f", maxMin, maxMedian, maxMax)
}
if known, err := query.PeopleFaces(); err != nil {
log.Errorf("faces: %s", err)
} else if len(known) == 0 {
log.Infof("faces: no faces found")
} else {
c := len(known)
dist := make(map[string][]float64)
for i := 0; i < c; i++ {
f1 := known[i]
if f1.PersonUID == "" {
continue
}
e1 := f1.UnmarshalEmbedding()
min := -1.0
max := -1.0
if k, ok := dist[f1.PersonUID]; ok {
min = k[0]
max = k[1]
}
for j := 0; j < c; j++ {
if i == j {
continue
}
f2 := known[j]
if f1.PersonUID != f2.PersonUID || f2.PersonUID == "" {
continue
}
e2 := f2.UnmarshalEmbedding()
d := clusters.EuclideanDistance(e1, e2)
if min < 0 || d < min {
min = d
}
if max < 0 || d > max {
max = d
}
}
if max > 0 {
dist[f1.PersonUID] = []float64{min, max}
}
}
for personUID, d := range dist {
log.Infof("faces: %s Ø min %f, max %f", personUID, d[0], d[1])
}
}
return nil
}
// Start face clustering and recognition.
func (m *People) Start() (err error) {
func (w *Faces) Start() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("people: %s (panic)\nstack: %s", r, debug.Stack())
err = fmt.Errorf("faces: %s (panic)\nstack: %s", r, debug.Stack())
log.Error(err)
}
}()
if !m.conf.Experimental() {
return fmt.Errorf("people: experimental features disabled")
} else if !m.conf.Settings().Features.People {
return fmt.Errorf("people: disabled in settings")
if !w.conf.Experimental() {
return fmt.Errorf("faces: experimental features disabled")
} else if !w.conf.Settings().Features.People {
return fmt.Errorf("faces: disabled in settings")
}
if err := mutex.MainWorker.Start(); err != nil {
@ -51,32 +164,32 @@ func (m *People) Start() (err error) {
defer mutex.MainWorker.Stop()
embeddings, err := query.Embeddings()
embeddings, err := query.Embeddings(false)
if err != nil {
return err
}
if len(embeddings) == 0 {
log.Infof("people: no faces detected")
log.Infof("faces: 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, m.conf.Workers(), clusters.EuclideanDistance)
c, e := clusters.DBSCAN(1, 1.0, w.conf.Workers(), clusters.EuclideanDistance)
if e != nil {
return e
}
if err := c.Learn(embeddings); err != nil {
log.Errorf("people: %s", err)
log.Errorf("faces: %s", err)
}
sizes := c.Sizes()
log.Infof("people: found %d embeddings, %d clusters", len(embeddings), len(sizes))
log.Infof("faces: found %d embeddings, %d clusters", len(embeddings), len(sizes))
faceClusters := make([]entity.Embeddings, len(sizes))
@ -102,22 +215,22 @@ func (m *People) Start() (err error) {
for _, clusterEmb := range faceClusters {
if emb, err := json.Marshal(entity.EmbeddingsMidpoint(clusterEmb)); err != nil {
updateErrors++
log.Errorf("people: %s", err)
log.Errorf("faces: %s", err)
} else if f := entity.NewPersonFace("", string(emb)); f == nil {
updateErrors++
log.Errorf("people: face should not be nil - bug?")
log.Errorf("faces: face should not be nil - bug?")
} else if err := f.Create(); err == nil {
addedFaces++
log.Tracef("people: added face %s", f.ID)
log.Tracef("faces: added face %s", f.ID)
} else if err := f.Updates(entity.Val{"UpdatedAt": entity.Timestamp()}); err != nil {
updateErrors++
log.Errorf("people: %s", err)
log.Errorf("faces: %s", err)
}
}
if err := query.PurgeUnknownFaces(); err != nil {
updateErrors++
log.Errorf("people: %s", err)
log.Errorf("faces: %s", err)
}
peopleFaces, err := query.PeopleFaces()
@ -153,7 +266,7 @@ func (m *People) Start() (err error) {
for _, marker := range markers {
if mutex.MainWorker.Canceled() {
return fmt.Errorf("people: worker canceled")
return fmt.Errorf("faces: worker canceled")
}
var faceId string
@ -180,24 +293,23 @@ func (m *People) Start() (err error) {
if marker.MarkerLabel == "" {
// Do nothing.
} else if person := entity.NewPerson(marker.MarkerLabel, entity.SrcMarker, 1); person == nil {
log.Errorf("people: person should not be nil - bug?")
log.Errorf("faces: person should not be nil - bug?")
} else if person = entity.FirstOrCreatePerson(person); person == nil {
log.Errorf("people: failed adding %s", txt.Quote(marker.MarkerLabel))
log.Errorf("faces: failed adding %s", txt.Quote(marker.MarkerLabel))
} else if f, ok := faceMap[faceId]; ok {
faceMap[faceId] = Face{Embedding: f.Embedding, PersonUID: person.PersonUID}
entity.Db().Model(&entity.PersonFace{}).Where("id = ?", faceId).Update("PersonUID", person.PersonUID)
log.Infof("people: added %s", txt.Quote(person.PersonName))
}
// Existing person?
if refUID := faceMap[faceId].PersonUID; refUID != "" {
if err := marker.Updates(entity.Val{"RefUID": refUID, "RefSrc": entity.SrcPeople, "FaceID": ""}); err != nil {
log.Errorf("people: %s while updating person uid", err)
log.Errorf("faces: %s while updating person uid", err)
} else {
recognized++
}
} else if err := marker.Updates(entity.Val{"FaceID": faceId}); err != nil {
log.Errorf("people: %s while updating marker face id", err)
log.Errorf("faces: %s while updating marker face id", err)
} else {
markersUpdated++
}
@ -208,12 +320,12 @@ func (m *People) Start() (err error) {
time.Sleep(50 * time.Millisecond)
}
log.Infof("people: %d faces added, %d recognized, %d unknown, %d errors", addedFaces, recognized, markersUpdated, updateErrors)
log.Infof("faces: %d added, %d recognized, %d unknown, %d errors", addedFaces, recognized, markersUpdated, updateErrors)
return nil
}
// Cancel stops the current operation.
func (m *People) Cancel() {
func (w *Faces) Cancel() {
mutex.MainWorker.Cancel()
}

View file

@ -9,7 +9,7 @@ import (
func TestPeople_Start(t *testing.T) {
conf := config.TestConfig()
m := NewPeople(conf)
m := NewFaces(conf)
err := m.Start()
if err != nil {

View file

@ -822,9 +822,9 @@ func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
}
}
elapsed := time.Since(start)
log.Debugf("index: image classification took %s", elapsed)
if len(labels) > 0 {
log.Infof("index: found %d labels for %s [%s]", len(labels), txt.Quote(jpeg.BaseName()), time.Since(start))
}
return results
}
@ -864,9 +864,9 @@ func (ind *Index) detectFaces(jpeg *MediaFile) face.Faces {
log.Debugf("%s in %s", err, txt.Quote(jpeg.BaseName()))
}
elapsed := time.Since(start)
log.Debugf("index: face detection took %s", elapsed)
if len(faces) > 0 {
log.Infof("index: detected %d faces in %s [%s]", len(faces), txt.Quote(jpeg.BaseName()), time.Since(start))
}
return faces
}

View file

@ -29,7 +29,7 @@ func NewMoments(conf *config.Config) *Moments {
}
// Start creates albums based on popular locations, dates and categories.
func (m *Moments) Start() (err error) {
func (w *Moments) Start() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("moments: %s (panic)\nstack: %s", r, debug.Stack())
@ -230,7 +230,7 @@ func (m *Moments) Start() (err error) {
log.Errorf("moments: %s (update album dates)", err.Error())
}
if count, err := BackupAlbums(m.conf.AlbumsPath(), false); err != nil {
if count, err := BackupAlbums(w.conf.AlbumsPath(), false); err != nil {
log.Errorf("moments: %s (backup albums)", err.Error())
} else if count > 0 {
log.Debugf("moments: %d albums saved as yaml files", count)
@ -240,6 +240,6 @@ func (m *Moments) Start() (err error) {
}
// Cancel stops the current operation.
func (m *Moments) Cancel() {
func (w *Moments) Cancel() {
mutex.MainWorker.Cancel()
}

View file

@ -15,7 +15,7 @@ import (
"github.com/photoprism/photoprism/pkg/fs"
)
// Resample represents a thumbnail generator.
// Resample represents a thumbnail generator worker.
type Resample struct {
conf *config.Config
}
@ -26,7 +26,7 @@ func NewResample(conf *config.Config) *Resample {
}
// Start creates default thumbnails for all files in originalsPath.
func (rs *Resample) Start(force bool) (err error) {
func (w *Resample) Start(force bool) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("resample: %s (panic)\nstack: %s", r, debug.Stack())
@ -40,14 +40,14 @@ func (rs *Resample) Start(force bool) (err error) {
defer mutex.MainWorker.Stop()
originalsPath := rs.conf.OriginalsPath()
thumbnailsPath := rs.conf.ThumbPath()
originalsPath := w.conf.OriginalsPath()
thumbnailsPath := w.conf.ThumbPath()
jobs := make(chan ResampleJob)
// Start a fixed number of goroutines to read and digest files.
var wg sync.WaitGroup
var numWorkers = rs.conf.Workers()
var numWorkers = w.conf.Workers()
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func() {

View file

@ -36,8 +36,8 @@ func Markers(limit, offset int, markerType string, embeddings, unmatched bool) (
return result, err
}
// Embeddings finds all face embeddings.
func Embeddings() (result entity.Embeddings, err error) {
// Embeddings returns existing face embeddings.
func Embeddings(single bool) (result entity.Embeddings, err error) {
var col []string
stmt := Db().
@ -52,7 +52,13 @@ func Embeddings() (result entity.Embeddings, err error) {
for _, embeddingsJson := range col {
if embeddings := entity.UnmarshalEmbeddings(embeddingsJson); len(embeddings) > 0 {
result = append(result, embeddings...)
if single {
// Single embedding per face detected.
result = append(result, embeddings[0])
} else {
// Return all embedding otherwise.
result = append(result, embeddings...)
}
}
}

19
internal/service/faces.go Normal file
View file

@ -0,0 +1,19 @@
package service
import (
"sync"
"github.com/photoprism/photoprism/internal/photoprism"
)
var onceFaces sync.Once
func initFaces() {
services.Faces = photoprism.NewFaces(Config())
}
func Faces() *photoprism.Faces {
onceFaces.Do(initFaces)
return services.Faces
}

View file

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

View file

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