Added computed aesthetic scores, closes #141, closes #122

This commit is contained in:
Rhet Turnbull
2020-06-14 08:09:37 -07:00
parent 435868a0a7
commit 937da9e617
9 changed files with 425 additions and 9 deletions

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.29.18"
__version__ = "0.29.19"

View File

@@ -6,4 +6,5 @@ PhotosDB.photos() returns a list of PhotoInfo objects
from ._photoinfo_exifinfo import ExifInfo
from ._photoinfo_export import ExportResults
from ._photoinfo_scoreinfo import ScoreInfo
from .photoinfo import PhotoInfo

View File

@@ -0,0 +1,119 @@
""" PhotoInfo methods to expose computed score info from the library """
import logging
from dataclasses import dataclass
from .._constants import _PHOTOS_4_VERSION
@dataclass(frozen=True)
class ScoreInfo:
""" Computed photo score info associated with a photo from the Photos library """
overall: float
curation: float
promotion: float
highlight_visibility: float
behavioral: float
failure: float
harmonious_color: float
immersiveness: float
interaction: float
interesting_subject: float
intrusive_object_presence: float
lively_color: float
low_light: float
noise: float
pleasant_camera_tilt: float
pleasant_composition: float
pleasant_lighting: float
pleasant_pattern: float
pleasant_perspective: float
pleasant_post_processing: float
pleasant_reflection: float
pleasant_symmetry: float
sharply_focused_subject: float
tastefully_blurred: float
well_chosen_subject: float
well_framed_subject: float
well_timed_shot: float
@property
def score(self):
""" Computed score information for a photo
Returns:
ScoreInfo instance
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
logging.debug(f"score not implemented for this database version")
return None
try:
return self._scoreinfo # pylint: disable=access-member-before-definition
except AttributeError:
try:
scores = self._db._db_scoreinfo_uuid[self.uuid]
self._scoreinfo = ScoreInfo(
overall=scores["overall_aesthetic"],
curation=scores["curation"],
promotion=scores["promotion"],
highlight_visibility=scores["highlight_visibility"],
behavioral=scores["behavioral"],
failure=scores["failure"],
harmonious_color=scores["harmonious_color"],
immersiveness=scores["immersiveness"],
interaction=scores["interaction"],
interesting_subject=scores["interesting_subject"],
intrusive_object_presence=scores["intrusive_object_presence"],
lively_color=scores["lively_color"],
low_light=scores["low_light"],
noise=scores["noise"],
pleasant_camera_tilt=scores["pleasant_camera_tilt"],
pleasant_composition=scores["pleasant_composition"],
pleasant_lighting=scores["pleasant_lighting"],
pleasant_pattern=scores["pleasant_pattern"],
pleasant_perspective=scores["pleasant_perspective"],
pleasant_post_processing=scores["pleasant_post_processing"],
pleasant_reflection=scores["pleasant_reflection"],
pleasant_symmetry=scores["pleasant_symmetry"],
sharply_focused_subject=scores["sharply_focused_subject"],
tastefully_blurred=scores["tastefully_blurred"],
well_chosen_subject=scores["well_chosen_subject"],
well_framed_subject=scores["well_framed_subject"],
well_timed_shot=scores["well_timed_shot"],
)
return self._scoreinfo
except KeyError:
self._scoreinfo = ScoreInfo(
overall=0.0,
curation=0.0,
promotion=0.0,
highlight_visibility=0.0,
behavioral=0.0,
failure=0.0,
harmonious_color=0.0,
immersiveness=0.0,
interaction=0.0,
interesting_subject=0.0,
intrusive_object_presence=0.0,
lively_color=0.0,
low_light=0.0,
noise=0.0,
pleasant_camera_tilt=0.0,
pleasant_composition=0.0,
pleasant_lighting=0.0,
pleasant_pattern=0.0,
pleasant_perspective=0.0,
pleasant_post_processing=0.0,
pleasant_reflection=0.0,
pleasant_symmetry=0.0,
sharply_focused_subject=0.0,
tastefully_blurred=0.0,
well_chosen_subject=0.0,
well_framed_subject=0.0,
well_timed_shot=0.0,
)
return self._scoreinfo

View File

@@ -55,6 +55,7 @@ class PhotoInfo:
_xmp_sidecar,
ExportResults,
)
from ._photoinfo_scoreinfo import score, ScoreInfo
def __init__(self, db=None, uuid=None, info=None):
self._uuid = uuid
@@ -664,6 +665,8 @@ class PhotoInfo:
date_modified_iso = (
self.date_modified.isoformat() if self.date_modified else None
)
exif = str(self.exif_info) if self.exif_info else None
score = str(self.score) if self.score else None
info = {
"uuid": self.uuid,
@@ -704,6 +707,9 @@ class PhotoInfo:
"has_raw": self.has_raw,
"uti_raw": self.uti_raw,
"path_raw": self.path_raw,
"place": self.place,
"exif": exif,
"score": score,
}
return yaml.dump(info, sort_keys=False)
@@ -716,6 +722,7 @@ class PhotoInfo:
folders = {album.title: album.folder_names for album in self.album_info}
exif = dataclasses.asdict(self.exif_info) if self.exif_info else {}
place = self.place.as_dict() if self.place else {}
score = dataclasses.asdict(self.score) if self.score else {}
pic = {
"uuid": self.uuid,
@@ -761,6 +768,7 @@ class PhotoInfo:
"path_raw": self.path_raw,
"place": place,
"exif": exif,
"score": score,
}
return json.dumps(pic)

View File

@@ -0,0 +1,145 @@
""" Methods for PhotosDB to add Photos 5 photo score info
ref: https://simonwillison.net/2020/May/21/dogsheep-photos/
"""
import logging
from .._constants import _PHOTOS_4_VERSION
from ..utils import _open_sql_file
"""
This module should be imported in the class defintion of PhotosDB in photosdb.py
Do not import this module directly
This module adds the following method to PhotosDB:
_process_scoreinfo: process photo score info
The following data structures are added to PhotosDB
self._db_scoreinfo_uuid
These methods only work on Photos 5 databases. Will print warning on earlier library versions.
"""
def _process_scoreinfo(self):
""" Process computed photo scores
Note: Only works on Photos version == 5.0
"""
# _db_scoreinfo_uuid is dict in form {uuid: {score values}}
self._db_scoreinfo_uuid = {}
if self._db_version <= _PHOTOS_4_VERSION:
raise NotImplementedError(
f"search info not implemented for this database version"
)
else:
_process_scoreinfo_5(self)
def _process_scoreinfo_5(photosdb):
""" Process computed photo scores for Photos 5 databases
Args:
photosdb: an OSXPhotosDB instance
"""
db = photosdb._tmp_db
(conn, cursor) = _open_sql_file(db)
result = cursor.execute(
"""
SELECT
ZGENERICASSET.ZUUID,
ZGENERICASSET.ZOVERALLAESTHETICSCORE,
ZGENERICASSET.ZCURATIONSCORE,
ZGENERICASSET.ZPROMOTIONSCORE,
ZGENERICASSET.ZHIGHLIGHTVISIBILITYSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZBEHAVIORALSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZFAILURESCORE,
ZCOMPUTEDASSETATTRIBUTES.ZHARMONIOUSCOLORSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZIMMERSIVENESSSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZINTERACTIONSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZINTERESTINGSUBJECTSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZINTRUSIVEOBJECTPRESENCESCORE,
ZCOMPUTEDASSETATTRIBUTES.ZLIVELYCOLORSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZLOWLIGHT,
ZCOMPUTEDASSETATTRIBUTES.ZNOISESCORE,
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCAMERATILTSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCOMPOSITIONSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTLIGHTINGSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPATTERNSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPERSPECTIVESCORE,
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPOSTPROCESSINGSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTREFLECTIONSSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTSYMMETRYSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZSHARPLYFOCUSEDSUBJECTSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZTASTEFULLYBLURREDSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZWELLCHOSENSUBJECTSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZWELLFRAMEDSUBJECTSCORE,
ZCOMPUTEDASSETATTRIBUTES.ZWELLTIMEDSHOTSCORE
FROM ZGENERICASSET
JOIN ZCOMPUTEDASSETATTRIBUTES ON ZCOMPUTEDASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK
"""
)
# 0 ZGENERICASSET.ZUUID,
# 1 ZGENERICASSET.ZOVERALLAESTHETICSCORE,
# 2 ZGENERICASSET.ZCURATIONSCORE,
# 3 ZGENERICASSET.ZPROMOTIONSCORE,
# 4 ZGENERICASSET.ZHIGHLIGHTVISIBILITYSCORE,
# 5 ZCOMPUTEDASSETATTRIBUTES.ZBEHAVIORALSCORE,
# 6 ZCOMPUTEDASSETATTRIBUTES.ZFAILURESCORE,
# 7 ZCOMPUTEDASSETATTRIBUTES.ZHARMONIOUSCOLORSCORE,
# 8 ZCOMPUTEDASSETATTRIBUTES.ZIMMERSIVENESSSCORE,
# 9 ZCOMPUTEDASSETATTRIBUTES.ZINTERACTIONSCORE,
# 10 ZCOMPUTEDASSETATTRIBUTES.ZINTERESTINGSUBJECTSCORE,
# 11 ZCOMPUTEDASSETATTRIBUTES.ZINTRUSIVEOBJECTPRESENCESCORE,
# 12 ZCOMPUTEDASSETATTRIBUTES.ZLIVELYCOLORSCORE,
# 13 ZCOMPUTEDASSETATTRIBUTES.ZLOWLIGHT,
# 14 ZCOMPUTEDASSETATTRIBUTES.ZNOISESCORE,
# 15 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCAMERATILTSCORE,
# 16 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTCOMPOSITIONSCORE,
# 17 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTLIGHTINGSCORE,
# 18 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPATTERNSCORE,
# 19 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPERSPECTIVESCORE,
# 20 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTPOSTPROCESSINGSCORE,
# 21 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTREFLECTIONSSCORE,
# 22 ZCOMPUTEDASSETATTRIBUTES.ZPLEASANTSYMMETRYSCORE,
# 23 ZCOMPUTEDASSETATTRIBUTES.ZSHARPLYFOCUSEDSUBJECTSCORE,
# 24 ZCOMPUTEDASSETATTRIBUTES.ZTASTEFULLYBLURREDSCORE,
# 25 ZCOMPUTEDASSETATTRIBUTES.ZWELLCHOSENSUBJECTSCORE,
# 26 ZCOMPUTEDASSETATTRIBUTES.ZWELLFRAMEDSUBJECTSCORE,
# 27 ZCOMPUTEDASSETATTRIBUTES.ZWELLTIMEDSHOTSCORE
for row in result:
uuid = row[0]
scores = {"uuid": uuid}
scores["overall_aesthetic"] = row[1]
scores["curation"] = row[2]
scores["promotion"] = row[3]
scores["highlight_visibility"] = row[4]
scores["behavioral"] = row[5]
scores["failure"] = row[6]
scores["harmonious_color"] = row[7]
scores["immersiveness"] = row[8]
scores["interaction"] = row[9]
scores["interesting_subject"] = row[10]
scores["intrusive_object_presence"] = row[11]
scores["lively_color"] = row[12]
scores["low_light"] = row[13]
scores["noise"] = row[14]
scores["pleasant_camera_tilt"] = row[15]
scores["pleasant_composition"] = row[16]
scores["pleasant_lighting"] = row[17]
scores["pleasant_pattern"] = row[18]
scores["pleasant_perspective"] = row[19]
scores["pleasant_post_processing"] = row[20]
scores["pleasant_reflection"] = row[21]
scores["pleasant_symmetry"] = row[22]
scores["sharply_focused_subject"] = row[23]
scores["tastefully_blurred"] = row[24]
scores["well_chosen_subject"] = row[25]
scores["well_framed_subject"] = row[26]
scores["well_timed_shot"] = row[27]
photosdb._db_scoreinfo_uuid[uuid] = scores

View File

@@ -102,7 +102,7 @@ def _process_searchinfo(self):
# 8: groups.lookup_identifier
for row in c:
uuid = ints_to_uuid(row[1],row[2])
uuid = ints_to_uuid(row[1], row[2])
# strings have null character appended, so strip it
record = {}
record["uuid"] = uuid
@@ -123,13 +123,9 @@ def _process_searchinfo(self):
category = record["category"]
try:
_db_searchinfo_categories[category].append(
record["normalized_string"]
)
_db_searchinfo_categories[category].append(record["normalized_string"])
except KeyError:
_db_searchinfo_categories[category] = [
record["normalized_string"]
]
_db_searchinfo_categories[category] = [record["normalized_string"]]
if category == SEARCH_CATEGORY_LABEL:
label = record["content_string"]
@@ -198,6 +194,7 @@ def labels_normalized_as_dict(self):
# The following method is not imported into PhotosDB
@lru_cache(maxsize=128)
def ints_to_uuid(uuid_0, uuid_1):
""" convert two signed ints into a UUID strings

View File

@@ -64,6 +64,7 @@ class PhotosDB:
labels_as_dict,
labels_normalized_as_dict,
)
from ._photosdb_process_scoreinfo import _process_scoreinfo
def __init__(self, *dbfile_, dbfile=None):
""" create a new PhotosDB object
@@ -1862,6 +1863,9 @@ class PhotosDB:
# process exif info
self._process_exifinfo()
# process computed scores
self._process_scoreinfo()
# done processing, dump debug data if requested
if _debug():
logging.debug("Faces (_dbfaces_uuid):")