Refactored person processing to enable implementation of #181

This commit is contained in:
Rhet Turnbull
2020-07-06 00:10:22 -07:00
parent dfcbfa725a
commit fcff8ec5f8
9 changed files with 197 additions and 59 deletions

View File

@@ -520,10 +520,14 @@ def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid):
print("_dbkeywords_uuid:") print("_dbkeywords_uuid:")
pprint.pprint(photosdb._dbkeywords_uuid) pprint.pprint(photosdb._dbkeywords_uuid)
elif attr == "persons": elif attr == "persons":
print("_dbfaces_person:")
pprint.pprint(photosdb._dbfaces_person)
print("_dbfaces_uuid:") print("_dbfaces_uuid:")
pprint.pprint(photosdb._dbfaces_uuid) pprint.pprint(photosdb._dbfaces_uuid)
print("_dbfaces_pk:")
pprint.pprint(photosdb._dbfaces_pk)
print("_dbpersons_pk:")
pprint.pprint(photosdb._dbpersons_pk)
print("_dbpersons_fullname:")
pprint.pprint(photosdb._dbpersons_fullname)
elif attr == "photos": elif attr == "photos":
if uuid: if uuid:
for uuid_ in uuid: for uuid_ in uuid:

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.30.7" __version__ = "0.30.8"

View File

@@ -339,7 +339,7 @@ class PhotoInfo:
@property @property
def persons(self): def persons(self):
""" list of persons in picture """ """ list of persons in picture """
return self._info["persons"] return [self._db._dbpersons_pk[k]["fullname"] for k in self._info["persons"]]
@property @property
def albums(self): def albums(self):

View File

