Browse Source

feat(web): add setting for minimum face count for face detection (#4128)

* feat: add setting for minimum face count for face detection

Adds the minimum face count setting to the web interface to
circumvent detection of strangers and random background people
if desired.

* fix: codestyle, remove max for face count
GenericGuy 1 year ago
parent
commit
94cbbf3c4b

+ 6 - 0
cli/src/api/open-api/api.ts

@@ -2152,6 +2152,12 @@ export interface RecognitionConfig {
      * @memberof RecognitionConfig
      */
     'maxDistance': number;
+    /**
+     * 
+     * @type {number}
+     * @memberof RecognitionConfig
+     */
+    'minFaces': number;
     /**
      * 
      * @type {number}

+ 2 - 1
docs/docs/install/config-file.md

@@ -70,7 +70,8 @@ The default configuration looks like this:
       "enabled": true,
       "modelName": "buffalo_l",
       "minScore": 0.7,
-      "maxDistance": 0.6
+      "maxDistance": 0.6,
+      "minFaces": 1
     }
   },
   "oauth": {

+ 1 - 0
mobile/openapi/doc/RecognitionConfig.md

@@ -10,6 +10,7 @@ Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
 **enabled** | **bool** |  | 
 **maxDistance** | **int** |  | 
+**minFaces** | **int** |  | 
 **minScore** | **int** |  | 
 **modelName** | **String** |  | 
 **modelType** | [**ModelType**](ModelType.md) |  | [optional] 

+ 9 - 1
mobile/openapi/lib/model/recognition_config.dart

@@ -15,6 +15,7 @@ class RecognitionConfig {
   RecognitionConfig({
     required this.enabled,
     required this.maxDistance,
+    required this.minFaces,
     required this.minScore,
     required this.modelName,
     this.modelType,
@@ -24,6 +25,8 @@ class RecognitionConfig {
 
   int maxDistance;
 
+  int minFaces;
+
   int minScore;
 
   String modelName;
@@ -40,6 +43,7 @@ class RecognitionConfig {
   bool operator ==(Object other) => identical(this, other) || other is RecognitionConfig &&
      other.enabled == enabled &&
      other.maxDistance == maxDistance &&
+     other.minFaces == minFaces &&
      other.minScore == minScore &&
      other.modelName == modelName &&
      other.modelType == modelType;
@@ -49,17 +53,19 @@ class RecognitionConfig {
     // ignore: unnecessary_parenthesis
     (enabled.hashCode) +
     (maxDistance.hashCode) +
+    (minFaces.hashCode) +
     (minScore.hashCode) +
     (modelName.hashCode) +
     (modelType == null ? 0 : modelType!.hashCode);
 
   @override
-  String toString() => 'RecognitionConfig[enabled=$enabled, maxDistance=$maxDistance, minScore=$minScore, modelName=$modelName, modelType=$modelType]';
+  String toString() => 'RecognitionConfig[enabled=$enabled, maxDistance=$maxDistance, minFaces=$minFaces, minScore=$minScore, modelName=$modelName, modelType=$modelType]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
       json[r'enabled'] = this.enabled;
       json[r'maxDistance'] = this.maxDistance;
+      json[r'minFaces'] = this.minFaces;
       json[r'minScore'] = this.minScore;
       json[r'modelName'] = this.modelName;
     if (this.modelType != null) {
@@ -80,6 +86,7 @@ class RecognitionConfig {
       return RecognitionConfig(
         enabled: mapValueOfType<bool>(json, r'enabled')!,
         maxDistance: mapValueOfType<int>(json, r'maxDistance')!,
+        minFaces: mapValueOfType<int>(json, r'minFaces')!,
         minScore: mapValueOfType<int>(json, r'minScore')!,
         modelName: mapValueOfType<String>(json, r'modelName')!,
         modelType: ModelType.fromJson(json[r'modelType']),
@@ -132,6 +139,7 @@ class RecognitionConfig {
   static const requiredKeys = <String>{
     'enabled',
     'maxDistance',
+    'minFaces',
     'minScore',
     'modelName',
   };

+ 5 - 0
mobile/openapi/test/recognition_config_test.dart

@@ -26,6 +26,11 @@ void main() {
       // TODO
     });
 
+    // int minFaces
+    test('to test the property `minFaces`', () async {
+      // TODO
+    });
+
     // int minScore
     test('to test the property `minScore`', () async {
       // TODO

+ 4 - 0
server/immich-openapi-specs.json

@@ -6471,6 +6471,9 @@
           "maxDistance": {
             "type": "integer"
           },
+          "minFaces": {
+            "type": "integer"
+          },
           "minScore": {
             "type": "integer"
           },
@@ -6484,6 +6487,7 @@
         "required": [
           "minScore",
           "maxDistance",
+          "minFaces",
           "enabled",
           "modelName"
         ],

+ 1 - 0
server/src/domain/facial-recognition/facial-recognition.service.spec.ts

@@ -205,6 +205,7 @@ describe(FacialRecognitionService.name, () => {
           enabled: true,
           maxDistance: 0.6,
           minScore: 0.7,
+          minFaces: 1,
           modelName: 'buffalo_l',
         },
       );

+ 5 - 1
server/src/domain/person/person.service.spec.ts

@@ -6,11 +6,13 @@ import {
   newJobRepositoryMock,
   newPersonRepositoryMock,
   newStorageRepositoryMock,
+  newSystemConfigRepositoryMock,
   personStub,
 } from '@test';
 import { BulkIdErrorReason } from '../asset';
 import { IJobRepository, JobName } from '../job';
 import { IStorageRepository } from '../storage';
+import { ISystemConfigRepository } from '../system-config';
 import { PersonResponseDto } from './person.dto';
 import { IPersonRepository } from './person.repository';
 import { PersonService } from './person.service';
@@ -26,14 +28,16 @@ const responseDto: PersonResponseDto = {
 describe(PersonService.name, () => {
   let sut: PersonService;
   let personMock: jest.Mocked<IPersonRepository>;
+  let configMock: jest.Mocked<ISystemConfigRepository>;
   let storageMock: jest.Mocked<IStorageRepository>;
   let jobMock: jest.Mocked<IJobRepository>;
 
   beforeEach(async () => {
     personMock = newPersonRepositoryMock();
     storageMock = newStorageRepositoryMock();
+    configMock = newSystemConfigRepositoryMock();
     jobMock = newJobRepositoryMock();
-    sut = new PersonService(personMock, storageMock, jobMock);
+    sut = new PersonService(personMock, configMock, storageMock, jobMock);
   });
 
   it('should be defined', () => {

+ 8 - 2
server/src/domain/person/person.service.ts

@@ -4,6 +4,7 @@ import { AuthUserDto } from '../auth';
 import { mimeTypes } from '../domain.constant';
 import { IJobRepository, JobName } from '../job';
 import { IStorageRepository, ImmichReadStream } from '../storage';
+import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
 import {
   MergePersonDto,
   PeopleResponseDto,
@@ -17,17 +18,22 @@ import { IPersonRepository, UpdateFacesData } from './person.repository';
 
 @Injectable()
 export class PersonService {
+  private configCore: SystemConfigCore;
   readonly logger = new Logger(PersonService.name);
 
   constructor(
     @Inject(IPersonRepository) private repository: IPersonRepository,
+    @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
     @Inject(IStorageRepository) private storageRepository: IStorageRepository,
     @Inject(IJobRepository) private jobRepository: IJobRepository,
-  ) {}
+  ) {
+    this.configCore = new SystemConfigCore(configRepository);
+  }
 
   async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
+    const { machineLearning } = await this.configCore.getConfig();
     const people = await this.repository.getAllForUser(authUser.id, {
-      minimumFaceCount: 1,
+      minimumFaceCount: machineLearning.facialRecognition.minFaces,
       withHidden: dto.withHidden || false,
     });
     const persons: PersonResponseDto[] = people

+ 6 - 0
server/src/domain/smart-info/dto/model-config.dto.ts

@@ -48,4 +48,10 @@ export class RecognitionConfig extends ModelConfig {
   @Type(() => Number)
   @ApiProperty({ type: 'integer' })
   maxDistance!: number;
+
+  @IsNumber()
+  @Min(1)
+  @Type(() => Number)
+  @ApiProperty({ type: 'integer' })
+  minFaces!: number;
 }

+ 1 - 0
server/src/domain/system-config/system-config.core.ts

@@ -72,6 +72,7 @@ export const defaults = Object.freeze<SystemConfig>({
       modelName: 'buffalo_l',
       minScore: 0.7,
       maxDistance: 0.6,
+      minFaces: 1,
     },
   },
   map: {

+ 1 - 0
server/src/domain/system-config/system-config.service.spec.ts

@@ -71,6 +71,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
       modelName: 'buffalo_l',
       minScore: 0.7,
       maxDistance: 0.6,
+      minFaces: 1,
     },
   },
   map: {

+ 2 - 0
server/src/infra/entities/system-config.entity.ts

@@ -57,6 +57,7 @@ export enum SystemConfigKey {
   MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL_NAME = 'machineLearning.facialRecognition.modelName',
   MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_SCORE = 'machineLearning.facialRecognition.minScore',
   MACHINE_LEARNING_FACIAL_RECOGNITION_MAX_DISTANCE = 'machineLearning.facialRecognition.maxDistance',
+  MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES = 'machineLearning.facialRecognition.minFaces',
 
   MAP_ENABLED = 'map.enabled',
   MAP_TILE_URL = 'map.tileUrl',
@@ -164,6 +165,7 @@ export interface SystemConfig {
       enabled: boolean;
       modelName: string;
       minScore: number;
+      minFaces: number;
       maxDistance: number;
     };
   };

+ 6 - 0
web/src/api/open-api/api.ts

@@ -2152,6 +2152,12 @@ export interface RecognitionConfig {
      * @memberof RecognitionConfig
      */
     'maxDistance': number;
+    /**
+     * 
+     * @type {number}
+     * @memberof RecognitionConfig
+     */
+    'minFaces': number;
     /**
      * 
      * @type {number}

+ 11 - 0
web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte

@@ -196,6 +196,17 @@
               isEdited={machineLearningConfig.facialRecognition.maxDistance !==
                 savedConfig.facialRecognition.maxDistance}
             />
+
+            <SettingInputField
+              inputType={SettingInputFieldType.NUMBER}
+              label="MIN FACES DETECTED"
+              desc="The minimum number of faces of a person that must be detected for them to appear in the People tab. Setting this to a value greater than 1 can prevent strangers or blurry faces that are not the main subject of the image from being displayed."
+              bind:value={machineLearningConfig.facialRecognition.minFaces}
+              step="1"
+              min="1"
+              disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.facialRecognition.enabled}
+              isEdited={machineLearningConfig.facialRecognition.minFaces !== savedConfig.facialRecognition.minFaces}
+            />
           </div>
         </SettingAccordion>