mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 20:25:32 -04:00
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
This commit is contained in:
parent
40b802a5a9
commit
94cbbf3c4b
6
cli/src/api/open-api/api.ts
generated
6
cli/src/api/open-api/api.ts
generated
@ -2152,6 +2152,12 @@ export interface RecognitionConfig {
|
|||||||
* @memberof RecognitionConfig
|
* @memberof RecognitionConfig
|
||||||
*/
|
*/
|
||||||
'maxDistance': number;
|
'maxDistance': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof RecognitionConfig
|
||||||
|
*/
|
||||||
|
'minFaces': number;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
|
@ -70,7 +70,8 @@ The default configuration looks like this:
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"modelName": "buffalo_l",
|
"modelName": "buffalo_l",
|
||||||
"minScore": 0.7,
|
"minScore": 0.7,
|
||||||
"maxDistance": 0.6
|
"maxDistance": 0.6,
|
||||||
|
"minFaces": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
|
1
mobile/openapi/doc/RecognitionConfig.md
generated
1
mobile/openapi/doc/RecognitionConfig.md
generated
@ -10,6 +10,7 @@ Name | Type | Description | Notes
|
|||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**enabled** | **bool** | |
|
**enabled** | **bool** | |
|
||||||
**maxDistance** | **int** | |
|
**maxDistance** | **int** | |
|
||||||
|
**minFaces** | **int** | |
|
||||||
**minScore** | **int** | |
|
**minScore** | **int** | |
|
||||||
**modelName** | **String** | |
|
**modelName** | **String** | |
|
||||||
**modelType** | [**ModelType**](ModelType.md) | | [optional]
|
**modelType** | [**ModelType**](ModelType.md) | | [optional]
|
||||||
|
10
mobile/openapi/lib/model/recognition_config.dart
generated
10
mobile/openapi/lib/model/recognition_config.dart
generated
@ -15,6 +15,7 @@ class RecognitionConfig {
|
|||||||
RecognitionConfig({
|
RecognitionConfig({
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
required this.maxDistance,
|
required this.maxDistance,
|
||||||
|
required this.minFaces,
|
||||||
required this.minScore,
|
required this.minScore,
|
||||||
required this.modelName,
|
required this.modelName,
|
||||||
this.modelType,
|
this.modelType,
|
||||||
@ -24,6 +25,8 @@ class RecognitionConfig {
|
|||||||
|
|
||||||
int maxDistance;
|
int maxDistance;
|
||||||
|
|
||||||
|
int minFaces;
|
||||||
|
|
||||||
int minScore;
|
int minScore;
|
||||||
|
|
||||||
String modelName;
|
String modelName;
|
||||||
@ -40,6 +43,7 @@ class RecognitionConfig {
|
|||||||
bool operator ==(Object other) => identical(this, other) || other is RecognitionConfig &&
|
bool operator ==(Object other) => identical(this, other) || other is RecognitionConfig &&
|
||||||
other.enabled == enabled &&
|
other.enabled == enabled &&
|
||||||
other.maxDistance == maxDistance &&
|
other.maxDistance == maxDistance &&
|
||||||
|
other.minFaces == minFaces &&
|
||||||
other.minScore == minScore &&
|
other.minScore == minScore &&
|
||||||
other.modelName == modelName &&
|
other.modelName == modelName &&
|
||||||
other.modelType == modelType;
|
other.modelType == modelType;
|
||||||
@ -49,17 +53,19 @@ class RecognitionConfig {
|
|||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(enabled.hashCode) +
|
(enabled.hashCode) +
|
||||||
(maxDistance.hashCode) +
|
(maxDistance.hashCode) +
|
||||||
|
(minFaces.hashCode) +
|
||||||
(minScore.hashCode) +
|
(minScore.hashCode) +
|
||||||
(modelName.hashCode) +
|
(modelName.hashCode) +
|
||||||
(modelType == null ? 0 : modelType!.hashCode);
|
(modelType == null ? 0 : modelType!.hashCode);
|
||||||
|
|
||||||
@override
|
@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() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'enabled'] = this.enabled;
|
json[r'enabled'] = this.enabled;
|
||||||
json[r'maxDistance'] = this.maxDistance;
|
json[r'maxDistance'] = this.maxDistance;
|
||||||
|
json[r'minFaces'] = this.minFaces;
|
||||||
json[r'minScore'] = this.minScore;
|
json[r'minScore'] = this.minScore;
|
||||||
json[r'modelName'] = this.modelName;
|
json[r'modelName'] = this.modelName;
|
||||||
if (this.modelType != null) {
|
if (this.modelType != null) {
|
||||||
@ -80,6 +86,7 @@ class RecognitionConfig {
|
|||||||
return RecognitionConfig(
|
return RecognitionConfig(
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
maxDistance: mapValueOfType<int>(json, r'maxDistance')!,
|
maxDistance: mapValueOfType<int>(json, r'maxDistance')!,
|
||||||
|
minFaces: mapValueOfType<int>(json, r'minFaces')!,
|
||||||
minScore: mapValueOfType<int>(json, r'minScore')!,
|
minScore: mapValueOfType<int>(json, r'minScore')!,
|
||||||
modelName: mapValueOfType<String>(json, r'modelName')!,
|
modelName: mapValueOfType<String>(json, r'modelName')!,
|
||||||
modelType: ModelType.fromJson(json[r'modelType']),
|
modelType: ModelType.fromJson(json[r'modelType']),
|
||||||
@ -132,6 +139,7 @@ class RecognitionConfig {
|
|||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'enabled',
|
'enabled',
|
||||||
'maxDistance',
|
'maxDistance',
|
||||||
|
'minFaces',
|
||||||
'minScore',
|
'minScore',
|
||||||
'modelName',
|
'modelName',
|
||||||
};
|
};
|
||||||
|
5
mobile/openapi/test/recognition_config_test.dart
generated
5
mobile/openapi/test/recognition_config_test.dart
generated
@ -26,6 +26,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// int minFaces
|
||||||
|
test('to test the property `minFaces`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// int minScore
|
// int minScore
|
||||||
test('to test the property `minScore`', () async {
|
test('to test the property `minScore`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -6471,6 +6471,9 @@
|
|||||||
"maxDistance": {
|
"maxDistance": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"minFaces": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"minScore": {
|
"minScore": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
@ -6484,6 +6487,7 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"minScore",
|
"minScore",
|
||||||
"maxDistance",
|
"maxDistance",
|
||||||
|
"minFaces",
|
||||||
"enabled",
|
"enabled",
|
||||||
"modelName"
|
"modelName"
|
||||||
],
|
],
|
||||||
|
@ -205,6 +205,7 @@ describe(FacialRecognitionService.name, () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
maxDistance: 0.6,
|
maxDistance: 0.6,
|
||||||
minScore: 0.7,
|
minScore: 0.7,
|
||||||
|
minFaces: 1,
|
||||||
modelName: 'buffalo_l',
|
modelName: 'buffalo_l',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -6,11 +6,13 @@ import {
|
|||||||
newJobRepositoryMock,
|
newJobRepositoryMock,
|
||||||
newPersonRepositoryMock,
|
newPersonRepositoryMock,
|
||||||
newStorageRepositoryMock,
|
newStorageRepositoryMock,
|
||||||
|
newSystemConfigRepositoryMock,
|
||||||
personStub,
|
personStub,
|
||||||
} from '@test';
|
} from '@test';
|
||||||
import { BulkIdErrorReason } from '../asset';
|
import { BulkIdErrorReason } from '../asset';
|
||||||
import { IJobRepository, JobName } from '../job';
|
import { IJobRepository, JobName } from '../job';
|
||||||
import { IStorageRepository } from '../storage';
|
import { IStorageRepository } from '../storage';
|
||||||
|
import { ISystemConfigRepository } from '../system-config';
|
||||||
import { PersonResponseDto } from './person.dto';
|
import { PersonResponseDto } from './person.dto';
|
||||||
import { IPersonRepository } from './person.repository';
|
import { IPersonRepository } from './person.repository';
|
||||||
import { PersonService } from './person.service';
|
import { PersonService } from './person.service';
|
||||||
@ -26,14 +28,16 @@ const responseDto: PersonResponseDto = {
|
|||||||
describe(PersonService.name, () => {
|
describe(PersonService.name, () => {
|
||||||
let sut: PersonService;
|
let sut: PersonService;
|
||||||
let personMock: jest.Mocked<IPersonRepository>;
|
let personMock: jest.Mocked<IPersonRepository>;
|
||||||
|
let configMock: jest.Mocked<ISystemConfigRepository>;
|
||||||
let storageMock: jest.Mocked<IStorageRepository>;
|
let storageMock: jest.Mocked<IStorageRepository>;
|
||||||
let jobMock: jest.Mocked<IJobRepository>;
|
let jobMock: jest.Mocked<IJobRepository>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
personMock = newPersonRepositoryMock();
|
personMock = newPersonRepositoryMock();
|
||||||
storageMock = newStorageRepositoryMock();
|
storageMock = newStorageRepositoryMock();
|
||||||
|
configMock = newSystemConfigRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
sut = new PersonService(personMock, storageMock, jobMock);
|
sut = new PersonService(personMock, configMock, storageMock, jobMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
|
@ -4,6 +4,7 @@ import { AuthUserDto } from '../auth';
|
|||||||
import { mimeTypes } from '../domain.constant';
|
import { mimeTypes } from '../domain.constant';
|
||||||
import { IJobRepository, JobName } from '../job';
|
import { IJobRepository, JobName } from '../job';
|
||||||
import { IStorageRepository, ImmichReadStream } from '../storage';
|
import { IStorageRepository, ImmichReadStream } from '../storage';
|
||||||
|
import { ISystemConfigRepository, SystemConfigCore } from '../system-config';
|
||||||
import {
|
import {
|
||||||
MergePersonDto,
|
MergePersonDto,
|
||||||
PeopleResponseDto,
|
PeopleResponseDto,
|
||||||
@ -17,17 +18,22 @@ import { IPersonRepository, UpdateFacesData } from './person.repository';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PersonService {
|
export class PersonService {
|
||||||
|
private configCore: SystemConfigCore;
|
||||||
readonly logger = new Logger(PersonService.name);
|
readonly logger = new Logger(PersonService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IPersonRepository) private repository: IPersonRepository,
|
@Inject(IPersonRepository) private repository: IPersonRepository,
|
||||||
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
) {}
|
) {
|
||||||
|
this.configCore = new SystemConfigCore(configRepository);
|
||||||
|
}
|
||||||
|
|
||||||
async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
|
async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
|
||||||
|
const { machineLearning } = await this.configCore.getConfig();
|
||||||
const people = await this.repository.getAllForUser(authUser.id, {
|
const people = await this.repository.getAllForUser(authUser.id, {
|
||||||
minimumFaceCount: 1,
|
minimumFaceCount: machineLearning.facialRecognition.minFaces,
|
||||||
withHidden: dto.withHidden || false,
|
withHidden: dto.withHidden || false,
|
||||||
});
|
});
|
||||||
const persons: PersonResponseDto[] = people
|
const persons: PersonResponseDto[] = people
|
||||||
|
@ -48,4 +48,10 @@ export class RecognitionConfig extends ModelConfig {
|
|||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
maxDistance!: number;
|
maxDistance!: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Type(() => Number)
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
minFaces!: number;
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
modelName: 'buffalo_l',
|
modelName: 'buffalo_l',
|
||||||
minScore: 0.7,
|
minScore: 0.7,
|
||||||
maxDistance: 0.6,
|
maxDistance: 0.6,
|
||||||
|
minFaces: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
map: {
|
map: {
|
||||||
|
@ -71,6 +71,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
modelName: 'buffalo_l',
|
modelName: 'buffalo_l',
|
||||||
minScore: 0.7,
|
minScore: 0.7,
|
||||||
maxDistance: 0.6,
|
maxDistance: 0.6,
|
||||||
|
minFaces: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
map: {
|
map: {
|
||||||
|
@ -57,6 +57,7 @@ export enum SystemConfigKey {
|
|||||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL_NAME = 'machineLearning.facialRecognition.modelName',
|
MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL_NAME = 'machineLearning.facialRecognition.modelName',
|
||||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_SCORE = 'machineLearning.facialRecognition.minScore',
|
MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_SCORE = 'machineLearning.facialRecognition.minScore',
|
||||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MAX_DISTANCE = 'machineLearning.facialRecognition.maxDistance',
|
MACHINE_LEARNING_FACIAL_RECOGNITION_MAX_DISTANCE = 'machineLearning.facialRecognition.maxDistance',
|
||||||
|
MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES = 'machineLearning.facialRecognition.minFaces',
|
||||||
|
|
||||||
MAP_ENABLED = 'map.enabled',
|
MAP_ENABLED = 'map.enabled',
|
||||||
MAP_TILE_URL = 'map.tileUrl',
|
MAP_TILE_URL = 'map.tileUrl',
|
||||||
@ -164,6 +165,7 @@ export interface SystemConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
modelName: string;
|
modelName: string;
|
||||||
minScore: number;
|
minScore: number;
|
||||||
|
minFaces: number;
|
||||||
maxDistance: number;
|
maxDistance: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
6
web/src/api/open-api/api.ts
generated
6
web/src/api/open-api/api.ts
generated
@ -2152,6 +2152,12 @@ export interface RecognitionConfig {
|
|||||||
* @memberof RecognitionConfig
|
* @memberof RecognitionConfig
|
||||||
*/
|
*/
|
||||||
'maxDistance': number;
|
'maxDistance': number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof RecognitionConfig
|
||||||
|
*/
|
||||||
|
'minFaces': number;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
|
@ -196,6 +196,17 @@
|
|||||||
isEdited={machineLearningConfig.facialRecognition.maxDistance !==
|
isEdited={machineLearningConfig.facialRecognition.maxDistance !==
|
||||||
savedConfig.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>
|
</div>
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user