Passed
Push — find-similar ( 3204f0...e726cf )
by Matias
04:36
created

PersonMapper::findFromFace()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 2
dl 0
loc 9
ccs 0
cts 8
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2018-2020, Matias De lellis <[email protected]>
4
 * @copyright Copyright (c) 2018-2019, 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 1
	public function __construct(IDBConnection $db) {
38 1
		parent::__construct($db, 'facerecog_persons', '\OCA\FaceRecognition\Db\Person');
39 1
	}
40
41
	/**
42
	 * @param string $userId ID of the user
43
	 * @param int $personId ID of the person
44
	 */
45 8
	public function find(string $userId, int $personId) {
46 8
		$qb = $this->db->getQueryBuilder();
47 8
		$qb->select('id', 'name')
48 8
			->from($this->getTableName(), 'p')
49 8
			->where($qb->expr()->eq('id', $qb->createNamedParameter($personId)))
50 8
			->andWhere($qb->expr()->eq('user', $qb->createNamedParameter($userId)));
51 8
		return $this->findEntity($qb);
52
	}
53
54
	/**
55
	 * @param string $userId ID of the user
56
	 * @param int $modelId ID of the model
57
	 * @param string $personName name of the person to find
58
	 * @return Person[]
59
	 */
60
	public function findByName(string $userId, int $modelId, string $personName): array {
61
		$sub = $this->db->getQueryBuilder();
62
		$sub->select(new Literal('1'))
63
			->from('facerecog_faces', 'f')
64
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
65
			->where($sub->expr()->eq('p.id', 'f.person'))
66
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
67
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')))
68
			->andwhere($sub->expr()->eq('p.name', $sub->createParameter('person_name')));
69
70
		$qb = $this->db->getQueryBuilder();
71
		$qb->select('id', 'name', 'is_valid')
72
			->from($this->getTableName(), 'p')
73
			->where('EXISTS (' . $sub->getSQL() . ')')
74
			->setParameter('user_id', $userId)
75
			->setParameter('model_id', $modelId)
76
			->setParameter('person_name', $personName);
77
78
		return $this->findEntities($qb);
79
	}
80
81
	/**
82
	 * @param string $userId ID of the user
83
	 * @param int $modelId ID of the model
84
	 * @return Person[]
85
	 */
86 12
	public function findAll(string $userId, int $modelId): array {
87 12
		$sub = $this->db->getQueryBuilder();
88 12
		$sub->select(new Literal('1'))
89 12
			->from('facerecog_faces', 'f')
90 12
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
91 12
			->where($sub->expr()->eq('p.id', 'f.person'))
92 12
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
93 12
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')));
94
95 12
		$qb = $this->db->getQueryBuilder();
96 12
		$qb->select('id', 'name', 'is_valid')
97 12
			->from($this->getTableName(), 'p')
98 12
			->where('EXISTS (' . $sub->getSQL() . ')')
99 12
			->setParameter('user_id', $userId)
100 12
			->setParameter('model_id', $modelId);
101
102 12
		return $this->findEntities($qb);
103
	}
104
105
	/**
106
	 *  Find a person that contains a face.
107
	 *
108
	 * @param string $userId ID of the user
109
	 * @param int $faceId ID of the face that belongs to the wanted person
110
	 * @return Person
111
	 */
112
	public function findFromFace(string $userId, int $faceId) {
113
		$qb = $this->db->getQueryBuilder();
114
		$qb->select('p.id', 'p.name', 'p.is_valid')
115
		   ->from($this->getTableName(), 'p')
116
		   ->innerJoin('p', 'facerecog_faces' ,'f', $qb->expr()->eq('p.id', 'f.person'))
117
		   ->where($qb->expr()->eq('p.user',  $qb->createNamedParameter($userId)))
118
		   ->andWhere($qb->expr()->eq('f.id', $qb->createNamedParameter($faceId)));
119
120
		return $this->findEntity($qb);
121
	}
122
123
	/**
124
	 * Returns count of persons (clusters) found for a given user.
125
	 *
126
	 * @param string $userId ID of the user
127
	 * @param int $modelId ID of the model
128
	 * @param bool $onlyInvalid True if client wants count of invalid persons only,
129
	 *  false if client want count of all persons
130
	 * @return int Count of persons
131
	 */
