Passed
Push — self-contained-model ( ef7b10...695739 )
by Matias
03:56
created

PersonMapper::findFromFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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