Passed
Pull Request — master (#751)
by Matias
07:33 queued 04:51
created

ImageProcessingTask   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Test Coverage

Coverage 86.92%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 110
c 2
b 0
f 0
dl 0
loc 249
ccs 93
cts 107
cp 0.8692
rs 10
wmc 24

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getMaxImageArea() 0 25 5
A __construct() 0 16 1
A getTempImage() 0 23 4
A getNormalizedFace() 0 10 1
A description() 0 2 1
D execute() 0 97 10
A getNormalizedLandmarks() 0 9 2
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017-2020 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\BackgroundJob\Tasks;
25
26
use OCP\Image as OCP_Image;
27
28
use OCP\Files\File;
29
use OCP\Files\Folder;
30
use OCP\Lock\ILockingProvider;
31
use OCP\IUser;
32
33
use OCA\FaceRecognition\BackgroundJob\FaceRecognitionBackgroundTask;
34
use OCA\FaceRecognition\BackgroundJob\FaceRecognitionContext;
35
36
use OCA\FaceRecognition\Db\Face;
37
use OCA\FaceRecognition\Db\Image;
38
use OCA\FaceRecognition\Db\ImageMapper;
39
40
use OCA\FaceRecognition\Helper\TempImage;
41
42
use OCA\FaceRecognition\Model\IModel;
43
use OCA\FaceRecognition\Model\ModelManager;
44
45
use OCA\FaceRecognition\Service\FileService;
46
use OCA\FaceRecognition\Service\SettingsService;
47
48
/**
49
 * Taks that get all images that are still not processed and processes them.
50
 * Processing image means that each image is prepared, faces extracted form it,
51
 * and for each found face - face descriptor is extracted.
52
 */
53
class ImageProcessingTask extends FaceRecognitionBackgroundTask {
54
55
	/** @var ImageMapper Image mapper*/
56
	protected $imageMapper;
57
58
	/** @var FileService */
59
	protected $fileService;
60
61
	/** @var SettingsService */
62
	protected $settingsService;
63
64
	/** @var ModelManager $modelManager */
65
	protected $modelManager;
66
67
	/** @var ILockingProvider $lockingProvider */
68
	protected ILockingProvider $lockingProvider;
69
70
	/** @var IModel $model */
71
	private $model;
72
73
	/** @var int|null $maxImageAreaCached Maximum image area (cached, so it is not recalculated for each image) */
74
	private $maxImageAreaCached;
75
76
77
	/**
78
	 * @param ImageMapper $imageMapper Image mapper
79
	 * @param FileService $fileService
80
	 * @param SettingsService $settingsService
81
	 * @param ModelManager $modelManager Model manager
82
	 * @param ILockingProvider $lockingProvider
83
	 */
84 4
	public function __construct(ImageMapper      $imageMapper,
85
	                            FileService      $fileService,
86
	                            SettingsService  $settingsService,
87
	                            ModelManager     $modelManager,
88
	                            ILockingProvider $lockingProvider)
89
	{
90 4
		parent::__construct();
91
92 4
		$this->imageMapper        = $imageMapper;
93 4
		$this->fileService        = $fileService;
94 4
		$this->settingsService    = $settingsService;
95 4
		$this->modelManager       = $modelManager;
96 4
		$this->lockingProvider    = $lockingProvider;
97
98 4
		$this->model              = null;
99 4
		$this->maxImageAreaCached = null;
100
	}
101
102
	/**
103
	 * @inheritdoc
104
	 */
105 4
	public function description() {
106 4
		return "Process all images to extract faces";
107
	}
108
109
	/**
110
	 * @inheritdoc
111
	 */
112 4
	public function execute(FaceRecognitionContext $context) {
113 4
		$this->setContext($context);
114
115 4
		$this->logInfo('NOTE: Starting face recognition. If you experience random crashes after this point, please look FAQ at https://github.com/matiasdelellis/facerecognition/wiki/FAQ');
116
117
		// Get current model.
118 4
		$this->model = $this->modelManager->getCurrentModel();
119
120
		// Open model.
121 4
		$this->model->open();
122
123 4
		$images = $context->propertyBag['images'];
124 4
		foreach($images as $image) {
125 4
			yield;
126
127 4
			$startMillis = round(microtime(true) * 1000);
128
129
			try {
130
				// Get a image lock
131 4
				$lockKey = 'facerecognition/' . $image->getId();
132 4
				$lockType = ILockingProvider::LOCK_EXCLUSIVE;
133 4
				$this->lockingProvider->acquireLock($lockKey, $lockType);
134
135 4
				$dbImage = $this->imageMapper->find($image->getUser(), $image->getId());
136 4
				if ($dbImage->getIsProcessed()) {
137
					$this->logInfo('Faces found: 0. Image will be skipped since it was already processed.');
138
					// Release lock of file.
139
					$this->lockingProvider->releaseLock($lockKey, $lockType);
140
					continue;
141
				}
142
143
144
				// Get an temp Image to process this image.
145 4
				$tempImage = $this->getTempImage($image);
146
147 3
				if (is_null($tempImage)) {
148
					// If we cannot find a file probably it was deleted out of our control and we must clean our tables.
149
					$this->settingsService->setNeedRemoveStaleImages(true, $image->user);
150
					$this->logInfo('File with ID ' . $image->file . ' doesn\'t exist anymore, skipping it');
151
					// Release lock of file.
152
					$this->lockingProvider->releaseLock($lockKey, $lockType);
153
					continue;
154
				}
155
156 3
				if ($tempImage->getSkipped() === true) {
157 1
					$this->logInfo('Faces found: 0 (image will be skipped because it is too small)');
158 1
					$this->imageMapper->imageProcessed($image, array(), 0);
159
					// Release lock of file.
160 1
					$this->lockingProvider->releaseLock($lockKey, $lockType);
161 1
					continue;
162
				}
163
164
				// Get faces in the temporary image
165 2
				$tempImagePath = $tempImage->getTempPath();
166 2
				$rawFaces = $this->model->detectFaces($tempImagePath);
167
168 2
				$this->logInfo('Faces found: ' . count($rawFaces));
169
170 2
				$faces = array();
171 2
				foreach ($rawFaces as $rawFace) {
172
					// Normalize face and landmarks from model to original size
173 1
					$normFace = $this->getNormalizedFace($rawFace, $tempImage->getRatio());
174
					// Convert from dictionary of face to our Face Db Entity.
175 1
					$face = Face::fromModel($image->getId(), $normFace);
176
					// Save the normalized Face to insert on database later.
177 1
					$faces[] = $face;
178
				}
179
180
				// Save new faces fo database
181 2
				$endMillis = round(microtime(true) * 1000);
182 2
				$duration = (int) max($endMillis - $startMillis, 0);
183 2
				$this->imageMapper->imageProcessed($image, $faces, $duration);
184
185
				// Release lock of file.
186 2
				$this->lockingProvider->releaseLock($lockKey, $lockType);
187 1
			} catch (\OCP\Lock\LockedException $e) {
188
				$this->logInfo('Faces found: 0. Image will be skipped because it is locked');
189 1
			} catch (\Exception $e) {
190 1
				if ($e->getMessage() === "std::bad_alloc") {
191
					throw new \RuntimeException("Not enough memory to run face recognition! Please look FAQ at https://github.com/matiasdelellis/facerecognition/wiki/FAQ");
192
				}
193 1
				$this->logInfo('Faces found: 0. Image will be skipped because of the following error: ' . $e->getMessage());
194 1
				$this->logDebug((string) $e);
195
196
				// Save an empty entry so it can be analyzed again later
197 1
				$this->imageMapper->imageProcessed($image, array(), 0, $e);
198
			} finally {
199
				// Clean temporary image.
200 4
				if (isset($tempImage)) {
201 3
					$tempImage->clean();
202
				}
203
				// If there are temporary files from external files, they must also be cleaned.
204 4
				$this->fileService->clean();
205
			}
206
		}
207
208 4
		return true;
209
	}
210
211
	/**
212
	 * Given an image, build a temporary image to perform the analysis
213
	 *
214
	 * return TempImage|null
215
	 */
216 4
	private function getTempImage(Image $image): ?TempImage {
217
		// todo: check if this hits I/O (database, disk...), consider having lazy caching to return user folder from user
218 4
		$file = $this->fileService->getFileById($image->getFile(), $image->getUser());
219 4
		if (empty($file)) {
220
			return null;
221
		}
222
223 4
		if (!$this->fileService->isAllowedNode($file)) {
224
			return null;
225
		}
226
227 4
		$imagePath = $this->fileService->getLocalFile($file);
228 4
		if ($imagePath === null)
229
			return null;
230
231 4
		$this->logInfo('Processing image ' . $imagePath);
232
233 4
		$tempImage = new TempImage($imagePath,
234 4
		                           $this->model->getPreferredMimeType(),
235 4
		                           $this->getMaxImageArea(),
236 4
		                           $this->settingsService->getMinimumImageSize());
237
238 3
		return $tempImage;
239
	}
240
241
	/**
242
	 * Obtains max image area lazily (from cache, or calculates it and puts it to cache)
243
	 *
244
	 * @return int Max image area (in pixels^2)
245
	 */
246 4
	private function getMaxImageArea(): int {
247
		// First check if is cached
248
		//
249 4
		if (!is_null($this->maxImageAreaCached)) {
250
			return $this->maxImageAreaCached;
251
		}
252
253
		// Get this setting on main app_config.
254
		// Note that this option has lower and upper limits and validations
255 4
		$this->maxImageAreaCached = $this->settingsService->getAnalysisImageArea();
256
257
		// Check if admin override it in config and it is valid value
258
		//
259 4
		$maxImageArea = $this->settingsService->getMaximumImageArea();
260 4
		if ($maxImageArea > 0) {
261 4
			$this->maxImageAreaCached = $maxImageArea;
262
		}
263
		// Also check if we are provided value from command line.
264
		//
265 4
		if ((array_key_exists('max_image_area', $this->context->propertyBag)) &&
266 4
		    (!is_null($this->context->propertyBag['max_image_area']))) {
267
			$this->maxImageAreaCached = $this->context->propertyBag['max_image_area'];
268
		}
269
270 4
		return $this->maxImageAreaCached;
271
	}
272
273
	/**
274
	 * Helper method, to normalize face sizes back to original dimensions, based on ratio
275
	 *
276
	 */
277 1
	private function getNormalizedFace(array $rawFace, float $ratio): array {
278 1
		$face = [];
279 1
		$face['left'] = intval(round($rawFace['left']*$ratio));
280 1
		$face['right'] = intval(round($rawFace['right']*$ratio));
281 1
		$face['top'] = intval(round($rawFace['top']*$ratio));
282 1
		$face['bottom'] = intval(round($rawFace['bottom']*$ratio));
283 1
		$face['detection_confidence'] = $rawFace['detection_confidence'];
284 1
		$face['landmarks'] = $this->getNormalizedLandmarks($rawFace['landmarks'], $ratio);
285 1
		$face['descriptor'] = $rawFace['descriptor'];
286 1
		return $face;
287
	}
288
289
	/**
290
	 * Helper method, to normalize landmarks sizes back to original dimensions, based on ratio
291
	 *
292
	 */
293 1
	private function getNormalizedLandmarks(array $rawLandmarks, float $ratio): array {
294 1
		$landmarks = [];
295 1
		foreach ($rawLandmarks as $rawLandmark) {
296 1
			$landmark = [];
297 1
			$landmark['x'] = intval(round($rawLandmark['x']*$ratio));
298 1
			$landmark['y'] = intval(round($rawLandmark['y']*$ratio));
299 1
			$landmarks[] = $landmark;
300
		}
301 1
		return $landmarks;
302
	}
303
304
}