132 14
	public function countPersons(string $userId, int $modelId, bool $onlyInvalid=false): int {
133 14
		$sub = $this->db->getQueryBuilder();
134 14
		$sub->select(new Literal('1'))
135 14
			->from('facerecog_faces', 'f')
136 14
			->innerJoin('f', 'facerecog_images' ,'i', $sub->expr()->eq('f.image', 'i.id'))
137 14
			->where($sub->expr()->eq('p.id', 'f.person'))
138 14
			->andWhere($sub->expr()->eq('i.user', $sub->createParameter('user_id')))
139 14
			->andWhere($sub->expr()->eq('i.model', $sub->createParameter('model_id')));
140
141 14
		$qb = $this->db->getQueryBuilder();
142 14
		$qb->select($qb->createFunction('COUNT(' . $qb->getColumnName('id') . ')'))
143 14
			->from($this->getTableName(), 'p')
144 14
			->where('EXISTS (' . $sub->getSQL() . ')');
145
146 14
		if ($onlyInvalid) {
147
			$qb = $qb
148
				->andWhere($qb->expr()->eq('is_valid', $qb->createParameter('is_valid')))
149
				->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL);
150
		}
151
152
		$qb = $qb
153 14
			->setParameter('user_id', $userId)
154 14
			->setParameter('model_id', $modelId);
155
156 14
		$resultStatement = $qb->execute();
157 14
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
158 14
		$resultStatement->closeCursor();
159
160 14
		return (int)$data[0];
161
	}
162
163
	/**
164
	 * Based on a given fileId, takes all person that belong to that image
165
	 * and return an array with that.
166
	 *
167
	 * @param string $userId ID of the user that clusters belong to
168
	 * @param int $modelId ID of the model that clusters belgon to
169
	 * @param int $fileId ID of file image for which to searh persons.
170
	 *
171
	 * @return Person[] Array of persons on that file
172
	 */
173
	public function findFromFile(string $userId, int $modelId, int $fileId): array {
174
		$qb = $this->db->getQueryBuilder();
175
		$qb->select('p.id', 'name');
176
		$qb->from($this->getTableName(), 'p')
177
			->innerJoin('p', 'facerecog_faces' ,'f', $qb->expr()->eq('p.id', 'f.person'))
178
			->innerJoin('p', 'facerecog_images' ,'i', $qb->expr()->eq('i.id', 'f.image'))
179
			->where($qb->expr()->eq('p.user', $qb->createNamedParameter($userId)))
180
			->andWhere($qb->expr()->eq('i.model', $qb->createNamedParameter($modelId)))
181
			->andWhere($qb->expr()->eq('i.file', $qb->createNamedParameter($fileId)));
182
		return $this->findEntities($qb);
183
	}
184
185
	/**
186
	 * Based on a given image, takes all faces that belong to that image
187
	 * and invalidates all person that those faces belongs to.
188
	 *
189
	 * @param int $imageId ID of image for which to invalidate persons for
190
	 */
191 12
	public function invalidatePersons(int $imageId) {
192 12
		$sub = $this->db->getQueryBuilder();
193 12
		$tableNameWithPrefixWithoutQuotes = trim($sub->getTableName($this->getTableName()), '`');
194 12
		$sub->select(new Literal('1'));
195 12
		$sub->from('facerecog_images', 'i')
196 12
			->innerJoin('i', 'facerecog_faces' ,'f', $sub->expr()->eq('i.id', 'f.image'))
197 12
			->where($sub->expr()->eq($tableNameWithPrefixWithoutQuotes . '.id', 'f.person'))
198 12
			->andWhere($sub->expr()->eq('i.id', $sub->createParameter('image_id')));
199
200 12
		$qb = $this->db->getQueryBuilder();
201 12
		$qb->update($this->getTableName())
202 12
			->set("is_valid", $qb->createParameter('is_valid'))
203 12
			->where('EXISTS (' . $sub->getSQL() . ')')
204 12
			->setParameter('image_id', $imageId)
205 12
			->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL)
206 12
			->execute();
207 12
	}
208
209
	/**
210
	 * Based on current clusters and new clusters, do database reconciliation.
211
	 * It tries to do that in minimal number of SQL queries. Operation is atomic.
212
	 *
213
	 * Clusters are array, where keys are ID of persons, and values are indexed arrays
214
	 * with values that are ID of the faces for those persons.
215
	 *
216
	 * @param string $userId ID of the user that clusters belong to
217
	 * @param array $currentClusters Current clusters
218
	 * @param array $newClusters New clusters
219
	 */
