Spaces:
Sleeping
Sleeping
| 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() |