Added QR code example
This commit is contained in:
182
examples/detect_qrcodes.py
Normal file
182
examples/detect_qrcodes.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
"""Detect QR Codes in photos in Apple Photos and add qrcode tag/keyword to photos contain a QR Code
|
||||||
|
|
||||||
|
Run with `osxphotos run detect_qrcodes.py`
|
||||||
|
|
||||||
|
Run with `osxphotos run detect_qrcodes.py --help` for help
|
||||||
|
|
||||||
|
You'll need to install opencv into your osxphotos environment. This can be done with:
|
||||||
|
`osxphotos install opencv-python`
|
||||||
|
|
||||||
|
All the other dependencies should be already installed with osxphotos (e.g. rich, click)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import click
|
||||||
|
import cv2
|
||||||
|
from photoscript import Photo, PhotosLibrary
|
||||||
|
from rich import print
|
||||||
|
from rich.progress import Progress
|
||||||
|
|
||||||
|
from osxphotos import PhotosDB
|
||||||
|
from osxphotos.cli.common import get_data_dir
|
||||||
|
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||||
|
|
||||||
|
QRCODE_KEYWORD = "qrcode"
|
||||||
|
|
||||||
|
|
||||||
|
def detect_qrcode_in_image_cv2(filename: str) -> Optional[str]:
|
||||||
|
"""Detect QR Code in image file"""
|
||||||
|
image = cv2.imread(filename)
|
||||||
|
qr_detect = cv2.QRCodeDetector()
|
||||||
|
decoded_text, points, qrcode = qr_detect.detectAndDecode(image)
|
||||||
|
return decoded_text or None
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--keyword",
|
||||||
|
"-k",
|
||||||
|
default=QRCODE_KEYWORD,
|
||||||
|
help=f"Keyword to add to photos with QR Code; default = '{QRCODE_KEYWORD}'.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--description",
|
||||||
|
"-d",
|
||||||
|
is_flag=True,
|
||||||
|
help="Set the description of the photo to the decoded QR Code text.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--verbose",
|
||||||
|
"-V",
|
||||||
|
"verbose_mode",
|
||||||
|
is_flag=True,
|
||||||
|
help="Print verbose output.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--dry-run",
|
||||||
|
"-n",
|
||||||
|
is_flag=True,
|
||||||
|
help="Dry run mode: don't actually update keywords in Photos library.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--selected",
|
||||||
|
"-s",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only process selected photos.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--reset",
|
||||||
|
"-R",
|
||||||
|
help="Reset the database of previously processed photos.",
|
||||||
|
is_flag=True,
|
||||||
|
)
|
||||||
|
def detect_qrcodes(keyword, description, verbose_mode, dry_run, selected, reset):
|
||||||
|
"""Detect QR Codes in your photos and add a tag/keyword to photos containing a QR Code."""
|
||||||
|
# osxphotos includes a simple sqlite-based key-value store for storing data
|
||||||
|
# Use this to store photos that have already been processed so if the script is re-run
|
||||||
|
# it doesn't re-process photos that have already been processed
|
||||||
|
|
||||||
|
# get_data_dir() returns the path to the user's XDG data directory, usually ~/.local/share/osxphotos
|
||||||
|
# reference: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||||
|
|
||||||
|
def verbose(msg):
|
||||||
|
if not verbose_mode:
|
||||||
|
return
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
db_path = os.path.join(get_data_dir(), "qrcodes.db")
|
||||||
|
if reset:
|
||||||
|
verbose(f"Resetting database: {db_path}")
|
||||||
|
if os.path.exists(db_path):
|
||||||
|
os.remove(db_path)
|
||||||
|
verbose(f"Using database {db_path}")
|
||||||
|
# enable write-ahead logging for performance, serialize/deserialize data as JSON
|
||||||
|
kvstore = SQLiteKVStore(
|
||||||
|
db_path, wal=True, serialize=json.dumps, deserialize=json.loads
|
||||||
|
)
|
||||||
|
|
||||||
|
# get list of photos to process
|
||||||
|
verbose("Getting list of photos to process...")
|
||||||
|
# Capture selection before loading the Photos database it can take a while to process the database
|
||||||
|
# and the selection may change while the database is being processed
|
||||||
|
selection = PhotosLibrary().selection if selected else []
|
||||||
|
# the QR detection doesn't work on movies so exclude movies
|
||||||
|
photos = PhotosDB().photos(movies=False)
|
||||||
|
if selected:
|
||||||
|
selected_uuid = [p.uuid for p in selection]
|
||||||
|
photos = [p for p in photos if p.uuid in selected_uuid]
|
||||||
|
|
||||||
|
# track number of photos processed for reporting at the end
|
||||||
|
num_photos = len(photos)
|
||||||
|
num_processed = 0
|
||||||
|
num_previously_processed = 0
|
||||||
|
num_skipped = 0
|
||||||
|
num_qrcodes = 0
|
||||||
|
|
||||||
|
# process all the photos
|
||||||
|
with Progress() as progress:
|
||||||
|
task = progress.add_task(f"Processing {num_photos} photos", total=num_photos)
|
||||||
|
for photo in photos:
|
||||||
|
# check if photo has already been processed
|
||||||
|
if kvstore.get(photo.uuid):
|
||||||
|
verbose(
|
||||||
|
f"Skipping previously processed photo {photo.original_filename} ({photo.uuid})"
|
||||||
|
)
|
||||||
|
num_previously_processed += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# cv2.imread() doesn't work on some file types like HEIC but every photo has a
|
||||||
|
# JPEG preview image ("derivative" in Photos) so use that. The preview image is
|
||||||
|
# smaller and lower-resolution, but in my testing, good enough for QR detection
|
||||||
|
if not photo.path_derivatives:
|
||||||
|
verbose(
|
||||||
|
f"Skipping {photo.original_filename} ({photo.uuid}), could not find photo path"
|
||||||
|
)
|
||||||
|
num_skipped += 1
|
||||||
|
continue
|
||||||
|
photo_path = photo.path_derivatives[0]
|
||||||
|
|
||||||
|
# record that will be stored in the kvstore database
|
||||||
|
record = {
|
||||||
|
"datetime": datetime.datetime.now().isoformat(),
|
||||||
|
"uuid": photo.uuid,
|
||||||
|
"original_filename": photo.original_filename,
|
||||||
|
"qrcode": None,
|
||||||
|
}
|
||||||
|
verbose(f"Processing {photo.original_filename} ({photo.uuid})")
|
||||||
|
num_processed += 1
|
||||||
|
if qrcode_text := detect_qrcode_in_image_cv2(photo_path):
|
||||||
|
# add qrcode tag/keyword to photo
|
||||||
|
# osxphotos PhotoInfo objects are read-only but you can get a photoscript Photo object
|
||||||
|
# that allows you to modify certain data about the Photo via the Photos app AppleScript interface
|
||||||
|
photo_ = Photo(photo.uuid)
|
||||||
|
if not dry_run:
|
||||||
|
photo_.keywords = list(set(photo_.keywords + [keyword]))
|
||||||
|
if description:
|
||||||
|
photo_.description = qrcode_text
|
||||||
|
record["qrcode"] = qrcode_text
|
||||||
|
verbose(
|
||||||
|
f"Added {keyword} to {photo.original_filename} ({photo.uuid}), detected QR Code: {qrcode_text}"
|
||||||
|
)
|
||||||
|
num_qrcodes += 1
|
||||||
|
|
||||||
|
# store photo in kvstore to indicate it's been processed
|
||||||
|
if not dry_run:
|
||||||
|
kvstore.set(photo.uuid, record)
|
||||||
|
|
||||||
|
progress.advance(task)
|
||||||
|
|
||||||
|
print(f"Processed {num_photos} photos")
|
||||||
|
print(f"Previously processed {num_previously_processed} photos")
|
||||||
|
print(f"Skipped {num_skipped} missing photos")
|
||||||
|
print(f"Processed {num_processed} photos this time")
|
||||||
|
print(f"Detected {num_qrcodes} QR Codes")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
detect_qrcodes()
|
||||||
Reference in New Issue
Block a user