Загрузка данных


import cv2
import easyocr
import numpy as np
import time
import threading
from PIL import Image, ImageDraw, ImageFont

CAMERA_INDEX    = 0
FRAME_WIDTH     = 1280
FRAME_HEIGHT    = 720
USE_GPU         = False
PROCESS_EVERY_N = 2
CONF_THRESHOLD  = 0.3
LANGUAGES       = ['ru', 'en']
ROI             = None

BOX_COLOR       = (0, 255, 0)
TEXT_BG_COLOR   = (0, 0, 0)
TEXT_COLOR      = (0, 255, 255)

INDICATOR_COLOR = (0, 200, 80)


def load_font(size=22):
    candidates = [
        "C:/Windows/Fonts/arial.ttf",
        "C:/Windows/Fonts/segoeui.ttf",
        "C:/Windows/Fonts/tahoma.ttf",
    ]
    for path in candidates:
        try:
            return ImageFont.truetype(path, size)
        except Exception:
            continue
    return ImageFont.load_default()


def draw_text_pil(frame_bgr, text, pos, font):
    img_pil = Image.fromarray(cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB))
    draw = ImageDraw.Draw(img_pil)
    x, y = pos
    bbox = draw.textbbox((x, y), text, font=font)
    pad = 3
    draw.rectangle((bbox[0] - pad, bbox[1] - pad, bbox[2] + pad, bbox[3] + pad), fill=TEXT_BG_COLOR)
    draw.text((x, y), text, font=font, fill=TEXT_COLOR)
    return cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)


def preprocess_frame(frame):
    img = frame.copy()
    if ROI:
        x, y, w, h = ROI
        img = img[y:y+h, x:x+w]
    gray     = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    denoised = cv2.bilateralFilter(gray, d=5, sigmaColor=35, sigmaSpace=35)
    clahe    = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
    enhanced = clahe.apply(denoised)
    upscaled = cv2.resize(enhanced, None, fx=2.0, fy=2.0, interpolation=cv2.INTER_CUBIC)
    binary   = cv2.adaptiveThreshold(upscaled, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 8)
    kernel   = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
    closed   = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
    return cv2.cvtColor(closed, cv2.COLOR_GRAY2BGR)


def draw_results(frame, results, font):
    overlay  = frame.copy()
    offset_x = ROI[0] if ROI else 0
    offset_y = ROI[1] if ROI else 0

    for (bbox, text, conf) in results:
        if conf < CONF_THRESHOLD:
            continue
        pts = np.array(bbox, dtype=np.float32)
        pts /= 2.0
        pts[:, 0] += offset_x
        pts[:, 1] += offset_y
        pts = pts.astype(np.int32)
        cv2.polylines(overlay, [pts], isClosed=True, color=BOX_COLOR, thickness=2)
        tx = pts[0][0]
        ty = pts[0][1] - 28
        if ty < 5:
            ty = pts[2][1] + 5
        overlay = draw_text_pil(overlay, f"{text} ({conf:.2f})", (tx, ty), font)

    return overlay


def draw_indicator(frame):
    h, w  = frame.shape[:2]
    cx    = w - 32
    cy    = 32
    bgr   = (INDICATOR_COLOR[2], INDICATOR_COLOR[1], INDICATOR_COLOR[0])
    cv2.circle(frame, (cx, cy), 14, bgr, -1, cv2.LINE_AA)
    cv2.circle(frame, (cx, cy), 14, (255, 255, 255), 1, cv2.LINE_AA)


class AsyncOCR:
    def __init__(self, reader):
        self.reader  = reader
        self.results = []
        self.busy    = False
        self._lock   = threading.Lock()

    def process(self, img):
        if self.busy:
            return
        self.busy = True
        threading.Thread(target=self._run, args=(img,), daemon=True).start()

    def _run(self, img):
        try:
            res = self.reader.readtext(
                img,
                detail=1,
                paragraph=False,
                batch_size=1,
                contrast_ths=0.2,
                adjust_contrast=0.7,
                text_threshold=0.5,
                low_text=0.3,
                link_threshold=0.3,
                canvas_size=1280,
                mag_ratio=1.0,
            )
            with self._lock:
                self.results = res
        finally:
            self.busy = False

    def get_results(self):
        with self._lock:
            return list(self.results)


def main():
    print("Загружаем EasyOCR...")
    reader = easyocr.Reader(LANGUAGES, gpu=USE_GPU, verbose=False)
    font   = load_font(size=22)
    cap    = cv2.VideoCapture(CAMERA_INDEX)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH,  FRAME_WIDTH)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)

    if not cap.isOpened():
        print(f"Не могу открыть камеру {CAMERA_INDEX}")
        return

    ocr       = AsyncOCR(reader)
    frame_idx = 0
    print("Q — выход, S — скриншот")

    while True:
        ret, frame = cap.read()
        if not ret:
            break

        frame_idx += 1
        if frame_idx % PROCESS_EVERY_N == 0:
            ocr.process(preprocess_frame(frame))

        display = draw_results(frame, ocr.get_results(), font)

        if ROI:
            x, y, w, h = ROI
            cv2.rectangle(display, (x, y), (x+w, y+h), (255, 100, 0), 1)

        draw_indicator(display)
        cv2.imshow("Cable OCR", display)

        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break
        elif key == ord('s'):
            fname = f"screenshot_{int(time.time())}.jpg"
            cv2.imwrite(fname, display)
            print(f"Сохранено: {fname}")

    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()