From 8410f4516e58f43d011e69863ceaf48a6bd1f0be Mon Sep 17 00:00:00 2001 From: Mark Cottman-Fields Date: Sun, 28 May 2023 19:45:31 +1000 Subject: [PATCH] docs: regenerate docs --- docs/leaf_focus/ocr/keras_ocr.html | 1486 ++++++++++++++-------------- 1 file changed, 749 insertions(+), 737 deletions(-) diff --git a/docs/leaf_focus/ocr/keras_ocr.html b/docs/leaf_focus/ocr/keras_ocr.html index 3f76f54..f660033 100644 --- a/docs/leaf_focus/ocr/keras_ocr.html +++ b/docs/leaf_focus/ocr/keras_ocr.html @@ -134,269 +134,273 @@

48 49 tf.get_logger().setLevel(log_level) 50 - 51 try: - 52 import keras_ocr # pylint: disable=import-outside-toplevel - 53 except ModuleNotFoundError as error: - 54 msg = "Cannot run ocr on this Python version." - 55 logger.error(msg) - 56 raise utils.LeafFocusError(msg) from error - 57 - 58 # TODO: allow specifying path to weights files for detector - 59 # detector_weights_path = "" - 60 # detector = keras_ocr.detection.Detector(weights=None) - 61 # detector.model = keras_ocr.detection.build_keras_model( - 62 # weights_path=detector_weights_path, backbone_name="vgg" - 63 # ) - 64 # detector.model.compile(loss="mse", optimizer="adam") - 65 detector = None - 66 - 67 # TODO: allow specifying path to weights files for recogniser - 68 # recognizer_weights_path = "" - 69 # recognizer = keras_ocr.recognition.Recognizer( - 70 # alphabet=keras_ocr.recognition.DEFAULT_ALPHABET, weights=None - 71 # ) - 72 # recognizer.model.load_weights(recognizer_weights_path) - 73 recognizer = None - 74 - 75 # see: https://github.com/faustomorales/keras-ocr - 76 # keras-ocr will automatically download pretrained - 77 # weights for the detector and recognizer. - 78 self._pipeline = keras_ocr.pipeline.Pipeline( - 79 detector=detector, - 80 recognizer=recognizer, - 81 ) - 82 - 83 def engine_run( - 84 self, - 85 image_file: pathlib.Path, - 86 ) -> typing.Tuple[typing.List, typing.Any]: - 87 """Run the recognition engine. - 88 - 89 Args: - 90 image_file: The path to the image file. - 91 - 92 Returns: - 93 typing.Tuple[typing.List, typing.Any]: The list of images - 94 and list of recognition results. - 95 """ - 96 try: - 97 import keras_ocr # pylint: disable=import-outside-toplevel - 98 except ModuleNotFoundError as error: - 99 msg = "Cannot run ocr on this Python version." -100 logger.error(msg) -101 raise utils.LeafFocusError(msg) from error -102 -103 self.engine_create() -104 -105 if not self._pipeline: -106 msg = "Keras OCR pipeline has not been initialised yet." -107 logger.error(msg) -108 raise utils.LeafFocusError(msg) -109 -110 images = [keras_ocr.tools.read(str(image_file))] -111 return images, self._pipeline.recognize(images) -112 -113 def engine_annotate( -114 self, -115 image: typing.Optional[np.ndarray], -116 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], -117 axis, -118 ) -> None: -119 """Run the annotation engine. -120 -121 Args: -122 image: The image data. -123 predictions: The recognised text from the image. -124 axis: The plot axis for drawing annotations. -125 -126 Returns: -127 None -128 """ -129 try: -130 import keras_ocr # pylint: disable=import-outside-toplevel -131 except ModuleNotFoundError as error: -132 msg = "Cannot run ocr on this Python version." -133 logger.error(msg) -134 raise utils.LeafFocusError(msg) from error -135 -136 keras_ocr.tools.drawAnnotations(image=image, predictions=predictions, ax=axis) -137 -138 def recognise_text( -139 self, -140 image_file: pathlib.Path, -141 output_dir: pathlib.Path, -142 ) -> model.KerasOcrResult: -143 """Recognise text in an image file. -144 -145 Args: -146 image_file: The path to the image file. -147 output_dir: The directory to write the results. + 51 # check the CPU / GPU in use + 52 gpus = tf.config.list_physical_devices("GPU") + 53 logger.info("GPUs in use: '%s'.", gpus) + 54 + 55 try: + 56 import keras_ocr # pylint: disable=import-outside-toplevel + 57 except ModuleNotFoundError as error: + 58 msg = "Cannot run ocr on this Python version." + 59 logger.error(msg) + 60 raise utils.LeafFocusError(msg) from error + 61 + 62 # TODO: allow specifying path to weights files for detector + 63 # detector_weights_path = "" + 64 # detector = keras_ocr.detection.Detector(weights=None) + 65 # detector.model = keras_ocr.detection.build_keras_model( + 66 # weights_path=detector_weights_path, backbone_name="vgg" + 67 # ) + 68 # detector.model.compile(loss="mse", optimizer="adam") + 69 detector = None + 70 + 71 # TODO: allow specifying path to weights files for recogniser + 72 # recognizer_weights_path = "" + 73 # recognizer = keras_ocr.recognition.Recognizer( + 74 # alphabet=keras_ocr.recognition.DEFAULT_ALPHABET, weights=None + 75 # ) + 76 # recognizer.model.load_weights(recognizer_weights_path) + 77 recognizer = None + 78 + 79 # see: https://github.com/faustomorales/keras-ocr + 80 # keras-ocr will automatically download pretrained + 81 # weights for the detector and recognizer. + 82 self._pipeline = keras_ocr.pipeline.Pipeline( + 83 detector=detector, + 84 recognizer=recognizer, + 85 ) + 86 + 87 def engine_run( + 88 self, + 89 image_file: pathlib.Path, + 90 ) -> typing.Tuple[typing.List, typing.Any]: + 91 """Run the recognition engine. + 92 + 93 Args: + 94 image_file: The path to the image file. + 95 + 96 Returns: + 97 typing.Tuple[typing.List, typing.Any]: The list of images + 98 and list of recognition results. + 99 """ +100 try: +101 import keras_ocr # pylint: disable=import-outside-toplevel +102 except ModuleNotFoundError as error: +103 msg = "Cannot run ocr on this Python version." +104 logger.error(msg) +105 raise utils.LeafFocusError(msg) from error +106 +107 self.engine_create() +108 +109 if not self._pipeline: +110 msg = "Keras OCR pipeline has not been initialised yet." +111 logger.error(msg) +112 raise utils.LeafFocusError(msg) +113 +114 images = [keras_ocr.tools.read(str(image_file))] +115 return images, self._pipeline.recognize(images) +116 +117 def engine_annotate( +118 self, +119 image: typing.Optional[np.ndarray], +120 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], +121 axis, +122 ) -> None: +123 """Run the annotation engine. +124 +125 Args: +126 image: The image data. +127 predictions: The recognised text from the image. +128 axis: The plot axis for drawing annotations. +129 +130 Returns: +131 None +132 """ +133 try: +134 import keras_ocr # pylint: disable=import-outside-toplevel +135 except ModuleNotFoundError as error: +136 msg = "Cannot run ocr on this Python version." +137 logger.error(msg) +138 raise utils.LeafFocusError(msg) from error +139 +140 keras_ocr.tools.drawAnnotations(image=image, predictions=predictions, ax=axis) +141 +142 def recognise_text( +143 self, +144 image_file: pathlib.Path, +145 output_dir: pathlib.Path, +146 ) -> model.KerasOcrResult: +147 """Recognise text in an image file. 148 -149 Returns: -150 model.KerasOcrResult: The text recognition results. -151 """ -152 if not image_file: -153 msg = "Must supply image file." -154 raise utils.LeafFocusError(msg) -155 if not output_dir: -156 msg = "Must supply output directory." -157 raise utils.LeafFocusError(msg) -158 if not image_file.exists(): -159 msg = f"Image file does not exist '{image_file}'." -160 raise utils.LeafFocusError(msg) from FileNotFoundError(image_file) -161 -162 # check if output files already exist -163 annotations_file = utils.output_root(image_file, "annotations", output_dir) -164 annotations_file = annotations_file.with_suffix(".png") +149 Args: +150 image_file: The path to the image file. +151 output_dir: The directory to write the results. +152 +153 Returns: +154 model.KerasOcrResult: The text recognition results. +155 """ +156 if not image_file: +157 msg = "Must supply image file." +158 raise utils.LeafFocusError(msg) +159 if not output_dir: +160 msg = "Must supply output directory." +161 raise utils.LeafFocusError(msg) +162 if not image_file.exists(): +163 msg = f"Image file does not exist '{image_file}'." +164 raise utils.LeafFocusError(msg) from FileNotFoundError(image_file) 165 -166 predictions_file = utils.output_root(image_file, "predictions", output_dir) -167 predictions_file = predictions_file.with_suffix(".csv") -168 -169 result = model.KerasOcrResult( -170 output_dir=output_dir, -171 annotations_file=annotations_file, -172 predictions_file=predictions_file, -173 items=[], -174 ) -175 -176 if annotations_file.exists() and predictions_file.exists(): -177 logger.debug( -178 "Predictions and annotations files already exist for '%s'.", -179 image_file.stem, -180 ) -181 all_items = list(model.TextItem.load(predictions_file)) -182 result.items = model.TextItem.order_text_lines(all_items) -183 return result -184 -185 # read in the image -186 logger.debug( -187 "Creating predictions and annotations files for '%s'.", -188 image_file.stem, -189 ) -190 -191 # Each list of predictions in prediction_groups is a list of -192 # (word, box) tuples. -193 images, prediction_groups = self.engine_run(image_file) +166 # check if output files already exist +167 annotations_file = utils.output_root(image_file, "annotations", output_dir) +168 annotations_file = annotations_file.with_suffix(".png") +169 +170 predictions_file = utils.output_root(image_file, "predictions", output_dir) +171 predictions_file = predictions_file.with_suffix(".csv") +172 +173 result = model.KerasOcrResult( +174 output_dir=output_dir, +175 annotations_file=annotations_file, +176 predictions_file=predictions_file, +177 items=[], +178 ) +179 +180 if annotations_file.exists() and predictions_file.exists(): +181 logger.debug( +182 "Predictions and annotations files already exist for '%s'.", +183 image_file.stem, +184 ) +185 all_items = list(model.TextItem.load(predictions_file)) +186 result.items = model.TextItem.order_text_lines(all_items) +187 return result +188 +189 # read in the image +190 logger.debug( +191 "Creating predictions and annotations files for '%s'.", +192 image_file.stem, +193 ) 194 -195 # Plot and save the predictions -196 for image, predictions in zip(images, prediction_groups): -197 self.save_figure(annotations_file, image, predictions) +195 # Each list of predictions in prediction_groups is a list of +196 # (word, box) tuples. +197 images, prediction_groups = self.engine_run(image_file) 198 -199 items = self.convert_predictions(predictions) -200 self.save_items(predictions_file, [item for line in items for item in line]) -201 result.items = items +199 # Plot and save the predictions +200 for image, predictions in zip(images, prediction_groups): +201 self.save_figure(annotations_file, image, predictions) 202 -203 return result -204 -205 def save_figure( -206 self, -207 annotation_file: pathlib.Path, -208 image: typing.Optional[np.ndarray], -209 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], -210 ) -> None: -211 """Save the annotated image. -212 -213 Args: -214 annotation_file: The path to the file containing annotations. -215 image: The image data. -216 predictions: The text recognition results. -217 -218 Returns: -219 None -220 """ -221 if not annotation_file: -222 msg = "Must supply annotation file." -223 raise utils.LeafFocusError(msg) -224 -225 expected_image_shape = 3 -226 if image is None or image.size < 1 or len(image.shape) != expected_image_shape: -227 msg_image = image.shape if image is not None else None -228 msg = f"Must supply valid image data, not '{msg_image}'." -229 raise utils.LeafFocusError(msg) -230 if not predictions: -231 predictions = [] -232 -233 logger.info("Saving OCR image to '%s'.", annotation_file) -234 -235 import matplotlib as mpl # pylint: disable=import-outside-toplevel -236 from matplotlib import pyplot as plt # pylint: disable=import-outside-toplevel -237 -238 mpl.use("agg") -239 -240 annotation_file.parent.mkdir(exist_ok=True, parents=True) +203 items = self.convert_predictions(predictions) +204 self.save_items(predictions_file, [item for line in items for item in line]) +205 result.items = items +206 +207 return result +208 +209 def save_figure( +210 self, +211 annotation_file: pathlib.Path, +212 image: typing.Optional[np.ndarray], +213 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], +214 ) -> None: +215 """Save the annotated image. +216 +217 Args: +218 annotation_file: The path to the file containing annotations. +219 image: The image data. +220 predictions: The text recognition results. +221 +222 Returns: +223 None +224 """ +225 if not annotation_file: +226 msg = "Must supply annotation file." +227 raise utils.LeafFocusError(msg) +228 +229 expected_image_shape = 3 +230 if image is None or image.size < 1 or len(image.shape) != expected_image_shape: +231 msg_image = image.shape if image is not None else None +232 msg = f"Must supply valid image data, not '{msg_image}'." +233 raise utils.LeafFocusError(msg) +234 if not predictions: +235 predictions = [] +236 +237 logger.info("Saving OCR image to '%s'.", annotation_file) +238 +239 import matplotlib as mpl # pylint: disable=import-outside-toplevel +240 from matplotlib import pyplot as plt # pylint: disable=import-outside-toplevel 241 -242 fig, axis = plt.subplots(figsize=(20, 20)) +242 mpl.use("agg") 243 -244 self.engine_annotate(image, predictions, axis) +244 annotation_file.parent.mkdir(exist_ok=True, parents=True) 245 -246 fig.savefig(str(annotation_file)) -247 plt.close(fig) -248 -249 def convert_predictions( -250 self, -251 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], -252 ) -> typing.List[typing.List[model.TextItem]]: -253 """Convert predictions to items. -254 -255 Args: -256 predictions: The list of recognised text. -257 -258 Returns: -259 typing.List[typing.List[model.TextItem]]: The equivalent text items. -260 """ -261 if not predictions: -262 predictions = [] -263 -264 items = [] -265 for prediction in predictions: -266 items.append(model.TextItem.from_prediction(prediction)) +246 fig, axis = plt.subplots(figsize=(20, 20)) +247 +248 self.engine_annotate(image, predictions, axis) +249 +250 fig.savefig(str(annotation_file)) +251 plt.close(fig) +252 +253 def convert_predictions( +254 self, +255 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], +256 ) -> typing.List[typing.List[model.TextItem]]: +257 """Convert predictions to items. +258 +259 Args: +260 predictions: The list of recognised text. +261 +262 Returns: +263 typing.List[typing.List[model.TextItem]]: The equivalent text items. +264 """ +265 if not predictions: +266 predictions = [] 267 -268 # order_text_lines sets the line number and line order -269 line_items = model.TextItem.order_text_lines(items) -270 -271 return line_items -272 -273 def save_items( -274 self, -275 items_file: pathlib.Path, -276 items: typing.Iterable[model.TextItem], -277 ) -> None: -278 """Save items to csv file. -279 -280 Args: -281 items_file: Write the text items to this file. -282 items: The text items to save. +268 items = [] +269 for prediction in predictions: +270 items.append(model.TextItem.from_prediction(prediction)) +271 +272 # order_text_lines sets the line number and line order +273 line_items = model.TextItem.order_text_lines(items) +274 +275 return line_items +276 +277 def save_items( +278 self, +279 items_file: pathlib.Path, +280 items: typing.Iterable[model.TextItem], +281 ) -> None: +282 """Save items to csv file. 283 -284 Returns: -285 None -286 """ -287 if not items_file: -288 msg = "Must supply predictions file." -289 raise utils.LeafFocusError(msg) -290 if not items: -291 msg = "Must supply predictions data." -292 raise utils.LeafFocusError(msg) -293 -294 logger.info("Saving OCR predictions to '%s'.", items_file) -295 -296 items_list = list(items) -297 model.TextItem.save(items_file, items_list) -298 -299 def _build_name(self, prefix: str, middle: str, suffix: str): -300 """Build the file name. -301 -302 Args: -303 prefix: The text to add at the start. -304 middle: The text to add in the middle. -305 suffix: The text to add at the end. -306 -307 Returns: -308 str: The built name. -309 """ -310 prefix = prefix.strip("-") -311 middle = middle.strip("-") -312 suffix = suffix if suffix.startswith(".") else "." + suffix -313 return f"{prefix}-{middle}" + suffix +284 Args: +285 items_file: Write the text items to this file. +286 items: The text items to save. +287 +288 Returns: +289 None +290 """ +291 if not items_file: +292 msg = "Must supply predictions file." +293 raise utils.LeafFocusError(msg) +294 if not items: +295 msg = "Must supply predictions data." +296 raise utils.LeafFocusError(msg) +297 +298 logger.info("Saving OCR predictions to '%s'.", items_file) +299 +300 items_list = list(items) +301 model.TextItem.save(items_file, items_list) +302 +303 def _build_name(self, prefix: str, middle: str, suffix: str): +304 """Build the file name. +305 +306 Args: +307 prefix: The text to add at the start. +308 middle: The text to add in the middle. +309 suffix: The text to add at the end. +310 +311 Returns: +312 str: The built name. +313 """ +314 prefix = prefix.strip("-") +315 middle = middle.strip("-") +316 suffix = suffix if suffix.startswith(".") else "." + suffix +317 return f"{prefix}-{middle}" + suffix @@ -448,269 +452,273 @@

