import customtkinter as ctk from tkinter import filedialog, messagebox from PIL import Image import cv2 import threading import numpy as np # Import Logic from scanner import scan_document from ocr import extract_text class ModernScannerApp(ctk.CTk): def __init__(self): super().__init__() ctk.set_appearance_mode("Dark") ctk.set_default_color_theme("blue") self.title("AI Document Scanner - Pro (with Deskew)") self.geometry("1200x750") self.grid_columnconfigure(1, weight=1) self.grid_rowconfigure(0, weight=1) # === ตัวแปรสำหรับเก็บสถานะรูปภาพ === self.original_scan = None # เก็บภาพตั้งต้นหลังสแกน (ไม่หมุน ไม่ฟิลเตอร์) self.processed_image = None # เก็บภาพที่หมุน/ปรับสีแล้ว (พร้อมโชว์/OCR) # ค่าสถานะการปรับแต่ง self.current_rotation_angle = 0 # องศาการหมุนละเอียด (Slider) self.base_rotation = 0 # องศาการหมุน 90 (0, 90, 180, 270) self.setup_ui() def setup_ui(self): # 1. Sidebar (ซ้าย) self.sidebar = ctk.CTkFrame(self, width=280, corner_radius=0) self.sidebar.grid(row=0, column=0, sticky="nsew") self.sidebar.grid_rowconfigure(9, weight=1) # ดันปุ่ม OCR ลงล่าง self.logo = ctk.CTkLabel(self.sidebar, text="DocScanner\nPro", font=ctk.CTkFont(size=24, weight="bold")) self.logo.grid(row=0, column=0, padx=20, pady=(40, 20)) # ปุ่มเปิดไฟล์ self.btn_open = ctk.CTkButton(self.sidebar, text="📂 เปิดรูปภาพ", height=40, command=self.process_image) self.btn_open.grid(row=1, column=0, padx=20, pady=10, sticky="ew") # --- ส่วนปรับแต่งภาพ --- ctk.CTkLabel(self.sidebar, text="เครื่องมือปรับภาพ", anchor="w", font=ctk.CTkFont(weight="bold")).grid(row=2, column=0, padx=20, pady=(20, 5), sticky="w") # 1. ปุ่มหมุน 90 องศา self.btn_rotate = ctk.CTkButton(self.sidebar, text="🔄 หมุน 90°", fg_color="#F39C12", hover_color="#D68910", command=self.rotate_90_degrees) self.btn_rotate.grid(row=3, column=0, padx=20, pady=5, sticky="ew") # 2. Slider หมุนละเอียด (แก้ภาพเฉียง) ctk.CTkLabel(self.sidebar, text="แก้ภาพเฉียง (Fine Tune):", anchor="w").grid(row=4, column=0, padx=20, pady=(10, 0), sticky="w") self.slider_rotate = ctk.CTkSlider(self.sidebar, from_=-45, to=45, number_of_steps=90, command=self.slider_event) self.slider_rotate.set(0) # เริ่มต้นที่ 0 self.slider_rotate.grid(row=5, column=0, padx=20, pady=5, sticky="ew") self.label_angle = ctk.CTkLabel(self.sidebar, text="0°", font=ctk.CTkFont(size=12)) self.label_angle.grid(row=6, column=0, padx=20, pady=0) # 3. โหมดสี ctk.CTkLabel(self.sidebar, text="โหมดสี (Filters)", anchor="w").grid(row=7, column=0, padx=20, pady=(20, 5), sticky="w") self.filter_mode = ctk.StringVar(value="B&W") self.seg_button = ctk.CTkSegmentedButton(self.sidebar, values=["B&W", "Gray", "Original"], command=self.apply_transformations, variable=self.filter_mode) self.seg_button.grid(row=8, column=0, padx=20, pady=5, sticky="ew") # 2. Image Area (กลาง) self.image_frame = ctk.CTkFrame(self, fg_color="transparent") self.image_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew") # Label แสดงภาพ (สร้างเตรียมไว้เลย) self.image_label = ctk.CTkLabel(self.image_frame, text="[ กรุณาเลือกไฟล์ภาพ ]", font=ctk.CTkFont(size=16)) self.image_label.pack(expand=True) # 3. Text Area (ขวา) self.text_frame = ctk.CTkFrame(self, width=320) self.text_frame.grid(row=0, column=2, padx=(0, 20), pady=20, sticky="nsew") ctk.CTkLabel(self.text_frame, text="📄 ผลลัพธ์ข้อความ (OCR)", font=ctk.CTkFont(weight="bold")).pack(pady=10) self.textbox = ctk.CTkTextbox(self.text_frame, width=300) self.textbox.pack(padx=10, pady=10, expand=True, fill="both") # ปุ่ม OCR self.btn_ocr = ctk.CTkButton(self.text_frame, text="🔍 เริ่มอ่านข้อความ (OCR)", height=50, fg_color="#2ECC71", hover_color="#27AE60", font=ctk.CTkFont(size=16, weight="bold"), command=self.start_ocr_thread) self.btn_ocr.pack(padx=10, pady=20, side="bottom", fill="x") def reset_ui(self): """ ล้างค่าต่างๆ เพื่อเริ่มภาพใหม่ """ self.textbox.delete("0.0", "end") self.slider_rotate.set(0) self.label_angle.configure(text="0°") self.current_rotation_angle = 0 self.base_rotation = 0 # เทคนิคแก้ Pyimage Error: ลบ Label ทิ้งสร้างใหม่ if hasattr(self, 'image_label') and self.image_label is not None: self.image_label.destroy() self.image_label = ctk.CTkLabel(self.image_frame, text="", font=ctk.CTkFont(size=16)) self.image_label.pack(expand=True) def process_image(self): file_path = filedialog.askopenfilename(filetypes=[("Images", "*.jpg *.jpeg *.png")]) if not file_path: return try: self.reset_ui() self.image_label.configure(text="⏳ กำลังสแกน... กรุณารอสักครู่", image=None) self.update() # สแกนภาพ (รับค่าภาพสีมาใช้) warped_color, _ = scan_document(file_path) # เก็บภาพต้นฉบับ self.original_scan = warped_color # แสดงผลครั้งแรก self.apply_transformations() except Exception as e: self.image_label.configure(text=f"Error: {e}", image=None) messagebox.showerror("Error", str(e)) # --- Logic การหมุนภาพและปรับสี (รวมศูนย์ที่เดียว) --- def rotate_90_degrees(self): """ กดปุ่มหมุน 90 องศา """ if self.original_scan is None: return self.base_rotation = (self.base_rotation + 90) % 360 self.apply_transformations() def slider_event(self, value): """ เลื่อน Slider เพื่อแก้ภาพเฉียง """ if self.original_scan is None: return self.current_rotation_angle = value self.label_angle.configure(text=f"{int(value)}°") self.apply_transformations() def apply_transformations(self, _=None): """ ฟังก์ชันรวม: รับภาพต้นฉบับ -> หมุน 90 -> หมุนละเอียด -> ใส่สี -> แสดงผล """ if self.original_scan is None: return image = self.original_scan.copy() # 1. หมุน 90 องศา (Base Rotation) if self.base_rotation == 90: image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) elif self.base_rotation == 180: image = cv2.rotate(image, cv2.ROTATE_180) elif self.base_rotation == 270: image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE) # 2. หมุนละเอียด (Fine Rotation / Deskew) if self.current_rotation_angle != 0: (h, w) = image.shape[:2] center = (w // 2, h // 2) # สร้าง Matrix การหมุน M = cv2.getRotationMatrix2D(center, -self.current_rotation_angle, 1.0) # หมุนภาพ (ใช้สีขาวเติมขอบที่แหว่ง) image = cv2.warpAffine(image, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255)) # 3. ใส่ Filter สี mode = self.filter_mode.get() if mode == "Gray": image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) elif mode == "B&W": gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) _, image = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) # 4. บันทึกผลลัพธ์และแสดงภาพ self.processed_image = image # <--- ภาพนี้จะถูกส่งไป OCR self.display_image(image) def display_image(self, cv_img): # แปลงสีเพื่อแสดงผล if len(cv_img.shape) == 2: rgb = cv2.cvtColor(cv_img, cv2.COLOR_GRAY2RGB) else: rgb = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) pil_img = Image.fromarray(rgb) # Resize ให้พอดีจอ h_target = 550 ratio = pil_img.width / pil_img.height w_target = int(h_target * ratio) ctk_img = ctk.CTkImage(pil_img, size=(w_target, h_target)) self.image_label.configure(image=ctk_img, text="") def start_ocr_thread(self): if self.processed_image is None: messagebox.showwarning("Warning", "กรุณาเปิดไฟล์ภาพก่อน") return self.textbox.delete("0.0", "end") self.textbox.insert("0.0", "⏳ กำลังอ่านข้อความ...") # ส่งภาพที่ผ่านการหมุนแล้ว (processed_image) ไปให้ OCR threading.Thread(target=self.run_ocr).start() def run_ocr(self): text = extract_text(self.processed_image) self.textbox.delete("0.0", "end") self.textbox.insert("0.0", text) if __name__ == "__main__": app = ModernScannerApp() app.mainloop()