220 13
	public function mergeClusterToDatabase(string $userId, $currentClusters, $newClusters) {
221 13
		$this->db->beginTransaction();
222 13
		$currentDateTime = new \DateTime();
223
224
		try {
225
			// Delete clusters that do not exist anymore
226 13
			foreach($currentClusters as $oldPerson => $oldFaces) {
227 11
				if (array_key_exists($oldPerson, $newClusters)) {
228 6
					continue;
229
				}
230
231
				// OK, we bumped into cluster that existed and now it does not exist.
232
				// We need to remove all references to it and to delete it.
233 7
				foreach ($oldFaces as $oldFace) {
234 7
					$this->updateFace($oldFace, null);
235
				}
236
237
				// todo: this is not very cool. What if user had associated linked user to this. And all lost?
238 7
				$qb = $this->db->getQueryBuilder();
239
				// todo: for extra safety, we should probably add here additional condition, where (user=$userId)
240
				$qb
241 7
					->delete($this->getTableName())
242 7
					->where($qb->expr()->eq('id', $qb->createNamedParameter($oldPerson)))
243 7
					->execute();
244
			}
245
246
			// Modify existing clusters
247 13
			foreach($newClusters as $newPerson=>$newFaces) {
248 11
				if (!array_key_exists($newPerson, $currentClusters)) {
249
					// This cluster didn't exist, there is nothing to modify
250
					// It will be processed during cluster adding operation
251 8
					continue;
252
				}
253
254 6
				$oldFaces = $currentClusters[$newPerson];
255 6
				if ($newFaces === $oldFaces) {
256
					// Set cluster as valid now
257 2
					$qb = $this->db->getQueryBuilder();
258
					$qb
259 2
						->update($this->getTableName())
260 2
						->set("is_valid", $qb->createParameter('is_valid'))
261 2
						->where($qb->expr()->eq('id', $qb->createNamedParameter($newPerson)))
262 2
						->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
263 2
						->execute();
264 2
					continue;
265
				}
266
267
				// OK, set of faces do differ. Now, we could potentially go into finer grain details
268
				// and add/remove each individual face, but this seems too detailed. Enough is to
269
				// reset all existing faces to null and to add new faces to new person. That should
270
				// take care of both faces that are removed from cluster, as well as for newly added
271
				// faces to this cluster.
272
273
				// First remove all old faces from any cluster (reset them to null)
274 5
				foreach ($oldFaces as $oldFace) {
275
					// Reset face to null only if it wasn't moved to other cluster!
276
					// (if face is just moved to other cluster, do not reset to null, as some other
277
					// pass for some other cluster will eventually update it to proper cluster)
278 5
					if ($this->isFaceInClusters($oldFace, $newClusters) === false) {
279 5
						$this->updateFace($oldFace, null);
280
					}
281
				}
282
283
				// Then set all new faces to belong to this cluster
284 5
				foreach ($newFaces as $newFace) {
285 5
					$this->updateFace($newFace, $newPerson);
286
				}
287
288
				// Set cluster as valid now
289 5
				$qb = $this->db->getQueryBuilder();
290
				$qb
291 5
					->update($this->getTableName())
292 5
					->set("is_valid", $qb->createParameter('is_valid'))
293 5
					->where($qb->expr()->eq('id', $qb->createNamedParameter($newPerson)))
294 5
					->setParameter('is_valid', true, IQueryBuilder::PARAM_BOOL)
295 5
					->execute();
296
			}
297
298
			// Add new clusters
299 13
			foreach($newClusters as $newPerson=>$newFaces) {
300 11
				if (array_key_exists($newPerson, $currentClusters)) {
301
					// This cluster already existed, nothing to add
302
					// It was already processed during modify cluster operation
303 6
					continue;
304
				}
305
306
				// Create new cluster and add all faces to it
307 8
				$qb = $this->db->getQueryBuilder();
308
				$qb
309 8
					->insert($this->getTableName())
310 8
					->values([
311 8
						'user' => $qb->createNamedParameter($userId),
312 8
						'name' => $qb->createNamedParameter(sprintf("New person %d", $newPerson)),
313 8
						'is_valid' => $qb->createNamedParameter(true),
314 8
						'last_generation_time' => $qb->createNamedParameter($currentDateTime, IQueryBuilder::PARAM_DATE),
315 8
						'linked_user' => $qb->createNamedParameter(null)])
316 8
					->execute();
317 8
				$insertedPersonId = $this->db->lastInsertId($this->getTableName());
318 8
				foreach ($newFaces as $newFace) {
319 8
					$this->updateFace($newFace, $insertedPersonId);
320
				}
321
			}
322
323 13
			$this->db->commit();
324
		} catch (\Exception $e) {
325
			$this->db->rollBack();
326
			throw $e;
327
		}
328 13
	}
