Compare commits

..

6 Commits

Author SHA1 Message Date
Rhet Turnbull
2041789ff4 Updated README.md [skip ci] 2021-08-15 14:12:15 -07:00
Rhet Turnbull
aec86f93ea Added inspect() to repl, closes #501 2021-08-15 13:50:37 -07:00
Rhet Turnbull
57bfb03e05 Updated CHANGELOG.md [skip ci] 2021-08-02 05:55:19 -07:00
Rhet Turnbull
c2b2476e38 Updated docs for Text Detection [skip ci] 2021-08-02 05:52:48 -07:00
Rhet Turnbull
fa2027d453 Improved caching of detected_text results 2021-08-02 05:10:26 -07:00
Rhet Turnbull
9d980e4917 Updated CHANGELOG.md [skip ci] 2021-07-29 21:27:51 -07:00
10 changed files with 79 additions and 55 deletions

View File

@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.42.72](https://github.com/RhetTbull/osxphotos/compare/v0.42.71...v0.42.72)
> 2 August 2021
- Improved caching of detected_text results [`fa2027d`](https://github.com/RhetTbull/osxphotos/commit/fa2027d45308738d2335d4b5a72c3ef5c478491a)
#### [v0.42.71](https://github.com/RhetTbull/osxphotos/compare/v0.42.70...v0.42.71)
> 29 July 2021
- Updated text_detection to detect macOS version [`7376223`](https://github.com/RhetTbull/osxphotos/commit/7376223eb87a4919fd54cc685a3f263e83626879)
- Updated detected_text docs to make it clear this only works on Catalina+ [`ecd0b8e`](https://github.com/RhetTbull/osxphotos/commit/ecd0b8e22f8bf1f8d1e98d64834bebf0394dd903)
- Fix for #500, check for macOS version before loading Vision [`673243c`](https://github.com/RhetTbull/osxphotos/commit/673243c6cd1c267b6b741b5429cdb63c062648d1)
#### [v0.42.70](https://github.com/RhetTbull/osxphotos/compare/v0.42.69...v0.42.70) #### [v0.42.70](https://github.com/RhetTbull/osxphotos/compare/v0.42.69...v0.42.70)
> 29 July 2021 > 29 July 2021

View File

@@ -35,6 +35,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
+ [Raw Photos](#raw-photos) + [Raw Photos](#raw-photos)
+ [Template System](#template-system) + [Template System](#template-system)
+ [ExifTool](#exiftoolExifTool) + [ExifTool](#exiftoolExifTool)
+ [Text Detection](#textdetection)
+ [Utility Functions](#utility-functions) + [Utility Functions](#utility-functions)
* [Examples](#examples) * [Examples](#examples)
* [Related Projects](#related-projects) * [Related Projects](#related-projects)
@@ -1700,7 +1701,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline} {lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r' {cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n' {crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.42.71' {osxphotos_version} The osxphotos version, e.g. '0.42.73'
{osxphotos_cmd_line} The full command line used to run osxphotos {osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for The following substitutions may result in multiple values. Thus if specified for
@@ -2788,7 +2789,8 @@ Some substitutions, notably `album`, `keyword`, and `person` could return multip
See [Template System](#template-system) for additional details. See [Template System](#template-system) for additional details.
#### `detected_text(confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD)`
#### <a name="detected_text_method">`detected_text(confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD)`</a>
Detects text in photo and returns lists of results as (detected text, confidence) Detects text in photo and returns lists of results as (detected text, confidence)
@@ -2800,6 +2802,8 @@ Returns: list of (detected text, confidence) tuples.
Note: This is *not* the same as Live Text in macOS Monterey. When using `detected_text()`, osxphotos will use Apple's [Vision framework](https://developer.apple.com/documentation/vision/recognizing_text_in_images?language=objc) to perform text detection on the image. On my circa 2013 MacBook Pro, this takes about 2 seconds per image. `detected_text()` does memoize the results for a given `confidence_threshold` so repeated calls will not re-process the photo. This works only on macOS Catalina (10.15) or later. Note: This is *not* the same as Live Text in macOS Monterey. When using `detected_text()`, osxphotos will use Apple's [Vision framework](https://developer.apple.com/documentation/vision/recognizing_text_in_images?language=objc) to perform text detection on the image. On my circa 2013 MacBook Pro, this takes about 2 seconds per image. `detected_text()` does memoize the results for a given `confidence_threshold` so repeated calls will not re-process the photo. This works only on macOS Catalina (10.15) or later.
See also [Text Detection](#textdetection).
### ExifInfo ### ExifInfo
[PhotosInfo.exif_info](#exif-info) returns an `ExifInfo` object with some EXIF data about the photo (Photos 5 only). `ExifInfo` contains the following properties: [PhotosInfo.exif_info](#exif-info) returns an `ExifInfo` object with some EXIF data about the photo (Photos 5 only). `ExifInfo` contains the following properties:
@@ -3554,7 +3558,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}| |{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'| |{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'| |{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.71'| |{osxphotos_version}|The osxphotos version, e.g. '0.42.73'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos| |{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in| |{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder| |{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
@@ -3634,6 +3638,14 @@ osxphotos.exiftool also provides an `ExifToolCaching` class which caches all met
`ExifTool()` runs `exiftool` as a subprocess using the `-stay_open True` flag to keep the process running in the background. The subprocess will be cleaned up when your main script terminates. `ExifTool()` uses a singleton pattern to ensure that only one instance of `exiftool` is created. Multiple instances of `ExifTool()` will all use the same `exiftool` subprocess. `ExifTool()` runs `exiftool` as a subprocess using the `-stay_open True` flag to keep the process running in the background. The subprocess will be cleaned up when your main script terminates. `ExifTool()` uses a singleton pattern to ensure that only one instance of `exiftool` is created. Multiple instances of `ExifTool()` will all use the same `exiftool` subprocess.
### <a name="textdetection">Text Detection</a>
The [PhotoInfo.detected_text()](#detected_text_method) and the `{detected_text}` template will perform text detection on the photos in your library. Text detection is a slow process so to avoid unnecessary re-processing of photos, osxphotos will cache the results of the text detection process as an extended attribute on the photo image file. Extended attributes do not modify the actual file. The extended attribute is named `osxphotos.metadata:detected_text` and can be viewed using the built-in [xattr](https://ss64.com/osx/xattr.html) command or my [osxmetadata](https://github.com/RhetTbull/osxmetadata) tool. If you want to remove the cached attribute, you can do so with osxmetadata as follows:
`osxmetadata --clear osxphotos.metadata:detected_text --walk ~/Pictures/Photos\ Library.photoslibrary/`
### Utility Functions ### Utility Functions
The following functions are located in osxphotos.utils The following functions are located in osxphotos.utils

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.42.71" __version__ = "0.42.73"

View File

@@ -4074,6 +4074,10 @@ def _get_selected(photosdb):
@click.pass_context @click.pass_context
def repl(ctx, cli_obj, db): def repl(ctx, cli_obj, db):
"""Run interactive osxphotos shell""" """Run interactive osxphotos shell"""
from osxphotos import PhotosDB, PhotoInfo, ExifTool
from rich import inspect as _inspect
pretty.install() pretty.install()
print(f"python version: {sys.version}") print(f"python version: {sys.version}")
print(f"osxphotos version: {osxphotos._version.__version__}") print(f"osxphotos version: {osxphotos._version.__version__}")
@@ -4090,6 +4094,10 @@ def repl(ctx, cli_obj, db):
show = _show_photo show = _show_photo
get_selected = _get_selected(photosdb) get_selected = _get_selected(photosdb)
def inspect(obj):
"""inspect object"""
return _inspect(obj, methods=True)
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds") print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds")
print("The following variables are defined:") print("The following variables are defined:")
print(f"- photosdb: PhotosDB() instance for {photosdb.library_path}") print(f"- photosdb: PhotosDB() instance for {photosdb.library_path}")
@@ -4105,5 +4113,8 @@ def repl(ctx, cli_obj, db):
print( print(
f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)" f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
) )
print(
f"- inspect(object): print information about an object; for example inspect(photosdb)"
)
print(f"- quit(): exit this interactive shell\n") print(f"- quit(): exit this interactive shell\n")
code.interact(banner="", local=locals()) code.interact(banner="", local=locals())

View File

@@ -14,6 +14,7 @@ from datetime import timedelta, timezone
from typing import Optional from typing import Optional
import yaml import yaml
from osxmetadata import OSXMetaData
from .._constants import ( from .._constants import (
_MOVIE_TYPE, _MOVIE_TYPE,
@@ -1118,6 +1119,28 @@ class PhotoInfo:
Returns: list of (detected text, confidence) tuples Returns: list of (detected text, confidence) tuples
""" """
try:
return self._detected_text_cache[confidence_threshold]
except (AttributeError, KeyError) as e:
if isinstance(e, AttributeError):
self._detected_text_cache = {}
try:
detected_text = self._detected_text()
except Exception as e:
logging.warning(f"Error detecting text in photo {self.uuid}: {e}")
detected_text = []
self._detected_text_cache[confidence_threshold] = [
(text, confidence)
for text, confidence in detected_text
if confidence >= confidence_threshold
]
return self._detected_text_cache[confidence_threshold]
def _detected_text(self):
"""detect text in photo, either from cached extended attribute or by attempting text detection"""
path = ( path = (
self.path_edited if self.hasadjustments and self.path_edited else self.path self.path_edited if self.hasadjustments and self.path_edited else self.path
) )
@@ -1125,24 +1148,12 @@ class PhotoInfo:
if not path: if not path:
return [] return []
try: md = OSXMetaData(path)
return self._detected_text[(path, confidence_threshold)] detected_text = md.get_attribute("osxphotos_detected_text")
except (AttributeError, KeyError) as e: if detected_text is None:
if isinstance(e, AttributeError): detected_text = detect_text(path)
self._detected_text = {} md.set_attribute("osxphotos_detected_text", detected_text)
return detected_text
try:
detected_text = detect_text(path)
except Exception as e:
logging.warning(f"Error detecting text in photo {self.uuid} at {path}: {e}")
detected_text = []
self._detected_text[(path, confidence_threshold)] = [
(text, confidence)
for text, confidence in detected_text
if confidence >= confidence_threshold
]
return self._detected_text[(path, confidence_threshold)]
@property @property
def _longitude(self): def _longitude(self):

View File

@@ -1445,25 +1445,8 @@ def _get_detected_text(photo, exportdb, confidence=TEXT_DETECTION_CONFIDENCE_THR
else TEXT_DETECTION_CONFIDENCE_THRESHOLD else TEXT_DETECTION_CONFIDENCE_THRESHOLD
) )
detected_text = exportdb.get_detected_text_for_uuid(photo.uuid) # _detected_text caches the text detection results in an extended attribute
if detected_text is not None: # so the first time this gets called is slow but repeated accesses are fast
detected_text = json.loads(detected_text) detected_text = photo._detected_text()
else: exportdb.set_detected_text_for_uuid(photo.uuid, json.dumps(detected_text))
path = (
photo.path_edited
if photo.hasadjustments and photo.path_edited
else photo.path
)
path = path or photo.path_derivatives[0] if photo.path_derivatives else None
if not path:
detected_text = []
else:
try:
detected_text = detect_text(path)
except Exception as e:
logging.warning(
f"Error detecting text in image {photo.uuid} at {path}: {e}"
)
return []
exportdb.set_detected_text_for_uuid(photo.uuid, json.dumps(detected_text))
return [text for text, conf in detected_text if conf >= confidence] return [text for text, conf in detected_text if conf >= confidence]

View File

@@ -52,6 +52,9 @@ def detect_text(img_path: str) -> List:
vision_request.dealloc() vision_request.dealloc()
vision_handler.dealloc() vision_handler.dealloc()
for result in results:
result[0] = str(result[0])
return results return results

View File

@@ -16,7 +16,7 @@ dataclasses==0.7;python_version<'3.7'
wurlitzer==2.1.0 wurlitzer==2.1.0
photoscript==0.1.4 photoscript==0.1.4
toml==0.10.2 toml==0.10.2
osxmetadata==0.99.26 osxmetadata==0.99.31
textx==2.3.0 textx==2.3.0
rich==10.6.0 rich==10.6.0
bitmath==1.3.3.1 bitmath==1.3.3.1

View File

@@ -91,7 +91,7 @@ setup(
"wurlitzer==2.1.0", "wurlitzer==2.1.0",
"photoscript==0.1.4", "photoscript==0.1.4",
"toml==0.10.2", "toml==0.10.2",
"osxmetadata==0.99.26", "osxmetadata==0.99.31",
"textx==2.3.0", "textx==2.3.0",
"rich==10.6.0", "rich==10.6.0",
"bitmath==1.3.3.1", "bitmath==1.3.3.1",

View File

@@ -1179,13 +1179,3 @@ def test_detected_text(photosdb):
for template, value in TEMPLATE_VALUES_DETECTED_TEXT.items(): for template, value in TEMPLATE_VALUES_DETECTED_TEXT.items():
rendered, _ = photo.render_template(template) rendered, _ = photo.render_template(template)
assert value in "".join(rendered) assert value in "".join(rendered)
def test_detected_text_caching(photosdb):
"""Test {detected_text} template caches values"""
exportdb = ExportDBInMemory(None)
exportdb.set_detected_text_for_uuid(UUID_DETECTED_TEXT, json.dumps([["foo", 0.9]]))
photo = photosdb.get_photo(UUID_DETECTED_TEXT)
options = RenderOptions(exportdb=exportdb)
rendered, _ = photo.render_template("{detected_text}", options=options)
assert rendered[0] == "foo"