49 50 tf.get_logger().setLevel(log_level) 51 - 52 try: - 53 import keras_ocr # pylint: disable=import-outside-toplevel - 54 except ModuleNotFoundError as error: - 55 msg = "Cannot run ocr on this Python version." - 56 logger.error(msg) - 57 raise utils.LeafFocusError(msg) from error - 58 - 59 # TODO: allow specifying path to weights files for detector - 60 # detector_weights_path = "" - 61 # detector = keras_ocr.detection.Detector(weights=None) - 62 # detector.model = keras_ocr.detection.build_keras_model( - 63 # weights_path=detector_weights_path, backbone_name="vgg" - 64 # ) - 65 # detector.model.compile(loss="mse", optimizer="adam") - 66 detector = None - 67 - 68 # TODO: allow specifying path to weights files for recogniser - 69 # recognizer_weights_path = "" - 70 # recognizer = keras_ocr.recognition.Recognizer( - 71 # alphabet=keras_ocr.recognition.DEFAULT_ALPHABET, weights=None - 72 # ) - 73 # recognizer.model.load_weights(recognizer_weights_path) - 74 recognizer = None - 75 - 76 # see: https://github.com/faustomorales/keras-ocr - 77 # keras-ocr will automatically download pretrained - 78 # weights for the detector and recognizer. - 79 self._pipeline = keras_ocr.pipeline.Pipeline( - 80 detector=detector, - 81 recognizer=recognizer, - 82 ) - 83 - 84 def engine_run( - 85 self, - 86 image_file: pathlib.Path, - 87 ) -> typing.Tuple[typing.List, typing.Any]: - 88 """Run the recognition engine. - 89 - 90 Args: - 91 image_file: The path to the image file. - 92 - 93 Returns: - 94 typing.Tuple[typing.List, typing.Any]: The list of images - 95 and list of recognition results. - 96 """ - 97 try: - 98 import keras_ocr # pylint: disable=import-outside-toplevel - 99 except ModuleNotFoundError as error: -100 msg = "Cannot run ocr on this Python version." -101 logger.error(msg) -102 raise utils.LeafFocusError(msg) from error -103 -104 self.engine_create() -105 -106 if not self._pipeline: -107 msg = "Keras OCR pipeline has not been initialised yet." -108 logger.error(msg) -109 raise utils.LeafFocusError(msg) -110 -111 images = [keras_ocr.tools.read(str(image_file))] -112 return images, self._pipeline.recognize(images) -113 -114 def engine_annotate( -115 self, -116 image: typing.Optional[np.ndarray], -117 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], -118 axis, -119 ) -> None: -120 """Run the annotation engine. -121 -122 Args: -123 image: The image data. -124 predictions: The recognised text from the image. -125 axis: The plot axis for drawing annotations. -126 -127 Returns: -128 None -129 """ -130 try: -131 import keras_ocr # pylint: disable=import-outside-toplevel -132 except ModuleNotFoundError as error: -133 msg = "Cannot run ocr on this Python version." -134 logger.error(msg) -135 raise utils.LeafFocusError(msg) from error -136 -137 keras_ocr.tools.drawAnnotations(image=image, predictions=predictions, ax=axis) -138 -139 def recognise_text( -140 self, -141 image_file: pathlib.Path, -142 output_dir: pathlib.Path, -143 ) -> model.KerasOcrResult: -144 """Recognise text in an image file. -145 -146 Args: -147 image_file: The path to the image file. -148 output_dir: The directory to write the results. + 52 # check the CPU / GPU in use + 53 gpus = tf.config.list_physical_devices("GPU") + 54 logger.info("GPUs in use: '%s'.", gpus) + 55 + 56 try: + 57 import keras_ocr # pylint: disable=import-outside-toplevel + 58 except ModuleNotFoundError as error: + 59 msg = "Cannot run ocr on this Python version." + 60 logger.error(msg) + 61 raise utils.LeafFocusError(msg) from error + 62 + 63 # TODO: allow specifying path to weights files for detector + 64 # detector_weights_path = "" + 65 # detector = keras_ocr.detection.Detector(weights=None) + 66 # detector.model = keras_ocr.detection.build_keras_model( + 67 # weights_path=detector_weights_path, backbone_name="vgg" + 68 # ) + 69 # detector.model.compile(loss="mse", optimizer="adam") + 70 detector = None + 71 + 72 # TODO: allow specifying path to weights files for recogniser + 73 # recognizer_weights_path = "" + 74 # recognizer = keras_ocr.recognition.Recognizer( + 75 # alphabet=keras_ocr.recognition.DEFAULT_ALPHABET, weights=None + 76 # ) + 77 # recognizer.model.load_weights(recognizer_weights_path) + 78 recognizer = None + 79 + 80 # see: https://github.com/faustomorales/keras-ocr + 81 # keras-ocr will automatically download pretrained + 82 # weights for the detector and recognizer. + 83 self._pipeline = keras_ocr.pipeline.Pipeline( + 84 detector=detector, + 85 recognizer=recognizer, + 86 ) + 87 + 88 def engine_run( + 89 self, + 90 image_file: pathlib.Path, + 91 ) -> typing.Tuple[typing.List, typing.Any]: + 92 """Run the recognition engine. + 93 + 94 Args: + 95 image_file: The path to the image file. + 96 + 97 Returns: + 98 typing.Tuple[typing.List, typing.Any]: The list of images + 99 and list of recognition results. +100 """ +101 try: +102 import keras_ocr # pylint: disable=import-outside-toplevel +103 except ModuleNotFoundError as error: +104 msg = "Cannot run ocr on this Python version." +105 logger.error(msg) +106 raise utils.LeafFocusError(msg) from error +107 +108 self.engine_create() +109 +110 if not self._pipeline: +111 msg = "Keras OCR pipeline has not been initialised yet." +112 logger.error(msg) +113 raise utils.LeafFocusError(msg) +114 +115 images = [keras_ocr.tools.read(str(image_file))] +116 return images, self._pipeline.recognize(images) +117 +118 def engine_annotate( +119 self, +120 image: typing.Optional[np.ndarray], +121 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], +122 axis, +123 ) -> None: +124 """Run the annotation engine. +125 +126 Args: +127 image: The image data. +128 predictions: The recognised text from the image. +129 axis: The plot axis for drawing annotations. +130 +131 Returns: +132 None +133 """ +134 try: +135 import keras_ocr # pylint: disable=import-outside-toplevel +136 except ModuleNotFoundError as error: +137 msg = "Cannot run ocr on this Python version." +138 logger.error(msg) +139 raise utils.LeafFocusError(msg) from error +140 +141 keras_ocr.tools.drawAnnotations(image=image, predictions=predictions, ax=axis) +142 +143 def recognise_text( +144 self, +145 image_file: pathlib.Path, +146 output_dir: pathlib.Path, +147 ) -> model.KerasOcrResult: +148 """Recognise text in an image file. 149 -150 Returns: -151 model.KerasOcrResult: The text recognition results. -152 """ -153 if not image_file: -154 msg = "Must supply image file." -155 raise utils.LeafFocusError(msg) -156 if not output_dir: -157 msg = "Must supply output directory." -158 raise utils.LeafFocusError(msg) -159 if not image_file.exists(): -160 msg = f"Image file does not exist '{image_file}'." -161 raise utils.LeafFocusError(msg) from FileNotFoundError(image_file) -162 -163 # check if output files already exist -164 annotations_file = utils.output_root(image_file, "annotations", output_dir) -165 annotations_file = annotations_file.with_suffix(".png") +150 Args: +151 image_file: The path to the image file. +152 output_dir: The directory to write the results. +153 +154 Returns: +155 model.KerasOcrResult: The text recognition results. +156 """ +157 if not image_file: +158 msg = "Must supply image file." +159 raise utils.LeafFocusError(msg) +160 if not output_dir: +161 msg = "Must supply output directory." +162 raise utils.LeafFocusError(msg) +163 if not image_file.exists(): +164 msg = f"Image file does not exist '{image_file}'." +165 raise utils.LeafFocusError(msg) from FileNotFoundError(image_file) 166 -167 predictions_file = utils.output_root(image_file, "predictions", output_dir) -168 predictions_file = predictions_file.with_suffix(".csv") -169 -170 result = model.KerasOcrResult( -171 output_dir=output_dir, -172 annotations_file=annotations_file, -173 predictions_file=predictions_file, -174 items=[], -175 ) -176 -177 if annotations_file.exists() and predictions_file.exists(): -178 logger.debug( -179 "Predictions and annotations files already exist for '%s'.", -180 image_file.stem, -181 ) -182 all_items = list(model.TextItem.load(predictions_file)) -183 result.items = model.TextItem.order_text_lines(all_items) -184 return result -185 -186 # read in the image -187 logger.debug( -188 "Creating predictions and annotations files for '%s'.", -189 image_file.stem, -190 ) -191 -192 # Each list of predictions in prediction_groups is a list of -193 # (word, box) tuples. -194 images, prediction_groups = self.engine_run(image_file) +167 # check if output files already exist +168 annotations_file = utils.output_root(image_file, "annotations", output_dir) +169 annotations_file = annotations_file.with_suffix(".png") +170 +171 predictions_file = utils.output_root(image_file, "predictions", output_dir) +172 predictions_file = predictions_file.with_suffix(".csv") +173 +174 result = model.KerasOcrResult( +175 output_dir=output_dir, +176 annotations_file=annotations_file, +177 predictions_file=predictions_file, +178 items=[], +179 ) +180 +181 if annotations_file.exists() and predictions_file.exists(): +182 logger.debug( +183 "Predictions and annotations files already exist for '%s'.", +184 image_file.stem, +185 ) +186 all_items = list(model.TextItem.load(predictions_file)) +187 result.items = model.TextItem.order_text_lines(all_items) +188 return result +189 +190 # read in the image +191 logger.debug( +192 "Creating predictions and annotations files for '%s'.", +193 image_file.stem, +194 ) 195 -196 # Plot and save the predictions -197 for image, predictions in zip(images, prediction_groups): -198 self.save_figure(annotations_file, image, predictions) +196 # Each list of predictions in prediction_groups is a list of +197 # (word, box) tuples. +198 images, prediction_groups = self.engine_run(image_file) 199 -200 items = self.convert_predictions(predictions) -201 self.save_items(predictions_file, [item for line in items for item in line]) -202 result.items = items +200 # Plot and save the predictions +201 for image, predictions in zip(images, prediction_groups): +202 self.save_figure(annotations_file, image, predictions) 203 -204 return result -205 -206 def save_figure( -207 self, -208 annotation_file: pathlib.Path, -209 image: typing.Optional[np.ndarray], -210 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], -211 ) -> None: -212 """Save the annotated image. -213 -214 Args: -215 annotation_file: The path to the file containing annotations. -216 image: The image data. -217 predictions: The text recognition results. -218 -219 Returns: -220 None -221 """ -222 if not annotation_file: -223 msg = "Must supply annotation file." -224 raise utils.LeafFocusError(msg) -225 -226 expected_image_shape = 3 -227 if image is None or image.size < 1 or len(image.shape) != expected_image_shape: -228 msg_image = image.shape if image is not None else None -229 msg = f"Must supply valid image data, not '{msg_image}'." -230 raise utils.LeafFocusError(msg) -231 if not predictions: -232 predictions = [] -233 -234 logger.info("Saving OCR image to '%s'.", annotation_file) -235 -236 import matplotlib as mpl # pylint: disable=import-outside-toplevel -237 from matplotlib import pyplot as plt # pylint: disable=import-outside-toplevel -238 -239 mpl.use("agg") -240 -241 annotation_file.parent.mkdir(exist_ok=True, parents=True) +204 items = self.convert_predictions(predictions) +205 self.save_items(predictions_file, [item for line in items for item in line]) +206 result.items = items +207 +208 return result +209 +210 def save_figure( +211 self, +212 annotation_file: pathlib.Path, +213 image: typing.Optional[np.ndarray], +214 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], +215 ) -> None: +216 """Save the annotated image. +217 +218 Args: +219 annotation_file: The path to the file containing annotations. +220 image: The image data. +221 predictions: The text recognition results. +222 +223 Returns: +224 None +225 """ +226 if not annotation_file: +227 msg = "Must supply annotation file." +228 raise utils.LeafFocusError(msg) +229 +230 expected_image_shape = 3 +231 if image is None or image.size < 1 or len(image.shape) != expected_image_shape: +232 msg_image = image.shape if image is not None else None +233 msg = f"Must supply valid image data, not '{msg_image}'." +234 raise utils.LeafFocusError(msg) +235 if not predictions: +236 predictions = [] +237 +238 logger.info("Saving OCR image to '%s'.", annotation_file) +239 +240 import matplotlib as mpl # pylint: disable=import-outside-toplevel +241 from matplotlib import pyplot as plt # pylint: disable=import-outside-toplevel 242 -243 fig, axis = plt.subplots(figsize=(20, 20)) +243 mpl.use("agg") 244 -245 self.engine_annotate(image, predictions, axis) +245 annotation_file.parent.mkdir(exist_ok=True, parents=True) 246 -247 fig.savefig(str(annotation_file)) -248 plt.close(fig) -249 -250 def convert_predictions( -251 self, -252 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], -253 ) -> typing.List[typing.List[model.TextItem]]: -254 """Convert predictions to items. -255 -256 Args: -257 predictions: The list of recognised text. -258 -259 Returns: -260 typing.List[typing.List[model.TextItem]]: The equivalent text items. -261 """ -262 if not predictions: -263 predictions = [] -264 -265 items = [] -266 for prediction in predictions: -267 items.append(model.TextItem.from_prediction(prediction)) +247 fig, axis = plt.subplots(figsize=(20, 20)) +248 +249 self.engine_annotate(image, predictions, axis) +250 +251 fig.savefig(str(annotation_file)) +252 plt.close(fig) +253 +254 def convert_predictions( +255 self, +256 predictions: typing.List[typing.Tuple[typing.Any, typing.Any]], +257 ) -> typing.List[typing.List[model.TextItem]]: +258 """Convert predictions to items. +259 +260 Args: +261 predictions: The list of recognised text. +262 +263 Returns: +264 typing.List[typing.List[model.TextItem]]: The equivalent text items. +265 """ +266 if not predictions: +267 predictions = [] 268 -269 # order_text_lines sets the line number and line order -270 line_items = model.TextItem.order_text_lines(items) -271 -272 return line_items -273 -274 def save_items( -275 self, -276 items_file: pathlib.Path, -277 items: typing.Iterable[model.TextItem], -278 ) -> None: -279 """Save items to csv file. -280 -281 Args: -282 items_file: Write the text items to this file. -283 items: The text items to save. +269 items = [] +270 for prediction in predictions: +271 items.append(model.TextItem.from_prediction(prediction)) +272 +273 # order_text_lines sets the line number and line order +274 line_items = model.TextItem.order_text_lines(items) +275 +276 return line_items +277 +278 def save_items( +279 self, +280 items_file: pathlib.Path, +281 items: typing.Iterable[model.TextItem], +282 ) -> None: +283 """Save items to csv file. 284 -285 Returns: -286 None -287 """ -288 if not items_file: -289 msg = "Must supply predictions file." -290 raise utils.LeafFocusError(msg) -291 if not items: -292 msg = "Must supply predictions data." -293 raise utils.LeafFocusError(msg) -294 -295 logger.info("Saving OCR predictions to '%s'.", items_file) -296 -297 items_list = list(items) -298 model.TextItem.save(items_file, items_list) -299 -300 def _build_name(self, prefix: str, middle: str, suffix: str): -301 """Build the file name. -302 -303 Args: -304 prefix: The text to add at the start. -305 middle: The text to add in the middle. -306 suffix: The text to add at the end. -307 -308 Returns: -309 str: The built name. -310 """ -311 prefix = prefix.strip("-") -312 middle = middle.strip("-") -313 suffix = suffix if suffix.startswith(".") else "." + suffix -314 return f"{prefix}-{middle}" + suffix +285 Args: +286 items_file: Write the text items to this file. +287 items: The text items to save. +288 +289 Returns: +290 None +291 """ +292 if not items_file: +293 msg = "Must supply predictions file." +294 raise utils.LeafFocusError(msg) +295 if not items: +296 msg = "Must supply predictions data." +297 raise utils.LeafFocusError(msg) +298 +299 logger.info("Saving OCR predictions to '%s'.", items_file) +300 +301 items_list = list(items) +302 model.TextItem.save(items_file, items_list) +303 +304 def _build_name(self, prefix: str, middle: str, suffix: str): +305 """Build the file name. +306 +307 Args: +308 prefix: The text to add at the start. +309 middle: The text to add in the middle. +310 suffix: The text to add at the end. +311 +312 Returns: +313 str: The built name. +314 """ +315 prefix = prefix.strip("-") +316 middle = middle.strip("-") +317 suffix = suffix if suffix.startswith(".") else "." + suffix +318 return f"{prefix}-{middle}" + suffix @@ -779,37 +787,41 @@

49 50 tf.get_logger().setLevel(log_level) 51 -52 try: -53 import keras_ocr # pylint: disable=import-outside-toplevel -54 except ModuleNotFoundError as error: -55 msg = "Cannot run ocr on this Python version." -56 logger.error(msg) -57 raise utils.LeafFocusError(msg) from error -58 -59 # TODO: allow specifying path to weights files for detector -60 # detector_weights_path = "" -61 # detector = keras_ocr.detection.Detector(weights=None) -62 # detector.model = keras_ocr.detection.build_keras_model( -63 # weights_path=detector_weights_path, backbone_name="vgg" -64 # ) -65 # detector.model.compile(loss="mse", optimizer="adam") -66 detector = None -67 -68 # TODO: allow specifying path to weights files for recogniser -69 # recognizer_weights_path = "" -70 # recognizer = keras_ocr.recognition.Recognizer( -71 # alphabet=keras_ocr.recognition.DEFAULT_ALPHABET, weights=None -72 # ) -73 # recognizer.model.load_weights(recognizer_weights_path) -74 recognizer = None -75 -76 # see: https://github.com/faustomorales/keras-ocr -77 # keras-ocr will automatically download pretrained -78 # weights for the detector and recognizer. -79 self._pipeline = keras_ocr.pipeline.Pipeline( -80 detector=detector, -81 recognizer=recognizer, -82 ) +52 # check the CPU / GPU in use +53 gpus = tf.config.list_physical_devices("GPU") +54 logger.info("GPUs in use: '%s'.", gpus) +55 +56 try: +57 import keras_ocr # pylint: disable=import-outside-toplevel +58 except ModuleNotFoundError as error: +59 msg = "Cannot run ocr on this Python version." +60 logger.error(msg) +61 raise utils.LeafFocusError(msg) from error +62 +63 # TODO: allow specifying path to weights files for detector +64 # detector_weights_path = "" +65 # detector = keras_ocr.detection.Detector(weights=None) +66 # detector.model = keras_ocr.detection.build_keras_model( +67 # weights_path=detector_weights_path, backbone_name="vgg" +68 # ) +69 # detector.model.compile(loss="mse", optimizer="adam") +70 detector = None +71 +72 # TODO: allow specifying path to weights files for recogniser +73 # recognizer_weights_path = "" +74 # recognizer = keras_ocr.recognition.Recognizer( +75 # alphabet=keras_ocr.recognition.DEFAULT_ALPHABET, weights=None +76 # ) +77 # recognizer.model.load_weights(recognizer_weights_path) +78 recognizer = None +79 +80 # see: https://github.com/faustomorales/keras-ocr +81 # keras-ocr will automatically download pretrained +82 # weights for the detector and recognizer. +83 self._pipeline = keras_ocr.pipeline.Pipeline( +84 detector=detector, +85 recognizer=recognizer, +86 ) @@ -835,35 +847,35 @@

Returns:
-
 84    def engine_run(
- 85        self,
- 86        image_file: pathlib.Path,
- 87    ) -> typing.Tuple[typing.List, typing.Any]:
- 88        """Run the recognition engine.
- 89
- 90        Args:
- 91            image_file: The path to the image file.
- 92
- 93        Returns:
- 94            typing.Tuple[typing.List, typing.Any]: The list of images
- 95                and list of recognition results.
- 96        """
- 97        try:
- 98            import keras_ocr  # pylint: disable=import-outside-toplevel
- 99        except ModuleNotFoundError as error:
-100            msg = "Cannot run ocr on this Python version."
-101            logger.error(msg)
-102            raise utils.LeafFocusError(msg) from error
-103
-104        self.engine_create()
-105
-106        if not self._pipeline:
-107            msg = "Keras OCR pipeline has not been initialised yet."
-108            logger.error(msg)
-109            raise utils.LeafFocusError(msg)
-110
-111        images = [keras_ocr.tools.read(str(image_file))]
-112        return images, self._pipeline.recognize(images)
+            
 88    def engine_run(
+ 89        self,
+ 90        image_file: pathlib.Path,
+ 91    ) -> typing.Tuple[typing.List, typing.Any]:
+ 92        """Run the recognition engine.
+ 93
+ 94        Args:
+ 95            image_file: The path to the image file.
+ 96
+ 97        Returns:
+ 98            typing.Tuple[typing.List, typing.Any]: The list of images
+ 99                and list of recognition results.
+100        """
+101        try:
+102            import keras_ocr  # pylint: disable=import-outside-toplevel
+103        except ModuleNotFoundError as error:
+104            msg = "Cannot run ocr on this Python version."
+105            logger.error(msg)
+106            raise utils.LeafFocusError(msg) from error
+107
+108        self.engine_create()
+109
+110        if not self._pipeline:
+111            msg = "Keras OCR pipeline has not been initialised yet."
+112            logger.error(msg)
+113            raise utils.LeafFocusError(msg)
+114
+115        images = [keras_ocr.tools.read(str(image_file))]
+116        return images, self._pipeline.recognize(images)
 
