Source code for go2.modules.ocr.ocr_module
import cv2
import numpy as np
import pytesseract
from collections import Counter
from typing_extensions import override
from .ocr_config import OCRConfig
from ...core.module import DogModule
[docs]
class OCRModule(DogModule):
"""
Users should not access or construct this class directly.
Rather, they should access it through the :class:`~core.controller.Go2Controller` instance.
"""
def __init__(self) -> None:
super().__init__("OCR")
@override
def _initialize(self) -> None:
if self._initialized:
return
self._initialized = True
# See ref: https://medium.com/@EnginDenizTangut/from-image-to-voice-building-an-ocr-tts-app-with-python-opencv-tesseract-5f5db8ea3b7b
[docs]
def extract_text_from_image(self, image: np.ndarray, config: OCRConfig = OCRConfig()) -> tuple[list[str], np.ndarray]:
"""
Run OCR on a single image and return the detected text along with an annotated copy.
Parameters
----------
image : np.ndarray
Input BGR image to extract text from.
config : OCRConfig, optional
OCR configuration controlling preprocessing, confidence threshold, and Tesseract CLI options.
Returns
-------
tuple[list[str], np.ndarray]
The list of confidently detected words and the annotated image with bounding boxes drawn around them.
"""
words, annotated = self._filter_and_highlight(self._preprocess(image), config)
return words, annotated
[docs]
def extract_text_from_images_temporal_voting(self, images: np.ndarray[np.ndarray], config: OCRConfig = OCRConfig()) -> tuple[list[str], np.ndarray[np.ndarray]]:
"""
Run OCR across multiple images of the same scene and keep only words detected
in at least ``config.temporal_voting_threshold`` of them, reducing false positives
from single-frame noise.
Parameters
----------
images : np.ndarray[np.ndarray]
Array of input BGR images to extract text from.
config : OCRConfig, optional
OCR configuration controlling preprocessing, confidence threshold, temporal voting threshold, and Tesseract CLI options.
Returns
-------
tuple[list[str], np.ndarray[np.ndarray]]
The list of words that met the temporal voting threshold and the array of annotated images corresponding to each input image.
Raises
------
ValueError
If the number of provided images is less than ``config.temporal_voting_threshold``.
"""
if len(images) < config.temporal_voting_threshold:
raise ValueError(
f"The number of provided images ({len(images)}) is less than the required "
f"temporal voting threshold ({config.temporal_voting_threshold})."
)
annotated_images = np.empty(len(images), dtype=object)
detected_words = []
for idx, image in enumerate(images):
words, annotated = self._filter_and_highlight(self._preprocess(image), config)
detected_words.extend(words)
annotated_images[idx] = annotated
word_counts = Counter(detected_words)
final_words = [word for word, count in word_counts.items() if count >= config.temporal_voting_threshold]
return final_words, annotated_images
def _preprocess(self, image: np.ndarray) -> np.ndarray:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.resize(gray, None, fx=1.5, fy=1.5, interpolation=cv2.INTER_CUBIC)
gray = cv2.bilateralFilter(gray, 9, 75, 75)
thresh = cv2.adaptiveThreshold(
gray, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY, 31, 10
)
kernel = np.ones((2, 2), np.uint8)
clean = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
clean = cv2.morphologyEx(clean, cv2.MORPH_CLOSE, kernel)
return clean
def _filter_and_highlight(self, image: np.ndarray, config: OCRConfig) -> tuple[list[str], np.ndarray]:
ocr_data = pytesseract.image_to_data(image, output_type=pytesseract.Output.DICT, config=config.cli_command)
output = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
confident_words = []
for i in range (len(ocr_data["text"])):
word = ocr_data['text'][i].strip()
conf = int(ocr_data['conf'][i])
if conf > config.min_conf:
confident_words.append(word)
x, y, w, h = (ocr_data["left"][i], ocr_data['top'][i], ocr_data['width'][i], ocr_data['height'][i])
cv2.rectangle(output, (x, y), (x + w, y + h), (0, 255, 0), 2)
return confident_words, output
@override
def _shutdown(self) -> None:
self._initialized = False