325 lines
9.1 KiB
PHP
325 lines
9.1 KiB
PHP
<?php
|
|
/**
|
|
* @copyright Copyright (c) 2020, Matias De lellis <mati86dl@gmail.com>
|
|
* @copyright Copyright (c) 2019, Branko Kokanovic <branko@kokanovic.org>
|
|
*
|
|
* @author Branko Kokanovic <branko@kokanovic.org>
|
|
*
|
|
* @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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
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;
|
|
}
|
|
|
|
}
|