|
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 { |
|
|
|
|
|
|
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
|
|
|
|
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.