Passed
Push — split-cluster-person ( 7ada7e...098531 )
by Matias
05:26
created

PersonMapper::findUnassigned()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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