Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2041789ff4 | ||
|
|
aec86f93ea | ||
|
|
57bfb03e05 | ||
|
|
c2b2476e38 | ||
|
|
fa2027d453 | ||
|
|
9d980e4917 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -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
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -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
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.42.71"
|
__version__ = "0.42.73"
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user