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

ImageProcessingTask::execute()   D

Complexity

Conditions 10
Paths 532

Size

Total Lines 97
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 10.5186

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 10
eloc 53
c 2
b 0
f 0
nc 532
nop 1
dl 0
loc 97
ccs 43
cts 52
cp 0.8269
crap 10.5186
rs 4.3454

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
}