@@ -896,30 +908,30 @@
Returns:
-
114    def engine_annotate(
-115        self,
-116        image: typing.Optional[np.ndarray],
-117        predictions: typing.List[typing.Tuple[typing.Any, typing.Any]],
-118        axis,
-119    ) -> None:
-120        """Run the annotation engine.
-121
-122        Args:
-123            image: The image data.
-124            predictions: The recognised text from the image.
-125            axis: The plot axis for drawing annotations.
-126
-127        Returns:
-128            None
-129        """
-130        try:
-131            import keras_ocr  # pylint: disable=import-outside-toplevel
-132        except ModuleNotFoundError as error:
-133            msg = "Cannot run ocr on this Python version."
-134            logger.error(msg)
-135            raise utils.LeafFocusError(msg) from error
-136
-137        keras_ocr.tools.drawAnnotations(image=image, predictions=predictions, ax=axis)
+            
118    def engine_annotate(
+119        self,
+120        image: typing.Optional[np.ndarray],
+121        predictions: typing.List[typing.Tuple[typing.Any, typing.Any]],
+122        axis,
+123    ) -> None:
+124        """Run the annotation engine.
+125
+126        Args:
+127            image: The image data.
+128            predictions: The recognised text from the image.
+129            axis: The plot axis for drawing annotations.
+130
+131        Returns:
+132            None
+133        """
+134        try:
+135            import keras_ocr  # pylint: disable=import-outside-toplevel
+136        except ModuleNotFoundError as error:
+137            msg = "Cannot run ocr on this Python version."
+138            logger.error(msg)
+139            raise utils.LeafFocusError(msg) from error
+140
+141        keras_ocr.tools.drawAnnotations(image=image, predictions=predictions, ax=axis)
 
