nextcloud-custom-apps-face-.../facerecognition/lib/Command/MigrateCommand.php
2024-09-03 09:12:12 +05:00

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;
}
}