People: Optimize face crop quality by using larger thumbs #22
This commit is contained in:
parent
6d1179dc03
commit
cdde0c5d84
2 changed files with 91 additions and 22 deletions
|
@ -69,7 +69,7 @@ func indexAction(ctx *cli.Context) error {
|
|||
if w := service.Index(); w != nil {
|
||||
opt := photoprism.IndexOptions{
|
||||
Path: subPath,
|
||||
Rescan: ctx.Bool("all"),
|
||||
Rescan: ctx.Bool("force"),
|
||||
Convert: conf.Settings().Index.Convert && conf.SidecarWritable(),
|
||||
Stack: true,
|
||||
}
|
||||
|
|
|
@ -13,33 +13,49 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
// FromThumb returns a cropped area from an existing thumbnail image.
|
||||
func FromThumb(thumbName string, area Area, size Size, cache bool) (img image.Image, err error) {
|
||||
// Use same folder for caching if "cache" is true.
|
||||
cacheFolder := filepath.Dir(thumbName)
|
||||
// Filenames of usable thumb sizes.
|
||||
var thumbFileNames = []string{
|
||||
"%s_720x720_fit.jpg",
|
||||
"%s_1280x1024_fit.jpg",
|
||||
"%s_1920x1200_fit.jpg",
|
||||
"%s_2048x2048_fit.jpg",
|
||||
"%s_4096x4096_fit.jpg",
|
||||
"%s_7680x4320_fit.jpg",
|
||||
}
|
||||
|
||||
// Use existing thumbnail name as cached crop filename prefix.
|
||||
thumbBase := filepath.Base(thumbName)
|
||||
if i := strings.Index(thumbBase, "_"); i > 0 {
|
||||
thumbBase = thumbBase[:i]
|
||||
}
|
||||
// Usable thumb file sizes.
|
||||
var thumbFileSizes = []thumb.Size{
|
||||
thumb.Sizes[thumb.Fit720],
|
||||
thumb.Sizes[thumb.Fit1280],
|
||||
thumb.Sizes[thumb.Fit1920],
|
||||
thumb.Sizes[thumb.Fit2048],
|
||||
thumb.Sizes[thumb.Fit4096],
|
||||
thumb.Sizes[thumb.Fit7680],
|
||||
}
|
||||
|
||||
// FromThumb returns a cropped area from an existing thumbnail image.
|
||||
func FromThumb(fileName string, area Area, size Size, cache bool) (img image.Image, err error) {
|
||||
// Use same folder for caching if "cache" is true.
|
||||
filePath := filepath.Dir(fileName)
|
||||
|
||||
// Extract hash from file name.
|
||||
hash := thumbHash(fileName)
|
||||
|
||||
// Compose cached crop image file name.
|
||||
cacheBase := fmt.Sprintf("%s_%dx%d_crop_%s", thumbBase, size.Width, size.Height, area.String())
|
||||
cropFile := filepath.Join(cacheFolder, cacheBase+fs.JpegExt)
|
||||
cropBase := fmt.Sprintf("%s_%dx%d_crop_%s%s", hash, size.Width, size.Height, area.String(), fs.JpegExt)
|
||||
cropName := filepath.Join(filePath, cropBase)
|
||||
|
||||
// Cached?
|
||||
if !fs.FileExists(cropFile) {
|
||||
if !fs.FileExists(cropName) {
|
||||
// Do nothing.
|
||||
} else if img, err := imaging.Open(cropFile); err != nil {
|
||||
log.Errorf("crop: failed loading %s", filepath.Base(cropFile))
|
||||
} else if img, err := imaging.Open(cropName); err != nil {
|
||||
log.Errorf("crop: failed loading %s", filepath.Base(cropName))
|
||||
} else {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// Open image.
|
||||
imageBuffer, err := ioutil.ReadFile(thumbName)
|
||||
img, err = imaging.Decode(bytes.NewReader(imageBuffer), imaging.AutoOrientation(true))
|
||||
// Open thumb image file.
|
||||
img, err = openIdealThumbFile(fileName, hash, area, size)
|
||||
|
||||
if err != nil {
|
||||
return img, err
|
||||
|
@ -49,7 +65,7 @@ func FromThumb(thumbName string, area Area, size Size, cache bool) (img image.Im
|
|||
min, max, dim := area.Bounds(img)
|
||||
|
||||
if dim < size.Width {
|
||||
log.Debugf("crop: %s too small, crop size %dpx, actual size %dpx", filepath.Base(thumbName), size.Width, dim)
|
||||
log.Debugf("crop: %s too small, crop size %dpx, actual size %dpx", filepath.Base(fileName), size.Width, dim)
|
||||
}
|
||||
|
||||
// Crop area from image.
|
||||
|
@ -60,12 +76,65 @@ func FromThumb(thumbName string, area Area, size Size, cache bool) (img image.Im
|
|||
|
||||
// Cache crop image?
|
||||
if cache {
|
||||
if err := imaging.Save(img, cropFile); err != nil {
|
||||
log.Errorf("crop: failed caching %s", filepath.Base(cropFile))
|
||||
if err := imaging.Save(img, cropName); err != nil {
|
||||
log.Errorf("crop: failed caching %s", filepath.Base(cropName))
|
||||
} else {
|
||||
log.Debugf("crop: saved %s", filepath.Base(cropFile))
|
||||
log.Debugf("crop: saved %s", filepath.Base(cropName))
|
||||
}
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// thumbHash returns the thumb filename base without extension and size.
|
||||
func thumbHash(fileName string) (base string) {
|
||||
base = filepath.Base(fileName)
|
||||
|
||||
// Example: 01244519acf35c62a5fea7a5a7dcefdbec4fb2f5_1280x1024_fit.jpg
|
||||
i := strings.Index(base, "_")
|
||||
|
||||
if i <= 0 {
|
||||
return fs.StripExt(base)
|
||||
}
|
||||
|
||||
return base[:i]
|
||||
}
|
||||
|
||||
// idealThumbFileName returns the filename of the ideal thumb size for the given width.
|
||||
func idealThumbFileName(fileName, hash string, width int) string {
|
||||
filePath := filepath.Dir(fileName)
|
||||
|
||||
for i, s := range thumbFileSizes {
|
||||
if s.Width < width {
|
||||
continue
|
||||
}
|
||||
|
||||
name := filepath.Join(filePath, fmt.Sprintf(thumbFileNames[i], hash))
|
||||
|
||||
if fs.FileExists(name) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
return fileName
|
||||
}
|
||||
|
||||
// openIdealThumbFile opens the thumbnail file and returns an image.
|
||||
func openIdealThumbFile(fileName, hash string, area Area, size Size) (image.Image, error) {
|
||||
if len(hash) != 40 || area.W <= 0 || size.Width <= 0 {
|
||||
// Not a standard thumb name with sha1 hash prefix.
|
||||
if imageBuffer, err := ioutil.ReadFile(fileName); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return imaging.Decode(bytes.NewReader(imageBuffer), imaging.AutoOrientation(true))
|
||||
}
|
||||
}
|
||||
|
||||
minWidth := int(float32(size.Width) / area.W)
|
||||
|
||||
if imageBuffer, err := ioutil.ReadFile(idealThumbFileName(fileName, hash, minWidth)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return imaging.Decode(bytes.NewReader(imageBuffer))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue