Passed
Push — dependabot/npm_and_yarn/decode... ( a4b439 )
by
unknown
11:31
created

ImageProcessingTask::execute()   C

Complexity

Conditions 8
Paths 275

Size

Total Lines 72
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 8.0691

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 8
eloc 40
c 3
b 0
f 0
nc 275
nop 1
dl 0
loc 72
ccs 35
cts 39
cp 0.8974
crap 8.0691
rs 6.6188

How to fix   Long Method   

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