Str1k3rch0

Digit Detector App 1.0

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