@@ -953,72 +965,72 @@
Returns:
-
139    def recognise_text(
-140        self,
-141        image_file: pathlib.Path,
-142        output_dir: pathlib.Path,
-143    ) -> model.KerasOcrResult:
-144        """Recognise text in an image file.
-145
-146        Args:
-147            image_file: The path to the image file.
-148            output_dir: The directory to write the results.
+            
143    def recognise_text(
+144        self,
+145        image_file: pathlib.Path,
+146        output_dir: pathlib.Path,
+147    ) -> model.KerasOcrResult:
+148        """Recognise text in an image file.
 149
-150        Returns:
-151            model.KerasOcrResult: The text recognition results.
-152        """
-153        if not image_file:
-154            msg = "Must supply image file."
-155            raise utils.LeafFocusError(msg)
-156        if not output_dir:
-157            msg = "Must supply output directory."
-158            raise utils.LeafFocusError(msg)
-159        if not image_file.exists():
-160            msg = f"Image file does not exist '{image_file}'."
-161            raise utils.LeafFocusError(msg) from FileNotFoundError(image_file)
-162
-163        # check if output files already exist
-164        annotations_file = utils.output_root(image_file, "annotations", output_dir)
-165        annotations_file = annotations_file.with_suffix(".png")
+150        Args:
+151            image_file: The path to the image file.
+152            output_dir: The directory to write the results.
+153
+154        Returns:
+155            model.KerasOcrResult: The text recognition results.
+156        """
+157        if not image_file:
+158            msg = "Must supply image file."
+159            raise utils.LeafFocusError(msg)
+160        if not output_dir:
+161            msg = "Must supply output directory."
+162            raise utils.LeafFocusError(msg)
+163        if not image_file.exists():
+164            msg = f"Image file does not exist '{image_file}'."
+165            raise utils.LeafFocusError(msg) from FileNotFoundError(image_file)
 166
