提交 4cd82dd3 authored 作者: 贺阳's avatar 贺阳

ai识别优化

上级 18e31d9e
......@@ -22,7 +22,7 @@
<field name='interval_type'>hours</field>
<field name="nextcall" eval="(DateTime.now().replace(hour=0, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
<field name="numbercall">-1</field>
<field name="active" eval="True"/>
<field name="active" eval="False"/>
</record>
<!-- 清理向导生成的临时附件-->
......
......@@ -43,18 +43,18 @@ class AIImageEditService:
# 构建坐标检测提示词(按照image-to-coordinate.py的格式)
prompt_text = f"""(仅归一化坐标,严格 JSON)
你是一名版面定位助手。请在下图中定位并分别框出以下四个单词:AGN、UCLINK、LOGISITICS、LTD。
你是一名版面定位助手。请在下图中定位并分别框出以下四个单词:AGN、UCLINK、LOGISITICS、LTD。
坐标系与输出要求:
坐标系与输出要求:
- 图像尺寸:宽 {img_w} 像素,高 {img_h} 像素。
- 原点位于图像左上角;x 向右增大,y 向下增大。
- 为每个目标词返回它的最小外接矩形框,边界紧贴字形,不要添加额外边距。
- 返回坐标为相对宽高的归一化浮点数,范围 [0,1],保留 4 位小数;保证 0 ≤ x1 < x2 ≤ 1,0 ≤ y1 < y2 ≤ 1。
- 禁止任何图片预处理(裁剪、缩放、加边距、重采样);坐标必须对应原始图像。
- 严格只输出下面的压缩的 JSON,不要附加解释或其他文本。
- JSON中不要出现不在实例中的参数,例如bbox_2d,确保bbox_norm中有且仅有x1,y1,x2,y2四个参数。
输出 JSON 格式(示例为格式演示,实际数值请识别后填充):"""
- 图像尺寸:宽 {img_w} 像素,高 {img_h} 像素。
- 原点位于图像左上角;x 向右增大,y 向下增大。
- 为每个目标词返回它的最小外接矩形框,边界紧贴字形,不要添加额外边距。
- 返回坐标为相对宽高的归一化浮点数,范围 [0,1],保留 4 位小数;保证 0 ≤ x1 < x2 ≤ 1,0 ≤ y1 < y2 ≤ 1。
- 禁止任何图片预处理(裁剪、缩放、加边距、重采样);坐标必须对应原始图像。
- 目标文字有可能因为被遮挡而无法被准确识别,那么请根据可见部分的位置进行定位。
- 严格只输出下面的压缩的 JSON,不要附加解释或其他文本。
- JSON中不要出现不在实例中的参数,例如bbox_2d。确保bbox_norm中有且仅有x1,y1,x2,y2四个参数,且每个参数都是浮点数,范围[0,1],保留4位小数。
输出 JSON 格式(严格按照示例的格式,实际数值请识别后填充):"""
prompt_text += '[{"text":"AGN","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"UCLINK","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"LOGISITICS","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"LTD","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}}]'
......
......@@ -843,13 +843,14 @@ class BatchGetPodInfoWizard(models.TransientModel):
def _process_pdf_with_ai_image_edit(self, pdf_data, bl_no):
"""
使用AI图片编辑处理PDF:PDF转图片 -> AI抹除文字 -> 图片转回PDF
使用AI图片编辑处理PDF:PDF转图片 -> AI抹除文字 -> 图片转回PDF(按照image-to-coordinate.py的逻辑)
:param pdf_data: PDF二进制数据
:param bl_no: 提单号(用于日志)
:return: 处理后的PDF二进制数据
"""
import fitz # PyMuPDF
import base64
import mimetypes
from PIL import Image
import time
......@@ -861,83 +862,107 @@ class BatchGetPodInfoWizard(models.TransientModel):
# 打开PDF文档
pdf_document = fitz.open(stream=pdf_data, filetype="pdf")
processed_pages = []
processed_images = [] # 存储处理后的PIL图片对象
total_pages = len(pdf_document)
total_ai_time = 0.0 # 累计AI总耗时
# 遍历每一页
_logger.info(f"PDF总页数: {total_pages}")
# 遍历每一页(按照image-to-coordinate.py的逻辑)
for page_num in range(total_pages):
page_start_time = time.time()
page = pdf_document[page_num]
_logger.info(f"正在处理第{page_num + 1}页")
# 将页面转换为图像,使用更高分辨率确保清晰度
mat = fitz.Matrix(3.0, 3.0) # 进一步提高分辨率,从2.0提升到3.0
# 将页面转换为图像(按照image-to-coordinate.py的pdf_to_images函数,使用dpi=150)
dpi = 150
mat = fitz.Matrix(dpi / 72, dpi / 72)
pix = page.get_pixmap(matrix=mat)
# 将pixmap转换为PIL Image对象
img_data = pix.tobytes("png")
# 转换为base64
img_base64 = base64.b64encode(img_data).decode('utf-8')
img = Image.open(io.BytesIO(img_data))
# 使用AI编辑图片,移除指定文字
ai_start_time = time.time()
edited_img_base64 = ai_service.edit_image_remove_text(
img_base64,
text_to_remove="AGN UCLINK LOGISITICS LTD"
)
ai_end_time = time.time()
ai_processing_time = ai_end_time - ai_start_time
total_ai_time += ai_processing_time # 累计AI耗时
# 获取图片尺寸(按照image-to-coordinate.py的逻辑)
img_w, img_h = img.size
_logger.info(f"第{page_num + 1}页页面尺寸: {img_w}x{img_h} 像素")
# 将图片编码为base64(按照image-to-coordinate.py的encode_file函数逻辑)
img_bytes_io = io.BytesIO()
img.save(img_bytes_io, format='PNG')
img_bytes_io.seek(0)
encoded_string = base64.b64encode(img_bytes_io.read()).decode('utf-8')
mime_type = 'image/png'
img_base64 = f"data:{mime_type};base64,{encoded_string}"
# 使用AI编辑图片,移除指定文字(带重试机制)
edited_img_base64 = None
ai_processing_time = 0.0
max_retries = 2 # 最多尝试2次(首次+1次重试)
for attempt in range(1, max_retries + 1):
ai_start_time = time.time()
try:
# 调用AI服务(使用base64编码的图片数据,不带data:前缀)
edited_img_base64_raw = ai_service.edit_image_remove_text(
encoded_string, # 传入不带data:前缀的base64字符串
text_to_remove="AGN UCLINK LOGISITICS LTD"
)
ai_end_time = time.time()
attempt_time = ai_end_time - ai_start_time
ai_processing_time += attempt_time # 累计AI耗时
total_ai_time += attempt_time # 累计总AI耗时
if edited_img_base64_raw:
edited_img_base64 = edited_img_base64_raw
_logger.info(f"第{page_num + 1}页AI处理成功(第{attempt}次尝试),耗时: {attempt_time:.2f}秒")
break
else:
if attempt < max_retries:
_logger.warning(f"第{page_num + 1}页AI处理失败(第{attempt}次尝试),将重试,耗时: {attempt_time:.2f}秒")
else:
_logger.warning(f"第{page_num + 1}页AI处理失败(第{attempt}次尝试,已用尽重试),耗时: {attempt_time:.2f}秒")
except Exception as e:
ai_end_time = time.time()
attempt_time = ai_end_time - ai_start_time
ai_processing_time += attempt_time
total_ai_time += attempt_time
_logger.error(f"第{page_num + 1}页AI处理异常(第{attempt}次尝试): {str(e)},耗时: {attempt_time:.2f}秒")
if attempt < max_retries:
_logger.info(f"第{page_num + 1}页将进行第{attempt + 1}次重试")
edited_img_base64 = None
if edited_img_base64:
# 解码base64图片数据
# 解码base64图片数据并转换为PIL Image对象(按照image-to-coordinate.py的逻辑)
edited_img_data = base64.b64decode(edited_img_base64)
# 保存处理后的图片数据
processed_pages.append({
'img_data': edited_img_data,
'is_edited': True
})
_logger.info(f"第{page_num + 1}页AI处理成功,耗时: {ai_processing_time:.2f}秒")
edited_img = Image.open(io.BytesIO(edited_img_data)).convert('RGB')
processed_images.append(edited_img)
_logger.info(f"第{page_num + 1}页AI处理最终成功,总耗时: {ai_processing_time:.2f}秒")
else:
_logger.warning(f"第{page_num + 1}页AI处理失败,使用原始页面,耗时: {ai_processing_time:.2f}秒")
_logger.warning(f"第{page_num + 1}页AI处理最终失败(已重试),使用原始页面,总耗时: {ai_processing_time:.2f}秒")
# 如果AI处理失败,使用原始图片
processed_pages.append({
'img_data': img_data,
'is_edited': False
})
processed_images.append(img.convert('RGB'))
page_end_time = time.time()
page_processing_time = page_end_time - page_start_time
_logger.info(f"第{page_num + 1}页总处理时间: {page_processing_time:.2f}秒")
# 创建新的PDF文档
pdf_document.close()
# 将处理后的图片转换为PDF(按照image-to-coordinate.py的images_to_pdf函数逻辑)
pdf_creation_start = time.time()
output_doc = fitz.open()
for page_info in processed_pages:
img_data = page_info['img_data']
is_edited = page_info['is_edited']
# 将图片加载到内存
img_bytes_io = io.BytesIO(img_data)
img = Image.open(img_bytes_io)
# 创建新页面
page = output_doc.new_page(
width=img.width,
height=img.height
)
# 将图片插入PDF页面
page.insert_image(
fitz.Rect(0, 0, img.width, img.height),
stream=img_data,
)
if not processed_images:
_logger.error("没有需要写入PDF的图片")
return None
# 保存处理后的PDF
# 使用PIL的save方法将图片保存为PDF(按照image-to-coordinate.py的逻辑)
output_buffer = io.BytesIO()
output_doc.save(output_buffer, garbage=4, deflate=True)
output_doc.close()
pdf_document.close()
first = processed_images[0]
rest = processed_images[1:] # 按照image-to-coordinate.py的逻辑,直接使用切片
# 按照image-to-coordinate.py的images_to_pdf函数:first.save(output_pdf, save_all=True, append_images=rest)
# 即使rest是空列表,也直接传入(PIL会正确处理)
first.save(output_buffer, format='PDF', save_all=True, append_images=rest)
output_buffer.seek(0)
pdf_creation_end = time.time()
result_data = output_buffer.getvalue()
......
......@@ -11,8 +11,8 @@
<sheet>
<!-- <group> -->
<group>
<field name="remove_specified_text" readonly="1" widget="boolean_toggle"
attrs="{'invisible': [('pdf_file', '!=', False)]}"/>
<!-- attrs="{'invisible': [('pdf_file', '!=', False)]}" -->
<field name="remove_specified_text" readonly="1" widget="boolean_toggle"/>
<field name="skip_ocr_direct_ai" readonly="0" widget="boolean_toggle"
attrs="{'invisible': [('pdf_file', '!=', False)]}"/>
</group>
......@@ -30,7 +30,7 @@
<div class="alert alert-info" role="alert">
<strong>Description:</strong> <!-- 说明: -->
<ul>
<li attrs="{'invisible': [('pdf_file', '!=', False)]}">
<li>
<strong>Remove Specified Text:</strong>
Remove specified text (AGN, UCLINK LOGISITICS LTD) from PDF files
</li> <!-- 涂抹指定文字:对PDF文件中的指定文字进行涂抹处理 -->
......
......@@ -16,7 +16,7 @@ client = OpenAI(
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
pdf_path = "C:/Users/Administrator/Desktop/43610281036.pdf"
pdf_path = "./43610236590 (3).pdf"
def pdf_to_images(pdf_path, output_dir='./pdf_pages', dpi=150):
"""
......@@ -28,7 +28,9 @@ def pdf_to_images(pdf_path, output_dir='./pdf_pages', dpi=150):
"""
os.makedirs(output_dir, exist_ok=True)
doc = fitz.open(pdf_path)
image_paths = []
print(f"PDF总页数: {len(doc)}")
for page_num in range(len(doc)):
page = doc.load_page(page_num)
mat = fitz.Matrix(dpi / 72, dpi / 72)
......@@ -60,12 +62,6 @@ def encode_file(file_path):
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
return f"data:{mime_type};base64,{encoded_string}"
image_paths = pdf_to_images(pdf_path)
image_base64 = encode_file(image_paths[0])
# 获取图片分辨率,并在提示中要求模型按比例(相对宽高的0-1浮点数)返回坐标
img_w, img_h = Image.open(image_paths[0]).size
print(f"页面尺寸: {img_w}x{img_h} 像素")
def safe_extract_json(text: str):
"""从模型返回文本中尽可能鲁棒地提取JSON对象。"""
# 直接尝试解析
......@@ -268,69 +264,76 @@ def images_to_pdf(image_paths, output_pdf):
first.save(output_pdf, save_all=True, append_images=rest)
print(f"已生成PDF: {output_pdf}")
text = f"""(仅归一化坐标,严格 JSON)
你是一名版面定位助手。请在下图中定位并分别框出以下四个单词:AGN、UCLINK、LOGISITICS、LTD。
坐标系与输出要求:
- 图像尺寸:宽 {img_w} 像素,高 {img_h} 像素。
- 原点位于图像左上角;x 向右增大,y 向下增大。
- 为每个目标词返回它的最小外接矩形框,边界紧贴字形,不要添加额外边距。
- 返回坐标为相对宽高的归一化浮点数,范围 [0,1],保留 4 位小数;保证 0 ≤ x1 < x2 ≤ 1,0 ≤ y1 < y2 ≤ 1。
- 禁止任何图片预处理(裁剪、缩放、加边距、重采样);坐标必须对应原始图像。
- 严格只输出下面的压缩的 JSON,不要附加解释或其他文本。
- JSON中不要出现不在实例中的参数,例如bbox_2d,确保bbox_norm中有且仅有x1,y1,x2,y2四个参数。
输出 JSON 格式(示例为格式演示,实际数值请识别后填充):"""
text += '[{"text":"AGN","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"UCLINK","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"LOGISITICS","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"LTD","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}}]'
# 记录AI处理开始时间
ai_start_time = time.time()
completion = client.chat.completions.create(
model="qwen3-vl-plus", # 此处以qwen3-vl-plus为例,可按需更换模型名称。模型列表:https://help.aliyun.com/zh/model-studio/models
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": image_base64
image_paths = pdf_to_images(pdf_path)
final_images = []
location_map = ''
for index, image_path in enumerate(image_paths):
image_base64 = encode_file(image_path)
# 获取图片分辨率,并在提示中要求模型按比例(相对宽高的0-1浮点数)返回坐标
img_w, img_h = Image.open(image_path).size
print(f"页面尺寸: {img_w}x{img_h} 像素")
text = f"""(仅归一化坐标,严格 JSON)
你是一名版面定位助手。请在下图中定位并分别框出以下四个单词:AGN、UCLINK、LOGISITICS、LTD。
坐标系与输出要求:
- 图像尺寸:宽 {img_w} 像素,高 {img_h} 像素。
- 原点位于图像左上角;x 向右增大,y 向下增大。
- 为每个目标词返回它的最小外接矩形框,边界紧贴字形,不要添加额外边距。
- 返回坐标为相对宽高的归一化浮点数,范围 [0,1],保留 4 位小数;保证 0 ≤ x1 < x2 ≤ 1,0 ≤ y1 < y2 ≤ 1。
- 禁止任何图片预处理(裁剪、缩放、加边距、重采样);坐标必须对应原始图像。
- 目标文字有可能因为被遮挡而无法被准确识别,那么请根据可见部分的位置进行定位。
- 严格只输出下面的压缩的 JSON,不要附加解释或其他文本。
- JSON中不要出现不在实例中的参数,例如bbox_2d。确保bbox_norm中有且仅有x1,y1,x2,y2四个参数,且每个参数都是浮点数,范围[0,1],保留4位小数。
输出 JSON 格式(严格按照示例的格式):"""
text += '[{"text":"AGN","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"UCLINK","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"LOGISITICS","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"LTD","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}}]'
completion = client.chat.completions.create(
model="qwen3-vl-plus", # 此处以qwen3-vl-plus为例,可按需更换模型名称。模型列表:https://help.aliyun.com/zh/model-studio/models
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": image_base64
},
},
},
{"type": "text", "text": text},
],
},
],
temperature=0.1,
)
# 记录AI处理结束时间
ai_end_time = time.time()
ai_processing_time = ai_end_time - ai_start_time
raw_text = completion.choices[0].message.content
# raw_text = '```json[{"bbox_norm": {"x1": 0.1028, "y1": 0.1934, "x2": 0.1325, "y2": 0.2006}, "text": "AGN", "occurrence_index": 0},{"bbox_norm": {"x1": 0.1028, "y1": 0.2057, "x2": 0.1608, "y2": 0.2165}, "text": "UCLINK", "occurrence_index": 0},{"bbox_norm": {"x1": 0.1677, "y1": 0.2057, "x2": 0.2657, "y2": 0.2165}, "text": "LOGISITICS", "occurrence_index": 0},{"bbox_norm": {"x1": 0.2726, "y1": 0.2057, "x2": 0.3023, "y2": 0.2165}, "text": "LTD", "occurrence_index": 0}]```'
print(raw_text)
result = safe_extract_json(raw_text)
if result is None or not isinstance(result, dict):
raise RuntimeError("模型返回内容无法解析为JSON坐标,请检查返回格式。")
# 只处理第一页:将抹除后的图片写入 output/cleaned_page_1.png,然后重新生成PDF
cleaned_dir = os.path.join("./output")
cleaned_first = os.path.join(cleaned_dir, "cleaned_page_1.png")
debug_first = os.path.join(cleaned_dir, "debug_page_1.png")
coords_map = convert_ai_json_to_coords_map(result, img_w, img_h)
if not coords_map:
raise RuntimeError("无法从AI返回中提取矩形框坐标,请检查输出格式或提示词。")
print(f"解析并统一后的坐标字典: {coords_map}")
draw_debug_boxes(image_paths[0], coords_map, debug_first)
erase_regions_on_image(image_paths[0], coords_map, cleaned_first)
# 合成PDF:第一页使用清理后的图片,其余页沿用原图
final_images = [cleaned_first] + image_paths[1:]
images_to_pdf(final_images, os.path.join(cleaned_dir, "cleaned.pdf"))
{"type": "text", "text": text},
],
},
],
temperature=0.1,
)
raw_text = completion.choices[0].message.content
# raw_text = '```json[{"bbox_norm": {"x1": 0.1028, "y1": 0.1934, "x2": 0.1325, "y2": 0.2006}, "text": "AGN", "occurrence_index": 0},{"bbox_norm": {"x1": 0.1028, "y1": 0.2057, "x2": 0.1608, "y2": 0.2165}, "text": "UCLINK", "occurrence_index": 0},{"bbox_norm": {"x1": 0.1677, "y1": 0.2057, "x2": 0.2657, "y2": 0.2165}, "text": "LOGISITICS", "occurrence_index": 0},{"bbox_norm": {"x1": 0.2726, "y1": 0.2057, "x2": 0.3023, "y2": 0.2165}, "text": "LTD", "occurrence_index": 0}]```'
print(raw_text)
result = safe_extract_json(raw_text)
if not location_map:
location_map = json.dumps(result['rects'], ensure_ascii=False)
if result is None or not isinstance(result, dict):
raise RuntimeError("模型返回内容无法解析为JSON坐标,请检查返回格式。")
# 只处理第一页:将抹除后的图片写入 output/cleaned_page_1.png,然后重新生成PDF
cleaned_dir = os.path.join("./output")
cleaned_first = os.path.join(cleaned_dir, f"cleaned_page_{index}.png")
debug_first = os.path.join(cleaned_dir, f"debug_page_{index}.png")
coords_map = convert_ai_json_to_coords_map(result, img_w, img_h)
if not coords_map:
raise RuntimeError("无法从AI返回中提取矩形框坐标,请检查输出格式或提示词。")
print(f"解析并统一后的坐标字典: {coords_map}")
draw_debug_boxes(image_path, coords_map, debug_first)
erase_regions_on_image(image_path, coords_map, cleaned_first)
# 合成PDF:第一页使用清理后的图片,其余页沿用原图
final_images.append(cleaned_first)
images_to_pdf(final_images, os.path.join(cleaned_dir, f"cleaned_page.pdf"))
end_time = time.time()
total_time = end_time - begin_time
print(f"总耗时: {total_time:.2f} 秒")
print(f"AI处理耗时: {ai_processing_time:.2f} 秒(AI API调用时间)")
print(f"耗时: {end_time - begin_time} 秒")
......@@ -53,6 +53,9 @@
</div>
</button>
</button>
<field name="process_time" position="after">
<field name="push_remark" readonly="1" attrs="{'invisible': [('push_remark', '=', '')]}"/>
</field>
<notebook position="inside">
<page string="Sync Log">
<field name="bl_sync_log_ids" widget="one2many_list"/>
......@@ -60,9 +63,6 @@
<field name="is_bl_sync" string="Is Sync" readonly="1"/>
<field name="state_explain" string="State Explain"/>
</group>
<group attrs="{'invisible': [('push_remark', '=', '')]}" >
<field name="push_remark" readonly="1"/>
</group>
</page>
</notebook>
</field>
......@@ -74,13 +74,13 @@
<field name="model">cc.bl</field>
<field name="inherit_id" ref="ccs_base.search_cc_bl_view"/>
<field name="arch" type="xml">
<filter name="filter_state_not_finished" position="after">
<filter name="filter_state_not_finished" position="after">
<separator/>
<filter string="Push Failed" name="filter_push_failed" domain="[('push_remark', '!=', '')]"/>
</filter>
</field>
</record>
</field>
</record>
<record id="action_batch_sync_package_status" model="ir.actions.server">
<field name="name">Batch Sync Package Status</field>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论