real-time-ocr / src /app.py
etuncer's picture
Rename src/streamlit_app.py to src/app.py
6235f5a verified
Raw
History Blame Contribute Delete
7.52 kB
import cv2
import pytesseract
import streamlit as st
import av
import numpy as np
from streamlit_webrtc import webrtc_streamer, VideoProcessorBase
class OCRProcessor(VideoProcessorBase):
def __init__(self):
# Quick knobs we’ll tweak from the UI
self.conf_threshold = 25
self.box_color_bgr = (0, 255, 0)
self.text_color_bgr = (0, 255, 0)
self.box_thickness = 1
self.text_thickness = 1
def recv(self, frame):
img = frame.to_ndarray(format="bgr24")
# Step 1: simplify colors → grayscale helps OCR focus on shapes
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Step 2: separate text/background using Otsu threshold
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
try:
# Step 3: run OCR and get word boxes + confidences
data = pytesseract.image_to_data(thresh, output_type=pytesseract.Output.DICT)
# Step 4: draw only what we trust
for i in range(len(data['text'])):
# Keep boxes with decent confidence and non-empty text
if int(data['conf'][i]) > self.conf_threshold and data['text'][i].strip():
x, y, w, h = data['left'][i], data['top'][i], data['width'][i], data['height'][i]
# Box shows where Tesseract thinks the word is
cv2.rectangle(img, (x, y), (x + w, y + h), self.box_color_bgr, self.box_thickness)
# Label it so we see what was read
cv2.putText(img, data['text'][i], (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, self.text_color_bgr, self.text_thickness)
except Exception as e:
st.error(f"OCR Error: {str(e)}")
return av.VideoFrame.from_ndarray(img, format="bgr24")
def _hex_to_bgr(hex_color):
# Streamlit gives hex; OpenCV expects BGR tuples
hex_color = hex_color.lstrip('#')
if len(hex_color) == 6:
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
return (b, g, r)
return (0, 255, 0)
def run_ocr_on_bgr(image_bgr, conf_threshold=25, box_color_bgr=(0, 255, 0), text_color_bgr=(0, 255, 0), box_thickness=1, text_thickness=1):
# Same pipeline for still images
gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
annotated = image_bgr.copy()
texts = []
try:
data = pytesseract.image_to_data(thresh, output_type=pytesseract.Output.DICT)
for i in range(len(data['text'])):
text = data['text'][i]
conf_str = data['conf'][i]
try:
conf = int(conf_str)
except Exception:
conf = -1
# Quick sanity checks: enough confidence and not empty
if conf > conf_threshold and text.strip():
x, y, w, h = data['left'][i], data['top'][i], data['width'][i], data['height'][i]
cv2.rectangle(annotated, (x, y), (x + w, y + h), box_color_bgr, box_thickness)
cv2.putText(annotated, text, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, text_color_bgr, text_thickness)
texts.append(text)
except Exception as e:
st.error(f"OCR Error: {str(e)}")
return annotated, texts
def read_settings_from_state():
# Pull current UI settings so everything stays in sync
conf_threshold = st.session_state["conf_threshold"]
box_color_bgr = _hex_to_bgr(st.session_state["box_color"])
text_color_bgr = _hex_to_bgr(st.session_state["text_color"])
box_thickness = st.session_state["box_thickness"]
text_thickness = st.session_state["text_thickness"]
return conf_threshold, box_color_bgr, text_color_bgr, box_thickness, text_thickness
def decode_image_from_uploader(file_like):
# Turn the uploaded file into an OpenCV image
file_bytes = np.frombuffer(file_like.getvalue(), np.uint8)
return cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
def display_annotated_and_text(annotated_bgr, texts, caption):
# Show what OCR saw and list the tokens found
st.image(cv2.cvtColor(annotated_bgr, cv2.COLOR_BGR2RGB), caption=caption)
if texts:
st.subheader("Detected Text:")
for t in texts:
st.write(f"• {t}")
else:
st.info("No text detected.")
st.title("Real-time OCR - Computer Vision")
st.write("Pick a mode below and let’s read some text together.")
# Sidebar controls
st.sidebar.header("OCR Settings")
# Defaults
_DEFAULTS = {
"conf_threshold": 25,
"box_color": "#00FF00",
"text_color": "#00FF00",
"box_thickness": 1,
"text_thickness": 1,
}
# Initialize session state
for k, v in _DEFAULTS.items():
if k not in st.session_state:
st.session_state[k] = v
# Reset button
if st.sidebar.button("Reset to defaults"):
for k, v in _DEFAULTS.items():
st.session_state[k] = v
st.rerun()
# Widgets bound to session state keys
st.sidebar.slider("Confidence threshold", min_value=0, max_value=100, step=1, key="conf_threshold")
st.sidebar.color_picker("Bounding box color", key="box_color")
st.sidebar.color_picker("Text color", key="text_color")
st.sidebar.slider("Box thickness", min_value=1, max_value=5, key="box_thickness")
st.sidebar.slider("Text thickness", min_value=1, max_value=3, key="text_thickness")
# Read current settings
conf_threshold, box_color_bgr, text_color_bgr, box_thickness, text_thickness = read_settings_from_state()
mode = st.radio("Mode", ("Real-time", "Take Photo", "Upload Photo"), horizontal=True)
if mode == "Real-time":
st.write("We’ll use your webcam and draw boxes live.")
ctx = webrtc_streamer(
key="ocr-camera",
video_processor_factory=OCRProcessor,
rtc_configuration={
"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]
}
)
if ctx and ctx.video_processor:
# Pass current UI choices into the processor
ctx.video_processor.conf_threshold = conf_threshold
ctx.video_processor.box_color_bgr = box_color_bgr
ctx.video_processor.text_color_bgr = text_color_bgr
ctx.video_processor.box_thickness = box_thickness
ctx.video_processor.text_thickness = text_thickness
elif mode == "Take Photo":
img_file = st.camera_input("Take a photo")
if img_file is not None:
image_bgr = decode_image_from_uploader(img_file)
if image_bgr is not None:
# One-click OCR on your snapshot
annotated, texts = run_ocr_on_bgr(image_bgr, conf_threshold, box_color_bgr, text_color_bgr, box_thickness, text_thickness)
display_annotated_and_text(annotated, texts, caption="Annotated photo")
else:
st.error("Could not decode image.")
elif mode == "Upload Photo":
uploaded = st.file_uploader("Upload an image", type=["png", "jpg", "jpeg"])
if uploaded is not None:
image_bgr = decode_image_from_uploader(uploaded)
if image_bgr is not None:
# Run the same pipeline for your file
annotated, texts = run_ocr_on_bgr(image_bgr, conf_threshold, box_color_bgr, text_color_bgr, box_thickness, text_thickness)
display_annotated_and_text(annotated, texts, caption="Annotated upload")
else:
st.error("Could not decode image.")