-167        predictions_file = utils.output_root(image_file, "predictions", output_dir)
-168        predictions_file = predictions_file.with_suffix(".csv")
-169
-170        result = model.KerasOcrResult(
-171            output_dir=output_dir,
-172            annotations_file=annotations_file,
-173            predictions_file=predictions_file,
-174            items=[],
-175        )
-176
-177        if annotations_file.exists() and predictions_file.exists():
-178            logger.debug(
-179                "Predictions and annotations files already exist for '%s'.",
-180                image_file.stem,
-181            )
-182            all_items = list(model.TextItem.load(predictions_file))
-183            result.items = model.TextItem.order_text_lines(all_items)
-184            return result
-185
-186        # read in the image
-187        logger.debug(
-188            "Creating predictions and annotations files for '%s'.",
-189            image_file.stem,
-190        )
-191
-192        # Each list of predictions in prediction_groups is a list of
-193        # (word, box) tuples.
-194        images, prediction_groups = self.engine_run(image_file)
+167        # check if output files already exist
+168        annotations_file = utils.output_root(image_file, "annotations", output_dir)
+169        annotations_file = annotations_file.with_suffix(".png")
+170
+171        predictions_file = utils.output_root(image_file, "predictions", output_dir)
+172        predictions_file = predictions_file.with_suffix(".csv")
+173
+174        result = model.KerasOcrResult(
+175            output_dir=output_dir,
+176            annotations_file=annotations_file,
+177            predictions_file=predictions_file,
+178            items=[],
+179        )
+180
+181        if annotations_file.exists() and predictions_file.exists():
+182            logger.debug(
+183                "Predictions and annotations files already exist for '%s'.",
+184                image_file.stem,
+185            )
+186            all_items = list(model.TextItem.load(predictions_file))
+187            result.items = model.TextItem.order_text_lines(all_items)
+188            return result
+189
+190        # read in the image
+191        logger.debug(
+192            "Creating predictions and annotations files for '%s'.",
+193            image_file.stem,
+194        )
 195
-196        # Plot and save the predictions
-197        for image, predictions in zip(images, prediction_groups):
-198            self.save_figure(annotations_file, image, predictions)
+196        # Each list of predictions in prediction_groups is a list of
+197        # (word, box) tuples.
+198        images, prediction_groups = self.engine_run(image_file)
 199
