Completed
Pull Request — master (#49)
by Matias
02:08
created

PersonMapper::findFromFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 0
cts 13
cp 0
rs 9.7
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
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;
27
28
use OCP\IDBConnection;
29
use OCP\IUser;
30
31
use OCP\AppFramework\Db\Mapper;
32
use OCP\AppFramework\Db\DoesNotExistException;
33
use OCP\DB\QueryBuilder\IQueryBuilder;
34
35
class PersonMapper extends Mapper {
36
37
	public function __construct(IDBConnection $db) {
38
		parent::__construct($db, 'face_recognition_persons', '\OCA\FaceRecognition\Db\Person');
39
	}
40
41
42 View Code Duplication
	public function find (string $userId, int $personId): Person {
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
43
		$qb = $this->db->getQueryBuilder();
44
		$qb->select('id', 'name')
45
			->from('face_recognition_persons', 'p')
46
			->where($qb->expr()->eq('id', $qb->createParameter('person_id')))
47
			->andWhere($qb->expr()->eq('user', $qb->createParameter('user_id')));
48
49
		$params = array();
50
		$params['person_id'] = $personId;
51
		$params['user_id'] = $userId;
52
53
		$person = $this->findEntity($qb->getSQL(), $params);
54
		return $person;
55
	}
56
57
	/**
58
	 * Returns count of persons (clusters) found for a given user.
59
	 *
60
	 * @param string $userId ID of the user
61
	 *
62
	 * @return int Count of persons
63
	 */
64
	public function countPersons(string $userId): int {
65
		$qb = $this->db->getQueryBuilder();
66
		$query = $qb
67
			->select($qb->createFunction('COUNT(' . $qb->getColumnName('id') . ')'))
68
			->from($this->getTableName())
69
			->where($qb->expr()->eq('user', $qb->createParameter('user')))
70
			->setParameter('user', $userId);
71
		$resultStatement = $query->execute();
72
		$data = $resultStatement->fetch(\PDO::FETCH_NUM);
73
		$resultStatement->closeCursor();
74
75
		return (int)$data[0];
76
	}
77
78
	/**
79
	 * Based on a given fileId, takes all person that belong to that image
80
	 * and return an array with that.
81
	 *
82
	 * @param string $userId ID of the user that clusters belong to
83
	 * @param int $fileId ID of file image for which to searh persons.
84
	 *
85
	 * @return array of persons
86
	 */
87
	public function findFromFile(string $userId, int $fileId): array {
88
		$qb = $this->db->getQueryBuilder();
89
		$qb->select('p.id', 'name');
90
		$qb->from("face_recognition_persons", "p")
91
			->innerJoin('p', 'face_recognition_faces' ,'f', $qb->expr()->eq('p.id', 'f.person'))
92
			->innerJoin('p', 'face_recognition_images' ,'i', $qb->expr()->eq('i.id', 'f.image'))
93
			->where($qb->expr()->eq('p.user', $qb->createParameter('user')))
94
			->andWhere($qb->expr()->eq('i.file', $qb->createParameter('file_id')));
95
96
		$params = array();
97
		$params['user'] = $userId;
98
		$params['file_id'] = $fileId;
99
100
		$persons = $this->findEntities($qb->getSQL(), $params);
101
102
		return $persons;
103
	}
104
105
	/**
106
	 * Based on a given image, takes all faces that belong to that image
107
	 * and invalidates all person that those faces belongs to.
108
	 *
109
	 * @param int $imageId ID of image for which to invalidate persons for
110
	 */
111
	public function invalidatePersons(int $imageId) {
112
		$sub = $this->db->getQueryBuilder();
113
		$sub->select(new Literal('1'));
114
		$sub->from("face_recognition_images", "i")
115
			->innerJoin('i', 'face_recognition_faces' ,'f', $sub->expr()->eq('i.id', 'f.image'))
116
			->where($sub->expr()->eq('p.id', 'f.person'))
117
			->andWhere($sub->expr()->eq('i.id', $sub->createParameter('image_id')));
118
119
		$qb = $this->db->getQueryBuilder();
120
		$qb->update($this->getTableName(), 'p')
121
			->set("is_valid", $qb->createParameter('is_valid'))
122
			->where('EXISTS (' . $sub->getSQL() . ')')
123
			->setParameter('image_id', $imageId)
124
			->setParameter('is_valid', false, IQueryBuilder::PARAM_BOOL)
125
			->execute();
126
	}
127
128
	/**
129
	 * Updates one face with $faceId to database to person ID $personId.
130
	 *
131
	 * @param int $faceId ID of the face
132
	 * @param int|null $personId ID of the person
133
	 */
134
	private function updateFace(int $faceId, $personId) {
135
		$qb = $this->db->getQueryBuilder();
136
		$qb->update('face_recognition_faces')
137
			->set("person", $qb->createNamedParameter($personId))
138
			->where($qb->expr()->eq('id', $qb->createNamedParameter($faceId)))
139
			->execute();
140
	}
141
142
	/**
143
	 * Based on current clusters and new clusters, do database reconciliation.
144
	 * It tries to do that in minumal number of SQL queries. Operation is atomic.
145
	 *
146
	 * Clusters are array, where keys are ID of persons, and values are indexed arrays
147
	 * with values that are ID of the faces for those persons.
148
	 *
149
	 * @param string $userId ID of the user that clusters belong to
150
	 * @param array $currentClusters Current clusters
151
	 * @param array $newClusters New clusters
152
	 */
153
	public function mergeClusterToDatabase(string $userId, $currentClusters, $newClusters) {
154
		$this->db->beginTransaction();
155
		$currentDateTime = new \DateTime();
156
157
		try {
158
			// Delete clusters that do not exist anymore
159
			foreach($currentClusters as $oldPerson => $oldFaces) {
160
				if (array_key_exists($oldPerson, $newClusters)) {
161
					continue;
162
				}
163
164
				// OK, we bumped into cluster that existed and now it does not exist.
165
				// We need to remove all references to it and to delete it.
166
				foreach ($oldFaces as $oldFace) {
167
					$this->updateFace($oldFace, null);
168
				}
169
170
				// todo: this is not very cool. What if user had associated linked user to this. And all lost?
171
				$qb = $this->db->getQueryBuilder();
172
				// todo: for extra safety, we should probably add here additional condition, where (user=$userId)
173
				$qb
174
					->delete($this->getTableName())
175
					->where($qb->expr()->eq('id', $qb->createNamedParameter($oldPerson)))
176
					->execute();
177
			}
178
179
			// Add or modify existing clusters
180
			foreach($newClusters as $newPerson=>$newFaces) {
181
				if (array_key_exists($newPerson, $currentClusters)) {
182
					// This cluster existed, check if faces match
183
					$oldFaces = $currentClusters[$newPerson];
184
					if ($newFaces == $oldFaces) {
185
						continue;
186
					}
187
188
					// OK, set of faces do differ. Now, we could potentially go into finer grain details
189
					// and add/remove each individual face, but this seems too detailed. Enough is to
190
					// reset all existing faces to null and to add new faces to new person. That should
191
					// take care of both faces that are removed from cluster, as well as for newly added
192
					// faces to this cluster.
193
					foreach ($oldFaces as $oldFace) {
194
						$this->updateFace($oldFace, null);
195
					}
196
					foreach ($newFaces as $newFace) {
197
						$this->updateFace($newFace, $newPerson);
198
					}
199
				} else {
200
					// This person doesn't even exist, insert it
201
					$qb = $this->db->getQueryBuilder();
202
					$qb
203
						->insert($this->getTableName())
204
						->values([
205
							'user' => $qb->createNamedParameter($userId),
206
							'name' => $qb->createNamedParameter(sprintf("New person %d", $newPerson)),
207
							'is_valid' => $qb->createNamedParameter(true),
208
							'last_generation_time' => $qb->createNamedParameter($currentDateTime, IQueryBuilder::PARAM_DATE),
209
							'linked_user' => $qb->createNamedParameter(null)])
210
						->execute();
211
					$insertedPersonId = $this->db->lastInsertId($this->getTableName());
212
					foreach ($newFaces as $newFace) {
213
						$this->updateFace($newFace, $insertedPersonId);
214
					}
215
				}
216
			}
217
218
			$this->db->commit();
219
		} catch (\Exception $e) {
220
			$this->db->rollBack();
221
			throw $e;
222
		}
223
	}
224
}
225