Compare commits

..

9 Commits

Author SHA1 Message Date
Rhet Turnbull
673243c6cd Fix for #500, check for macOS version before loading Vision 2021-07-29 21:16:33 -07:00
Rhet Turnbull
7376223eb8 Updated text_detection to detect macOS version 2021-07-29 07:39:01 -07:00
Rhet Turnbull
ecd0b8e22f Updated detected_text docs to make it clear this only works on Catalina+ 2021-07-29 07:03:04 -07:00
Rhet Turnbull
c4a608b5bd Updated CHANGELOG.md [skip ci] 2021-07-29 07:02:38 -07:00
Rhet Turnbull
4d9e21ea16 Added error logging to PhotoInfo.detected_text 2021-07-29 06:32:07 -07:00
Rhet Turnbull
1ee3e035c4 Updated README.md [skip ci] 2021-07-29 06:25:59 -07:00
Rhet Turnbull
b1c0fb3e82 Added error logging to {detected_text} processing, #499 2021-07-29 06:23:02 -07:00
Rhet Turnbull
de715d2afd Updated CHANGELOG.md [skip ci] 2021-07-28 06:36:10 -07:00
Rhet Turnbull
607cf80dda Removed unneeded test file [skip ci] 2021-07-28 06:27:44 -07:00
10 changed files with 98 additions and 29 deletions

View File

