# 報紙工作欄位自動擷取專案報告 ## 摘要 (Abstract) 本專案開發了一個自動化工具,用於從掃描報紙圖像或 PDF 文件中提取獨立的工作招募欄位,並將其保存為單獨的圖像檔案,以支援數位化存檔或後續處理。該工具採用傳統電腦視覺技術,通過圖像預處理、輪廓檢測、智能過濾、嵌套分析和缺失區域檢測等步驟實現目標。經過多次優化,成功解決了過大/過小區塊、重複提取及部分遺漏等問題,在測試數據集(8 張報紙,約 249 個欄位)中達到 89.16% 的正確提取率和 93.17% 的多區塊提取率。該工具適用於文檔數位化、OCR 前處理及版面分析等場景,未來可通過深度學習、參數自適應及文字方向分析進一步提升性能。 --- ## 1. 當時情境描述 (Scenario Description) 本次專案的目標是自動化處理掃描的報紙圖片(或 PDF 文件),希望能精確地將報紙上刊登的每一個獨立的工作招募欄位(Job Advertisement Block)擷取下來,儲存成獨立的圖片檔案,以便後續處理或數位化存檔。輸入可以是單張圖片或多頁 PDF 文件,輸出則為每個檢測到的欄位獨立保存的圖片以及一個組合圖像。 ## 2. 第一次預想方式 (Initial Approach Idea) 初步構想是利用傳統電腦視覺技術來達成目標,主要思路如下: - **圖片預處理**:對原始報紙圖片進行預處理,以強化欄位邊界並降低背景干擾。具體步驟包括: - 灰階轉換(`cv2.cvtColor` 轉為灰階)。 - 高斯模糊(`cv2.GaussianBlur` 去除雜訊)。 - 自適應二值化(`cv2.adaptiveThreshold` 處理光照不均並突出邊界)。 - Canny 邊緣檢測(`cv2.Canny` 找出明確的邊緣線)。 - **輪廓檢測**:在預處理後的邊緣圖像上,使用輪廓檢測演算法(`cv2.findContours`)找出代表各個欄位的封閉區域。 - **區域裁剪與儲存**:將檢測到的每個輪廓對應的原始圖片區域裁剪下來,儲存為單獨的圖片檔案。 ## 3. 第一次成果 (Initial Result) 根據初步構想實作後,成功擷取出部分工作欄位,但結果不理想,存在以下問題: - **尺寸過大的區塊**:檢測到許多大型區域,例如報紙標題或橫跨多欄的文章區塊。 - **尺寸過小的雜訊區塊**:圖片噪點或細小線條被誤認為獨立輪廓。 - **重複或嵌套區塊**:同一區域因不同層次的邊界檢測而被重複提取。 - **遺漏明顯欄位**:部分工作欄位因邊界模糊或品質問題未被檢測到。 ## 4. 優化過程與策略 (Optimization Process & Strategies) 針對初步成果中發現的問題,進行了以下幾項優化: ### a. 處理過大圖塊 (Handling Oversized Blocks) - **問題描述**:初始輪廓檢測將報紙上的大型區域(如標題區或多欄文章)視為單一輪廓,這些並非目標工作欄位。 - **圖片範例**:![job_section_95](https://hackmd.io/_uploads/Sy_l-yEJeg.jpg) - **說明**:此圖展示一個錯誤檢測的大型區塊,包含報紙標題與多個廣告。 - **解法 (Code Implementation)**: - 在 `process_image` 函數中,設定面積閾值 `max_area_threshold = 0.2 * image.shape[0] * image.shape[1]`(圖片總面積的 20%)。 - 對每個輪廓計算邊界框面積(`w * h`),若超過閾值則忽略。 ```python max_area_threshold = 0.2 * image.shape[0] * image.shape[1] filtered_contours_size = [] for contour in contours: x, y, w, h = cv2.boundingRect(contour) area = w * h if area > max_area_threshold: continue filtered_contours_size.append(contour) ``` - **思維 (Rationale)**:工作欄位通常不會佔據整張報紙的大比例面積,設定上限可排除無關的大型區塊。 - **成效 (Effectiveness)**:顯著減少了輸出中大型無關區塊的數量,保留更合理的候選區域。 ### b. 處理過小圖塊/噪點 (Handling Small Blocks/Noise) - **問題描述**:圖片噪點、文字間空隙或版面細線被誤檢測為小輪廓,這些不是有效工作欄位。 - **圖片範例**:![job_section_41](https://hackmd.io/_uploads/B1HqfJEJgg.jpg) ![job_section_21](https://hackmd.io/_uploads/Hy4CzyEyel.jpg) - **說明**:這些圖展示錯誤檢測的小型區塊,如標題或廣告的片段。 - **解法 (Code Implementation)**: - 設定最小尺寸 `min_dim = 120`(寬度和高度均需大於 120 像素)。 - 設定長寬比限制 `max_aspect_ratio = 5.0`(寬高比和 Highness/Width 比需小於 5)。 ```python min_dim = 120 max_aspect_ratio = 5.0 for contour in filtered_contours_size: x, y, w, h = cv2.boundingRect(contour) if w > min_dim and h > min_dim: if w / h < max_aspect_ratio and h / w < max_aspect_ratio: valid_contours_initial.append(contour) ``` - **思維 (Rationale)**:有效欄位應有一定最小尺寸,且形狀不應過於狹長,這些條件可濾除噪點和線條。 - **成效 (Effectiveness)**:大幅減少了微小、無意義的圖片數量,提升輸出品質。 ### c. 處理重複/嵌套圖塊 (Handling Duplicate/Nested Blocks) - **問題描述**:輪廓檢測可能在不同層次檢測到邊界,導致嵌套或重複提取同一區域。 - **圖片範例**:<img src="https://hackmd.io/_uploads/SkUOKyNyee.jpg" width="150" /> <img src="https://hackmd.io/_uploads/B1af5yVyxg.jpg" width="150" /> - **說明**:此兩張圖為同一區域,因大小不同被切成兩張。 - **解法 (Code Implementation)**: - 使用 `is_contained_bbox(bbox1, bbox2, tolerance=10)` 函數檢查一個邊界框是否被另一個包含(容差 10 像素)。 - 在初次過濾和最終檢查中,移除被包含的較小輪廓,保留較大區域。 ```python def is_contained_bbox(bbox1, bbox2, tolerance=10): x1, y1, w1, h1 = bbox1 x2, y2, w2, h2 = bbox2 return (x1 >= (x2 - tolerance) and y1 >= (y2 - tolerance) and (x1 + w1) <= (x2 + w2 + tolerance) and (y1 + h1) <= (y2 + h2 + tolerance)) ``` - 初次過濾中比較面積,保留較大者: ```python for i, contour in enumerate(valid_contours_initial): bbox_i = temp_initial_bboxes[i] for j, other_bbox in enumerate(temp_initial_bboxes): if i != j and other_bbox is not None and is_contained_bbox(bbox_i, other_bbox): area_i = bbox_i[2] * bbox_i[3] area_j = other_bbox[2] * other_bbox[3] if area_i >= area_j: temp_initial_bboxes[j] = None else: is_contained_by_others = True break ``` - **思維 (Rationale)**:被包含的輪廓通常是外部輪廓的一部分,移除冗餘可保留完整區塊。 - **成效 (Effectiveness)**:減少了重複提取的情況,輸出更乾淨。 ### d. 處理遺漏的區塊 (Handling Missed Blocks) - **問題描述**:因圖片品質或欄位邊界模糊,`cv2.findContours` 未檢測到部分工作欄位。 - **圖片範例**:<img src="https://hackmd.io/_uploads/r1gR_e4Jex.jpg" width="200" /> <img src="https://hackmd.io/_uploads/Bke0OxN1gl.jpg" width="200" /> - **說明**:左圖顯示初次處理後遺漏區塊,右圖為填補後結果。 - **解法 (Code Implementation)**: - **後處理填補策略**: 1. 使用初次檢測的區塊建立遮罩(`mask`),標記已覆蓋區域。 2. 應用形態學操作(膨脹和侵蝕,`cv2.dilate` 和 `cv2.erode`,核大小 30)平滑遮罩。 3. 在反轉遮罩(`inv_mask`)上檢測未覆蓋區域(`cv2.findContours`)。 4. 過濾條件:最小面積 5000、最小尺寸 120、長寬比小於 5、與初次區塊重疊比例低於 50%。 ```python mask = np.zeros((mask_height, mask_width), dtype=np.uint8) for (x, y, w, h) in initial_block_bboxes: relative_y = y - min_y_overall relative_x = x - min_x_overall mask[relative_y:relative_y + h, relative_x:relative_x + w] = 255 kernel = np.ones((30, 30), np.uint8) mask = cv2.dilate(mask, kernel, iterations=3) mask = cv2.erode(mask, kernel, iterations=3) inv_mask = cv2.bitwise_not(mask) missing_contours, _ = cv2.findContours(inv_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) ``` - 將通過過濾的未覆蓋區域加入最終輸出。 - **思維 (Rationale)**:假設初次檢測勾勒出主要內容,空白區域可能是遺漏欄位,填補可提高完整性。 - **成效 (Effectiveness)**:生成更完整的組合圖像(`_final_combined.jpg`),填補部分遺漏,但可能引入非目標內容。 ## 5. 潛在問題與限制 (Potential Issues & Limitations) - **輪廓合併問題**:多個欄位緊密相連或被共同邊框包圍時,`cv2.findContours` 可能將其視為單一輪廓,導致提取的圖片包含多個欄位。 - **參數敏感性**:預處理參數(如高斯模糊核大小 5x5、自適應閾值塊大小 11)、過濾條件(如 `min_dim=120`、`max_area_threshold=0.2`)對不同圖片敏感,需針對具體輸入調優。 - **填補限制**:後處理填補可能誤將空白或分隔線納入,且無法保證語義獨立性。 - **旋轉未處理**:程式碼未處理欄位旋轉問題,需額外分析文字方向。 ## 6. 最終成功率 (Final Success Rate) 經過多次優化後,系統在測試數據集(8 張報紙掃描圖片,包含約 249 個工作招募欄位)上的表現如下: - **正確擷取**:222 個欄位(獨立、完整、無重複或錯誤的欄位)。 - **錯誤擷取**: - 10 個欄位(多區塊區域)。 - 22 個欄位(無效區域)。 - **遺漏欄位**:27 個欄位(未檢測到的真實工作欄位)。 - **成功率計算**: - 總真實欄位數 = 正確擷取數 + 遺漏數 = 222 + 27 = 249。 - 多區塊欄位數 = 正確擷取數 + 多區塊欄位數 = 222 + 10 = 232。 - 真實成功率 = 正確擷取數 / 總真實欄位數 = 222 / 249 = **89.16%**。 - 多區塊成功率 = 多區塊擷取數 / 總真實欄位數 = 232 / 249 = **93.17%**。 ## 7. 可應用的場域 (Applicable Fields) 此方法適用於以下場景: - **文檔數位化**:從掃描件中快速分離內容區塊。 - **OCR 前處理**:為文字識別提供候選區域。 - **資料標註輔助**:生成初步區域,減少人工標註工作。 - **版面分析**:研究出版物佈局特徵。 ## 8. 優化建議 (Optimization Suggestions) - **進階分割**:引入分水嶺演算法或深度學習(如 YOLO、Mask R-CNN)處理輪廓合併問題。 - **參數自適應**:根據圖片分辨率動態調整閾值和尺寸參數。 - **旋轉檢測**:整合文字方向分析(如 Tesseract OCR)處理旋轉欄位。 - **整合 OCR**:提取欄位後自動識別文字,提升自動化程度。 --- [GitHub 專案連結](https://github.com/tom1030507/newspaper_job_extractor) ---