Passed
Push — master ( 132edd...186cf7 )
by Matias
21:30 queued 04:34
created

ImageProcessingTask::execute()   C

Complexity

Conditions 8
Paths 317

Size

Total Lines 81
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 8.0449

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 8
eloc 45
c 5
b 0
f 0
nc 317
nop 1
dl 0
loc 81
ccs 41
cts 45
cp 0.9111
crap 8.0449
rs 6.1188

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 */
64
	protected $modelManager;
65
66
	/** @var IModel */
67
	private $model;
68
69
	/** @var int|null 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 4
	}
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
					// Get landmarks of face from model
147 1
					$rawLandmarks = $this->model->detectLandmarks($tempImage->getTempPath(), $rawFace);
148
					// Get descriptor of face from model
149 1
					$descriptor = $this->model->computeDescriptor($tempImage->getTempPath(), $rawLandmarks);
150
151
					// Normalize face and landmarks from model to original size
152 1
					$normFace = $this->getNormalizedFace($rawFace, $tempImage->getRatio());
153 1
					$normLandmarks = $this->getNormalizedLandmarks($rawLandmarks['parts'], $tempImage->getRatio());
154
155
					// Convert from dictionary of faces to our Face Db Entity and put Landmarks and descriptor
156 1
					$face = Face::fromModel($image->getId(), $normFace);
157 1
					$face->landmarks = $normLandmarks;
158 1
					$face->descriptor = $descriptor;
159
160 1
					$faces[] = $face;
161
				}
162
163
				// Save new faces fo database
164 2
				$endMillis = round(microtime(true) * 1000);
165 2
				$duration = max($endMillis - $startMillis, 0);
166 2
				$this->imageMapper->imageProcessed($image, $faces, $duration);
167 1
			} catch (\Exception $e) {
168 1
				if ($e->getMessage() === "std::bad_alloc") {
169
					throw new \RuntimeException("Not enough memory to run face recognition! Please look FAQ at https://github.com/matiasdelellis/facerecognition/wiki/FAQ");
170
				}
171 1
				$this->logInfo('Faces found: 0. Image will be skipped because of the following error: ' . $e->getMessage());
172 1
				$this->logDebug($e);
173 1
				$this->imageMapper->imageProcessed($image, array(), 0, $e);
174 3
			} finally {
175
				// Clean temporary image.
176 4
				if (isset($tempImage)) {
177 3
					$tempImage->clean();
178
				}
179
				// If there are temporary files from external files, they must also be cleaned.
180 4
				$this->fileService->clean();
181
			}
182
		}
183
184 4
		return true;
185
	}
186
187
	/**
188
	 * Given an image, build a temporary image to perform the analysis
189
	 *
190
	 * return TempImage|null
191
	 */
192 4
	private function getTempImage(Image $image): ?TempImage {
193
		// todo: check if this hits I/O (database, disk...), consider having lazy caching to return user folder from user
194 4
		$file = $this->fileService->getFileById($image->getFile(), $image->getUser());
195 4
		if (empty($file)) {
196
			return null;
197
		}
198
199 4
		$imagePath = $this->fileService->getLocalFile($file);
200
201 4
		$this->logInfo('Processing image ' . $imagePath);
202
203 4
		$tempImage = new TempImage($imagePath,
204 4
		                           $this->model->getPreferredMimeType(),
205 4
		                           $this->getMaxImageArea(),
206 4
		                           $this->settingsService->getMinimumImageSize());
207
208 3
		return $tempImage;
209
	}
210
211
	/**
212
	 * Obtains max image area lazily (from cache, or calculates it and puts it to cache)
213
	 *
214
	 * @return int Max image area (in pixels^2)
215
	 */
216 4
	private function getMaxImageArea(): int {
217
		// First check if is cached
218
		//
219 4
		if (!is_null($this->maxImageAreaCached)) {
220
			return $this->maxImageAreaCached;
221
		}
222
223
		// Get this setting on main app_config.
224
		// Note that this option has lower and upper limits and validations
225 4
		$this->maxImageAreaCached = $this->settingsService->getAnalysisImageArea();
226
227
		// Check if admin override it in config and it is valid value
228
		//
229 4
		$maxImageArea = $this->settingsService->getMaximumImageArea();
230 4
		if ($maxImageArea > 0) {
231 4
			$this->maxImageAreaCached = $maxImageArea;
232
		}
233
		// Also check if we are provided value from command line.
234
		//
235 4
		if ((array_key_exists('max_image_area', $this->context->propertyBag)) &&
236 4
		    (!is_null($this->context->propertyBag['max_image_area']))) {
237
			$this->maxImageAreaCached = $this->context->propertyBag['max_image_area'];
238
		}
239
240 4
		return $this->maxImageAreaCached;
241
	}
242
243
	/**
244
	 * Helper method, to normalize face sizes back to original dimensions, based on ratio
245
	 *
246
	 */
247 1
	private function getNormalizedFace(array $rawFace, float $ratio): array {
248 1
		$face = [];
249 1
		$face['left'] = intval(round(max($rawFace['left'], 0)*$ratio));
250 1
		$face['right'] = intval(round($rawFace['right']*$ratio));
251 1
		$face['top'] = intval(round(max($rawFace['top'], 0)*$ratio));
252 1
		$face['bottom'] = intval(round($rawFace['bottom']*$ratio));
253 1
		$face['detection_confidence'] = $rawFace['detection_confidence'];
254 1
		return $face;
255
	}
256
257
	/**
258
	 * Helper method, to normalize landmarks sizes back to original dimensions, based on ratio
259
	 *
260
	 */
261 1
	private function getNormalizedLandmarks(array $rawLandmarks, float $ratio): array {
262 1
		$landmarks = [];
263 1
		foreach ($rawLandmarks as $rawLandmark) {
264 1
			$landmark = [];
265 1
			$landmark['x'] = intval(round($rawLandmark['x']*$ratio));
266 1
			$landmark['y'] = intval(round($rawLandmark['y']*$ratio));
267 1
			$landmarks[] = $landmark;
268
		}
269 1
		return $landmarks;
270
	}
271
272
}