Spaces:
No application file
No application file
File size: 30,433 Bytes
0a59e8b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 | 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()
|