@@ -122,17 +122,28 @@ class PhotosDB:
# currently used to get information on RAW images # currently used to get information on RAW images
self._dbphotos_master = {} self._dbphotos_master = {}
# Dict with information about all persons by person PK
# key is person PK, value is dict with info about each person
# e.g. {3: {"pk": 3, "fullname": "Maria Smith"...}}
self._dbpersons_pk = {}
# Dict with information about all persons by person fullname
# key is person PK, value is list of person PKs with fullname
# there may be more than one person PK with the same fullname
# e.g. {"Maria Smith": [1, 2]}
self._dbpersons_fullname = {}
# Dict with information about all persons/photos by uuid # Dict with information about all persons/photos by uuid
# key is photo UUID, value is list of face names in that photo # key is photo UUID, value is list of person primary keys of persons in the photo
# Note: Photos 5 identifies faces even if not given a name # Note: Photos 5 identifies faces even if not given a name
# and those are labeled by process_database as _UNKNOWN_ # and those are labeled by process_database as _UNKNOWN_
# e.g. {'1EB2B765-0765-43BA-A90C-0D0580E6172C': ['Katie', '_UNKNOWN_', 'Suzy']} # e.g. {'1EB2B765-0765-43BA-A90C-0D0580E6172C': [1, 3, 5]}
self._dbfaces_uuid = {} self._dbfaces_uuid = {}
# Dict with information about all persons/photos by person # Dict with information about detected faces by person primary key
# key is person name, value is list of photo UUIDs # key is person pk, value is list of photo UUIDs
# e.g. {'Maria': ['E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51']} # e.g. {3: ['E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51']}
self._dbfaces_person = {} self._dbfaces_pk = {}
# Dict with information about all keywords/photos by uuid # Dict with information about all keywords/photos by uuid
# key is photo uuid and value is list of keywords # key is photo uuid and value is list of keywords
@@ -303,7 +314,13 @@ class PhotosDB:
@property @property
def persons_as_dict(self): def persons_as_dict(self):
""" return persons as dict of person, count in reverse sorted order (descending) """ """ return persons as dict of person, count in reverse sorted order (descending) """
persons = {k: len(self._dbfaces_person[k]) for k in self._dbfaces_person.keys()} persons = {}
for pk in self._dbfaces_pk:
fullname = self._dbpersons_pk[pk]["fullname"]
try:
persons[fullname] += len(self._dbfaces_pk[pk])
except KeyError:
persons[fullname] = len(self._dbfaces_pk[pk])
persons = dict(sorted(persons.items(), key=lambda kv: kv[1], reverse=True)) persons = dict(sorted(persons.items(), key=lambda kv: kv[1], reverse=True))
return persons return persons
@@ -352,7 +369,7 @@ class PhotosDB:
@property @property
def persons(self): def persons(self):
""" return list of persons found in photos database """ """ return list of persons found in photos database """
persons = self._dbfaces_person.keys() persons = {self._dbpersons_pk[k]["fullname"] for k in self._dbfaces_pk}
return list(persons) return list(persons)
@property @property
@@ -536,22 +553,77 @@ class PhotosDB:
(conn, c) = _open_sql_file(self._tmp_db) (conn, c) = _open_sql_file(self._tmp_db)
# Look for all combinations of persons and pictures # get info to associate persons with photos
# then get detected faces in each photo and link to persons
c.execute( c.execute(
""" select RKPerson.name, RKVersion.uuid from RKFace, RKPerson, RKVersion, RKMaster """ SELECT
where RKFace.personID = RKperson.modelID and RKVersion.modelId = RKFace.ImageModelId RKPerson.modelID,
and RKVersion.masterUuid = RKMaster.uuid RKPerson.uuid,
RKPerson.name,
RKPerson.faceCount,
RKPerson.displayName
FROM RKPerson
""" """
) )
# 0 RKPerson.modelID,
# 1 RKPerson.uuid,
# 2 RKPerson.name,
# 3 RKPerson.faceCount,
# 4 RKPerson.displayName
for person in c: for person in c:
if person[0] is None: pk = person[0]
continue fullname = person[2] if person[2] is not None else _UNKNOWN_PERSON
if not person[1] in self._dbfaces_uuid: self._dbpersons_pk[pk] = {
self._dbfaces_uuid[person[1]] = [] "pk": pk,
if not person[0] in self._dbfaces_person: "uuid": person[1],
self._dbfaces_person[person[0]] = [] "fullname": fullname,
self._dbfaces_uuid[person[1]].append(person[0]) "facecount": person[3],
self._dbfaces_person[person[0]].append(person[1]) "keyface": None,
"displayname": person[4],
}
try:
self._dbpersons_fullname[fullname].append(pk)
except KeyError:
self._dbpersons_fullname[fullname] = [pk]
# get information on detected faces
c.execute(
""" SELECT
RKPerson.modelID,
RKVersion.uuid
FROM
RKFace, RKPerson, RKVersion, RKMaster
WHERE
RKFace.personID = RKperson.modelID AND
RKVersion.modelId = RKFace.ImageModelId AND
RKVersion.masterUuid = RKMaster.uuid
"""
)
# 0 RKPerson.modelID
# 1 RKVersion.uuid
for face in c:
pk = face[0]
uuid = face[1]
try:
self._dbfaces_uuid[uuid].append(pk)
except KeyError:
self._dbfaces_uuid[uuid] = [pk]
try:
self._dbfaces_pk[pk].append(uuid)
except KeyError:
self._dbfaces_pk[pk] = [uuid]
if _debug():
logging.debug(f"Finished walking through persons")
logging.debug(pformat(self._dbpersons_pk))
logging.debug(pformat(self._dbpersons_fullname))
logging.debug(pformat(self._dbfaces_pk))
logging.debug(pformat(self._dbfaces_uuid))
# Get info on albums # Get info on albums
c.execute( c.execute(
@@ -1233,8 +1305,8 @@ class PhotosDB:
logging.debug("Faces (_dbfaces_uuid):") logging.debug("Faces (_dbfaces_uuid):")
logging.debug(pformat(self._dbfaces_uuid)) logging.debug(pformat(self._dbfaces_uuid))
logging.debug("Faces by person (_dbfaces_person):") logging.debug("Persons (_dbpersons_pk):")
logging.debug(pformat(self._dbfaces_person)) logging.debug(pformat(self._dbpersons_pk))
logging.debug("Keywords by uuid (_dbkeywords_uuid):") logging.debug("Keywords by uuid (_dbkeywords_uuid):")
logging.debug(pformat(self._dbkeywords_uuid)) logging.debug(pformat(self._dbkeywords_uuid))
@@ -1295,8 +1367,12 @@ class PhotosDB:
return folders return folders
def _process_database5(self): def _process_database5(self):
""" process the Photos database to extract info """ """ process the Photos database to extract info
""" works on Photos version >= 5.0 """ works on Photos version >= 5.0
This is a big hairy 700 line function that should probably be refactored
but it works so don't touch it.
"""
if _debug(): if _debug():
logging.debug(f"_process_database5") logging.debug(f"_process_database5")
@@ -1310,26 +1386,76 @@ class PhotosDB:
if _debug(): if _debug():
logging.debug(f"Getting information about persons") logging.debug(f"Getting information about persons")
# get info to associate persons with photos
# then get detected faces in each photo and link to persons
c.execute( c.execute(
"SELECT ZPERSON.ZFULLNAME, ZGENERICASSET.ZUUID " """ SELECT
"FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET " ZPERSON.Z_PK,
"WHERE ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK AND ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK " ZPERSON.ZPERSONUUID,
ZPERSON.ZFULLNAME,
ZPERSON.ZFACECOUNT,
ZPERSON.ZKEYFACE,
ZPERSON.ZDISPLAYNAME
FROM ZPERSON
"""
) )
# 0 ZPERSON.Z_PK,
# 1 ZPERSON.ZPERSONUUID,
# 2 ZPERSON.ZFULLNAME,
# 3 ZPERSON.ZFACECOUNT,
# 4 ZPERSON.ZKEYFACE,
# 5 ZPERSON.ZDISPLAYNAME,
for person in c: for person in c:
if person[0] is None: pk = person[0]
continue fullname = person[2] if person[2] != "" else _UNKNOWN_PERSON
person_name = person[0] if person[0] != "" else _UNKNOWN_PERSON self._dbpersons_pk[pk] = {
if not person[1] in self._dbfaces_uuid: "pk": pk,
self._dbfaces_uuid[person[1]] = [] "uuid": person[1],
if not person_name in self._dbfaces_person: "fullname": fullname,
self._dbfaces_person[person_name] = [] "facecount": person[3],
self._dbfaces_uuid[person[1]].append(person_name) "keyface": person[4],
self._dbfaces_person[person_name].append(person[1]) "displayname": person[5],
}
try:
self._dbpersons_fullname[fullname].append(pk)
except KeyError:
self._dbpersons_fullname[fullname] = [pk]
# get information on detected faces
c.execute(
""" SELECT
ZPERSON.Z_PK,
ZGENERICASSET.ZUUID
FROM ZPERSON, ZDETECTEDFACE, ZGENERICASSET
WHERE ZDETECTEDFACE.ZPERSON = ZPERSON.Z_PK AND
ZDETECTEDFACE.ZASSET = ZGENERICASSET.Z_PK;
"""
)
# 0 ZPERSON.Z_PK,
# 1 ZGENERICASSET.ZUUID,
for face in c:
pk = face[0]
uuid = face[1]
try:
self._dbfaces_uuid[uuid].append(pk)
except KeyError:
self._dbfaces_uuid[uuid] = [pk]
try:
self._dbfaces_pk[pk].append(uuid)
except KeyError:
self._dbfaces_pk[pk] = [uuid]
if _debug(): if _debug():
logging.debug(f"Finished walking through persons") logging.debug(f"Finished walking through persons")
logging.debug(pformat(self._dbfaces_person)) logging.debug(pformat(self._dbpersons_pk))
logging.debug(self._dbfaces_uuid) logging.debug(pformat(self._dbpersons_fullname))
logging.debug(pformat(self._dbfaces_pk))
logging.debug(pformat(self._dbfaces_uuid))
# get details about albums # get details about albums
c.execute( c.execute(
@@ -1921,8 +2047,8 @@ class PhotosDB:
logging.debug("Faces (_dbfaces_uuid):") logging.debug("Faces (_dbfaces_uuid):")
logging.debug(pformat(self._dbfaces_uuid)) logging.debug(pformat(self._dbfaces_uuid))
logging.debug("Faces by person (_dbfaces_person):") logging.debug("Persons (_dbpersons_pk):")
logging.debug(pformat(self._dbfaces_person)) logging.debug(pformat(self._dbpersons_pk))
logging.debug("Keywords by uuid (_dbkeywords_uuid):") logging.debug("Keywords by uuid (_dbkeywords_uuid):")
logging.debug(pformat(self._dbkeywords_uuid)) logging.debug(pformat(self._dbkeywords_uuid))
@@ -2283,8 +2409,9 @@ class PhotosDB:
if persons: if persons:
person_set = set() person_set = set()
for person in persons: for person in persons:
if person in self._dbfaces_person: if person in self._dbpersons_fullname:
person_set.update(self._dbfaces_person[person]) for pk in self._dbpersons_fullname[person]:
person_set.update(self._dbfaces_pk[pk])
else: else:
logging.debug(f"Could not find person '{person}' in database") logging.debug(f"Could not find person '{person}' in database")
photos_sets.append(person_set) photos_sets.append(person_set)

View File

@@ -1,7 +1,6 @@
import pytest import pytest
# TODO: put some of this code into a pre-function from osxphotos._constants import _UNKNOWN_PERSON
# TODO: All the hardocded uuids, etc in test functions should be in some sort of config
PHOTOS_DB = "./tests/Test-10.12.6.photoslibrary/database/photos.db" PHOTOS_DB = "./tests/Test-10.12.6.photoslibrary/database/photos.db"
KEYWORDS = [ KEYWORDS = [
@@ -15,7 +14,7 @@ KEYWORDS = [
"UK", "UK",
"United Kingdom", "United Kingdom",
] ]
PERSONS = ["Katie", "Suzy", "Maria"] PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
ALBUMS = ["Pumpkin Farm", "AlbumInFolder"] ALBUMS = ["Pumpkin Farm", "AlbumInFolder"]
KEYWORDS_DICT = { KEYWORDS_DICT = {
"Kids": 4, "Kids": 4,
@@ -28,7 +27,7 @@ KEYWORDS_DICT = {
"UK": 1, "UK": 1,
"United Kingdom": 1, "United Kingdom": 1,
} }
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1} PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1}
ALBUM_DICT = {"Pumpkin Farm": 3, "AlbumInFolder": 1} ALBUM_DICT = {"Pumpkin Farm": 3, "AlbumInFolder": 1}

View File

@@ -560,6 +560,13 @@ def test_album_folder_name():
photos = photosdb.photos(albums=["Pumpkin Farm"]) photos = photosdb.photos(albums=["Pumpkin Farm"])
assert sorted(p.uuid for p in photos) == sorted(UUID_PUMPKIN_FARM) assert sorted(p.uuid for p in photos) == sorted(UUID_PUMPKIN_FARM)
def test_multi_person():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
photos = photosdb.photos(persons=["Katie", "Suzy"])
assert len(photos) == 3
def test_get_db_path(): def test_get_db_path():
import osxphotos import osxphotos

View File

@@ -1,6 +1,6 @@
import pytest import pytest
# TODO: put some of this code into a pre-function from osxphotos._constants import _UNKNOWN_PERSON
PHOTOS_DB = "./tests/Test-10.13.6.photoslibrary/database/photos.db" PHOTOS_DB = "./tests/Test-10.13.6.photoslibrary/database/photos.db"
KEYWORDS = [ KEYWORDS = [
@@ -14,7 +14,7 @@ KEYWORDS = [
"UK", "UK",
"United Kingdom", "United Kingdom",
] ]
PERSONS = ["Katie", "Suzy", "Maria"] PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
ALBUMS = ["Pumpkin Farm", "AlbumInFolder", "TestAlbum"] ALBUMS = ["Pumpkin Farm", "AlbumInFolder", "TestAlbum"]
KEYWORDS_DICT = { KEYWORDS_DICT = {
"Kids": 4, "Kids": 4,
@@ -27,7 +27,7 @@ KEYWORDS_DICT = {
"UK": 1, "UK": 1,
"United Kingdom": 1, "United Kingdom": 1,
} }
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1} PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1}
ALBUM_DICT = {"Pumpkin Farm": 3, "TestAlbum": 1, "AlbumInFolder": 1} ALBUM_DICT = {"Pumpkin Farm": 3, "TestAlbum": 1, "AlbumInFolder": 1}

View File

@@ -1,6 +1,7 @@
import pytest import pytest
# TODO: put some of this code into a pre-function
from osxphotos._constants import _UNKNOWN_PERSON
PHOTOS_DB = "./tests/Test-10.14.5.photoslibrary/database/photos.db" PHOTOS_DB = "./tests/Test-10.14.5.photoslibrary/database/photos.db"
KEYWORDS = [ KEYWORDS = [
@@ -14,7 +15,7 @@ KEYWORDS = [
"UK", "UK",
"United Kingdom", "United Kingdom",
] ]
PERSONS = ["Katie", "Suzy", "Maria"] PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
ALBUMS = ["Pumpkin Farm"] ALBUMS = ["Pumpkin Farm"]
KEYWORDS_DICT = { KEYWORDS_DICT = {
"Kids": 4, "Kids": 4,
@@ -27,7 +28,7 @@ KEYWORDS_DICT = {
"UK": 1, "UK": 1,
"United Kingdom": 1, "United Kingdom": 1,
} }
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1} PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1}
ALBUM_DICT = {"Pumpkin Farm": 3} ALBUM_DICT = {"Pumpkin Farm": 3}

View File

@@ -1,6 +1,6 @@
import pytest import pytest
# TODO: put some of this code into a pre-function from osxphotos._constants import _UNKNOWN_PERSON
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db" PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
PHOTOS_DB_PATH = "/Test-10.14.6.photoslibrary/database/photos.db" PHOTOS_DB_PATH = "/Test-10.14.6.photoslibrary/database/photos.db"
@@ -17,7 +17,7 @@ KEYWORDS = [
"UK", "UK",
"United Kingdom", "United Kingdom",
] ]
PERSONS = ["Katie", "Suzy", "Maria"] PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
ALBUMS = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album (1)"] ALBUMS = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album (1)"]
KEYWORDS_DICT = { KEYWORDS_DICT = {
"Kids": 4, "Kids": 4,
@@ -30,7 +30,7 @@ KEYWORDS_DICT = {
"UK": 1, "UK": 1,
"United Kingdom": 1, "United Kingdom": 1,
} }
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1} PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1}
ALBUM_DICT = { ALBUM_DICT = {
"Pumpkin Farm": 3, "Pumpkin Farm": 3,
"AlbumInFolder": 1, "AlbumInFolder": 1,