Str1k3rch0

Digit Detector App 0.3

Jul 21st, 2025
214
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 11.29 KB | Software | 0 0
  1. import cv2
  2. import pytesseract
  3. import os
  4. import re
  5. import time
  6. import joblib
  7. import numpy as np
  8. from datetime import datetime
  9. import tkinter as tk
  10. from tkinter import ttk
  11. from PIL import Image, ImageTk
  12. from sklearn.ensemble import RandomForestClassifier
  13.  
  14. # Constants
  15. IMG_SIZE = (32, 32)
  16. COOLDOWN = 5  # seconds
  17. MIN_OCR_CONF = 60
  18. ACTIVE_LEARNING_BATCH = 10
  19. SCAN_INTERVAL = 1.0  # seconds between rescans of frozen frame
  20. MIN_WHITE_PIXELS = 500  # minimum pixels to consider frame valid
  21. DATASET_DIR = "dataset"
  22. MODEL_PATH = "model.pkl"
  23.  
  24. class DigitDetectorApp:
  25.     def __init__(self, model_path=MODEL_PATH):
  26.         # Load or initialize model
  27.         if os.path.exists(model_path):
  28.             self.model = joblib.load(model_path)
  29.         else:
  30.             self.model = RandomForestClassifier(n_estimators=100)
  31.         self.new_data = []  # for active learning
  32.  
  33.         # Tesseract config
  34.         pytesseract.pytesseract.tesseract_cmd = r"C:\\Program Files\\Tesseract-OCR\\tesseract.exe"
  35.         self.ocr_config = r'--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789'
  36.  
  37.         # Output dirs
  38.         os.makedirs("detections", exist_ok=True)
  39.         os.makedirs(DATASET_DIR, exist_ok=True)
  40.         self.feedback_file = "feedback_log.csv"
  41.         if not os.path.exists(self.feedback_file):
  42.             with open(self.feedback_file, "w") as f:
  43.                 f.write("number,confidence,label,timestamp\n")
  44.  
  45.         # State
  46.         self.pending_feedback = False
  47.         self.freeze_mode = False
  48.         self.last_detected = None
  49.         self.last_time = 0
  50.         self.last_scan_time = 0
  51.         self.history = []  # undo stack
  52.  
  53.         # Setup GUI
  54.         self.root = tk.Tk()
  55.         self.root.title("Digit Detector App")
  56.         self.root.geometry("1280x650")
  57.         self._build_gui()
  58.  
  59.         # Video capture
  60.         self.cap = cv2.VideoCapture(0)
  61.         self.update_frames()
  62.         self.root.mainloop()
  63.  
  64.     def _build_gui(self):
  65.         frame = self.root
  66.         frame.grid_columnconfigure(0, weight=1)
  67.         frame.grid_columnconfigure(1, weight=1)
  68.         frame.grid_rowconfigure(0, weight=1)
  69.  
  70.         self.label_cam = ttk.Label(frame)
  71.         self.label_cam.grid(row=0, column=0, padx=5, pady=5, sticky="nsew")
  72.         self.label_proc = ttk.Label(frame)
  73.         self.label_proc.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")
  74.  
  75.         self.label_info = ttk.Label(frame, text="🧠 Waiting...", font=('Arial', 14))
  76.         self.label_info.grid(row=1, column=0, columnspan=2)
  77.  
  78.         # Buttons
  79.         btn_frame = ttk.Frame(frame)
  80.         btn_frame.grid(row=2, column=0, columnspan=2)
  81.         ttk.Button(btn_frame, text="✅ Correct", command=lambda: self.save_feedback(True)).pack(side="left", padx=5)
  82.         ttk.Button(btn_frame, text="❌ Wrong", command=lambda: self.save_feedback(False)).pack(side="left", padx=5)
  83.         ttk.Button(btn_frame, text="⟲ Undo", command=self.undo).pack(side="left", padx=5)
  84.         ttk.Button(btn_frame, text="🛑 Cancel Freeze", command=self.cancel_freeze).pack(side="left", padx=5)
  85.         ttk.Button(btn_frame, text="🌡️ Heatmap", command=self.toggle_heatmap).pack(side="left", padx=5)
  86.  
  87.         # Correction entry
  88.         self.entry = ttk.Entry(frame)
  89.         self.btn_correct = ttk.Button(frame, text="Submit", command=lambda: self.manual_correction(self.entry.get()))
  90.         self.entry.grid(row=3, column=0, padx=5, pady=5)
  91.         self.btn_correct.grid(row=3, column=1, padx=5, pady=5)
  92.         self.entry.grid_remove(); self.btn_correct.grid_remove()
  93.  
  94.         self.show_heat = False
  95.  
  96.     def cancel_freeze(self):
  97.         # Stop freeze and resume normal scanning
  98.         self.freeze_mode = False
  99.         self.pending_feedback = False
  100.         self.entry.grid_remove(); self.btn_correct.grid_remove()
  101.         self.label_info.config(text="⏹️ Scan canceled. Resuming live detection...")
  102.  
  103.     def preprocess(self, frame):
  104.         gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
  105.         blur = cv2.GaussianBlur(gray, (3, 3), 0)
  106.         _, thresh = cv2.threshold(blur, 170, 255, cv2.THRESH_BINARY)
  107.         return thresh
  108.  
  109.     def predict_ocr(self, img):
  110.         data = pytesseract.image_to_data(img, config=self.ocr_config, output_type=pytesseract.Output.DICT)
  111.         for i, txt in enumerate(data['text']):
  112.             txt = txt.strip()
  113.             try: conf = int(data['conf'][i])
  114.             except: conf = 0
  115.             if re.fullmatch(r'\d{1,3}', txt) and conf >= MIN_OCR_CONF:
  116.                 return txt, conf
  117.         return None, 0
  118.  
  119.     def predict_ml(self, img):
  120.         resized = cv2.resize(img, IMG_SIZE).flatten().reshape(1, -1)
  121.         pred = self.model.predict(resized)[0]
  122.         conf = int(max(self.model.predict_proba(resized)[0]) * 100) if hasattr(self.model, 'predict_proba') else 0
  123.         return str(pred), conf
  124.  
  125.     def ensemble_predict(self, img):
  126.         # Only predict if frame has sufficient content
  127.         if cv2.countNonZero(img) < MIN_WHITE_PIXELS:
  128.             return None, 0
  129.         num_o, conf_o = self.predict_ocr(img)
  130.         num_m, conf_m = self.predict_ml(img)
  131.         if num_o and conf_o >= conf_m:
  132.             return num_o, conf_o
  133.         return num_m, conf_m
  134.  
  135.     def toggle_heatmap(self):
  136.         self.show_heat = not self.show_heat
  137.  
  138.     def save_feedback(self, correct):
  139.         if not hasattr(self, 'current') or self.current is None:
  140.             return
  141.         num, conf, img = self.current
  142.         label = 'correct' if correct else 'wrong'
  143.         ts = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
  144.         with open(self.feedback_file, 'a') as f:
  145.             f.write(f"{num},{conf},{label},{ts}\n")
  146.         folder = num if correct else 'unlabeled'
  147.         path = os.path.join(DATASET_DIR, folder)
  148.         os.makedirs(path, exist_ok=True)
  149.         cv2.imwrite(os.path.join(path, f"{ts}.png"), img)
  150.         if correct:
  151.             self.new_data.append((img, int(num)))
  152.             if len(self.new_data) >= ACTIVE_LEARNING_BATCH:
  153.                 self.retrain_model()
  154.         self.entry.grid_remove(); self.btn_correct.grid_remove()
  155.         self.pending_feedback = False
  156.         self.freeze_mode = False  # Unfreeze after feedback
  157.         self.label_info.config(text=f"✅ Saved: {num} as {label}")
  158.  
  159.     def manual_correction(self, val):
  160.         val = val.strip()
  161.         if not (val.isdigit() or val.lower()=='nothing'):
  162.             self.label_info.config(text="⚠️ Enter valid number or 'nothing'")
  163.             return
  164.         num, conf, img = self.current
  165.         ts = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
  166.         folder = val if val.isdigit() else 'unlabeled'
  167.         path = os.path.join(DATASET_DIR, folder)
  168.         os.makedirs(path, exist_ok=True)
  169.         cv2.imwrite(os.path.join(path, f"{ts}.png"), img)
  170.         with open(self.feedback_file, 'a') as f:
  171.             f.write(f"{val},{conf},manual,{ts}\n")
  172.         self.entry.grid_remove(); self.btn_correct.grid_remove()
  173.         self.pending_feedback = False
  174.         self.freeze_mode = False  # Unfreeze after correction
  175.         self.label_info.config(text=f"📤 Correction saved: {val}")
  176.  
  177.     def undo(self):
  178.         if not self.history:
  179.             self.label_info.config(text="⟲ Nothing to undo")
  180.             return
  181.         num, ts, img = self.history.pop()
  182.         self.label_info.config(text=f"⟲ Undone: {num} at {ts}")
  183.         self.display_images(img)
  184.  
  185.     def retrain_model(self):
  186.         X, y = [], []
  187.         for label in os.listdir(DATASET_DIR):
  188.             if label == 'unlabeled':
  189.                 continue
  190.             label_dir = os.path.join(DATASET_DIR, label)
  191.             if not os.path.isdir(label_dir):
  192.                 continue
  193.             for fname in os.listdir(label_dir):
  194.                 img = cv2.imread(os.path.join(label_dir, fname), cv2.IMREAD_GRAYSCALE)
  195.                 if img is None:
  196.                     continue
  197.                 resized = cv2.resize(img, IMG_SIZE).flatten()
  198.                 X.append(resized)
  199.                 y.append(int(label))
  200.         for img, lbl in self.new_data:
  201.             resized = cv2.resize(img, IMG_SIZE).flatten()
  202.             X.append(resized)
  203.             y.append(lbl)
  204.         if X:
  205.             X = np.array(X)
  206.             y = np.array(y)
  207.             self.model = RandomForestClassifier(n_estimators=100)
  208.             self.model.fit(X, y)
  209.             joblib.dump(self.model, MODEL_PATH)
  210.             self.new_data.clear()
  211.             self.label_info.config(text="🤖 Model retrained with feedback")
  212.  
  213.     def display_images(self, proc):
  214.         frame = cv2.resize(self.last_frame, (600,450))
  215.         img_proc = proc
  216.         if self.show_heat:
  217.             img_proc = cv2.applyColorMap(proc, cv2.COLORMAP_JET)
  218.         cam_p = ImageTk.PhotoImage(Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)))
  219.         pr_p = ImageTk.PhotoImage(Image.fromarray(img_proc))
  220.         self.label_cam.config(image=cam_p)
  221.         self.label_cam.imgtk = cam_p
  222.         self.label_proc.config(image=pr_p)
  223.         self.label_proc.imgtk = pr_p
  224.  
  225.     def update_frames(self):
  226.         # If frozen, rescan last frozen frame at intervals
  227.         if self.freeze_mode and hasattr(self, 'current') and self.current:
  228.             now = time.time()
  229.             if now - self.last_scan_time > SCAN_INTERVAL:
  230.                 num_old, conf_old, img = self.current
  231.                 num_new, conf_new = self.ensemble_predict(img)
  232.                 if num_new and num_new != num_old:
  233.                     self.current = (num_new, conf_new, img)
  234.                     self.label_info.config(text=f"🔁 Retry: Was {num_new} correct?")
  235.                 elif num_new is None:
  236.                     self.label_info.config(text="📷 Weak image, no digit detected.")
  237.                 self.last_scan_time = now
  238.             self.display_images(self.current[2])
  239.             self.root.after(30, self.update_frames)
  240.             return
  241.  
  242.         # Normal capture and detection
  243.         ret, frame = self.cap.read()
  244.         if not ret:
  245.             self.root.after(30, self.update_frames)
  246.             return
  247.         self.last_frame = frame.copy()
  248.         proc = self.preprocess(frame)
  249.         now = time.time()
  250.  
  251.         # Skip detection if too few white pixels
  252.         if cv2.countNonZero(proc) < MIN_WHITE_PIXELS:
  253.             self.label_info.config(text="📷 Weak image, no digit detected.")
  254.             self.display_images(proc)
  255.             self.root.after(30, self.update_frames)
  256.             return
  257.  
  258.         if not self.pending_feedback and not self.freeze_mode:
  259.             num, conf = self.ensemble_predict(proc)
  260.             if num and (num != self.last_detected or now - self.last_time > COOLDOWN):
  261.                 ts = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
  262.                 cv2.imwrite(f"detections/detected_{ts}.png", frame)
  263.                 self.history.append((num, ts, proc))
  264.                 self.current = (num, conf, proc)
  265.                 self.last_detected, self.last_time = num, now
  266.                 self.pending_feedback = True
  267.                 self.freeze_mode = True  # Freeze until feedback is given
  268.                 self.label_info.config(text=f"❓ Was {num} correct?")
  269.                 self.entry.delete(0, tk.END)
  270.                 self.entry.grid(); self.btn_correct.grid()
  271.  
  272.         self.display_images(proc)
  273.         self.root.after(30, self.update_frames)
  274.  
  275. if __name__ == "__main__":
  276.     DigitDetectorApp()
  277.  
Advertisement
Add Comment
Please, Sign In to add comment