Implement score to sort photos by quality #288
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
e55df7ed37
commit
77cea5d719
14 changed files with 136 additions and 43 deletions
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
53
internal/entity/photo_quality.go
Normal file
53
internal/entity/photo_quality.go
Normal 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
|
||||
}
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue