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

ImageProcessingTask::getNormalizedLandmarks()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 9
ccs 8
cts 8
cp 1
crap 2
rs 10
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
}