329
330
	/**
331
	 * Deletes all persons from that user.
332
	 *
333
	 * @param string $userId User to drop persons from a table.
334
	 */
335 27
	public function deleteUserPersons(string $userId) {
336 27
		$qb = $this->db->getQueryBuilder();
337 27
		$qb->delete($this->getTableName())
338 27
			->where($qb->expr()->eq('user', $qb->createNamedParameter($userId)))
339 27
			->execute();
340 27
	}
341
342
	/**
343
	 * Deletes person if it is empty (have no faces associated to it)
344
	 *
345
	 * @param int $personId Person to check if it should be deleted
346
	 */
347
	public function removeIfEmpty(int $personId) {
348
		$sub = $this->db->getQueryBuilder();
349
		$sub->select(new Literal('1'));
350
		$sub->from('facerecog_faces', 'f')
351
			->where($sub->expr()->eq('f.person', $sub->createParameter('person')));
352
353
		$qb = $this->db->getQueryBuilder();
354
		$qb->delete($this->getTableName())
355
			->where($qb->expr()->eq('id', $qb->createParameter('person')))
356
			->andWhere('NOT EXISTS (' . $sub->getSQL() . ')')
357
			->setParameter('person', $personId)
358
			->execute();
359
	}
360
361
	/**
362
	 * Deletes all persons that have no faces associated to them
363
	 *
364
	 * @param string $userId ID of user for which we are deleting orphaned persons
365
	 */
366
	public function deleteOrphaned(string $userId): int {
367
		$sub = $this->db->getQueryBuilder();
368
		$sub->select(new Literal('1'));
369
		$sub->from('facerecog_faces', 'f')
370
			->where($sub->expr()->eq('f.person', 'p.id'));
371
372
		$qb = $this->db->getQueryBuilder();
373
		$qb->select('p.id')
374
			->from($this->getTableName(), 'p')
375
			->where($qb->expr()->eq('p.user', $qb->createParameter('user')))
376
			->andWhere('NOT EXISTS (' . $sub->getSQL() . ')')
377
			->setParameter('user', $userId);
378
		$orphanedPersons = $this->findEntities($qb);
379
380
		$orphaned = 0;
381
		foreach ($orphanedPersons as $person) {
382
			$qb = $this->db->getQueryBuilder();
383
			$orphaned += $qb->delete($this->getTableName())
384
				->where($qb->expr()->eq('id', $qb->createNamedParameter($person->id)))
385
				->execute();
386
		}
387
		return $orphaned;
388
	}
389
390
	/**
391
	 * Updates one face with $faceId to database to person ID $personId.
392
	 *
393
	 * @param int $faceId ID of the face
394
	 * @param int|null $personId ID of the person
395
	 */
396 11
	private function updateFace(int $faceId, $personId) {
397 11
		$qb = $this->db->getQueryBuilder();
398 11
		$qb->update('facerecog_faces')
399 11
			->set("person", $qb->createNamedParameter($personId))
400 11
			->where($qb->expr()->eq('id', $qb->createNamedParameter($faceId)))
401 11
			->execute();
402 11
	}
403
404
	/**
405
	 * Checks if face with a given ID is in any cluster.
406
	 *
407
	 * @param int $faceId ID of the face to check
408
	 * @param array $cluster All clusters to check into
409
	 *
410
	 * @return bool True if face is found in any cluster, false otherwise.
411
	 */
412 5
	private function isFaceInClusters(int $faceId, array $clusters): bool {
413 5
		foreach ($clusters as $_=>$faces) {
414 5
			if (in_array($faceId, $faces)) {
415 5
				return true;
416
			}
417
		}
418 1
		return false;
419
	}
420
}
421