Passed
Push — install-models-command ( 24a3b7...46ff74 )
by Matias
04:25
created

PersonMapper::updateFace()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 6
ccs 0
cts 6
cp 0
crap 2
rs 10
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017, Matias De lellis <[email protected]>
4
 * @copyright Copyright (c) 2018, Branko Kokanovic <[email protected]>
5
 *
6
 * @author Branko Kokanovic <[email protected]>
7
 *
8
 * @license GNU AGPL version 3 or any later version
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License as
12
 * published by the Free Software Foundation, either version 3 of the
13
 * License, or (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 *
23
 */
24
namespace OCA\FaceRecognition\Db;
25
26
use OC\DB\QueryBuilder\Literal;
0 ignored issues
show
Bug introduced by
The type OC\DB\QueryBuilder\Literal was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
28
use OCP\IDBConnection;
29
use OCP\IUser;
30
31
use OCP\AppFramework\Db\QBMapper;
32
use OCP\AppFramework\Db\DoesNotExistException;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
35
class PersonMapper extends QBMapper {
36
37
	public function __construct(IDBConnection $db) {
38
		parent::__construct($db, 'face_recognition_persons', '\OCA\FaceRecognition\Db\Person');
39
	}
40
41
42
	public function find(string $userId, int $personId): Person {
43
		$qb = $this->db->getQueryBuilder();
44
		$qb->select('id', 'name')
45
			->from('face_recognition_persons', 'p')
46
			->where($qb->expr()->eq('id', $qb->createNamedParameter($personId)))
47
			->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
48
		$person = $this->findEntity($qb);
49
		return $person;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $person returns the type OCP\AppFramework\Db\Entity which includes types incompatible with the type-hinted return OCA\FaceRecognition\Db\Person.
Loading history...
50
	}
51
52
	public function findAll(string $userId): array {
53
		$qb = $this->db->getQueryBuilder();
54
		$qb->select('id', 'name', 'is_valid')
55
			->from('face_recognition_persons', 'p')
56
			->where($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
57
58
		$person = $this->findEntities($qb);
59
		return $person;
60
	}
61
62
	/**
63
	 * Returns count of persons (clusters) found for a given user.
64
	 *
65
	 * @param string $userId ID of the user
66
	 * @param bool $onlyInvalid True if client wants count of invalid persons only,
67
	 *  false if client want count of all persons
68
	 * @return int Count of persons
69
	 */
70
	public function countPersons(string $userId, bool $onlyInvalid=false): int {
71
		$qb = $this->db->getQueryBuilder();
72
		$qb = $qb
73
			->select($qb->createFunction('COUNT(' . $qb->getColumnName('id') . ')'))
74
			->from($this->getTableName())
75
			->where($qb->expr()->eq('user', $qb->createParameter('user')));
76
		if ($onlyInvalid) {
77
			$qb = $qb
78
				->andWhere($qb->expr()->eq('is_valid', $qb->createParameter('is_valid')))
79
				->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL);
80
		}
81
		$query = $qb->setParameter('user', $userId);
82
		$resultStatement = $query->execute();
83
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
84
		$resultStatement->closeCursor();
85
86
		return (int)$data[0];
87
	}
88
89
	/**
90
	 * Based on a given fileId, takes all person that belong to that image
91
	 * and return an array with that.
92
	 *
93
	 * @param string $userId ID of the user that clusters belong to
94
	 * @param int $fileId ID of file image for which to searh persons.
95
	 *
96
	 * @return array of persons
97
	 */
98
	public function findFromFile(string $userId, int $fileId): array {
99
		$qb = $this->db->getQueryBuilder();
100
		$qb->select('p.id', 'name');
101
		$qb->from("face_recognition_persons", "p")
102
			->innerJoin('p', 'face_recognition_faces' ,'f', $qb->expr()->eq('p.id', 'f.person'))
103
			->innerJoin('p', 'face_recognition_images' ,'i', $qb->expr()->eq('i.id', 'f.image'))
104
			->where($qb->expr()->eq('p.user', $qb->createNamedParameter($userId)))
105
			->andWhere($qb->expr()->eq('i.file', $qb->createNamedParameter($fileId)));
106
		$persons = $this->findEntities($qb);
107
108
		return $persons;
109
	}
110
111
	/**
112
	 * Based on a given image, takes all faces that belong to that image
113
	 * and invalidates all person that those faces belongs to.
114
	 *
115
	 * @param int $imageId ID of image for which to invalidate persons for
116
	 */
117
	public function invalidatePersons(int $imageId) {
118
		$sub = $this->db->getQueryBuilder();
119
		$tableNameWithPrefixWithoutQuotes = trim($sub->getTableName($this->getTableName()), '`');
120
		$sub->select(new Literal('1'));
121
		$sub->from("face_recognition_images", "i")
122
			->innerJoin('i', 'face_recognition_faces' ,'f', $sub->expr()->eq('i.id', 'f.image'))
123
			->where($sub->expr()->eq($tableNameWithPrefixWithoutQuotes . '.id', 'f.person'))
124
			->andWhere($sub->expr()->eq('i.id', $sub->createParameter('image_id')));
125
126
		$qb = $this->db->getQueryBuilder();
127
		$qb->update($this->getTableName())
128
			->set("is_valid", $qb->createParameter('is_valid'))
129
			->where('EXISTS (' . $sub->getSQL() . ')')
130
			->setParameter('image_id', $imageId)
131
			->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL)
132
			->execute();
133
	}
134
135
	/**
136
	 * Updates one face with $faceId to database to person ID $personId.
137
	 *
138
	 * @param int $faceId ID of the face
139
	 * @param int|null $personId ID of the person
140
	 */
141
	private function updateFace(int $faceId, $personId) {
142
		$qb = $this->db->getQueryBuilder();
143
		$qb->update('face_recognition_faces')
144
			->set("person", $qb->createNamedParameter($personId))
145
			->where($qb->expr()->eq('id', $qb->createNamedParameter($faceId)))
146
			->execute();
147
	}
148
149
	/**
150
	 * Based on current clusters and new clusters, do database reconciliation.
151
	 * It tries to do that in minimal number of SQL queries. Operation is atomic.
152
	 *
153
	 * Clusters are array, where keys are ID of persons, and values are indexed arrays
154
	 * with values that are ID of the faces for those persons.
155
	 *
156
	 * @param string $userId ID of the user that clusters belong to
157
	 * @param array $currentClusters Current clusters
158
	 * @param array $newClusters New clusters
159
	 */
160
	public function mergeClusterToDatabase(string $userId, $currentClusters, $newClusters) {
161
		$this->db->beginTransaction();
162
		$currentDateTime = new \DateTime();
163
164
		try {
165
			// Delete clusters that do not exist anymore
166
			foreach($currentClusters as $oldPerson => $oldFaces) {
167
				if (array_key_exists($oldPerson, $newClusters)) {
168
					continue;
169
				}
170
171
				// OK, we bumped into cluster that existed and now it does not exist.
172
				// We need to remove all references to it and to delete it.
173
				foreach ($oldFaces as $oldFace) {
174
					$this->updateFace($oldFace, null);
175
				}
176
177
				// todo: this is not very cool. What if user had associated linked user to this. And all lost?
178
				$qb = $this->db->getQueryBuilder();
179
				// todo: for extra safety, we should probably add here additional condition, where (user=$userId)
180
				$qb
181
					->delete($this->getTableName())
182
					->where($qb->expr()->eq('id', $qb->createNamedParameter($oldPerson)))
183
					->execute();
184
			}
185
186
			// Modify existing clusters
187
			foreach($newClusters as $newPerson=>$newFaces) {
188
				if (!array_key_exists($newPerson, $currentClusters)) {
189
					// This cluster didn't exist, there is nothing to modify
190
					// It will be processed during cluster adding operation
191
					continue;
192
				}
193
194
				$oldFaces = $currentClusters[$newPerson];
195
				if ($newFaces === $oldFaces) {
196
					// Set cluster as valid now
197
					$qb = $this->db->getQueryBuilder();
198
					$qb
199
						->update($this->getTableName())
200
						->set("is_valid", $qb->createParameter('is_valid'))
201
						->where($qb->expr()->eq('id', $qb->createNamedParameter($newPerson)))
202
						->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
203
						->execute();
204
					continue;
205
				}
206
207
				// OK, set of faces do differ. Now, we could potentially go into finer grain details
208
				// and add/remove each individual face, but this seems too detailed. Enough is to
209
				// reset all existing faces to null and to add new faces to new person. That should
210
				// take care of both faces that are removed from cluster, as well as for newly added
211
				// faces to this cluster.
212
213
				// First remove all old faces from any cluster (reset them to null)
214
				foreach ($oldFaces as $oldFace) {
215
					// Reset face to null only if it wasn't moved to other cluster!
216
					// (if face is just moved to other cluster, do not reset to null, as some other
217
					// pass for some other cluster will eventually update it to proper cluster)
218
					if ($this->isFaceInClusters($oldFace, $newClusters) === false) {
219
						$this->updateFace($oldFace, null);
220
					}
221
				}
222
223
				// Then set all new faces to belong to this cluster
224
				foreach ($newFaces as $newFace) {
225
					$this->updateFace($newFace, $newPerson);
226
				}
227
228
				// Set cluster as valid now
229
				$qb = $this->db->getQueryBuilder();
230
				$qb
231
					->update($this->getTableName())
232
					->set("is_valid", $qb->createParameter('is_valid'))
233
					->where($qb->expr()->eq('id', $qb->createNamedParameter($newPerson)))
234
					->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
235
					->execute();
236
			}
237
238
			// Add new clusters
239
			foreach($newClusters as $newPerson=>$newFaces) {
240
				if (array_key_exists($newPerson, $currentClusters)) {
241
					// This cluster already existed, nothing to add
242
					// It was already processed during modify cluster operation
243
					continue;
244
				}
245
246
				// Create new cluster and add all faces to it
247
				$qb = $this->db->getQueryBuilder();
248
				$qb
249
					->insert($this->getTableName())
250
					->values([
251
						'user' => $qb->createNamedParameter($userId),
252
						'name' => $qb->createNamedParameter(sprintf("New person %d", $newPerson)),
253
						'is_valid' => $qb->createNamedParameter(true),
254
						'last_generation_time' => $qb->createNamedParameter($currentDateTime, IQueryBuilder::PARAM_DATE),
255
						'linked_user' => $qb->createNamedParameter(null)])
256
					->execute();
257
				$insertedPersonId = $this->db->lastInsertId($this->getTableName());
258
				foreach ($newFaces as $newFace) {
259
					$this->updateFace($newFace, $insertedPersonId);
260
				}
261
			}
262
263
			$this->db->commit();
264
		} catch (\Exception $e) {
265
			$this->db->rollBack();
266
			throw $e;
267
		}
268
	}
269
270
	/**
271
	 * Deletes all persons from that user.
272
	 *
273
	 * @param string $userId User to drop persons from a table.
274
	 */
275
	public function deleteUserPersons(string $userId) {
276
		$qb = $this->db->getQueryBuilder();
277
		$qb->delete($this->getTableName())
278
			->where($qb->expr()->eq('user', $qb->createNamedParameter($userId)))
279
			->execute();
280
	}
281
282
	/**
283
	 * Deletes person if it is empty (have no faces associated to it)
284
	 *
285
	 * @param int $personId Person to check if it should be deleted
286
	 */
287
	public function removeIfEmpty(int $personId) {
288
		$sub = $this->db->getQueryBuilder();
289
		$sub->select(new Literal('1'));
290
		$sub->from("face_recognition_faces", "f")
291
			->where($sub->expr()->eq('f.person', $sub->createParameter('person')));
292
293
		$qb = $this->db->getQueryBuilder();
294
		$qb->delete($this->getTableName())
295
			->where($qb->expr()->eq('id', $qb->createParameter('person')))
296
			->andWhere('NOT EXISTS (' . $sub->getSQL() . ')')
297
			->setParameter('person', $personId)
298
			->execute();
299
	}
300
301
	/**
302
	 * Deletes all persons that have no faces associated to them
303
	 *
304
	 * @param string $userId ID of user for which we are deleting orphaned persons
305
	 */
306
	public function deleteOrphaned(string $userId): int {
307
		$sub = $this->db->getQueryBuilder();
308
		$sub->select(new Literal('1'));
309
		$sub->from("face_recognition_faces", "f")
310
			->where($sub->expr()->eq('f.person', 'p.id'));
311
312
		$qb = $this->db->getQueryBuilder();
313
		$qb->select('p.id')
314
			->from($this->getTableName(), 'p')
315
			->where($qb->expr()->eq('p.user', $qb->createParameter('user')))
316
			->andWhere('NOT EXISTS (' . $sub->getSQL() . ')')
317
			->setParameter('user', $userId);
318
		$orphanedPersons = $this->findEntities($qb);
319
320
		$orphaned = 0;
321
		foreach ($orphanedPersons as $person) {
322
			$qb = $this->db->getQueryBuilder();
323
			$orphaned += $qb->delete($this->getTableName())
324
				->where($qb->expr()->eq('id', $qb->createNamedParameter($person->id)))
325
				->execute();
326
		}
327
		return $orphaned;
328
	}
329
330
	/**
331
	 * Checks if face with a given ID is in any cluster.
332
	 *
333
	 * @param int $faceId ID of the face to check
334
	 * @param array $cluster All clusters to check into
335
	 *
336
	 * @return bool True if face is found in any cluster, false otherwise.
337
	 */
338
	private function isFaceInClusters(int $faceId, array $clusters): bool {
339
		foreach ($clusters as $_=>$faces) {
340
			if (in_array($faceId, $faces)) {
341
				return true;
342
			}
343
		}
344
		return false;
345
	}
346
}
347