mnhkahn commited on
Commit
c2e1fed
·
1 Parent(s): e02acba

feat: 添加表格检测与OCR功能

Browse files

实现表格检测、结构识别和OCR文本提取功能
- 新增表格检测模型加载和预处理工具
- 添加表格结构识别和单元格坐标提取功能
- 集成EasyOCR进行文本识别并输出CSV格式
- 重构前端使用Streamlit展示检测结果
- 更新依赖项以支持新功能

__pycache__/app.cpython-313.pyc ADDED
Binary file (17.8 kB). View file
 
app.py CHANGED
@@ -17,6 +17,19 @@ import logging
17
  from PIL.ExifTags import TAGS
18
  from gradio_client import Client, handle_file
19
  import requests
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  # 设置日志
22
  logging.basicConfig(level=logging.INFO)
@@ -319,52 +332,6 @@ async def upload_text(file: UploadFile = File(...)):
319
 
320
  - **file**: 需要识别的图片文件 (jpg, png, etc.)
321
  """
322
- # 检查文件类型
323
- if not file.content_type.startswith("image/"):
324
- raise HTTPException(status_code=400, detail="上传的文件必须是图片格式。")
325
-
326
- try:
327
- # 保存上传的文件到临时目录
328
- temp_file_path = f"/tmp/{file.filename}"
329
- with open(temp_file_path, "wb") as f:
330
- f.write(await file.read())
331
-
332
- # 调用千帆模型进行OCR识别
333
- client = Client("baidu/Qianfan-OCR-Demo", hf_token=hf_token)
334
- result = client.predict(
335
- file_paths=[handle_file(temp_file_path)],
336
- prompt="Please extract the text from the image.",
337
- layout_as_thought=False,
338
- max_new_tokens=2048,
339
- api_name="/run_inference",
340
- )
341
-
342
- # 清理临时文件
343
- if os.path.exists(temp_file_path):
344
- os.remove(temp_file_path)
345
-
346
- # 检查结果类型并进行适当处理
347
- logger.info(f"OCR识别结果类型: {type(result)}")
348
- logger.info(f"OCR识别结果: {result}")
349
-
350
- # 确保返回的是有效的JSON
351
- if isinstance(result, str):
352
- return {"result": result}
353
- elif isinstance(result, dict):
354
- return {"result": result}
355
- else:
356
- return {"result": str(result)}
357
- except Exception as e:
358
- # 清理临时文件
359
- if os.path.exists(temp_file_path):
360
- os.remove(temp_file_path)
361
- logger.error(f"OCR识别错误: {e}")
362
- import traceback
363
-
364
- logger.error(f"错误堆栈: {traceback.format_exc()}")
365
- raise HTTPException(
366
- status_code=500, detail=f"服务器内部错误: {str(e)} {traceback.format_exc()}"
367
- )
368
 
369
 
370
  from fastapi.staticfiles import StaticFiles
 
17
  from PIL.ExifTags import TAGS
18
  from gradio_client import Client, handle_file
19
  import requests
20
+ import streamlit as st
21
+ from PIL import Image
22
+ from utils.model import load_detection_model, load_structure_model
23
+ from utils.preprocessing import prepare_image, prepare_cropped_image
24
+ from utils.detection import (
25
+ detect_tables,
26
+ detect_cells,
27
+ outputs_to_objects,
28
+ objects_to_crops,
29
+ get_cell_coordinates_by_row,
30
+ )
31
+ from utils.visualization import visualize_detected_tables, plot_results
32
+ from utils.ocr import apply_ocr, save_csv
33
 
34
  # 设置日志
35
  logging.basicConfig(level=logging.INFO)
 
332
 
333
  - **file**: 需要识别的图片文件 (jpg, png, etc.)
334
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
 
337
  from fastapi.staticfiles import StaticFiles
requirements.txt CHANGED
@@ -7,4 +7,11 @@ opencv-python
7
  numpy
8
  python-multipart==0.0.9
9
  gradio_client
10
- requests
 
 
 
 
 
 
 
 
7
  numpy
8
  python-multipart==0.0.9
9
  gradio_client
10
+ requests
11
+ streamlit
12
+ transformers
13
+ huggingface_hub
14
+ matplotlib
15
+ easyocr
16
+ tqdm
17
+ pandas
utils/detection.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ def detect_tables(model, pixel_values):
4
+ with torch.no_grad():
5
+ outputs = model(pixel_values)
6
+ return outputs
7
+
8
+ def detect_cells(model, pixel_values):
9
+ with torch.no_grad():
10
+ outputs = model(pixel_values)
11
+ return outputs
12
+
13
+ def outputs_to_objects(outputs, img_size, id2label):
14
+ def box_cxcywh_to_xyxy(x):
15
+ x_c, y_c, w, h = x.unbind(-1)
16
+ b = [(x_c - 0.5 * w), (y_c - 0.5 * h), (x_c + 0.5 * w), (y_c + 0.5 * h)]
17
+ return torch.stack(b, dim=1)
18
+
19
+ def rescale_bboxes(out_bbox, size):
20
+ img_w, img_h = size
21
+ b = box_cxcywh_to_xyxy(out_bbox)
22
+ b = b * torch.tensor([img_w, img_h, img_w, img_h], dtype=torch.float32)
23
+ return b
24
+
25
+ # Add "no object" to id2label if not present
26
+ if len(id2label) not in id2label:
27
+ id2label[len(id2label)] = "no object"
28
+
29
+ m = outputs.logits.softmax(-1).max(-1)
30
+ pred_labels = list(m.indices.detach().cpu().numpy())[0]
31
+ pred_scores = list(m.values.detach().cpu().numpy())[0]
32
+ pred_bboxes = outputs['pred_boxes'].detach().cpu()[0]
33
+ pred_bboxes = [elem.tolist() for elem in rescale_bboxes(pred_bboxes, img_size)]
34
+
35
+ print(f"Predicted labels: {pred_labels}")
36
+ print(f"id2label: {id2label}")
37
+
38
+ objects = []
39
+ for label, score, bbox in zip(pred_labels, pred_scores, pred_bboxes):
40
+ try:
41
+ class_label = id2label[int(label)]
42
+ except KeyError:
43
+ print(f"Label {label} not found in id2label. Skipping.")
44
+ continue
45
+ if not class_label == 'no object':
46
+ objects.append({'label': class_label, 'score': float(score),
47
+ 'bbox': [float(elem) for elem in bbox]})
48
+
49
+ return objects
50
+
51
+ def objects_to_crops(img, tokens, objects, class_thresholds, padding=10):
52
+ table_crops = []
53
+ for obj in objects:
54
+ if obj['score'] < class_thresholds[obj['label']]:
55
+ continue
56
+
57
+ cropped_table = {}
58
+ bbox = obj['bbox']
59
+ bbox = [bbox[0] - padding, bbox[1] - padding, bbox[2] + padding, bbox[3] + padding]
60
+ cropped_img = img.crop(bbox)
61
+
62
+ table_tokens = [token for token in tokens if iob(token['bbox'], bbox) >= 0.5]
63
+ for token in table_tokens:
64
+ token['bbox'] = [token['bbox'][0] - bbox[0], token['bbox'][1] - bbox[1], token['bbox'][2] - bbox[0], token['bbox'][3] - bbox[1]]
65
+
66
+ if obj['label'] == 'table rotated':
67
+ cropped_img = cropped_img.rotate(270, expand=True)
68
+ for token in table_tokens:
69
+ bbox = token['bbox']
70
+ bbox = [cropped_img.size[0] - bbox[3] - 1, bbox[0], cropped_img.size[0] - bbox[1] - 1, bbox[2]]
71
+ token['bbox'] = bbox
72
+
73
+ cropped_table['image'] = cropped_img
74
+ cropped_table['tokens'] = table_tokens
75
+ table_crops.append(cropped_table)
76
+
77
+ return table_crops
78
+
79
+ def get_cell_coordinates_by_row(table_data):
80
+ rows = [entry for entry in table_data if entry['label'] == 'table row']
81
+ columns = [entry for entry in table_data if entry['label'] == 'table column']
82
+ rows.sort(key=lambda x: x['bbox'][1])
83
+ columns.sort(key=lambda x: x['bbox'][0])
84
+
85
+ def find_cell_coordinates(row, column):
86
+ cell_bbox = [column['bbox'][0], row['bbox'][1], column['bbox'][2], row['bbox'][3]]
87
+ return cell_bbox
88
+
89
+ cell_coordinates = []
90
+ for row in rows:
91
+ row_cells = []
92
+ for column in columns:
93
+ cell_bbox = find_cell_coordinates(row, column)
94
+ row_cells.append({'column': column['bbox'], 'cell': cell_bbox})
95
+ row_cells.sort(key=lambda x: x['column'][0])
96
+ cell_coordinates.append({'row': row['bbox'], 'cells': row_cells, 'cell_count': len(row_cells)})
97
+ cell_coordinates.sort(key=lambda x: x['row'][1])
98
+ return cell_coordinates
utils/model.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import AutoModelForObjectDetection, TableTransformerForObjectDetection
3
+
4
+ def load_detection_model():
5
+ model = AutoModelForObjectDetection.from_pretrained("microsoft/table-transformer-detection", revision="no_timm")
6
+ device = "cuda" if torch.cuda.is_available() else "cpu"
7
+ model.to(device)
8
+ return model, device
9
+
10
+ def load_structure_model(device):
11
+ model = TableTransformerForObjectDetection.from_pretrained("microsoft/table-structure-recognition-v1.1-all")
12
+ model.to(device)
13
+ return model
utils/ocr.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import easyocr
3
+ from tqdm.auto import tqdm
4
+ import csv
5
+
6
+ reader = easyocr.Reader(['en']) # this needs to run only once to load the model into memory
7
+
8
+ def apply_ocr(cell_coordinates, cropped_table):
9
+ data = dict()
10
+ max_num_columns = 0
11
+ for idx, row in enumerate(tqdm(cell_coordinates)):
12
+ row_text = []
13
+ for cell in row["cells"]:
14
+ cell_image = np.array(cropped_table.crop(cell["cell"]))
15
+ result = reader.readtext(np.array(cell_image))
16
+ if len(result) > 0:
17
+ text = " ".join([x[1] for x in result])
18
+ row_text.append(text)
19
+ if len(row_text) > max_num_columns:
20
+ max_num_columns = len(row_text)
21
+ data[idx] = row_text
22
+
23
+ print("Max number of columns:", max_num_columns)
24
+ for row, row_data in data.copy().items():
25
+ if len(row_data) != max_num_columns:
26
+ row_data = row_data + ["" for _ in range(max_num_columns - len(row_data))]
27
+ data[row] = row_data
28
+ return data
29
+
30
+ def save_csv(data):
31
+ with open('output.csv', 'w') as result_file:
32
+ wr = csv.writer(result_file, dialect='excel')
33
+ for row, row_text in data.items():
34
+ wr.writerow(row_text)
utils/preprocessing.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from torchvision import transforms
2
+ from PIL import Image
3
+
4
+ class MaxResize(object):
5
+ def __init__(self, max_size=800):
6
+ self.max_size = max_size
7
+
8
+ def __call__(self, image):
9
+ width, height = image.size
10
+ current_max_size = max(width, height)
11
+ scale = self.max_size / current_max_size
12
+ resized_image = image.resize((int(round(scale*width)), int(round(scale*height))))
13
+ return resized_image
14
+
15
+ detection_transform = transforms.Compose([
16
+ MaxResize(800),
17
+ transforms.ToTensor(),
18
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
19
+ ])
20
+
21
+ structure_transform = transforms.Compose([
22
+ MaxResize(1000),
23
+ transforms.ToTensor(),
24
+ transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
25
+ ])
26
+
27
+ def prepare_image(image, device):
28
+ pixel_values = detection_transform(image).unsqueeze(0)
29
+ pixel_values = pixel_values.to(device)
30
+ return pixel_values
31
+
32
+ def prepare_cropped_image(cropped_image, device):
33
+ pixel_values = structure_transform(cropped_image).unsqueeze(0)
34
+ pixel_values = pixel_values.to(device)
35
+ return pixel_values
utils/visualization.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+ import matplotlib.patches as patches
3
+ from matplotlib.patches import Patch
4
+
5
+ def visualize_detected_tables(img, det_tables):
6
+ plt.imshow(img, interpolation="lanczos")
7
+ fig = plt.gcf()
8
+ fig.set_size_inches(20, 20)
9
+ ax = plt.gca()
10
+
11
+ for det_table in det_tables:
12
+ bbox = det_table['bbox']
13
+ if det_table['label'] == 'table':
14
+ facecolor = (1, 0, 0.45)
15
+ edgecolor = (1, 0, 0.45)
16
+ alpha = 0.3
17
+ linewidth = 2
18
+ hatch = '//////'
19
+ elif det_table['label'] == 'table rotated':
20
+ facecolor = (0.95, 0.6, 0.1)
21
+ edgecolor = (0.95, 0.6, 0.1)
22
+ alpha = 0.3
23
+ linewidth = 2
24
+ hatch = '//////'
25
+ else:
26
+ continue
27
+
28
+ rect = patches.Rectangle(bbox[:2], bbox[2] - bbox[0], bbox[3] - bbox[1], linewidth=linewidth,
29
+ edgecolor='none', facecolor=facecolor, alpha=0.1)
30
+ ax.add_patch(rect)
31
+ rect = patches.Rectangle(bbox[:2], bbox[2] - bbox[0], bbox[3] - bbox[1], linewidth=linewidth,
32
+ edgecolor=edgecolor, facecolor='none', linestyle='-', alpha=alpha)
33
+ ax.add_patch(rect)
34
+ rect = patches.Rectangle(bbox[:2], bbox[2] - bbox[0], bbox[3] - bbox[1], linewidth=0,
35
+ edgecolor=edgecolor, facecolor='none', linestyle='-', hatch=hatch, alpha=0.2)
36
+ ax.add_patch(rect)
37
+
38
+ plt.xticks([], [])
39
+ plt.yticks([], [])
40
+ legend_elements = [Patch(facecolor=(1, 0, 0.45), edgecolor=(1, 0, 0.45), label='Table', hatch='//////', alpha=0.3),
41
+ Patch(facecolor=(0.95, 0.6, 0.1), edgecolor=(0.95, 0.6, 0.1), label='Table (rotated)', hatch='//////', alpha=0.3)]
42
+ plt.legend(handles=legend_elements, bbox_to_anchor=(0.5, -0.02), loc='upper center', borderaxespad=0,
43
+ fontsize=10, ncol=2)
44
+ plt.gcf().set_size_inches(10, 10)
45
+ plt.axis('off')
46
+ return fig
47
+
48
+ def plot_results(cropped_table, cells, class_to_visualize):
49
+ plt.figure(figsize=(16, 10))
50
+ plt.imshow(cropped_table)
51
+ ax = plt.gca()
52
+
53
+ for cell in cells:
54
+ score = cell["score"]
55
+ bbox = cell["bbox"]
56
+ label = cell["label"]
57
+ if label == class_to_visualize:
58
+ xmin, ymin, xmax, ymax = tuple(bbox)
59
+ ax.add_patch(plt.Rectangle((xmin, ymin), xmax - xmin, ymax - ymin, fill=False, color="red", linewidth=3))
60
+ text = f'{cell["label"]}: {score:0.2f}'
61
+ ax.text(xmin, ymin, text, fontsize=15, bbox=dict(facecolor='yellow', alpha=0.5))
62
+ plt.axis('off')
63
+ return plt.gcf()