@@ -4,6 +4,22 @@ 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).
#### [v0.42.70](https://github.com/RhetTbull/osxphotos/compare/v0.42.69...v0.42.70)
> 29 July 2021
- Added error logging to {detected_text} processing, #499 [`b1c0fb3`](https://github.com/RhetTbull/osxphotos/commit/b1c0fb3e8284600394ddbfdd7dfa94916a843c81)
- Updated README.md [skip ci] [`1ee3e03`](https://github.com/RhetTbull/osxphotos/commit/1ee3e035c42d687158f7cf73382f0f263516dc37)
- Removed unneeded test file [skip ci] [`607cf80`](https://github.com/RhetTbull/osxphotos/commit/607cf80dda37ad529edd91fe92af3885b04b9a37)
#### [v0.42.69](https://github.com/RhetTbull/osxphotos/compare/v0.42.67...v0.42.69)
> 28 July 2021
- Added {detected_text} template [`c233523`](https://github.com/RhetTbull/osxphotos/commit/c2335236be7a1eecf4f25a9dcb844df4d6372b5c)
- Added PhotoInfo.detected_text() [`123340e`](https://github.com/RhetTbull/osxphotos/commit/123340eadabb0fb07209c4207ccad13a53de3619)
- Updated dependencies [`0c8fbd6`](https://github.com/RhetTbull/osxphotos/commit/0c8fbd69af7a0d696de5224bf3c302e0c240905f)
#### [v0.42.67](https://github.com/RhetTbull/osxphotos/compare/v0.42.66...v0.42.67)
> 24 July 2021

View File

@@ -1700,7 +1700,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.42.69'
{osxphotos_version} The osxphotos version, e.g. '0.42.71'
{osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for
@@ -1769,9 +1769,10 @@ Substitution Description
not need to reprocess each photo. You may pass a
confidence threshold value between 0.0 and 1.0 after
a colon as in '{detected_text:0.5}'; The default
confidence threshold is 0.75. Note: this feature is
not the same thing as Live Text in macOS Monterey,
which osxphotos does not yet support.
confidence threshold is 0.75. '{detected_text}' works
only on macOS Catalina (10.15) or later. Note: this
feature is not the same thing as Live Text in macOS
Monterey, which osxphotos does not yet support.
{shell_quote} Use in form '{shell_quote,TEMPLATE}'; quotes the
rendered TEMPLATE value(s) for safe usage in the
shell, e.g. My file.jpeg => 'My file.jpeg'; only adds
@@ -2797,7 +2798,7 @@ If photo is edited, uses the edited photo, otherwise the original; falls back to
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.
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.
### 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:
@@ -3553,7 +3554,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.69'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.71'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{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|
@@ -3568,7 +3569,7 @@ The following template field substitutions are availabe for use the templating s
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.|
|{detected_text}|List of text strings found in the image after performing text detection. Using '{detected_text}' will cause osxphotos to perform text detection on your photos using the built-in macOS text detection algorithms which will slow down your export. The results for each photo will be cached in the export database so that future exports with '--update' do not need to reprocess each photo. You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in '{detected_text:0.5}'; The default confidence threshold is 0.75. Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.|
|{detected_text}|List of text strings found in the image after performing text detection. Using '{detected_text}' will cause osxphotos to perform text detection on your photos using the built-in macOS text detection algorithms which will slow down your export. The results for each photo will be cached in the export database so that future exports with '--update' do not need to reprocess each photo. You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in '{detected_text:0.5}'; The default confidence threshold is 0.75. '{detected_text}' works only on macOS Catalina (10.15) or later. Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.|
|{shell_quote}|Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.|
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.|
<!-- OSXPHOTOS-TEMPLATE-TABLE:END -->

View File

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

View File

@@ -89,11 +89,11 @@ class ExportDB_ABC(ABC):
pass
@abstractmethod
def get_detected_text_for_uuid(self, uuid, confidence):
def get_detected_text_for_uuid(self, uuid):
pass
@abstractmethod
def set_detected_text_for_uuid(self, uuid, text, confidence):
def set_detected_text_for_uuid(self, uuid, json_text):
pass
@abstractmethod
@@ -170,10 +170,10 @@ class ExportDBNoOp(ExportDB_ABC):
def get_previous_uuids(self):
return []
def get_detected_text_for_uuid(self, uuid, confidence):
return []
def get_detected_text_for_uuid(self, uuid):
return None
def set_detected_text_for_uuid(self, uuid, text, confidence):
def set_detected_text_for_uuid(self, uuid, json_text):
pass
def set_data(
@@ -493,7 +493,7 @@ class ExportDB(ExportDB_ABC):
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO detected_text(uuid, text_data) VALUES (?, ?);",
(uuid, text_json),
(uuid, text_json,),
)
conn.commit()
except Error as e:

View File

@@ -1134,6 +1134,7 @@ class PhotoInfo:
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)] = [

View File

@@ -3,6 +3,7 @@
import datetime
import json
import locale
import logging
import os
import pathlib
import shlex
@@ -205,6 +206,7 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
+ "The results for each photo will be cached in the export database so that future exports with '--update' do not need to reprocess each photo. "
+ "You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in '{detected_text:0.5}'; "
+ f"The default confidence threshold is {TEXT_DETECTION_CONFIDENCE_THRESHOLD}. "
+ "'{detected_text}' works only on macOS Catalina (10.15) or later. "
+ "Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.",
"{shell_quote}": "Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.",
"{function}": "Execute a python function from an external file and use return value as template substitution. "
@@ -1444,7 +1446,7 @@ def _get_detected_text(photo, exportdb, confidence=TEXT_DETECTION_CONFIDENCE_THR
)
detected_text = exportdb.get_detected_text_for_uuid(photo.uuid)
if detected_text:
if detected_text is not None:
detected_text = json.loads(detected_text)
else:
path = (
@@ -1459,6 +1461,9 @@ def _get_detected_text(photo, exportdb, confidence=TEXT_DETECTION_CONFIDENCE_THR
try:
detected_text = detect_text(path)
except Exception as e:
detected_text = []
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]

View File

@@ -1,19 +1,33 @@
""" Use Apple's Vision Framework via PyObjC to perform text detection on images """
""" Use Apple's Vision Framework via PyObjC to perform text detection on images (macOS 10.15+ only) """
import logging
from typing import List
import objc
import Quartz
import Vision
from Cocoa import NSURL
from Foundation import NSDictionary
from typing import List
# needed to capture system-level stderr
from wurlitzer import pipes
from .utils import _get_os_version
ver, major, minor = _get_os_version()
if ver == "10" and int(major) < 15:
vision = False
else:
import Vision
vision = True
def detect_text(img_path: str) -> List:
"""process image at img_path with VNRecognizeTextRequest and return list of results"""
if not vision:
logging.warning(f"detect_text not implemented for this version of macOS")
return []
with objc.autorelease_pool():
input_url = NSURL.fileURLWithPath_(img_path)

View File

@@ -1,5 +1,7 @@
""" Test ExportDB """
import json
import pytest
EXIF_DATA = """[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos", "EXIF:ImageDescription": "\u2068Elder Park\u2069, \u2068Adelaide\u2069, \u2068Australia\u2069", "XMP:Description": "\u2068Elder Park\u2069, \u2068Adelaide\u2069, \u2068Australia\u2069", "XMP:Title": "Elder Park", "EXIF:GPSLatitude": "34 deg 55' 8.01\" S", "EXIF:GPSLongitude": "138 deg 35' 48.70\" E", "Composite:GPSPosition": "34 deg 55' 8.01\" S, 138 deg 35' 48.70\" E", "EXIF:GPSLatitudeRef": "South", "EXIF:GPSLongitudeRef": "East", "EXIF:DateTimeOriginal": "2017:06:20 17:18:56", "EXIF:OffsetTimeOriginal": "+09:30", "EXIF:ModifyDate": "2020:05:18 14:42:04"}]"""
@@ -12,10 +14,11 @@ DATABASE_VERSION1 = "tests/export_db_version1.db"
def test_export_db():
""" test ExportDB """
"""test ExportDB"""
import os
import tempfile
from osxphotos.export_db import ExportDB, OSXPHOTOS_EXPORTDB_VERSION
from osxphotos.export_db import OSXPHOTOS_EXPORTDB_VERSION, ExportDB
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
dbname = os.path.join(tempdir.name, ".osxphotos_export.db")
@@ -47,6 +50,9 @@ def test_export_db():
assert db.get_sidecar_for_file(filepath) == (SIDECAR_DATA, (13, 14, 15))
assert db.get_previous_uuids() == ["FOO-BAR"]
db.set_detected_text_for_uuid("FOO-BAR", json.dumps([["foo", 0.5]]))
assert json.loads(db.get_detected_text_for_uuid("FOO-BAR")) == [["foo", 0.5]]
# test set_data which sets all at the same time
filepath2 = os.path.join(tempdir.name, "test2.jpg")
db.set_data(
@@ -80,6 +86,7 @@ def test_export_db():
assert db.get_stat_converted_for_file(filepath2) == (7, 8, 9)
assert db.get_stat_edited_for_file(filepath2) == (10, 11, 12)
assert sorted(db.get_previous_uuids()) == (["BAR-FOO", "FOO-BAR"])
assert json.loads(db.get_detected_text_for_uuid("FOO-BAR")) == [["foo", 0.5]]
# update data
db.set_uuid_for_file(filepath, "FUBAR")
@@ -88,10 +95,11 @@ def test_export_db():
def test_export_db_no_op():
""" test ExportDBNoOp """
"""test ExportDBNoOp"""
import os
import tempfile
from osxphotos.export_db import ExportDBNoOp, OSXPHOTOS_EXPORTDB_VERSION
from osxphotos.export_db import OSXPHOTOS_EXPORTDB_VERSION, ExportDBNoOp
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
db = ExportDBNoOp()
@@ -121,6 +129,9 @@ def test_export_db_no_op():
assert db.get_sidecar_for_file(filepath) == (None, (None, None, None))
assert db.get_previous_uuids() == []
db.set_detected_text_for_uuid("FOO-BAR", json.dumps([["foo", 0.5]]))
assert db.get_detected_text_for_uuid("FOO-BAR") is None
# test set_data which sets all at the same time
filepath2 = os.path.join(tempdir.name, "test2.jpg")
db.set_data(
@@ -148,13 +159,14 @@ def test_export_db_no_op():
def test_export_db_in_memory():
""" test ExportDBInMemory """
"""test ExportDBInMemory"""
import os
import tempfile
from osxphotos.export_db import (
OSXPHOTOS_EXPORTDB_VERSION,
ExportDB,
ExportDBInMemory,
OSXPHOTOS_EXPORTDB_VERSION,
)
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
@@ -174,6 +186,7 @@ def test_export_db_in_memory():
db.set_stat_edited_for_file(filepath, (10, 11, 12))
db.set_sidecar_for_file(filepath, SIDECAR_DATA, (13, 14, 15))
assert db.get_previous_uuids() == ["FOO-BAR"]
db.set_detected_text_for_uuid("FOO-BAR", json.dumps([["foo", 0.5]]))
db.close()
@@ -192,6 +205,7 @@ def test_export_db_in_memory():
assert dbram.get_stat_edited_for_file(filepath) == (10, 11, 12)
assert dbram.get_sidecar_for_file(filepath) == (SIDECAR_DATA, (13, 14, 15))
assert dbram.get_previous_uuids() == ["FOO-BAR"]
assert json.loads(dbram.get_detected_text_for_uuid("FOO-BAR")) == [["foo", 0.5]]
# change a value
dbram.set_uuid_for_file(filepath, "FUBAR")
@@ -202,6 +216,7 @@ def test_export_db_in_memory():
dbram.set_stat_converted_for_file(filepath, (1, 2, 3))
dbram.set_stat_edited_for_file(filepath, (4, 5, 6))
dbram.set_sidecar_for_file(filepath, "FUBAR", (20, 21, 22))
dbram.set_detected_text_for_uuid("FUBAR", json.dumps([["bar", 0.5]]))
assert dbram.get_uuid_for_file(filepath_lower) == "FUBAR"
assert dbram.get_info_for_uuid("FUBAR") == INFO_DATA2
@@ -212,6 +227,7 @@ def test_export_db_in_memory():
assert dbram.get_stat_edited_for_file(filepath) == (4, 5, 6)
assert dbram.get_sidecar_for_file(filepath) == ("FUBAR", (20, 21, 22))
assert dbram.get_previous_uuids() == ["FUBAR"]
assert json.loads(dbram.get_detected_text_for_uuid("FUBAR")) == [["bar", 0.5]]
dbram.close()
@@ -228,13 +244,15 @@ def test_export_db_in_memory():
assert db.get_previous_uuids() == ["FOO-BAR"]
assert db.get_info_for_uuid("FUBAR") is None
assert db.get_detected_text_for_uuid("FUBAR") is None
def test_export_db_in_memory_nofile():
""" test ExportDBInMemory with no dbfile """
"""test ExportDBInMemory with no dbfile"""
import os
import tempfile
from osxphotos.export_db import ExportDBInMemory, OSXPHOTOS_EXPORTDB_VERSION
from osxphotos.export_db import OSXPHOTOS_EXPORTDB_VERSION, ExportDBInMemory
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
filepath = os.path.join(tempdir.name, "test.JPG")
@@ -254,6 +272,7 @@ def test_export_db_in_memory_nofile():
dbram.set_stat_converted_for_file(filepath, (1, 2, 3))
dbram.set_stat_edited_for_file(filepath, (4, 5, 6))
dbram.set_sidecar_for_file(filepath, "FUBAR", (20, 21, 22))
dbram.set_detected_text_for_uuid("FUBAR", json.dumps([["bar", 0.5]]))
assert dbram.get_uuid_for_file(filepath_lower) == "FUBAR"
assert dbram.get_info_for_uuid("FUBAR") == INFO_DATA2
@@ -264,5 +283,6 @@ def test_export_db_in_memory_nofile():
assert dbram.get_stat_edited_for_file(filepath) == (4, 5, 6)
assert dbram.get_sidecar_for_file(filepath) == ("FUBAR", (20, 21, 22))
assert dbram.get_previous_uuids() == ["FUBAR"]
assert json.loads(dbram.get_detected_text_for_uuid("FUBAR")) == [["bar", 0.5]]
dbram.close()

View File

@@ -1,4 +1,5 @@
""" Test template.py """
import json
import os
import re
@@ -7,6 +8,7 @@ from photoinfo_mock import PhotoInfoMock
import osxphotos
from osxphotos.exiftool import get_exiftool_path
from osxphotos.export_db import ExportDBInMemory
from osxphotos.phototemplate import (
TEMPLATE_SUBSTITUTIONS,
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
@@ -1177,3 +1179,13 @@ def test_detected_text(photosdb):
for template, value in TEMPLATE_VALUES_DETECTED_TEXT.items():
rendered, _ = photo.render_template(template)
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"