Skip to content

Commit f0c463e

Browse files
许瑞许瑞
许瑞
authored and
许瑞
committed
Merge branch 'master' of https://github.com/myhloli/Magic-PDF
2 parents efed5fa + 3d2fcc9 commit f0c463e

File tree

5 files changed

+228
-83
lines changed

5 files changed

+228
-83
lines changed

demo/ocr_demo.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,10 @@ def ocr_parse_core(book_name, ocr_pdf_path, ocr_pdf_model_info, start_page_id=0,
9090

9191

9292
if __name__ == '__main__':
93-
# pdf_path = r"/home/cxu/workspace/Magic-PDF/ocr_demo/j.1540-627x.2006.00176.x.pdf"
94-
# json_file_path = r"/home/cxu/workspace/Magic-PDF/ocr_demo/j.1540-627x.2006.00176.x.json"
93+
pdf_path = r"/home/cxu/workspace/Magic-PDF/ocr_demo/j.1540-627x.2006.00176.x.pdf"
94+
json_file_path = r"/home/cxu/workspace/Magic-PDF/ocr_demo/j.1540-627x.2006.00176.x.json"
9595
# ocr_local_parse(pdf_path, json_file_path)
96-
# book_name = "数学新星网/edu_00001236"
97-
# ocr_online_parse(book_name)
96+
book_name = "科数网/edu_00011318"
97+
ocr_online_parse(book_name)
98+
9899
pass

magic_pdf/dict2md/ocr_mkcontent.py

+22-6
Original file line numberDiff line numberDiff line change
@@ -72,33 +72,42 @@ def ocr_mk_mm_markdown_with_para(pdf_info_dict: dict):
7272
markdown = []
7373
for _, page_info in pdf_info_dict.items():
7474
paras_of_layout = page_info.get("para_blocks")
75-
page_markdown = ocr_mk_mm_markdown_with_para_core(paras_of_layout)
75+
page_markdown = ocr_mk_mm_markdown_with_para_core(paras_of_layout, "mm")
7676
markdown.extend(page_markdown)
7777
return '\n\n'.join(markdown)
7878

7979

80+
def ocr_mk_nlp_markdown_with_para(pdf_info_dict: dict):
81+
markdown = []
82+
for _, page_info in pdf_info_dict.items():
83+
paras_of_layout = page_info.get("para_blocks")
84+
page_markdown = ocr_mk_mm_markdown_with_para_core(paras_of_layout, "nlp")
85+
markdown.extend(page_markdown)
86+
return '\n\n'.join(markdown)
87+
8088
def ocr_mk_mm_markdown_with_para_and_pagination(pdf_info_dict: dict):
8189
markdown_with_para_and_pagination = []
8290
for page_no, page_info in pdf_info_dict.items():
8391
paras_of_layout = page_info.get("para_blocks")
8492
if not paras_of_layout:
8593
continue
86-
page_markdown = ocr_mk_mm_markdown_with_para_core(paras_of_layout)
94+
page_markdown = ocr_mk_mm_markdown_with_para_core(paras_of_layout, "mm")
8795
markdown_with_para_and_pagination.append({
8896
'page_no': page_no,
8997
'md_content': '\n\n'.join(page_markdown)
9098
})
9199
return markdown_with_para_and_pagination
92100

93101

94-
def ocr_mk_mm_markdown_with_para_core(paras_of_layout):
102+
def ocr_mk_mm_markdown_with_para_core(paras_of_layout, mode):
95103
page_markdown = []
96104
for paras in paras_of_layout:
97105
for para in paras:
98106
para_text = ''
99107
for line in para:
100108
for span in line['spans']:
101109
span_type = span.get('type')
110+
content = ''
102111
if span_type == ContentType.Text:
103112
content = split_long_words(span['content'])
104113
# content = span['content']
@@ -107,9 +116,16 @@ def ocr_mk_mm_markdown_with_para_core(paras_of_layout):
107116
elif span_type == ContentType.InterlineEquation:
108117
content = f"\n$$\n{span['content']}\n$$\n"
109118
elif span_type in [ContentType.Image, ContentType.Table]:
110-
content = f"\n![]({join_path(s3_image_save_path, span['image_path'])})\n"
111-
para_text += content + ' '
112-
page_markdown.append(para_text.strip() + ' ')
119+
if mode == 'mm':
120+
content = f"\n![]({join_path(s3_image_save_path, span['image_path'])})\n"
121+
elif mode == 'nlp':
122+
pass
123+
if content != '':
124+
para_text += content + ' '
125+
if para_text.strip() == '':
126+
continue
127+
else:
128+
page_markdown.append(para_text.strip() + ' ')
113129
return page_markdown
114130

115131

magic_pdf/para/para_split.py

+163-25
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
LINE_STOP_FLAG = ['.', '!', '?', '。', '!', '?',":", ":", ")", ")", ";"]
1010
INLINE_EQUATION = ContentType.InlineEquation
1111
INTERLINE_EQUATION = ContentType.InterlineEquation
12-
TEXT = "text"
12+
TEXT = ContentType.Text
1313

1414

1515
def __get_span_text(span):
@@ -20,7 +20,7 @@ def __get_span_text(span):
2020
return c
2121

2222

23-
def __detect_list_lines(lines, new_layout_bboxes, lang='en'):
23+
def __detect_list_lines(lines, new_layout_bboxes, lang):
2424
"""
2525
探测是否包含了列表,并且把列表的行分开.
2626
这样的段落特点是,顶格字母大写/数字,紧跟着几行缩进的。缩进的行首字母含小写的。
@@ -315,11 +315,14 @@ def __split_para_in_layoutbox(lines_group, new_layout_bbox, lang="en", char_avg_
315315

316316
return layout_paras, list_info
317317

318-
def __connect_list_inter_layout(layout_paras, new_layout_bbox, layout_list_info, page_num, lang="en"):
318+
def __connect_list_inter_layout(layout_paras, new_layout_bbox, layout_list_info, page_num, lang):
319319
"""
320-
如果上个layout的最后一个段落是列表,下一个layout的第一个段落也是列表,那么将他们连接起来。
320+
如果上个layout的最后一个段落是列表,下一个layout的第一个段落也是列表,那么将他们连接起来。 TODO 因为没有区分列表和段落,所以这个方法暂时不实现。
321321
根据layout_list_info判断是不是列表。,下个layout的第一个段如果不是列表,那么看他们是否有几行都有相同的缩进。
322322
"""
323+
if len(layout_paras)==0 or len(layout_list_info)==0: # 0的时候最后的return 会出错
324+
return layout_paras, [False, False]
325+
323326
for i in range(1, len(layout_paras)):
324327
pre_layout_list_info = layout_list_info[i-1]
325328
next_layout_list_info = layout_list_info[i]
@@ -345,7 +348,37 @@ def __connect_list_inter_layout(layout_paras, new_layout_bbox, layout_list_info,
345348
pre_last_para.extend(may_list_lines)
346349
layout_paras[i] = layout_paras[i][len(may_list_lines):]
347350

348-
return layout_paras
351+
return layout_paras, [layout_list_info[0][0], layout_list_info[-1][1]] # 同时还返回了这个页面级别的开头、结尾是不是列表的信息
352+
353+
354+
def __connect_list_inter_page(pre_page_paras, next_page_paras, pre_page_layout_bbox, next_page_layout_bbox, pre_page_list_info, next_page_list_info, page_num, lang):
355+
"""
356+
如果上个layout的最后一个段落是列表,下一个layout的第一个段落也是列表,那么将他们连接起来。 TODO 因为没有区分列表和段落,所以这个方法暂时不实现。
357+
根据layout_list_info判断是不是列表。,下个layout的第一个段如果不是列表,那么看他们是否有几行都有相同的缩进。
358+
"""
359+
if len(pre_page_paras)==0 or len(next_page_paras)==0: # 0的时候最后的return 会出错
360+
return False
361+
362+
if pre_page_list_info[1] and not next_page_list_info[0]: # 前一个是列表结尾,后一个是非列表开头,此时检测是否有相同的缩进
363+
logger.info(f"连接page {page_num} 内的list")
364+
# 向layout_paras[i] 寻找开头具有相同缩进的连续的行
365+
may_list_lines = []
366+
for j in range(len(next_page_paras[0])):
367+
line = next_page_paras[0][j]
368+
if len(line)==1: # 只可能是一行,多行情况再需要分析了
369+
if line[0]['bbox'][0] > __find_layout_bbox_by_line(line[0]['bbox'], next_page_layout_bbox)[0]:
370+
may_list_lines.append(line[0])
371+
else:
372+
break
373+
else:
374+
break
375+
# 如果这些行的缩进是相等的,那么连到上一个layout的最后一个段落上。
376+
if len(may_list_lines)>0 and len(set([x['bbox'][0] for x in may_list_lines]))==1:
377+
pre_page_paras[-1].append(may_list_lines)
378+
next_page_paras[0] = next_page_paras[0][len(may_list_lines):]
379+
return True
380+
381+
return False
349382

350383

351384
def __find_layout_bbox_by_line(line_bbox, layout_bboxes):
@@ -358,7 +391,7 @@ def __find_layout_bbox_by_line(line_bbox, layout_bboxes):
358391
return None
359392

360393

361-
def __connect_para_inter_layoutbox(layout_paras, new_layout_bbox, lang="en"):
394+
def __connect_para_inter_layoutbox(layout_paras, new_layout_bbox, lang):
362395
"""
363396
layout之间进行分段。
364397
主要是计算前一个layOut的最后一行和后一个layout的第一行是否可以连接。
@@ -368,10 +401,19 @@ def __connect_para_inter_layoutbox(layout_paras, new_layout_bbox, lang="en"):
368401
369402
"""
370403
connected_layout_paras = []
404+
if len(layout_paras)==0:
405+
return connected_layout_paras
406+
371407
connected_layout_paras.append(layout_paras[0])
372408
for i in range(1, len(layout_paras)):
373-
pre_last_line = layout_paras[i-1][-1][-1]
374-
next_first_line = layout_paras[i][0][0]
409+
try:
410+
if len(layout_paras[i])==0 or len(layout_paras[i-1])==0: # TODO 考虑连接问题,
411+
continue
412+
pre_last_line = layout_paras[i-1][-1][-1]
413+
next_first_line = layout_paras[i][0][0]
414+
except Exception as e:
415+
logger.error(f"page layout {i} has no line")
416+
continue
375417
pre_last_line_text = ''.join([__get_span_text(span) for span in pre_last_line['spans']])
376418
pre_last_line_type = pre_last_line['spans'][-1]['type']
377419
next_first_line_text = ''.join([__get_span_text(span) for span in next_first_line['spans']])
@@ -400,15 +442,15 @@ def __connect_para_inter_layoutbox(layout_paras, new_layout_bbox, lang="en"):
400442
return connected_layout_paras
401443

402444

403-
def __connect_para_inter_page(pre_page_paras, next_page_paras, pre_page_layout_bbox, next_page_layout_bbox, lang):
445+
def __connect_para_inter_page(pre_page_paras, next_page_paras, pre_page_layout_bbox, next_page_layout_bbox, page_num, lang):
404446
"""
405447
连接起来相邻两个页面的段落——前一个页面最后一个段落和后一个页面的第一个段落。
406448
是否可以连接的条件:
407449
1. 前一个页面的最后一个段落最后一行沾满整个行。并且没有结尾符号。
408450
2. 后一个页面的第一个段落第一行没有空白开头。
409451
"""
410452
# 有的页面可能压根没有文字
411-
if len(pre_page_paras)==0 or len(next_page_paras)==0:
453+
if len(pre_page_paras)==0 or len(next_page_paras)==0 or len(pre_page_paras[0])==0 or len(next_page_paras[0])==0: # TODO [[]]为什么出现在pre_page_paras里?
412454
return False
413455
pre_last_para = pre_page_paras[-1][-1]
414456
next_first_para = next_page_paras[0][0]
@@ -436,8 +478,85 @@ def __connect_para_inter_page(pre_page_paras, next_page_paras, pre_page_layout_b
436478
else:
437479
return False
438480

481+
def find_consecutive_true_regions(input_array):
482+
start_index = None # 连续True区域的起始索引
483+
regions = [] # 用于保存所有连续True区域的起始和结束索引
484+
485+
for i in range(len(input_array)):
486+
# 如果我们找到了一个True值,并且当前并没有在连续True区域中
487+
if input_array[i] and start_index is None:
488+
start_index = i # 记录连续True区域的起始索引
439489

440-
def __do_split(blocks, layout_bboxes, new_layout_bbox, page_num, lang="en"):
490+
# 如果我们找到了一个False值,并且当前在连续True区域中
491+
elif not input_array[i] and start_index is not None:
492+
# 如果连续True区域长度大于1,那么将其添加到结果列表中
493+
if i - start_index > 1:
494+
regions.append((start_index, i-1))
495+
start_index = None # 重置起始索引
496+
497+
# 如果最后一个元素是True,那么需要将最后一个连续True区域加入到结果列表中
498+
if start_index is not None and len(input_array) - start_index > 1:
499+
regions.append((start_index, len(input_array)-1))
500+
501+
return regions
502+
503+
504+
def __connect_middle_align_text(page_paras, new_layout_bbox, page_num, lang, debug_mode):
505+
"""
506+
找出来中间对齐的连续单行文本,如果连续行高度相同,那么合并为一个段落。
507+
一个line居中的条件是:
508+
1. 水平中心点跨越layout的中心点。
509+
2. 左右两侧都有空白
510+
"""
511+
512+
for layout_i, layout_para in enumerate(page_paras):
513+
layout_box = new_layout_bbox[layout_i]
514+
single_line_paras_tag = []
515+
for i in range(len(layout_para)):
516+
single_line_paras_tag.append(len(layout_para[i])==1 and layout_para[i][0]['spans'][0]['type']==TEXT)
517+
518+
"""找出来连续的单行文本,如果连续行高度相同,那么合并为一个段落。"""
519+
consecutive_single_line_indices = find_consecutive_true_regions(single_line_paras_tag)
520+
if len(consecutive_single_line_indices)>0:
521+
index_offset = 0
522+
"""检查这些行是否是高度相同的,居中的"""
523+
for start, end in consecutive_single_line_indices:
524+
start += index_offset
525+
end += index_offset
526+
line_hi = np.array([line[0]['bbox'][3]-line[0]['bbox'][1] for line in layout_para[start:end+1]])
527+
first_line_text = ''.join([__get_span_text(span) for span in layout_para[start][0]['spans']])
528+
if "Table" in first_line_text or "Figure" in first_line_text:
529+
pass
530+
if debug_mode:
531+
logger.info(line_hi.std())
532+
533+
if line_hi.std()<2:
534+
"""行高度相同,那么判断是否居中"""
535+
all_left_x0 = [line[0]['bbox'][0] for line in layout_para[start:end+1]]
536+
all_right_x1 = [line[0]['bbox'][2] for line in layout_para[start:end+1]]
537+
layout_center = (layout_box[0] + layout_box[2]) / 2
538+
if all([x0 < layout_center < x1 for x0, x1 in zip(all_left_x0, all_right_x1)]) \
539+
and not all([x0==layout_box[0] for x0 in all_left_x0]) \
540+
and not all([x1==layout_box[2] for x1 in all_right_x1]):
541+
merge_para = [l[0] for l in layout_para[start:end+1]]
542+
para_text = ''.join([__get_span_text(span) for line in merge_para for span in line['spans']])
543+
if debug_mode:
544+
logger.info(para_text)
545+
layout_para[start:end+1] = [merge_para]
546+
index_offset -= end-start
547+
548+
return
549+
550+
551+
def __merge_signle_list_text(page_paras, new_layout_bbox, page_num, lang):
552+
"""
553+
找出来连续的单行文本,如果首行顶格,接下来的几个单行段落缩进对齐,那么合并为一个段落。
554+
"""
555+
556+
pass
557+
558+
559+
def __do_split_page(blocks, layout_bboxes, new_layout_bbox, page_num, lang):
441560
"""
442561
根据line和layout情况进行分段
443562
先实现一个根据行末尾特征分段的简单方法。
@@ -451,35 +570,54 @@ def __do_split(blocks, layout_bboxes, new_layout_bbox, page_num, lang="en"):
451570
"""
452571
lines_group = __group_line_by_layout(blocks, layout_bboxes, lang) # block内分段
453572
layout_paras, layout_list_info = __split_para_in_layoutbox(lines_group, new_layout_bbox, lang) # layout内分段
454-
layout_paras2 = __connect_list_inter_layout(layout_paras, new_layout_bbox, layout_list_info, page_num, lang) # layout之间连接列表段落
573+
layout_paras2, page_list_info = __connect_list_inter_layout(layout_paras, new_layout_bbox, layout_list_info, page_num, lang) # layout之间连接列表段落
455574
connected_layout_paras = __connect_para_inter_layoutbox(layout_paras2, new_layout_bbox, lang) # layout间链接段落
456575

457-
return connected_layout_paras
458-
459576

460-
def para_split(pdf_info_dict, lang="en"):
577+
return connected_layout_paras, page_list_info
578+
579+
580+
def para_split(pdf_info_dict, debug_mode, lang="en"):
461581
"""
462582
根据line和layout情况进行分段
463583
"""
464584
new_layout_of_pages = [] # 数组的数组,每个元素是一个页面的layoutS
585+
all_page_list_info = [] # 保存每个页面开头和结尾是否是列表
465586
for page_num, page in pdf_info_dict.items():
466587
blocks = page['preproc_blocks']
467588
layout_bboxes = page['layout_bboxes']
468589
new_layout_bbox = __common_pre_proc(blocks, layout_bboxes)
469590
new_layout_of_pages.append(new_layout_bbox)
470-
splited_blocks = __do_split(blocks, layout_bboxes, new_layout_bbox, page_num, lang)
591+
splited_blocks, page_list_info = __do_split_page(blocks, layout_bboxes, new_layout_bbox, page_num, lang)
592+
all_page_list_info.append(page_list_info)
471593
page['para_blocks'] = splited_blocks
472594

473595
"""连接页面与页面之间的可能合并的段落"""
474596
pdf_infos = list(pdf_info_dict.values())
475-
for i, page in enumerate(pdf_info_dict.values()):
476-
if i==0:
597+
for page_num, page in enumerate(pdf_info_dict.values()):
598+
if page_num==0:
477599
continue
478-
pre_page_paras = pdf_infos[i-1]['para_blocks']
479-
next_page_paras = pdf_infos[i]['para_blocks']
480-
pre_page_layout_bbox = new_layout_of_pages[i-1]
481-
next_page_layout_bbox = new_layout_of_pages[i]
600+
pre_page_paras = pdf_infos[page_num-1]['para_blocks']
601+
next_page_paras = pdf_infos[page_num]['para_blocks']
602+
pre_page_layout_bbox = new_layout_of_pages[page_num-1]
603+
next_page_layout_bbox = new_layout_of_pages[page_num]
482604

483-
is_conn= __connect_para_inter_page(pre_page_paras, next_page_paras, pre_page_layout_bbox, next_page_layout_bbox, lang)
484-
if is_conn:
485-
logger.info(f"连接了第{i-1}页和第{i}页的段落")
605+
is_conn = __connect_para_inter_page(pre_page_paras, next_page_paras, pre_page_layout_bbox, next_page_layout_bbox, page_num, lang)
606+
if debug_mode:
607+
if is_conn:
608+
logger.info(f"连接了第{page_num-1}页和第{page_num}页的段落")
609+
610+
is_list_conn = __connect_list_inter_page(pre_page_paras, next_page_paras, pre_page_layout_bbox, next_page_layout_bbox, all_page_list_info[page_num-1], all_page_list_info[page_num], page_num, lang)
611+
if debug_mode:
612+
if is_list_conn:
613+
logger.info(f"连接了第{page_num-1}页和第{page_num}页的列表段落")
614+
615+
"""接下来可能会漏掉一些特别的一些可以合并的内容,对他们进行段落连接
616+
1. 正文中有时出现一个行顶格,接下来几行缩进的情况。
617+
2. 居中的一些连续单行,如果高度相同,那么可能是一个段落。
618+
"""
619+
for page_num, page in enumerate(pdf_info_dict.values()):
620+
page_paras = page['para_blocks']
621+
new_layout_bbox = new_layout_of_pages[page_num]
622+
__connect_middle_align_text(page_paras, new_layout_bbox, page_num, lang, debug_mode=debug_mode)
623+
__merge_signle_list_text(page_paras, new_layout_bbox, page_num, lang)

0 commit comments

Comments
 (0)