Passed
Push — enconde-from-orig ( 13bb2b )
by Matias
05:13
created

ImageProcessingTask::execute()   C

Complexity

Conditions 8
Paths 317

Size

Total Lines 79
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 8.048

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 8
eloc 44
c 5
b 0
f 0
nc 317
nop 1
dl 0
loc 79
ccs 40
cts 44
cp 0.9091
crap 8.048
rs 6.1348

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
					// Normalize face and get landmarks of face from model to original size
147 1
					$normFace = $this->getNormalizedFace($rawFace, $tempImage->getRatio());
148 1
					$landmarks = $this->model->detectLandmarks($tempImage->getImagePath(), $normFace);
149
150
					// Get descriptor of face from model
151 1
					$descriptor = $this->model->computeDescriptor($tempImage->getImagePath(), $landmarks, 100);
0 ignored issues
show
Unused Code introduced by
The call to OCA\FaceRecognition\Mode...el::computeDescriptor() has too many arguments starting with 100. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

151
					/** @scrutinizer ignore-call */ 
152
     $descriptor = $this->model->computeDescriptor($tempImage->getImagePath(), $landmarks, 100);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Unused Code introduced by
The call to OCA\FaceRecognition\Mode...el::computeDescriptor() has too many arguments starting with 100. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

151
					/** @scrutinizer ignore-call */ 
152
     $descriptor = $this->model->computeDescriptor($tempImage->getImagePath(), $landmarks, 100);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

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