People: Simplify thumbnail API for face crops #22

This commit is contained in:
Michael Mayer 2021-09-20 16:17:10 +02:00
parent 895d41cd61
commit 8e5a97ed4a
27 changed files with 312 additions and 224 deletions

View file

@ -82,21 +82,11 @@ export class Face extends RestModel {
}
thumbnailUrl(size) {
if (!this.Marker || !this.Marker.FileHash) {
if (!this.Marker) {
return `${config.contentUri}/svg/portrait`;
}
if (!size) {
size = "tile_160";
}
if (this.Marker.CropArea && (size === "tile_160" || size === "tile_320")) {
return `${config.contentUri}/t/${this.Marker.FileHash}/${config.previewToken()}/${size}/${
this.Marker.CropArea
}`;
} else {
return `${config.contentUri}/t/${this.Marker.FileHash}/${config.previewToken()}/${size}`;
}
return this.Marker.thumbnailUrl(size);
}
getDateString() {
@ -114,7 +104,7 @@ export class Face extends RestModel {
}
static batchSize() {
return 500;
return 120;
}
static getCollectionResource() {

View file

@ -40,8 +40,7 @@ export class Marker extends RestModel {
return {
UID: "",
FileUID: "",
FileHash: "",
CropArea: "",
Thumb: "",
Type: "",
Src: "",
Name: "",
@ -87,10 +86,8 @@ export class Marker extends RestModel {
size = "tile_160";
}
if (this.FileHash && this.CropArea) {
return `${config.contentUri}/t/${this.FileHash}/${config.previewToken()}/${size}/${
this.CropArea
}`;
if (this.Thumb) {
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
} else {
return `${config.contentUri}/svg/portrait`;
}

View file

@ -40,8 +40,6 @@ export class Subject extends RestModel {
getDefaults() {
return {
UID: "",
MarkerUID: "",
MarkerSrc: "",
Type: "",
Src: "",
Slug: "",
@ -53,8 +51,8 @@ export class Subject extends RestModel {
Private: false,
Excluded: false,
FileCount: 0,
FileHash: "",
CropArea: "",
Thumb: "",
ThumbSrc: "",
Metadata: {},
CreatedAt: "",
UpdatedAt: "",
@ -90,7 +88,7 @@ export class Subject extends RestModel {
}
thumbnailUrl(size) {
if (!this.FileHash) {
if (!this.Thumb) {
return `${config.contentUri}/svg/portrait`;
}
@ -98,13 +96,7 @@ export class Subject extends RestModel {
size = "tile_160";
}
if (this.CropArea && (size === "tile_160" || size === "tile_320")) {
return `${config.contentUri}/t/${this.FileHash}/${config.previewToken()}/${size}/${
this.CropArea
}`;
} else {
return `${config.contentUri}/t/${this.FileHash}/${config.previewToken()}/${size}`;
}
return `${config.contentUri}/t/${this.Thumb}/${config.previewToken()}/${size}`;
}
getDateString() {
@ -132,7 +124,7 @@ export class Subject extends RestModel {
}
static batchSize() {
return 500;
return 480;
}
static getCollectionResource() {

View file

@ -29,17 +29,6 @@
import Subjects from "pages/people/subjects.vue";
import Faces from "pages/people/faces.vue";
function initTabs(flag, tabs) {
let i = 0;
while (i < tabs.length) {
if (!tabs[i][flag]) {
tabs.splice(i, 1);
} else {
i++;
}
}
}
export default {
name: 'PPagePeople',
props: {
@ -61,8 +50,6 @@ export default {
'class': '',
'path': '/people',
'icon': 'people_alt',
'readonly': true,
'demo': true,
},
{
'name': 'people-faces',
@ -72,8 +59,6 @@ export default {
'class': '',
'path': '/people/new',
'icon': 'person_add',
'readonly': true,
'demo': true,
},
];

View file

@ -182,15 +182,19 @@ export default {
this.filter.q = query["q"] ? query["q"] : "";
this.filter.all = query["all"] ? query["all"] : "";
this.filter.order = this.sortOrder();
this.lastFilter = {};
this.routeName = this.$route.name;
if (this.dirty) {
this.lastFilter = {};
}
this.search();
}
},
created() {
this.search();
// this.subscriptions.push(Event.subscribe("subjects", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("faces", (ev, data) => this.onUpdate(ev, data)));
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));

View file

@ -198,8 +198,12 @@ export default {
this.filter.q = query["q"] ? query["q"] : "";
this.filter.all = query["all"] ? query["all"] : "";
this.filter.order = this.sortOrder();
this.lastFilter = {};
this.routeName = this.$route.name;
if (this.dirty) {
this.lastFilter = {};
}
this.search();
}
},

View file

@ -5,6 +5,8 @@ import (
"path/filepath"
"time"
"github.com/photoprism/photoprism/internal/crop"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism"
@ -15,16 +17,16 @@ import (
"github.com/photoprism/photoprism/pkg/txt"
)
// GetThumb returns a thumbnail image matching the hash and type.
// GetThumb returns a thumbnail image matching the file hash, crop area, and type.
//
// GET /api/v1/t/:hash/:token/:size
// GET /api/v1/t/:thumb/:token/:size
//
// Parameters:
// hash: string sha1 file hash
// thumb: string sha1 file hash plus optional crop area
// token: string url security token, see config
// size: string thumb type, see thumb.Sizes
func GetThumb(router *gin.RouterGroup) {
router.GET("/t/:hash/:token/:size", func(c *gin.Context) {
router.GET("/t/:thumb/:token/:size", func(c *gin.Context) {
if InvalidPreviewToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
@ -34,9 +36,45 @@ func GetThumb(router *gin.RouterGroup) {
start := time.Now()
conf := service.Config()
fileHash := c.Param("hash")
thumbName := thumb.Name(c.Param("size"))
download := c.Query("download") != ""
fileHash, cropArea := crop.ParseThumb(c.Param("thumb"))
// Is cropped thumbnail?
if cropArea != "" {
cropName := crop.Name(c.Param("size"))
cropSize, ok := crop.Sizes[cropName]
if !ok {
log.Errorf("%s: invalid size %s", logPrefix, cropName)
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
fileName, err := crop.FromRequest(fileHash, cropArea, cropSize, conf.ThumbPath())
if err != nil {
log.Warnf("%s: %s", logPrefix, err)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
} else if fileName == "" {
log.Errorf("%s: empty file name, potential bug", logPrefix)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
AddThumbCacheHeader(c)
if download {
c.FileAttachment(fileName, cropName.Jpeg())
} else {
c.File(fileName)
}
return
}
thumbName := thumb.Name(c.Param("size"))
size, ok := thumb.Sizes[thumbName]

View file

@ -1,64 +0,0 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/crop"
"github.com/photoprism/photoprism/internal/service"
)
// GetThumbCrop returns a cropped thumbnail image matching the hash and type.
//
// GET /api/v1/t/:hash/:token/:size/:crop
//
// Parameters:
// hash: string sha1 file hash
// token: string url security token, see config
// size: string crop size, see crop.Sizes
// area: string image area identifier, e.g. 1690960ff17f
func GetThumbCrop(router *gin.RouterGroup) {
router.GET("/t/:hash/:token/:size/:area", func(c *gin.Context) {
if InvalidPreviewToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
logPrefix := "thumb-crop"
conf := service.Config()
fileHash := c.Param("hash")
cropName := crop.Name(c.Param("size"))
cropArea := c.Param("area")
download := c.Query("download") != ""
cropSize, ok := crop.Sizes[cropName]
if !ok {
log.Errorf("%s: invalid size %s", logPrefix, cropName)
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
fileName, err := crop.FromRequest(fileHash, cropArea, cropSize, conf.ThumbPath())
if err != nil {
log.Warnf("%s: %s", logPrefix, err)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
} else if fileName == "" {
log.Errorf("%s: empty file name, potential bug", logPrefix)
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
return
}
AddThumbCacheHeader(c)
if download {
c.FileAttachment(fileName, cropName.Jpeg())
} else {
c.File(fileName)
}
})
}

View file

@ -46,34 +46,31 @@ func TestGetThumb(t *testing.T) {
r := PerformRequest(app, "GET", "/api/v1/t/acad9168fa6acc5c5c2965ddf6ec465ca42fd832/"+conf.PreviewToken()+"/fit_7680")
assert.Equal(t, http.StatusOK, r.Code)
})
}
func TestGetThumbCrop(t *testing.T) {
t.Run("NotFound", func(t *testing.T) {
app, router, conf := NewApiTest()
GetThumbCrop(router)
r := PerformRequest(app, "GET", "/api/v1/t/46f5b5c0c027f0c1b15136644f404c57210bf20c/"+conf.PreviewToken()+"/tile_160/016014058037")
GetThumb(router)
r := PerformRequest(app, "GET", "/api/v1/t/46f5b5c0c027f0c1b15136644f404c57210bf20c-016014058037/"+conf.PreviewToken()+"/tile_160")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("InvalidType", func(t *testing.T) {
app, router, conf := NewApiTest()
GetThumbCrop(router)
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.PreviewToken()+"/xxx/016014058037")
GetThumb(router)
r := PerformRequest(app, "GET", "/api/v1/t/1-016014058037/"+conf.PreviewToken()+"/xxx")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("InvalidHash", func(t *testing.T) {
app, router, conf := NewApiTest()
GetThumbCrop(router)
r := PerformRequest(app, "GET", "/api/v1/t/1/"+conf.PreviewToken()+"/tile_500/016014058037")
GetThumb(router)
r := PerformRequest(app, "GET", "/api/v1/t/1-016014058037/"+conf.PreviewToken()+"/tile_500")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("invalid token", func(t *testing.T) {
app, router, _ := NewApiTest()
GetThumbCrop(router)
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818/xxx/tile_500/016014058037")
GetThumb(router)
r := PerformRequest(app, "GET", "/api/v1/t/2cad9168fa6acc5c5c2965ddf6ec465ca42fd818-016014058037/xxx/tile_500")
assert.Equal(t, http.StatusForbidden, r.Code)
})
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"image"
"strconv"
"strings"
"github.com/photoprism/photoprism/pkg/rnd"
)
@ -20,11 +21,29 @@ type Area struct {
H float32 `json:"h,omitempty"`
}
// Empty tests if the area is empty.
func (a Area) Empty() bool {
return a.X == 0 && a.Y == 0 && a.W == 0 && a.H == 0
}
// String returns a string identifying the crop area.
func (a Area) String() string {
if a.Empty() {
return ""
}
return fmt.Sprintf("%03x%03x%03x%03x", int(a.X*1000), int(a.Y*1000), int(a.W*1000), int(a.H*1000))
}
// Thumb returns a string identifying the file and crop area to create a thumb.
func (a Area) Thumb(fileHash string) string {
if len(fileHash) < 40 {
return a.String()
}
return fmt.Sprintf("%040s-%012s", fileHash, a.String())
}
// Bounds returns absolute coordinates and dimension.
func (a Area) Bounds(img image.Image) (min, max image.Point, dim int) {
size := img.Bounds().Max
@ -76,3 +95,37 @@ func AreaFromString(s string) Area {
return NewArea("crop", float32(x)/1000, float32(y)/1000, float32(w)/1000, float32(h)/1000)
}
// IsCroppedThumb tests if the string represents a cropped thumbnail and returns the split position if true.
func IsCroppedThumb(thumb string) int {
if thumb == "" || len(thumb) < 41 {
return -1
}
if i := strings.IndexRune(thumb, '-'); i >= 40 && i < len(thumb)-1 {
return i
}
return -1
}
// ParseThumb splits a thumbnail string into the crop area and file hash.
func ParseThumb(thumb string) (fileHash, area string) {
if len(thumb) == 12 {
return "", thumb
} else if len(thumb) < 41 {
return thumb, ""
}
s := strings.SplitN(strings.Trim(thumb, "/ -"), "-", 2)
fileHash = s[0]
if len(s) < 2 {
// Do nothing.
} else if len(s[1]) >= 12 {
area = s[1]
}
return fileHash, area
}

View file

@ -33,6 +33,42 @@ func TestArea_String(t *testing.T) {
m := NewArea("", -2.0001, 0.123, -0.1, 4.00000001)
assert.Equal(t, "00007b0003e8", m.String())
})
t.Run("00007b0003e8", func(t *testing.T) {
m := NewArea("", 0, 0, 0, 0)
assert.Equal(t, "", m.String())
})
}
func TestArea_Thumb(t *testing.T) {
t.Run("3e814d0011f4", func(t *testing.T) {
expected := fmt.Sprintf("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-%x%x00%x%x", 1000, 333, 1, 500)
m := NewArea("face", 1.000, 0.33333, 0.001, 0.5)
assert.Equal(t, expected, m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
})
t.Run("3360a7064042_face", func(t *testing.T) {
m := NewArea("face", 0.822059, 0.167969, 0.1, 0.0664062)
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-3360a7064042", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
})
t.Run("3360a7064042_back", func(t *testing.T) {
m := NewArea("back", 0.822059, 0.167969, 0.1, 0.0664062)
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-3360a7064042", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
})
t.Run("0c93e801e000", func(t *testing.T) {
m := NewArea("face", 0.201, 1.000, 0.03, 0.00000001)
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-0c93e801e000", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
})
t.Run("0003e8000000", func(t *testing.T) {
m := NewArea("face", 0.0001, 1.000, 0, 0.00000001)
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-0003e8000000", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
})
t.Run("ShortHash", func(t *testing.T) {
m := NewArea("", -2.0001, 0.123, -0.1, 4.00000001)
assert.Equal(t, "00007b0003e8", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41"))
})
t.Run("Empty", func(t *testing.T) {
m := NewArea("", 0, 0, 0, 0)
assert.Equal(t, "", m.Thumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41"))
})
}
func TestAreaFromString(t *testing.T) {
@ -48,3 +84,56 @@ func TestAreaFromString(t *testing.T) {
assert.Equal(t, NewArea("crop", 0.822, 0.167, 0.1, 0.066), a)
})
}
func TestIsCroppedThumb(t *testing.T) {
t.Run("True", func(t *testing.T) {
assert.Equal(t, 40, IsCroppedThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-00007b0003e8"))
})
t.Run("Invalid", func(t *testing.T) {
assert.Equal(t, -1, IsCroppedThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e3100007b0003e8-"))
})
t.Run("CropArea", func(t *testing.T) {
assert.Equal(t, -1, IsCroppedThumb("00007b0003e8"))
})
t.Run("ShortHash", func(t *testing.T) {
assert.Equal(t, -1, IsCroppedThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41"))
})
t.Run("HashOnly", func(t *testing.T) {
assert.Equal(t, -1, IsCroppedThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31"))
})
}
func TestParseThumb(t *testing.T) {
t.Run("True", func(t *testing.T) {
h, a := ParseThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31-00007b0003e8")
assert.Equal(t, 40, len(h))
assert.Equal(t, 12, len(a))
assert.False(t, AreaFromString(a).Empty())
})
t.Run("Invalid", func(t *testing.T) {
h, a := ParseThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e3100007b0003e8-")
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e3100007b0003e8", h)
assert.Equal(t, 0, len(a))
assert.True(t, AreaFromString(a).Empty())
})
t.Run("CropArea", func(t *testing.T) {
h, a := ParseThumb("00007b0003e8")
assert.Equal(t, 0, len(h))
assert.Equal(t, 12, len(a))
assert.False(t, AreaFromString(a).Empty())
})
t.Run("ShortHash", func(t *testing.T) {
h, a := ParseThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41")
assert.Equal(t, 37, len(h))
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41", h)
assert.Equal(t, 0, len(a))
assert.True(t, AreaFromString(a).Empty())
})
t.Run("HashOnly", func(t *testing.T) {
h, a := ParseThumb("346b3897eec9ef75e35fbf0bbc4c83c55ca41e31")
assert.Equal(t, 40, len(h))
assert.Equal(t, "346b3897eec9ef75e35fbf0bbc4c83c55ca41e31", h)
assert.Equal(t, 0, len(a))
assert.True(t, AreaFromString(a).Empty())
})
}

View file

@ -12,6 +12,10 @@ func (n Name) Jpeg() string {
// Names of standard crop sizes.
const (
Tile50 Name = "tile_50"
Tile100 Name = "tile_100"
Tile160 Name = "tile_160"
Tile224 Name = "tile_224"
Tile320 Name = "tile_320"
Tile500 Name = "tile_500"
)

View file

@ -19,6 +19,10 @@ type SizeMap map[Name]Size
// Sizes contains the properties of all thumbnail sizes.
var Sizes = SizeMap{
Tile50: {Tile50, Tile320, "Lists", 50, 50, DefaultOptions},
Tile100: {Tile100, Tile320, "Maps", 100, 100, DefaultOptions},
Tile160: {Tile160, Tile320, "FaceNet", 160, 160, DefaultOptions},
Tile224: {Tile224, Tile320, "TensorFlow, Mosaic", 224, 224, DefaultOptions},
Tile320: {Tile320, "", "UI", 320, 320, DefaultOptions},
Tile500: {Tile500, "", "FaceNet", 500, 500, DefaultOptions},
}

View file

@ -30,8 +30,6 @@ type Album struct {
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
AlbumUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
ParentUID string `gorm:"type:VARBINARY(42);default:''" json:"ParentUID,omitempty" yaml:"ParentUID,omitempty"`
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
AlbumSlug string `gorm:"type:VARBINARY(255);index;" json:"Slug" yaml:"Slug"`
AlbumPath string `gorm:"type:VARBINARY(500);index;" json:"Path,omitempty" yaml:"Path,omitempty"`
AlbumType string `gorm:"type:VARBINARY(8);default:'album';" json:"Type" yaml:"Type,omitempty"`
@ -50,6 +48,8 @@ type Album struct {
AlbumDay int `gorm:"index:idx_albums_ymd" json:"Day" yaml:"Day,omitempty"`
AlbumFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"`
AlbumPrivate bool `json:"Private" yaml:"Private,omitempty"`
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb" yaml:"Thumb,omitempty"`
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
CreatedAt time.Time `json:"CreatedAt" yaml:"CreatedAt,omitempty"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"UpdatedAt,omitempty"`
DeletedAt *time.Time `sql:"index" json:"DeletedAt" yaml:"DeletedAt,omitempty"`

View file

@ -20,8 +20,6 @@ type Labels []Label
type Label struct {
ID uint `gorm:"primary_key" json:"ID" yaml:"-"`
LabelUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
LabelSlug string `gorm:"type:VARBINARY(255);unique_index;" json:"Slug" yaml:"-"`
CustomSlug string `gorm:"type:VARBINARY(255);index;" json:"CustomSlug" yaml:"-"`
LabelName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name"`
@ -31,6 +29,8 @@ type Label struct {
LabelNotes string `gorm:"type:TEXT;" json:"Notes" yaml:"Notes,omitempty"`
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id" json:"-" yaml:"-"`
PhotoCount int `gorm:"default:1" json:"PhotoCount" yaml:"-"`
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb" yaml:"Thumb,omitempty"`
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`

View file

@ -27,8 +27,6 @@ const (
type Marker struct {
MarkerUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
FileUID string `gorm:"type:VARBINARY(42);index;" json:"FileUID" yaml:"FileUID"`
FileHash string `gorm:"type:VARBINARY(128);index" json:"FileHash" yaml:"FileHash,omitempty"`
CropArea string `gorm:"type:VARBINARY(16);default:''" json:"CropArea" yaml:"CropArea,omitempty"`
MarkerType string `gorm:"type:VARBINARY(8);default:'';" json:"Type" yaml:"Type"`
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
MarkerName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name,omitempty"`
@ -50,6 +48,7 @@ type Marker struct {
Q int `json:"Q" yaml:"Q,omitempty"`
Size int `gorm:"default:-1" json:"Size" yaml:"Size,omitempty"`
Score int `gorm:"type:SMALLINT" json:"Score" yaml:"Score,omitempty"`
Thumb string `gorm:"type:VARBINARY(128);index" json:"Thumb" yaml:"Thumb,omitempty"`
MatchedAt *time.Time `sql:"index" json:"MatchedAt" yaml:"MatchedAt,omitempty"`
CreatedAt time.Time
UpdatedAt time.Time
@ -70,18 +69,23 @@ func (m *Marker) BeforeCreate(scope *gorm.Scope) error {
}
// NewMarker creates a new entity.
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string) *Marker {
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string, size, score int) *Marker {
m := &Marker{
FileUID: file.FileUID,
FileHash: file.FileHash,
CropArea: area.String(),
MarkerSrc: markerSrc,
MarkerType: markerType,
MarkerReview: score < 30,
MarkerInvalid: false,
SubjUID: subjUID,
FaceDist: -1,
X: area.X,
Y: area.Y,
W: area.W,
H: area.H,
Q: int(float32(math.Log(float64(score))) * float32(size) * area.W),
Size: size,
Score: score,
Thumb: area.Thumb(file.FileHash),
MatchedAt: nil,
}
@ -90,13 +94,8 @@ func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string)
// NewFaceMarker creates a new entity.
func NewFaceMarker(f face.Face, file File, subjUID string) *Marker {
m := NewMarker(file, f.CropArea(), subjUID, SrcImage, MarkerFace)
m := NewMarker(file, f.CropArea(), subjUID, SrcImage, MarkerFace, f.Size(), f.Score)
m.Size = f.Size()
m.Q = int(float32(math.Log(float64(f.Score))) * float32(m.Size) * m.W)
m.Score = f.Score
m.MarkerReview = f.Score < 30
m.FaceDist = -1
m.EmbeddingsJSON = f.EmbeddingsJSON()
m.LandmarksJSON = f.RelativeLandmarksJSON()
@ -403,6 +402,11 @@ func (m *Marker) ClearSubject(src string) error {
// Face returns a matching face entity if possible.
func (m *Marker) Face() (f *Face) {
if m.MarkerUID == "" {
log.Debugf("marker: empty uid while finding face")
return nil
}
if m.face != nil {
if m.FaceID == m.face.ID {
return m.face
@ -412,7 +416,7 @@ func (m *Marker) Face() (f *Face) {
// Add face if size
if m.SubjSrc != SrcAuto && m.FaceID == "" {
if m.Size < face.ClusterMinSize || m.Score < face.ClusterMinScore {
log.Debugf("faces: skipped adding face for low-quality marker %s, size %d, score %d", m.MarkerUID, m.Size, m.Score)
log.Debugf("marker: skipped adding face due to low-quality (uid %s, size %d, score %d)", txt.Quote(m.MarkerUID), m.Size, m.Score)
return nil
} else if emb := m.Embeddings(); len(emb) == 0 {
log.Warnf("marker: %s has no embeddings", m.MarkerUID)
@ -537,7 +541,7 @@ func FindFaceMarker(faceId string) *Marker {
var result Marker
if err := Db().Where("face_id = ?", faceId).
Where("file_hash <> '' AND marker_invalid = 0").
Where("thumb <> '' AND marker_invalid = 0").
Order("face_dist ASC, q DESC").First(&result).Error; err != nil {
log.Warnf("face: no marker for %s", txt.Quote(faceId))
return nil
@ -567,7 +571,6 @@ func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
err := result.Updates(map[string]interface{}{
"MarkerType": m.MarkerType,
"MarkerSrc": m.MarkerSrc,
"CropArea": m.CropArea,
"X": m.X,
"Y": m.Y,
"W": m.W,
@ -575,6 +578,7 @@ func UpdateOrCreateMarker(m *Marker) (*Marker, error) {
"Q": m.Q,
"Size": m.Size,
"Score": m.Score,
"Thumb": m.Thumb,
"LandmarksJSON": m.LandmarksJSON,
"EmbeddingsJSON": m.EmbeddingsJSON,
})

View file

@ -3,7 +3,7 @@ package entity
import "github.com/photoprism/photoprism/internal/crop"
// UnknownMarker can be used as a default for unknown markers.
var UnknownMarker = NewMarker(File{}, crop.Area{}, "", SrcDefault, MarkerUnknown)
var UnknownMarker = NewMarker(File{}, crop.Area{}, "", SrcDefault, MarkerUnknown, 0, 0)
type MarkerMap map[string]Marker
@ -27,7 +27,7 @@ var MarkerFixtures = MarkerMap{
"1000003-1": Marker{ //Photo04
MarkerUID: "mqu0xs11qekk9jx8",
FileUID: "ft2es39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
SubjUID: "jqu0xs11qekk9jx8",
MarkerSrc: SrcImage,
MarkerType: MarkerLabel,
@ -41,7 +41,7 @@ var MarkerFixtures = MarkerMap{
"1000003-2": Marker{ //Photo04
MarkerUID: "mt9k3pw1wowuy3c3",
FileUID: "ft2es39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
SubjUID: "lt9k3pw1wowuy3c3",
FaceID: "LRG2HJBDZE66LYG7Q5SRFXO2MDTOES52",
MarkerName: "Unknown",
@ -57,7 +57,7 @@ var MarkerFixtures = MarkerMap{
"1000003-3": Marker{ //Photo04
MarkerUID: "mt9k3pw1wowuy111",
FileUID: "ft2es39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
SubjUID: "",
MarkerSrc: SrcImage,
MarkerType: MarkerLabel,
@ -72,7 +72,7 @@ var MarkerFixtures = MarkerMap{
"1000003-4": Marker{ //Photo04
MarkerUID: "mt9k3pw1wowuy222",
FileUID: "ft2es39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
SubjUID: "",
MarkerSrc: SrcImage,
MarkerType: MarkerFace,
@ -89,7 +89,7 @@ var MarkerFixtures = MarkerMap{
"1000003-5": Marker{ //Photo04
MarkerUID: "mt9k3pw1wowuy333",
FileUID: "ft2es39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
FaceID: FaceFixtures.Get("unknown").ID,
SubjUID: "",
SubjSrc: SrcAuto,
@ -108,7 +108,7 @@ var MarkerFixtures = MarkerMap{
"1000003-6": Marker{ //Photo04
MarkerUID: "mt9k3pw1wowuy444",
FileUID: "ft2es39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
FaceID: FaceFixtures.Get("john-doe").ID,
FaceDist: 0.2,
SubjSrc: SrcAuto,
@ -128,7 +128,7 @@ var MarkerFixtures = MarkerMap{
"ma-ba-1": Marker{ //Photo27
MarkerUID: "mt9k3pw1wowuy555",
FileUID: "ft2es49qhhinlple",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
FaceID: FaceFixtures.Get("fa-gr").ID,
FaceDist: 0.5,
SubjSrc: "",
@ -148,7 +148,7 @@ var MarkerFixtures = MarkerMap{
"fa-gr-1": Marker{ //Photo27
MarkerUID: "mt9k3pw1wowuy666",
FileUID: "ft2es49qhhinlple",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
FaceID: FaceFixtures.Get("fa-gr").ID,
FaceDist: 0.6,
SubjSrc: SrcAuto,
@ -168,7 +168,7 @@ var MarkerFixtures = MarkerMap{
"fa-gr-2": Marker{ //Photo03
MarkerUID: "mt9k3pw1wowuy777",
FileUID: "ft2es49w15bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
FaceID: FaceFixtures.Get("fa-gr").ID,
FaceDist: 0.6,
SubjSrc: SrcAuto,
@ -188,7 +188,7 @@ var MarkerFixtures = MarkerMap{
"fa-gr-3": Marker{ //19800101_000002_D640C559
MarkerUID: "mt9k3pw1wowuy888",
FileUID: "ft8es39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
FaceID: FaceFixtures.Get("fa-gr").ID,
FaceDist: 0.6,
SubjSrc: SrcAuto,
@ -208,9 +208,8 @@ var MarkerFixtures = MarkerMap{
"actress-a-1": Marker{ //Photo27
MarkerUID: "mt9k3pw1wowuy999",
FileUID: "ft2es49qhhinlple",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818-045038063041",
FaceID: FaceFixtures.Get("actress-1").ID,
CropArea: "045038063041",
FaceDist: 0.26852392873736236,
SubjSrc: SrcManual,
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
@ -229,9 +228,8 @@ var MarkerFixtures = MarkerMap{
"actress-a-2": Marker{ //Photo03 - non primary file
MarkerUID: "mt9k3pw1wowu1000",
FileUID: "ft2es49whhbnlqdn",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818-046045043065",
FaceID: FaceFixtures.Get("actress-1").ID,
CropArea: "046045043065",
FaceDist: 0.4507357278575355,
SubjSrc: "",
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
@ -250,9 +248,8 @@ var MarkerFixtures = MarkerMap{
"actress-a-3": Marker{ //19800101_000002_D640C559
MarkerUID: "mt9k3pw1wowu1001",
FileUID: "ft8es39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818-05403304060446",
FaceID: FaceFixtures.Get("actress-1").ID,
CropArea: "05403304060446",
FaceDist: 0.5099754448545762,
SubjSrc: "",
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
@ -271,7 +268,7 @@ var MarkerFixtures = MarkerMap{
"actor-a-1": Marker{ //Photo05
MarkerUID: "mt9k3pw1wowu1002",
FileUID: "ft3es39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
FaceID: FaceFixtures.Get("actor-1").ID,
FaceDist: 0.5223304453393212,
SubjSrc: "",
@ -291,7 +288,7 @@ var MarkerFixtures = MarkerMap{
"actor-a-2": Marker{ //Photo02
MarkerUID: "mt9k3pw1wowu1003",
FileUID: "ft2es39q45bnlqd0",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
FaceID: FaceFixtures.Get("actor-1").ID,
FaceDist: 0.5088545446490167,
SubjSrc: "",
@ -311,7 +308,7 @@ var MarkerFixtures = MarkerMap{
"actor-a-3": Marker{ //Photo10
MarkerUID: "mt9k3pw1wowu1004",
FileUID: "fikjs39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
FaceID: FaceFixtures.Get("actor-1").ID,
FaceDist: 0.3139983399779298,
SubjSrc: "",
@ -331,7 +328,7 @@ var MarkerFixtures = MarkerMap{
"actor-a-4": Marker{ //19800101_000002_D640C559
MarkerUID: "mt9k3pw1wowu1005",
FileUID: "ft8es39w45bnlqdw",
FileHash: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
Thumb: "pcad9168fa6acc5c5c2965ddf6ec465ca42fd818",
FaceID: FaceFixtures.Get("actor-1").ID,
FaceDist: 0.3139983399779298,
SubjSrc: "",

View file

@ -19,8 +19,6 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
UID string
FileUID string
FileHash string
CropArea string
Type string
Src string
Name string
@ -36,12 +34,11 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
Q int `json:",omitempty"`
Size int `json:",omitempty"`
Score int `json:",omitempty"`
Thumb string
CreatedAt time.Time
}{
UID: m.MarkerUID,
FileUID: m.FileUID,
FileHash: m.FileHash,
CropArea: m.CropArea,
Type: m.MarkerType,
Src: m.MarkerSrc,
Name: name,
@ -57,6 +54,7 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
Q: m.Q,
Size: m.Size,
Score: m.Score,
Thumb: m.Thumb,
CreatedAt: m.CreatedAt,
})
}

View file

@ -22,12 +22,14 @@ func TestMarker_TableName(t *testing.T) {
}
func TestNewMarker(t *testing.T) {
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel)
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 100, 29)
assert.IsType(t, &Marker{}, m)
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
assert.Equal(t, "2cad9168fa6acc5c5c2965ddf6ec465ca42fd818", m.FileHash)
assert.Equal(t, "1340ce163163", m.CropArea)
assert.Equal(t, "2cad9168fa6acc5c5c2965ddf6ec465ca42fd818-1340ce163163", m.Thumb)
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjUID)
assert.True(t, m.MarkerReview)
assert.Equal(t, 119, m.Q)
assert.Equal(t, 29, m.Score)
assert.Equal(t, SrcImage, m.MarkerSrc)
assert.Equal(t, MarkerLabel, m.MarkerType)
}
@ -93,7 +95,7 @@ func TestMarker_SaveForm(t *testing.T) {
func TestUpdateOrCreateMarker(t *testing.T) {
t.Run("success", func(t *testing.T) {
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel)
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel, 100, 65)
assert.IsType(t, &Marker{}, m)
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjUID)
@ -118,7 +120,7 @@ func TestUpdateOrCreateMarker(t *testing.T) {
func TestMarker_Updates(t *testing.T) {
t.Run("success", func(t *testing.T) {
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel)
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
m, err := UpdateOrCreateMarker(m)
if err != nil {
@ -143,7 +145,7 @@ func TestMarker_Updates(t *testing.T) {
func TestMarker_Update(t *testing.T) {
t.Run("success", func(t *testing.T) {
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel)
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
m, err := UpdateOrCreateMarker(m)
if err != nil {
@ -167,7 +169,7 @@ func TestMarker_Update(t *testing.T) {
func TestMarker_Save(t *testing.T) {
t.Run("success", func(t *testing.T) {
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel)
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c4", SrcImage, MarkerLabel, 100, 65)
m, err := UpdateOrCreateMarker(m)
@ -200,8 +202,6 @@ func TestMarker_Save(t *testing.T) {
assert.Empty(t, p.Files)
p.PreloadFiles()
assert.NotEmpty(t, p.Files)
// t.Logf("FILES: %#v", p.Files)
})
t.Run("invalid position", func(t *testing.T) {
m := Marker{X: 0, Y: 0}
@ -431,7 +431,7 @@ func TestMarker_Subject(t *testing.T) {
func TestMarker_GetFace(t *testing.T) {
t.Run("ExistingFaceID", func(t *testing.T) {
m := Marker{face: &Face{ID: "1234"}, FaceID: "1234"}
m := Marker{MarkerUID: "mqzop6s14ahkyd24", FaceID: "1234", face: &Face{ID: "1234"}}
if f := m.Face(); f == nil {
t.Fatal("return value must not be nil")
@ -441,7 +441,7 @@ func TestMarker_GetFace(t *testing.T) {
}
})
t.Run("ConflictingFaceID", func(t *testing.T) {
m := Marker{face: &Face{ID: "1234"}, FaceID: "8888"}
m := Marker{MarkerUID: "mqzop6s14ahkyd24", FaceID: "8888", face: &Face{ID: "1234"}}
if f := m.Face(); f != nil {
t.Fatal("return value must be nil")
@ -451,7 +451,7 @@ func TestMarker_GetFace(t *testing.T) {
}
})
t.Run("find face with ID", func(t *testing.T) {
m := Marker{FaceID: "VF7ANLDET2BKZNT4VQWJMMC6HBEFDOG6"}
m := Marker{MarkerUID: "mqzop6s14ahkyd24", FaceID: "VF7ANLDET2BKZNT4VQWJMMC6HBEFDOG6"}
if f := m.Face(); f == nil {
t.Fatal("return value must not be nil")
@ -460,12 +460,13 @@ func TestMarker_GetFace(t *testing.T) {
}
})
t.Run("low quality marker", func(t *testing.T) {
m := Marker{FaceID: "", SubjSrc: SrcManual, Size: 130}
m := Marker{MarkerUID: "", FaceID: "", SubjSrc: SrcManual, Size: 130}
assert.Nil(t, m.Face())
})
t.Run("create face", func(t *testing.T) {
m := Marker{
MarkerUID: "mqzop6s14ahkyd24",
FaceID: "",
EmbeddingsJSON: MarkerFixtures.Get("actress-a-1").EmbeddingsJSON,
SubjSrc: SrcManual,
@ -522,7 +523,6 @@ func TestMarker_SetFace(t *testing.T) {
assert.True(t, updated2)
assert.Empty(t, m.FaceID)
})
}
func TestMarker_RefreshPhotos(t *testing.T) {

View file

@ -15,9 +15,9 @@ var cropArea4 = crop.Area{Name: "face", X: 0.298133, Y: 0.216944, W: 0.255556, H
func TestMarkers_Contains(t *testing.T) {
t.Run("Examples", func(t *testing.T) {
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace)
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea2, "lt9k3pw1wowuy1c2", SrcImage, MarkerFace)
m3 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace)
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace, 100, 65)
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea2, "lt9k3pw1wowuy1c2", SrcImage, MarkerFace, 100, 65)
m3 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace, 100, 65)
m := Markers{m1}
@ -28,11 +28,11 @@ func TestMarkers_Contains(t *testing.T) {
file := File{FileHash: "cca7c46a4d39e933c30805e546028fe3eab361b5"}
markers := Markers{
*NewMarker(file, crop.Area{Name: "subj-1", X: 0.549479, Y: 0.179688, W: 0.393229, H: 0.294922}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace),
*NewMarker(file, crop.Area{Name: "subj-2", X: 0.0833333, Y: 0.321289, W: 0.476562, H: 0.357422}, "jqyzml91cf2yyfi7", SrcImage, MarkerFace),
*NewMarker(file, crop.Area{Name: "subj-1", X: 0.549479, Y: 0.179688, W: 0.393229, H: 0.294922}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65),
*NewMarker(file, crop.Area{Name: "subj-2", X: 0.0833333, Y: 0.321289, W: 0.476562, H: 0.357422}, "jqyzml91cf2yyfi7", SrcImage, MarkerFace, 100, 65),
}
conflicting := *NewMarker(file, crop.Area{Name: "subj-2", X: 0.190104, Y: 0.40918, W: 0.31901, H: 0.239258}, "jqyzml91cf2yyfi7", SrcImage, MarkerFace)
conflicting := *NewMarker(file, crop.Area{Name: "subj-2", X: 0.190104, Y: 0.40918, W: 0.31901, H: 0.239258}, "jqyzml91cf2yyfi7", SrcImage, MarkerFace, 100, 65)
assert.True(t, markers.Contains(conflicting))
})
@ -40,10 +40,10 @@ func TestMarkers_Contains(t *testing.T) {
file := File{FileHash: "a6c46e43b83fc02309b1c49e1ed7273f1f414610"}
markers := Markers{
*NewMarker(file, crop.Area{Name: "subj-1", X: 0.388021, Y: 0.365234, W: 0.179688, H: 0.134766}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace),
*NewMarker(file, crop.Area{Name: "subj-1", X: 0.388021, Y: 0.365234, W: 0.179688, H: 0.134766}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65),
}
conflicting := *NewMarker(file, crop.Area{Name: "subj-1", X: 0.34375, Y: 0.291992, W: 0.266927, H: 0.200195}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace)
conflicting := *NewMarker(file, crop.Area{Name: "subj-1", X: 0.34375, Y: 0.291992, W: 0.266927, H: 0.200195}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65)
assert.True(t, markers.Contains(conflicting))
})
@ -51,19 +51,19 @@ func TestMarkers_Contains(t *testing.T) {
file := File{FileHash: "243cdbe99b865607f98a951e748d528bc22f3143"}
markers := Markers{
*NewMarker(file, crop.Area{Name: "no-face", X: 0.322656, Y: 0.3, W: 0.180469, H: 0.240625}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace),
*NewMarker(file, crop.Area{Name: "no-face", X: 0.322656, Y: 0.3, W: 0.180469, H: 0.240625}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65),
}
conflicting := *NewMarker(file, crop.Area{Name: "face", X: 0.325, Y: 0.0510417, W: 0.136719, H: 0.182292}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace)
conflicting := *NewMarker(file, crop.Area{Name: "face", X: 0.325, Y: 0.0510417, W: 0.136719, H: 0.182292}, "jqyzmgbquh1msz6o", SrcImage, MarkerFace, 100, 65)
assert.False(t, markers.Contains(conflicting))
})
}
func TestMarkers_FaceCount(t *testing.T) {
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace)
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea4, "lt9k3pw1wowuy1c2", SrcImage, MarkerFace)
m3 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace)
m1 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea1, "lt9k3pw1wowuy1c1", SrcImage, MarkerFace, 100, 65)
m2 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea4, "lt9k3pw1wowuy1c2", SrcImage, MarkerFace, 100, 65)
m3 := *NewMarker(FileFixtures.Get("exampleFileName.jpg"), cropArea3, "lt9k3pw1wowuy1c3", SrcImage, MarkerFace, 100, 65)
m3.MarkerInvalid = true
m := Markers{m1, m2, m3}

View file

@ -21,8 +21,6 @@ type Subjects []Subject
// Subject represents a named photo subject, typically a person.
type Subject struct {
SubjUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
MarkerUID string `gorm:"type:VARBINARY(42);index" json:"MarkerUID" yaml:"MarkerUID,omitempty"`
MarkerSrc string `gorm:"type:VARBINARY(8);default:''" json:"MarkerSrc,omitempty" yaml:"MarkerSrc,omitempty"`
SubjType string `gorm:"type:VARBINARY(8);default:''" json:"Type,omitempty" yaml:"Type,omitempty"`
SubjSrc string `gorm:"type:VARBINARY(8);default:''" json:"Src,omitempty" yaml:"Src,omitempty"`
SubjSlug string `gorm:"type:VARBINARY(255);index;default:''" json:"Slug" yaml:"-"`
@ -34,6 +32,8 @@ type Subject struct {
SubjPrivate bool `gorm:"default:false" json:"Private" yaml:"Private,omitempty"`
SubjExcluded bool `gorm:"default:false" json:"Excluded" yaml:"Excluded,omitempty"`
FileCount int `gorm:"default:0" json:"FileCount" yaml:"-"`
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb" yaml:"Thumb,omitempty"`
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
MetadataJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"Metadata,omitempty" yaml:"Metadata,omitempty"`
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`

View file

@ -170,13 +170,13 @@ func UpdateSubjectPreviews() (err error) {
Error */
err = Db().Table(entity.Subject{}.TableName()).
UpdateColumn("marker_uid", gorm.Expr("(SELECT m.marker_uid FROM "+
UpdateColumn("thumb", gorm.Expr("(SELECT m.thumb FROM "+
fmt.Sprintf(
"%s m WHERE m.subj_uid = %s.subj_uid ",
entity.Marker{}.TableName(),
entity.Subject{}.TableName())+
` AND m.file_hash <> '' ORDER BY m.subj_src DESC, m.q DESC LIMIT 1)
WHERE marker_src = '' AND deleted_at IS NULL`)).
` AND m.thumb <> '' ORDER BY m.subj_src DESC, m.q DESC LIMIT 1)
WHERE (thumb_src = '' OR thumb_src IS NULL) AND deleted_at IS NULL`)).
Error
/** err = Db().Table(entity.Subject{}.TableName()).

View file

@ -10,7 +10,7 @@ type Album struct {
AlbumUID string `json:"UID"`
ParentUID string `json:"ParentUID"`
Thumb string `json:"Thumb"`
ThumbSrc string `json:"ThumbSrc"`
ThumbSrc string `json:"ThumbSrc,omitempty"`
AlbumSlug string `json:"Slug"`
AlbumType string `json:"Type"`
AlbumTitle string `json:"Title"`

View file

@ -9,7 +9,7 @@ type Label struct {
ID uint `json:"ID"`
LabelUID string `json:"UID"`
Thumb string `json:"Thumb"`
ThumbSrc string `json:"ThumbSrc"`
ThumbSrc string `json:"ThumbSrc,omitempty"`
LabelSlug string `json:"Slug"`
CustomSlug string `json:"CustomSlug"`
LabelName string `json:"Name"`

View file

@ -19,10 +19,7 @@ func Subjects(f form.SubjectSearch) (results SubjectResults, err error) {
// Base query.
s := UnscopedDb().Table(entity.Subject{}.TableName()).
Select(fmt.Sprintf("%s.*, m.file_hash, m.crop_area", entity.Subject{}.TableName()))
// Join markers table for face thumbs.
s = s.Joins(fmt.Sprintf("LEFT JOIN %s m ON m.marker_uid = %s.marker_uid", entity.Marker{}.TableName(), entity.Subject{}.TableName()))
Select(fmt.Sprintf("%s.*", entity.Subject{}.TableName()))
// Limit result count.
if f.Count > 0 && f.Count <= MaxResults {

View file

@ -13,8 +13,8 @@ type Subject struct {
SubjPrivate bool `json:"Private"`
SubjExcluded bool `json:"Excluded"`
FileCount int `json:"FileCount"`
FileHash string `json:"FileHash"`
CropArea string `json:"CropArea"`
Thumb string `json:"Thumb"`
ThumbSrc string `json:"ThumbSrc,omitempty"`
}
// SubjectResults represents subject search results.

View file

@ -65,7 +65,6 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
// Thumbnails and downloads.
api.GetThumb(v1)
api.GetThumbCrop(v1)
api.GetDownload(v1)
api.GetVideo(v1)
api.CreateZip(v1)