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()