Passed
Push — dependabot/npm_and_yarn/ellipt... ( eb0fee )
by
unknown
24:49
created

ImageProcessingTask::getTempImage()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.128

Importance

Changes 0
Metric Value
cc 4
eloc 14
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 23
ccs 12
cts 15
cp 0.8
crap 4.128
rs 9.7998
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 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 = 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($e);
164 1
				$this->imageMapper->imageProcessed($image, array(), 0, $e);
165 3
			} 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
}