2021-02-15 17:35:28 +01:00
|
|
|
package models
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"database/sql/driver"
|
|
|
|
"encoding/binary"
|
2021-02-16 11:27:28 +01:00
|
|
|
"fmt"
|
|
|
|
"image"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2021-02-15 17:35:28 +01:00
|
|
|
|
|
|
|
"github.com/Kagami/go-face"
|
2021-05-06 21:54:31 +02:00
|
|
|
"github.com/photoview/photoview/api/scanner/media_encoding/media_utils"
|
2021-03-12 14:26:51 +01:00
|
|
|
"gorm.io/gorm"
|
|
|
|
"gorm.io/gorm/schema"
|
2021-02-15 17:35:28 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type FaceGroup struct {
|
|
|
|
Model
|
2021-09-18 21:21:40 +02:00
|
|
|
Label *string
|
|
|
|
ImageFaces []ImageFace `gorm:"constraint:OnDelete:CASCADE;"`
|
|
|
|
PreviewImageFace ImageFace `gorm:"constraint:OnDelete:CASCADE;"`
|
2021-02-15 17:35:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type ImageFace struct {
|
|
|
|
Model
|
2021-02-19 23:30:43 +01:00
|
|
|
FaceGroupID int `gorm:"not null;index"`
|
|
|
|
FaceGroup *FaceGroup
|
2021-02-15 17:35:28 +01:00
|
|
|
MediaID int `gorm:"not null;index"`
|
|
|
|
Media Media `gorm:"constraint:OnDelete:CASCADE;"`
|
|
|
|
Descriptor FaceDescriptor `gorm:"not null"`
|
2021-02-16 11:27:28 +01:00
|
|
|
Rectangle FaceRectangle `gorm:"not null"`
|
2021-02-15 17:35:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type FaceDescriptor face.Descriptor
|
|
|
|
|
|
|
|
// GormDataType datatype used in database
|
2021-03-12 14:26:51 +01:00
|
|
|
func (FaceDescriptor) GormDBDataType(db *gorm.DB, field *schema.Field) string {
|
|
|
|
switch db.Dialector.Name() {
|
|
|
|
case "mysql", "sqlite":
|
|
|
|
return "BLOB"
|
|
|
|
case "postgres":
|
|
|
|
return "BYTEA"
|
|
|
|
}
|
|
|
|
return ""
|
2021-02-15 17:35:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Scan tells GORM how to convert database data to Go format
|
|
|
|
func (fd *FaceDescriptor) Scan(value interface{}) error {
|
|
|
|
byteValue := value.([]byte)
|
|
|
|
reader := bytes.NewReader(byteValue)
|
|
|
|
binary.Read(reader, binary.LittleEndian, fd)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Value tells GORM how to save into the database
|
|
|
|
func (fd FaceDescriptor) Value() (driver.Value, error) {
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
if err := binary.Write(buf, binary.LittleEndian, fd); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return buf.Bytes(), nil
|
|
|
|
}
|
2021-02-16 11:27:28 +01:00
|
|
|
|
|
|
|
type FaceRectangle struct {
|
2021-02-16 12:01:10 +01:00
|
|
|
MinX, MaxX float64
|
|
|
|
MinY, MaxY float64
|
2021-02-16 11:27:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// ToDBFaceRectangle converts a pixel absolute rectangle to a relative FaceRectangle to be saved in the database
|
|
|
|
func ToDBFaceRectangle(imgRec image.Rectangle, imagePath string) (*FaceRectangle, error) {
|
2021-05-06 21:54:31 +02:00
|
|
|
size, err := media_utils.GetPhotoDimensions(imagePath)
|
2021-02-16 11:27:28 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &FaceRectangle{
|
2021-02-16 12:01:10 +01:00
|
|
|
MinX: float64(imgRec.Min.X) / float64(size.Width),
|
|
|
|
MaxX: float64(imgRec.Max.X) / float64(size.Width),
|
|
|
|
MinY: float64(imgRec.Min.Y) / float64(size.Height),
|
|
|
|
MaxY: float64(imgRec.Max.Y) / float64(size.Height),
|
2021-02-16 11:27:28 +01:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GormDataType datatype used in database
|
|
|
|
func (fr FaceRectangle) GormDataType() string {
|
|
|
|
return "VARCHAR(64)"
|
|
|
|
}
|
|
|
|
|
|
|
|
// Scan tells GORM how to convert database data to Go format
|
|
|
|
func (fr *FaceRectangle) Scan(value interface{}) error {
|
2021-03-12 14:26:51 +01:00
|
|
|
stringArray, ok := value.(string)
|
|
|
|
if !ok {
|
|
|
|
byteArray := value.([]uint8)
|
|
|
|
stringArray = string(byteArray)
|
|
|
|
}
|
|
|
|
|
|
|
|
slices := strings.Split(stringArray, ":")
|
2021-02-16 11:27:28 +01:00
|
|
|
|
|
|
|
if len(slices) != 4 {
|
|
|
|
return fmt.Errorf("Invalid face rectangle format, expected 4 values, got %d", len(slices))
|
|
|
|
}
|
|
|
|
|
2021-02-16 12:41:34 +01:00
|
|
|
var err error
|
|
|
|
|
|
|
|
fr.MinX, err = strconv.ParseFloat(slices[0], 32)
|
2021-02-16 11:27:28 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-02-16 12:41:34 +01:00
|
|
|
fr.MaxX, err = strconv.ParseFloat(slices[1], 32)
|
2021-02-16 11:27:28 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-02-16 12:41:34 +01:00
|
|
|
fr.MinY, err = strconv.ParseFloat(slices[2], 32)
|
2021-02-16 11:27:28 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-02-16 12:41:34 +01:00
|
|
|
fr.MaxY, err = strconv.ParseFloat(slices[3], 32)
|
2021-02-16 11:27:28 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Value tells GORM how to save into the database
|
|
|
|
func (fr FaceRectangle) Value() (driver.Value, error) {
|
2021-02-16 12:01:10 +01:00
|
|
|
result := fmt.Sprintf("%f:%f:%f:%f", fr.MinX, fr.MaxX, fr.MinY, fr.MaxY)
|
2021-02-16 11:27:28 +01:00
|
|
|
return result, nil
|
|
|
|
}
|