Phash distance filter (#3596)

* Add phash_distance filter criterion
* Add distance to phash filter in UI
This commit is contained in:
WithoutPants
2023-04-17 15:36:51 +10:00
committed by GitHub
parent 62a1bc22c9
commit dcc73c4873
13 changed files with 184 additions and 17 deletions

View File

@@ -165,7 +165,9 @@ input SceneFilterType {
"""Filter by file checksum"""
checksum: StringCriterionInput
"""Filter by file phash"""
phash: StringCriterionInput
phash: StringCriterionInput @deprecated(reason: "Use phash_distance instead")
"""Filter by file phash distance"""
phash_distance: PhashDistanceCriterionInput
"""Filter by path"""
path: StringCriterionInput
"""Filter by file count"""
@@ -527,6 +529,12 @@ input TimestampCriterionInput {
modifier: CriterionModifier!
}
input PhashDistanceCriterionInput {
value: String!
modifier: CriterionModifier!
distance: Int
}
enum FilterMode {
SCENES,
PERFORMERS,

View File

@@ -136,3 +136,9 @@ type TimestampCriterionInput struct {
Value2 *string `json:"value2"`
Modifier CriterionModifier `json:"modifier"`
}
type PhashDistanceCriterionInput struct {
Value string `json:"value"`
Modifier CriterionModifier `json:"modifier"`
Distance *int `json:"distance"`
}

View File

@@ -27,6 +27,8 @@ type SceneFilterType struct {
Checksum *StringCriterionInput `json:"checksum"`
// Filter by file phash
Phash *StringCriterionInput `json:"phash"`
// Filter by phash distance
PhashDistance *PhashDistanceCriterionInput `json:"phash_distance"`
// Filter by path
Path *StringCriterionInput `json:"path"`
// Filter by file count

View File

@@ -29,6 +29,7 @@ func (d *CustomSQLiteDriver) Open(dsn string) (driver.Conn, error) {
"regexp": regexFn,
"durationToTinyInt": durationToTinyIntFn,
"basename": basenameFn,
"phash_distance": phashDistanceFn,
}
for name, fn := range funcs {

10
pkg/sqlite/phash.go Normal file
View File

@@ -0,0 +1,10 @@
package sqlite
import "github.com/corona10/goimagehash"
func phashDistanceFn(phash1 int64, phash2 int64) (int64, error) {
hash1 := goimagehash.NewImageHash(uint64(phash1), goimagehash.PHash)
hash2 := goimagehash.NewImageHash(uint64(phash2), goimagehash.PHash)
distance, _ := hash1.Distance(hash2)
return int64(distance), nil
}

View File

@@ -882,17 +882,16 @@ func (qb *SceneStore) makeFilter(ctx context.Context, sceneFilter *models.SceneF
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if sceneFilter.Phash != nil {
qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
value, _ := utils.StringToPhash(sceneFilter.Phash.Value)
intCriterionHandler(&models.IntCriterionInput{
Value: int(value),
// backwards compatibility
scenePhashDistanceCriterionHandler(qb, &models.PhashDistanceCriterionInput{
Value: sceneFilter.Phash.Value,
Modifier: sceneFilter.Phash.Modifier,
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
})(ctx, f)
}
}))
query.handleCriterion(ctx, scenePhashDistanceCriterionHandler(qb, sceneFilter.PhashDistance))
query.handleCriterion(ctx, intCriterionHandler(sceneFilter.Rating100, "scenes.rating", nil))
// legacy rating handler
query.handleCriterion(ctx, rating5CriterionHandler(sceneFilter.Rating, "scenes.rating", nil))
@@ -1382,6 +1381,45 @@ INNER JOIN (` + valuesClause + `) t ON t.column2 = pt.tag_id
}
}
func scenePhashDistanceCriterionHandler(qb *SceneStore, phashDistance *models.PhashDistanceCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if phashDistance != nil {
qb.addSceneFilesTable(f)
f.addLeftJoin(fingerprintTable, "fingerprints_phash", "scenes_files.file_id = fingerprints_phash.file_id AND fingerprints_phash.type = 'phash'")
value, _ := utils.StringToPhash(phashDistance.Value)
distance := 0
if phashDistance.Distance != nil {
distance = *phashDistance.Distance
}
if distance == 0 {
// use the default handler
intCriterionHandler(&models.IntCriterionInput{
Value: int(value),
Modifier: phashDistance.Modifier,
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
}
switch {
case phashDistance.Modifier == models.CriterionModifierEquals && distance > 0:
// needed to avoid a type mismatch
f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'")
f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) < ?", value, distance)
case phashDistance.Modifier == models.CriterionModifierNotEquals && distance > 0:
// needed to avoid a type mismatch
f.addWhere("typeof(fingerprints_phash.fingerprint) = 'integer'")
f.addWhere("phash_distance(fingerprints_phash.fingerprint, ?) > ?", value, distance)
default:
intCriterionHandler(&models.IntCriterionInput{
Value: int(value),
Modifier: phashDistance.Modifier,
}, "fingerprints_phash.fingerprint", nil)(ctx, f)
}
}
}
}
func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindFilterType) {
if findFilter == nil || findFilter.Sort == nil || *findFilter.Sort == "" {
return

View File

@@ -38,6 +38,8 @@ import { RatingFilter } from "./Filters/RatingFilter";
import { BooleanFilter } from "./Filters/BooleanFilter";
import { OptionsListFilter } from "./Filters/OptionsListFilter";
import { PathFilter } from "./Filters/PathFilter";
import { PhashCriterion } from "src/models/list-filter/criteria/phash";
import { PhashFilter } from "./Filters/PhashFilter";
interface IGenericCriterionEditor {
criterion: Criterion<CriterionValue>;
@@ -172,6 +174,11 @@ const GenericCriterionEditor: React.FC<IGenericCriterionEditor> = ({
<RatingFilter criterion={criterion} onValueChanged={onValueChanged} />
);
}
if (criterion instanceof PhashCriterion) {
return (
<PhashFilter criterion={criterion} onValueChanged={onValueChanged} />
);
}
if (
criterion instanceof CountryCriterion &&
(criterion.modifier === CriterionModifier.Equals ||

View File

@@ -0,0 +1,63 @@
import React from "react";
import { Form } from "react-bootstrap";
import { useIntl } from "react-intl";
import { IPhashDistanceValue } from "../../../models/list-filter/types";
import { Criterion } from "../../../models/list-filter/criteria/criterion";
import { CriterionModifier } from "src/core/generated-graphql";
interface IPhashFilterProps {
criterion: Criterion<IPhashDistanceValue>;
onValueChanged: (value: IPhashDistanceValue) => void;
}
export const PhashFilter: React.FC<IPhashFilterProps> = ({
criterion,
onValueChanged,
}) => {
const intl = useIntl();
const { value } = criterion;
function valueChanged(event: React.ChangeEvent<HTMLInputElement>) {
onValueChanged({
value: event.target.value,
distance: criterion.value.distance,
});
}
function distanceChanged(event: React.ChangeEvent<HTMLInputElement>) {
let distance = parseInt(event.target.value);
if (distance < 0 || isNaN(distance)) {
distance = 0;
}
onValueChanged({
distance,
value: criterion.value.value,
});
}
return (
<div>
<Form.Group>
<Form.Control
className="btn-secondary"
onChange={valueChanged}
value={value ? value.value : ""}
placeholder={intl.formatMessage({ id: "phash" })}
/>
</Form.Group>
{criterion.modifier !== CriterionModifier.IsNull &&
criterion.modifier !== CriterionModifier.NotNull && (
<Form.Group>
<Form.Control
className="btn-secondary"
onChange={distanceChanged}
type="number"
value={value ? value.distance : ""}
placeholder={intl.formatMessage({ id: "distance" })}
/>
</Form.Group>
)}
</div>
);
};

View File

@@ -22,6 +22,7 @@ import {
IStashIDValue,
IDateValue,
ITimestampValue,
IPhashDistanceValue,
} from "../types";
export type Option = string | number | IOptionType;
@@ -32,7 +33,8 @@ export type CriterionValue =
| INumberValue
| IStashIDValue
| IDateValue
| ITimestampValue;
| ITimestampValue
| IPhashDistanceValue;
const modifierMessageIDs = {
[CriterionModifier.Equals]: "criterion_modifier.equals",

View File

@@ -48,7 +48,7 @@ import { MoviesCriterionOption } from "./movies";
import { GalleriesCriterion } from "./galleries";
import { CriterionType } from "../types";
import { InteractiveCriterion } from "./interactive";
import { DuplicatedCriterion, PhashCriterionOption } from "./phash";
import { DuplicatedCriterion, PhashCriterion } from "./phash";
import { CaptionCriterion } from "./captions";
import { RatingCriterion } from "./rating";
import { CountryCriterion } from "./country";
@@ -167,7 +167,7 @@ export function makeCriteria(
new StringCriterionOption("media_info.checksum", type, "checksum")
);
case "phash":
return new StringCriterion(PhashCriterionOption);
return new PhashCriterion();
case "duplicated":
return new DuplicatedCriterion();
case "country":

View File

@@ -1,15 +1,19 @@
import { CriterionModifier } from "src/core/generated-graphql";
import {
CriterionModifier,
PhashDistanceCriterionInput,
} from "src/core/generated-graphql";
import { IPhashDistanceValue } from "../types";
import {
BooleanCriterionOption,
Criterion,
CriterionOption,
PhashDuplicateCriterion,
StringCriterion,
} from "./criterion";
export const PhashCriterionOption = new CriterionOption({
messageID: "media_info.phash",
type: "phash",
parameterName: "phash",
parameterName: "phash_distance",
inputType: "text",
modifierOptions: [
CriterionModifier.Equals,
@@ -19,9 +23,30 @@ export const PhashCriterionOption = new CriterionOption({
],
});
export class PhashCriterion extends StringCriterion {
export class PhashCriterion extends Criterion<IPhashDistanceValue> {
constructor() {
super(PhashCriterionOption);
super(PhashCriterionOption, { value: "", distance: 0 });
}
public getLabelValue() {
const { value, distance } = this.value;
if (
(this.modifier === CriterionModifier.Equals ||
this.modifier === CriterionModifier.NotEquals) &&
distance
) {
return `${value} (${distance})`;
} else {
return `${value}`;
}
}
protected toCriterionInput(): PhashDistanceCriterionInput {
return {
value: this.value.value,
modifier: this.modifier,
distance: this.value.distance,
};
}
}

View File

@@ -48,6 +48,11 @@ export interface ITimestampValue {
value2: string | undefined;
}
export interface IPhashDistanceValue {
value: string;
distance?: number;
}
export function criterionIsHierarchicalLabelValue(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any

View File

@@ -289,7 +289,7 @@ const makeScenesPHashMatchUrl = (phash: GQL.Maybe<string> | undefined) => {
if (!phash) return "#";
const filter = new ListFilterModel(GQL.FilterMode.Scenes, undefined);
const criterion = new PhashCriterion();
criterion.value = phash;
criterion.value = { value: phash };
filter.criteria.push(criterion);
return `/scenes?${filter.makeQueryParameters()}`;
};