Str1k3rch0

Digit Detector App 0.2

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