Completed
Push — exception_in_tasks ( 7463cd...3899e4 )
by Branko
01:58
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 7
cp 0
rs 9.7
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2

1 Method

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