-200            items = self.convert_predictions(predictions)
-201            self.save_items(predictions_file, [item for line in items for item in line])
-202            result.items = items
+200        # Plot and save the predictions
+201        for image, predictions in zip(images, prediction_groups):
+202            self.save_figure(annotations_file, image, predictions)
 203
-204        return result
+204            items = self.convert_predictions(predictions)
+205            self.save_items(predictions_file, [item for line in items for item in line])
+206            result.items = items
+207
+208        return result
 
@@ -1051,49 +1063,49 @@
Returns:
-
206    def save_figure(
-207        self,
-208        annotation_file: pathlib.Path,
-209        image: typing.Optional[np.ndarray],
-210        predictions: typing.List[typing.Tuple[typing.Any, typing.Any]],
-211    ) -> None:
-212        """Save the annotated image.
-213
-214        Args:
-215            annotation_file: The path to the file containing annotations.
-216            image: The image data.
-217            predictions: The text recognition results.
-218
-219        Returns:
-220            None
-221        """
-222        if not annotation_file:
-223            msg = "Must supply annotation file."
-224            raise utils.LeafFocusError(msg)
-225
-226        expected_image_shape = 3
-227        if image is None or image.size < 1 or len(image.shape) != expected_image_shape:
-228            msg_image = image.shape if image is not None else None
-229            msg = f"Must supply valid image data, not '{msg_image}'."
-230            raise utils.LeafFocusError(msg)
-231        if not predictions:
-232            predictions = []
-233
-234        logger.info("Saving OCR image to '%s'.", annotation_file)
-235
-236        import matplotlib as mpl  # pylint: disable=import-outside-toplevel
-237        from matplotlib import pyplot as plt  # pylint: disable=import-outside-toplevel
-238
-239        mpl.use("agg")
-240
-241        annotation_file.parent.mkdir(exist_ok=True, parents=True)
+            
210    def save_figure(
+211        self,
+212        annotation_file: pathlib.Path,
+213        image: typing.Optional[np.ndarray],
+214        predictions: typing.List[typing.Tuple[typing.Any, typing.Any]],
+215    ) -> None:
+216        """Save the annotated image.
+217
+218        Args:
+219            annotation_file: The path to the file containing annotations.
+220            image: The image data.
+221            predictions: The text recognition results.
+222
+223        Returns:
+224            None
+225        """
+226        if not annotation_file:
+227            msg = "Must supply annotation file."
+228            raise utils.LeafFocusError(msg)
+229
+230        expected_image_shape = 3
+231        if image is None or image.size < 1 or len(image.shape) != expected_image_shape:
+232            msg_image = image.shape if image is not None else None
+233            msg = f"Must supply valid image data, not '{msg_image}'."
+234            raise utils.LeafFocusError(msg)
+235        if not predictions:
+236            predictions = []
+237
+238        logger.info("Saving OCR image to '%s'.", annotation_file)
+239
+240        import matplotlib as mpl  # pylint: disable=import-outside-toplevel
+241        from matplotlib import pyplot as plt  # pylint: disable=import-outside-toplevel
 242
-243        fig, axis = plt.subplots(figsize=(20, 20))
+243        mpl.use("agg")
 244
-245        self.engine_annotate(image, predictions, axis)
+245        annotation_file.parent.mkdir(exist_ok=True, parents=True)
 246
-247        fig.savefig(str(annotation_file))
-248        plt.close(fig)
+247        fig, axis = plt.subplots(figsize=(20, 20))
+248
+249        self.engine_annotate(image, predictions, axis)
+250
+251        fig.savefig(str(annotation_file))
+252        plt.close(fig)
 
@@ -1127,29 +1139,29 @@
Returns:
-
250    def convert_predictions(
-251        self,
-252        predictions: typing.List[typing.Tuple[typing.Any, typing.Any]],
-253    ) -> typing.List[typing.List[model.TextItem]]:
-254        """Convert predictions to items.
-255
-256        Args:
-257            predictions: The list of recognised text.
-258
-259        Returns:
-260            typing.List[typing.List[model.TextItem]]: The equivalent text items.
-261        """
-262        if not predictions:
-263            predictions = []
-264
-265        items = []
-266        for prediction in predictions:
-267            items.append(model.TextItem.from_prediction(prediction))
+            
254    def convert_predictions(
+255        self,
+256        predictions: typing.List[typing.Tuple[typing.Any, typing.Any]],
+257    ) -> typing.List[typing.List[model.TextItem]]:
+258        """Convert predictions to items.
+259
+260        Args:
+261            predictions: The list of recognised text.
+262
+263        Returns:
+264            typing.List[typing.List[model.TextItem]]: The equivalent text items.
+265        """
+266        if not predictions:
+267            predictions = []
 268
-269        # order_text_lines sets the line number and line order
-270        line_items = model.TextItem.order_text_lines(items)
-271
-272        return line_items
+269        items = []
+270        for prediction in predictions:
+271            items.append(model.TextItem.from_prediction(prediction))
+272
+273        # order_text_lines sets the line number and line order
+274        line_items = model.TextItem.order_text_lines(items)
+275
+276        return line_items
 
@@ -1181,31 +1193,31 @@
Returns:
-
274    def save_items(
-275        self,
-276        items_file: pathlib.Path,
-277        items: typing.Iterable[model.TextItem],
-278    ) -> None:
-279        """Save items to csv file.
-280
-281        Args:
-282            items_file: Write the text items to this file.
-283            items: The text items to save.
+            
278    def save_items(
+279        self,
+280        items_file: pathlib.Path,
+281        items: typing.Iterable[model.TextItem],
+282    ) -> None:
+283        """Save items to csv file.
 284
-285        Returns:
-286            None
-287        """
-288        if not items_file:
-289            msg = "Must supply predictions file."
-290            raise utils.LeafFocusError(msg)
-291        if not items:
-292            msg = "Must supply predictions data."
-293            raise utils.LeafFocusError(msg)
-294
-295        logger.info("Saving OCR predictions to '%s'.", items_file)
-296
-297        items_list = list(items)
-298        model.TextItem.save(items_file, items_list)
+285        Args:
+286            items_file: Write the text items to this file.
+287            items: The text items to save.
+288
+289        Returns:
+290            None
+291        """
+292        if not items_file:
+293            msg = "Must supply predictions file."
+294            raise utils.LeafFocusError(msg)
+295        if not items:
+296            msg = "Must supply predictions data."
+297            raise utils.LeafFocusError(msg)
+298
+299        logger.info("Saving OCR predictions to '%s'.", items_file)
+300
+301        items_list = list(items)
+302        model.TextItem.save(items_file, items_list)