import os import cv2 import numpy as np from imutils.object_detection import non_max_suppression import math import re import yaml import ast import time from datetime import datetime import gradio as gr import hashlib from pypdf import PdfReader # from fitz import Document, Pixmap # 計算兩點距離 def calculate_the_distance_between_2_points(points1, points2): return ((points2[0] - points1[0]) ** 2 + (points2[1] - points1[1]) ** 2) ** 0.5 # 計算兩點中點座標 def calculate_the_center_point_of_2_points(points): return [(points[0] + points[2])/2, (points[1] + points[3])/2] # 回傳錯誤訊息 def error_message(message, check_error=""): global CONFIG if CONFIG["img"]["interface"]: if check_error == "error": raise gr.Error("請上傳檔案", title="錯誤") elif check_error == "warning": gr.Warning(message, title="警告") else: gr.Info(message, title="提示") else: print(f"ERROR: {message}") return # 讀取圖片檔案 def read_image_file(filename): return cv2.imdecode(np.fromfile(filename,dtype=np.uint8),-1) def log_safe_filename(name:str): out = name.replace("\\", "/").strip("/").replace(".log", "") log_time = datetime.now().strftime("%Y-%m-%d") out_path = out.replace("%name%", log_time) return f'{out_path}.log' ## 記錄檔使用 # 讀取記錄檔 def read_csv(filename): global CONFIG filename = log_safe_filename(filename) lst = {} with open(f'{filename}', 'r', encoding='utf-8') as csvfile: linedata = csvfile.readlines() for i in linedata: data = i.strip().split(CONFIG["read_write_log"]["data_delimiter"]) # lst[data[0]] = eval(data[1]) lst[data[0]] = ast.literal_eval(data[1]) return lst # def write_dict_csv(filename, lst:dict): # global CONFIG # filename = safe_filename(filename) # with open(f'{filename}', 'w', encoding='utf-8') as csvfile: # for i in lst: # csvfile.write(f'{i}{CONFIG["read_write_log"]["data_delimiter"]}') # csvfile.write(str(lst[i])) # csvfile.write("\n") # 寫入記錄檔 def write_csv(filename, keyname, value): global CONFIG filename = log_safe_filename(filename) with open(f'{filename}', 'a', encoding='utf-8') as csvfile: csvfile.write(f'{keyname}{CONFIG["read_write_log"]["data_delimiter"]}{str(value)}\n') ## # 影像預處理 def preprocess_image(image): gray = image if len(image.shape) == 2 else cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 灰階 # 高斯模糊 blurred_image = cv2.GaussianBlur(gray, (11, 11), 0) # 使用二值化 preprocessing_dualization = int(CONFIG["find_table"]["preprocessing_dualization"]) if preprocessing_dualization >= 255: preprocessing_dualization = 255 elif preprocessing_dualization <= 0: preprocessing_dualization = 0 _, binary_image = cv2.threshold(blurred_image, preprocessing_dualization, 255, cv2.THRESH_BINARY) # 反轉圖片(黑色變白色,白色變黑色) inverted_image = cv2.bitwise_not(binary_image) return inverted_image # 計算匹配並回傳 def match_template(old_image, template_image, CONFIG): old_image = cv2.dilate(old_image, None, iterations=1) template_image = cv2.dilate(template_image, None, iterations=1) result = cv2.matchTemplate(old_image, template_image, cv2.TM_CCOEFF_NORMED) # 找出匹配值高於設定值的部分 (yCoords, xCoords) = np.where(result >= CONFIG["find_table"]["position_image_domain_value"]) # 取得模板大小 template_tH, template_tW = template_image.shape[:2] # 構建所需執行矩形 template_rects = [(x, y, x + template_tW, y + template_tH) for (x, y) in zip(xCoords, yCoords)] # 對矩形應用非極大值抑制(即為定位點的座標) template_pick = non_max_suppression(np.array(template_rects)) # 計算定位點的中心點 position_points = [calculate_the_center_point_of_2_points(rect) for rect in template_pick] return position_points # 檔案或路徑名稱防呆 def safe_filename(name:str): return name.replace("\\", "/").strip(".").strip("/") # 匯出檔案名稱防呆 def safe_filename_export(name:str): out = name.replace("\\", "/").strip("/") return f'{out}/{datetime.now().strftime("%Y_%m_%d_%Hh%Mm%Ss")}.csv' # 陣列文字大小寫統一 def safe_text(lst:list): return [char.lower() for char in lst] # 格式化座號 def format_number(number): number = str(number).replace(" ", "").zfill(2) if number == "": return "00" return number # 檢查字典 def check_dict(data, key): if key not in data: return False, key count = 1 temp_key = key while temp_key in data: temp_key = f"{key}_{count}" count += 1 return True, temp_key # 轉換為字母 def convert_alphabet(data): """ 字母對照(參考考選部https://wwwc.moex.gov.tw/main/content/wHandMenuFile.ashx?file_id=404) A:A, B:B, C:C, D:D, F:AB, G:AC, H:AD, J:BC, K:BD, M:CD, P:ABC, Q:ABD, S:ACD V:BCD, Z:ABCD, 未作答:= """ if type(data) == list: if data == [False, False, False, False]: return "=" elif data == [True, False, False, False]: return "A" elif data == [False, True, False, False]: return "B" elif data == [False, False, True, False]: return "C" elif data == [False, False, False, True]: return "D" elif data == [True, True, False, False]: return "F" elif data == [True, False, True, False]: return "G" elif data == [True, False, False, True]: return "H" elif data == [False, True, True, False]: return "J" elif data == [False, True, False, True]: return "K" elif data == [False, False, True, True]: return "M" elif data == [True, True, True, False]: return "P" elif data == [True, True, False, True]: return "Q" elif data == [True, False, True, True]: return "S" elif data == [False, True, True, True]: return "V" elif data == [True, True, True, True]: return "Z" else: return "Error" else: return data # 取得答案檔 def get_ans_file(all_img_file:list, CONFIG): global ans_file_name # 取得檔案名稱 file_name = safe_filename(CONFIG["img"]["ans_file_name"]) if file_name.split(".")[-1].lower() in safe_text(CONFIG["img"]["format"]): if file_name in all_img_file: ans_file_name = file_name all_img_file.remove(file_name) return [f"{file_name}/ansfile?.ansfile?"] + all_img_file else: if CONFIG["img"]["interface"]: return [f"{file_name}/ansfile?.ansfile?"] + all_img_file else: return ["nofile?.ansfile?"] + all_img_file else: for file_extension in safe_text(CONFIG["img"]["format"]): file_name = f"{file_name}.{file_extension}" if file_name in all_img_file: ans_file_name = file_name all_img_file.remove(file_name) return [f"{file_name}/ansfile?.ansfile?"] + all_img_file else: return ["nofile?.ansfile?"] + all_img_file # 載入配置文件 with open("config.yaml", "r", encoding="utf-8") as fr: CONFIG = yaml.safe_load(fr) # 載入答案定位圖片 template = read_image_file(safe_filename(f'{CONFIG["img"]["contrast_folder"]}/{CONFIG["img"]["ans_template_file"]}')) # 載入座號定位圖片 number_template = read_image_file(safe_filename(f'{CONFIG["img"]["contrast_folder"]}/{CONFIG["img"]["number_template_file"]}')) # 主程式 def main_function(): global CONFIG # 確保有此資料夾 folder_path = safe_filename(CONFIG["img"]["folder_path"]) if not os.path.exists(folder_path): os.makedirs(folder_path) error_message(f"查無此資料夾 以自動建立資料夾 {CONFIG['img']['folder_path']}", "warning") # 確保有答案 have_ans = [] # 記錄每題答錯人數 question_error_count = [0]*CONFIG["score_setting"]["number_of_questions"] # 紀錄每人資料 everyone_data = {} # 使用for提取資料夾中所有圖片 for filename in get_ans_file(os.listdir(folder_path), CONFIG): filename = safe_filename(filename) print(filename) # 檢測是否符合格式 增加一個條件用於檢測答案檔 check_is_ans_file = False # 判斷是否為答案檔案 check_file_name = filename.split(".")[-1].lower() if check_file_name == "ansfile?": ans_file_split = filename.split("/") if ans_file_split[-1] == "ansfile?.ansfile?": filename = "/".join(ans_file_split[:-1]) check_is_ans_file = True elif ans_file_split[-1] == "nofile?.ansfile?": error_message(f"找不到指定的答案檔案", "wraning") continue elif not check_file_name in safe_text(CONFIG["img"]["format"]): error_message(f"{filename} 不是符合要求的圖片格式 以下是可接受的格式:\n{', '.join(CONFIG['img']['format'])}", "wraning") continue # 讀取影像 if check_is_ans_file and CONFIG["img"]["interface"]: image = read_image_file(f"{filename}") else: image = read_image_file(f"{safe_filename(CONFIG['img']['folder_path'])}/{filename}") if image is None: error_message(f"讀取 {filename} 失敗", "wraning") continue # 影像預處理 thresh = preprocess_image(image) # 檢測用放大圖片 thresh = cv2.dilate(preprocess_image(image), None, iterations=2) contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 過濾輪廓並找出矩形 並指定最大值 max_area = 0 contour_with_max_area = None for contour in contours: peri = cv2.arcLength(contour, True) approx = cv2.approxPolyDP(contour, 0.02 * peri, True) if len(approx) == 4: area = cv2.contourArea(contour) if area > max_area: max_area = area contour_with_max_area = approx if contour_with_max_area is None: error_message(f"{filename} 找不到有效的矩形", "wraning") continue # 使用透視變換切割圖片 if CONFIG["find_table"]["auto_perspective"]: # 重塑矩形 sorting_pos_pts = np.array(contour_with_max_area).reshape(4, 2) # 建立一個存放最後結果的矩形 contour_with_max_area_ordered = np.zeros((4, 2), dtype="float32") # 找出左上角和右下角 sorting_pos_s = [sum(i) for i in sorting_pos_pts] contour_with_max_area_ordered[0] = sorting_pos_pts[np.argmin(sorting_pos_s)] contour_with_max_area_ordered[2] = sorting_pos_pts[np.argmax(sorting_pos_s)] # 找出右上角和左下角 sorting_pos_diff = np.diff(sorting_pos_pts, axis=1) contour_with_max_area_ordered[1] = sorting_pos_pts[np.argmin(sorting_pos_diff)] contour_with_max_area_ordered[3] = sorting_pos_pts[np.argmax(sorting_pos_diff)] # 取得圖像寬度 existing_image_width = image.shape[1] # 計算左上角到右上角的距離 distance_between_top_left_and_top_right = calculate_the_distance_between_2_points(contour_with_max_area_ordered[0], contour_with_max_area_ordered[1]) # 計算左上角到左下角的距離 distance_between_top_left_and_bottom_left = calculate_the_distance_between_2_points(contour_with_max_area_ordered[0], contour_with_max_area_ordered[3]) # 計算長寬比 aspect_ratio = distance_between_top_left_and_bottom_left / distance_between_top_left_and_top_right # 設定新圖像的長寬 new_image_width = existing_image_width new_image_height = int(new_image_width * aspect_ratio) # 透視變換 perspective_transform_pts1 = np.float32(contour_with_max_area_ordered) perspective_transform_pts2 = np.float32([[0, 0], [new_image_width, 0], [new_image_width, new_image_height], [0, new_image_height]]) # 計算透視變換矩陣(好像是把pts1映射到pts2) perspective_transform_matrix = cv2.getPerspectiveTransform(perspective_transform_pts1, perspective_transform_pts2) # 將它應用到圖像上 perspective_transformed_image就是表格了 use_image = cv2.warpPerspective(image, perspective_transform_matrix, (new_image_width, new_image_height)) # 直接檢測方格裁切 else: x, y, w, h = cv2.boundingRect(contour_with_max_area) use_image = image[y:y+h, x:x+w] # 將圖片調整為指定大小 use_image = cv2.resize(use_image, (CONFIG["find_table"]["crop_ratio_mult"]*12, CONFIG["find_table"]["crop_ratio_mult"]*13)) # 大概是12:13(寬:高)的比例 # 儲存空白用 # cv2.imwrite("minus_blank.png", use_image) # 尋找出定位點 # 預處理圖片 use_image_preprocess = preprocess_image(use_image) # 裁切後的原始圖片 number_template_preprocess = preprocess_image(number_template) # 座號定位模板 template_preprocess = preprocess_image(template) # 答案定位模板 # 將原有答案卷部分清除 只保留劃記痕跡 # use_image_blank = cv2.subtract(use_image_preprocess, minus_blank_preprocess) # 影像預處理(相對於常用的預處理不太一樣 ) use_img_gray = use_image if len(use_image.shape) == 2 else cv2.cvtColor(use_image, cv2.COLOR_BGR2GRAY) use_img_blur = cv2.GaussianBlur(use_img_gray, (3,3), 0) use_img_thresh = cv2.threshold(use_img_blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1] # 檢測水平線 horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (100,1)) horizontal_mask = cv2.morphologyEx(use_img_thresh, cv2.MORPH_OPEN, horizontal_kernel, iterations=1) # 檢測垂直線 vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1,100)) vertical_mask = cv2.morphologyEx(use_img_thresh, cv2.MORPH_OPEN, vertical_kernel, iterations=1) # 合併水平線和垂直線 table_mask = cv2.bitwise_or(horizontal_mask, vertical_mask) # 將答案卷中線條清除 use_image_blank = cv2.cvtColor(use_image_preprocess, cv2.COLOR_GRAY2BGR) use_image_blank[np.where(table_mask==255)] = [0,0,0] use_image_blank = use_image_preprocess # 檢查方向是否顛倒 # 答案座標 position_points = match_template(use_image_blank, template_preprocess, CONFIG) np.random.shuffle(position_points) # 號碼座標 position_points_number = match_template(use_image_blank, number_template_preprocess, CONFIG) np.random.shuffle(position_points_number) # 檢查圖片 if (position_points[0][2] < position_points_number[0][2]) and (position_points[-1][2] < position_points_number[-1][2]): error_message(f"{filename} 答案卷方向錯誤, 以自動轉正", "info") use_image_blank = cv2.rotate(use_image_blank, cv2.ROTATE_180) elif (position_points[0][2] < position_points_number[0][2]): error_message(f"{filename} 答案卷疑似有問題, 結果可能有誤, 以自動轉正", "warning") use_image_blank = cv2.rotate(use_image_blank, cv2.ROTATE_180) # 取得答案 # 計算匹配值 position_points = match_template(use_image_preprocess, template_preprocess, CONFIG) # 存放答案用陣列 all_ans_out = [] # 計算寬度間隔 position_quantity = len(position_points) each_lattice_width = (CONFIG["find_table"]["crop_ratio_mult"]*11) / (1 if position_quantity < 1 else position_quantity) / (CONFIG["find_table"]["number_of_answers"]+1) # 檢查用 # ct = 0 # clone = use_image.copy() ### break_check = False for position in sorted(position_points): # 計算高度間隔 each_lattice_height = ((CONFIG["find_table"]["crop_ratio_mult"]*13)-position[1]) / (CONFIG["find_table"]["number_of_rows"]+1) # 計算格子座標 ans_number_selected = [] for number in range(CONFIG["find_table"]["number_of_rows"]): # 檢測超出範圍退出 if len(all_ans_out)*CONFIG["find_table"]["number_of_rows"]+len(ans_number_selected) >= CONFIG["score_setting"]["number_of_questions"]: break_check = True break # 計算高度 pos_y = position[1]+each_lattice_width/20 + (number+1)*each_lattice_height + number/3*each_lattice_height*0.1 ans_selected_option = [] for option in range(CONFIG["find_table"]["number_of_answers"]): # 計算寬度 pos_x = position[0]-each_lattice_width/3.1 + (option+1)*each_lattice_width + option*each_lattice_width*0.065 # 取得答案 get_ans_img = use_image_blank[int(pos_y-each_lattice_height/3):int(pos_y+each_lattice_height/3), int(pos_x):int(pos_x+each_lattice_width/1.5-(each_lattice_width/20 if option >= CONFIG["find_table"]["number_of_answers"]-1 else 0))] # 檢查用 # cv2.rectangle(clone, (int(pos_x), int(pos_y-each_lattice_height/3)), (int(pos_x+each_lattice_width/1.5-(each_lattice_width/20 if option >= CONFIG["find_table"]["number_of_answers"]-1 else 0)), int(pos_y+each_lattice_height/3)), (0, 0, 255), 1) # cv2.imwrite(f"out.png", clone) ### # 將圖片中內容放大 ans_img_dilated = cv2.dilate(get_ans_img, None, iterations=CONFIG["find_table"]["option_detect_expansion_degree"]) # 計算像素點數量 total_pixels = ans_img_dilated.size # 計算白色像素點數量 white_pixels = np.sum(ans_img_dilated > 200) # 計算比率 white_pixel_ratio = white_pixels/total_pixels*100 if white_pixel_ratio > CONFIG["find_table"]["option_detect_domain_value"]: ans_selected_option.append(True) else: ans_selected_option.append(False) ans_number_selected.append(ans_selected_option) # 達到設定值退出 all_ans_out.append(ans_number_selected) # 檢測退出 if break_check: break # 檢查用 # ct += 1 # cv2.imwrite(f"out.png", clone) ### # 讀取座號 # 計算匹配值 position_points_number = sorted(match_template(use_image_preprocess, number_template_preprocess, CONFIG), key=lambda x: x[1]) # 計算座號 # 檢查用 # clone = use_image.copy() ### seat_number = ["", ""] if len(position_points_number) == 3: interval_height = abs(position_points_number[1][1]-position_points_number[0][1]) for posnum in range(1, 3): position = position_points_number[posnum] pos_y = position[1] for num in range(10): pos_x = position[0] - position[0]/13*(num+1) - position[0]*0.0015*(num) - position[0]*0.003*num/3 # 取得座號 get_number_img = use_image_blank[int(pos_y-0.22*CONFIG["find_table"]["crop_ratio_mult"]):int(pos_y+0.16*CONFIG["find_table"]["crop_ratio_mult"]), int(pos_x-0.16*CONFIG["find_table"]["crop_ratio_mult"]):int(pos_x+0.16*CONFIG["find_table"]["crop_ratio_mult"])] # 檢查用 # cv2.rectangle(clone, (int(pos_x-0.16*CONFIG["find_table"]["crop_ratio_mult"]), int(pos_y-0.22*CONFIG["find_table"]["crop_ratio_mult"])), (int(pos_x+0.16*CONFIG["find_table"]["crop_ratio_mult"]), int(pos_y+0.16*CONFIG["find_table"]["crop_ratio_mult"])), (0, 0, 255), 3) # cv2.imwrite(f"out.png", clone) # cv2.imwrite(f"out.png", get_number_img) ### # 將座號放大 number_img_dilated = cv2.dilate(get_number_img, None, iterations=CONFIG["find_table"]["option_detect_expansion_degree"]) # 計算像素點數量 total_pixels = number_img_dilated.size # 計算白色像素點數量 white_pixels = np.sum(number_img_dilated > 200) # 計算比率 white_pixel_ratio = white_pixels/total_pixels*100 if white_pixel_ratio > CONFIG["find_table"]["option_detect_domain_value"]: seat_number[posnum-1] += str(9-num) break # 格式化座號 image_number = format_number("".join(seat_number)) else: # 無座號直接使用檔案名稱 image_number = format_number(filename) # 紀錄時間 write_csv(f'{safe_filename(CONFIG["read_write_log"]["log_file_name"])}', f">>現在時間", datetime.strftime(datetime.now(), "%Y年%m月%d日%H點%M分%S秒")) # 如果是答案就存起來 if check_is_ans_file: have_ans = all_ans_out.copy() save_data = [CONFIG["img"]["ans_file_name"], all_ans_out, 100] # everyone_data[CONFIG["img"]["ans_file_name"]] = [0, all_ans_out, CONFIG["score_setting"]["number_of_questions"], 100] write_csv(f'{safe_filename(CONFIG["read_write_log"]["log_file_name"])}', filename, save_data) error_message(f"已找到答案檔案 {filename} 並用於評分") continue # 確認是否有答案 if len(have_ans) <= 0: # 儲存答案 finish_check, finish_number = check_dict(everyone_data, image_number) save_data = [finish_number, all_ans_out, -1] everyone_data[finish_number] = [int(image_number) if image_number.isdigit() else image_number, all_ans_out, -1, -1] write_csv(f'{safe_filename(CONFIG["read_write_log"]["log_file_name"])}', filename, save_data) # 有答案 else: # have_ans all_ans_out # 計算分數 score_count = 0 one_question_score = CONFIG["score_setting"]["total_score"]/CONFIG["score_setting"]["number_of_questions"] for column in range(len(all_ans_out)): for row in range(CONFIG["find_table"]["number_of_rows"]): if (column*CONFIG["find_table"]["number_of_rows"]+row) >= CONFIG["score_setting"]["number_of_questions"]:break if have_ans[column][row] == all_ans_out[column][row]: score_count += 1 all_ans_out[column][row] = "-" else: question_error_count[column*CONFIG["find_table"]["number_of_rows"]+row] += 1 score = math.ceil(score_count * one_question_score) # 儲存答案 finish_check, finish_number = check_dict(everyone_data, image_number) save_data = [finish_number, all_ans_out, score] everyone_data[finish_number] = [int(image_number) if image_number.isdigit() else image_number, all_ans_out, score_count, score] write_csv(f'{safe_filename(CONFIG["read_write_log"]["log_file_name"])}', filename, save_data) continue # 將結果匯出成表格 # 設定說明文字 explanation_text = [ "符號說明,答案正確:-,答案錯誤:呈現學生答案,未作答:=", "複選時代碼對照,A:A,B:B,C:C", "D:D,F:AB,G:AC,H:AD", "J:BC,K:BD,M:CD,P:ABC", "Q:ABD,S:ACD,V:BCD,Z:ABCD" ] explanation_count = 2 output_file_name = f'{safe_filename_export(CONFIG["img"]["folder_path"])}' with open(f'{output_file_name}', "w", encoding="utf-8-sig") as csvfile: # 寫入正確答案 如沒有填入空白 if len(have_ans) <= 0: csvfile.write("\\ ,"+f'標準答案 ,{" ,"*CONFIG["score_setting"]["number_of_questions"]}答對題數,得分 ') else: csvfile.write("\\ ,"+f'標準答案 ,') for column in have_ans: for row in column: csvfile.write(f" {convert_alphabet(row)},") csvfile.write(f'答對題數,得分 ') # 第一行說明 csvfile.write(f",{explanation_text[0]}\n") # 寫入題號 csvfile.write("座號 ,題號 ,",) for num in range(CONFIG["score_setting"]["number_of_questions"]): csvfile.write(f"{str(num+1).zfill(2)},") # 第二行說明 csvfile.write(f'{" "*8},{" "*6},{explanation_text[1]}\n') # 寫入學生答案 total_score = [] for number in sorted(everyone_data.items(), key=lambda x: (x[0])): csvfile.write(f'{number[0]:4s}號,學生答案 ,') for column in number[1][1]: for row in column: csvfile.write(f" {convert_alphabet(row)},") csvfile.write(f'{str(number[1][2]):8s},{str(number[1][3]):6s}') # 寫入後續說明 if explanation_count < len(explanation_text): csvfile.write(f',{explanation_text[explanation_count]}\n') explanation_count += 1 else: csvfile.write('\n') total_score.append(number[1][3]) # 補足說明部分 while explanation_count < len(explanation_text): csvfile.write(f'{" "*6},{" "*10},{" ,"*CONFIG["score_setting"]["number_of_questions"]}{" "*8},{" "*6},{explanation_text[explanation_count]}\n') explanation_count += 1 csvfile.write(f'\\ ,答錯人數 ,') for count in question_error_count: csvfile.write(f'{str(count).zfill(2)},') # 寫入平均 total_score = [score for score in total_score if score != -1] if len(have_ans) <= 0: total_score = [0] csvfile.write(f'平均 ,{sum(total_score)/len(total_score):<6.1f}\n') # 最高分 csvfile.write(f'{" "*6},{" "*10},{" ,"*CONFIG["score_setting"]["number_of_questions"]}最高分 ,{max(total_score):<6d}\n') # 最低分 csvfile.write(f'{" "*6},{" "*10},{" ,"*CONFIG["score_setting"]["number_of_questions"]}最低分 ,{min(total_score):<6d}\n') return output_file_name # 頁面用函數 def run_script(pdf_file, number_of_questions, total_score, ans_file): global CONFIG # 檢查檔案 if not pdf_file: raise gr.Error("請上傳檔案", title="錯誤") # 建立資料夾 work_folder = os.path.join(CONFIG["img"]["pdf_to_img_folder"], datetime.now().strftime("%Y%m%d%H%M%S")) os.makedirs(work_folder, exist_ok=True) # 轉換資料夾 # pdf_doc = Document(pdf_file) # for dct in range(len(pdf_doc)): # for img in pdf_doc.get_page_images(dct): # xref = img[0] # pix = Pixmap(pdf_doc, xref) # pix.save(os.path.join(work_folder, f'{dct}_{hashlib.md5(f"img_{dct+1}_datetime.now()".encode()).hexdigest()}.png')) pdf_doc = PdfReader(pdf_file.name) for page_num, pdf_page in enumerate(pdf_doc.pages): # 使用 enumerate 為頁面編號 for pdf_image in pdf_page.images: # 使用圖片索引確保唯一性 # 使用頁碼和圖片名稱來生成唯一檔名 image_path = os.path.join(work_folder, f'{page_num}_{hashlib.md5(f"{pdf_image.name}_{page_num + 1}_{datetime.now()}".encode()).hexdigest()}') with open(f"{image_path}.png", "wb") as pdf_path: pdf_path.write(pdf_image.data) # 將資料寫入設定檔 CONFIG["img"]["folder_path"] = work_folder CONFIG["score_setting"]["number_of_questions"] = number_of_questions CONFIG["score_setting"]["total_score"] = total_score CONFIG["img"]["ans_file_name"] = ans_file.name if ans_file else "" output_file_name = main_function() return output_file_name # 執行程式 if __name__ == "__main__": if CONFIG["img"]["interface"]: # 建立介面 iface = gr.Interface( fn=run_script, inputs=[ gr.File(label="上傳檔案", file_types=[".pdf"]), gr.Slider(minimum=1, maximum=50, label="輸入題目數量", step=1, value=50), gr.Slider(minimum=1, maximum=100, label="輸入總分", value=100), gr.File(label="上傳上傳答案檔") ], outputs=[ gr.File(label="下載檔案", type="filepath") ], title="智慧視覺閱卷系統", description="上傳掃描過後的答案卷來自動評分", submit_btn = "一鍵執行" ) iface.launch(share=True, inbrowser=True) # 自動於瀏覽器中開啟 else: main_function()