Passed
Pull Request — master (#269)
by Matias
06:58 queued 05:22
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
					// 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
		if (!$this->fileService->isAllowedNode($file)) {
200
			return null;
201
		}
202
203 4
		$imagePath = $this->fileService->getLocalFile($file);
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
	/**
262
	 * Helper method, to normalize landmarks sizes back to original dimensions, based on ratio
263
	 *
264
	 */
265 1
	private function getNormalizedLandmarks(array $rawLandmarks, float $ratio): array {
266 1
		$landmarks = [];
267 1
		foreach ($rawLandmarks as $rawLandmark) {
268 1
			$landmark = [];
269 1
			$landmark['x'] = intval(round($rawLandmark['x']*$ratio));
270 1
			$landmark['y'] = intval(round($rawLandmark['y']*$ratio));
271 1
			$landmarks[] = $landmark;
272
		}
273 1
		return $landmarks;
274
	}
275
276
}