* @copyright Copyright (c) 2019, Branko Kokanovic * * @author Branko Kokanovic * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ namespace OCA\FaceRecognition\Command; use OCP\IUserManager; use OCP\IUser; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Helper\ProgressBar; use OCA\FaceRecognition\Db\Face; use OCA\FaceRecognition\Db\FaceMapper; use OCA\FaceRecognition\Db\Image; use OCA\FaceRecognition\Db\ImageMapper; use OCA\FaceRecognition\Model\IModel; use OCA\FaceRecognition\Model\ModelManager; use OCA\FaceRecognition\Service\FaceManagementService; use OCA\FaceRecognition\Service\FileService; use OCA\FaceRecognition\Helper\CommandLock; use OCP\Image as OCP_Image; class MigrateCommand extends Command { /** @var FaceManagementService */ protected $faceManagementService; /** @var FileService */ protected $fileService; /** @var IUserManager */ protected $userManager; /** @var ModelManager */ protected $modelManager; /** @var FaceMapper */ protected $faceMapper; /** @var ImageMapper Image mapper*/ protected $imageMapper; /** @var OutputInterface $output */ protected $output; /** * @param FaceManagementService $faceManagementService * @param IUserManager $userManager */ public function __construct(FaceManagementService $faceManagementService, FileService $fileService, IUserManager $userManager, ModelManager $modelManager, FaceMapper $faceMapper, ImageMapper $imageMapper) { parent::__construct(); $this->faceManagementService = $faceManagementService; $this->fileService = $fileService; $this->userManager = $userManager; $this->modelManager = $modelManager; $this->faceMapper = $faceMapper; $this->imageMapper = $imageMapper; } /** * @return void */ protected function configure() { $this ->setName('face:migrate') ->setDescription( 'Migrate the faces found in a model and analyze with the current model.') ->addOption( 'model_id', 'm', InputOption::VALUE_REQUIRED, 'The identifier number of the model to migrate', null ) ->addOption( 'user_id', 'u', InputOption::VALUE_REQUIRED, 'Migrate data for a given user only. If not given, migrate everything for all users.', null ); } /** * @param InputInterface $input * @param OutputInterface $output * * @return int * */ protected function execute(InputInterface $input, OutputInterface $output): int { $this->output = $output; /** * Check the considerations of the models to migrate. */ $modelId = $input->getOption('model_id'); if (is_null($modelId)) { $output->writeln("You must indicate the ID of the model to migrate"); return 1; } $model = $this->modelManager->getModel($modelId); if (is_null($model)) { $output->writeln("Invalid model Id"); return 1; } if (!$model->isInstalled()) { $output->writeln("The model <$modelId> is not installed"); return 1; } $currentModel = $this->modelManager->getCurrentModel(); $currentModelId = (!is_null($currentModel)) ? $currentModel->getId() : -1; if ($currentModelId === $modelId) { $output->writeln("The proposed model <$modelId> to migrate must be other than the current one <$currentModelId>"); return 1; } /** * Check the user if it is provided. */ $userId = $input->getOption('user_id'); if ($userId !== null) { $user = $this->userManager->get($userId); if ($user === null) { $output->writeln("User with id <$userId> is unknown."); return 1; } } /** * Get user to migrate */ $userIds = $this->getEligiblesUserId($userId); /** * Check that any user have data in the current model */ foreach ($userIds as $mUserId) { if ($this->faceManagementService->hasDataForUser($mUserId, $currentModelId)) { $output->writeln("The user <$mUserId> in current model <$currentModelId> already has data. You cannot migrate to a used model."); return 1; } } // Get lock to avoid potential errors. // $lock = CommandLock::Lock("face:migrate"); if (!$lock) { $output->writeln("Another command ('". CommandLock::IsLockedBy(). "') is already running that prevents it from continuing."); return 1; } /** * Open the model and migrate to users. */ $currentModel->open(); foreach ($userIds as $mUserId) { $this->migrateUser($currentModel, $modelId, $mUserId); } $output->writeln("The faces migration is done. Remember that you must recreate the clusters with the background_job command"); // Release obtained lock // CommandLock::Unlock($lock); return 0; } /** * @return void */ private function migrateUser(IModel $currentModel, int $oldModelId, $userId) { if (!$this->faceManagementService->hasDataForUser($userId, $oldModelId)) { $this->output->writeln("User <$userId> has no data in model <$oldModelId> to migrate."); return; } $this->output->writeln("Will be migrated <$userId> from model <$oldModelId>"); $currentModelId = $currentModel->getId(); $oldImages = $this->imageMapper->findAll($userId, $oldModelId); $progressBar = new ProgressBar($this->output, count($oldImages)); $progressBar->start(); foreach ($oldImages as $oldImage) { $newImage = $this->migrateImage($oldImage, $userId, $currentModelId); $oldFaces = $this->faceMapper->findFromFile($userId, $oldModelId, $newImage->getFile()); if (count($oldFaces) > 0) { $filePath = $this->getImageFilePath($newImage); if ($filePath === null) continue; foreach ($oldFaces as $oldFace) { $this->migrateFace($currentModel, $oldFace, $newImage, $filePath); } $this->fileService->clean(); } $progressBar->advance(1); } $progressBar->finish(); $this->output->writeln("Done"); } private function migrateImage($oldImage, string $userId, int $modelId): Image { $image = new Image(); $image->setUser($userId); $image->setFile($oldImage->getFile()); $image->setModel($modelId); $image->setIsProcessed($oldImage->getIsProcessed()); $image->setError($oldImage->getError()); $image->setLastProcessedTime($oldImage->getLastProcessedTime()); $image->setProcessingDuration($oldImage->getProcessingDuration()); return $this->imageMapper->insert($image); } /** * @param string $filePath */ private function migrateFace(IModel $model, Face $oldFace, Image $image, string $filePath): void { // Get the rectangle and the confidence of the original face. $faceRect = $this->getFaceRect($oldFace); // Get the landmarks and descriptor with the new model. $faceAssoc = $model->compute($filePath, $faceRect); // Convert the assoc array into an Face object and insert on database. $face = Face::fromModel($image->getId(), $faceAssoc); $this->faceMapper->insertFace($face); } private function getImageFilePath(Image $image): ?string { $file = $this->fileService->getFileById($image->getFile(), $image->getUser()); if (empty($file)) { return null; } $localPath = $this->fileService->getLocalFile($file); $image = new OCP_Image(); $image->loadFromFile($localPath); if ($image->getOrientation() > 1) { $tempPath = $this->fileService->getTemporaryFile(); $image->fixOrientation(); $image->save($tempPath, 'image/png'); return $tempPath; } return $localPath; } private function getFaceRect(Face $face): array { $rect = []; $rect['left'] = (int)$face->getX(); $rect['right'] = (int)$face->getX() + $face->getWidth(); $rect['top'] = (int)$face->getY(); $rect['bottom'] = (int)$face->getY() + $face->getHeight(); $rect['detection_confidence'] = $face->getConfidence(); return $rect; } /** * Get an array with the eligibles users taking into account the user argument, * or all users. */ private function getEligiblesUserId(string $userId = null): array { $eligible_users = array(); if (is_null($userId)) { $this->userManager->callForAllUsers(function (IUser $user) use (&$eligible_users) { $eligible_users[] = $user->getUID(); }); } else { $eligible_users[] = $userId; } return $eligible_users; } }