Initial implementation of inspect command (#700)
This commit is contained in:
@@ -59,6 +59,7 @@ from .keywords import keywords
|
|||||||
from .labels import labels
|
from .labels import labels
|
||||||
from .list import _list_libraries, list_libraries
|
from .list import _list_libraries, list_libraries
|
||||||
from .persons import persons
|
from .persons import persons
|
||||||
|
from .photo_inspect import photo_inspect
|
||||||
from .places import places
|
from .places import places
|
||||||
from .query import query
|
from .query import query
|
||||||
from .repl import repl
|
from .repl import repl
|
||||||
@@ -88,6 +89,7 @@ __all__ = [
|
|||||||
"list_libraries",
|
"list_libraries",
|
||||||
"load_uuid_from_file",
|
"load_uuid_from_file",
|
||||||
"persons",
|
"persons",
|
||||||
|
"photo_inspect",
|
||||||
"places",
|
"places",
|
||||||
"query",
|
"query",
|
||||||
"repl",
|
"repl",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from .keywords import keywords
|
|||||||
from .labels import labels
|
from .labels import labels
|
||||||
from .list import list_libraries
|
from .list import list_libraries
|
||||||
from .persons import persons
|
from .persons import persons
|
||||||
|
from .photo_inspect import photo_inspect
|
||||||
from .places import places
|
from .places import places
|
||||||
from .query import query
|
from .query import query
|
||||||
from .repl import repl
|
from .repl import repl
|
||||||
@@ -79,6 +80,7 @@ for command in [
|
|||||||
labels,
|
labels,
|
||||||
list_libraries,
|
list_libraries,
|
||||||
persons,
|
persons,
|
||||||
|
photo_inspect,
|
||||||
places,
|
places,
|
||||||
query,
|
query,
|
||||||
repl,
|
repl,
|
||||||
|
|||||||
468
osxphotos/cli/photo_inspect.py
Normal file
468
osxphotos/cli/photo_inspect.py
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
"""Inspect photos selected in Photos """
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import re
|
||||||
|
from fractions import Fraction
|
||||||
|
from multiprocessing import Process, Queue
|
||||||
|
from queue import Empty
|
||||||
|
from time import gmtime, sleep, strftime
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import bitmath
|
||||||
|
import click
|
||||||
|
from applescript import ScriptError
|
||||||
|
from photoscript import PhotosLibrary
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.layout import Layout
|
||||||
|
from rich.live import Live
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
|
from osxphotos import PhotoInfo, PhotosDB
|
||||||
|
from osxphotos._constants import _UNKNOWN_PERSON
|
||||||
|
from osxphotos.rich_utils import add_rich_markup_tag
|
||||||
|
from osxphotos.text_detection import detect_text as detect_text_in_photo
|
||||||
|
from osxphotos.utils import dd_to_dms_str
|
||||||
|
|
||||||
|
from .color_themes import get_theme
|
||||||
|
from .common import DB_OPTION, THEME_OPTION, get_photos_db
|
||||||
|
|
||||||
|
# global that tracks UUID being inspected
|
||||||
|
CURRENT_UUID = None
|
||||||
|
|
||||||
|
# helpers for markup
|
||||||
|
bold = add_rich_markup_tag("bold")
|
||||||
|
dim = add_rich_markup_tag("dim")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_uuid(text: str) -> str:
|
||||||
|
"""Extract a UUID from a string"""
|
||||||
|
if match := re.search(
|
||||||
|
r"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})",
|
||||||
|
text,
|
||||||
|
):
|
||||||
|
return match[1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def trim(text: str, pad: str = "") -> str:
|
||||||
|
"""Truncate a string to a fit in console, - len(pad) - 4; also removes new lines"""
|
||||||
|
width = Console().width - len(pad) - 4
|
||||||
|
text = text.replace("\n", " ")
|
||||||
|
return text if len(text) <= width else f"{text[: width- 3]}..."
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_photo(photo: PhotoInfo, detected_text: Optional[str] = None) -> str:
|
||||||
|
"""Get info about an osxphotos PhotoInfo object formatted for printing"""
|
||||||
|
|
||||||
|
properties = [
|
||||||
|
bold("Filename: ") + f"[filename]{photo.original_filename}[/]",
|
||||||
|
bold("Type: ") + get_photo_type(photo),
|
||||||
|
bold("UUID: ") + f"[uuid]{photo.uuid}[/]",
|
||||||
|
bold("Date: ") + f"[time]{photo.date.isoformat()}[/]",
|
||||||
|
bold("Date added: ") + f"[time]{photo.date_added.isoformat()}[/]",
|
||||||
|
]
|
||||||
|
if photo.date_modified:
|
||||||
|
properties.append(
|
||||||
|
bold("Date modified: ") + f"[time]{photo.date_modified.isoformat()}[/]"
|
||||||
|
)
|
||||||
|
|
||||||
|
if photo.intrash and photo.date_trashed:
|
||||||
|
properties.append(
|
||||||
|
bold("Date deleted: ") + f"[time]{photo.date_trashed.isoformat()}[/]"
|
||||||
|
)
|
||||||
|
|
||||||
|
if photo.import_info and photo.import_info.creation_date:
|
||||||
|
properties.append(
|
||||||
|
bold("Date imported: ")
|
||||||
|
+ f"[time]{photo.import_info.creation_date.isoformat()}[/]"
|
||||||
|
)
|
||||||
|
|
||||||
|
properties.extend(
|
||||||
|
[
|
||||||
|
bold("Dimensions: ")
|
||||||
|
+ f"[num]{photo.width}[/] x [num]{photo.height}[/] "
|
||||||
|
+ bold("Orientation: ")
|
||||||
|
+ f"[num]{photo.orientation}[/]",
|
||||||
|
bold("File size: ")
|
||||||
|
+ f"[num]{float(bitmath.Byte(photo.original_filesize).to_MB()):.2f} MB[/]",
|
||||||
|
bold("Title: ") + f"{photo.title or '-'}",
|
||||||
|
bold("Description: ")
|
||||||
|
+ f"{trim(photo.description or '-', 'Description: ')}",
|
||||||
|
bold("Edited: ")
|
||||||
|
+ f"{'✔' if photo.hasadjustments else '-'} "
|
||||||
|
+ bold("External edits: ")
|
||||||
|
+ f"{'✔' if photo.external_edit else '-'}",
|
||||||
|
bold("Keywords: ") + f"{', '.join(photo.keywords) or '-'}",
|
||||||
|
bold("Persons: ")
|
||||||
|
+ f"{', '.join(p for p in photo.persons if p != _UNKNOWN_PERSON) or '-'}",
|
||||||
|
bold("Location: ")
|
||||||
|
+ f"{', '.join(dd_to_dms_str(*photo.location)) if photo.location[0] else '-'}",
|
||||||
|
bold("Place: ") + f"{photo.place.name if photo.place else '-'}",
|
||||||
|
bold("Categories: ") + f"{', '.join(photo.labels) or '-'}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
properties.append(format_flags(photo))
|
||||||
|
properties.append(format_albums(photo))
|
||||||
|
|
||||||
|
if photo.project_info:
|
||||||
|
properties.append(
|
||||||
|
bold("Projects: ")
|
||||||
|
+ f"{', '.join(p.title for p in photo.project_info) or '-'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if photo.moment_info:
|
||||||
|
properties.append(bold("Moment: ") + f"{photo.moment_info.title or '-'}")
|
||||||
|
|
||||||
|
if photo.comments:
|
||||||
|
comments = [f"{c.user}: {c.text}" for c in photo.comments]
|
||||||
|
properties.append(
|
||||||
|
bold("Comments: ") + trim(f"{', '.join(comments)}", "Comments: ")
|
||||||
|
)
|
||||||
|
|
||||||
|
if photo.likes:
|
||||||
|
properties.append(
|
||||||
|
bold("Likes: ")
|
||||||
|
+ trim(f"{', '.join(l.user for l in photo.likes)}", "Likes: ")
|
||||||
|
)
|
||||||
|
|
||||||
|
properties.append(format_exif_info(photo))
|
||||||
|
properties.append(format_score_info(photo))
|
||||||
|
|
||||||
|
if detected_text:
|
||||||
|
# have detected text for this photo
|
||||||
|
properties.append(
|
||||||
|
bold("Detected text: ") + trim(detected_text, "Detected text: ")
|
||||||
|
)
|
||||||
|
|
||||||
|
properties.append(format_paths(photo))
|
||||||
|
|
||||||
|
return "\n".join(properties)
|
||||||
|
|
||||||
|
|
||||||
|
def format_score_info(photo: PhotoInfo) -> str:
|
||||||
|
"""Format score_info"""
|
||||||
|
score_str = bold("Score: ")
|
||||||
|
if photo.score:
|
||||||
|
score_str += f"[num]{photo.score.overall}[/]" if photo.score else "-"
|
||||||
|
return score_str
|
||||||
|
|
||||||
|
|
||||||
|
def format_flags(photo: PhotoInfo) -> str:
|
||||||
|
"""Format special properties"""
|
||||||
|
flag_str = bold("Flags: ")
|
||||||
|
flags = []
|
||||||
|
if photo.favorite:
|
||||||
|
flags.append("favorite")
|
||||||
|
if photo.visible:
|
||||||
|
flags.append("visible")
|
||||||
|
if photo.hidden:
|
||||||
|
flags.append("hidden")
|
||||||
|
if photo.ismissing:
|
||||||
|
flags.append("missing")
|
||||||
|
if photo.intrash:
|
||||||
|
flags.append("in trash")
|
||||||
|
if photo.iscloudasset:
|
||||||
|
flags.append("cloud asset")
|
||||||
|
if photo.incloud:
|
||||||
|
flags.append("in cloud")
|
||||||
|
if photo.shared:
|
||||||
|
flags.append("shared")
|
||||||
|
|
||||||
|
flag_str += f"{', '.join(flags) or '-'}"
|
||||||
|
return flag_str
|
||||||
|
|
||||||
|
|
||||||
|
def format_albums(photo: PhotoInfo) -> str:
|
||||||
|
"""Format albums for inspect_photo"""
|
||||||
|
album_str = bold("Albums: ")
|
||||||
|
album_names = []
|
||||||
|
for album in photo.album_info:
|
||||||
|
if album.folder_names:
|
||||||
|
folder_str = "/".join(album.folder_names)
|
||||||
|
album_names.append(f"{folder_str}/{album.title}")
|
||||||
|
else:
|
||||||
|
album_names.append(album.title)
|
||||||
|
album_str += f"{', '.join(album_names) or '-'}"
|
||||||
|
return album_str
|
||||||
|
|
||||||
|
|
||||||
|
def format_path_link(path: str) -> str:
|
||||||
|
"""Format a path as URI for display in terminal"""
|
||||||
|
return f"[link={pathlib.Path(path).as_uri()}]{path}[/link]"
|
||||||
|
|
||||||
|
|
||||||
|
def format_paths(photo: PhotoInfo) -> str:
|
||||||
|
"""format photo paths for inspect_photo"""
|
||||||
|
path_str = bold("Path original: ")
|
||||||
|
path_str += f"[filepath]{format_path_link(photo.path)}[/]" if photo.path else "-"
|
||||||
|
if photo.path_edited:
|
||||||
|
path_str += "\n"
|
||||||
|
path_str += bold("Path edited: ")
|
||||||
|
path_str += f"[filepath]{format_path_link(photo.path_edited)}[/]"
|
||||||
|
if photo.path_raw:
|
||||||
|
path_str += "\n"
|
||||||
|
path_str += bold("Path raw: ")
|
||||||
|
path_str += f"[filepath]{format_path_link(photo.path_raw)}[/]"
|
||||||
|
if photo.path_derivatives:
|
||||||
|
path_str += "\n"
|
||||||
|
path_str += bold("Path preview: ")
|
||||||
|
path_str += f"[filepath]{format_path_link(photo.path_derivatives[0])}[/]"
|
||||||
|
return path_str
|
||||||
|
|
||||||
|
|
||||||
|
def format_exif_info(photo: PhotoInfo) -> str:
|
||||||
|
"""Format exif_info for inspect_photo"""
|
||||||
|
exif_str = bold("EXIF: ")
|
||||||
|
exif = photo.exif_info
|
||||||
|
if not exif:
|
||||||
|
return f"{exif_str}-"
|
||||||
|
|
||||||
|
if exif.camera_make:
|
||||||
|
exif_str += f"{exif.camera_make} "
|
||||||
|
if exif.camera_model:
|
||||||
|
exif_str += f"{exif.camera_model} "
|
||||||
|
if exif.focal_length:
|
||||||
|
exif_str += f"{exif.focal_length:.2f}mm "
|
||||||
|
if exif.iso:
|
||||||
|
exif_str += f"ISO {exif.iso} "
|
||||||
|
if exif.flash_fired:
|
||||||
|
exif_str += "Flash "
|
||||||
|
if exif.exposure_bias is not None:
|
||||||
|
exif_str += (
|
||||||
|
f"{int(exif.exposure_bias)} ev "
|
||||||
|
if exif.exposure_bias == int(exif.exposure_bias)
|
||||||
|
else f"{exif.exposure_bias} ev "
|
||||||
|
)
|
||||||
|
if exif.aperture:
|
||||||
|
exif_str += (
|
||||||
|
f"ƒ{int(exif.aperture)} "
|
||||||
|
if exif.aperture == int(exif.aperture)
|
||||||
|
else f"ƒ{exif.aperture:.1f} "
|
||||||
|
)
|
||||||
|
if exif.shutter_speed:
|
||||||
|
exif_str += f"{Fraction(exif.shutter_speed).limit_denominator(100_000)}s "
|
||||||
|
if exif.bit_rate:
|
||||||
|
exif_str += f"{exif.bit_rate} bit rate"
|
||||||
|
if exif.sample_rate:
|
||||||
|
exif_str += f"{exif.sample_rate} sample rate"
|
||||||
|
if exif.fps:
|
||||||
|
exif_str += f"{exif.fps or 0:.1f}FPS "
|
||||||
|
if exif.duration:
|
||||||
|
exif_str += f"{strftime('%H:%M:%S', gmtime(exif.duration or 0))} "
|
||||||
|
if exif.codec:
|
||||||
|
exif_str += f"{exif.codec}"
|
||||||
|
if exif.track_format:
|
||||||
|
exif_str += f"{exif.track_format}"
|
||||||
|
return exif_str
|
||||||
|
|
||||||
|
|
||||||
|
def get_photo_type(photo: PhotoInfo) -> str:
|
||||||
|
"""Return a string describing the type of photo"""
|
||||||
|
photo_type = "video" if photo.ismovie else "photo"
|
||||||
|
if photo.has_raw:
|
||||||
|
photo_type += " RAW+JPEG"
|
||||||
|
if photo.israw:
|
||||||
|
photo_type += " RAW"
|
||||||
|
if photo.burst:
|
||||||
|
photo_type += " burst"
|
||||||
|
if photo.live_photo:
|
||||||
|
photo_type += " live"
|
||||||
|
if photo.selfie:
|
||||||
|
photo_type += " selfie"
|
||||||
|
if photo.panorama:
|
||||||
|
photo_type += " panorama"
|
||||||
|
if photo.hdr:
|
||||||
|
photo_type += " HDR"
|
||||||
|
if photo.screenshot:
|
||||||
|
photo_type += " screenshot"
|
||||||
|
if photo.slow_mo:
|
||||||
|
photo_type += " slow-mo"
|
||||||
|
if photo.time_lapse:
|
||||||
|
photo_type += " time-lapse"
|
||||||
|
if photo.portrait:
|
||||||
|
photo_type += " portrait"
|
||||||
|
return photo_type
|
||||||
|
|
||||||
|
|
||||||
|
def start_text_detection(photo: PhotoInfo) -> Tuple[Process, Queue]:
|
||||||
|
"""Start text detection process for a photo"""
|
||||||
|
path_preview = photo.path_derivatives[0] if photo.path_derivatives else None
|
||||||
|
path = photo.path_edited or photo.path or path_preview
|
||||||
|
if not path:
|
||||||
|
raise ValueError("No path to photo")
|
||||||
|
queue = Queue()
|
||||||
|
p = Process(
|
||||||
|
target=_get_detected_text,
|
||||||
|
args=(photo.uuid, path, photo.orientation, queue),
|
||||||
|
)
|
||||||
|
p.start()
|
||||||
|
return (p, queue)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_detected_text(uuid: str, path: str, orientation: int, queue: Queue) -> None:
|
||||||
|
"""Called by start_text_detection to run text detection in separate process"""
|
||||||
|
try:
|
||||||
|
if text := detect_text_in_photo(path, orientation):
|
||||||
|
queue.put([uuid, " ".join(t[0] for t in text if t[1] > 0.5)])
|
||||||
|
except Exception as e:
|
||||||
|
queue.put([uuid, str(e)])
|
||||||
|
|
||||||
|
|
||||||
|
def get_uuid_for_photos_selection() -> List[str]:
|
||||||
|
"""Get the uuid for the first photo selected in Photos
|
||||||
|
|
||||||
|
Returns: tuple of (uuid, total_selected_photos)"""
|
||||||
|
photoslib = PhotosLibrary()
|
||||||
|
try:
|
||||||
|
if photos := photoslib.selection:
|
||||||
|
return photos[0].uuid, len(photos)
|
||||||
|
except (ValueError, ScriptError) as e:
|
||||||
|
if uuid := extract_uuid(str(e)):
|
||||||
|
return uuid, 1
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
return None, 0
|
||||||
|
|
||||||
|
|
||||||
|
def make_layout() -> Layout:
|
||||||
|
"""Define the layout."""
|
||||||
|
layout = Layout(name="root")
|
||||||
|
|
||||||
|
layout.split(
|
||||||
|
Layout(name="main", ratio=1),
|
||||||
|
Layout(name="status", size=1),
|
||||||
|
Layout(name="footer", size=1),
|
||||||
|
)
|
||||||
|
return layout
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(name="inspect")
|
||||||
|
@click.option("--detect-text", "-t", is_flag=True, help="Detect text in photos")
|
||||||
|
@THEME_OPTION
|
||||||
|
@DB_OPTION
|
||||||
|
def photo_inspect(db, theme, detect_text):
|
||||||
|
"""Interactively inspect photos selected in Photos"""
|
||||||
|
db = get_photos_db(db)
|
||||||
|
if not db:
|
||||||
|
raise click.UsageError(
|
||||||
|
"Did not locate Photos database. Try running with --db option."
|
||||||
|
)
|
||||||
|
|
||||||
|
theme = get_theme(theme)
|
||||||
|
console = Console(theme=theme)
|
||||||
|
|
||||||
|
layout = make_layout()
|
||||||
|
layout["footer"].update("Press Ctrl+C to quit")
|
||||||
|
layout["main"].update(
|
||||||
|
Panel("Loading Photos database, hold on...", title="Loading...")
|
||||||
|
)
|
||||||
|
|
||||||
|
def loading_status(status: str):
|
||||||
|
layout["status"].update(f"Loading database: {status}")
|
||||||
|
|
||||||
|
def update_status(status: str):
|
||||||
|
layout["status"].update(status)
|
||||||
|
|
||||||
|
def update_detected_text(photo: PhotoInfo, uuid: str, text: str):
|
||||||
|
global CURRENT_UUID
|
||||||
|
if uuid == CURRENT_UUID:
|
||||||
|
layout["main"].update(
|
||||||
|
Panel(
|
||||||
|
inspect_photo(photo, text),
|
||||||
|
title=photo.title or photo.original_filename,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with Live(layout, console=console, refresh_per_second=10, screen=True):
|
||||||
|
photosdb = PhotosDB(dbfile=db, verbose=loading_status)
|
||||||
|
layout["main"].update(
|
||||||
|
Panel("Select a photo in Photos to inspect", title="Select a photo")
|
||||||
|
)
|
||||||
|
processes = []
|
||||||
|
global CURRENT_UUID
|
||||||
|
detected_text_cache = {}
|
||||||
|
last_uuid = None
|
||||||
|
uuid = None
|
||||||
|
total = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
uuid, total = get_uuid_for_photos_selection()
|
||||||
|
except Exception as e:
|
||||||
|
layout["main"].update(Panel(f"Error: {e}", title="Error"))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
# allow Ctrl+C to quit
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
if uuid and uuid != last_uuid:
|
||||||
|
if total > 1:
|
||||||
|
update_status(
|
||||||
|
f"{total} photos selected; inspecting uuid={uuid}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
update_status(f"Inspecting uuid={uuid}")
|
||||||
|
CURRENT_UUID = uuid
|
||||||
|
if photo := photosdb.get_photo(uuid):
|
||||||
|
layout["main"].update(
|
||||||
|
Panel(
|
||||||
|
inspect_photo(
|
||||||
|
photo,
|
||||||
|
detected_text=detected_text_cache.get(uuid, None),
|
||||||
|
),
|
||||||
|
title=photo.title or photo.original_filename,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# start text detection if requested
|
||||||
|
if (
|
||||||
|
detect_text
|
||||||
|
and (
|
||||||
|
photo.path
|
||||||
|
or photo.path_edited
|
||||||
|
or photo.path_derivatives
|
||||||
|
)
|
||||||
|
and uuid not in detected_text_cache
|
||||||
|
):
|
||||||
|
# can only detect text on photos if on disk
|
||||||
|
# start text detection in separate process as it can take a few seconds
|
||||||
|
update_detected_text_callback = functools.partial(
|
||||||
|
update_detected_text, photo
|
||||||
|
)
|
||||||
|
process, queue = start_text_detection(photo)
|
||||||
|
processes.append(
|
||||||
|
[True, process, queue, update_detected_text_callback]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
layout["main"].update(
|
||||||
|
Panel(
|
||||||
|
f"No photo found in database for uuid={uuid}",
|
||||||
|
title="Error",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
last_uuid = uuid
|
||||||
|
if not uuid:
|
||||||
|
last_uuid = None
|
||||||
|
layout["main"].update(
|
||||||
|
Panel("Select a photo in Photos to inspect", title="Select a photo")
|
||||||
|
)
|
||||||
|
update_status("Select a photo in Photos to inspect")
|
||||||
|
if detect_text:
|
||||||
|
# check on text detection processes
|
||||||
|
for _, values in enumerate(processes):
|
||||||
|
alive, process, queue, update_detected_text_callback = values
|
||||||
|
if alive:
|
||||||
|
# process hasn't been marked as dead yet
|
||||||
|
try:
|
||||||
|
uuid, text = queue.get(False)
|
||||||
|
update_detected_text_callback(uuid, text)
|
||||||
|
detected_text_cache[uuid] = text
|
||||||
|
process.join()
|
||||||
|
# set alive = False
|
||||||
|
values[0] = False
|
||||||
|
except Empty:
|
||||||
|
if not process.is_alive():
|
||||||
|
# process has finished, nothing in the queue
|
||||||
|
process.join()
|
||||||
|
# set alive = False
|
||||||
|
values[0] = False
|
||||||
|
sleep(0.100)
|
||||||
@@ -32,7 +32,7 @@ def detect_text(img_path: str, orientation: Optional[int] = None) -> List:
|
|||||||
orientation: optional EXIF orientation (if known, passing orientation may improve quality of results)
|
orientation: optional EXIF orientation (if known, passing orientation may improve quality of results)
|
||||||
"""
|
"""
|
||||||
if not vision:
|
if not vision:
|
||||||
logging.warning(f"detect_text not implemented for this version of macOS")
|
logging.warning("detect_text not implemented for this version of macOS")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
with objc.autorelease_pool():
|
with objc.autorelease_pool():
|
||||||
@@ -47,18 +47,18 @@ def detect_text(img_path: str, orientation: Optional[int] = None) -> List:
|
|||||||
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
||||||
|
|
||||||
vision_options = NSDictionary.dictionaryWithDictionary_({})
|
vision_options = NSDictionary.dictionaryWithDictionary_({})
|
||||||
if orientation is not None:
|
if orientation is None:
|
||||||
if not 1 <= orientation <= 8:
|
|
||||||
raise ValueError("orientation must be between 1 and 8")
|
|
||||||
vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_orientation_options_(
|
|
||||||
input_image, orientation, vision_options
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
vision_handler = (
|
vision_handler = (
|
||||||
Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
|
Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
|
||||||
input_image, vision_options
|
input_image, vision_options
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
elif 1 <= orientation <= 8:
|
||||||
|
vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_orientation_options_(
|
||||||
|
input_image, orientation, vision_options
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError("orientation must be between 1 and 8")
|
||||||
results = []
|
results = []
|
||||||
handler = make_request_handler(results)
|
handler = make_request_handler(results)
|
||||||
vision_request = (
|
vision_request = (
|
||||||
|
|||||||
Reference in New Issue
Block a user