Implement score to sort photos by quality #288

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-04-24 13:21:18 +02:00
parent e55df7ed37
commit 77cea5d719
14 changed files with 136 additions and 43 deletions

View file

@ -155,6 +155,7 @@
{value: 'newest', text: this.$gettext('Newest first')},
{value: 'oldest', text: this.$gettext('Oldest first')},
{value: 'similar', text: this.$gettext('Similar')},
{value: 'relevance', text: this.$gettext('Relevance')},
],
},
labels: {

View file

@ -71,7 +71,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile :to="{name: 'photos', query: { q: 'mono:true' }}" :exact="true" @click="">
<v-list-tile :to="{name: 'photos', query: { q: 'mono:true quality:3' }}" :exact="true" @click="">
<v-list-tile-content>
<v-list-tile-title>
<translate>Monochrome</translate>
@ -79,10 +79,10 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile :to="{name: 'photos', query: { q: 'chroma:50' }}" :exact="true" @click="">
<v-list-tile :to="{name: 'photos', query: { q: 'review:true' }}" :exact="true" @click="">
<v-list-tile-content>
<v-list-tile-title>
<translate>Vibrant</translate>
<translate>Review</translate>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>

View file

@ -170,6 +170,7 @@
{value: 'newest', text: this.$gettext('Newest first')},
{value: 'oldest', text: this.$gettext('Oldest first')},
{value: 'similar', text: this.$gettext('Similar')},
{value: 'relevance', text: this.$gettext('Relevance')},
],
},
labels: {

View file

@ -25,10 +25,11 @@ class Photo extends RestModel {
PhotoTitle: "",
TitleSrc: "",
PhotoFavorite: false,
PhotoStory: false,
PhotoPrivate: false,
PhotoNSFW: false,
PhotoStory: false,
PhotoReview: false,
PhotoResolution: 0,
PhotoQuality: 0,
PhotoLat: 0.0,
PhotoLng: 0.0,
PhotoAltitude: 0,
@ -85,13 +86,13 @@ class Photo extends RestModel {
getColor() {
switch (this.PhotoColor) {
case "brown":
case "black":
case "white":
case "grey":
return "grey lighten-2";
default:
return this.PhotoColor + " lighten-4";
case "brown":
case "black":
case "white":
case "grey":
return "grey lighten-2";
default:
return this.PhotoColor + " lighten-4";
}
}

View file

@ -55,6 +55,7 @@
this.filter.year = query['year'] ? parseInt(query['year']) : 0;
this.filter.color = query['color'] ? query['color'] : '';
this.filter.label = query['label'] ? query['label'] : '';
this.filter.order = this.sortOrder();
this.settings.view = this.viewType();
this.lastFilter = {};
this.routeName = this.$route.name;
@ -64,7 +65,7 @@
data() {
const query = this.$route.query;
const routeName = this.$route.name;
const order = query['order'] ? query['order'] : 'newest';
const order = this.sortOrder();
const camera = query['camera'] ? parseInt(query['camera']) : 0;
const q = query['q'] ? query['q'] : '';
const country = query['country'] ? query['country'] : '';
@ -120,10 +121,10 @@
methods: {
viewType() {
let queryParam = this.$route.query['view'];
let storedType = window.localStorage.getItem("photo_view_type");
let storedType = window.localStorage.getItem("photo_view");
if (queryParam) {
window.localStorage.setItem("photo_view_type", queryParam);
window.localStorage.setItem("photo_view", queryParam);
return queryParam;
} else if (storedType) {
return storedType;
@ -133,6 +134,19 @@
return 'cards';
},
sortOrder() {
let queryParam = this.$route.query['order'];
let storedType = window.localStorage.getItem("photo_order");
if (queryParam) {
window.localStorage.setItem("photo_order", queryParam);
return queryParam;
} else if (storedType) {
return storedType;
}
return 'newest';
},
openLocation(index) {
const photo = this.results[index];

View file

@ -150,6 +150,7 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
}
m.PhotoFavorite = true
m.PhotoQuality = m.QualityScore()
conf.Db().Save(&m)
event.Publish("count.favorites", event.Data{
@ -183,6 +184,7 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
}
m.PhotoFavorite = false
m.PhotoQuality = m.QualityScore()
conf.Db().Save(&m)
event.Publish("count.favorites", event.Data{

View file

@ -2,18 +2,14 @@ package entity
import (
"strings"
"sync"
"time"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/maps"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/pkg/s2"
"github.com/photoprism/photoprism/pkg/txt"
)
var locationMutex = sync.Mutex{}
// Location used to associate photos to location
type Location struct {
ID string `gorm:"type:varbinary(16);primary_key;auto_increment:false;"`
@ -26,16 +22,6 @@ type Location struct {
UpdatedAt time.Time
}
// Lock location for updates
func (Location) Lock() {
locationMutex.Lock()
}
// Unlock location for updates
func (Location) Unlock() {
locationMutex.Unlock()
}
// NewLocation creates a location using a token extracted from coordinate
func NewLocation(lat, lng float64) *Location {
result := &Location{}
@ -47,9 +33,6 @@ func NewLocation(lat, lng float64) *Location {
// Find gets the location using either the db or the api if not in the db
func (m *Location) Find(db *gorm.DB, api string) error {
mutex.Db.Lock()
defer mutex.Db.Unlock()
if err := db.First(m, "id = ?", m.ID).Error; err == nil {
m.Place = FindPlace(m.PlaceID, db)
return nil
@ -77,8 +60,12 @@ func (m *Location) Find(db *gorm.DB, api string) error {
m.LocCategory = l.LocCategory
m.LocSource = l.LocSource
if err := db.Create(m).Error; err != nil {
log.Errorf("location: %s", err)
if err := db.Create(m).Error; err == nil {
return nil
} else if err := db.First(m, "id = ?", m.ID).Error; err == nil {
// avoid mutex by trying again to find location
m.Place = FindPlace(m.PlaceID, db)
} else {
return err
}

View file

@ -25,11 +25,12 @@ type Photo struct {
PhotoName string `gorm:"type:varbinary(256);"`
PhotoTitle string `json:"PhotoTitle"`
TitleSrc string `gorm:"type:varbinary(8);" json:"TitleSrc"`
PhotoQuality int `gorm:"type:SMALLINT" json:"PhotoQuality"`
PhotoResolution int `gorm:"type:SMALLINT" json:"PhotoResolution"`
PhotoFavorite bool `json:"PhotoFavorite"`
PhotoPrivate bool `json:"PhotoPrivate"`
PhotoNSFW bool `json:"PhotoNSFW"`
PhotoStory bool `json:"PhotoStory"`
PhotoReview bool `json:"PhotoReview"`
PhotoNSFW bool `json:"PhotoNSFW"`
PhotoLat float64 `gorm:"index;" json:"PhotoLat"`
PhotoLng float64 `gorm:"index;" json:"PhotoLng"`
PhotoAltitude int `json:"PhotoAltitude"`
@ -100,6 +101,8 @@ func SavePhotoForm(model Photo, form form.Photo, db *gorm.DB, geoApi string) err
log.Warnf("%s (%s)", err.Error(), model.PhotoUUID)
}
model.PhotoQuality = model.QualityScore()
return db.Unscoped().Save(&model).Error
}
@ -121,6 +124,8 @@ func (m *Photo) Save(db *gorm.DB) error {
log.Error(err)
}
m.PhotoQuality = m.QualityScore()
return db.Unscoped().Save(m).Error
}

View file

@ -46,9 +46,6 @@ func (m *Photo) GetTakenAt() time.Time {
func (m *Photo) UpdateLocation(db *gorm.DB, geoApi string) (keywords []string, labels classify.Labels) {
var location = NewLocation(m.PhotoLat, m.PhotoLng)
location.Lock()
defer location.Unlock()
err := location.Find(db, geoApi)
if err == nil {

View file

@ -0,0 +1,53 @@
package entity
import (
"strings"
"github.com/photoprism/photoprism/pkg/txt"
)
var QualityBlacklist = map[string]bool{
"screenshot": true,
"screenshots": true,
"info": true,
}
// QualityScore returns a score based on photo properties like size and metadata.
func (m *Photo) QualityScore() (score int) {
if m.PhotoFavorite {
score += 3
}
if m.TakenSrc != SrcAuto {
score++
}
if m.HasLatLng() {
score++
}
if m.PhotoResolution >= 2 {
score++
}
blacklisted := false
if m.Description.PhotoKeywords != "" {
keywords := txt.Words(m.Description.PhotoKeywords)
for _, w := range keywords {
w = strings.ToLower(w)
if _, ok := QualityBlacklist[w]; ok {
blacklisted = true
break
}
}
}
if !blacklisted {
score++
}
return score
}

View file

@ -29,6 +29,8 @@ type PhotoSearch struct {
Year uint `form:"year"`
Month uint `form:"month"`
Color string `form:"color"`
Quality int `form:"quality"`
Review bool `form:"review"`
Camera int `form:"camera"`
Lens int `form:"lens"`
Before time.Time `form:"before" time_format:"2006-01-02"`

View file

@ -2,6 +2,7 @@ package photoprism
import (
"errors"
"math"
"path/filepath"
"sort"
"strings"
@ -258,7 +259,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
}
}
if photo.Place != nil && (photo.PhotoCountry == "" || photo.PhotoCountry == "zz") {
if photo.Place == nil {
photo.Place = entity.UnknownPlace
photo.PlaceID = entity.UnknownPlace.ID
}
if photo.PhotoCountry == "" || photo.PhotoCountry == "zz" {
photo.PhotoCountry = photo.Place.LocCountry
}
@ -301,6 +307,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
file.FileHeight = m.Height()
file.FileAspectRatio = m.AspectRatio()
file.FilePortrait = m.Width() < m.Height()
megapixels := int(math.Round(float64(file.FileWidth*file.FileHeight) / 1000000))
if megapixels > photo.PhotoResolution {
photo.PhotoResolution = megapixels
}
}
}
@ -368,6 +380,8 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
log.Debug("index: no photo keywords")
}
photo.PhotoQuality = photo.QualityScore()
if err := ind.db.Unscoped().Save(&photo).Error; err != nil {
log.Errorf("index: %s", err)
result.Status = IndexFailed
@ -378,6 +392,15 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
if err := photo.IndexKeywords(ind.db); err != nil {
log.Warnf("%s (%s)", err.Error(), photo.PhotoUUID)
}
} else {
photo.PhotoQuality = photo.QualityScore()
if err := ind.db.Unscoped().Save(&photo).Error; err != nil {
log.Errorf("index: %s", err)
result.Status = IndexFailed
result.Error = err
return result
}
}
result.Status = IndexUpdated

View file

@ -42,6 +42,7 @@ func (q *Query) Geo(f form.GeoSearch) (results []GeoResult, err error) {
AND files.file_missing = 0 AND files.file_primary AND files.deleted_at IS NULL`).
Where("photos.deleted_at IS NULL").
Where("photos.photo_lat <> 0").
Where("photos.photo_quality > 2").
Group("photos.id, files.id")
if f.Query != "" {

View file

@ -33,8 +33,6 @@ type PhotoResult struct {
PhotoCountry string
PhotoFavorite bool
PhotoPrivate bool
PhotoSensitive bool
PhotoStory bool
PhotoLat float64
PhotoLng float64
PhotoAltitude int
@ -42,6 +40,8 @@ type PhotoResult struct {
PhotoFocalLength int
PhotoFNumber float64
PhotoExposure string
PhotoQuality int
PhotoResolution int
Merged bool
// Camera
@ -322,6 +322,12 @@ func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err
s = s.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma)
}
if f.Review {
s = s.Where("photos.photo_quality < 3")
} else if f.Quality != 0 {
s = s.Where("photos.photo_quality >= ?", f.Quality)
}
if f.Diff != 0 {
s = s.Where("files.file_diff = ?", f.Diff)
}
@ -363,7 +369,7 @@ func (q *Query) Photos(f form.PhotoSearch) (results PhotoResults, count int, err
switch f.Order {
case entity.SortOrderRelevance:
s = s.Order("photo_story DESC, photo_favorite DESC, taken_at DESC, files.file_primary DESC")
s = s.Order("photo_quality DESC, taken_at DESC, files.file_primary DESC")
case entity.SortOrderNewest:
s = s.Order("taken_at DESC, photos.photo_uuid, files.file_primary DESC")
case entity.SortOrderOldest: