Spaces:
Running on Zero
Running on Zero
Remove unused modules/assets from earlier upload
Browse files- NaviOCR/data_reader_writer/__init__.py +0 -10
- NaviOCR/data_reader_writer/base.py +0 -62
- NaviOCR/data_reader_writer/filebase.py +0 -62
- NaviOCR/data_reader_writer/imagefile.py +0 -66
- NaviOCR/engine.py +0 -161
- NaviOCR/src/__init__.py +0 -1
- NaviOCR/src/model_output_to_middle_json.py +0 -37
- NaviOCR/src/vlm_analyze.py +0 -63
- NaviOCR/src/vlm_magic_model.py +0 -561
- NaviOCR/src/vlm_middle_json_mkcontent.py +0 -176
- NaviOCR/tools/boxbase.py +0 -224
- NaviOCR/tools/char_utils.py +0 -55
- NaviOCR/tools/check_sys_env.py +0 -38
- NaviOCR/tools/config_reader.py +0 -136
- NaviOCR/tools/cut_image.py +0 -36
- NaviOCR/tools/draw_bbox.py +0 -634
- NaviOCR/tools/enum_class.py +0 -67
- NaviOCR/tools/guess_suffix_or_lang.py +0 -55
- NaviOCR/tools/hash_utils.py +0 -30
- NaviOCR/tools/language.py +0 -48
- NaviOCR/tools/magic_model_utils.py +0 -251
- NaviOCR/tools/model_utils.py +0 -462
- NaviOCR/tools/os_env_config.py +0 -30
- NaviOCR/tools/pdf_image_tools.py +0 -26
- NaviOCR/tools/pdf_image_tools_PyMuPDF.py +0 -289
- NaviOCR/tools/pdf_image_tools_pdfium.py +0 -268
- NaviOCR/tools/pdf_page_id.py +0 -10
- NaviOCR/tools/pdf_reader.py +0 -111
- NaviOCR/tools/read_file.py +0 -21
- NaviOCR/version.py +0 -1
- NaviOCR/vlm_utils/NaviOCR_model.py +0 -126
- NaviOCR/vlm_utils/version.py +0 -1
- NaviOCR/vlm_utils/vlm_client/vllm_async_engine_client.py +0 -259
- NaviOCR/vlm_utils/vlm_client/vllm_engine_client.py +0 -255
- NaviOCR/vlm_utils/vlm_client/vllm_v1_no_repeat_ngram.py +0 -93
- examples/layout.jpg +0 -3
- examples/layout_distorted.jpg +0 -3
- examples/text.png +0 -0
NaviOCR/data_reader_writer/__init__.py
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
from .base import DataReader, DataWriter
|
| 2 |
-
from .filebase import FileBasedDataReader, FileBasedDataWriter
|
| 3 |
-
from .imagefile import ImageDataWriter
|
| 4 |
-
__all__ = [
|
| 5 |
-
"DataReader",
|
| 6 |
-
"DataWriter",
|
| 7 |
-
"FileBasedDataReader",
|
| 8 |
-
"FileBasedDataWriter",
|
| 9 |
-
"ImageDataWriter",
|
| 10 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/data_reader_writer/base.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
from abc import ABC, abstractmethod
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
class DataReader(ABC):
|
| 6 |
-
|
| 7 |
-
def read(self, path: str) -> bytes:
|
| 8 |
-
"""Read the file.
|
| 9 |
-
|
| 10 |
-
Args:
|
| 11 |
-
path (str): file path to read
|
| 12 |
-
|
| 13 |
-
Returns:
|
| 14 |
-
bytes: the content of the file
|
| 15 |
-
"""
|
| 16 |
-
return self.read_at(path)
|
| 17 |
-
|
| 18 |
-
@abstractmethod
|
| 19 |
-
def read_at(self, path: str, offset: int = 0, limit: int = -1) -> bytes:
|
| 20 |
-
"""Read the file at offset and limit.
|
| 21 |
-
|
| 22 |
-
Args:
|
| 23 |
-
path (str): the file path
|
| 24 |
-
offset (int, optional): the number of bytes skipped. Defaults to 0.
|
| 25 |
-
limit (int, optional): the length of bytes want to read. Defaults to -1.
|
| 26 |
-
|
| 27 |
-
Returns:
|
| 28 |
-
bytes: the content of the file
|
| 29 |
-
"""
|
| 30 |
-
pass
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
class DataWriter(ABC):
|
| 34 |
-
@abstractmethod
|
| 35 |
-
def write(self, path: str, data: bytes) -> None:
|
| 36 |
-
"""Write the data to the file.
|
| 37 |
-
|
| 38 |
-
Args:
|
| 39 |
-
path (str): the target file where to write
|
| 40 |
-
data (bytes): the data want to write
|
| 41 |
-
"""
|
| 42 |
-
pass
|
| 43 |
-
|
| 44 |
-
def write_string(self, path: str, data: str) -> None:
|
| 45 |
-
"""Write the data to file, the data will be encoded to bytes.
|
| 46 |
-
|
| 47 |
-
Args:
|
| 48 |
-
path (str): the target file where to write
|
| 49 |
-
data (str): the data want to write
|
| 50 |
-
"""
|
| 51 |
-
|
| 52 |
-
def safe_encode(data: str, method: str):
|
| 53 |
-
try:
|
| 54 |
-
bit_data = data.encode(encoding=method, errors='replace')
|
| 55 |
-
return bit_data, True
|
| 56 |
-
except: # noqa
|
| 57 |
-
return None, False
|
| 58 |
-
for method in ['utf-8', 'ascii']:
|
| 59 |
-
bit_data, flag = safe_encode(data, method)
|
| 60 |
-
if flag:
|
| 61 |
-
self.write(path, bit_data)
|
| 62 |
-
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/data_reader_writer/filebase.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
-
from .base import DataReader, DataWriter
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
class FileBasedDataReader(DataReader):
|
| 7 |
-
def __init__(self, parent_dir: str = ''):
|
| 8 |
-
"""Initialized with parent_dir.
|
| 9 |
-
|
| 10 |
-
Args:
|
| 11 |
-
parent_dir (str, optional): the parent directory that may be used within methods. Defaults to ''.
|
| 12 |
-
"""
|
| 13 |
-
self._parent_dir = parent_dir
|
| 14 |
-
|
| 15 |
-
def read_at(self, path: str, offset: int = 0, limit: int = -1) -> bytes:
|
| 16 |
-
"""Read at offset and limit.
|
| 17 |
-
|
| 18 |
-
Args:
|
| 19 |
-
path (str): the path of file, if the path is relative path, it will be joined with parent_dir.
|
| 20 |
-
offset (int, optional): the number of bytes skipped. Defaults to 0.
|
| 21 |
-
limit (int, optional): the length of bytes want to read. Defaults to -1.
|
| 22 |
-
|
| 23 |
-
Returns:
|
| 24 |
-
bytes: the content of file
|
| 25 |
-
"""
|
| 26 |
-
fn_path = path
|
| 27 |
-
if not os.path.isabs(fn_path) and len(self._parent_dir) > 0:
|
| 28 |
-
fn_path = os.path.join(self._parent_dir, path)
|
| 29 |
-
|
| 30 |
-
with open(fn_path, 'rb') as f:
|
| 31 |
-
f.seek(offset)
|
| 32 |
-
if limit == -1:
|
| 33 |
-
return f.read()
|
| 34 |
-
else:
|
| 35 |
-
return f.read(limit)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
class FileBasedDataWriter(DataWriter):
|
| 39 |
-
def __init__(self, parent_dir: str = '') -> None:
|
| 40 |
-
"""Initialized with parent_dir.
|
| 41 |
-
|
| 42 |
-
Args:
|
| 43 |
-
parent_dir (str, optional): the parent directory that may be used within methods. Defaults to ''.
|
| 44 |
-
"""
|
| 45 |
-
self._parent_dir = parent_dir
|
| 46 |
-
|
| 47 |
-
def write(self, path: str, data: bytes) -> None:
|
| 48 |
-
"""Write file with data.
|
| 49 |
-
|
| 50 |
-
Args:
|
| 51 |
-
path (str): the path of file, if the path is relative path, it will be joined with parent_dir.
|
| 52 |
-
data (bytes): the data want to write
|
| 53 |
-
"""
|
| 54 |
-
fn_path = path
|
| 55 |
-
if not os.path.isabs(fn_path) and len(self._parent_dir) > 0:
|
| 56 |
-
fn_path = os.path.join(self._parent_dir, path)
|
| 57 |
-
|
| 58 |
-
if not os.path.exists(os.path.dirname(fn_path)) and os.path.dirname(fn_path) != "":
|
| 59 |
-
os.makedirs(os.path.dirname(fn_path), exist_ok=True)
|
| 60 |
-
|
| 61 |
-
with open(fn_path, 'wb') as f:
|
| 62 |
-
f.write(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/data_reader_writer/imagefile.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from io import BytesIO
|
| 3 |
-
from PIL import Image
|
| 4 |
-
from typing import Any, Dict, List
|
| 5 |
-
|
| 6 |
-
class ImageDataWriter:
|
| 7 |
-
def __init__(self, parent_dir: str = "", format = "JPEG") -> None:
|
| 8 |
-
|
| 9 |
-
self._parent_dir = parent_dir
|
| 10 |
-
self.images = {}
|
| 11 |
-
self.format = format
|
| 12 |
-
|
| 13 |
-
def add_image(self, img_bytes, image_name: str) -> None:
|
| 14 |
-
self.images[image_name] = img_bytes
|
| 15 |
-
|
| 16 |
-
def save_image(self, image_name: str) -> None:
|
| 17 |
-
if image_name not in self.images:
|
| 18 |
-
raise ValueError(f"Image {image_name} not found in memory.")
|
| 19 |
-
|
| 20 |
-
img_bytes = self.images[image_name]
|
| 21 |
-
file_path = os.path.join(self._parent_dir, image_name)
|
| 22 |
-
|
| 23 |
-
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
| 24 |
-
|
| 25 |
-
with open(file_path, "wb") as f:
|
| 26 |
-
f.write(img_bytes)
|
| 27 |
-
|
| 28 |
-
def save_all_images(self):
|
| 29 |
-
os.makedirs(self._parent_dir, exist_ok=True)
|
| 30 |
-
for image_name in self.images:
|
| 31 |
-
self.save_image(image_name)
|
| 32 |
-
self.clear_buffer()
|
| 33 |
-
|
| 34 |
-
def clear_buffer(self):
|
| 35 |
-
self.images.clear()
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def replace_image_path_in_json(
|
| 40 |
-
contents: Any,
|
| 41 |
-
mapping: Dict[str, str],
|
| 42 |
-
sub_type: List[str],
|
| 43 |
-
key_name: str = "image_path",
|
| 44 |
-
) -> Any:
|
| 45 |
-
def _replace_value(filename: str) -> str:
|
| 46 |
-
if filename in mapping:
|
| 47 |
-
return mapping[filename]
|
| 48 |
-
raise KeyError(f"Missing mapping for {filename}")
|
| 49 |
-
|
| 50 |
-
def _walk(obj: Any) -> Any:
|
| 51 |
-
if isinstance(obj, dict):
|
| 52 |
-
return {
|
| 53 |
-
k: _replace_value(v) if k == key_name and isinstance(v, str) else _walk(v)
|
| 54 |
-
for k, v in obj.items()
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
if isinstance(obj, list):
|
| 58 |
-
return [_walk(i) for i in obj]
|
| 59 |
-
|
| 60 |
-
return obj
|
| 61 |
-
|
| 62 |
-
for i, content in enumerate(contents):
|
| 63 |
-
if isinstance(content, dict) and content.get("type") in sub_type:
|
| 64 |
-
contents[i] = _walk(content)
|
| 65 |
-
|
| 66 |
-
return contents
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/engine.py
DELETED
|
@@ -1,161 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import os
|
| 3 |
-
import time
|
| 4 |
-
from loguru import logger
|
| 5 |
-
|
| 6 |
-
from NaviOCR.tools.pdf_image_tools import convert_pdf_bytes_to_bytes
|
| 7 |
-
from NaviOCR.data_reader_writer import FileBasedDataWriter, ImageDataWriter
|
| 8 |
-
from NaviOCR.tools.draw_bbox import draw_layout_bbox
|
| 9 |
-
from NaviOCR.src.vlm_middle_json_mkcontent import union_make
|
| 10 |
-
from NaviOCR.src.vlm_analyze import doc_analyze
|
| 11 |
-
from NaviOCR.src.vlm_analyze import aio_doc_analyze
|
| 12 |
-
|
| 13 |
-
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
| 14 |
-
|
| 15 |
-
def prepare_env(output_dir, pdf_file_name):
|
| 16 |
-
local_md_dir = str(os.path.join(output_dir, pdf_file_name))
|
| 17 |
-
local_image_dir = os.path.join(str(local_md_dir), "images")
|
| 18 |
-
os.makedirs(local_image_dir, exist_ok=True)
|
| 19 |
-
os.makedirs(local_md_dir, exist_ok=True)
|
| 20 |
-
return local_image_dir, local_md_dir
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def _prepare_pdf_bytes(pdf_bytes_list, valid_page_ids):
|
| 24 |
-
result = []
|
| 25 |
-
for idx, pdf_bytes in enumerate(pdf_bytes_list):
|
| 26 |
-
valid_single_page_ids = (
|
| 27 |
-
valid_page_ids[idx]
|
| 28 |
-
if valid_page_ids and idx < len(valid_page_ids)
|
| 29 |
-
else None
|
| 30 |
-
)
|
| 31 |
-
new_pdf_bytes = convert_pdf_bytes_to_bytes(pdf_bytes, valid_single_page_ids)
|
| 32 |
-
result.append(new_pdf_bytes)
|
| 33 |
-
return result
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def _process_output(
|
| 37 |
-
pdf_info,
|
| 38 |
-
pdf_bytes,
|
| 39 |
-
pdf_file_name,
|
| 40 |
-
local_md_dir,
|
| 41 |
-
local_image_dir,
|
| 42 |
-
md_writer,
|
| 43 |
-
middle_json,
|
| 44 |
-
f_draw_layout_bbox=True,
|
| 45 |
-
f_dump_md=True,
|
| 46 |
-
f_dump_middle_json=True,
|
| 47 |
-
):
|
| 48 |
-
if f_draw_layout_bbox:
|
| 49 |
-
draw_layout_bbox(pdf_info, pdf_bytes, local_md_dir, f"{pdf_file_name}_layout.pdf")
|
| 50 |
-
image_dir = str(os.path.basename(local_image_dir))
|
| 51 |
-
if f_dump_md:
|
| 52 |
-
md_content_str = union_make(pdf_info, image_dir)
|
| 53 |
-
md_writer.write_string(
|
| 54 |
-
f"{pdf_file_name}.md",
|
| 55 |
-
md_content_str,
|
| 56 |
-
)
|
| 57 |
-
if f_dump_middle_json:
|
| 58 |
-
md_writer.write_string(
|
| 59 |
-
f"{pdf_file_name}_middle.json",
|
| 60 |
-
json.dumps(middle_json, ensure_ascii=False, indent=4),
|
| 61 |
-
)
|
| 62 |
-
logger.info(f"local output dir is {local_md_dir}")
|
| 63 |
-
|
| 64 |
-
async def _async_process_vlm(output_dir, pdf_file_names, pdf_bytes_list, **kwargs,):
|
| 65 |
-
results = []
|
| 66 |
-
for idx, pdf_bytes in enumerate(pdf_bytes_list):
|
| 67 |
-
pdf_file_name = pdf_file_names[idx]
|
| 68 |
-
local_image_dir, local_md_dir = prepare_env(output_dir, pdf_file_name)
|
| 69 |
-
image_writer = ImageDataWriter(local_image_dir)
|
| 70 |
-
vlm_doc_analyze_time = time.time()
|
| 71 |
-
middle_json = await aio_doc_analyze(
|
| 72 |
-
pdf_bytes, image_writer=image_writer, **kwargs,
|
| 73 |
-
)
|
| 74 |
-
vlm_doc_analyze_time = round(time.time() - vlm_doc_analyze_time, 2)
|
| 75 |
-
logger.debug(f"doc_analyze cost: {vlm_doc_analyze_time}")
|
| 76 |
-
|
| 77 |
-
pdf_info = middle_json["pdf_info"]
|
| 78 |
-
md_writer = FileBasedDataWriter(local_md_dir)
|
| 79 |
-
|
| 80 |
-
process_output_time = time.time()
|
| 81 |
-
_process_output(
|
| 82 |
-
pdf_info, pdf_bytes, pdf_file_name, local_md_dir, local_image_dir,
|
| 83 |
-
md_writer, middle_json
|
| 84 |
-
)
|
| 85 |
-
image_writer.save_all_images()
|
| 86 |
-
process_output_time = round(time.time() - process_output_time, 2)
|
| 87 |
-
logger.debug(f"process_output_time cost: {process_output_time}")
|
| 88 |
-
results.append(middle_json)
|
| 89 |
-
return results
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def _process_vlm(output_dir, pdf_file_names, pdf_bytes_list, **kwargs,):
|
| 93 |
-
results = []
|
| 94 |
-
for idx, pdf_bytes in enumerate(pdf_bytes_list):
|
| 95 |
-
pdf_file_name = pdf_file_names[idx]
|
| 96 |
-
local_image_dir, local_md_dir = prepare_env(output_dir, pdf_file_name)
|
| 97 |
-
image_writer = ImageDataWriter(local_image_dir)
|
| 98 |
-
|
| 99 |
-
vlm_doc_analyze_time = time.time()
|
| 100 |
-
middle_json = doc_analyze(
|
| 101 |
-
pdf_bytes, image_writer=image_writer, **kwargs,
|
| 102 |
-
)
|
| 103 |
-
vlm_doc_analyze_time = round(time.time() - vlm_doc_analyze_time, 2)
|
| 104 |
-
logger.debug(f"doc_analyze cost: {vlm_doc_analyze_time}")
|
| 105 |
-
|
| 106 |
-
pdf_info = middle_json["pdf_info"]
|
| 107 |
-
md_writer = FileBasedDataWriter(local_md_dir)
|
| 108 |
-
|
| 109 |
-
process_output_time = time.time()
|
| 110 |
-
_process_output(
|
| 111 |
-
pdf_info, pdf_bytes, pdf_file_name, local_md_dir, local_image_dir,
|
| 112 |
-
md_writer, middle_json
|
| 113 |
-
)
|
| 114 |
-
image_writer.save_all_images()
|
| 115 |
-
process_output_time = round(time.time() - process_output_time, 2)
|
| 116 |
-
logger.debug(f"process_output_time cost: {process_output_time}")
|
| 117 |
-
|
| 118 |
-
results.append(middle_json)
|
| 119 |
-
return results
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def inplace_change_page_ids(results, valid_page_ids):
|
| 123 |
-
for result, valid_page_id in zip(results, valid_page_ids):
|
| 124 |
-
if valid_page_id is None:
|
| 125 |
-
continue
|
| 126 |
-
for page in result.get("pages", []):
|
| 127 |
-
page_id = page.get("page_id")
|
| 128 |
-
if (
|
| 129 |
-
isinstance(page_id, int)
|
| 130 |
-
and 0 <= page_id < len(valid_page_id)
|
| 131 |
-
):
|
| 132 |
-
page["page_id"] = valid_page_id[page_id]
|
| 133 |
-
return results
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
async def aio_do_parse(
|
| 137 |
-
output_dir,
|
| 138 |
-
pdf_file_names: list[str],
|
| 139 |
-
pdf_bytes_list: list[bytes],
|
| 140 |
-
valid_page_ids: list[list[int] | None],
|
| 141 |
-
**kwargs,
|
| 142 |
-
):
|
| 143 |
-
pdf_bytes_list = _prepare_pdf_bytes(pdf_bytes_list, valid_page_ids)
|
| 144 |
-
Results = await _async_process_vlm(
|
| 145 |
-
output_dir, pdf_file_names, pdf_bytes_list, **kwargs,
|
| 146 |
-
)
|
| 147 |
-
return Results
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
def do_parse(
|
| 151 |
-
output_dir,
|
| 152 |
-
pdf_file_names: list[str],
|
| 153 |
-
pdf_bytes_list: list[bytes],
|
| 154 |
-
valid_page_ids: list[list[int] | None],
|
| 155 |
-
**kwargs,
|
| 156 |
-
):
|
| 157 |
-
pdf_bytes_list = _prepare_pdf_bytes(pdf_bytes_list, valid_page_ids)
|
| 158 |
-
Results = _process_vlm(
|
| 159 |
-
output_dir, pdf_file_names, pdf_bytes_list, **kwargs,
|
| 160 |
-
)
|
| 161 |
-
return Results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/src/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
|
|
|
|
|
|
NaviOCR/src/model_output_to_middle_json.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
from tqdm import tqdm
|
| 2 |
-
from NaviOCR.tools.pdf_image_tools import get_page_size
|
| 3 |
-
from NaviOCR.src.vlm_magic_model import MagicModel
|
| 4 |
-
from NaviOCR.tools.cut_image import cut_image_and_table
|
| 5 |
-
from NaviOCR.tools.enum_class import ContentType
|
| 6 |
-
from NaviOCR.tools.hash_utils import bytes_md5
|
| 7 |
-
from NaviOCR.version import __version__
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def blocks_to_page_info(page_blocks,image_dict,page,image_writer,page_index) -> dict:
|
| 11 |
-
scale = image_dict["scale"]
|
| 12 |
-
page_pil_img = image_dict["img_pil"]
|
| 13 |
-
page_img_md5 = bytes_md5(page_pil_img.tobytes())
|
| 14 |
-
width, height = map(int, get_page_size(page))
|
| 15 |
-
magic_model = MagicModel(page_blocks, width, height)
|
| 16 |
-
all_spans = magic_model.get_all_spans()
|
| 17 |
-
for span in all_spans:
|
| 18 |
-
if span["type"] in [
|
| 19 |
-
ContentType.IMAGE,
|
| 20 |
-
ContentType.SEAL,
|
| 21 |
-
ContentType.CHAR,
|
| 22 |
-
]:
|
| 23 |
-
cut_image_and_table(span,page_pil_img,page_img_md5,page_index,image_writer,scale=scale)
|
| 24 |
-
page_blocks = magic_model.get_page_blocks()
|
| 25 |
-
page_info = {"para_blocks": page_blocks,"discarded_blocks": [],"page_size": [width, height],"page_idx": page_index}
|
| 26 |
-
return page_info
|
| 27 |
-
|
| 28 |
-
def result_to_middle_json(model_output_blocks_list, images_list, pdf_doc, image_writer):
|
| 29 |
-
middle_json = {"pdf_info": [],"_backend": "vlm","_version_name": __version__,}
|
| 30 |
-
for index, page_blocks in enumerate(tqdm(model_output_blocks_list)):
|
| 31 |
-
page = pdf_doc[index]
|
| 32 |
-
image_dict = images_list[index]
|
| 33 |
-
page_info = blocks_to_page_info(page_blocks,image_dict,page,image_writer,index)
|
| 34 |
-
middle_json['pdf_info'].append(page_info)
|
| 35 |
-
|
| 36 |
-
pdf_doc.close()
|
| 37 |
-
return middle_json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/src/vlm_analyze.py
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
import time
|
| 2 |
-
from loguru import logger
|
| 3 |
-
from ..data_reader_writer import DataWriter
|
| 4 |
-
from ..tools.enum_class import ImageType
|
| 5 |
-
from .model_output_to_middle_json import result_to_middle_json
|
| 6 |
-
from NaviOCR.vlm_utils.NaviOCR_model import NaviOCRMODEL_SERVICE
|
| 7 |
-
from NaviOCR.tools.pdf_image_tools import load_images_from_pdf
|
| 8 |
-
import NaviOCR.config as CONFIG
|
| 9 |
-
|
| 10 |
-
def _init_model():
|
| 11 |
-
backend=CONFIG.BACKEND
|
| 12 |
-
model_path = CONFIG.model_path
|
| 13 |
-
predictor = NaviOCRMODEL_SERVICE.get_model(backend, model_path, None)
|
| 14 |
-
return predictor
|
| 15 |
-
|
| 16 |
-
async def aio_doc_analyze(
|
| 17 |
-
pdf_bytes,
|
| 18 |
-
predictor = None,
|
| 19 |
-
image_writer = None,
|
| 20 |
-
):
|
| 21 |
-
if predictor is None:
|
| 22 |
-
predictor = _init_model()
|
| 23 |
-
load_images_start = time.time()
|
| 24 |
-
images_list, pdf_doc = load_images_from_pdf(pdf_bytes, image_type=ImageType.PIL, threads=CONFIG.PDF_TOOLS_WORKER_MAX_NUM)
|
| 25 |
-
images_pil_list = [image_dict["img_pil"] for image_dict in images_list]
|
| 26 |
-
load_images_time = round(time.time() - load_images_start, 2)
|
| 27 |
-
logger.debug(f"load images cost: {load_images_time}, speed: {round(load_images_time/len(images_pil_list), 3)} images/s")
|
| 28 |
-
infer_start = time.time()
|
| 29 |
-
results = await predictor.aio_batch_two_step_extract(images=images_pil_list)
|
| 30 |
-
infer_time = round(time.time() - infer_start, 2)
|
| 31 |
-
logger.debug(f"infer finished, cost: {infer_time}, speed: {round(infer_time / len(results), 3)} page/s")
|
| 32 |
-
|
| 33 |
-
output_start = time.time()
|
| 34 |
-
middle_json = result_to_middle_json(results, images_list, pdf_doc, image_writer)
|
| 35 |
-
output_time = round(time.time() - output_start, 2)
|
| 36 |
-
logger.debug(f"output json finished, cost: {output_time}, speed: {round(output_time / len(results), 3)} page/s")
|
| 37 |
-
return middle_json
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def doc_analyze(
|
| 41 |
-
pdf_bytes,
|
| 42 |
-
image_writer: DataWriter | None,
|
| 43 |
-
predictor = None,
|
| 44 |
-
):
|
| 45 |
-
if predictor is None:
|
| 46 |
-
predictor = _init_model()
|
| 47 |
-
|
| 48 |
-
load_images_start = time.time()
|
| 49 |
-
images_list, pdf_doc = load_images_from_pdf(pdf_bytes, image_type=ImageType.PIL, threads=CONFIG.PDF_TOOLS_WORKER_MAX_NUM)
|
| 50 |
-
images_pil_list = [image_dict["img_pil"] for image_dict in images_list]
|
| 51 |
-
load_images_time = round(time.time() - load_images_start, 2)
|
| 52 |
-
logger.debug(f"load images cost: {load_images_time}, speed: {round(load_images_time/len(images_pil_list), 3)} images/s")
|
| 53 |
-
|
| 54 |
-
infer_start = time.time()
|
| 55 |
-
results = predictor.batch_two_step_extract(images=images_pil_list)
|
| 56 |
-
infer_time = round(time.time() - infer_start, 2)
|
| 57 |
-
logger.debug(f"infer finished, cost: {infer_time}, speed: {round(len(results)/infer_time, 3)} page/s")
|
| 58 |
-
|
| 59 |
-
output_start = time.time()
|
| 60 |
-
middle_json = result_to_middle_json(results, images_list, pdf_doc, image_writer)
|
| 61 |
-
output_time = round(time.time() - output_start, 2)
|
| 62 |
-
logger.debug(f"output json finished, cost: {output_time}, speed: {round(output_time / len(results), 3)} page/s")
|
| 63 |
-
return middle_json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/src/vlm_magic_model.py
DELETED
|
@@ -1,561 +0,0 @@
|
|
| 1 |
-
import re
|
| 2 |
-
from typing import Literal
|
| 3 |
-
|
| 4 |
-
from loguru import logger
|
| 5 |
-
|
| 6 |
-
from NaviOCR.tools.boxbase import calculate_overlap_area_in_bbox1_area_ratio
|
| 7 |
-
from NaviOCR.tools.enum_class import ContentType, BlockType
|
| 8 |
-
from NaviOCR.tools.guess_suffix_or_lang import guess_language_by_text
|
| 9 |
-
from NaviOCR.tools.magic_model_utils import reduct_overlap, tie_up_category_by_index
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class MagicModel:
|
| 13 |
-
def __init__(self, page_blocks: list, width, height):
|
| 14 |
-
self.page_blocks = page_blocks
|
| 15 |
-
|
| 16 |
-
blocks = []
|
| 17 |
-
self.all_spans = []
|
| 18 |
-
for index, block_info in enumerate(page_blocks):
|
| 19 |
-
block_bbox = block_info["bbox"]
|
| 20 |
-
try:
|
| 21 |
-
if len(block_bbox) % 2 != 0:
|
| 22 |
-
logger.warning(f"Invalid bbox length: {block_bbox}")
|
| 23 |
-
else:
|
| 24 |
-
pts = []
|
| 25 |
-
for i in range(0, len(block_bbox), 2):
|
| 26 |
-
pts.append(int(block_bbox[i] * width))
|
| 27 |
-
pts.append(int(block_bbox[i + 1] * height))
|
| 28 |
-
block_bbox = tuple(pts)
|
| 29 |
-
block_type = block_info["type"]
|
| 30 |
-
block_content = block_info["content"]
|
| 31 |
-
block_angle = block_info["angle"]
|
| 32 |
-
except Exception as e:
|
| 33 |
-
logger.warning(f"Invalid block format: {block_info}, error: {e}")
|
| 34 |
-
continue
|
| 35 |
-
|
| 36 |
-
span_type = "unknown"
|
| 37 |
-
code_block_sub_type = None
|
| 38 |
-
guess_lang = None
|
| 39 |
-
|
| 40 |
-
# 确定类型
|
| 41 |
-
if block_type in ["list"]:
|
| 42 |
-
span_type = ContentType.TEXT
|
| 43 |
-
|
| 44 |
-
elif block_type in [
|
| 45 |
-
"text",
|
| 46 |
-
"title",
|
| 47 |
-
"image_caption",
|
| 48 |
-
"image_footnote",
|
| 49 |
-
"table_caption",
|
| 50 |
-
"table_footnote",
|
| 51 |
-
"code_caption",
|
| 52 |
-
"ref_text",
|
| 53 |
-
"phonetic",
|
| 54 |
-
"header",
|
| 55 |
-
"footer",
|
| 56 |
-
"page_number",
|
| 57 |
-
"aside_text",
|
| 58 |
-
"page_footnote",
|
| 59 |
-
]:
|
| 60 |
-
span_type = ContentType.TEXT
|
| 61 |
-
elif block_type in ["image"]:
|
| 62 |
-
block_type = BlockType.IMAGE_BODY
|
| 63 |
-
span_type = ContentType.IMAGE
|
| 64 |
-
elif block_type in ["table"]:
|
| 65 |
-
block_type = BlockType.TABLE_BODY
|
| 66 |
-
span_type = ContentType.TABLE
|
| 67 |
-
elif block_type in ["code", "algorithm"]:
|
| 68 |
-
guess_lang, block_content = guess_language_by_text(block_content)
|
| 69 |
-
code_block_sub_type = block_type
|
| 70 |
-
block_type = BlockType.CODE_BODY
|
| 71 |
-
span_type = ContentType.TEXT
|
| 72 |
-
elif block_type in ["equation"]:
|
| 73 |
-
block_type = BlockType.INTERLINE_EQUATION
|
| 74 |
-
span_type = ContentType.INTERLINE_EQUATION
|
| 75 |
-
elif block_type in ['seal']:
|
| 76 |
-
block_type = BlockType.SEAL
|
| 77 |
-
span_type = ContentType.SEAL
|
| 78 |
-
elif block_type in ['char']:
|
| 79 |
-
block_type = BlockType.CHAR
|
| 80 |
-
span_type = ContentType.CHAR
|
| 81 |
-
|
| 82 |
-
switch_code_to_algorithm = False
|
| 83 |
-
|
| 84 |
-
if span_type in [ContentType.IMAGE, ContentType.TABLE]:
|
| 85 |
-
span = {
|
| 86 |
-
"bbox": block_bbox,
|
| 87 |
-
"type": span_type,
|
| 88 |
-
}
|
| 89 |
-
if span_type == ContentType.TABLE:
|
| 90 |
-
span["html"] = block_content
|
| 91 |
-
|
| 92 |
-
elif span_type in [ContentType.INTERLINE_EQUATION]:
|
| 93 |
-
span = {
|
| 94 |
-
"bbox": block_bbox,
|
| 95 |
-
"type": span_type,
|
| 96 |
-
"content": block_content,
|
| 97 |
-
}
|
| 98 |
-
elif span_type in [ContentType.SEAL]:
|
| 99 |
-
span = {
|
| 100 |
-
"bbox": block_bbox,
|
| 101 |
-
"type": span_type,
|
| 102 |
-
"content": block_content
|
| 103 |
-
}
|
| 104 |
-
elif span_type in [ContentType.CHAR]:
|
| 105 |
-
span = {
|
| 106 |
-
"bbox": block_bbox,
|
| 107 |
-
"type": span_type,
|
| 108 |
-
"content": block_content
|
| 109 |
-
}
|
| 110 |
-
else:
|
| 111 |
-
if block_type == "title" and block_content:
|
| 112 |
-
block_content = re.sub(r"\n\s*", " ", block_content).strip()
|
| 113 |
-
span = {
|
| 114 |
-
"bbox": block_bbox,
|
| 115 |
-
"type": span_type,
|
| 116 |
-
"content": block_content,
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
if isinstance(span, dict) and "bbox" in span:
|
| 120 |
-
self.all_spans.append(span)
|
| 121 |
-
spans = [span]
|
| 122 |
-
elif isinstance(span, list):
|
| 123 |
-
self.all_spans.extend(span)
|
| 124 |
-
spans = span
|
| 125 |
-
else:
|
| 126 |
-
raise ValueError(f"Invalid span type: {span_type}, expected dict or list, got {type(span)}")
|
| 127 |
-
|
| 128 |
-
if block_type in [BlockType.CODE_BODY]:
|
| 129 |
-
if switch_code_to_algorithm and code_block_sub_type == "code":
|
| 130 |
-
code_block_sub_type = "algorithm"
|
| 131 |
-
line = {"bbox": block_bbox, "spans": spans, "extra": {"type": code_block_sub_type, "guess_lang": guess_lang}}
|
| 132 |
-
else:
|
| 133 |
-
line = {"bbox": block_bbox, "spans": spans}
|
| 134 |
-
|
| 135 |
-
blocks.append(
|
| 136 |
-
{
|
| 137 |
-
"bbox": block_bbox,
|
| 138 |
-
"type": block_type,
|
| 139 |
-
"angle": block_angle,
|
| 140 |
-
"lines": [line],
|
| 141 |
-
"index": index,
|
| 142 |
-
}
|
| 143 |
-
)
|
| 144 |
-
|
| 145 |
-
# 收集各式的blocks
|
| 146 |
-
self.image_blocks = []
|
| 147 |
-
self.table_blocks = []
|
| 148 |
-
self.interline_equation_blocks = []
|
| 149 |
-
self.text_blocks = []
|
| 150 |
-
self.title_blocks = []
|
| 151 |
-
self.code_blocks = []
|
| 152 |
-
self.discarded_blocks = []
|
| 153 |
-
self.ref_text_blocks = []
|
| 154 |
-
self.phonetic_blocks = []
|
| 155 |
-
self.list_blocks = []
|
| 156 |
-
self.seal_blocks = []
|
| 157 |
-
self.char_blocks = []
|
| 158 |
-
for block in blocks:
|
| 159 |
-
if block["type"] in [BlockType.IMAGE_BODY, BlockType.IMAGE_CAPTION, BlockType.IMAGE_FOOTNOTE]:
|
| 160 |
-
self.image_blocks.append(block)
|
| 161 |
-
elif block["type"] in [BlockType.TABLE_BODY, BlockType.TABLE_CAPTION, BlockType.TABLE_FOOTNOTE]:
|
| 162 |
-
self.table_blocks.append(block)
|
| 163 |
-
elif block["type"] in [BlockType.CODE_BODY, BlockType.CODE_CAPTION]:
|
| 164 |
-
self.code_blocks.append(block)
|
| 165 |
-
elif block["type"] == BlockType.INTERLINE_EQUATION:
|
| 166 |
-
self.interline_equation_blocks.append(block)
|
| 167 |
-
elif block["type"] == BlockType.TEXT:
|
| 168 |
-
self.text_blocks.append(block)
|
| 169 |
-
elif block["type"] == BlockType.TITLE:
|
| 170 |
-
self.title_blocks.append(block)
|
| 171 |
-
elif block["type"] in [BlockType.REF_TEXT]:
|
| 172 |
-
self.ref_text_blocks.append(block)
|
| 173 |
-
elif block["type"] in [BlockType.PHONETIC]:
|
| 174 |
-
self.phonetic_blocks.append(block)
|
| 175 |
-
elif block["type"] in [BlockType.HEADER, BlockType.FOOTER, BlockType.PAGE_NUMBER, BlockType.ASIDE_TEXT, BlockType.PAGE_FOOTNOTE]:
|
| 176 |
-
self.discarded_blocks.append(block)
|
| 177 |
-
elif block["type"] == BlockType.LIST:
|
| 178 |
-
self.list_blocks.append(block)
|
| 179 |
-
elif block["type"] == BlockType.SEAL:
|
| 180 |
-
self.seal_blocks.append(block)
|
| 181 |
-
elif block["type"] == BlockType.CHAR:
|
| 182 |
-
self.char_blocks.append(block)
|
| 183 |
-
else:
|
| 184 |
-
continue
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
self.list_blocks, self.text_blocks, self.ref_text_blocks = fix_list_blocks(self.list_blocks, self.text_blocks, self.ref_text_blocks)
|
| 188 |
-
self.image_blocks, not_include_image_blocks = fix_two_layer_blocks(self.image_blocks, BlockType.IMAGE)
|
| 189 |
-
self.table_blocks, not_include_table_blocks = fix_two_layer_blocks(self.table_blocks, BlockType.TABLE)
|
| 190 |
-
self.code_blocks, not_include_code_blocks = fix_two_layer_blocks(self.code_blocks, BlockType.CODE)
|
| 191 |
-
for code_block in self.code_blocks:
|
| 192 |
-
for block in code_block['blocks']:
|
| 193 |
-
if block['type'] == BlockType.CODE_BODY:
|
| 194 |
-
if len(block["lines"]) > 0:
|
| 195 |
-
line = block["lines"][0]
|
| 196 |
-
code_block["sub_type"] = line["extra"]["type"]
|
| 197 |
-
if code_block["sub_type"] in ["code"]:
|
| 198 |
-
code_block["guess_lang"] = line["extra"]["guess_lang"]
|
| 199 |
-
del line["extra"]
|
| 200 |
-
else:
|
| 201 |
-
code_block["sub_type"] = "code"
|
| 202 |
-
code_block["guess_lang"] = "txt"
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
for block in not_include_image_blocks + not_include_table_blocks + not_include_code_blocks:
|
| 206 |
-
block["type"] = BlockType.TEXT
|
| 207 |
-
self.text_blocks.append(block)
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
def get_list_blocks(self):
|
| 211 |
-
|
| 212 |
-
return self.list_blocks
|
| 213 |
-
|
| 214 |
-
def get_image_blocks(self):
|
| 215 |
-
|
| 216 |
-
return self.image_blocks
|
| 217 |
-
|
| 218 |
-
def get_table_blocks(self):
|
| 219 |
-
|
| 220 |
-
return self.table_blocks
|
| 221 |
-
|
| 222 |
-
def get_code_blocks(self):
|
| 223 |
-
|
| 224 |
-
return self.code_blocks
|
| 225 |
-
|
| 226 |
-
def get_ref_text_blocks(self):
|
| 227 |
-
|
| 228 |
-
return self.ref_text_blocks
|
| 229 |
-
|
| 230 |
-
def get_phonetic_blocks(self):
|
| 231 |
-
|
| 232 |
-
return self.phonetic_blocks
|
| 233 |
-
|
| 234 |
-
def get_title_blocks(self):
|
| 235 |
-
|
| 236 |
-
return self.title_blocks
|
| 237 |
-
|
| 238 |
-
def get_text_blocks(self):
|
| 239 |
-
|
| 240 |
-
return self.text_blocks
|
| 241 |
-
|
| 242 |
-
def get_interline_equation_blocks(self):
|
| 243 |
-
|
| 244 |
-
return self.interline_equation_blocks
|
| 245 |
-
|
| 246 |
-
def get_discarded_blocks(self):
|
| 247 |
-
|
| 248 |
-
return self.discarded_blocks
|
| 249 |
-
|
| 250 |
-
def get_all_spans(self):
|
| 251 |
-
return self.all_spans
|
| 252 |
-
|
| 253 |
-
def get_content_blocks(self, content_blocks):
|
| 254 |
-
new_content_blocks = []
|
| 255 |
-
for inter_block in content_blocks:
|
| 256 |
-
contents = extract_contents(inter_block.get('lines', []), 'content')
|
| 257 |
-
if len(contents) > 0:
|
| 258 |
-
new_content_blocks.append({
|
| 259 |
-
"type":inter_block.get("type", "text"),
|
| 260 |
-
"angle":inter_block.get("angle", 0),
|
| 261 |
-
"index":inter_block.get('index', -1),
|
| 262 |
-
"pos":inter_block.get('bbox', []),
|
| 263 |
-
"text":contents[0],
|
| 264 |
-
})
|
| 265 |
-
return new_content_blocks
|
| 266 |
-
|
| 267 |
-
def get_seal_blocks(self):
|
| 268 |
-
|
| 269 |
-
return self.seal_blocks
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
def get_char_blocks(self):
|
| 273 |
-
|
| 274 |
-
return self.char_blocks
|
| 275 |
-
|
| 276 |
-
def get_page_blocks(self, pass_blocks=False):
|
| 277 |
-
image_blocks = self.get_image_blocks()
|
| 278 |
-
table_blocks = self.get_table_blocks()
|
| 279 |
-
title_blocks = self.get_title_blocks()
|
| 280 |
-
discarded_blocks = self.get_discarded_blocks()
|
| 281 |
-
code_blocks = self.get_code_blocks()
|
| 282 |
-
ref_text_blocks = self.get_ref_text_blocks()
|
| 283 |
-
phonetic_blocks = self.get_phonetic_blocks()
|
| 284 |
-
list_blocks = self.get_list_blocks()
|
| 285 |
-
text_blocks = self.get_text_blocks()
|
| 286 |
-
interline_equation_blocks = self.get_interline_equation_blocks()
|
| 287 |
-
seal_blocks = self.get_seal_blocks()
|
| 288 |
-
char_blocks = self.get_char_blocks()
|
| 289 |
-
|
| 290 |
-
if pass_blocks:
|
| 291 |
-
page_blocks = [
|
| 292 |
-
*table_blocks,
|
| 293 |
-
*code_blocks,
|
| 294 |
-
*ref_text_blocks,
|
| 295 |
-
*phonetic_blocks,
|
| 296 |
-
*title_blocks,
|
| 297 |
-
*text_blocks,
|
| 298 |
-
*interline_equation_blocks,
|
| 299 |
-
*discarded_blocks,
|
| 300 |
-
]
|
| 301 |
-
else:
|
| 302 |
-
page_blocks = [
|
| 303 |
-
*image_blocks,
|
| 304 |
-
*table_blocks,
|
| 305 |
-
*code_blocks,
|
| 306 |
-
*ref_text_blocks,
|
| 307 |
-
*phonetic_blocks,
|
| 308 |
-
*title_blocks,
|
| 309 |
-
*text_blocks,
|
| 310 |
-
*interline_equation_blocks,
|
| 311 |
-
*list_blocks,
|
| 312 |
-
*discarded_blocks,
|
| 313 |
-
*seal_blocks,
|
| 314 |
-
*char_blocks,
|
| 315 |
-
]
|
| 316 |
-
page_blocks.sort(key=lambda x: x["index"])
|
| 317 |
-
for i, block in enumerate(page_blocks):
|
| 318 |
-
block["index"] = i
|
| 319 |
-
return page_blocks
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
def extract_contents(data, key):
|
| 325 |
-
image_paths = []
|
| 326 |
-
def _walk(obj):
|
| 327 |
-
if isinstance(obj, dict):
|
| 328 |
-
if key in obj:
|
| 329 |
-
image_paths.append(obj[key])
|
| 330 |
-
for v in obj.values():
|
| 331 |
-
_walk(v)
|
| 332 |
-
elif isinstance(obj, list):
|
| 333 |
-
for item in obj:
|
| 334 |
-
_walk(item)
|
| 335 |
-
_walk(data)
|
| 336 |
-
return image_paths
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
def __tie_up_category_by_index(blocks, subject_block_type, object_block_type):
|
| 340 |
-
"""基于index的主客体关联包装函数"""
|
| 341 |
-
# 定义获取主体和客体对象的函数
|
| 342 |
-
def get_subjects():
|
| 343 |
-
return reduct_overlap(
|
| 344 |
-
list(
|
| 345 |
-
map(
|
| 346 |
-
lambda x: {"bbox": x["bbox"], "lines": x["lines"], "index": x["index"], "angle": x["angle"]},
|
| 347 |
-
filter(
|
| 348 |
-
lambda x: x["type"] == subject_block_type,
|
| 349 |
-
blocks,
|
| 350 |
-
),
|
| 351 |
-
)
|
| 352 |
-
)
|
| 353 |
-
)
|
| 354 |
-
|
| 355 |
-
def get_objects():
|
| 356 |
-
return reduct_overlap(
|
| 357 |
-
list(
|
| 358 |
-
map(
|
| 359 |
-
lambda x: {"bbox": x["bbox"], "lines": x["lines"], "index": x["index"], "angle": x["angle"]},
|
| 360 |
-
filter(
|
| 361 |
-
lambda x: x["type"] == object_block_type,
|
| 362 |
-
blocks,
|
| 363 |
-
),
|
| 364 |
-
)
|
| 365 |
-
)
|
| 366 |
-
)
|
| 367 |
-
|
| 368 |
-
# 调用通用方法
|
| 369 |
-
return tie_up_category_by_index(
|
| 370 |
-
get_subjects,
|
| 371 |
-
get_objects
|
| 372 |
-
)
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
def get_type_blocks(blocks, block_type: Literal["image", "table", "code"]):
|
| 376 |
-
with_captions = __tie_up_category_by_index(blocks, f"{block_type}_body", f"{block_type}_caption")
|
| 377 |
-
with_footnotes = __tie_up_category_by_index(blocks, f"{block_type}_body", f"{block_type}_footnote")
|
| 378 |
-
ret = []
|
| 379 |
-
for v in with_captions:
|
| 380 |
-
record = {
|
| 381 |
-
f"{block_type}_body": v["sub_bbox"],
|
| 382 |
-
f"{block_type}_caption_list": v["obj_bboxes"],
|
| 383 |
-
}
|
| 384 |
-
filter_idx = v["sub_idx"]
|
| 385 |
-
d = next(filter(lambda x: x["sub_idx"] == filter_idx, with_footnotes))
|
| 386 |
-
record[f"{block_type}_footnote_list"] = d["obj_bboxes"]
|
| 387 |
-
ret.append(record)
|
| 388 |
-
return ret
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
def fix_two_layer_blocks(blocks, fix_type: Literal["image", "table", "code"]):
|
| 392 |
-
need_fix_blocks = get_type_blocks(blocks, fix_type)
|
| 393 |
-
fixed_blocks = []
|
| 394 |
-
not_include_blocks = []
|
| 395 |
-
processed_indices = set()
|
| 396 |
-
|
| 397 |
-
# 特殊处理表格类型,确保标题在表格前,注脚在表格后
|
| 398 |
-
if fix_type in ["table", "image"]:
|
| 399 |
-
# 收集所有不合适的caption和footnote
|
| 400 |
-
misplaced_captions = [] # 存储(caption, 原始block索引)
|
| 401 |
-
misplaced_footnotes = [] # 存储(footnote, 原始block索引)
|
| 402 |
-
|
| 403 |
-
# 第一步:移除不符合位置要求的footnote
|
| 404 |
-
for block_idx, block in enumerate(need_fix_blocks):
|
| 405 |
-
body = block[f"{fix_type}_body"]
|
| 406 |
-
body_index = body["index"]
|
| 407 |
-
|
| 408 |
-
# 检查footnote应在body后或同位置
|
| 409 |
-
valid_footnotes = []
|
| 410 |
-
for footnote in block[f"{fix_type}_footnote_list"]:
|
| 411 |
-
if footnote["index"] >= body_index:
|
| 412 |
-
valid_footnotes.append(footnote)
|
| 413 |
-
else:
|
| 414 |
-
misplaced_footnotes.append((footnote, block_idx))
|
| 415 |
-
block[f"{fix_type}_footnote_list"] = valid_footnotes
|
| 416 |
-
|
| 417 |
-
# 第三步:重新分配不合规的footnote到合适的body
|
| 418 |
-
for footnote, original_block_idx in misplaced_footnotes:
|
| 419 |
-
footnote_index = footnote["index"]
|
| 420 |
-
best_block_idx = None
|
| 421 |
-
min_distance = float('inf')
|
| 422 |
-
|
| 423 |
-
# 寻找索引小于等于footnote_index的最近body
|
| 424 |
-
for idx, block in enumerate(need_fix_blocks):
|
| 425 |
-
body_index = block[f"{fix_type}_body"]["index"]
|
| 426 |
-
if body_index <= footnote_index and idx != original_block_idx:
|
| 427 |
-
distance = footnote_index - body_index
|
| 428 |
-
if distance < min_distance:
|
| 429 |
-
min_distance = distance
|
| 430 |
-
best_block_idx = idx
|
| 431 |
-
|
| 432 |
-
if best_block_idx is not None:
|
| 433 |
-
# 找到合适的body,添加到对应block的footnote_list
|
| 434 |
-
need_fix_blocks[best_block_idx][f"{fix_type}_footnote_list"].append(footnote)
|
| 435 |
-
else:
|
| 436 |
-
# 没找到合适的body,作为普通block处理
|
| 437 |
-
not_include_blocks.append(footnote)
|
| 438 |
-
|
| 439 |
-
# 第四步:将每个block的caption_list和footnote_list中不连续index的元素提出来作为普通block处理
|
| 440 |
-
for block in need_fix_blocks:
|
| 441 |
-
caption_list = block[f"{fix_type}_caption_list"]
|
| 442 |
-
footnote_list = block[f"{fix_type}_footnote_list"]
|
| 443 |
-
body_index = block[f"{fix_type}_body"]["index"]
|
| 444 |
-
|
| 445 |
-
# 处理caption_list (从body往前看,caption在body之前)
|
| 446 |
-
if caption_list:
|
| 447 |
-
# 按index降序排列,从最接近body的开始检查
|
| 448 |
-
caption_list.sort(key=lambda x: x["index"], reverse=True)
|
| 449 |
-
filtered_captions = [caption_list[0]]
|
| 450 |
-
for i in range(1, len(caption_list)):
|
| 451 |
-
prev_index = caption_list[i - 1]["index"]
|
| 452 |
-
curr_index = caption_list[i]["index"]
|
| 453 |
-
|
| 454 |
-
# 检查是否连续
|
| 455 |
-
if curr_index == prev_index - 1:
|
| 456 |
-
filtered_captions.append(caption_list[i])
|
| 457 |
-
else:
|
| 458 |
-
# 检查gap中是否只有body_index
|
| 459 |
-
gap_indices = set(range(curr_index + 1, prev_index))
|
| 460 |
-
if gap_indices == {body_index}:
|
| 461 |
-
# gap中只有body_index,不算真正的gap
|
| 462 |
-
filtered_captions.append(caption_list[i])
|
| 463 |
-
else:
|
| 464 |
-
# 出现真正的gap,后续所有caption都作为普通block
|
| 465 |
-
not_include_blocks.extend(caption_list[i:])
|
| 466 |
-
break
|
| 467 |
-
# 恢复升序
|
| 468 |
-
filtered_captions.reverse()
|
| 469 |
-
block[f"{fix_type}_caption_list"] = filtered_captions
|
| 470 |
-
|
| 471 |
-
# 处理footnote_list (从body往后看,footnote在body之后)
|
| 472 |
-
if footnote_list:
|
| 473 |
-
# 按index升序排列,从最接近body的开始检查
|
| 474 |
-
footnote_list.sort(key=lambda x: x["index"])
|
| 475 |
-
filtered_footnotes = [footnote_list[0]]
|
| 476 |
-
for i in range(1, len(footnote_list)):
|
| 477 |
-
# 检查是否与前一个footnote连续
|
| 478 |
-
if footnote_list[i]["index"] == footnote_list[i - 1]["index"] + 1:
|
| 479 |
-
filtered_footnotes.append(footnote_list[i])
|
| 480 |
-
else:
|
| 481 |
-
# 出现gap,后续所有footnote都作为普通block
|
| 482 |
-
not_include_blocks.extend(footnote_list[i:])
|
| 483 |
-
break
|
| 484 |
-
block[f"{fix_type}_footnote_list"] = filtered_footnotes
|
| 485 |
-
|
| 486 |
-
# 构建两层结构blocks
|
| 487 |
-
for block in need_fix_blocks:
|
| 488 |
-
body = block[f"{fix_type}_body"]
|
| 489 |
-
caption_list = block[f"{fix_type}_caption_list"]
|
| 490 |
-
footnote_list = block[f"{fix_type}_footnote_list"]
|
| 491 |
-
|
| 492 |
-
body["type"] = f"{fix_type}_body"
|
| 493 |
-
for caption in caption_list:
|
| 494 |
-
caption["type"] = f"{fix_type}_caption"
|
| 495 |
-
processed_indices.add(caption["index"])
|
| 496 |
-
for footnote in footnote_list:
|
| 497 |
-
footnote["type"] = f"{fix_type}_footnote"
|
| 498 |
-
processed_indices.add(footnote["index"])
|
| 499 |
-
|
| 500 |
-
processed_indices.add(body["index"])
|
| 501 |
-
|
| 502 |
-
two_layer_block = {
|
| 503 |
-
"type": fix_type,
|
| 504 |
-
"bbox": body["bbox"],
|
| 505 |
-
"blocks": [body],
|
| 506 |
-
"index": body["index"],
|
| 507 |
-
}
|
| 508 |
-
two_layer_block["blocks"].extend([*caption_list, *footnote_list])
|
| 509 |
-
# 对blocks按index排序
|
| 510 |
-
two_layer_block["blocks"].sort(key=lambda x: x["index"])
|
| 511 |
-
|
| 512 |
-
fixed_blocks.append(two_layer_block)
|
| 513 |
-
|
| 514 |
-
# 添加未处理的blocks
|
| 515 |
-
for block in blocks:
|
| 516 |
-
block.pop("type", None)
|
| 517 |
-
if block["index"] not in processed_indices and block not in not_include_blocks:
|
| 518 |
-
not_include_blocks.append(block)
|
| 519 |
-
|
| 520 |
-
return fixed_blocks, not_include_blocks
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
def fix_list_blocks(list_blocks, text_blocks, ref_text_blocks):
|
| 524 |
-
for list_block in list_blocks:
|
| 525 |
-
list_block["blocks"] = []
|
| 526 |
-
if "lines" in list_block:
|
| 527 |
-
del list_block["lines"]
|
| 528 |
-
|
| 529 |
-
temp_text_blocks = text_blocks + ref_text_blocks
|
| 530 |
-
need_remove_blocks = []
|
| 531 |
-
for block in temp_text_blocks:
|
| 532 |
-
for list_block in list_blocks:
|
| 533 |
-
if calculate_overlap_area_in_bbox1_area_ratio(block["bbox"], list_block["bbox"]) >= 0.8:
|
| 534 |
-
list_block["blocks"].append(block)
|
| 535 |
-
need_remove_blocks.append(block)
|
| 536 |
-
break
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
for block in need_remove_blocks:
|
| 540 |
-
if block in text_blocks:
|
| 541 |
-
text_blocks.remove(block)
|
| 542 |
-
elif block in ref_text_blocks:
|
| 543 |
-
ref_text_blocks.remove(block)
|
| 544 |
-
|
| 545 |
-
# 移除blocks为空的list_block
|
| 546 |
-
list_blocks = [lb for lb in list_blocks if lb["blocks"]]
|
| 547 |
-
|
| 548 |
-
for list_block in list_blocks:
|
| 549 |
-
# 统计list_block["blocks"]中所有block的type,用众数作为list_block的sub_type
|
| 550 |
-
type_count = {}
|
| 551 |
-
for sub_block in list_block["blocks"]:
|
| 552 |
-
sub_block_type = sub_block["type"]
|
| 553 |
-
if sub_block_type not in type_count:
|
| 554 |
-
type_count[sub_block_type] = 0
|
| 555 |
-
type_count[sub_block_type] += 1
|
| 556 |
-
|
| 557 |
-
if type_count:
|
| 558 |
-
list_block["sub_type"] = max(type_count, key=type_count.get)
|
| 559 |
-
else:
|
| 560 |
-
list_block["sub_type"] = "unknown"
|
| 561 |
-
return list_blocks, text_blocks, ref_text_blocks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/src/vlm_middle_json_mkcontent.py
DELETED
|
@@ -1,176 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
-
from loguru import logger
|
| 4 |
-
|
| 5 |
-
from NaviOCR.tools.char_utils import full_to_half_exclude_marks, is_hyphen_at_line_end
|
| 6 |
-
from NaviOCR.tools.config_reader import get_latex_delimiter_config
|
| 7 |
-
from NaviOCR.tools.enum_class import BlockType, ContentType
|
| 8 |
-
from NaviOCR.tools.language import detect_lang
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def merge_para_with_text(para_block):
|
| 12 |
-
block_text = ''
|
| 13 |
-
for line in para_block['lines']:
|
| 14 |
-
for span in line['spans']:
|
| 15 |
-
if span['type'] in [ContentType.TEXT]:
|
| 16 |
-
try:
|
| 17 |
-
span['content'] = full_to_half_exclude_marks(span['content'])
|
| 18 |
-
except:
|
| 19 |
-
continue
|
| 20 |
-
block_text += span['content']
|
| 21 |
-
block_lang = detect_lang(block_text)
|
| 22 |
-
|
| 23 |
-
para_text = ''
|
| 24 |
-
for i, line in enumerate(para_block['lines']):
|
| 25 |
-
for j, span in enumerate(line['spans']):
|
| 26 |
-
span_type = span['type']
|
| 27 |
-
content = ''
|
| 28 |
-
if span_type == ContentType.TEXT:
|
| 29 |
-
content = span['content']
|
| 30 |
-
elif span_type == ContentType.INLINE_EQUATION:
|
| 31 |
-
content = span['content']
|
| 32 |
-
elif span_type == ContentType.INTERLINE_EQUATION:
|
| 33 |
-
content = span['content']
|
| 34 |
-
try:
|
| 35 |
-
content = content.strip()
|
| 36 |
-
except:
|
| 37 |
-
continue
|
| 38 |
-
if content:
|
| 39 |
-
if span_type == ContentType.INTERLINE_EQUATION:
|
| 40 |
-
para_text += content
|
| 41 |
-
continue
|
| 42 |
-
cjk_langs = {'zh', 'ja', 'ko'}
|
| 43 |
-
is_last_span = j == len(line['spans']) - 1
|
| 44 |
-
|
| 45 |
-
if block_lang in cjk_langs:
|
| 46 |
-
if is_last_span and span_type != ContentType.INLINE_EQUATION:
|
| 47 |
-
para_text += content
|
| 48 |
-
else:
|
| 49 |
-
para_text += f'{content} '
|
| 50 |
-
else:
|
| 51 |
-
if span_type in [ContentType.TEXT, ContentType.INLINE_EQUATION]:
|
| 52 |
-
if (
|
| 53 |
-
is_last_span
|
| 54 |
-
and span_type == ContentType.TEXT
|
| 55 |
-
and is_hyphen_at_line_end(content)
|
| 56 |
-
):
|
| 57 |
-
if (
|
| 58 |
-
i+1 < len(para_block['lines'])
|
| 59 |
-
and para_block['lines'][i + 1].get('spans')
|
| 60 |
-
and para_block['lines'][i + 1]['spans'][0].get('type') == ContentType.TEXT
|
| 61 |
-
and para_block['lines'][i + 1]['spans'][0].get('content', '')
|
| 62 |
-
and para_block['lines'][i + 1]['spans'][0]['content'][0].islower()
|
| 63 |
-
):
|
| 64 |
-
para_text += content[:-1]
|
| 65 |
-
else:
|
| 66 |
-
para_text += content
|
| 67 |
-
else:
|
| 68 |
-
para_text += f'{content} '
|
| 69 |
-
return para_text
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def mk_blocks_to_markdown(para_blocks, img_buket_path=''):
|
| 73 |
-
page_markdown = []
|
| 74 |
-
for para_block in para_blocks:
|
| 75 |
-
para_text = ''
|
| 76 |
-
para_type = para_block['type']
|
| 77 |
-
if para_type in [BlockType.TEXT, BlockType.INTERLINE_EQUATION, BlockType.PHONETIC, BlockType.REF_TEXT, BlockType.FOOTER, BlockType.HEADER]:
|
| 78 |
-
para_text = merge_para_with_text(para_block)
|
| 79 |
-
elif para_type == BlockType.LIST:
|
| 80 |
-
for block in para_block['blocks']:
|
| 81 |
-
item_text = merge_para_with_text(block)
|
| 82 |
-
para_text += f"{item_text} \n"
|
| 83 |
-
elif para_type == BlockType.TITLE:
|
| 84 |
-
title_level = get_title_level(para_block)
|
| 85 |
-
para_text = f'{"#" * title_level} {merge_para_with_text(para_block)}'
|
| 86 |
-
elif para_type == BlockType.IMAGE:
|
| 87 |
-
has_image_footnote = any(block['type'] == BlockType.IMAGE_FOOTNOTE for block in para_block['blocks'])
|
| 88 |
-
if has_image_footnote:
|
| 89 |
-
for block in para_block['blocks']: # 1st.拼image_caption
|
| 90 |
-
if block['type'] == BlockType.IMAGE_CAPTION:
|
| 91 |
-
para_text += merge_para_with_text(block) + ' \n'
|
| 92 |
-
for block in para_block['blocks']: # 2nd.拼image_body
|
| 93 |
-
if block['type'] == BlockType.IMAGE_BODY:
|
| 94 |
-
for line in block['lines']:
|
| 95 |
-
for span in line['spans']:
|
| 96 |
-
if span['type'] == ContentType.IMAGE:
|
| 97 |
-
if span.get('image_path', ''):
|
| 98 |
-
para_text += f""
|
| 99 |
-
for block in para_block['blocks']: # 3rd.拼image_footnote
|
| 100 |
-
if block['type'] == BlockType.IMAGE_FOOTNOTE:
|
| 101 |
-
para_text += ' \n' + merge_para_with_text(block)
|
| 102 |
-
else:
|
| 103 |
-
for block in para_block['blocks']:
|
| 104 |
-
if block['type'] == BlockType.IMAGE_BODY:
|
| 105 |
-
for line in block['lines']:
|
| 106 |
-
for span in line['spans']:
|
| 107 |
-
if span['type'] == ContentType.IMAGE:
|
| 108 |
-
if span.get('image_path', ''):
|
| 109 |
-
para_text += f""
|
| 110 |
-
for block in para_block['blocks']:
|
| 111 |
-
if block['type'] == BlockType.IMAGE_CAPTION:
|
| 112 |
-
para_text += ' \n' + merge_para_with_text(block)
|
| 113 |
-
elif para_type == BlockType.TABLE:
|
| 114 |
-
for block in para_block['blocks']:
|
| 115 |
-
if block['type'] == BlockType.TABLE_CAPTION:
|
| 116 |
-
para_text += merge_para_with_text(block) + ' \n'
|
| 117 |
-
for block in para_block['blocks']:
|
| 118 |
-
if block['type'] == BlockType.TABLE_BODY:
|
| 119 |
-
for line in block['lines']:
|
| 120 |
-
for span in line['spans']:
|
| 121 |
-
if span['type'] == ContentType.TABLE:
|
| 122 |
-
if span.get('html', ''):
|
| 123 |
-
para_text += f"\n{span['html']}\n"
|
| 124 |
-
elif span.get('image_path', ''):
|
| 125 |
-
para_text += f""
|
| 126 |
-
for block in para_block['blocks']:
|
| 127 |
-
if block['type'] == BlockType.TABLE_FOOTNOTE:
|
| 128 |
-
para_text += '\n' + merge_para_with_text(block) + ' '
|
| 129 |
-
elif para_type == BlockType.CODE:
|
| 130 |
-
sub_type = para_block["sub_type"]
|
| 131 |
-
for block in para_block['blocks']:
|
| 132 |
-
if block['type'] == BlockType.CODE_CAPTION:
|
| 133 |
-
para_text += merge_para_with_text(block) + ' \n'
|
| 134 |
-
for block in para_block['blocks']:
|
| 135 |
-
if block['type'] == BlockType.CODE_BODY:
|
| 136 |
-
if sub_type == BlockType.CODE:
|
| 137 |
-
guess_lang = para_block["guess_lang"]
|
| 138 |
-
para_text += f"```{guess_lang}\n{merge_para_with_text(block)}\n```"
|
| 139 |
-
elif sub_type == BlockType.ALGORITHM:
|
| 140 |
-
para_text += merge_para_with_text(block)
|
| 141 |
-
elif para_type == BlockType.CHAR:
|
| 142 |
-
for line in para_block['lines']:
|
| 143 |
-
spans = line['spans']
|
| 144 |
-
for span in spans:
|
| 145 |
-
content = span['content']
|
| 146 |
-
if len(content) <= 5:
|
| 147 |
-
continue
|
| 148 |
-
else:
|
| 149 |
-
para_text += f"\n{content}\n"
|
| 150 |
-
if para_text.strip() == '':
|
| 151 |
-
continue
|
| 152 |
-
else:
|
| 153 |
-
page_markdown.append(para_text.strip())
|
| 154 |
-
|
| 155 |
-
return page_markdown
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
def union_make(pdf_info_dict: list,img_buket_path: str = '', ):
|
| 159 |
-
output_content = []
|
| 160 |
-
for page_info in pdf_info_dict:
|
| 161 |
-
paras_of_layout = page_info.get('para_blocks')
|
| 162 |
-
|
| 163 |
-
if not paras_of_layout:
|
| 164 |
-
continue
|
| 165 |
-
page_markdown = mk_blocks_to_markdown(paras_of_layout, img_buket_path)
|
| 166 |
-
output_content.extend(page_markdown)
|
| 167 |
-
|
| 168 |
-
return '\n\n'.join(output_content)
|
| 169 |
-
|
| 170 |
-
def get_title_level(block):
|
| 171 |
-
title_level = block.get('level', 1)
|
| 172 |
-
if title_level > 4:
|
| 173 |
-
title_level = 4
|
| 174 |
-
elif title_level < 1:
|
| 175 |
-
title_level = 0
|
| 176 |
-
return title_level
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/boxbase.py
DELETED
|
@@ -1,224 +0,0 @@
|
|
| 1 |
-
import math
|
| 2 |
-
|
| 3 |
-
def normalize_bbox(bbox):
|
| 4 |
-
if isinstance(bbox[0], (int, float)) and len(bbox) == 4:
|
| 5 |
-
x0, y0, x1, y1 = bbox
|
| 6 |
-
return x0, y0, x1, y1
|
| 7 |
-
if isinstance(bbox[0], (int, float)) and len(bbox) % 2 == 0:
|
| 8 |
-
xs = bbox[0::2]
|
| 9 |
-
ys = bbox[1::2]
|
| 10 |
-
return min(xs), min(ys), max(xs), max(ys)
|
| 11 |
-
xs = [p[0] for p in bbox]
|
| 12 |
-
ys = [p[1] for p in bbox]
|
| 13 |
-
return min(xs), min(ys), max(xs), max(ys)
|
| 14 |
-
|
| 15 |
-
def is_in(box1, box2) -> bool:
|
| 16 |
-
x0_1, y0_1, x1_1, y1_1 = normalize_bbox(box1)
|
| 17 |
-
x0_2, y0_2, x1_2, y1_2 = normalize_bbox(box2)
|
| 18 |
-
return (
|
| 19 |
-
x0_1 >= x0_2
|
| 20 |
-
and y0_1 >= y0_2
|
| 21 |
-
and x1_1 <= x1_2
|
| 22 |
-
and y1_1 <= y1_2
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
def bbox_relative_pos(bbox1, bbox2):
|
| 26 |
-
"""判断两个矩形框的相对位置关系.
|
| 27 |
-
|
| 28 |
-
Args:
|
| 29 |
-
bbox1: 一个四元组,表示第一个矩形框的左上角和右下角的坐标,格式为(x1, y1, x1b, y1b)
|
| 30 |
-
bbox2: 一个四元组,表示第二个矩形框的左上角和右下角的坐标,格式为(x2, y2, x2b, y2b)
|
| 31 |
-
|
| 32 |
-
Returns:
|
| 33 |
-
一个四元组,表示矩形框1相对于矩形框2的位置关系,格式为(left, right, bottom, top)
|
| 34 |
-
其中,left表示矩形框1是否在矩形框2的左侧,right表示矩形框1是否在矩形框2的右侧,
|
| 35 |
-
bottom表示矩形框1是否在矩形框2的下方,top表示矩形框1是否在矩形框2的上方
|
| 36 |
-
"""
|
| 37 |
-
x1, y1, x1b, y1b = bbox1
|
| 38 |
-
x2, y2, x2b, y2b = bbox2
|
| 39 |
-
|
| 40 |
-
left = x2b < x1
|
| 41 |
-
right = x1b < x2
|
| 42 |
-
bottom = y2b < y1
|
| 43 |
-
top = y1b < y2
|
| 44 |
-
return left, right, bottom, top
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def bbox_distance(bbox1, bbox2):
|
| 48 |
-
"""计算两个矩形框的距离。
|
| 49 |
-
Args:
|
| 50 |
-
bbox1 (tuple): 第一个矩形框的坐标,格式为 (x1, y1, x2, y2),其中 (x1, y1) 为左上角坐标,(x2, y2) 为右下角坐标。
|
| 51 |
-
bbox2 (tuple): 第二个矩形框的坐标,格式为 (x1, y1, x2, y2),其中 (x1, y1) 为左上角坐标,(x2, y2) 为右下角坐标。
|
| 52 |
-
Returns:
|
| 53 |
-
float: 矩形框之间的距离。
|
| 54 |
-
"""
|
| 55 |
-
|
| 56 |
-
def dist(point1, point2):
|
| 57 |
-
return math.sqrt((point1[0] - point2[0]) ** 2 + (point1[1] - point2[1]) ** 2)
|
| 58 |
-
|
| 59 |
-
x1, y1, x1b, y1b = bbox1
|
| 60 |
-
x2, y2, x2b, y2b = bbox2
|
| 61 |
-
|
| 62 |
-
left, right, bottom, top = bbox_relative_pos(bbox1, bbox2)
|
| 63 |
-
|
| 64 |
-
if top and left:
|
| 65 |
-
return dist((x1, y1b), (x2b, y2))
|
| 66 |
-
elif left and bottom:
|
| 67 |
-
return dist((x1, y1), (x2b, y2b))
|
| 68 |
-
elif bottom and right:
|
| 69 |
-
return dist((x1b, y1), (x2, y2b))
|
| 70 |
-
elif right and top:
|
| 71 |
-
return dist((x1b, y1b), (x2, y2))
|
| 72 |
-
elif left:
|
| 73 |
-
return x1 - x2b
|
| 74 |
-
elif right:
|
| 75 |
-
return x2 - x1b
|
| 76 |
-
elif bottom:
|
| 77 |
-
return y1 - y2b
|
| 78 |
-
elif top:
|
| 79 |
-
return y2 - y1b
|
| 80 |
-
return 0.0
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def _bbox_center(bbox):
|
| 84 |
-
if len(bbox) < 4 or len(bbox) % 2 != 0:
|
| 85 |
-
raise ValueError("bbox must contain an even number of coordinates (>=4)")
|
| 86 |
-
xs = bbox[0::2]
|
| 87 |
-
ys = bbox[1::2]
|
| 88 |
-
n = len(xs)
|
| 89 |
-
return sum(xs) / n, sum(ys) / n
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def bbox_center_distance(bbox1, bbox2):
|
| 93 |
-
c1x, c1y = _bbox_center(bbox1)
|
| 94 |
-
c2x, c2y = _bbox_center(bbox2)
|
| 95 |
-
return math.sqrt((c1x - c2x) ** 2 + (c1y - c2y) ** 2)
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def get_minbox_if_overlap_by_ratio(bbox1, bbox2, ratio):
|
| 99 |
-
"""通过calculate_overlap_area_2_minbox_area_ratio计算两个bbox重叠的面积占最小面积的box的比例
|
| 100 |
-
如果比例大于ratio,则返回小的那个bbox, 否则返回None."""
|
| 101 |
-
x1_min, y1_min, x1_max, y1_max = bbox1
|
| 102 |
-
x2_min, y2_min, x2_max, y2_max = bbox2
|
| 103 |
-
area1 = (x1_max - x1_min) * (y1_max - y1_min)
|
| 104 |
-
area2 = (x2_max - x2_min) * (y2_max - y2_min)
|
| 105 |
-
overlap_ratio = calculate_overlap_area_2_minbox_area_ratio(bbox1, bbox2)
|
| 106 |
-
if overlap_ratio > ratio:
|
| 107 |
-
if area1 <= area2:
|
| 108 |
-
return bbox1
|
| 109 |
-
else:
|
| 110 |
-
return bbox2
|
| 111 |
-
else:
|
| 112 |
-
return None
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
def calculate_overlap_area_2_minbox_area_ratio(bbox1, bbox2):
|
| 116 |
-
"""计算box1和box2的重叠面积占最小面积的box的比例."""
|
| 117 |
-
# Determine the coordinates of the intersection rectangle
|
| 118 |
-
x_left = max(bbox1[0], bbox2[0])
|
| 119 |
-
y_top = max(bbox1[1], bbox2[1])
|
| 120 |
-
x_right = min(bbox1[2], bbox2[2])
|
| 121 |
-
y_bottom = min(bbox1[3], bbox2[3])
|
| 122 |
-
|
| 123 |
-
if x_right < x_left or y_bottom < y_top:
|
| 124 |
-
return 0.0
|
| 125 |
-
|
| 126 |
-
# The area of overlap area
|
| 127 |
-
intersection_area = (x_right - x_left) * (y_bottom - y_top)
|
| 128 |
-
min_box_area = min([(bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]),
|
| 129 |
-
(bbox2[3] - bbox2[1]) * (bbox2[2] - bbox2[0])])
|
| 130 |
-
if min_box_area == 0:
|
| 131 |
-
return 0
|
| 132 |
-
else:
|
| 133 |
-
return intersection_area / min_box_area
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
def calculate_iou(bbox1, bbox2):
|
| 137 |
-
"""计算两个边界框的交并比(IOU)。
|
| 138 |
-
|
| 139 |
-
Args:
|
| 140 |
-
bbox1 (list[float]): 第一个边界框的坐标,格式为 [x1, y1, x2, y2],其中 (x1, y1) 为左上角坐标,(x2, y2) 为右下角坐标。
|
| 141 |
-
bbox2 (list[float]): 第二个边界框的坐标,格式与 `bbox1` 相同。
|
| 142 |
-
|
| 143 |
-
Returns:
|
| 144 |
-
float: 两个边界框的交并比(IOU),取值范围为 [0, 1]。
|
| 145 |
-
"""
|
| 146 |
-
# Determine the coordinates of the intersection rectangle
|
| 147 |
-
x_left = max(bbox1[0], bbox2[0])
|
| 148 |
-
y_top = max(bbox1[1], bbox2[1])
|
| 149 |
-
x_right = min(bbox1[2], bbox2[2])
|
| 150 |
-
y_bottom = min(bbox1[3], bbox2[3])
|
| 151 |
-
|
| 152 |
-
if x_right < x_left or y_bottom < y_top:
|
| 153 |
-
return 0.0
|
| 154 |
-
|
| 155 |
-
# The area of overlap area
|
| 156 |
-
intersection_area = (x_right - x_left) * (y_bottom - y_top)
|
| 157 |
-
|
| 158 |
-
# The area of both rectangles
|
| 159 |
-
bbox1_area = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
|
| 160 |
-
bbox2_area = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1])
|
| 161 |
-
|
| 162 |
-
if any([bbox1_area == 0, bbox2_area == 0]):
|
| 163 |
-
return 0
|
| 164 |
-
|
| 165 |
-
# Compute the intersection over union by taking the intersection area
|
| 166 |
-
# and dividing it by the sum of both areas minus the intersection area
|
| 167 |
-
iou = intersection_area / float(bbox1_area + bbox2_area - intersection_area)
|
| 168 |
-
|
| 169 |
-
return iou
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
def calculate_overlap_area_in_bbox1_area_ratio(bbox1, bbox2):
|
| 173 |
-
"""计算box1和box2的重叠面积占bbox1的比例."""
|
| 174 |
-
# Determine the coordinates of the intersection rectangle
|
| 175 |
-
x_left = max(bbox1[0], bbox2[0])
|
| 176 |
-
y_top = max(bbox1[1], bbox2[1])
|
| 177 |
-
x_right = min(bbox1[2], bbox2[2])
|
| 178 |
-
y_bottom = min(bbox1[3], bbox2[3])
|
| 179 |
-
|
| 180 |
-
if x_right < x_left or y_bottom < y_top:
|
| 181 |
-
return 0.0
|
| 182 |
-
|
| 183 |
-
# The area of overlap area
|
| 184 |
-
intersection_area = (x_right - x_left) * (y_bottom - y_top)
|
| 185 |
-
bbox1_area = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1])
|
| 186 |
-
if bbox1_area == 0:
|
| 187 |
-
return 0
|
| 188 |
-
else:
|
| 189 |
-
return intersection_area / bbox1_area
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def calculate_vertical_projection_overlap_ratio(block1, block2):
|
| 193 |
-
"""
|
| 194 |
-
Calculate the proportion of the x-axis covered by the vertical projection of two blocks.
|
| 195 |
-
|
| 196 |
-
Args:
|
| 197 |
-
block1 (tuple): Coordinates of the first block (x0, y0, x1, y1).
|
| 198 |
-
block2 (tuple): Coordinates of the second block (x0, y0, x1, y1).
|
| 199 |
-
|
| 200 |
-
Returns:
|
| 201 |
-
float: The proportion of the x-axis covered by the vertical projection of the two blocks.
|
| 202 |
-
"""
|
| 203 |
-
x0_1, _, x1_1, _ = block1
|
| 204 |
-
x0_2, _, x1_2, _ = block2
|
| 205 |
-
|
| 206 |
-
# Calculate the intersection of the x-coordinates
|
| 207 |
-
x_left = max(x0_1, x0_2)
|
| 208 |
-
x_right = min(x1_1, x1_2)
|
| 209 |
-
|
| 210 |
-
if x_right < x_left:
|
| 211 |
-
return 0.0
|
| 212 |
-
|
| 213 |
-
# Length of the intersection
|
| 214 |
-
intersection_length = x_right - x_left
|
| 215 |
-
|
| 216 |
-
# Length of the x-axis projection of the first block
|
| 217 |
-
block1_length = x1_1 - x0_1
|
| 218 |
-
|
| 219 |
-
if block1_length == 0:
|
| 220 |
-
return 0.0
|
| 221 |
-
|
| 222 |
-
# Proportion of the x-axis covered by the intersection
|
| 223 |
-
# logger.info(f"intersection_length: {intersection_length}, block1_length: {block1_length}")
|
| 224 |
-
return intersection_length / block1_length
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/char_utils.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
# Copyright (c) Opendatalab. All rights reserved.
|
| 2 |
-
import re
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
def is_hyphen_at_line_end(line):
|
| 6 |
-
"""Check if a line ends with one or more letters followed by a hyphen.
|
| 7 |
-
|
| 8 |
-
Args:
|
| 9 |
-
line (str): The line of text to check.
|
| 10 |
-
|
| 11 |
-
Returns:
|
| 12 |
-
bool: True if the line ends with one or more letters followed by a hyphen, False otherwise.
|
| 13 |
-
"""
|
| 14 |
-
# Use regex to check if the line ends with one or more letters followed by a hyphen
|
| 15 |
-
return bool(re.search(r'[A-Za-z]+-\s*$', line))
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def full_to_half_exclude_marks(text: str) -> str:
|
| 19 |
-
"""Convert full-width characters to half-width characters using code point manipulation.
|
| 20 |
-
|
| 21 |
-
Args:
|
| 22 |
-
text: String containing full-width characters
|
| 23 |
-
|
| 24 |
-
Returns:
|
| 25 |
-
String with full-width characters converted to half-width
|
| 26 |
-
"""
|
| 27 |
-
result = []
|
| 28 |
-
for char in text:
|
| 29 |
-
code = ord(char)
|
| 30 |
-
# Full-width letters and numbers (FF21-FF3A for A-Z, FF41-FF5A for a-z, FF10-FF19 for 0-9)
|
| 31 |
-
if (0xFF21 <= code <= 0xFF3A) or (0xFF41 <= code <= 0xFF5A) or (0xFF10 <= code <= 0xFF19):
|
| 32 |
-
result.append(chr(code - 0xFEE0)) # Shift to ASCII range
|
| 33 |
-
else:
|
| 34 |
-
result.append(char)
|
| 35 |
-
return ''.join(result)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def full_to_half(text: str) -> str:
|
| 39 |
-
"""Convert full-width characters to half-width characters using code point manipulation.
|
| 40 |
-
|
| 41 |
-
Args:
|
| 42 |
-
text: String containing full-width characters
|
| 43 |
-
|
| 44 |
-
Returns:
|
| 45 |
-
String with full-width characters converted to half-width
|
| 46 |
-
"""
|
| 47 |
-
result = []
|
| 48 |
-
for char in text:
|
| 49 |
-
code = ord(char)
|
| 50 |
-
# Full-width letters, numbers and punctuation (FF01-FF5E)
|
| 51 |
-
if 0xFF01 <= code <= 0xFF5E:
|
| 52 |
-
result.append(chr(code - 0xFEE0)) # Shift to ASCII range
|
| 53 |
-
else:
|
| 54 |
-
result.append(char)
|
| 55 |
-
return ''.join(result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/check_sys_env.py
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import platform
|
| 3 |
-
|
| 4 |
-
from packaging import version
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
def is_windows_environment() -> bool:
|
| 8 |
-
return platform.system() == "Windows"
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
# Detect if the current environment is a Mac computer
|
| 12 |
-
def is_mac_environment() -> bool:
|
| 13 |
-
return platform.system() == "Darwin"
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def is_linux_environment() -> bool:
|
| 17 |
-
return platform.system() == "Linux"
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
# Detect if CPU is Apple Silicon architecture
|
| 21 |
-
def is_apple_silicon_cpu() -> bool:
|
| 22 |
-
return platform.machine() in ["arm64", "aarch64"]
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
# If Mac computer with Apple Silicon architecture, check if macOS version is 13.5 or above
|
| 26 |
-
def is_mac_os_version_supported(min_version: str = "13.5") -> bool:
|
| 27 |
-
if not is_mac_environment() or not is_apple_silicon_cpu():
|
| 28 |
-
return False
|
| 29 |
-
mac_version = platform.mac_ver()[0]
|
| 30 |
-
if not mac_version:
|
| 31 |
-
return False
|
| 32 |
-
# print("Mac OS Version:", mac_version)
|
| 33 |
-
return version.parse(mac_version) >= version.parse(min_version)
|
| 34 |
-
|
| 35 |
-
if __name__ == "__main__":
|
| 36 |
-
print("Is Mac Environment:", is_mac_environment())
|
| 37 |
-
print("Is Apple Silicon CPU:", is_apple_silicon_cpu())
|
| 38 |
-
print("Is Mac OS Version Supported (>=13.5):", is_mac_os_version_supported())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/config_reader.py
DELETED
|
@@ -1,136 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import json
|
| 3 |
-
import os
|
| 4 |
-
from loguru import logger
|
| 5 |
-
|
| 6 |
-
try:
|
| 7 |
-
import torch
|
| 8 |
-
import torch_npu
|
| 9 |
-
except ImportError:
|
| 10 |
-
pass
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
# 定义配置文件名常量
|
| 14 |
-
CONFIG_FILE_NAME = os.getenv('NaviOCR_TOOLS_CONFIG_JSON', 'NaviOCR.json')
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def read_config():
|
| 18 |
-
if os.path.isabs(CONFIG_FILE_NAME):
|
| 19 |
-
config_file = CONFIG_FILE_NAME
|
| 20 |
-
else:
|
| 21 |
-
home_dir = os.path.expanduser('~')
|
| 22 |
-
config_file = os.path.join(home_dir, CONFIG_FILE_NAME)
|
| 23 |
-
|
| 24 |
-
if not os.path.exists(config_file):
|
| 25 |
-
# logger.warning(f'{config_file} not found, using default configuration')
|
| 26 |
-
return None
|
| 27 |
-
else:
|
| 28 |
-
with open(config_file, 'r', encoding='utf-8') as f:
|
| 29 |
-
config = json.load(f)
|
| 30 |
-
return config
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def get_s3_config(bucket_name: str):
|
| 34 |
-
"""~/magic-pdf.json 读出来."""
|
| 35 |
-
config = read_config()
|
| 36 |
-
|
| 37 |
-
bucket_info = config.get('bucket_info')
|
| 38 |
-
if bucket_name not in bucket_info:
|
| 39 |
-
access_key, secret_key, storage_endpoint = bucket_info['[default]']
|
| 40 |
-
else:
|
| 41 |
-
access_key, secret_key, storage_endpoint = bucket_info[bucket_name]
|
| 42 |
-
|
| 43 |
-
if access_key is None or secret_key is None or storage_endpoint is None:
|
| 44 |
-
raise Exception(f'ak, sk or endpoint not found in {CONFIG_FILE_NAME}')
|
| 45 |
-
|
| 46 |
-
# logger.info(f"get_s3_config: ak={access_key}, sk={secret_key}, endpoint={storage_endpoint}")
|
| 47 |
-
|
| 48 |
-
return access_key, secret_key, storage_endpoint
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def get_s3_config_dict(path: str):
|
| 52 |
-
access_key, secret_key, storage_endpoint = get_s3_config(get_bucket_name(path))
|
| 53 |
-
return {'ak': access_key, 'sk': secret_key, 'endpoint': storage_endpoint}
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def get_bucket_name(path):
|
| 57 |
-
bucket, key = parse_bucket_key(path)
|
| 58 |
-
return bucket
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def parse_bucket_key(s3_full_path: str):
|
| 62 |
-
"""
|
| 63 |
-
输入 s3://bucket/path/to/my/file.txt
|
| 64 |
-
输出 bucket, path/to/my/file.txt
|
| 65 |
-
"""
|
| 66 |
-
s3_full_path = s3_full_path.strip()
|
| 67 |
-
if s3_full_path.startswith("s3://"):
|
| 68 |
-
s3_full_path = s3_full_path[5:]
|
| 69 |
-
if s3_full_path.startswith("/"):
|
| 70 |
-
s3_full_path = s3_full_path[1:]
|
| 71 |
-
bucket, key = s3_full_path.split("/", 1)
|
| 72 |
-
return bucket, key
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def get_device():
|
| 76 |
-
device_mode = os.getenv('NaviOCR_DEVICE_MODE', None)
|
| 77 |
-
if device_mode is not None:
|
| 78 |
-
return device_mode
|
| 79 |
-
else:
|
| 80 |
-
if torch.cuda.is_available():
|
| 81 |
-
return "cuda"
|
| 82 |
-
elif torch.backends.mps.is_available():
|
| 83 |
-
return "mps"
|
| 84 |
-
else:
|
| 85 |
-
try:
|
| 86 |
-
if torch_npu.npu.is_available():
|
| 87 |
-
return "npu"
|
| 88 |
-
except Exception as e:
|
| 89 |
-
pass
|
| 90 |
-
return "cpu"
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
def get_formula_enable(formula_enable):
|
| 94 |
-
formula_enable_env = os.getenv('NaviOCR_FORMULA_ENABLE')
|
| 95 |
-
formula_enable = formula_enable if formula_enable_env is None else formula_enable_env.lower() == 'true'
|
| 96 |
-
return formula_enable
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
def get_table_enable(table_enable):
|
| 100 |
-
table_enable_env = os.getenv('NaviOCR_TABLE_ENABLE')
|
| 101 |
-
table_enable = table_enable if table_enable_env is None else table_enable_env.lower() == 'true'
|
| 102 |
-
return table_enable
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def get_latex_delimiter_config():
|
| 106 |
-
config = read_config()
|
| 107 |
-
if config is None:
|
| 108 |
-
return None
|
| 109 |
-
latex_delimiter_config = config.get('latex-delimiter-config', None)
|
| 110 |
-
if latex_delimiter_config is None:
|
| 111 |
-
# logger.warning(f"'latex-delimiter-config' not found in {CONFIG_FILE_NAME}, use 'None' as default")
|
| 112 |
-
return None
|
| 113 |
-
else:
|
| 114 |
-
return latex_delimiter_config
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
def get_llm_aided_config():
|
| 118 |
-
config = read_config()
|
| 119 |
-
if config is None:
|
| 120 |
-
return None
|
| 121 |
-
llm_aided_config = config.get('llm-aided-config', None)
|
| 122 |
-
if llm_aided_config is None:
|
| 123 |
-
# logger.warning(f"'llm-aided-config' not found in {CONFIG_FILE_NAME}, use 'None' as default")
|
| 124 |
-
return None
|
| 125 |
-
else:
|
| 126 |
-
return llm_aided_config
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def get_local_models_dir():
|
| 130 |
-
config = read_config()
|
| 131 |
-
if config is None:
|
| 132 |
-
return None
|
| 133 |
-
models_dir = config.get('models-dir')
|
| 134 |
-
if models_dir is None:
|
| 135 |
-
logger.warning(f"'models-dir' not found in {CONFIG_FILE_NAME}, use None as default")
|
| 136 |
-
return models_dir
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/cut_image.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
from loguru import logger
|
| 2 |
-
|
| 3 |
-
from .pdf_image_tools import cut_image
|
| 4 |
-
import numpy as np
|
| 5 |
-
|
| 6 |
-
def merge_bboxes_if_needed(bboxes):
|
| 7 |
-
if len(bboxes) > 4:
|
| 8 |
-
bboxes = np.array(bboxes).reshape(-1, 2)
|
| 9 |
-
minx = int(bboxes[:, 0].min())
|
| 10 |
-
miny = int(bboxes[:, 1].min())
|
| 11 |
-
maxx = int(bboxes[:, 0].max())
|
| 12 |
-
maxy = int(bboxes[:, 1].max())
|
| 13 |
-
return [minx, miny, maxx, maxy]
|
| 14 |
-
return [int(x) for x in bboxes]
|
| 15 |
-
|
| 16 |
-
def cut_image_and_table(span, page_pil_img, page_img_md5, page_id, image_writer, scale=2):
|
| 17 |
-
def return_path(path_type):
|
| 18 |
-
return f"{path_type}/{page_img_md5}"
|
| 19 |
-
|
| 20 |
-
span_type = span["type"]
|
| 21 |
-
span["bbox"] = merge_bboxes_if_needed(span["bbox"])
|
| 22 |
-
|
| 23 |
-
if not check_img_bbox(span["bbox"]) or not image_writer:
|
| 24 |
-
span["image_path"] = ""
|
| 25 |
-
else:
|
| 26 |
-
span["image_path"] = cut_image(
|
| 27 |
-
span["bbox"], page_id, page_pil_img, return_path=return_path(span_type), image_writer=image_writer, scale=scale
|
| 28 |
-
)
|
| 29 |
-
return span
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def check_img_bbox(bbox) -> bool:
|
| 33 |
-
if any([bbox[0] >= bbox[2], bbox[1] >= bbox[3]]):
|
| 34 |
-
logger.warning(f"image_bboxes: 错误的box, {bbox}")
|
| 35 |
-
return False
|
| 36 |
-
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/draw_bbox.py
DELETED
|
@@ -1,634 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
from io import BytesIO
|
| 3 |
-
|
| 4 |
-
from loguru import logger
|
| 5 |
-
from pypdf import PdfReader, PdfWriter, PageObject
|
| 6 |
-
from reportlab.pdfgen import canvas
|
| 7 |
-
|
| 8 |
-
from .enum_class import BlockType, ContentType, SplitFlag
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def cal_canvas_quad(page, bbox):
|
| 12 |
-
page_width, page_height = float(page.cropbox[2]), float(page.cropbox[3])
|
| 13 |
-
actual_width = page_width
|
| 14 |
-
actual_height = page_height
|
| 15 |
-
rotation_obj = page.get("/Rotate", 0)
|
| 16 |
-
try:
|
| 17 |
-
rotation = int(rotation_obj) % 360
|
| 18 |
-
except (ValueError, TypeError) as e:
|
| 19 |
-
logger.warning(f"Invalid /Rotate value {rotation_obj!r}; defaulting to 0. Error: {e}")
|
| 20 |
-
rotation = 0
|
| 21 |
-
if rotation in [90, 270]:
|
| 22 |
-
actual_width, actual_height = actual_height, actual_width
|
| 23 |
-
|
| 24 |
-
pts = []
|
| 25 |
-
for i in range(0, len(bbox), 2):
|
| 26 |
-
x = bbox[i]
|
| 27 |
-
y = bbox[i + 1]
|
| 28 |
-
if rotation == 270:
|
| 29 |
-
x, y = actual_height - y, actual_width - x
|
| 30 |
-
elif rotation == 180:
|
| 31 |
-
x = page_width - x
|
| 32 |
-
elif rotation == 90:
|
| 33 |
-
x, y = y, x
|
| 34 |
-
else:
|
| 35 |
-
y = page_height - y
|
| 36 |
-
pts.extend([x, y])
|
| 37 |
-
return pts
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def cal_canvas_rect(page, bbox):
|
| 41 |
-
"""
|
| 42 |
-
Calculate the rectangle coordinates on the canvas based on the original PDF page and bounding box.
|
| 43 |
-
|
| 44 |
-
Args:
|
| 45 |
-
page: A PyPDF2 Page object representing a single page in the PDF.
|
| 46 |
-
bbox: [x0, y0, x1, y1] representing the bounding box coordinates.
|
| 47 |
-
|
| 48 |
-
Returns:
|
| 49 |
-
rect: [x0, y0, width, height] representing the rectangle coordinates on the canvas.
|
| 50 |
-
"""
|
| 51 |
-
page_width, page_height = float(page.cropbox[2]), float(page.cropbox[3])
|
| 52 |
-
|
| 53 |
-
actual_width = page_width # The width of the final PDF display
|
| 54 |
-
actual_height = page_height # The height of the final PDF display
|
| 55 |
-
|
| 56 |
-
rotation_obj = page.get("/Rotate", 0)
|
| 57 |
-
try:
|
| 58 |
-
rotation = int(rotation_obj) % 360 # cast rotation to int to handle IndirectObject
|
| 59 |
-
except (ValueError, TypeError) as e:
|
| 60 |
-
logger.warning(f"Invalid /Rotate value {rotation_obj!r} on page; defaulting to 0. Error: {e}")
|
| 61 |
-
rotation = 0
|
| 62 |
-
|
| 63 |
-
if rotation in [90, 270]:
|
| 64 |
-
# PDF is rotated 90 degrees or 270 degrees, and the width and height need to be swapped
|
| 65 |
-
actual_width, actual_height = actual_height, actual_width
|
| 66 |
-
|
| 67 |
-
x0, y0, x1, y1 = bbox
|
| 68 |
-
rect_w = abs(x1 - x0)
|
| 69 |
-
rect_h = abs(y1 - y0)
|
| 70 |
-
|
| 71 |
-
if rotation == 270:
|
| 72 |
-
rect_w, rect_h = rect_h, rect_w
|
| 73 |
-
x0 = actual_height - y1
|
| 74 |
-
y0 = actual_width - x1
|
| 75 |
-
elif rotation == 180:
|
| 76 |
-
x0 = page_width - x1
|
| 77 |
-
# y0 stays the same
|
| 78 |
-
elif rotation == 90:
|
| 79 |
-
rect_w, rect_h = rect_h, rect_w
|
| 80 |
-
x0, y0 = y0, x0
|
| 81 |
-
else:
|
| 82 |
-
# rotation == 0
|
| 83 |
-
y0 = page_height - y1
|
| 84 |
-
|
| 85 |
-
rect = [x0, y0, rect_w, rect_h]
|
| 86 |
-
return rect
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def draw_bbox_without_number(i, bbox_list, page, c, rgb_config, fill_config):
|
| 90 |
-
new_rgb = [float(color) / 255 for color in rgb_config]
|
| 91 |
-
page_data = bbox_list[i]
|
| 92 |
-
|
| 93 |
-
for bbox in page_data:
|
| 94 |
-
# ---------- 2点框 ----------
|
| 95 |
-
if len(bbox) == 4:
|
| 96 |
-
rect = cal_canvas_rect(page, bbox)
|
| 97 |
-
|
| 98 |
-
if fill_config:
|
| 99 |
-
c.setFillColorRGB(new_rgb[0], new_rgb[1], new_rgb[2], 0.3)
|
| 100 |
-
c.rect(rect[0], rect[1], rect[2], rect[3], stroke=0, fill=1)
|
| 101 |
-
else:
|
| 102 |
-
c.setStrokeColorRGB(new_rgb[0], new_rgb[1], new_rgb[2])
|
| 103 |
-
c.rect(rect[0], rect[1], rect[2], rect[3], stroke=1, fill=0)
|
| 104 |
-
# ---------- 多点框 ----------
|
| 105 |
-
elif len(bbox) % 2 == 0:
|
| 106 |
-
pts = cal_canvas_quad(page, bbox)
|
| 107 |
-
path = c.beginPath()
|
| 108 |
-
path.moveTo(pts[0], pts[1])
|
| 109 |
-
for j in range(2, len(pts), 2):
|
| 110 |
-
path.lineTo(pts[j], pts[j + 1])
|
| 111 |
-
path.close()
|
| 112 |
-
if fill_config:
|
| 113 |
-
c.setFillColorRGB(*new_rgb, 0.3)
|
| 114 |
-
c.drawPath(path, stroke=0, fill=1)
|
| 115 |
-
else:
|
| 116 |
-
c.setStrokeColorRGB(*new_rgb)
|
| 117 |
-
c.drawPath(path, stroke=1, fill=0)
|
| 118 |
-
else:
|
| 119 |
-
logger.warning(f"Invalid bbox length: {bbox}")
|
| 120 |
-
continue
|
| 121 |
-
|
| 122 |
-
return c
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def draw_bbox_with_number(i, bbox_list, page, c, rgb_config, fill_config, draw_bbox=True):
|
| 126 |
-
new_rgb = [float(color) / 255 for color in rgb_config]
|
| 127 |
-
page_data = bbox_list[i]
|
| 128 |
-
|
| 129 |
-
page_width, page_height = float(page.cropbox[2]), float(page.cropbox[3])
|
| 130 |
-
|
| 131 |
-
for j, (bbox, angle) in enumerate(page_data):
|
| 132 |
-
|
| 133 |
-
# ---------- 2点框 ----------
|
| 134 |
-
if len(bbox) == 4:
|
| 135 |
-
rect = cal_canvas_rect(page, bbox)
|
| 136 |
-
|
| 137 |
-
if draw_bbox:
|
| 138 |
-
if fill_config:
|
| 139 |
-
c.setFillColorRGB(*new_rgb, 0.3)
|
| 140 |
-
c.rect(rect[0], rect[1], rect[2], rect[3], stroke=0, fill=1)
|
| 141 |
-
else:
|
| 142 |
-
c.setStrokeColorRGB(*new_rgb)
|
| 143 |
-
c.rect(rect[0], rect[1], rect[2], rect[3], stroke=1, fill=0)
|
| 144 |
-
|
| 145 |
-
# ---------- 多点框 ----------
|
| 146 |
-
elif len(bbox) % 2 == 0:
|
| 147 |
-
pts = cal_canvas_quad(page, bbox)
|
| 148 |
-
|
| 149 |
-
xs = pts[0::2]
|
| 150 |
-
ys = pts[1::2]
|
| 151 |
-
|
| 152 |
-
rect = [min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)]
|
| 153 |
-
|
| 154 |
-
if draw_bbox:
|
| 155 |
-
path = c.beginPath()
|
| 156 |
-
path.moveTo(pts[0], pts[1])
|
| 157 |
-
for i in range(2, len(pts), 2):
|
| 158 |
-
path.lineTo(pts[i], pts[i + 1])
|
| 159 |
-
path.close()
|
| 160 |
-
if fill_config:
|
| 161 |
-
c.setFillColorRGB(*new_rgb, 0.3)
|
| 162 |
-
c.drawPath(path, stroke=0, fill=1)
|
| 163 |
-
else:
|
| 164 |
-
c.setStrokeColorRGB(*new_rgb)
|
| 165 |
-
c.drawPath(path, stroke=1, fill=0)
|
| 166 |
-
else:
|
| 167 |
-
logger.warning(f"Invalid bbox length: {bbox}")
|
| 168 |
-
continue
|
| 169 |
-
c.setFillColorRGB(*new_rgb, 1.0)
|
| 170 |
-
c.setFontSize(size=10)
|
| 171 |
-
|
| 172 |
-
c.saveState()
|
| 173 |
-
|
| 174 |
-
rotation_obj = page.get("/Rotate", 0)
|
| 175 |
-
try:
|
| 176 |
-
rotation = int(rotation_obj) % 360
|
| 177 |
-
except (ValueError, TypeError):
|
| 178 |
-
logger.warning(f"Invalid /Rotate value: {rotation_obj!r}, defaulting to 0")
|
| 179 |
-
rotation = 0
|
| 180 |
-
|
| 181 |
-
if rotation == 0:
|
| 182 |
-
c.translate(rect[0] + rect[2] + 2, rect[1] + rect[3] - 10)
|
| 183 |
-
elif rotation == 90:
|
| 184 |
-
c.translate(rect[0] + 10, rect[1] + rect[3] + 2)
|
| 185 |
-
elif rotation == 180:
|
| 186 |
-
c.translate(rect[0] - 2, rect[1] + 10)
|
| 187 |
-
elif rotation == 270:
|
| 188 |
-
c.translate(rect[0] + rect[2] - 10, rect[1] - 2)
|
| 189 |
-
|
| 190 |
-
c.rotate(rotation)
|
| 191 |
-
c.drawString(0, 0, str(j + 1) + '+' + str(angle))
|
| 192 |
-
|
| 193 |
-
c.restoreState()
|
| 194 |
-
|
| 195 |
-
return c
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
def draw_layout_bbox(pdf_info, pdf_bytes, out_path, filename):
|
| 199 |
-
dropped_bbox_list = []
|
| 200 |
-
|
| 201 |
-
tables_body_list, tables_caption_list, tables_footnote_list = [], [], []
|
| 202 |
-
imgs_body_list, imgs_caption_list, imgs_footnote_list = [], [], []
|
| 203 |
-
codes_body_list, codes_caption_list = [], []
|
| 204 |
-
|
| 205 |
-
titles_list = []
|
| 206 |
-
texts_list = []
|
| 207 |
-
interequations_list = []
|
| 208 |
-
lists_list = []
|
| 209 |
-
list_items_list = []
|
| 210 |
-
indexs_list = []
|
| 211 |
-
seal_list = []
|
| 212 |
-
char_list = []
|
| 213 |
-
|
| 214 |
-
for page in pdf_info:
|
| 215 |
-
page_dropped_list = []
|
| 216 |
-
|
| 217 |
-
tables_body, tables_caption, tables_footnote = [], [], []
|
| 218 |
-
imgs_body, imgs_caption, imgs_footnote = [], [], []
|
| 219 |
-
codes_body, codes_caption = [], []
|
| 220 |
-
|
| 221 |
-
titles = []
|
| 222 |
-
texts = []
|
| 223 |
-
interequations = []
|
| 224 |
-
lists = []
|
| 225 |
-
list_items = []
|
| 226 |
-
indices = []
|
| 227 |
-
seals = []
|
| 228 |
-
chars = []
|
| 229 |
-
|
| 230 |
-
for dropped_bbox in page['discarded_blocks']:
|
| 231 |
-
page_dropped_list.append(dropped_bbox['bbox'])
|
| 232 |
-
|
| 233 |
-
dropped_bbox_list.append(page_dropped_list)
|
| 234 |
-
|
| 235 |
-
for block in page["para_blocks"]:
|
| 236 |
-
block_bbox = block["bbox"]
|
| 237 |
-
|
| 238 |
-
if block["type"] == BlockType.TABLE:
|
| 239 |
-
for nested_block in block.get("blocks", []):
|
| 240 |
-
nested_bbox = nested_block["bbox"]
|
| 241 |
-
|
| 242 |
-
if nested_block["type"] == BlockType.TABLE_BODY:
|
| 243 |
-
tables_body.append(nested_bbox)
|
| 244 |
-
|
| 245 |
-
elif nested_block["type"] == BlockType.TABLE_CAPTION:
|
| 246 |
-
tables_caption.append(nested_bbox)
|
| 247 |
-
|
| 248 |
-
elif nested_block["type"] == BlockType.TABLE_FOOTNOTE:
|
| 249 |
-
if nested_block.get(SplitFlag.CROSS_PAGE, False):
|
| 250 |
-
continue
|
| 251 |
-
tables_footnote.append(nested_bbox)
|
| 252 |
-
|
| 253 |
-
elif block["type"] == BlockType.IMAGE:
|
| 254 |
-
for nested_block in block.get("blocks", []):
|
| 255 |
-
nested_bbox = nested_block["bbox"]
|
| 256 |
-
|
| 257 |
-
if nested_block["type"] == BlockType.IMAGE_BODY:
|
| 258 |
-
imgs_body.append(nested_bbox)
|
| 259 |
-
|
| 260 |
-
elif nested_block["type"] == BlockType.IMAGE_CAPTION:
|
| 261 |
-
imgs_caption.append(nested_bbox)
|
| 262 |
-
|
| 263 |
-
elif nested_block["type"] == BlockType.IMAGE_FOOTNOTE:
|
| 264 |
-
imgs_footnote.append(nested_bbox)
|
| 265 |
-
|
| 266 |
-
elif block["type"] == BlockType.CODE:
|
| 267 |
-
for nested_block in block.get("blocks", []):
|
| 268 |
-
nested_bbox = nested_block["bbox"]
|
| 269 |
-
|
| 270 |
-
if nested_block["type"] == BlockType.CODE_BODY:
|
| 271 |
-
codes_body.append(nested_bbox)
|
| 272 |
-
|
| 273 |
-
elif nested_block["type"] == BlockType.CODE_CAPTION:
|
| 274 |
-
codes_caption.append(nested_bbox)
|
| 275 |
-
|
| 276 |
-
elif block["type"] == BlockType.TITLE:
|
| 277 |
-
titles.append(block_bbox)
|
| 278 |
-
|
| 279 |
-
elif block["type"] in [BlockType.TEXT, BlockType.REF_TEXT]:
|
| 280 |
-
texts.append(block_bbox)
|
| 281 |
-
|
| 282 |
-
elif block["type"] == BlockType.INTERLINE_EQUATION:
|
| 283 |
-
interequations.append(block_bbox)
|
| 284 |
-
|
| 285 |
-
elif block["type"] == BlockType.SEAL:
|
| 286 |
-
seals.append(block_bbox)
|
| 287 |
-
|
| 288 |
-
elif block["type"] == BlockType.CHAR:
|
| 289 |
-
chars.append(block_bbox)
|
| 290 |
-
|
| 291 |
-
elif block["type"] == BlockType.LIST:
|
| 292 |
-
lists.append(block_bbox)
|
| 293 |
-
|
| 294 |
-
if "blocks" in block:
|
| 295 |
-
for sub_block in block["blocks"]:
|
| 296 |
-
list_items.append(sub_block["bbox"])
|
| 297 |
-
|
| 298 |
-
elif block["type"] == BlockType.INDEX:
|
| 299 |
-
indices.append(block_bbox)
|
| 300 |
-
|
| 301 |
-
tables_body_list.append(tables_body)
|
| 302 |
-
tables_caption_list.append(tables_caption)
|
| 303 |
-
tables_footnote_list.append(tables_footnote)
|
| 304 |
-
|
| 305 |
-
imgs_body_list.append(imgs_body)
|
| 306 |
-
imgs_caption_list.append(imgs_caption)
|
| 307 |
-
imgs_footnote_list.append(imgs_footnote)
|
| 308 |
-
|
| 309 |
-
titles_list.append(titles)
|
| 310 |
-
texts_list.append(texts)
|
| 311 |
-
interequations_list.append(interequations)
|
| 312 |
-
|
| 313 |
-
seal_list.append(seals)
|
| 314 |
-
char_list.append(chars)
|
| 315 |
-
lists_list.append(lists)
|
| 316 |
-
list_items_list.append(list_items)
|
| 317 |
-
|
| 318 |
-
indexs_list.append(indices)
|
| 319 |
-
|
| 320 |
-
codes_body_list.append(codes_body)
|
| 321 |
-
codes_caption_list.append(codes_caption)
|
| 322 |
-
|
| 323 |
-
layout_bbox_list = []
|
| 324 |
-
|
| 325 |
-
table_type_order = {
|
| 326 |
-
"table_caption": 1,
|
| 327 |
-
"table_body": 2,
|
| 328 |
-
"table_footnote": 3
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
for page in pdf_info:
|
| 332 |
-
page_block_list = []
|
| 333 |
-
|
| 334 |
-
for block in page["para_blocks"]:
|
| 335 |
-
if block["type"] in [
|
| 336 |
-
BlockType.TEXT,
|
| 337 |
-
BlockType.REF_TEXT,
|
| 338 |
-
BlockType.TITLE,
|
| 339 |
-
BlockType.INTERLINE_EQUATION,
|
| 340 |
-
BlockType.SEAL,
|
| 341 |
-
BlockType.CHAR,
|
| 342 |
-
BlockType.LIST,
|
| 343 |
-
BlockType.INDEX,
|
| 344 |
-
]:
|
| 345 |
-
bbox = block["bbox"]
|
| 346 |
-
page_block_list.append([bbox, block["angle"]])
|
| 347 |
-
|
| 348 |
-
elif block["type"] == BlockType.IMAGE:
|
| 349 |
-
for sub_block in block.get("blocks", []):
|
| 350 |
-
bbox = sub_block["bbox"]
|
| 351 |
-
page_block_list.append([bbox, sub_block["angle"]])
|
| 352 |
-
|
| 353 |
-
elif block["type"] == BlockType.TABLE:
|
| 354 |
-
sorted_blocks = sorted(
|
| 355 |
-
block.get("blocks", []),
|
| 356 |
-
key=lambda x: table_type_order[x["type"]]
|
| 357 |
-
)
|
| 358 |
-
|
| 359 |
-
for sub_block in sorted_blocks:
|
| 360 |
-
if sub_block.get(SplitFlag.CROSS_PAGE, False):
|
| 361 |
-
continue
|
| 362 |
-
|
| 363 |
-
bbox = sub_block["bbox"]
|
| 364 |
-
page_block_list.append([bbox, sub_block["angle"]])
|
| 365 |
-
|
| 366 |
-
elif block["type"] == BlockType.CODE:
|
| 367 |
-
for sub_block in block.get("blocks", []):
|
| 368 |
-
bbox = sub_block["bbox"]
|
| 369 |
-
page_block_list.append([bbox, sub_block["angle"]])
|
| 370 |
-
|
| 371 |
-
layout_bbox_list.append(page_block_list)
|
| 372 |
-
|
| 373 |
-
pdf_bytes_io = BytesIO(pdf_bytes)
|
| 374 |
-
pdf_docs = PdfReader(pdf_bytes_io)
|
| 375 |
-
output_pdf = PdfWriter()
|
| 376 |
-
|
| 377 |
-
for i, page in enumerate(pdf_docs.pages):
|
| 378 |
-
page_width = float(page.cropbox[2])
|
| 379 |
-
page_height = float(page.cropbox[3])
|
| 380 |
-
|
| 381 |
-
custom_page_size = (page_width, page_height)
|
| 382 |
-
|
| 383 |
-
packet = BytesIO()
|
| 384 |
-
c = canvas.Canvas(packet, pagesize=custom_page_size)
|
| 385 |
-
|
| 386 |
-
c = draw_bbox_without_number(i, codes_body_list, page, c, [102, 0, 204], True)
|
| 387 |
-
c = draw_bbox_without_number(i, codes_caption_list, page, c, [204, 153, 255], True)
|
| 388 |
-
c = draw_bbox_without_number(i, dropped_bbox_list, page, c, [158, 158, 158], True)
|
| 389 |
-
|
| 390 |
-
c = draw_bbox_without_number(i, tables_body_list, page, c, [204, 204, 0], True)
|
| 391 |
-
c = draw_bbox_without_number(i, tables_caption_list, page, c, [255, 255, 102], True)
|
| 392 |
-
c = draw_bbox_without_number(i, tables_footnote_list, page, c, [229, 255, 204], True)
|
| 393 |
-
|
| 394 |
-
c = draw_bbox_without_number(i, imgs_body_list, page, c, [153, 255, 51], True)
|
| 395 |
-
c = draw_bbox_without_number(i, imgs_caption_list, page, c, [102, 178, 255], True)
|
| 396 |
-
c = draw_bbox_without_number(i, imgs_footnote_list, page, c, [255, 178, 102], True)
|
| 397 |
-
|
| 398 |
-
c = draw_bbox_without_number(i, titles_list, page, c, [102, 102, 255], True)
|
| 399 |
-
c = draw_bbox_without_number(i, texts_list, page, c, [153, 0, 76], True)
|
| 400 |
-
|
| 401 |
-
c = draw_bbox_without_number(i, interequations_list, page, c, [0, 255, 0], True)
|
| 402 |
-
c = draw_bbox_without_number(i, seal_list, page, c, [255, 0, 255], True)
|
| 403 |
-
c = draw_bbox_without_number(i, char_list, page, c, [255, 100, 255], True)
|
| 404 |
-
|
| 405 |
-
c = draw_bbox_without_number(i, lists_list, page, c, [40, 169, 92], True)
|
| 406 |
-
c = draw_bbox_without_number(i, list_items_list, page, c, [40, 169, 92], False)
|
| 407 |
-
|
| 408 |
-
c = draw_bbox_without_number(i, indexs_list, page, c, [40, 169, 92], True)
|
| 409 |
-
|
| 410 |
-
c = draw_bbox_with_number(
|
| 411 |
-
i,
|
| 412 |
-
layout_bbox_list,
|
| 413 |
-
page,
|
| 414 |
-
c,
|
| 415 |
-
[255, 0, 0],
|
| 416 |
-
False,
|
| 417 |
-
draw_bbox=False
|
| 418 |
-
)
|
| 419 |
-
|
| 420 |
-
c.save()
|
| 421 |
-
|
| 422 |
-
packet.seek(0)
|
| 423 |
-
overlay_pdf = PdfReader(packet)
|
| 424 |
-
|
| 425 |
-
if len(overlay_pdf.pages) > 0:
|
| 426 |
-
new_page = PageObject(pdf=None)
|
| 427 |
-
new_page.update(page)
|
| 428 |
-
|
| 429 |
-
page = new_page
|
| 430 |
-
page.merge_page(overlay_pdf.pages[0])
|
| 431 |
-
|
| 432 |
-
output_pdf.add_page(page)
|
| 433 |
-
|
| 434 |
-
with open(f"{out_path}/{filename}", "wb") as f:
|
| 435 |
-
output_pdf.write(f)
|
| 436 |
-
|
| 437 |
-
def draw_span_bbox(pdf_info, pdf_bytes, out_path, filename):
|
| 438 |
-
text_list = []
|
| 439 |
-
inline_equation_list = []
|
| 440 |
-
interline_equation_list = []
|
| 441 |
-
image_list = []
|
| 442 |
-
table_list = []
|
| 443 |
-
dropped_list = []
|
| 444 |
-
|
| 445 |
-
def get_span_info(span):
|
| 446 |
-
if span['type'] == ContentType.TEXT:
|
| 447 |
-
page_text_list.append(span['bbox'])
|
| 448 |
-
elif span['type'] == ContentType.INLINE_EQUATION:
|
| 449 |
-
page_inline_equation_list.append(span['bbox'])
|
| 450 |
-
elif span['type'] == ContentType.INTERLINE_EQUATION:
|
| 451 |
-
page_interline_equation_list.append(span['bbox'])
|
| 452 |
-
elif span['type'] == ContentType.IMAGE:
|
| 453 |
-
page_image_list.append(span['bbox'])
|
| 454 |
-
elif span['type'] == ContentType.TABLE:
|
| 455 |
-
page_table_list.append(span['bbox'])
|
| 456 |
-
|
| 457 |
-
for page in pdf_info:
|
| 458 |
-
page_text_list = []
|
| 459 |
-
page_inline_equation_list = []
|
| 460 |
-
page_interline_equation_list = []
|
| 461 |
-
page_image_list = []
|
| 462 |
-
page_table_list = []
|
| 463 |
-
page_dropped_list = []
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
# 构造dropped_list
|
| 467 |
-
for block in page['discarded_blocks']:
|
| 468 |
-
if block['type'] == BlockType.DISCARDED:
|
| 469 |
-
for line in block['lines']:
|
| 470 |
-
for span in line['spans']:
|
| 471 |
-
page_dropped_list.append(span['bbox'])
|
| 472 |
-
dropped_list.append(page_dropped_list)
|
| 473 |
-
# 构造其余useful_list
|
| 474 |
-
for block in page['para_blocks']: # span直接用分段合并前的结果就可以
|
| 475 |
-
# for block in page['preproc_blocks']:
|
| 476 |
-
# print(block.keys(), block['type'])
|
| 477 |
-
if block['type'] in [
|
| 478 |
-
BlockType.TEXT,
|
| 479 |
-
BlockType.TITLE,
|
| 480 |
-
BlockType.INTERLINE_EQUATION,
|
| 481 |
-
BlockType.INDEX,
|
| 482 |
-
]:
|
| 483 |
-
for line in block['lines']:
|
| 484 |
-
for span in line['spans']:
|
| 485 |
-
get_span_info(span)
|
| 486 |
-
elif block['type'] in [BlockType.IMAGE, BlockType.TABLE, BlockType.LIST]:
|
| 487 |
-
for sub_block in block['blocks']:
|
| 488 |
-
for line in sub_block['lines']:
|
| 489 |
-
for span in line['spans']:
|
| 490 |
-
get_span_info(span)
|
| 491 |
-
text_list.append(page_text_list)
|
| 492 |
-
inline_equation_list.append(page_inline_equation_list)
|
| 493 |
-
interline_equation_list.append(page_interline_equation_list)
|
| 494 |
-
image_list.append(page_image_list)
|
| 495 |
-
table_list.append(page_table_list)
|
| 496 |
-
|
| 497 |
-
pdf_bytes_io = BytesIO(pdf_bytes)
|
| 498 |
-
pdf_docs = PdfReader(pdf_bytes_io)
|
| 499 |
-
output_pdf = PdfWriter()
|
| 500 |
-
|
| 501 |
-
for i, page in enumerate(pdf_docs.pages):
|
| 502 |
-
# 获取原始页面尺寸
|
| 503 |
-
page_width, page_height = float(page.cropbox[2]), float(page.cropbox[3])
|
| 504 |
-
custom_page_size = (page_width, page_height)
|
| 505 |
-
|
| 506 |
-
packet = BytesIO()
|
| 507 |
-
# 使用原始PDF的尺寸创建canvas
|
| 508 |
-
c = canvas.Canvas(packet, pagesize=custom_page_size)
|
| 509 |
-
|
| 510 |
-
# 获取当前页面的数据
|
| 511 |
-
draw_bbox_without_number(i, text_list, page, c,[255, 0, 0], False)
|
| 512 |
-
draw_bbox_without_number(i, inline_equation_list, page, c, [0, 255, 0], False)
|
| 513 |
-
draw_bbox_without_number(i, interline_equation_list, page, c, [0, 0, 255], False)
|
| 514 |
-
draw_bbox_without_number(i, image_list, page, c, [255, 204, 0], False)
|
| 515 |
-
draw_bbox_without_number(i, table_list, page, c, [204, 0, 255], False)
|
| 516 |
-
draw_bbox_without_number(i, dropped_list, page, c, [158, 158, 158], False)
|
| 517 |
-
|
| 518 |
-
c.save()
|
| 519 |
-
packet.seek(0)
|
| 520 |
-
overlay_pdf = PdfReader(packet)
|
| 521 |
-
|
| 522 |
-
# 添加检查确保overlay_pdf.pages不为空
|
| 523 |
-
if len(overlay_pdf.pages) > 0:
|
| 524 |
-
new_page = PageObject(pdf=None)
|
| 525 |
-
new_page.update(page)
|
| 526 |
-
page = new_page
|
| 527 |
-
page.merge_page(overlay_pdf.pages[0])
|
| 528 |
-
else:
|
| 529 |
-
# 记录日志并继续处理下一个页面
|
| 530 |
-
# logger.warning(f"span.pdf: 第{i + 1}页未能生成有效的overlay PDF")
|
| 531 |
-
pass
|
| 532 |
-
|
| 533 |
-
output_pdf.add_page(page)
|
| 534 |
-
|
| 535 |
-
# Save the PDF
|
| 536 |
-
with open(f"{out_path}/{filename}", "wb") as f:
|
| 537 |
-
output_pdf.write(f)
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
def draw_line_sort_bbox(pdf_info, pdf_bytes, out_path, filename):
|
| 541 |
-
layout_bbox_list = []
|
| 542 |
-
|
| 543 |
-
for page in pdf_info:
|
| 544 |
-
page_line_list = []
|
| 545 |
-
for block in page['preproc_blocks']:
|
| 546 |
-
if block['type'] in [BlockType.TEXT]:
|
| 547 |
-
for line in block['lines']:
|
| 548 |
-
bbox = line['bbox']
|
| 549 |
-
index = line['index']
|
| 550 |
-
page_line_list.append({'index': index, 'bbox': bbox})
|
| 551 |
-
elif block['type'] in [BlockType.TITLE, BlockType.INTERLINE_EQUATION]:
|
| 552 |
-
if 'virtual_lines' in block:
|
| 553 |
-
if len(block['virtual_lines']) > 0 and block['virtual_lines'][0].get('index', None) is not None:
|
| 554 |
-
for line in block['virtual_lines']:
|
| 555 |
-
bbox = line['bbox']
|
| 556 |
-
index = line['index']
|
| 557 |
-
page_line_list.append({'index': index, 'bbox': bbox})
|
| 558 |
-
else:
|
| 559 |
-
for line in block['lines']:
|
| 560 |
-
bbox = line['bbox']
|
| 561 |
-
index = line['index']
|
| 562 |
-
page_line_list.append({'index': index, 'bbox': bbox})
|
| 563 |
-
elif block['type'] in [BlockType.IMAGE, BlockType.TABLE]:
|
| 564 |
-
for sub_block in block['blocks']:
|
| 565 |
-
if sub_block['type'] in [BlockType.IMAGE_BODY, BlockType.TABLE_BODY]:
|
| 566 |
-
if len(sub_block['virtual_lines']) > 0 and sub_block['virtual_lines'][0].get('index', None) is not None:
|
| 567 |
-
for line in sub_block['virtual_lines']:
|
| 568 |
-
bbox = line['bbox']
|
| 569 |
-
index = line['index']
|
| 570 |
-
page_line_list.append({'index': index, 'bbox': bbox})
|
| 571 |
-
else:
|
| 572 |
-
for line in sub_block['lines']:
|
| 573 |
-
bbox = line['bbox']
|
| 574 |
-
index = line['index']
|
| 575 |
-
page_line_list.append({'index': index, 'bbox': bbox})
|
| 576 |
-
elif sub_block['type'] in [BlockType.IMAGE_CAPTION, BlockType.TABLE_CAPTION, BlockType.IMAGE_FOOTNOTE, BlockType.TABLE_FOOTNOTE]:
|
| 577 |
-
for line in sub_block['lines']:
|
| 578 |
-
bbox = line['bbox']
|
| 579 |
-
index = line['index']
|
| 580 |
-
page_line_list.append({'index': index, 'bbox': bbox})
|
| 581 |
-
sorted_bboxes = sorted(page_line_list, key=lambda x: x['index'])
|
| 582 |
-
layout_bbox_list.append(sorted_bbox['bbox'] for sorted_bbox in sorted_bboxes)
|
| 583 |
-
pdf_bytes_io = BytesIO(pdf_bytes)
|
| 584 |
-
pdf_docs = PdfReader(pdf_bytes_io)
|
| 585 |
-
output_pdf = PdfWriter()
|
| 586 |
-
|
| 587 |
-
for i, page in enumerate(pdf_docs.pages):
|
| 588 |
-
# 获取原始页面尺寸
|
| 589 |
-
page_width, page_height = float(page.cropbox[2]), float(page.cropbox[3])
|
| 590 |
-
custom_page_size = (page_width, page_height)
|
| 591 |
-
|
| 592 |
-
packet = BytesIO()
|
| 593 |
-
# 使用原始PDF的尺寸创建canvas
|
| 594 |
-
c = canvas.Canvas(packet, pagesize=custom_page_size)
|
| 595 |
-
|
| 596 |
-
# 获取当前页面的数据
|
| 597 |
-
draw_bbox_with_number(i, layout_bbox_list, page, c, [255, 0, 0], False)
|
| 598 |
-
|
| 599 |
-
c.save()
|
| 600 |
-
packet.seek(0)
|
| 601 |
-
overlay_pdf = PdfReader(packet)
|
| 602 |
-
|
| 603 |
-
# 添加检查确保overlay_pdf.pages不为空
|
| 604 |
-
if len(overlay_pdf.pages) > 0:
|
| 605 |
-
new_page = PageObject(pdf=None)
|
| 606 |
-
new_page.update(page)
|
| 607 |
-
page = new_page
|
| 608 |
-
page.merge_page(overlay_pdf.pages[0])
|
| 609 |
-
else:
|
| 610 |
-
# 记录日志并继续处理下一个页面
|
| 611 |
-
# logger.warning(f"span.pdf: 第{i + 1}页未能生成有效的overlay PDF")
|
| 612 |
-
pass
|
| 613 |
-
|
| 614 |
-
output_pdf.add_page(page)
|
| 615 |
-
|
| 616 |
-
# Save the PDF
|
| 617 |
-
with open(f"{out_path}/{filename}", "wb") as f:
|
| 618 |
-
output_pdf.write(f)
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
if __name__ == "__main__":
|
| 622 |
-
# 读取PDF文件
|
| 623 |
-
pdf_path = "examples/demo1.pdf"
|
| 624 |
-
with open(pdf_path, "rb") as f:
|
| 625 |
-
pdf_bytes = f.read()
|
| 626 |
-
|
| 627 |
-
# 从json文件读取pdf_info
|
| 628 |
-
|
| 629 |
-
json_path = "examples/demo1_1746005777.0863056_middle.json"
|
| 630 |
-
with open(json_path, "r", encoding="utf-8") as f:
|
| 631 |
-
pdf_ann = json.load(f)
|
| 632 |
-
pdf_info = pdf_ann["pdf_info"]
|
| 633 |
-
# 调用可视化函数,输出到examples目录
|
| 634 |
-
draw_layout_bbox(pdf_info, pdf_bytes, "examples", "output_with_layout.pdf")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/enum_class.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
| 1 |
-
from enum import Enum
|
| 2 |
-
|
| 3 |
-
class BlockType:
|
| 4 |
-
IMAGE = 'image'
|
| 5 |
-
TABLE = 'table'
|
| 6 |
-
IMAGE_BODY = 'image_body'
|
| 7 |
-
TABLE_BODY = 'table_body'
|
| 8 |
-
IMAGE_CAPTION = 'image_caption'
|
| 9 |
-
TABLE_CAPTION = 'table_caption'
|
| 10 |
-
IMAGE_FOOTNOTE = 'image_footnote'
|
| 11 |
-
TABLE_FOOTNOTE = 'table_footnote'
|
| 12 |
-
TEXT = 'text'
|
| 13 |
-
TITLE = 'title'
|
| 14 |
-
INTERLINE_EQUATION = 'interline_equation'
|
| 15 |
-
LIST = 'list'
|
| 16 |
-
INDEX = 'index'
|
| 17 |
-
DISCARDED = 'discarded'
|
| 18 |
-
|
| 19 |
-
CODE = "code"
|
| 20 |
-
CODE_BODY = "code_body"
|
| 21 |
-
CODE_CAPTION = "code_caption"
|
| 22 |
-
ALGORITHM = "algorithm"
|
| 23 |
-
REF_TEXT = "ref_text"
|
| 24 |
-
PHONETIC = "phonetic"
|
| 25 |
-
HEADER = "header"
|
| 26 |
-
FOOTER = "footer"
|
| 27 |
-
PAGE_NUMBER = "page_number"
|
| 28 |
-
ASIDE_TEXT = "aside_text"
|
| 29 |
-
PAGE_FOOTNOTE = "page_footnote"
|
| 30 |
-
SEAL = 'seal'
|
| 31 |
-
CHAR = 'char'
|
| 32 |
-
|
| 33 |
-
class ContentType:
|
| 34 |
-
IMAGE = 'image'
|
| 35 |
-
TABLE = 'table'
|
| 36 |
-
TEXT = 'text'
|
| 37 |
-
INTERLINE_EQUATION = 'interline_equation'
|
| 38 |
-
INLINE_EQUATION = 'inline_equation'
|
| 39 |
-
EQUATION = 'equation'
|
| 40 |
-
CODE = 'code'
|
| 41 |
-
SEAL = 'seal'
|
| 42 |
-
CHAR = 'char'
|
| 43 |
-
|
| 44 |
-
class SplitFlag:
|
| 45 |
-
CROSS_PAGE = 'cross_page'
|
| 46 |
-
LINES_DELETED = 'lines_deleted'
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
class ImageType:
|
| 50 |
-
PIL = 'pil_img'
|
| 51 |
-
BASE64 = 'base64_img'
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
class NotExtractType(Enum):
|
| 55 |
-
TEXT = BlockType.TEXT
|
| 56 |
-
TITLE = BlockType.TITLE
|
| 57 |
-
HEADER = BlockType.HEADER
|
| 58 |
-
FOOTER = BlockType.FOOTER
|
| 59 |
-
PAGE_NUMBER = BlockType.PAGE_NUMBER
|
| 60 |
-
PAGE_FOOTNOTE = BlockType.PAGE_FOOTNOTE
|
| 61 |
-
REF_TEXT = BlockType.REF_TEXT
|
| 62 |
-
TABLE_CAPTION = BlockType.TABLE_CAPTION
|
| 63 |
-
IMAGE_CAPTION = BlockType.IMAGE_CAPTION
|
| 64 |
-
TABLE_FOOTNOTE = BlockType.TABLE_FOOTNOTE
|
| 65 |
-
IMAGE_FOOTNOTE = BlockType.IMAGE_FOOTNOTE
|
| 66 |
-
CODE_CAPTION = BlockType.CODE_CAPTION
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/guess_suffix_or_lang.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
-
|
| 3 |
-
from loguru import logger
|
| 4 |
-
from magika import Magika
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
DEFAULT_LANG = "txt"
|
| 8 |
-
PDF_SIG_BYTES = b'%PDF'
|
| 9 |
-
magika = Magika()
|
| 10 |
-
|
| 11 |
-
def code_content_clean(content):
|
| 12 |
-
if not content:
|
| 13 |
-
return ""
|
| 14 |
-
lines = content.splitlines()
|
| 15 |
-
start_idx = 0
|
| 16 |
-
end_idx = len(lines)
|
| 17 |
-
if lines and lines[0].startswith("```"):
|
| 18 |
-
start_idx = 1
|
| 19 |
-
if lines and end_idx > start_idx and lines[end_idx - 1].strip() == "```":
|
| 20 |
-
end_idx -= 1
|
| 21 |
-
if start_idx < end_idx:
|
| 22 |
-
return "\n".join(lines[start_idx:end_idx]).strip()
|
| 23 |
-
return ""
|
| 24 |
-
|
| 25 |
-
def guess_language_by_text(code):
|
| 26 |
-
code = code_content_clean(code)
|
| 27 |
-
if code.startswith("<_"):
|
| 28 |
-
end = code.find("_>")
|
| 29 |
-
if end != -1:
|
| 30 |
-
lang = code[2:end].lower()
|
| 31 |
-
code = code[end + 2:]
|
| 32 |
-
return lang, code
|
| 33 |
-
|
| 34 |
-
return (lang if lang != "unknown" else DEFAULT_LANG), code
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def guess_suffix_by_bytes(file_bytes, file_path=None) -> str:
|
| 38 |
-
suffix = magika.identify_bytes(file_bytes).prediction.output.label
|
| 39 |
-
if file_path and suffix in ["ai", "html"] and Path(file_path).suffix.lower() in [".pdf"] and file_bytes[:4] == PDF_SIG_BYTES:
|
| 40 |
-
suffix = "pdf"
|
| 41 |
-
return suffix
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def guess_suffix_by_path(file_path) -> str:
|
| 45 |
-
if not isinstance(file_path, Path):
|
| 46 |
-
file_path = Path(file_path)
|
| 47 |
-
suffix = magika.identify_path(file_path).prediction.output.label
|
| 48 |
-
if suffix in ["ai", "html"] and file_path.suffix.lower() in [".pdf"]:
|
| 49 |
-
try:
|
| 50 |
-
with open(file_path, 'rb') as f:
|
| 51 |
-
if f.read(4) == PDF_SIG_BYTES:
|
| 52 |
-
suffix = "pdf"
|
| 53 |
-
except Exception as e:
|
| 54 |
-
logger.warning(f"Failed to read file {file_path} for PDF signature check: {e}")
|
| 55 |
-
return suffix
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/hash_utils.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import hashlib
|
| 3 |
-
import json
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
def bytes_md5(file_bytes):
|
| 7 |
-
hasher = hashlib.md5()
|
| 8 |
-
hasher.update(file_bytes)
|
| 9 |
-
return hasher.hexdigest().upper()
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
def str_md5(input_string):
|
| 13 |
-
hasher = hashlib.md5()
|
| 14 |
-
# 在Python3中,需要将字符串转化为字节对象才能被哈希函数处理
|
| 15 |
-
input_bytes = input_string.encode('utf-8')
|
| 16 |
-
hasher.update(input_bytes)
|
| 17 |
-
return hasher.hexdigest()
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def str_sha256(input_string):
|
| 21 |
-
hasher = hashlib.sha256()
|
| 22 |
-
# 在Python3中,需要将字符串转化为字节对象才能被哈希函数处理
|
| 23 |
-
input_bytes = input_string.encode('utf-8')
|
| 24 |
-
hasher.update(input_bytes)
|
| 25 |
-
return hasher.hexdigest()
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def dict_md5(d):
|
| 29 |
-
json_str = json.dumps(d, sort_keys=True, ensure_ascii=False)
|
| 30 |
-
return hashlib.md5(json_str.encode('utf-8')).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/language.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import unicodedata
|
| 3 |
-
|
| 4 |
-
if not os.getenv("FTLANG_CACHE"):
|
| 5 |
-
current_file_path = os.path.abspath(__file__)
|
| 6 |
-
current_dir = os.path.dirname(current_file_path)
|
| 7 |
-
root_dir = os.path.dirname(current_dir)
|
| 8 |
-
ftlang_cache_dir = os.path.join(root_dir, 'resources', 'fasttext-langdetect')
|
| 9 |
-
os.environ["FTLANG_CACHE"] = str(ftlang_cache_dir)
|
| 10 |
-
# print(os.getenv("FTLANG_CACHE"))
|
| 11 |
-
|
| 12 |
-
from fast_langdetect import detect_language
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def remove_invalid_surrogates(text):
|
| 16 |
-
# 移除无效的 UTF-16 代理对
|
| 17 |
-
return ''.join(c for c in text if not (0xD800 <= ord(c) <= 0xDFFF))
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def detect_lang(text: str, MAX_LANG_LEN = 128) -> str:
|
| 21 |
-
|
| 22 |
-
if not text:
|
| 23 |
-
return ""
|
| 24 |
-
|
| 25 |
-
text = text.replace("\n", "")
|
| 26 |
-
text = remove_invalid_surrogates(text)
|
| 27 |
-
|
| 28 |
-
text = text[:MAX_LANG_LEN]
|
| 29 |
-
try:
|
| 30 |
-
lang_upper = detect_language(text)
|
| 31 |
-
except:
|
| 32 |
-
html_no_ctrl_chars = ''.join([l for l in text if unicodedata.category(l)[0] not in ['C', ]])
|
| 33 |
-
lang_upper = detect_language(html_no_ctrl_chars)
|
| 34 |
-
|
| 35 |
-
try:
|
| 36 |
-
lang = lang_upper.lower()
|
| 37 |
-
except:
|
| 38 |
-
lang = ""
|
| 39 |
-
return lang
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
if __name__ == '__main__':
|
| 43 |
-
print(os.getenv("FTLANG_CACHE"))
|
| 44 |
-
print(detect_lang("This is a test."*100000))
|
| 45 |
-
print(detect_lang("<html>This is a test</html>"))
|
| 46 |
-
print(detect_lang("这个是中文测试。"))
|
| 47 |
-
print(detect_lang("<html>这个是中文测试。</html>"))
|
| 48 |
-
print(detect_lang("〖\ud835\udc46\ud835〗这是个包含utf-16的中文测试"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/magic_model_utils.py
DELETED
|
@@ -1,251 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
包含两个MagicModel类中重复使用的方法和逻辑
|
| 3 |
-
"""
|
| 4 |
-
from typing import List, Dict, Any, Callable
|
| 5 |
-
from NaviOCR.tools.boxbase import bbox_distance, bbox_center_distance, is_in
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
def reduct_overlap(bboxes: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 9 |
-
"""
|
| 10 |
-
去除重叠的bbox,保留不被其他bbox包含的bbox
|
| 11 |
-
|
| 12 |
-
Args:
|
| 13 |
-
bboxes: 包含bbox信息的字典列表
|
| 14 |
-
|
| 15 |
-
Returns:
|
| 16 |
-
去重后的bbox列表
|
| 17 |
-
"""
|
| 18 |
-
N = len(bboxes)
|
| 19 |
-
keep = [True] * N
|
| 20 |
-
for i in range(N):
|
| 21 |
-
for j in range(N):
|
| 22 |
-
if i == j:
|
| 23 |
-
continue
|
| 24 |
-
if is_in(bboxes[i]['bbox'], bboxes[j]['bbox']):
|
| 25 |
-
keep[i] = False
|
| 26 |
-
return [bboxes[i] for i in range(N) if keep[i]]
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def tie_up_category_by_distance_v3(
|
| 30 |
-
get_subjects_func: Callable,
|
| 31 |
-
get_objects_func: Callable,
|
| 32 |
-
extract_subject_func: Callable = None,
|
| 33 |
-
extract_object_func: Callable = None
|
| 34 |
-
):
|
| 35 |
-
"""
|
| 36 |
-
通用的类别关联方法,用于将主体对象与客体对象进行关联
|
| 37 |
-
|
| 38 |
-
参数:
|
| 39 |
-
get_subjects_func: 函数,提取主体对象
|
| 40 |
-
get_objects_func: 函数,提取客体对象
|
| 41 |
-
extract_subject_func: 函数,自定义提取主体属性(默认使用bbox和其他属性)
|
| 42 |
-
extract_object_func: 函数,自定义提取客体属性(默认使用bbox和其他属性)
|
| 43 |
-
|
| 44 |
-
返回:
|
| 45 |
-
关联后的对象列表
|
| 46 |
-
"""
|
| 47 |
-
subjects = get_subjects_func()
|
| 48 |
-
objects = get_objects_func()
|
| 49 |
-
|
| 50 |
-
# 如果没有提供自定义提取函数,使用默认函数
|
| 51 |
-
if extract_subject_func is None:
|
| 52 |
-
extract_subject_func = lambda x: x
|
| 53 |
-
if extract_object_func is None:
|
| 54 |
-
extract_object_func = lambda x: x
|
| 55 |
-
|
| 56 |
-
ret = []
|
| 57 |
-
N, M = len(subjects), len(objects)
|
| 58 |
-
subjects.sort(key=lambda x: x["bbox"][0] ** 2 + x["bbox"][1] ** 2)
|
| 59 |
-
objects.sort(key=lambda x: x["bbox"][0] ** 2 + x["bbox"][1] ** 2)
|
| 60 |
-
|
| 61 |
-
OBJ_IDX_OFFSET = 10000
|
| 62 |
-
SUB_BIT_KIND, OBJ_BIT_KIND = 0, 1
|
| 63 |
-
|
| 64 |
-
all_boxes_with_idx = [(i, SUB_BIT_KIND, sub["bbox"][0], sub["bbox"][1]) for i, sub in enumerate(subjects)] + [
|
| 65 |
-
(i + OBJ_IDX_OFFSET, OBJ_BIT_KIND, obj["bbox"][0], obj["bbox"][1]) for i, obj in enumerate(objects)
|
| 66 |
-
]
|
| 67 |
-
seen_idx = set()
|
| 68 |
-
seen_sub_idx = set()
|
| 69 |
-
|
| 70 |
-
while N > len(seen_sub_idx):
|
| 71 |
-
candidates = []
|
| 72 |
-
for idx, kind, x0, y0 in all_boxes_with_idx:
|
| 73 |
-
if idx in seen_idx:
|
| 74 |
-
continue
|
| 75 |
-
candidates.append((idx, kind, x0, y0))
|
| 76 |
-
|
| 77 |
-
if len(candidates) == 0:
|
| 78 |
-
break
|
| 79 |
-
left_x = min([v[2] for v in candidates])
|
| 80 |
-
top_y = min([v[3] for v in candidates])
|
| 81 |
-
|
| 82 |
-
candidates.sort(key=lambda x: (x[2] - left_x) ** 2 + (x[3] - top_y) ** 2)
|
| 83 |
-
|
| 84 |
-
fst_idx, fst_kind, left_x, top_y = candidates[0]
|
| 85 |
-
fst_bbox = subjects[fst_idx]['bbox'] if fst_kind == SUB_BIT_KIND else objects[fst_idx - OBJ_IDX_OFFSET]['bbox']
|
| 86 |
-
candidates.sort(
|
| 87 |
-
key=lambda x: bbox_distance(fst_bbox, subjects[x[0]]['bbox']) if x[1] == SUB_BIT_KIND else bbox_distance(
|
| 88 |
-
fst_bbox, objects[x[0] - OBJ_IDX_OFFSET]['bbox']))
|
| 89 |
-
nxt = None
|
| 90 |
-
|
| 91 |
-
for i in range(1, len(candidates)):
|
| 92 |
-
if candidates[i][1] ^ fst_kind == 1:
|
| 93 |
-
nxt = candidates[i]
|
| 94 |
-
break
|
| 95 |
-
if nxt is None:
|
| 96 |
-
break
|
| 97 |
-
|
| 98 |
-
if fst_kind == SUB_BIT_KIND:
|
| 99 |
-
sub_idx, obj_idx = fst_idx, nxt[0] - OBJ_IDX_OFFSET
|
| 100 |
-
else:
|
| 101 |
-
sub_idx, obj_idx = nxt[0], fst_idx - OBJ_IDX_OFFSET
|
| 102 |
-
|
| 103 |
-
pair_dis = bbox_distance(subjects[sub_idx]["bbox"], objects[obj_idx]["bbox"])
|
| 104 |
-
nearest_dis = float("inf")
|
| 105 |
-
for i in range(N):
|
| 106 |
-
# 取消原先算法中 1对1 匹配的偏置
|
| 107 |
-
# if i in seen_idx or i == sub_idx:continue
|
| 108 |
-
nearest_dis = min(nearest_dis, bbox_distance(subjects[i]["bbox"], objects[obj_idx]["bbox"]))
|
| 109 |
-
|
| 110 |
-
if pair_dis >= 3 * nearest_dis:
|
| 111 |
-
seen_idx.add(sub_idx)
|
| 112 |
-
continue
|
| 113 |
-
|
| 114 |
-
seen_idx.add(sub_idx)
|
| 115 |
-
seen_idx.add(obj_idx + OBJ_IDX_OFFSET)
|
| 116 |
-
seen_sub_idx.add(sub_idx)
|
| 117 |
-
|
| 118 |
-
ret.append(
|
| 119 |
-
{
|
| 120 |
-
"sub_bbox": extract_subject_func(subjects[sub_idx]),
|
| 121 |
-
"obj_bboxes": [extract_object_func(objects[obj_idx])],
|
| 122 |
-
"sub_idx": sub_idx,
|
| 123 |
-
}
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
for i in range(len(objects)):
|
| 127 |
-
j = i + OBJ_IDX_OFFSET
|
| 128 |
-
if j in seen_idx:
|
| 129 |
-
continue
|
| 130 |
-
seen_idx.add(j)
|
| 131 |
-
nearest_dis, nearest_sub_idx = float("inf"), -1
|
| 132 |
-
for k in range(len(subjects)):
|
| 133 |
-
dis = bbox_distance(objects[i]["bbox"], subjects[k]["bbox"])
|
| 134 |
-
if dis < nearest_dis:
|
| 135 |
-
nearest_dis = dis
|
| 136 |
-
nearest_sub_idx = k
|
| 137 |
-
|
| 138 |
-
for k in range(len(subjects)):
|
| 139 |
-
if k != nearest_sub_idx:
|
| 140 |
-
continue
|
| 141 |
-
if k in seen_sub_idx:
|
| 142 |
-
for kk in range(len(ret)):
|
| 143 |
-
if ret[kk]["sub_idx"] == k:
|
| 144 |
-
ret[kk]["obj_bboxes"].append(extract_object_func(objects[i]))
|
| 145 |
-
break
|
| 146 |
-
else:
|
| 147 |
-
ret.append(
|
| 148 |
-
{
|
| 149 |
-
"sub_bbox": extract_subject_func(subjects[k]),
|
| 150 |
-
"obj_bboxes": [extract_object_func(objects[i])],
|
| 151 |
-
"sub_idx": k,
|
| 152 |
-
}
|
| 153 |
-
)
|
| 154 |
-
seen_sub_idx.add(k)
|
| 155 |
-
seen_idx.add(k)
|
| 156 |
-
|
| 157 |
-
for i in range(len(subjects)):
|
| 158 |
-
if i in seen_sub_idx:
|
| 159 |
-
continue
|
| 160 |
-
ret.append(
|
| 161 |
-
{
|
| 162 |
-
"sub_bbox": extract_subject_func(subjects[i]),
|
| 163 |
-
"obj_bboxes": [],
|
| 164 |
-
"sub_idx": i,
|
| 165 |
-
}
|
| 166 |
-
)
|
| 167 |
-
|
| 168 |
-
return ret
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
def tie_up_category_by_index(
|
| 172 |
-
get_subjects_func: Callable,
|
| 173 |
-
get_objects_func: Callable,
|
| 174 |
-
extract_subject_func: Callable = None,
|
| 175 |
-
extract_object_func: Callable = None
|
| 176 |
-
):
|
| 177 |
-
"""
|
| 178 |
-
基于index的类别关联方法,用于将主体对象与客体对象进行关联
|
| 179 |
-
客体优先匹配给index最接近的主体,index差值相同时使用bbox中心点距离作为tiebreaker
|
| 180 |
-
|
| 181 |
-
参数:
|
| 182 |
-
get_subjects_func: 函数,提取主体对象
|
| 183 |
-
get_objects_func: 函数,提取客体对象
|
| 184 |
-
extract_subject_func: 函数,自定义提取主体属性(默认使用bbox和其他属性)
|
| 185 |
-
extract_object_func: 函数,自定义提取客体属性(默认使用bbox和其他属性)
|
| 186 |
-
|
| 187 |
-
返回:
|
| 188 |
-
关联后的对象列表,按主体index升序排列
|
| 189 |
-
"""
|
| 190 |
-
subjects = get_subjects_func()
|
| 191 |
-
objects = get_objects_func()
|
| 192 |
-
|
| 193 |
-
# 如果没有提供自定义提取函数,使用默认函数
|
| 194 |
-
if extract_subject_func is None:
|
| 195 |
-
extract_subject_func = lambda x: x
|
| 196 |
-
if extract_object_func is None:
|
| 197 |
-
extract_object_func = lambda x: x
|
| 198 |
-
|
| 199 |
-
# 初始化结果字典,key为主体索引,value为关联信息
|
| 200 |
-
result_dict = {}
|
| 201 |
-
|
| 202 |
-
# 初始化所有主体
|
| 203 |
-
for i, subject in enumerate(subjects):
|
| 204 |
-
result_dict[i] = {
|
| 205 |
-
"sub_bbox": extract_subject_func(subject),
|
| 206 |
-
"obj_bboxes": [],
|
| 207 |
-
"sub_idx": i,
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
# 为每个客体找到最匹配的主体
|
| 211 |
-
for obj in objects:
|
| 212 |
-
if len(subjects) == 0:
|
| 213 |
-
# 如果没有主体,跳过客体
|
| 214 |
-
continue
|
| 215 |
-
|
| 216 |
-
obj_index = obj["index"]
|
| 217 |
-
min_index_diff = float("inf")
|
| 218 |
-
best_subject_indices = []
|
| 219 |
-
|
| 220 |
-
# 找出index差值最小的所有主体
|
| 221 |
-
for i, subject in enumerate(subjects):
|
| 222 |
-
sub_index = subject["index"]
|
| 223 |
-
index_diff = abs(obj_index - sub_index)
|
| 224 |
-
|
| 225 |
-
if index_diff < min_index_diff:
|
| 226 |
-
min_index_diff = index_diff
|
| 227 |
-
best_subject_indices = [i]
|
| 228 |
-
elif index_diff == min_index_diff:
|
| 229 |
-
best_subject_indices.append(i)
|
| 230 |
-
|
| 231 |
-
# 如果有多个主体的index差值相同,使用中心点距离作为tiebreaker
|
| 232 |
-
if len(best_subject_indices) > 1:
|
| 233 |
-
min_center_dist = float("inf")
|
| 234 |
-
best_subject_idx = best_subject_indices[0]
|
| 235 |
-
|
| 236 |
-
for idx in best_subject_indices:
|
| 237 |
-
center_dist = bbox_center_distance(obj["bbox"], subjects[idx]["bbox"])
|
| 238 |
-
if center_dist < min_center_dist:
|
| 239 |
-
min_center_dist = center_dist
|
| 240 |
-
best_subject_idx = idx
|
| 241 |
-
else:
|
| 242 |
-
best_subject_idx = best_subject_indices[0]
|
| 243 |
-
|
| 244 |
-
# 将客体添加到最佳主体的obj_bboxes中
|
| 245 |
-
result_dict[best_subject_idx]["obj_bboxes"].append(extract_object_func(obj))
|
| 246 |
-
|
| 247 |
-
# 转换为列表并按主体index排序
|
| 248 |
-
ret = list(result_dict.values())
|
| 249 |
-
ret.sort(key=lambda x: x["sub_idx"])
|
| 250 |
-
|
| 251 |
-
return ret
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/model_utils.py
DELETED
|
@@ -1,462 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import time
|
| 3 |
-
import gc
|
| 4 |
-
from PIL import Image
|
| 5 |
-
from loguru import logger
|
| 6 |
-
import numpy as np
|
| 7 |
-
|
| 8 |
-
from NaviOCR.tools.boxbase import get_minbox_if_overlap_by_ratio
|
| 9 |
-
|
| 10 |
-
try:
|
| 11 |
-
import torch
|
| 12 |
-
import torch_npu
|
| 13 |
-
except ImportError:
|
| 14 |
-
pass
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def crop_img(input_res, input_img, crop_paste_x=0, crop_paste_y=0):
|
| 18 |
-
|
| 19 |
-
crop_xmin, crop_ymin = int(input_res['poly'][0]), int(input_res['poly'][1])
|
| 20 |
-
crop_xmax, crop_ymax = int(input_res['poly'][4]), int(input_res['poly'][5])
|
| 21 |
-
|
| 22 |
-
# Calculate new dimensions
|
| 23 |
-
crop_new_width = crop_xmax - crop_xmin + crop_paste_x * 2
|
| 24 |
-
crop_new_height = crop_ymax - crop_ymin + crop_paste_y * 2
|
| 25 |
-
|
| 26 |
-
if isinstance(input_img, np.ndarray):
|
| 27 |
-
|
| 28 |
-
# Create a white background array
|
| 29 |
-
return_image = np.ones((crop_new_height, crop_new_width, 3), dtype=np.uint8) * 255
|
| 30 |
-
|
| 31 |
-
# Crop the original image using numpy slicing
|
| 32 |
-
cropped_img = input_img[crop_ymin:crop_ymax, crop_xmin:crop_xmax]
|
| 33 |
-
|
| 34 |
-
# Paste the cropped image onto the white background
|
| 35 |
-
return_image[crop_paste_y:crop_paste_y + (crop_ymax - crop_ymin),
|
| 36 |
-
crop_paste_x:crop_paste_x + (crop_xmax - crop_xmin)] = cropped_img
|
| 37 |
-
else:
|
| 38 |
-
# Create a white background array
|
| 39 |
-
return_image = Image.new('RGB', (crop_new_width, crop_new_height), 'white')
|
| 40 |
-
# Crop image
|
| 41 |
-
crop_box = (crop_xmin, crop_ymin, crop_xmax, crop_ymax)
|
| 42 |
-
cropped_img = input_img.crop(crop_box)
|
| 43 |
-
return_image.paste(cropped_img, (crop_paste_x, crop_paste_y))
|
| 44 |
-
|
| 45 |
-
return_list = [crop_paste_x, crop_paste_y, crop_xmin, crop_ymin, crop_xmax, crop_ymax, crop_new_width,
|
| 46 |
-
crop_new_height]
|
| 47 |
-
return return_image, return_list
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def get_coords_and_area(block_with_poly):
|
| 51 |
-
"""Extract coordinates and area from a table."""
|
| 52 |
-
xmin, ymin = int(block_with_poly['poly'][0]), int(block_with_poly['poly'][1])
|
| 53 |
-
xmax, ymax = int(block_with_poly['poly'][4]), int(block_with_poly['poly'][5])
|
| 54 |
-
area = (xmax - xmin) * (ymax - ymin)
|
| 55 |
-
return xmin, ymin, xmax, ymax, area
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def calculate_intersection(box1, box2):
|
| 59 |
-
"""Calculate intersection coordinates between two boxes."""
|
| 60 |
-
intersection_xmin = max(box1[0], box2[0])
|
| 61 |
-
intersection_ymin = max(box1[1], box2[1])
|
| 62 |
-
intersection_xmax = min(box1[2], box2[2])
|
| 63 |
-
intersection_ymax = min(box1[3], box2[3])
|
| 64 |
-
|
| 65 |
-
# Check if intersection is valid
|
| 66 |
-
if intersection_xmax <= intersection_xmin or intersection_ymax <= intersection_ymin:
|
| 67 |
-
return None
|
| 68 |
-
|
| 69 |
-
return intersection_xmin, intersection_ymin, intersection_xmax, intersection_ymax
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def calculate_iou(box1, box2):
|
| 73 |
-
"""Calculate IoU between two boxes."""
|
| 74 |
-
intersection = calculate_intersection(box1[:4], box2[:4])
|
| 75 |
-
|
| 76 |
-
if not intersection:
|
| 77 |
-
return 0
|
| 78 |
-
|
| 79 |
-
intersection_xmin, intersection_ymin, intersection_xmax, intersection_ymax = intersection
|
| 80 |
-
intersection_area = (intersection_xmax - intersection_xmin) * (intersection_ymax - intersection_ymin)
|
| 81 |
-
|
| 82 |
-
area1, area2 = box1[4], box2[4]
|
| 83 |
-
union_area = area1 + area2 - intersection_area
|
| 84 |
-
|
| 85 |
-
return intersection_area / union_area if union_area > 0 else 0
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def is_inside(small_box, big_box, overlap_threshold=0.8):
|
| 89 |
-
"""Check if small_box is inside big_box by at least overlap_threshold."""
|
| 90 |
-
intersection = calculate_intersection(small_box[:4], big_box[:4])
|
| 91 |
-
|
| 92 |
-
if not intersection:
|
| 93 |
-
return False
|
| 94 |
-
|
| 95 |
-
intersection_xmin, intersection_ymin, intersection_xmax, intersection_ymax = intersection
|
| 96 |
-
intersection_area = (intersection_xmax - intersection_xmin) * (intersection_ymax - intersection_ymin)
|
| 97 |
-
|
| 98 |
-
# Check if overlap exceeds threshold
|
| 99 |
-
return intersection_area >= overlap_threshold * small_box[4]
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def do_overlap(box1, box2):
|
| 103 |
-
"""Check if two boxes overlap."""
|
| 104 |
-
return calculate_intersection(box1[:4], box2[:4]) is not None
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def merge_high_iou_tables(table_res_list, layout_res, table_indices, iou_threshold=0.7):
|
| 108 |
-
"""Merge tables with IoU > threshold."""
|
| 109 |
-
if len(table_res_list) < 2:
|
| 110 |
-
return table_res_list, table_indices
|
| 111 |
-
|
| 112 |
-
table_info = [get_coords_and_area(table) for table in table_res_list]
|
| 113 |
-
merged = True
|
| 114 |
-
|
| 115 |
-
while merged:
|
| 116 |
-
merged = False
|
| 117 |
-
i = 0
|
| 118 |
-
while i < len(table_res_list) - 1:
|
| 119 |
-
j = i + 1
|
| 120 |
-
while j < len(table_res_list):
|
| 121 |
-
iou = calculate_iou(table_info[i], table_info[j])
|
| 122 |
-
|
| 123 |
-
if iou > iou_threshold:
|
| 124 |
-
# Merge tables by taking their union
|
| 125 |
-
x1_min, y1_min, x1_max, y1_max, _ = table_info[i]
|
| 126 |
-
x2_min, y2_min, x2_max, y2_max, _ = table_info[j]
|
| 127 |
-
|
| 128 |
-
union_xmin = min(x1_min, x2_min)
|
| 129 |
-
union_ymin = min(y1_min, y2_min)
|
| 130 |
-
union_xmax = max(x1_max, x2_max)
|
| 131 |
-
union_ymax = max(y1_max, y2_max)
|
| 132 |
-
|
| 133 |
-
# Create merged table
|
| 134 |
-
merged_table = table_res_list[i].copy()
|
| 135 |
-
merged_table['poly'] = [
|
| 136 |
-
union_xmin, union_ymin, union_xmax, union_ymin,
|
| 137 |
-
union_xmax, union_ymax, union_xmin, union_ymax
|
| 138 |
-
]
|
| 139 |
-
# Update layout_res
|
| 140 |
-
to_remove = [table_indices[j], table_indices[i]]
|
| 141 |
-
for idx in sorted(to_remove, reverse=True):
|
| 142 |
-
del layout_res[idx]
|
| 143 |
-
layout_res.append(merged_table)
|
| 144 |
-
|
| 145 |
-
# Update tracking lists
|
| 146 |
-
table_indices = [k if k < min(to_remove) else
|
| 147 |
-
k - 1 if k < max(to_remove) else
|
| 148 |
-
k - 2 if k > max(to_remove) else
|
| 149 |
-
len(layout_res) - 1
|
| 150 |
-
for k in table_indices
|
| 151 |
-
if k not in to_remove]
|
| 152 |
-
table_indices.append(len(layout_res) - 1)
|
| 153 |
-
|
| 154 |
-
# Update table lists
|
| 155 |
-
table_res_list.pop(j)
|
| 156 |
-
table_res_list.pop(i)
|
| 157 |
-
table_res_list.append(merged_table)
|
| 158 |
-
|
| 159 |
-
# Update table_info
|
| 160 |
-
table_info = [get_coords_and_area(table) for table in table_res_list]
|
| 161 |
-
|
| 162 |
-
merged = True
|
| 163 |
-
break
|
| 164 |
-
j += 1
|
| 165 |
-
|
| 166 |
-
if merged:
|
| 167 |
-
break
|
| 168 |
-
i += 1
|
| 169 |
-
|
| 170 |
-
return table_res_list, table_indices
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
def filter_nested_tables(table_res_list, overlap_threshold=0.8, area_threshold=0.8):
|
| 174 |
-
"""Remove big tables containing multiple smaller tables within them."""
|
| 175 |
-
if len(table_res_list) < 3:
|
| 176 |
-
return table_res_list
|
| 177 |
-
|
| 178 |
-
table_info = [get_coords_and_area(table) for table in table_res_list]
|
| 179 |
-
big_tables_idx = []
|
| 180 |
-
|
| 181 |
-
for i in range(len(table_res_list)):
|
| 182 |
-
# Find tables inside this one
|
| 183 |
-
tables_inside = [j for j in range(len(table_res_list))
|
| 184 |
-
if i != j and is_inside(table_info[j], table_info[i], overlap_threshold)]
|
| 185 |
-
|
| 186 |
-
# Continue if there are at least 3 tables inside
|
| 187 |
-
if len(tables_inside) >= 3:
|
| 188 |
-
# Check if inside tables overlap with each other
|
| 189 |
-
tables_overlap = any(do_overlap(table_info[tables_inside[idx1]], table_info[tables_inside[idx2]])
|
| 190 |
-
for idx1 in range(len(tables_inside))
|
| 191 |
-
for idx2 in range(idx1 + 1, len(tables_inside)))
|
| 192 |
-
|
| 193 |
-
# If no overlaps, check area condition
|
| 194 |
-
if not tables_overlap:
|
| 195 |
-
total_inside_area = sum(table_info[j][4] for j in tables_inside)
|
| 196 |
-
big_table_area = table_info[i][4]
|
| 197 |
-
|
| 198 |
-
if total_inside_area > area_threshold * big_table_area:
|
| 199 |
-
big_tables_idx.append(i)
|
| 200 |
-
|
| 201 |
-
return [table for i, table in enumerate(table_res_list) if i not in big_tables_idx]
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
def remove_overlaps_min_blocks(res_list):
|
| 205 |
-
|
| 206 |
-
for res in res_list:
|
| 207 |
-
res['bbox'] = [int(res['poly'][0]), int(res['poly'][1]), int(res['poly'][4]), int(res['poly'][5])]
|
| 208 |
-
|
| 209 |
-
# 重叠block,小的不能直接删除,需要和大的那个合并成一个更大的。
|
| 210 |
-
# 删除重叠blocks中较小的那些
|
| 211 |
-
need_remove = []
|
| 212 |
-
for i in range(len(res_list)):
|
| 213 |
-
# 如果当前元素已在需要移除列表中,则跳过
|
| 214 |
-
if res_list[i] in need_remove:
|
| 215 |
-
continue
|
| 216 |
-
|
| 217 |
-
for j in range(i + 1, len(res_list)):
|
| 218 |
-
# 如果比较对象已在需要移除列表中,则跳过
|
| 219 |
-
if res_list[j] in need_remove:
|
| 220 |
-
continue
|
| 221 |
-
|
| 222 |
-
overlap_box = get_minbox_if_overlap_by_ratio(
|
| 223 |
-
res_list[i]['bbox'], res_list[j]['bbox'], 0.8
|
| 224 |
-
)
|
| 225 |
-
|
| 226 |
-
if overlap_box is not None:
|
| 227 |
-
|
| 228 |
-
# 根据重叠框确定哪个是小块,哪个是大块
|
| 229 |
-
if overlap_box == res_list[i]['bbox']:
|
| 230 |
-
small_res, large_res = res_list[i], res_list[j]
|
| 231 |
-
elif overlap_box == res_list[j]['bbox']:
|
| 232 |
-
small_res, large_res = res_list[j], res_list[i]
|
| 233 |
-
else:
|
| 234 |
-
continue # 如果重叠框与任一块都不匹配,跳过处理
|
| 235 |
-
|
| 236 |
-
if small_res['score'] <= large_res['score']:
|
| 237 |
-
# 如果小块的分数低于大块,则小块为需要移除的块
|
| 238 |
-
if small_res is not None and small_res not in need_remove:
|
| 239 |
-
# 更新大块的边界为两者的并集
|
| 240 |
-
x1, y1, x2, y2 = large_res['bbox']
|
| 241 |
-
sx1, sy1, sx2, sy2 = small_res['bbox']
|
| 242 |
-
x1 = min(x1, sx1)
|
| 243 |
-
y1 = min(y1, sy1)
|
| 244 |
-
x2 = max(x2, sx2)
|
| 245 |
-
y2 = max(y2, sy2)
|
| 246 |
-
large_res['bbox'] = [x1, y1, x2, y2]
|
| 247 |
-
need_remove.append(small_res)
|
| 248 |
-
else:
|
| 249 |
-
# 如果大块的分数低于小块,则大块为需要移除的块, 这时不需要更新小块的边界
|
| 250 |
-
if large_res is not None and large_res not in need_remove:
|
| 251 |
-
need_remove.append(large_res)
|
| 252 |
-
|
| 253 |
-
# 从列表中移除标记的元素
|
| 254 |
-
for res in need_remove:
|
| 255 |
-
res_list.remove(res)
|
| 256 |
-
del res['bbox'] # 删除bbox字段
|
| 257 |
-
|
| 258 |
-
for res in res_list:
|
| 259 |
-
# 将res的poly使用bbox重构
|
| 260 |
-
res['poly'] = [res['bbox'][0], res['bbox'][1], res['bbox'][2], res['bbox'][1],
|
| 261 |
-
res['bbox'][2], res['bbox'][3], res['bbox'][0], res['bbox'][3]]
|
| 262 |
-
# 删除res的bbox
|
| 263 |
-
del res['bbox']
|
| 264 |
-
|
| 265 |
-
return res_list, need_remove
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
def remove_overlaps_low_confidence_blocks(combined_res_list, overlap_threshold=0.8):
|
| 269 |
-
"""
|
| 270 |
-
Remove low-confidence blocks that overlap with other blocks.
|
| 271 |
-
|
| 272 |
-
This function identifies and removes blocks with low confidence scores that overlap
|
| 273 |
-
with other blocks. It calculates the coordinates and area of each block, and checks
|
| 274 |
-
for overlaps based on a specified threshold. Blocks that meet the criteria for removal
|
| 275 |
-
are returned in a list.
|
| 276 |
-
|
| 277 |
-
Parameters:
|
| 278 |
-
combined_res_list (list): A list of blocks, where each block is a dictionary containing
|
| 279 |
-
keys like 'poly' (polygon coordinates) and optionally 'score' (confidence score).
|
| 280 |
-
overlap_threshold (float): The threshold for determining overlap between blocks. Default is 0.8.
|
| 281 |
-
|
| 282 |
-
Returns:
|
| 283 |
-
list: A list of blocks to be removed, based on the overlap and confidence criteria.
|
| 284 |
-
"""
|
| 285 |
-
# 计算每个block的坐标和面积
|
| 286 |
-
block_info = []
|
| 287 |
-
for block in combined_res_list:
|
| 288 |
-
xmin, ymin = int(block['poly'][0]), int(block['poly'][1])
|
| 289 |
-
xmax, ymax = int(block['poly'][4]), int(block['poly'][5])
|
| 290 |
-
area = (xmax - xmin) * (ymax - ymin)
|
| 291 |
-
score = block.get('score', 0.5) # 如果没有score字段,默认为0.5
|
| 292 |
-
block_info.append((xmin, ymin, xmax, ymax, area, score, block))
|
| 293 |
-
|
| 294 |
-
blocks_to_remove = []
|
| 295 |
-
marked_indices = set() # 跟踪已标记为删除的block索引
|
| 296 |
-
|
| 297 |
-
# 检查每个block内部是否有3个及以上的小block
|
| 298 |
-
for i, (xmin, ymin, xmax, ymax, area, score, block) in enumerate(block_info):
|
| 299 |
-
# 如果当前block已标记为删除,则跳过
|
| 300 |
-
if i in marked_indices:
|
| 301 |
-
continue
|
| 302 |
-
|
| 303 |
-
# 查找内部的小block (仅考虑尚未被标记为删除的block)
|
| 304 |
-
blocks_inside = [(j, j_score, j_block) for j, (xj_min, yj_min, xj_max, yj_max, j_area, j_score, j_block) in
|
| 305 |
-
enumerate(block_info)
|
| 306 |
-
if i != j and j not in marked_indices and is_inside(block_info[j], block_info[i],
|
| 307 |
-
overlap_threshold)]
|
| 308 |
-
|
| 309 |
-
# 如果内部有3个及以上的小block
|
| 310 |
-
if len(blocks_inside) >= 2:
|
| 311 |
-
# 计算小block的平均分数
|
| 312 |
-
avg_score = sum(s for _, s, _ in blocks_inside) / len(blocks_inside)
|
| 313 |
-
|
| 314 |
-
# 比较大block的分数和小block的平均分数
|
| 315 |
-
if score > avg_score:
|
| 316 |
-
# 保留大block,扩展其边界
|
| 317 |
-
# 首先将所有小block标记为要删除
|
| 318 |
-
for j, _, j_block in blocks_inside:
|
| 319 |
-
if j_block not in blocks_to_remove:
|
| 320 |
-
blocks_to_remove.append(j_block)
|
| 321 |
-
marked_indices.add(j) # 标记索引为已处理
|
| 322 |
-
|
| 323 |
-
# 扩展大block的边界以包含所有小block
|
| 324 |
-
new_xmin, new_ymin, new_xmax, new_ymax = xmin, ymin, xmax, ymax
|
| 325 |
-
for _, _, j_block in blocks_inside:
|
| 326 |
-
j_xmin, j_ymin = int(j_block['poly'][0]), int(j_block['poly'][1])
|
| 327 |
-
j_xmax, j_ymax = int(j_block['poly'][4]), int(j_block['poly'][5])
|
| 328 |
-
new_xmin = min(new_xmin, j_xmin)
|
| 329 |
-
new_ymin = min(new_ymin, j_ymin)
|
| 330 |
-
new_xmax = max(new_xmax, j_xmax)
|
| 331 |
-
new_ymax = max(new_ymax, j_ymax)
|
| 332 |
-
|
| 333 |
-
# 更新大block的边界
|
| 334 |
-
block['poly'][0] = block['poly'][6] = new_xmin
|
| 335 |
-
block['poly'][1] = block['poly'][3] = new_ymin
|
| 336 |
-
block['poly'][2] = block['poly'][4] = new_xmax
|
| 337 |
-
block['poly'][5] = block['poly'][7] = new_ymax
|
| 338 |
-
else:
|
| 339 |
-
# 保留小blocks,删除大block
|
| 340 |
-
blocks_to_remove.append(block)
|
| 341 |
-
marked_indices.add(i) # 标记当前索引为已处理
|
| 342 |
-
return blocks_to_remove
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
def get_res_list_from_layout_res(layout_res, iou_threshold=0.7, overlap_threshold=0.8, area_threshold=0.8):
|
| 346 |
-
"""Extract OCR, table and other regions from layout results."""
|
| 347 |
-
ocr_res_list = []
|
| 348 |
-
text_res_list = []
|
| 349 |
-
table_res_list = []
|
| 350 |
-
table_indices = []
|
| 351 |
-
single_page_mfdetrec_res = []
|
| 352 |
-
|
| 353 |
-
# Categorize regions
|
| 354 |
-
for i, res in enumerate(layout_res):
|
| 355 |
-
category_id = int(res['category_id'])
|
| 356 |
-
|
| 357 |
-
if category_id in [13, 14]: # Formula regions
|
| 358 |
-
single_page_mfdetrec_res.append({
|
| 359 |
-
"bbox": [int(res['poly'][0]), int(res['poly'][1]),
|
| 360 |
-
int(res['poly'][4]), int(res['poly'][5])],
|
| 361 |
-
})
|
| 362 |
-
elif category_id in [0, 2, 4, 6, 7, 3]: # OCR regions
|
| 363 |
-
ocr_res_list.append(res)
|
| 364 |
-
elif category_id == 5: # Table regions
|
| 365 |
-
table_res_list.append(res)
|
| 366 |
-
table_indices.append(i)
|
| 367 |
-
elif category_id in [1]: # Text regions
|
| 368 |
-
text_res_list.append(res)
|
| 369 |
-
|
| 370 |
-
# Process tables: merge high IoU tables first, then filter nested tables
|
| 371 |
-
table_res_list, table_indices = merge_high_iou_tables(
|
| 372 |
-
table_res_list, layout_res, table_indices, iou_threshold)
|
| 373 |
-
|
| 374 |
-
filtered_table_res_list = filter_nested_tables(
|
| 375 |
-
table_res_list, overlap_threshold, area_threshold)
|
| 376 |
-
|
| 377 |
-
filtered_table_res_list, table_need_remove = remove_overlaps_min_blocks(filtered_table_res_list)
|
| 378 |
-
|
| 379 |
-
for res in table_need_remove:
|
| 380 |
-
if res in layout_res:
|
| 381 |
-
layout_res.remove(res)
|
| 382 |
-
|
| 383 |
-
# Remove filtered out tables from layout_res
|
| 384 |
-
if len(filtered_table_res_list) < len(table_res_list):
|
| 385 |
-
kept_tables = set(id(table) for table in filtered_table_res_list)
|
| 386 |
-
tables_to_remove = [table for table in table_res_list if id(table) not in kept_tables]
|
| 387 |
-
for table in tables_to_remove:
|
| 388 |
-
if table in layout_res:
|
| 389 |
-
layout_res.remove(table)
|
| 390 |
-
|
| 391 |
-
# Remove overlaps in OCR and text regions
|
| 392 |
-
text_res_list, need_remove = remove_overlaps_min_blocks(text_res_list)
|
| 393 |
-
|
| 394 |
-
ocr_res_list.extend(text_res_list)
|
| 395 |
-
|
| 396 |
-
for res in need_remove:
|
| 397 |
-
if res in layout_res:
|
| 398 |
-
layout_res.remove(res)
|
| 399 |
-
|
| 400 |
-
# 检测大block内部是否包含多个小block, 合并ocr和table列表进行检测
|
| 401 |
-
combined_res_list = ocr_res_list + filtered_table_res_list
|
| 402 |
-
blocks_to_remove = remove_overlaps_low_confidence_blocks(combined_res_list, overlap_threshold)
|
| 403 |
-
# 移除需要删除的blocks
|
| 404 |
-
for block in blocks_to_remove:
|
| 405 |
-
if block in ocr_res_list:
|
| 406 |
-
ocr_res_list.remove(block)
|
| 407 |
-
elif block in filtered_table_res_list:
|
| 408 |
-
filtered_table_res_list.remove(block)
|
| 409 |
-
# 同时从layout_res中删除
|
| 410 |
-
if block in layout_res:
|
| 411 |
-
layout_res.remove(block)
|
| 412 |
-
|
| 413 |
-
return ocr_res_list, filtered_table_res_list, single_page_mfdetrec_res
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
def clean_memory(device='cuda'):
|
| 417 |
-
if device == 'cuda':
|
| 418 |
-
if torch.cuda.is_available():
|
| 419 |
-
torch.cuda.empty_cache()
|
| 420 |
-
torch.cuda.ipc_collect()
|
| 421 |
-
elif str(device).startswith("npu"):
|
| 422 |
-
if torch_npu.npu.is_available():
|
| 423 |
-
torch_npu.npu.empty_cache()
|
| 424 |
-
elif str(device).startswith("mps"):
|
| 425 |
-
torch.mps.empty_cache()
|
| 426 |
-
gc.collect()
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
def clean_vram(device, vram_threshold=8):
|
| 430 |
-
total_memory = get_vram(device)
|
| 431 |
-
if total_memory and total_memory <= vram_threshold:
|
| 432 |
-
gc_start = time.time()
|
| 433 |
-
clean_memory(device)
|
| 434 |
-
gc_time = round(time.time() - gc_start, 2)
|
| 435 |
-
# logger.info(f"gc time: {gc_time}")
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
def get_vram(device) -> int:
|
| 439 |
-
env_vram = os.getenv("NaviOCR_VIRTUAL_VRAM_SIZE")
|
| 440 |
-
|
| 441 |
-
# 如果环境变量已配置,尝试解析并返回
|
| 442 |
-
if env_vram is not None:
|
| 443 |
-
try:
|
| 444 |
-
total_memory = int(env_vram)
|
| 445 |
-
if total_memory > 0:
|
| 446 |
-
return total_memory
|
| 447 |
-
else:
|
| 448 |
-
logger.warning(
|
| 449 |
-
f"NaviOCR_VIRTUAL_VRAM_SIZE value '{env_vram}' is not positive, falling back to auto-detection")
|
| 450 |
-
except ValueError:
|
| 451 |
-
logger.warning(
|
| 452 |
-
f"NaviOCR_VIRTUAL_VRAM_SIZE value '{env_vram}' is not a valid integer, falling back to auto-detection")
|
| 453 |
-
|
| 454 |
-
# 环境变量未配置或配置错误,根据device自动获取
|
| 455 |
-
total_memory = 1
|
| 456 |
-
if torch.cuda.is_available() and str(device).startswith("cuda"):
|
| 457 |
-
total_memory = round(torch.cuda.get_device_properties(device).total_memory / (1024 ** 3)) # 将字节转换为 GB
|
| 458 |
-
elif str(device).startswith("npu"):
|
| 459 |
-
if torch_npu.npu.is_available():
|
| 460 |
-
total_memory = round(torch_npu.npu.get_device_properties(device).total_memory / (1024 ** 3)) # 转为 GB
|
| 461 |
-
|
| 462 |
-
return total_memory
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/os_env_config.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
def get_op_num_threads(env_name: str) -> int:
|
| 5 |
-
env_value = os.getenv(env_name, None)
|
| 6 |
-
return get_value_from_string(env_value, -1)
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
def get_load_images_timeout() -> int:
|
| 10 |
-
env_value = os.getenv('NaviOCR_PDF_RENDER_TIMEOUT', None)
|
| 11 |
-
return get_value_from_string(env_value, 300)
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
def get_value_from_string(env_value: str, default_value: int) -> int:
|
| 15 |
-
if env_value is not None:
|
| 16 |
-
try:
|
| 17 |
-
num_threads = int(env_value)
|
| 18 |
-
if num_threads > 0:
|
| 19 |
-
return num_threads
|
| 20 |
-
except ValueError:
|
| 21 |
-
return default_value
|
| 22 |
-
return default_value
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
if __name__ == '__main__':
|
| 26 |
-
print(get_value_from_string('1', -1))
|
| 27 |
-
print(get_value_from_string('0', -1))
|
| 28 |
-
print(get_value_from_string('-1', -1))
|
| 29 |
-
print(get_value_from_string('abc', -1))
|
| 30 |
-
print(get_load_images_timeout())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/pdf_image_tools.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
import importlib
|
| 2 |
-
|
| 3 |
-
import NaviOCR.config as CONFIG
|
| 4 |
-
|
| 5 |
-
PDF_MODULES = {
|
| 6 |
-
"PyMuPDF": ".pdf_image_tools_PyMuPDF",
|
| 7 |
-
"pypdfium2": ".pdf_image_tools_pdfium",
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
module = importlib.import_module(
|
| 11 |
-
PDF_MODULES[CONFIG.PDF_TOOLS],
|
| 12 |
-
package=__package__,
|
| 13 |
-
)
|
| 14 |
-
|
| 15 |
-
for name in (
|
| 16 |
-
"pdf_page_to_image",
|
| 17 |
-
"_load_images_from_pdf_worker",
|
| 18 |
-
"load_images_from_pdf",
|
| 19 |
-
"load_images_from_pdf_core",
|
| 20 |
-
"cut_image",
|
| 21 |
-
"get_crop_img",
|
| 22 |
-
"images_bytes_to_pdf_bytes",
|
| 23 |
-
"get_page_size",
|
| 24 |
-
"convert_pdf_bytes_to_bytes"
|
| 25 |
-
):
|
| 26 |
-
globals()[name] = getattr(module, name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/pdf_image_tools_PyMuPDF.py
DELETED
|
@@ -1,289 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import os
|
| 3 |
-
from io import BytesIO
|
| 4 |
-
from concurrent.futures import ProcessPoolExecutor, TimeoutError as FuturesTimeoutError
|
| 5 |
-
|
| 6 |
-
import fitz # PyMuPDF
|
| 7 |
-
import numpy as np
|
| 8 |
-
from loguru import logger
|
| 9 |
-
from PIL import Image, ImageOps
|
| 10 |
-
|
| 11 |
-
import NaviOCR.config as CONFIG
|
| 12 |
-
from NaviOCR.tools.check_sys_env import is_windows_environment
|
| 13 |
-
from NaviOCR.tools.enum_class import ImageType
|
| 14 |
-
from NaviOCR.tools.hash_utils import str_sha256
|
| 15 |
-
from NaviOCR.tools.os_env_config import get_load_images_timeout
|
| 16 |
-
from NaviOCR.tools.pdf_page_id import get_end_page_id
|
| 17 |
-
from NaviOCR.tools.pdf_reader import image_to_b64str, image_to_bytes
|
| 18 |
-
|
| 19 |
-
def convert_pdf_bytes_to_bytes(pdf_bytes,valid_single_page_ids=None):
|
| 20 |
-
pdf = fitz.open(stream=pdf_bytes, filetype="pdf")
|
| 21 |
-
output_pdf = fitz.open()
|
| 22 |
-
try:
|
| 23 |
-
total_pages = len(pdf)
|
| 24 |
-
|
| 25 |
-
if not valid_single_page_ids:
|
| 26 |
-
valid_single_page_ids = list(range(total_pages))
|
| 27 |
-
|
| 28 |
-
valid_single_page_ids = [
|
| 29 |
-
page_id
|
| 30 |
-
for page_id in valid_single_page_ids
|
| 31 |
-
if 0 <= page_id < total_pages
|
| 32 |
-
]
|
| 33 |
-
|
| 34 |
-
for page_index in valid_single_page_ids:
|
| 35 |
-
try:
|
| 36 |
-
output_pdf.insert_pdf(
|
| 37 |
-
pdf,
|
| 38 |
-
from_page=page_index,
|
| 39 |
-
to_page=page_index,
|
| 40 |
-
)
|
| 41 |
-
except Exception as page_error:
|
| 42 |
-
logger.warning(
|
| 43 |
-
f"Failed to import page {page_index}: "
|
| 44 |
-
f"{page_error}, skipping this page."
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
output_bytes = output_pdf.tobytes(
|
| 48 |
-
garbage=4,
|
| 49 |
-
deflate=True,
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
except Exception as e:
|
| 53 |
-
logger.warning(
|
| 54 |
-
f"Error in converting PDF bytes: {e}, "
|
| 55 |
-
f"Using original PDF bytes."
|
| 56 |
-
)
|
| 57 |
-
output_bytes = pdf_bytes
|
| 58 |
-
|
| 59 |
-
finally:
|
| 60 |
-
pdf.close()
|
| 61 |
-
output_pdf.close()
|
| 62 |
-
|
| 63 |
-
return output_bytes
|
| 64 |
-
|
| 65 |
-
def pdf_page_to_image(page: fitz.Page,dpi=200,image_type=ImageType.PIL) -> dict:
|
| 66 |
-
zoom = dpi / 72.0
|
| 67 |
-
matrix = fitz.Matrix(zoom, zoom)
|
| 68 |
-
pix = page.get_pixmap(matrix=matrix, alpha=False)
|
| 69 |
-
pil_img = Image.frombytes("RGB",(pix.width, pix.height),pix.samples,)
|
| 70 |
-
|
| 71 |
-
image_dict = {"scale": zoom,}
|
| 72 |
-
|
| 73 |
-
if image_type == ImageType.BASE64:
|
| 74 |
-
image_dict["img_base64"] = image_to_b64str(pil_img)
|
| 75 |
-
else:
|
| 76 |
-
image_dict["img_pil"] = pil_img
|
| 77 |
-
|
| 78 |
-
return image_dict
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def _load_images_from_pdf_worker(
|
| 82 |
-
pdf_bytes,
|
| 83 |
-
dpi,
|
| 84 |
-
start_page_id,
|
| 85 |
-
end_page_id,
|
| 86 |
-
image_type,
|
| 87 |
-
):
|
| 88 |
-
"""供多进程调用"""
|
| 89 |
-
return load_images_from_pdf_core(
|
| 90 |
-
pdf_bytes,
|
| 91 |
-
dpi,
|
| 92 |
-
start_page_id,
|
| 93 |
-
end_page_id,
|
| 94 |
-
image_type,
|
| 95 |
-
)
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def load_images_from_pdf(
|
| 99 |
-
pdf_bytes: bytes,
|
| 100 |
-
dpi=200,
|
| 101 |
-
start_page_id=0,
|
| 102 |
-
end_page_id=None,
|
| 103 |
-
image_type=ImageType.PIL,
|
| 104 |
-
timeout=None,
|
| 105 |
-
threads=4,
|
| 106 |
-
):
|
| 107 |
-
"""
|
| 108 |
-
带超时控制的 PDF 转图片函数,支持多进程。
|
| 109 |
-
|
| 110 |
-
Args:
|
| 111 |
-
pdf_bytes (bytes): PDF bytes.
|
| 112 |
-
dpi (int): render dpi.
|
| 113 |
-
start_page_id (int): start page.
|
| 114 |
-
end_page_id (int | None): end page.
|
| 115 |
-
image_type (ImageType): PIL or BASE64.
|
| 116 |
-
timeout (int | None): timeout seconds.
|
| 117 |
-
threads (int): process number.
|
| 118 |
-
"""
|
| 119 |
-
|
| 120 |
-
pdf_doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
| 121 |
-
|
| 122 |
-
if is_windows_environment() or CONFIG.PDF_TOOLS_WORKER_MAX_NUM==0:
|
| 123 |
-
return (
|
| 124 |
-
load_images_from_pdf_core(
|
| 125 |
-
pdf_bytes,
|
| 126 |
-
dpi,
|
| 127 |
-
start_page_id,
|
| 128 |
-
get_end_page_id(end_page_id, len(pdf_doc)),
|
| 129 |
-
image_type,
|
| 130 |
-
),
|
| 131 |
-
pdf_doc,
|
| 132 |
-
)
|
| 133 |
-
|
| 134 |
-
if timeout is None:
|
| 135 |
-
timeout = get_load_images_timeout()
|
| 136 |
-
|
| 137 |
-
end_page_id = get_end_page_id(end_page_id, len(pdf_doc))
|
| 138 |
-
|
| 139 |
-
total_pages = end_page_id - start_page_id + 1
|
| 140 |
-
|
| 141 |
-
actual_threads = min(max(1, int(os.cpu_count() * CONFIG.PDF_TOOLS_WORKER_RATIO)), threads, total_pages)
|
| 142 |
-
|
| 143 |
-
pages_per_thread = max(1, total_pages // actual_threads)
|
| 144 |
-
|
| 145 |
-
page_ranges = []
|
| 146 |
-
|
| 147 |
-
for i in range(actual_threads):
|
| 148 |
-
range_start = start_page_id + i * pages_per_thread
|
| 149 |
-
|
| 150 |
-
if i == actual_threads - 1:
|
| 151 |
-
range_end = end_page_id
|
| 152 |
-
else:
|
| 153 |
-
range_end = start_page_id + (i + 1) * pages_per_thread - 1
|
| 154 |
-
|
| 155 |
-
page_ranges.append((range_start, range_end))
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
with ProcessPoolExecutor(max_workers=actual_threads) as executor:
|
| 159 |
-
|
| 160 |
-
futures = []
|
| 161 |
-
|
| 162 |
-
for range_start, range_end in page_ranges:
|
| 163 |
-
future = executor.submit(
|
| 164 |
-
_load_images_from_pdf_worker,
|
| 165 |
-
pdf_bytes,
|
| 166 |
-
dpi,
|
| 167 |
-
range_start,
|
| 168 |
-
range_end,
|
| 169 |
-
image_type,
|
| 170 |
-
)
|
| 171 |
-
futures.append((range_start, future))
|
| 172 |
-
|
| 173 |
-
try:
|
| 174 |
-
all_results = []
|
| 175 |
-
|
| 176 |
-
for range_start, future in futures:
|
| 177 |
-
images_list = future.result(timeout=timeout)
|
| 178 |
-
all_results.append((range_start, images_list))
|
| 179 |
-
|
| 180 |
-
all_results.sort(key=lambda x: x[0])
|
| 181 |
-
|
| 182 |
-
images_list = []
|
| 183 |
-
|
| 184 |
-
for _, imgs in all_results:
|
| 185 |
-
images_list.extend(imgs)
|
| 186 |
-
|
| 187 |
-
return images_list, pdf_doc
|
| 188 |
-
|
| 189 |
-
except FuturesTimeoutError:
|
| 190 |
-
pdf_doc.close()
|
| 191 |
-
executor.shutdown(wait=False, cancel_futures=True)
|
| 192 |
-
raise TimeoutError(
|
| 193 |
-
f"PDF to images conversion timeout after {timeout}s"
|
| 194 |
-
)
|
| 195 |
-
|
| 196 |
-
def load_images_from_pdf_core(
|
| 197 |
-
pdf_bytes: bytes,
|
| 198 |
-
dpi=200,
|
| 199 |
-
start_page_id=0,
|
| 200 |
-
end_page_id=None,
|
| 201 |
-
image_type=ImageType.PIL, # PIL or BASE64
|
| 202 |
-
):
|
| 203 |
-
images_list = []
|
| 204 |
-
|
| 205 |
-
pdf_doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
| 206 |
-
|
| 207 |
-
pdf_page_num = len(pdf_doc)
|
| 208 |
-
|
| 209 |
-
end_page_id = get_end_page_id(end_page_id, pdf_page_num)
|
| 210 |
-
|
| 211 |
-
for index in range(start_page_id, end_page_id + 1):
|
| 212 |
-
# logger.debug(f"Converting page {index}/{pdf_page_num} to image")
|
| 213 |
-
page = pdf_doc[index]
|
| 214 |
-
image_dict = pdf_page_to_image(
|
| 215 |
-
page,
|
| 216 |
-
dpi=dpi,
|
| 217 |
-
image_type=image_type,
|
| 218 |
-
)
|
| 219 |
-
images_list.append(image_dict)
|
| 220 |
-
|
| 221 |
-
pdf_doc.close()
|
| 222 |
-
|
| 223 |
-
return images_list
|
| 224 |
-
|
| 225 |
-
def cut_image(
|
| 226 |
-
bbox: tuple,
|
| 227 |
-
page_num: int,
|
| 228 |
-
page_pil_img,
|
| 229 |
-
return_path,
|
| 230 |
-
image_writer,
|
| 231 |
-
scale=2,
|
| 232 |
-
):
|
| 233 |
-
filename = f"{page_num}_{int(bbox[0])}_{int(bbox[1])}_{int(bbox[2])}_{int(bbox[3])}"
|
| 234 |
-
rel_img_path = f"{return_path}_{filename}.jpeg" if return_path is not None else None
|
| 235 |
-
crop_img = get_crop_img(bbox, page_pil_img, scale=scale)
|
| 236 |
-
img_bytes = image_to_bytes(crop_img, image_format="JPEG")
|
| 237 |
-
image_writer.add_image(img_bytes, rel_img_path)
|
| 238 |
-
return rel_img_path
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
def get_crop_img(bbox: tuple, pil_img, scale=2):
|
| 242 |
-
scale_bbox = (
|
| 243 |
-
int(bbox[0] * scale),
|
| 244 |
-
int(bbox[1] * scale),
|
| 245 |
-
int(bbox[2] * scale),
|
| 246 |
-
int(bbox[3] * scale),
|
| 247 |
-
)
|
| 248 |
-
return pil_img.crop(scale_bbox)
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
# def get_crop_np_img(bbox: tuple, input_img, scale=2):
|
| 252 |
-
# if isinstance(input_img, Image.Image):
|
| 253 |
-
# np_img = np.asarray(input_img)
|
| 254 |
-
# elif isinstance(input_img, np.ndarray):
|
| 255 |
-
# np_img = input_img
|
| 256 |
-
# else:
|
| 257 |
-
# raise ValueError("Input must be a pillow object or a numpy array.")
|
| 258 |
-
|
| 259 |
-
# scale_bbox = (
|
| 260 |
-
# int(bbox[0] * scale),
|
| 261 |
-
# int(bbox[1] * scale),
|
| 262 |
-
# int(bbox[2] * scale),
|
| 263 |
-
# int(bbox[3] * scale),
|
| 264 |
-
# )
|
| 265 |
-
# return np_img[scale_bbox[1] : scale_bbox[3], scale_bbox[0] : scale_bbox[2]]
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
def images_bytes_to_pdf_bytes(image_bytes):
|
| 269 |
-
pdf_buffer = BytesIO()
|
| 270 |
-
image = Image.open(BytesIO(image_bytes))
|
| 271 |
-
image = ImageOps.exif_transpose(image) or image
|
| 272 |
-
if image.mode != "RGB":
|
| 273 |
-
image = image.convert("RGB")
|
| 274 |
-
image.save(
|
| 275 |
-
pdf_buffer,
|
| 276 |
-
format="PDF",
|
| 277 |
-
# save_all=True
|
| 278 |
-
)
|
| 279 |
-
pdf_bytes = pdf_buffer.getvalue()
|
| 280 |
-
pdf_buffer.close()
|
| 281 |
-
return pdf_bytes
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
def get_page_size(page):
|
| 285 |
-
rect = page.rect
|
| 286 |
-
w = rect.width
|
| 287 |
-
h = rect.height
|
| 288 |
-
return (w, h)
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/pdf_image_tools_pdfium.py
DELETED
|
@@ -1,268 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import os
|
| 3 |
-
from io import BytesIO
|
| 4 |
-
|
| 5 |
-
import numpy as np
|
| 6 |
-
import pypdfium2 as pdfium
|
| 7 |
-
import io
|
| 8 |
-
from loguru import logger
|
| 9 |
-
from PIL import Image, ImageOps
|
| 10 |
-
|
| 11 |
-
import NaviOCR.config as CONFIG
|
| 12 |
-
from NaviOCR.tools.check_sys_env import is_windows_environment
|
| 13 |
-
from NaviOCR.tools.os_env_config import get_load_images_timeout
|
| 14 |
-
from NaviOCR.tools.pdf_reader import image_to_b64str, image_to_bytes, page_to_image
|
| 15 |
-
from NaviOCR.tools.enum_class import ImageType
|
| 16 |
-
from NaviOCR.tools.hash_utils import str_sha256
|
| 17 |
-
from NaviOCR.tools.pdf_page_id import get_end_page_id
|
| 18 |
-
from concurrent.futures import ProcessPoolExecutor, TimeoutError as FuturesTimeoutError
|
| 19 |
-
|
| 20 |
-
def convert_pdf_bytes_to_bytes(pdf_bytes, valid_single_page_ids=None):
|
| 21 |
-
pdf = pdfium.PdfDocument(pdf_bytes)
|
| 22 |
-
output_pdf = pdfium.PdfDocument.new()
|
| 23 |
-
|
| 24 |
-
try:
|
| 25 |
-
total_pages = len(pdf)
|
| 26 |
-
if not valid_single_page_ids:
|
| 27 |
-
valid_single_page_ids = list(range(total_pages))
|
| 28 |
-
|
| 29 |
-
valid_single_page_ids = [page_id for page_id in valid_single_page_ids if 0 <= page_id < total_pages]
|
| 30 |
-
|
| 31 |
-
for page_index in valid_single_page_ids:
|
| 32 |
-
try:
|
| 33 |
-
output_pdf.import_pages(pdf, pages=[page_index])
|
| 34 |
-
except Exception as page_error:
|
| 35 |
-
logger.warning(
|
| 36 |
-
f"Failed to import page {page_index}: "
|
| 37 |
-
f"{page_error}, skipping this page."
|
| 38 |
-
)
|
| 39 |
-
|
| 40 |
-
output_buffer = io.BytesIO()
|
| 41 |
-
output_pdf.save(output_buffer)
|
| 42 |
-
output_bytes = output_buffer.getvalue()
|
| 43 |
-
|
| 44 |
-
except Exception as e:
|
| 45 |
-
logger.warning(
|
| 46 |
-
f"Error in converting PDF bytes: {e}, "
|
| 47 |
-
f"Using original PDF bytes."
|
| 48 |
-
)
|
| 49 |
-
output_bytes = pdf_bytes
|
| 50 |
-
|
| 51 |
-
pdf.close()
|
| 52 |
-
output_pdf.close()
|
| 53 |
-
|
| 54 |
-
return output_bytes
|
| 55 |
-
|
| 56 |
-
def pdf_page_to_image(page: pdfium.PdfPage, dpi=200, image_type=ImageType.PIL) -> dict:
|
| 57 |
-
"""Convert pdfium.PdfDocument to image, Then convert the image to base64.
|
| 58 |
-
|
| 59 |
-
Args:
|
| 60 |
-
page (_type_): pdfium.PdfPage
|
| 61 |
-
dpi (int, optional): reset the dpi of dpi. Defaults to 200.
|
| 62 |
-
image_type (ImageType, optional): The type of image to return. Defaults to ImageType.PIL.
|
| 63 |
-
|
| 64 |
-
Returns:
|
| 65 |
-
dict: {'img_base64': str, 'img_pil': pil_img, 'scale': float }
|
| 66 |
-
"""
|
| 67 |
-
pil_img, scale = page_to_image(page, dpi=dpi)
|
| 68 |
-
image_dict = {
|
| 69 |
-
"scale": scale,
|
| 70 |
-
}
|
| 71 |
-
if image_type == ImageType.BASE64:
|
| 72 |
-
image_dict["img_base64"] = image_to_b64str(pil_img)
|
| 73 |
-
else:
|
| 74 |
-
image_dict["img_pil"] = pil_img
|
| 75 |
-
|
| 76 |
-
return image_dict
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def _load_images_from_pdf_worker(
|
| 80 |
-
pdf_bytes, dpi, start_page_id, end_page_id, image_type
|
| 81 |
-
):
|
| 82 |
-
"""用于进程池的包装函数"""
|
| 83 |
-
return load_images_from_pdf_core(
|
| 84 |
-
pdf_bytes, dpi, start_page_id, end_page_id, image_type
|
| 85 |
-
)
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def load_images_from_pdf(
|
| 89 |
-
pdf_bytes: bytes,
|
| 90 |
-
dpi=200,
|
| 91 |
-
start_page_id=0,
|
| 92 |
-
end_page_id=None,
|
| 93 |
-
image_type=ImageType.PIL,
|
| 94 |
-
timeout=None,
|
| 95 |
-
threads=4,
|
| 96 |
-
):
|
| 97 |
-
"""带超时控制的 PDF 转图片函数,支持多进程加速
|
| 98 |
-
|
| 99 |
-
Args:
|
| 100 |
-
pdf_bytes (bytes): PDF 文件的 bytes
|
| 101 |
-
dpi (int, optional): reset the dpi of dpi. Defaults to 200.
|
| 102 |
-
start_page_id (int, optional): 起始页码. Defaults to 0.
|
| 103 |
-
end_page_id (int | None, optional): 结束页码. Defaults to None.
|
| 104 |
-
image_type (ImageType, optional): 图片类型. Defaults to ImageType.PIL.
|
| 105 |
-
timeout (int | None, optional): 超时时间(秒)。如果为 None,则从环境变量 NaviOCR_PDF_LOAD_IMAGES_TIMEOUT 读取,若未设置则默认为 300 秒。
|
| 106 |
-
threads (int): 进程数,默认 4
|
| 107 |
-
|
| 108 |
-
Raises:
|
| 109 |
-
TimeoutError: 当转换超时时抛出
|
| 110 |
-
"""
|
| 111 |
-
pdf_doc = pdfium.PdfDocument(pdf_bytes)
|
| 112 |
-
if is_windows_environment() or CONFIG.PDF_TOOLS_WORKER_MAX_NUM==0:
|
| 113 |
-
# Windows 环境下不使用多进程
|
| 114 |
-
return load_images_from_pdf_core(
|
| 115 |
-
pdf_bytes,
|
| 116 |
-
dpi,
|
| 117 |
-
start_page_id,
|
| 118 |
-
get_end_page_id(end_page_id, len(pdf_doc)),
|
| 119 |
-
image_type,
|
| 120 |
-
), pdf_doc
|
| 121 |
-
else:
|
| 122 |
-
if timeout is None:
|
| 123 |
-
timeout = get_load_images_timeout()
|
| 124 |
-
end_page_id = get_end_page_id(end_page_id, len(pdf_doc))
|
| 125 |
-
|
| 126 |
-
# 计算总页数
|
| 127 |
-
total_pages = end_page_id - start_page_id + 1
|
| 128 |
-
|
| 129 |
-
# 实际使用的进程数不超过总页数
|
| 130 |
-
actual_threads = min(max(1, int(os.cpu_count() * CONFIG.PDF_TOOLS_WORKER_RATIO)), threads, total_pages)
|
| 131 |
-
|
| 132 |
-
# 根据实际进程数分组页面范围
|
| 133 |
-
pages_per_thread = max(1, total_pages // actual_threads)
|
| 134 |
-
page_ranges = []
|
| 135 |
-
|
| 136 |
-
for i in range(actual_threads):
|
| 137 |
-
range_start = start_page_id + i * pages_per_thread
|
| 138 |
-
if i == actual_threads - 1:
|
| 139 |
-
# 最后一个进程处理剩余所有页面
|
| 140 |
-
range_end = end_page_id
|
| 141 |
-
else:
|
| 142 |
-
range_end = start_page_id + (i + 1) * pages_per_thread - 1
|
| 143 |
-
|
| 144 |
-
page_ranges.append((range_start, range_end))
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
with ProcessPoolExecutor(max_workers=actual_threads) as executor:
|
| 148 |
-
# 提交所有任务
|
| 149 |
-
futures = []
|
| 150 |
-
for range_start, range_end in page_ranges:
|
| 151 |
-
future = executor.submit(
|
| 152 |
-
_load_images_from_pdf_worker,
|
| 153 |
-
pdf_bytes,
|
| 154 |
-
dpi,
|
| 155 |
-
range_start,
|
| 156 |
-
range_end,
|
| 157 |
-
image_type,
|
| 158 |
-
)
|
| 159 |
-
futures.append((range_start, future))
|
| 160 |
-
|
| 161 |
-
try:
|
| 162 |
-
# 收集结果并按页码排序
|
| 163 |
-
all_results = []
|
| 164 |
-
for range_start, future in futures:
|
| 165 |
-
images_list = future.result(timeout=timeout)
|
| 166 |
-
all_results.append((range_start, images_list))
|
| 167 |
-
|
| 168 |
-
# 按起始页码排序并合并结果
|
| 169 |
-
all_results.sort(key=lambda x: x[0])
|
| 170 |
-
images_list = []
|
| 171 |
-
for _, imgs in all_results:
|
| 172 |
-
images_list.extend(imgs)
|
| 173 |
-
|
| 174 |
-
return images_list, pdf_doc
|
| 175 |
-
except FuturesTimeoutError:
|
| 176 |
-
pdf_doc.close()
|
| 177 |
-
executor.shutdown(wait=False, cancel_futures=True)
|
| 178 |
-
raise TimeoutError(f"PDF to images conversion timeout after {timeout}s")
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
def load_images_from_pdf_core(
|
| 182 |
-
pdf_bytes: bytes,
|
| 183 |
-
dpi=200,
|
| 184 |
-
start_page_id=0,
|
| 185 |
-
end_page_id=None,
|
| 186 |
-
image_type=ImageType.PIL, # PIL or BASE64
|
| 187 |
-
):
|
| 188 |
-
images_list = []
|
| 189 |
-
pdf_doc = pdfium.PdfDocument(pdf_bytes)
|
| 190 |
-
pdf_page_num = len(pdf_doc)
|
| 191 |
-
end_page_id = get_end_page_id(end_page_id, pdf_page_num)
|
| 192 |
-
|
| 193 |
-
for index in range(start_page_id, end_page_id + 1):
|
| 194 |
-
# logger.debug(f"Converting page {index}/{pdf_page_num} to image")
|
| 195 |
-
page = pdf_doc[index]
|
| 196 |
-
image_dict = pdf_page_to_image(page, dpi=dpi, image_type=image_type)
|
| 197 |
-
images_list.append(image_dict)
|
| 198 |
-
|
| 199 |
-
pdf_doc.close()
|
| 200 |
-
|
| 201 |
-
return images_list
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
def cut_image(
|
| 205 |
-
bbox: tuple,
|
| 206 |
-
page_num: int,
|
| 207 |
-
page_pil_img,
|
| 208 |
-
return_path,
|
| 209 |
-
image_writer,
|
| 210 |
-
scale=2,
|
| 211 |
-
):
|
| 212 |
-
filename = f"{page_num}_{int(bbox[0])}_{int(bbox[1])}_{int(bbox[2])}_{int(bbox[3])}"
|
| 213 |
-
rel_img_path = f"{return_path}_{filename}.jpeg" if return_path is not None else None
|
| 214 |
-
crop_img = get_crop_img(bbox, page_pil_img, scale=scale)
|
| 215 |
-
img_bytes = image_to_bytes(crop_img, image_format="JPEG")
|
| 216 |
-
image_writer.add_image(img_bytes, rel_img_path)
|
| 217 |
-
return rel_img_path
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
def get_crop_img(bbox: tuple, pil_img, scale=2):
|
| 221 |
-
scale_bbox = (
|
| 222 |
-
int(bbox[0] * scale),
|
| 223 |
-
int(bbox[1] * scale),
|
| 224 |
-
int(bbox[2] * scale),
|
| 225 |
-
int(bbox[3] * scale),
|
| 226 |
-
)
|
| 227 |
-
return pil_img.crop(scale_bbox)
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
# def get_crop_np_img(bbox: tuple, input_img, scale=2):
|
| 231 |
-
# if isinstance(input_img, Image.Image):
|
| 232 |
-
# np_img = np.asarray(input_img)
|
| 233 |
-
# elif isinstance(input_img, np.ndarray):
|
| 234 |
-
# np_img = input_img
|
| 235 |
-
# else:
|
| 236 |
-
# raise ValueError("Input must be a pillow object or a numpy array.")
|
| 237 |
-
|
| 238 |
-
# scale_bbox = (
|
| 239 |
-
# int(bbox[0] * scale),
|
| 240 |
-
# int(bbox[1] * scale),
|
| 241 |
-
# int(bbox[2] * scale),
|
| 242 |
-
# int(bbox[3] * scale),
|
| 243 |
-
# )
|
| 244 |
-
|
| 245 |
-
# return np_img[scale_bbox[1] : scale_bbox[3], scale_bbox[0] : scale_bbox[2]]
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
def images_bytes_to_pdf_bytes(image_bytes):
|
| 249 |
-
pdf_buffer = BytesIO()
|
| 250 |
-
image = Image.open(BytesIO(image_bytes))
|
| 251 |
-
image = ImageOps.exif_transpose(image) or image
|
| 252 |
-
if image.mode != "RGB":
|
| 253 |
-
image = image.convert("RGB")
|
| 254 |
-
# 第一张图保存为 PDF,其余追加
|
| 255 |
-
image.save(
|
| 256 |
-
pdf_buffer,
|
| 257 |
-
format="PDF",
|
| 258 |
-
# save_all=True
|
| 259 |
-
)
|
| 260 |
-
# 获取 PDF bytes 并重置指针(可选)
|
| 261 |
-
pdf_bytes = pdf_buffer.getvalue()
|
| 262 |
-
pdf_buffer.close()
|
| 263 |
-
return pdf_bytes
|
| 264 |
-
|
| 265 |
-
def get_page_size(page):
|
| 266 |
-
w, h = page.get_size()
|
| 267 |
-
return (w, h)
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/pdf_page_id.py
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
from loguru import logger
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
def get_end_page_id(end_page_id, pdf_page_num):
|
| 6 |
-
end_page_id = end_page_id if end_page_id is not None and end_page_id >= 0 else pdf_page_num - 1
|
| 7 |
-
if end_page_id > pdf_page_num - 1:
|
| 8 |
-
logger.warning("end_page_id is out of range, use images length")
|
| 9 |
-
end_page_id = pdf_page_num - 1
|
| 10 |
-
return end_page_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/pdf_reader.py
DELETED
|
@@ -1,111 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import base64
|
| 3 |
-
from io import BytesIO
|
| 4 |
-
|
| 5 |
-
from loguru import logger
|
| 6 |
-
from PIL import Image
|
| 7 |
-
import NaviOCR.config as CONFIG
|
| 8 |
-
from pypdfium2 import PdfBitmap, PdfDocument, PdfPage
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def page_to_image(
|
| 12 |
-
page: PdfPage,
|
| 13 |
-
dpi: int = 200,
|
| 14 |
-
max_width_or_height: int = 3500, # changed from 4500 to 3500
|
| 15 |
-
) -> (Image.Image, float):
|
| 16 |
-
scale = dpi / 72
|
| 17 |
-
|
| 18 |
-
long_side_length = max(*page.get_size())
|
| 19 |
-
if (long_side_length*scale) > max_width_or_height:
|
| 20 |
-
scale = max_width_or_height / long_side_length
|
| 21 |
-
|
| 22 |
-
bitmap: PdfBitmap = page.render(scale=scale) # type: ignore
|
| 23 |
-
|
| 24 |
-
image = bitmap.to_pil()
|
| 25 |
-
try:
|
| 26 |
-
bitmap.close()
|
| 27 |
-
except Exception as e:
|
| 28 |
-
logger.error(f"Failed to close bitmap: {e}")
|
| 29 |
-
return image, scale
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def image_to_bytes(
|
| 33 |
-
image: Image.Image,
|
| 34 |
-
# image_format: str = "PNG", # 也可以用 "JPEG"
|
| 35 |
-
image_format: str = "JPEG",
|
| 36 |
-
) -> bytes:
|
| 37 |
-
with BytesIO() as image_buffer:
|
| 38 |
-
image.save(image_buffer, format=image_format)
|
| 39 |
-
return image_buffer.getvalue()
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def image_to_b64str(
|
| 43 |
-
image: Image.Image,
|
| 44 |
-
# image_format: str = "PNG", # 也可以用 "JPEG"
|
| 45 |
-
image_format: str = "JPEG",
|
| 46 |
-
) -> str:
|
| 47 |
-
image_bytes = image_to_bytes(image, image_format)
|
| 48 |
-
return base64.b64encode(image_bytes).decode("utf-8")
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def base64_to_pil_image(
|
| 52 |
-
base64_str: str,
|
| 53 |
-
) -> Image.Image:
|
| 54 |
-
"""Convert base64 string to PIL Image."""
|
| 55 |
-
image_bytes = base64.b64decode(base64_str)
|
| 56 |
-
with BytesIO(image_bytes) as image_buffer:
|
| 57 |
-
return Image.open(image_buffer).convert("RGB")
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def pdf_to_images(
|
| 61 |
-
pdf: str | bytes | PdfDocument,
|
| 62 |
-
dpi: int = 200,
|
| 63 |
-
max_width_or_height: int = 3500,
|
| 64 |
-
start_page_id: int = 0,
|
| 65 |
-
end_page_id: int | None = None,
|
| 66 |
-
) -> list[Image.Image]:
|
| 67 |
-
doc = pdf if isinstance(pdf, PdfDocument) else PdfDocument(pdf)
|
| 68 |
-
page_num = len(doc)
|
| 69 |
-
|
| 70 |
-
end_page_id = end_page_id if end_page_id is not None and end_page_id >= 0 else page_num - 1
|
| 71 |
-
if end_page_id > page_num - 1:
|
| 72 |
-
logger.warning("end_page_id is out of range, use images length")
|
| 73 |
-
end_page_id = page_num - 1
|
| 74 |
-
|
| 75 |
-
images = []
|
| 76 |
-
try:
|
| 77 |
-
for i in range(start_page_id, end_page_id + 1):
|
| 78 |
-
image, _ = page_to_image(doc[i], dpi, max_width_or_height)
|
| 79 |
-
images.append(image)
|
| 80 |
-
finally:
|
| 81 |
-
try:
|
| 82 |
-
doc.close()
|
| 83 |
-
except Exception:
|
| 84 |
-
pass
|
| 85 |
-
return images
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def pdf_to_images_bytes(
|
| 89 |
-
pdf: str | bytes | PdfDocument,
|
| 90 |
-
dpi: int = 200,
|
| 91 |
-
max_width_or_height: int = 3500,
|
| 92 |
-
start_page_id: int = 0,
|
| 93 |
-
end_page_id: int | None = None,
|
| 94 |
-
# image_format: str = "PNG", # 也可以用 "JPEG"
|
| 95 |
-
image_format: str = "JPEG",
|
| 96 |
-
) -> list[bytes]:
|
| 97 |
-
images = pdf_to_images(pdf, dpi, max_width_or_height, start_page_id, end_page_id)
|
| 98 |
-
return [image_to_bytes(image, image_format) for image in images]
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def pdf_to_images_b64strs(
|
| 102 |
-
pdf: str | bytes | PdfDocument,
|
| 103 |
-
dpi: int = 200,
|
| 104 |
-
max_width_or_height: int = 3500,
|
| 105 |
-
start_page_id: int = 0,
|
| 106 |
-
end_page_id: int | None = None,
|
| 107 |
-
# image_format: str = "PNG", # 也可以用 "JPEG"
|
| 108 |
-
image_format: str = "JPEG",
|
| 109 |
-
) -> list[str]:
|
| 110 |
-
images = pdf_to_images(pdf, dpi, max_width_or_height, start_page_id, end_page_id)
|
| 111 |
-
return [image_to_b64str(image, image_format) for image in images]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/tools/read_file.py
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
from .guess_suffix_or_lang import guess_suffix_by_bytes
|
| 2 |
-
from .pdf_image_tools import images_bytes_to_pdf_bytes
|
| 3 |
-
from pathlib import Path
|
| 4 |
-
|
| 5 |
-
pdf_suffixes = ["pdf"]
|
| 6 |
-
image_suffixes = ["png", "jpeg", "jp2", "webp", "gif", "bmp", "jpg", "tiff"]
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def read_fn(path):
|
| 11 |
-
if not isinstance(path, Path):
|
| 12 |
-
path = Path(path)
|
| 13 |
-
with open(str(path), "rb") as input_file:
|
| 14 |
-
file_bytes = input_file.read()
|
| 15 |
-
file_suffix = guess_suffix_by_bytes(file_bytes, path)
|
| 16 |
-
if file_suffix in image_suffixes:
|
| 17 |
-
return images_bytes_to_pdf_bytes(file_bytes)
|
| 18 |
-
elif file_suffix in pdf_suffixes:
|
| 19 |
-
return file_bytes
|
| 20 |
-
else:
|
| 21 |
-
raise Exception(f"Unknown file suffix: {file_suffix}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
__version__='1.0.0'
|
|
|
|
|
|
NaviOCR/vlm_utils/NaviOCR_model.py
DELETED
|
@@ -1,126 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from .NaviOCR_client import NaviOCRClient
|
| 3 |
-
import NaviOCR.config as CONFIG
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
class NaviOCRMODEL:
|
| 7 |
-
_instance = None
|
| 8 |
-
_models = {}
|
| 9 |
-
def __new__(cls, *args, **kwargs):
|
| 10 |
-
if cls._instance is None:
|
| 11 |
-
cls._instance = super().__new__(cls)
|
| 12 |
-
return cls._instance
|
| 13 |
-
|
| 14 |
-
def get_model(
|
| 15 |
-
self,
|
| 16 |
-
backend: str,
|
| 17 |
-
model_path: str | None,
|
| 18 |
-
server_url: str | None,
|
| 19 |
-
**kwargs,
|
| 20 |
-
) -> NaviOCRClient:
|
| 21 |
-
print(backend, model_path, server_url)
|
| 22 |
-
key = (backend, model_path, server_url)
|
| 23 |
-
print(key not in self._models)
|
| 24 |
-
if key not in self._models:
|
| 25 |
-
model = None
|
| 26 |
-
processor = None
|
| 27 |
-
vllm_llm = None
|
| 28 |
-
lmdeploy_engine = None
|
| 29 |
-
vllm_async_llm = None
|
| 30 |
-
batch_size = 0
|
| 31 |
-
max_concurrency = 100
|
| 32 |
-
http_timeout = 600
|
| 33 |
-
server_headers = None
|
| 34 |
-
max_retries = 3
|
| 35 |
-
retry_backoff_factor = 0.5
|
| 36 |
-
if os.getenv('OMP_NUM_THREADS') is None:
|
| 37 |
-
os.environ["OMP_NUM_THREADS"] = "1"
|
| 38 |
-
if backend == "transformers":
|
| 39 |
-
if not model_path:
|
| 40 |
-
raise ValueError("model_path must be provided for the transformers backend.")
|
| 41 |
-
try:
|
| 42 |
-
import torch
|
| 43 |
-
from transformers import AutoProcessor
|
| 44 |
-
try:
|
| 45 |
-
from transformers import AutoModelForImageTextToText as AutoModelClass
|
| 46 |
-
except ImportError:
|
| 47 |
-
try:
|
| 48 |
-
from transformers import AutoModelForVision2Seq as AutoModelClass
|
| 49 |
-
except ImportError:
|
| 50 |
-
from transformers import AutoModel as AutoModelClass
|
| 51 |
-
except ImportError as exc:
|
| 52 |
-
raise ImportError("Please install torch and transformers to use the transformers backend.") from exc
|
| 53 |
-
|
| 54 |
-
processor = AutoProcessor.from_pretrained(
|
| 55 |
-
model_path,
|
| 56 |
-
trust_remote_code=True,
|
| 57 |
-
)
|
| 58 |
-
torch_dtype = kwargs.pop(
|
| 59 |
-
"torch_dtype",
|
| 60 |
-
torch.bfloat16 if torch.cuda.is_available() else torch.float32,
|
| 61 |
-
)
|
| 62 |
-
model = AutoModelClass.from_pretrained(
|
| 63 |
-
model_path,
|
| 64 |
-
trust_remote_code=True,
|
| 65 |
-
torch_dtype=torch_dtype,
|
| 66 |
-
**kwargs,
|
| 67 |
-
)
|
| 68 |
-
if torch.cuda.is_available():
|
| 69 |
-
model = model.cuda()
|
| 70 |
-
model = model.eval()
|
| 71 |
-
if backend == "vllm-engine":
|
| 72 |
-
try:
|
| 73 |
-
import vllm
|
| 74 |
-
except ImportError:
|
| 75 |
-
raise ImportError("Please install vllm to use the vllm-engine backend.")
|
| 76 |
-
if "gpu_memory_utilization" not in kwargs:
|
| 77 |
-
kwargs["gpu_memory_utilization"] = CONFIG.GPU_MEMORY_UTILIZATION
|
| 78 |
-
if "model" not in kwargs:
|
| 79 |
-
kwargs["model"] = model_path
|
| 80 |
-
if "logits_processors" not in kwargs:
|
| 81 |
-
from . import LogitsProcessor
|
| 82 |
-
kwargs["logits_processors"] = [LogitsProcessor]
|
| 83 |
-
if 'max_model_len' not in kwargs:
|
| 84 |
-
kwargs["max_model_len"] = CONFIG.MAX_MODEL_LEN
|
| 85 |
-
vllm_llm = vllm.LLM(**kwargs)
|
| 86 |
-
|
| 87 |
-
elif backend == "vllm-async-engine":
|
| 88 |
-
try:
|
| 89 |
-
from vllm.engine.arg_utils import AsyncEngineArgs
|
| 90 |
-
from vllm.v1.engine.async_llm import AsyncLLM
|
| 91 |
-
except ImportError:
|
| 92 |
-
raise ImportError("Please install vllm to use the vllm-async-engine backend.")
|
| 93 |
-
|
| 94 |
-
if "gpu_memory_utilization" not in kwargs:
|
| 95 |
-
kwargs["gpu_memory_utilization"] = CONFIG.GPU_MEMORY_UTILIZATION
|
| 96 |
-
|
| 97 |
-
if "model" not in kwargs:
|
| 98 |
-
kwargs["model"] = model_path
|
| 99 |
-
|
| 100 |
-
if "logits_processors" not in kwargs:
|
| 101 |
-
from . import NaviOCRLogitsProcessor
|
| 102 |
-
kwargs["logits_processors"] = [NaviOCRLogitsProcessor]
|
| 103 |
-
|
| 104 |
-
if 'max_model_len' not in kwargs:
|
| 105 |
-
kwargs["max_model_len"] = CONFIG.MAX_MODEL_LEN
|
| 106 |
-
vllm_async_llm = AsyncLLM.from_engine_args(AsyncEngineArgs(**kwargs))
|
| 107 |
-
|
| 108 |
-
self._models[key] = NaviOCRClient(
|
| 109 |
-
backend=backend,
|
| 110 |
-
model=model,
|
| 111 |
-
processor=processor,
|
| 112 |
-
lmdeploy_engine=lmdeploy_engine,
|
| 113 |
-
vllm_llm=vllm_llm,
|
| 114 |
-
vllm_async_llm=vllm_async_llm,
|
| 115 |
-
server_url=server_url,
|
| 116 |
-
batch_size=batch_size,
|
| 117 |
-
max_concurrency=max_concurrency,
|
| 118 |
-
http_timeout=http_timeout,
|
| 119 |
-
server_headers=server_headers,
|
| 120 |
-
max_retries=max_retries,
|
| 121 |
-
retry_backoff_factor=retry_backoff_factor,
|
| 122 |
-
)
|
| 123 |
-
return self._models[key]
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
NaviOCRMODEL_SERVICE = NaviOCRMODEL()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/vlm_utils/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
__version__ = "0.1.21"
|
|
|
|
|
|
NaviOCR/vlm_utils/vlm_client/vllm_async_engine_client.py
DELETED
|
@@ -1,259 +0,0 @@
|
|
| 1 |
-
import asyncio
|
| 2 |
-
import uuid
|
| 3 |
-
from io import BytesIO
|
| 4 |
-
from typing import TYPE_CHECKING, Sequence
|
| 5 |
-
|
| 6 |
-
if TYPE_CHECKING:
|
| 7 |
-
from vllm.outputs import RequestOutput
|
| 8 |
-
|
| 9 |
-
from PIL import Image
|
| 10 |
-
|
| 11 |
-
from .base_client import (
|
| 12 |
-
DEFAULT_SYSTEM_PROMPT,
|
| 13 |
-
DEFAULT_USER_PROMPT,
|
| 14 |
-
RequestError,
|
| 15 |
-
SamplingParams,
|
| 16 |
-
ServerError,
|
| 17 |
-
UnsupportedError,
|
| 18 |
-
VlmClient,
|
| 19 |
-
)
|
| 20 |
-
from .utils import aio_load_resource, gather_tasks, get_rgb_image
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
class VllmAsyncEngineVlmClient(VlmClient):
|
| 24 |
-
def __init__(
|
| 25 |
-
self,
|
| 26 |
-
vllm_async_llm, # vllm.v1.engine.async_llm.AsyncLLM instance
|
| 27 |
-
prompt: str = DEFAULT_USER_PROMPT,
|
| 28 |
-
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
|
| 29 |
-
sampling_params: SamplingParams | None = None,
|
| 30 |
-
text_before_image: bool = False,
|
| 31 |
-
allow_truncated_content: bool = False,
|
| 32 |
-
max_concurrency: int = 100,
|
| 33 |
-
debug: bool = False,
|
| 34 |
-
):
|
| 35 |
-
super().__init__(
|
| 36 |
-
prompt=prompt,
|
| 37 |
-
system_prompt=system_prompt,
|
| 38 |
-
sampling_params=sampling_params,
|
| 39 |
-
text_before_image=text_before_image,
|
| 40 |
-
allow_truncated_content=allow_truncated_content,
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
try:
|
| 44 |
-
from vllm import SamplingParams
|
| 45 |
-
from vllm.sampling_params import RequestOutputKind
|
| 46 |
-
from vllm.v1.engine.async_llm import AsyncLLM
|
| 47 |
-
except ImportError:
|
| 48 |
-
raise ImportError("Please install vllm to use VllmEngineVlmClient.")
|
| 49 |
-
|
| 50 |
-
if not vllm_async_llm:
|
| 51 |
-
raise ValueError("vllm_async_llm is None.")
|
| 52 |
-
if not isinstance(vllm_async_llm, AsyncLLM):
|
| 53 |
-
raise ValueError(f"vllm_async_llm must be an instance of {AsyncLLM}")
|
| 54 |
-
|
| 55 |
-
self.vllm_async_llm = vllm_async_llm
|
| 56 |
-
if vllm_async_llm.tokenizer is None:
|
| 57 |
-
raise ValueError("vllm_async_llm.tokenizer is None.")
|
| 58 |
-
|
| 59 |
-
tokenizer = vllm_async_llm.tokenizer
|
| 60 |
-
if hasattr(tokenizer, "get_lora_tokenizer"):
|
| 61 |
-
tokenizer = tokenizer.get_lora_tokenizer() # type: ignore
|
| 62 |
-
|
| 63 |
-
self.tokenizer = tokenizer
|
| 64 |
-
self.model_max_length = vllm_async_llm.model_config.max_model_len
|
| 65 |
-
self.VllmSamplingParams = SamplingParams
|
| 66 |
-
self.VllmRequestOutputKind = RequestOutputKind
|
| 67 |
-
self.max_concurrency = max_concurrency
|
| 68 |
-
self.debug = debug
|
| 69 |
-
|
| 70 |
-
def build_messages(self, prompt: str) -> list[dict]:
|
| 71 |
-
prompt = prompt or self.prompt
|
| 72 |
-
messages = []
|
| 73 |
-
if self.system_prompt:
|
| 74 |
-
messages.append({"role": "system", "content": self.system_prompt})
|
| 75 |
-
if "<image>" in prompt:
|
| 76 |
-
prompt_1, prompt_2 = prompt.split("<image>", 1)
|
| 77 |
-
user_messages = [
|
| 78 |
-
*([{"type": "text", "text": prompt_1}] if prompt_1.strip() else []),
|
| 79 |
-
{"type": "image"},
|
| 80 |
-
*([{"type": "text", "text": prompt_2}] if prompt_2.strip() else []),
|
| 81 |
-
]
|
| 82 |
-
elif self.text_before_image:
|
| 83 |
-
user_messages = [
|
| 84 |
-
{"type": "text", "text": prompt},
|
| 85 |
-
{"type": "image"},
|
| 86 |
-
]
|
| 87 |
-
else: # image before text, which is the default behavior.
|
| 88 |
-
user_messages = [
|
| 89 |
-
{"type": "image"},
|
| 90 |
-
{"type": "text", "text": prompt},
|
| 91 |
-
]
|
| 92 |
-
messages.append({"role": "user", "content": user_messages})
|
| 93 |
-
return messages
|
| 94 |
-
|
| 95 |
-
def build_vllm_sampling_params(self, sampling_params: SamplingParams | None):
|
| 96 |
-
sp = self.build_sampling_params(sampling_params)
|
| 97 |
-
|
| 98 |
-
vllm_sp_dict = {
|
| 99 |
-
"temperature": sp.temperature,
|
| 100 |
-
"top_p": sp.top_p,
|
| 101 |
-
"top_k": sp.top_k,
|
| 102 |
-
"presence_penalty": sp.presence_penalty,
|
| 103 |
-
"frequency_penalty": sp.frequency_penalty,
|
| 104 |
-
"repetition_penalty": sp.repetition_penalty,
|
| 105 |
-
# max_tokens should smaller than model max length
|
| 106 |
-
"max_tokens": sp.max_new_tokens if sp.max_new_tokens is not None else self.model_max_length,
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
if sp.no_repeat_ngram_size is not None:
|
| 110 |
-
vllm_sp_dict["extra_args"] = {
|
| 111 |
-
"no_repeat_ngram_size": sp.no_repeat_ngram_size,
|
| 112 |
-
"debug": self.debug,
|
| 113 |
-
}
|
| 114 |
-
|
| 115 |
-
return self.VllmSamplingParams(
|
| 116 |
-
**{k: v for k, v in vllm_sp_dict.items() if v is not None},
|
| 117 |
-
skip_special_tokens=False,
|
| 118 |
-
output_kind=self.VllmRequestOutputKind.FINAL_ONLY,
|
| 119 |
-
)
|
| 120 |
-
|
| 121 |
-
def get_output_content(self, output: "RequestOutput") -> str:
|
| 122 |
-
if not output.finished:
|
| 123 |
-
raise ServerError("The output generation was not finished.")
|
| 124 |
-
|
| 125 |
-
choices = output.outputs
|
| 126 |
-
if not (isinstance(choices, list) and choices):
|
| 127 |
-
raise ServerError("No choices found in the output.")
|
| 128 |
-
|
| 129 |
-
finish_reason = choices[0].finish_reason
|
| 130 |
-
if finish_reason is None:
|
| 131 |
-
raise ServerError("Finish reason is None in the output.")
|
| 132 |
-
if finish_reason == "length":
|
| 133 |
-
if not self.allow_truncated_content:
|
| 134 |
-
raise RequestError("The output was truncated due to length limit.")
|
| 135 |
-
else:
|
| 136 |
-
print("Warning: The output was truncated due to length limit.")
|
| 137 |
-
elif finish_reason != "stop":
|
| 138 |
-
raise RequestError(f"Unexpected finish reason: {finish_reason}")
|
| 139 |
-
|
| 140 |
-
return choices[0].text
|
| 141 |
-
|
| 142 |
-
def predict(
|
| 143 |
-
self,
|
| 144 |
-
image: Image.Image | bytes | str,
|
| 145 |
-
prompt: str = "",
|
| 146 |
-
sampling_params: SamplingParams | None = None,
|
| 147 |
-
priority: int | None = None,
|
| 148 |
-
) -> str:
|
| 149 |
-
raise UnsupportedError(
|
| 150 |
-
"Synchronous predict() is not supported in vllm-async-engine VlmClient(backend). "
|
| 151 |
-
"Please use aio_predict() instead. If you intend to use synchronous client, "
|
| 152 |
-
"please use vllm-engine VlmClient(backend)."
|
| 153 |
-
)
|
| 154 |
-
|
| 155 |
-
def batch_predict(
|
| 156 |
-
self,
|
| 157 |
-
images: Sequence[Image.Image | bytes | str],
|
| 158 |
-
prompts: Sequence[str] | str = "",
|
| 159 |
-
sampling_params: Sequence[SamplingParams | None] | SamplingParams | None = None,
|
| 160 |
-
priority: Sequence[int | None] | int | None = None,
|
| 161 |
-
) -> list[str]:
|
| 162 |
-
raise UnsupportedError(
|
| 163 |
-
"Synchronous batch_predict() is not supported in vllm-async-engine VlmClient(backend). "
|
| 164 |
-
"Please use aio_batch_predict() instead. If you intend to use synchronous client, "
|
| 165 |
-
"please use vllm-engine VlmClient(backend)."
|
| 166 |
-
)
|
| 167 |
-
|
| 168 |
-
async def aio_predict(
|
| 169 |
-
self,
|
| 170 |
-
image: Image.Image | bytes | str,
|
| 171 |
-
prompt: str = "",
|
| 172 |
-
sampling_params: SamplingParams | None = None,
|
| 173 |
-
priority: int | None = None,
|
| 174 |
-
) -> str:
|
| 175 |
-
if isinstance(image, str):
|
| 176 |
-
image = await aio_load_resource(image)
|
| 177 |
-
if not isinstance(image, Image.Image):
|
| 178 |
-
image = Image.open(BytesIO(image))
|
| 179 |
-
image = get_rgb_image(image)
|
| 180 |
-
|
| 181 |
-
chat_prompt: str = self.tokenizer.apply_chat_template(
|
| 182 |
-
self.build_messages(prompt), # type: ignore
|
| 183 |
-
tokenize=False,
|
| 184 |
-
add_generation_prompt=True,
|
| 185 |
-
)
|
| 186 |
-
|
| 187 |
-
vllm_sp = self.build_vllm_sampling_params(sampling_params)
|
| 188 |
-
|
| 189 |
-
generate_kwargs = {}
|
| 190 |
-
if priority is not None:
|
| 191 |
-
generate_kwargs["priority"] = priority
|
| 192 |
-
|
| 193 |
-
last_output = None
|
| 194 |
-
async for output in self.vllm_async_llm.generate(
|
| 195 |
-
prompt={"prompt": chat_prompt, "multi_modal_data": {"image": image}},
|
| 196 |
-
sampling_params=vllm_sp,
|
| 197 |
-
request_id=str(uuid.uuid4()),
|
| 198 |
-
**generate_kwargs,
|
| 199 |
-
):
|
| 200 |
-
last_output = output
|
| 201 |
-
|
| 202 |
-
if last_output is None: # this should not happen
|
| 203 |
-
raise ServerError("No output from the server.")
|
| 204 |
-
result = self.get_output_content(last_output)
|
| 205 |
-
# if 'table' in prompt:
|
| 206 |
-
# print(result)
|
| 207 |
-
return result
|
| 208 |
-
|
| 209 |
-
async def aio_batch_predict(
|
| 210 |
-
self,
|
| 211 |
-
images: Sequence[Image.Image | bytes | str],
|
| 212 |
-
prompts: Sequence[str] | str = "",
|
| 213 |
-
sampling_params: Sequence[SamplingParams | None] | SamplingParams | None = None,
|
| 214 |
-
priority: Sequence[int | None] | int | None = None,
|
| 215 |
-
semaphore: asyncio.Semaphore | None = None,
|
| 216 |
-
use_tqdm=False,
|
| 217 |
-
tqdm_desc: str | None = None,
|
| 218 |
-
) -> list[str]:
|
| 219 |
-
if isinstance(prompts, str):
|
| 220 |
-
prompts = [prompts] * len(images)
|
| 221 |
-
if not isinstance(sampling_params, Sequence):
|
| 222 |
-
sampling_params = [sampling_params] * len(images)
|
| 223 |
-
if not isinstance(priority, Sequence):
|
| 224 |
-
priority = [priority] * len(images)
|
| 225 |
-
|
| 226 |
-
assert len(prompts) == len(images), "Length of prompts and images must match."
|
| 227 |
-
assert len(sampling_params) == len(images), "Length of sampling_params and images must match."
|
| 228 |
-
assert len(priority) == len(images), "Length of priority and images must match."
|
| 229 |
-
|
| 230 |
-
if semaphore is None:
|
| 231 |
-
semaphore = asyncio.Semaphore(self.max_concurrency)
|
| 232 |
-
|
| 233 |
-
async def predict_with_semaphore(
|
| 234 |
-
image: Image.Image | bytes | str,
|
| 235 |
-
prompt: str,
|
| 236 |
-
sampling_params: SamplingParams | None,
|
| 237 |
-
priority: int | None,
|
| 238 |
-
):
|
| 239 |
-
async with semaphore:
|
| 240 |
-
return await self.aio_predict(
|
| 241 |
-
image=image,
|
| 242 |
-
prompt=prompt,
|
| 243 |
-
sampling_params=sampling_params,
|
| 244 |
-
priority=priority,
|
| 245 |
-
)
|
| 246 |
-
|
| 247 |
-
return await gather_tasks(
|
| 248 |
-
tasks=[
|
| 249 |
-
predict_with_semaphore(*args)
|
| 250 |
-
for args in zip(
|
| 251 |
-
images,
|
| 252 |
-
prompts,
|
| 253 |
-
sampling_params,
|
| 254 |
-
priority,
|
| 255 |
-
)
|
| 256 |
-
],
|
| 257 |
-
use_tqdm=use_tqdm,
|
| 258 |
-
tqdm_desc=tqdm_desc,
|
| 259 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/vlm_utils/vlm_client/vllm_engine_client.py
DELETED
|
@@ -1,255 +0,0 @@
|
|
| 1 |
-
import asyncio
|
| 2 |
-
from io import BytesIO
|
| 3 |
-
from typing import TYPE_CHECKING, Sequence
|
| 4 |
-
|
| 5 |
-
if TYPE_CHECKING:
|
| 6 |
-
from vllm.outputs import RequestOutput
|
| 7 |
-
from vllm.sampling_params import SamplingParams as VllmSamplingParams
|
| 8 |
-
|
| 9 |
-
from PIL import Image
|
| 10 |
-
|
| 11 |
-
from .base_client import (
|
| 12 |
-
DEFAULT_SYSTEM_PROMPT,
|
| 13 |
-
DEFAULT_USER_PROMPT,
|
| 14 |
-
RequestError,
|
| 15 |
-
SamplingParams,
|
| 16 |
-
ServerError,
|
| 17 |
-
UnsupportedError,
|
| 18 |
-
VlmClient,
|
| 19 |
-
)
|
| 20 |
-
from .utils import get_rgb_image, load_resource
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
class VllmEngineVlmClient(VlmClient):
|
| 24 |
-
def __init__(
|
| 25 |
-
self,
|
| 26 |
-
vllm_llm, # vllm.LLM instance
|
| 27 |
-
prompt: str = DEFAULT_USER_PROMPT,
|
| 28 |
-
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
|
| 29 |
-
sampling_params: SamplingParams | None = None,
|
| 30 |
-
text_before_image: bool = False,
|
| 31 |
-
allow_truncated_content: bool = False,
|
| 32 |
-
batch_size: int = 0,
|
| 33 |
-
use_tqdm: bool = True,
|
| 34 |
-
debug: bool = False,
|
| 35 |
-
):
|
| 36 |
-
super().__init__(
|
| 37 |
-
prompt=prompt,
|
| 38 |
-
system_prompt=system_prompt,
|
| 39 |
-
sampling_params=sampling_params,
|
| 40 |
-
text_before_image=text_before_image,
|
| 41 |
-
allow_truncated_content=allow_truncated_content,
|
| 42 |
-
)
|
| 43 |
-
|
| 44 |
-
try:
|
| 45 |
-
from vllm import LLM, SamplingParams
|
| 46 |
-
except ImportError:
|
| 47 |
-
raise ImportError("Please install vllm to use VllmEngineVlmClient.")
|
| 48 |
-
|
| 49 |
-
if not vllm_llm:
|
| 50 |
-
raise ValueError("vllm_llm is None.")
|
| 51 |
-
if not isinstance(vllm_llm, LLM):
|
| 52 |
-
raise ValueError("vllm_llm must be an instance of vllm.LLM.")
|
| 53 |
-
|
| 54 |
-
self.vllm_llm = vllm_llm
|
| 55 |
-
self.tokenizer = vllm_llm.get_tokenizer()
|
| 56 |
-
self.model_max_length = vllm_llm.llm_engine.model_config.max_model_len
|
| 57 |
-
self.VllmSamplingParams = SamplingParams
|
| 58 |
-
self.batch_size = batch_size
|
| 59 |
-
self.use_tqdm = use_tqdm
|
| 60 |
-
self.debug = debug
|
| 61 |
-
|
| 62 |
-
def build_messages(self, prompt: str) -> list[dict]:
|
| 63 |
-
prompt = prompt or self.prompt
|
| 64 |
-
messages = []
|
| 65 |
-
if self.system_prompt:
|
| 66 |
-
messages.append({"role": "system", "content": self.system_prompt})
|
| 67 |
-
if "<image>" in prompt:
|
| 68 |
-
prompt_1, prompt_2 = prompt.split("<image>", 1)
|
| 69 |
-
user_messages = [
|
| 70 |
-
*([{"type": "text", "text": prompt_1}] if prompt_1.strip() else []),
|
| 71 |
-
{"type": "image"},
|
| 72 |
-
*([{"type": "text", "text": prompt_2}] if prompt_2.strip() else []),
|
| 73 |
-
]
|
| 74 |
-
elif self.text_before_image:
|
| 75 |
-
user_messages = [
|
| 76 |
-
{"type": "text", "text": prompt},
|
| 77 |
-
{"type": "image"},
|
| 78 |
-
]
|
| 79 |
-
else: # image before text, which is the default behavior.
|
| 80 |
-
user_messages = [
|
| 81 |
-
{"type": "image"},
|
| 82 |
-
{"type": "text", "text": prompt},
|
| 83 |
-
]
|
| 84 |
-
messages.append({"role": "user", "content": user_messages})
|
| 85 |
-
return messages
|
| 86 |
-
|
| 87 |
-
def build_vllm_sampling_params(self, sampling_params: SamplingParams | None):
|
| 88 |
-
sp = self.build_sampling_params(sampling_params)
|
| 89 |
-
|
| 90 |
-
vllm_sp_dict = {
|
| 91 |
-
"temperature": sp.temperature,
|
| 92 |
-
"top_p": sp.top_p,
|
| 93 |
-
"top_k": sp.top_k,
|
| 94 |
-
"presence_penalty": sp.presence_penalty,
|
| 95 |
-
"frequency_penalty": sp.frequency_penalty,
|
| 96 |
-
"repetition_penalty": sp.repetition_penalty,
|
| 97 |
-
# max_tokens should smaller than model max length
|
| 98 |
-
"max_tokens": sp.max_new_tokens if sp.max_new_tokens is not None else self.model_max_length,
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
if sp.no_repeat_ngram_size is not None:
|
| 102 |
-
vllm_sp_dict["extra_args"] = {
|
| 103 |
-
"no_repeat_ngram_size": sp.no_repeat_ngram_size,
|
| 104 |
-
"debug": self.debug,
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
return self.VllmSamplingParams(
|
| 108 |
-
**{k: v for k, v in vllm_sp_dict.items() if v is not None},
|
| 109 |
-
skip_special_tokens=False,
|
| 110 |
-
)
|
| 111 |
-
|
| 112 |
-
def get_output_content(self, output: "RequestOutput") -> str:
|
| 113 |
-
if not output.finished:
|
| 114 |
-
raise ServerError("The output generation was not finished.")
|
| 115 |
-
|
| 116 |
-
choices = output.outputs
|
| 117 |
-
if not (isinstance(choices, list) and choices):
|
| 118 |
-
raise ServerError("No choices found in the output.")
|
| 119 |
-
|
| 120 |
-
finish_reason = choices[0].finish_reason
|
| 121 |
-
if finish_reason is None:
|
| 122 |
-
raise ServerError("Finish reason is None in the output.")
|
| 123 |
-
if finish_reason == "length":
|
| 124 |
-
if not self.allow_truncated_content:
|
| 125 |
-
raise RequestError("The output was truncated due to length limit.")
|
| 126 |
-
else:
|
| 127 |
-
print("Warning: The output was truncated due to length limit.")
|
| 128 |
-
elif finish_reason != "stop":
|
| 129 |
-
raise RequestError(f"Unexpected finish reason: {finish_reason}")
|
| 130 |
-
|
| 131 |
-
return choices[0].text
|
| 132 |
-
|
| 133 |
-
def predict(
|
| 134 |
-
self,
|
| 135 |
-
image: Image.Image | bytes | str,
|
| 136 |
-
prompt: str = "",
|
| 137 |
-
sampling_params: SamplingParams | None = None,
|
| 138 |
-
priority: int | None = None,
|
| 139 |
-
) -> str:
|
| 140 |
-
return self.batch_predict(
|
| 141 |
-
[image], # type: ignore
|
| 142 |
-
[prompt],
|
| 143 |
-
[sampling_params],
|
| 144 |
-
)[0]
|
| 145 |
-
|
| 146 |
-
def batch_predict(
|
| 147 |
-
self,
|
| 148 |
-
images: Sequence[Image.Image | bytes | str],
|
| 149 |
-
prompts: Sequence[str] | str = "",
|
| 150 |
-
sampling_params: Sequence[SamplingParams | None] | SamplingParams | None = None,
|
| 151 |
-
priority: Sequence[int | None] | int | None = None,
|
| 152 |
-
) -> list[str]:
|
| 153 |
-
if not isinstance(prompts, str):
|
| 154 |
-
assert len(prompts) == len(images), "Length of prompts and images must match."
|
| 155 |
-
if isinstance(sampling_params, Sequence):
|
| 156 |
-
assert len(sampling_params) == len(images), "Length of sampling_params and images must match."
|
| 157 |
-
if isinstance(priority, Sequence):
|
| 158 |
-
assert len(priority) == len(images), "Length of priority and images must match."
|
| 159 |
-
|
| 160 |
-
image_objs: list[Image.Image] = []
|
| 161 |
-
for image in images:
|
| 162 |
-
if isinstance(image, str):
|
| 163 |
-
image = load_resource(image)
|
| 164 |
-
if not isinstance(image, Image.Image):
|
| 165 |
-
image = Image.open(BytesIO(image))
|
| 166 |
-
image = get_rgb_image(image)
|
| 167 |
-
image_objs.append(image)
|
| 168 |
-
|
| 169 |
-
if isinstance(prompts, str):
|
| 170 |
-
chat_prompts: list[str] = [
|
| 171 |
-
self.tokenizer.apply_chat_template(
|
| 172 |
-
self.build_messages(prompts), # type: ignore
|
| 173 |
-
tokenize=False,
|
| 174 |
-
add_generation_prompt=True,
|
| 175 |
-
)
|
| 176 |
-
] * len(images)
|
| 177 |
-
else: # isinstance(prompts, Sequence[str])
|
| 178 |
-
chat_prompts: list[str] = [
|
| 179 |
-
self.tokenizer.apply_chat_template(
|
| 180 |
-
self.build_messages(prompt), # type: ignore
|
| 181 |
-
tokenize=False,
|
| 182 |
-
add_generation_prompt=True,
|
| 183 |
-
)
|
| 184 |
-
for prompt in prompts
|
| 185 |
-
]
|
| 186 |
-
|
| 187 |
-
if not isinstance(sampling_params, Sequence):
|
| 188 |
-
vllm_sp_list = [self.build_vllm_sampling_params(sampling_params)] * len(images)
|
| 189 |
-
else:
|
| 190 |
-
vllm_sp_list = [self.build_vllm_sampling_params(sp) for sp in sampling_params]
|
| 191 |
-
|
| 192 |
-
outputs = []
|
| 193 |
-
batch_size = self.batch_size if self.batch_size > 0 else len(images)
|
| 194 |
-
batch_size = max(1, batch_size)
|
| 195 |
-
|
| 196 |
-
for i in range(0, len(images), batch_size):
|
| 197 |
-
batch_image_objs = image_objs[i : i + batch_size]
|
| 198 |
-
batch_chat_prompts = chat_prompts[i : i + batch_size]
|
| 199 |
-
batch_sp_list = vllm_sp_list[i : i + batch_size]
|
| 200 |
-
batch_outputs = self._predict_one_batch(
|
| 201 |
-
batch_image_objs,
|
| 202 |
-
batch_chat_prompts,
|
| 203 |
-
batch_sp_list,
|
| 204 |
-
)
|
| 205 |
-
outputs.extend(batch_outputs)
|
| 206 |
-
|
| 207 |
-
return outputs
|
| 208 |
-
|
| 209 |
-
def _predict_one_batch(
|
| 210 |
-
self,
|
| 211 |
-
image_objs: list[Image.Image],
|
| 212 |
-
chat_prompts: list[str],
|
| 213 |
-
vllm_sampling_params: list["VllmSamplingParams"],
|
| 214 |
-
):
|
| 215 |
-
vllm_prompts = [
|
| 216 |
-
{"prompt": chat_prompt, "multi_modal_data": {"image": image}}
|
| 217 |
-
for chat_prompt, image in zip(chat_prompts, image_objs)
|
| 218 |
-
]
|
| 219 |
-
|
| 220 |
-
outputs = self.vllm_llm.generate(
|
| 221 |
-
prompts=vllm_prompts, # type: ignore
|
| 222 |
-
sampling_params=vllm_sampling_params,
|
| 223 |
-
use_tqdm=self.use_tqdm,
|
| 224 |
-
)
|
| 225 |
-
|
| 226 |
-
return [self.get_output_content(output) for output in outputs]
|
| 227 |
-
|
| 228 |
-
async def aio_predict(
|
| 229 |
-
self,
|
| 230 |
-
image: Image.Image | bytes | str,
|
| 231 |
-
prompt: str = "",
|
| 232 |
-
sampling_params: SamplingParams | None = None,
|
| 233 |
-
priority: int | None = None,
|
| 234 |
-
) -> str:
|
| 235 |
-
raise UnsupportedError(
|
| 236 |
-
"Asynchronous aio_predict() is not supported in vllm-engine VlmClient(backend). "
|
| 237 |
-
"Please use predict() instead. If you intend to use asynchronous client, "
|
| 238 |
-
"please use vllm-async-engine VlmClient(backend)."
|
| 239 |
-
)
|
| 240 |
-
|
| 241 |
-
async def aio_batch_predict(
|
| 242 |
-
self,
|
| 243 |
-
images: Sequence[Image.Image | bytes | str],
|
| 244 |
-
prompts: Sequence[str] | str = "",
|
| 245 |
-
sampling_params: Sequence[SamplingParams | None] | SamplingParams | None = None,
|
| 246 |
-
priority: Sequence[int | None] | int | None = None,
|
| 247 |
-
semaphore: asyncio.Semaphore | None = None,
|
| 248 |
-
use_tqdm=False,
|
| 249 |
-
tqdm_desc: str | None = None,
|
| 250 |
-
) -> list[str]:
|
| 251 |
-
raise UnsupportedError(
|
| 252 |
-
"Asynchronous aio_batch_predict() is not supported in vllm-engine VlmClient(backend). "
|
| 253 |
-
"Please use batch_predict() instead. If you intend to use asynchronous client, "
|
| 254 |
-
"please use vllm-async-engine VlmClient(backend)."
|
| 255 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
NaviOCR/vlm_utils/vlm_client/vllm_v1_no_repeat_ngram.py
DELETED
|
@@ -1,93 +0,0 @@
|
|
| 1 |
-
# Logits Processor for vLLM V1 Engine.
|
| 2 |
-
|
| 3 |
-
from typing import Any
|
| 4 |
-
|
| 5 |
-
import torch
|
| 6 |
-
from vllm.config import VllmConfig
|
| 7 |
-
|
| 8 |
-
try:
|
| 9 |
-
from vllm.v1.sample.logits_processor.interface import (
|
| 10 |
-
BatchUpdate,
|
| 11 |
-
LogitsProcessor,
|
| 12 |
-
MoveDirectionality,
|
| 13 |
-
)
|
| 14 |
-
except ImportError as e:
|
| 15 |
-
raise ImportError("Please install vllm>=0.10.1 to use this feature.") from e
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def _get_int_value(extra_args: dict[str, Any] | None, key: str) -> int | None:
|
| 19 |
-
if isinstance(extra_args, dict):
|
| 20 |
-
arg_value = extra_args.get(key)
|
| 21 |
-
if arg_value is not None:
|
| 22 |
-
try:
|
| 23 |
-
return int(arg_value)
|
| 24 |
-
except Exception:
|
| 25 |
-
pass
|
| 26 |
-
return None
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
class VllmV1NoRepeatNGramLogitsProcessor(LogitsProcessor):
|
| 30 |
-
"""
|
| 31 |
-
Prevents repeating the same n-gram of specified size in the output.
|
| 32 |
-
Inspired by Hugging Face's NoRepeatNGramLogitsProcessor.
|
| 33 |
-
Handled Extra Args:
|
| 34 |
-
no_repeat_ngram_size (int): Size of the n-gram to avoid repeating.
|
| 35 |
-
"""
|
| 36 |
-
|
| 37 |
-
def __init__(self, vllm_config: VllmConfig, device: torch.device, is_pin_memory: bool):
|
| 38 |
-
# mapping: index -> (no_repeat_ngram_size, output_tok_ids, cached_ngrams)
|
| 39 |
-
self.req_info: dict[int, tuple[int, list[int], dict[tuple, list[int]]]] = {}
|
| 40 |
-
|
| 41 |
-
def is_argmax_invariant(self) -> bool:
|
| 42 |
-
return False
|
| 43 |
-
|
| 44 |
-
def update_state(self, batch_update: BatchUpdate | None) -> None:
|
| 45 |
-
if not batch_update:
|
| 46 |
-
return
|
| 47 |
-
|
| 48 |
-
for index in batch_update.removed:
|
| 49 |
-
self.req_info.pop(index, None)
|
| 50 |
-
|
| 51 |
-
for index, params, _, output_tok_ids in batch_update.added:
|
| 52 |
-
val = _get_int_value(params.extra_args, "no_repeat_ngram_size")
|
| 53 |
-
no_repeat_ngram_size = 0 if (val is None or val < 0) else val
|
| 54 |
-
if isinstance(params.extra_args, dict) and params.extra_args.get("debug"):
|
| 55 |
-
print(f"Request {index}: no_repeat_ngram_size = {no_repeat_ngram_size}")
|
| 56 |
-
self.req_info[index] = (no_repeat_ngram_size, output_tok_ids, {})
|
| 57 |
-
|
| 58 |
-
for a_index, b_index, direct in batch_update.moved:
|
| 59 |
-
a_info = self.req_info.pop(a_index, None)
|
| 60 |
-
b_info = self.req_info.pop(b_index, None)
|
| 61 |
-
if a_info is not None:
|
| 62 |
-
self.req_info[b_index] = a_info
|
| 63 |
-
if direct == MoveDirectionality.SWAP and b_info is not None:
|
| 64 |
-
self.req_info[a_index] = b_info
|
| 65 |
-
|
| 66 |
-
def apply(self, logits: torch.Tensor) -> torch.Tensor:
|
| 67 |
-
for index in range(len(logits)):
|
| 68 |
-
req_info = self.req_info.get(index)
|
| 69 |
-
if req_info is None:
|
| 70 |
-
continue
|
| 71 |
-
no_repeat_ngram_size, output_tok_ids, cached_ngrams = req_info
|
| 72 |
-
if no_repeat_ngram_size <= 0:
|
| 73 |
-
continue
|
| 74 |
-
# Skip if there are not enough tokens to form an n-gram
|
| 75 |
-
if len(output_tok_ids) < no_repeat_ngram_size:
|
| 76 |
-
continue
|
| 77 |
-
|
| 78 |
-
# Get the n-gram prefix (all but the last token)
|
| 79 |
-
prev_ngram = tuple(output_tok_ids[-no_repeat_ngram_size:-1])
|
| 80 |
-
last_token = output_tok_ids[-1]
|
| 81 |
-
|
| 82 |
-
# Store this n-gram occurrence
|
| 83 |
-
cached_ngrams.setdefault(prev_ngram, []).append(last_token)
|
| 84 |
-
|
| 85 |
-
# Get the next-token candidates to ban based on current prefix
|
| 86 |
-
current_prefix = tuple(output_tok_ids[-no_repeat_ngram_size + 1 :])
|
| 87 |
-
banned_tokens = cached_ngrams.get(current_prefix, [])
|
| 88 |
-
|
| 89 |
-
# Set the logits of banned tokens to negative infinity
|
| 90 |
-
for token in banned_tokens:
|
| 91 |
-
logits[index][token] = -float("inf")
|
| 92 |
-
|
| 93 |
-
return logits
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/layout.jpg
DELETED
Git LFS Details
|
examples/layout_distorted.jpg
DELETED
Git LFS Details
|
examples/text.png
DELETED
|
Binary file (62 kB)
|
|
|