correct_exam / main.py
sunlemoncat's picture
Upload main.py
0a59e8b verified
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()