diff --git a/api/gqlgen.yml b/api/gqlgen.yml index 73a286af..0ca6f79b 100644 --- a/api/gqlgen.yml +++ b/api/gqlgen.yml @@ -48,6 +48,9 @@ models: model: github.com/photoview/photoview/api/graphql/models.FaceGroup ImageFace: model: github.com/photoview/photoview/api/graphql/models.ImageFace + fields: + faceGroup: + resolver: true FaceRectangle: model: github.com/photoview/photoview/api/graphql/models.FaceRectangle SiteInfo: diff --git a/api/graphql/generated.go b/api/graphql/generated.go index d8e468ed..5c01fe64 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -39,6 +39,7 @@ type Config struct { type ResolverRoot interface { Album() AlbumResolver + ImageFace() ImageFaceResolver Media() MediaResolver Mutation() MutationResolver Query() QueryResolver @@ -85,6 +86,7 @@ type ComplexityRoot struct { } ImageFace struct { + FaceGroup func(childComplexity int) int ID func(childComplexity int) int Media func(childComplexity int) int Rectangle func(childComplexity int) int @@ -258,6 +260,9 @@ type AlbumResolver interface { Path(ctx context.Context, obj *models.Album) ([]*models.Album, error) Shares(ctx context.Context, obj *models.Album) ([]*models.ShareToken, error) } +type ImageFaceResolver interface { + FaceGroup(ctx context.Context, obj *models.ImageFace) (*models.FaceGroup, error) +} type MediaResolver interface { Thumbnail(ctx context.Context, obj *models.Media) (*models.MediaURL, error) HighRes(ctx context.Context, obj *models.Media) (*models.MediaURL, error) @@ -486,6 +491,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.FaceRectangle.MinY(childComplexity), true + case "ImageFace.faceGroup": + if e.complexity.ImageFace.FaceGroup == nil { + break + } + + return e.complexity.ImageFace.FaceGroup(childComplexity), true + case "ImageFace.id": if e.complexity.ImageFace.ID == nil { break @@ -1857,6 +1869,7 @@ type ImageFace { id: ID! media: Media! rectangle: FaceRectangle + faceGroup: FaceGroup! } type FaceRectangle { @@ -3489,6 +3502,41 @@ func (ec *executionContext) _ImageFace_rectangle(ctx context.Context, field grap return ec.marshalOFaceRectangle2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceRectangle(ctx, field.Selections, res) } +func (ec *executionContext) _ImageFace_faceGroup(ctx context.Context, field graphql.CollectedField, obj *models.ImageFace) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ImageFace", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.ImageFace().FaceGroup(rctx, obj) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*models.FaceGroup) + fc.Result = res + return ec.marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx, field.Selections, res) +} + func (ec *executionContext) _Media_id(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -9188,15 +9236,29 @@ func (ec *executionContext) _ImageFace(ctx context.Context, sel ast.SelectionSet case "id": out.Values[i] = ec._ImageFace_id(ctx, field, obj) if out.Values[i] == graphql.Null { - invalids++ + atomic.AddUint32(&invalids, 1) } case "media": out.Values[i] = ec._ImageFace_media(ctx, field, obj) if out.Values[i] == graphql.Null { - invalids++ + atomic.AddUint32(&invalids, 1) } case "rectangle": out.Values[i] = ec._ImageFace_rectangle(ctx, field, obj) + case "faceGroup": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._ImageFace_faceGroup(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/api/graphql/models/face_detection.go b/api/graphql/models/face_detection.go index b4661a82..f3918dbb 100644 --- a/api/graphql/models/face_detection.go +++ b/api/graphql/models/face_detection.go @@ -21,7 +21,8 @@ type FaceGroup struct { type ImageFace struct { Model - FaceGroupID int `gorm:"not null;index"` + FaceGroupID int `gorm:"not null;index"` + FaceGroup *FaceGroup MediaID int `gorm:"not null;index"` Media Media `gorm:"constraint:OnDelete:CASCADE;"` Descriptor FaceDescriptor `gorm:"not null"` diff --git a/api/graphql/resolvers/faces.go b/api/graphql/resolvers/faces.go index fa4ff040..dbe77178 100644 --- a/api/graphql/resolvers/faces.go +++ b/api/graphql/resolvers/faces.go @@ -3,6 +3,7 @@ package resolvers import ( "context" + api "github.com/photoview/photoview/api/graphql" "github.com/photoview/photoview/api/graphql/auth" "github.com/photoview/photoview/api/graphql/models" "github.com/photoview/photoview/api/scanner/face_detection" @@ -10,6 +11,29 @@ import ( "gorm.io/gorm" ) +type imageFaceResolver struct { + *Resolver +} + +func (r *Resolver) ImageFace() api.ImageFaceResolver { + return imageFaceResolver{r} +} + +func (r imageFaceResolver) FaceGroup(ctx context.Context, obj *models.ImageFace) (*models.FaceGroup, error) { + if obj.FaceGroup != nil { + return obj.FaceGroup, nil + } + + var faceGroup models.FaceGroup + if err := r.Database.Model(&obj).Association("FaceGroup").Find(&faceGroup); err != nil { + return nil, err + } + + obj.FaceGroup = &faceGroup + + return &faceGroup, nil +} + func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagination) ([]*models.FaceGroup, error) { user := auth.UserFromContext(ctx) if user == nil { diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index cc10c9bd..36bb8ebd 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -338,6 +338,7 @@ type ImageFace { id: ID! media: Media! rectangle: FaceRectangle + faceGroup: FaceGroup! } type FaceRectangle { diff --git a/api/scanner/face_detection/face_detector.go b/api/scanner/face_detection/face_detector.go index 9cab8885..da4ab32e 100644 --- a/api/scanner/face_detection/face_detector.go +++ b/api/scanner/face_detection/face_detector.go @@ -104,7 +104,7 @@ func (fd *FaceDetector) DetectFaces(media *models.Media) error { } func (fd *FaceDetector) classifyDescriptor(descriptor face.Descriptor) int32 { - return int32(fd.rec.ClassifyThreshold(descriptor, 0.2)) + return int32(fd.rec.ClassifyThreshold(descriptor, 0.3)) } func (fd *FaceDetector) classifyFace(face *face.Face, media *models.Media, imagePath string) error { diff --git a/api/scanner/media_type.go b/api/scanner/media_type.go index bc77a0f8..46f9e2ea 100644 --- a/api/scanner/media_type.go +++ b/api/scanner/media_type.go @@ -150,6 +150,7 @@ var fileExtensions = map[string]MediaType{ ".arw": TypeARW, ".sr2": TypeSR2, ".srf": TypeSRF, + ".srw": TypeSRW, ".cr2": TypeCR2, ".crw": TypeCRW, ".erf": TypeERF, @@ -159,15 +160,22 @@ var fileExtensions = map[string]MediaType{ ".mrw": TypeMRW, ".nef": TypeNEF, ".nrw": TypeNRW, + ".mdc": TypeMDC, + ".mef": TypeMEF, ".orf": TypeORF, ".pef": TypePEF, ".raf": TypeRAF, ".raw": TypeRAW, + ".rw2": TypeRW2, ".dcs": TypeDCS, ".drf": TypeDRF, ".gpr": TypeGPR, ".3fr": Type3FR, ".fff": TypeFFF, + ".cap": TypeCap, + ".iiq": TypeIIQ, + ".mos": TypeMOS, + ".rwl": TypeRWL, // Video formats ".mp4": TypeMP4, diff --git a/ui/src/components/facesOverlay/FacesOverlay.js b/ui/src/components/facesOverlay/FacesOverlay.js index d6536673..74a69644 100644 --- a/ui/src/components/facesOverlay/FacesOverlay.js +++ b/ui/src/components/facesOverlay/FacesOverlay.js @@ -1,19 +1,28 @@ import PropTypes from 'prop-types' import React from 'react' +import { Link } from 'react-router-dom' import styled from 'styled-components' -const FaceBoxStyle = styled.div` +const FaceBoxStyle = styled(Link)` box-shadow: inset 0 0 2px 1px rgba(0, 0, 0, 0.3), 0 0 0 1px rgb(255, 255, 255); border-radius: 50%; position: absolute; - top: ${({ minY }) => minY * 100}%; - bottom: ${({ maxY }) => (1 - maxY) * 100}%; - left: ${({ minX }) => minX * 100}%; - right: ${({ maxX }) => (1 - maxX) * 100}%; + top: ${({ $minY }) => $minY * 100}%; + bottom: ${({ $maxY }) => (1 - $maxY) * 100}%; + left: ${({ $minX }) => $minX * 100}%; + right: ${({ $maxX }) => (1 - $maxX) * 100}%; ` const FaceBox = ({ face /*media*/ }) => { - return + return ( + + ) } FaceBox.propTypes = { diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js index 4e88a0db..906d29b5 100644 --- a/ui/src/components/sidebar/MediaSidebar.js +++ b/ui/src/components/sidebar/MediaSidebar.js @@ -62,6 +62,9 @@ const mediaQuery = gql` minY maxY } + faceGroup { + id + } } } }