Skip to content

Commit c04f225

Browse files
committed
fix: Add RemoveDuplicateFaceDetections repair step
Signed-off-by: Marcel Klehr <mklehr@gmx.net>
1 parent 1975dc6 commit c04f225

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

lib/Db/FaceDetectionMapper.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use OCP\AppFramework\Db\DoesNotExistException;
1212
use OCP\AppFramework\Db\Entity;
1313
use OCP\AppFramework\Db\QBMapper;
14+
use OCP\DB\Exception;
1415
use OCP\DB\QueryBuilder\IQueryBuilder;
1516
use OCP\IConfig;
1617
use OCP\IDBConnection;
@@ -64,6 +65,13 @@ public function insert(Entity $entity): FaceDetection {
6465
return $duplicates[0];
6566
}
6667

68+
/**
69+
* @throws Exception
70+
*/
71+
public function insertWithoutDeduplication(Entity $entity): FaceDetection {
72+
return parent::insert($entity);
73+
}
74+
6775
/**
6876
* @throws \OCP\DB\Exception
6977
*/
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* @copyright Copyright (c) 2025, Marcel Klehr <mklehr@gmx.net>
6+
*
7+
* @author Marcel Klehr <mklehr@gmx.net>
8+
*
9+
* @license GNU AGPL version 3 or any later version
10+
*
11+
* This program is free software: you can redistribute it and/or modify
12+
* it under the terms of the GNU Affero General Public License as
13+
* published by the Free Software Foundation, either version 3 of the
14+
* License, or (at your option) any later version.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU Affero General Public License for more details.
20+
*
21+
* You should have received a copy of the GNU Affero General Public License
22+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
23+
*
24+
*/
25+
namespace OCA\Recognize\Migration;
26+
27+
use OCP\IDBConnection;
28+
use OCP\Migration\IOutput;
29+
use OCP\Migration\IRepairStep;
30+
use Psr\Log\LoggerInterface;
31+
32+
final class RemoveDuplicateFaceDetections implements IRepairStep {
33+
34+
public function __construct(
35+
private IDBConnection $db,
36+
private LoggerInterface $logger,
37+
) {
38+
}
39+
40+
public function getName(): string {
41+
return 'Remove duplicate face detections';
42+
}
43+
44+
public function run(IOutput $output): void {
45+
try {
46+
$subQuery = $this->db->getQueryBuilder();
47+
$subQuery->select($subQuery->func()->min('id'))
48+
->from('recognize_face_detections')
49+
->groupBy('file_id', 'user_id', 'x', 'y', 'height', 'width');
50+
51+
$qb = $this->db->getQueryBuilder();
52+
$qb->delete('recognize_face_detections')
53+
->where($qb->expr()->notIn('id', $qb->createFunction('(' . $subQuery->getSQL() .')')));
54+
55+
$qb->executeStatement();
56+
} catch (\Throwable $e) {
57+
$output->warning('Failed to automatically remove duplicate face detections for recognize.');
58+
$this->logger->error('Failed to automatically remove duplicate face detections', ['exception' => $e]);
59+
}
60+
}
61+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
use OCA\Recognize\Db\FaceDetectionMapper;
4+
use OCA\Recognize\Migration\RemoveDuplicateFaceDetections;
5+
use OCP\IDBConnection;
6+
use OCP\Server;
7+
use Test\TestCase;
8+
9+
/**
10+
* @group DB
11+
*/
12+
class RemoveDuplicateFaceDetectionsTest extends TestCase {
13+
private IDBConnection $db;
14+
private FaceDetectionMapper $faceDetectionMapper;
15+
16+
public function setUp(): void {
17+
parent::setUp();
18+
$this->db = Server::get(IDBConnection::class);
19+
$this->faceDetectionMapper = Server::get(FaceDetectionMapper::class);
20+
21+
// Generate 11 face detections per file (1000 files per user; 100 users)
22+
// = 1.100.000 face detections out of which 900.000 are superfluous duplicates to be removed
23+
for ($k = 0; $k < 100; $k++) {
24+
for ($j = 0; $j < 1000; $j++) {
25+
$user = 'user' . $k;
26+
$x = rand(0, 100) / 100;
27+
$y = rand(0, 100) / 100;
28+
$height = rand(0, 100) / 100;
29+
$width = rand(0, 100) / 100;
30+
for ($i = 0; $i < 10; $i++) {
31+
$face = new \OCA\Recognize\Db\FaceDetection();
32+
$face->setUserId($user);
33+
$face->setX($x);
34+
$face->setY($y);
35+
$face->setHeight($height);
36+
$face->setWidth($width);
37+
$face->setFileId($j);
38+
$face->setThreshold(0.5);
39+
$face->setVector([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]);
40+
$this->faceDetectionMapper->insertWithoutDeduplication($face);
41+
}
42+
$face2 = new \OCA\Recognize\Db\FaceDetection();
43+
$face2->setUserId($user);
44+
$face2->setX(rand(0, 100) / 100);
45+
$face2->setY(rand(0, 100) / 100);
46+
$face2->setHeight(rand(0, 100) / 100);
47+
$face2->setWidth(rand(0, 100) / 100);
48+
$face2->setFileId($j);
49+
$face2->setThreshold(0.5);
50+
$face2->setVector([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]);
51+
$this->faceDetectionMapper->insertWithoutDeduplication($face2);
52+
}
53+
}
54+
}
55+
56+
public function testRepairStep() : void {
57+
$repairStep = Server::get(RemoveDuplicateFaceDetections::class);
58+
$output = $this->createMock(\OCP\Migration\IOutput::class);
59+
$repairStep->run($output);
60+
$qb = $this->db->getQueryBuilder();
61+
$count = $qb->select($qb->func()->count('*'))->from('recognize_face_detections')->executeQuery()->fetchOne();
62+
$this->assertEquals(900000, $count);
63+
}
64+
}

0 commit comments

Comments
 (0)