提交 571454e3 authored 作者: 贺阳's avatar 贺阳

同步的检查

上级 40a9aa79
......@@ -14,6 +14,7 @@
<field name="file" string="PDF File" required="1" filename="attachment_name"/>
<field name="is_upload" readonly="1"/>
<field name="upload_time"/>
<field name="create_date" optional="show"/>
<button name="action_sync" string="SyncTo.." type="object" icon="fa-upload"/>
<field name="attachment_name" invisible="1"/>
</tree>
......
import os
from openai import OpenAI
import requests
import mimetypes
import base64
import fitz # PyMuPDF
import json
from PIL import Image, ImageDraw
client = OpenAI(
# 若没有配置环境变量,请用阿里云百炼API Key将下行替换为:api_key="sk-xxx",
# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
api_key='sk-e41914f0d9c94035a5ae1322e9a61fb1',
# 以下是北京地域base_url,如果使用新加坡地域的模型,需要将base_url替换为:https://dashscope-intl.aliyuncs.com/compatible-mode/v1
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
pdf_path = "C:/Users/Administrator/Desktop/43610236590 (3).pdf"
def pdf_to_images(pdf_path, output_dir='./pdf_pages', dpi=150):
"""
将PDF文件逐页转为PNG图片
:param pdf_path: PDF文件路径
:param output_dir: 输出目录,默认当前目录下pdf_pages文件夹
:param dpi: 渲染分辨率,默认150
:return: 生成的图片路径列表
"""
os.makedirs(output_dir, exist_ok=True)
doc = fitz.open(pdf_path)
image_paths = []
for page_num in range(len(doc)):
page = doc.load_page(page_num)
mat = fitz.Matrix(dpi / 72, dpi / 72)
pix = page.get_pixmap(matrix=mat)
output_path = os.path.join(output_dir, f"page_{page_num + 1}.png")
pix.save(output_path)
image_paths.append(output_path)
doc.close()
return image_paths
def download_image(image_url, save_path='output.png'):
try:
response = requests.get(image_url, stream=True, timeout=300) # 设置超时
response.raise_for_status() # 如果HTTP状态码不是200,则引发异常
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"图像已成功下载到: {save_path}")
except requests.exceptions.RequestException as e:
print(f"图像下载失败: {e}")
def encode_file(file_path):
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type or not mime_type.startswith("image/"):
raise ValueError("不支持或无法识别的图像格式")
with open(file_path, "rb") as image_file:
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对象。"""
# 直接尝试解析
try:
return json.loads(text)
except Exception:
pass
# 尝试提取首尾花括号之间的内容
start = text.find('{')
end = text.rfind('}')
if start != -1 and end != -1 and end > start:
candidate = text[start:end+1]
try:
return json.loads(candidate)
except Exception:
# 将单引号替换为双引号再试一次(有些模型会返回单引号JSON样式)
candidate2 = candidate.replace("'", '"')
try:
return json.loads(candidate2)
except Exception:
return None
return None
def normalize_bbox(bbox, img_w, img_h):
"""
规范化bbox为像素坐标 [x1,y1,x2,y2],并保证边界有效。
支持:
- 像素坐标;
- 归一化坐标(0-1);
- [x1,y1,w,h] 形式(若x2<=x1或y2<=y1则按宽高处理)。
"""
if not isinstance(bbox, (list, tuple)) or len(bbox) != 4:
return None
x1, y1, x2, y2 = bbox
# 如果可能是归一化坐标
if 0 <= x1 <= 1 and 0 <= y1 <= 1 and 0 <= x2 <= 1 and 0 <= y2 <= 1:
x1 = int(round(x1 * img_w))
y1 = int(round(y1 * img_h))
x2 = int(round(x2 * img_w))
y2 = int(round(y2 * img_h))
else:
# 像素坐标或 [x1,y1,w,h]
x1 = int(round(x1))
y1 = int(round(y1))
x2 = int(round(x2))
y2 = int(round(y2))
# 如果x2<=x1或y2<=y1,尝试将其视作宽高
if x2 <= x1 or y2 <= y1:
x2 = x1 + max(1, x2)
y2 = y1 + max(1, y2)
# 修正边界并加上少量边距
if x1 > x2:
x1, x2 = x2, x1
if y1 > y2:
y1, y2 = y2, y1
# 边距按短边的2%(最少5px,最多30px)
short_side = min(img_w, img_h)
margin = max(5, min(30, int(0.02 * short_side)))
x1 = max(0, x1 - margin)
y1 = max(0, y1 - margin)
x2 = min(img_w - 1, x2 + margin)
y2 = min(img_h - 1, y2 + margin)
return [x1, y1, x2, y2]
def erase_regions_on_image(image_path: str, coords_map: dict, save_path: str):
img = Image.open(image_path).convert('RGB')
draw = ImageDraw.Draw(img)
w, h = img.size
for key, bbox in coords_map.items():
nb = normalize_bbox(bbox, w, h)
if nb is None:
print(f"坐标格式错误,跳过 {key}: {bbox}")
continue
x1, y1, x2, y2 = nb
draw.rectangle([x1, y1, x2, y2], fill=(255, 255, 255))
print(f"已抹除 {key} 区域: {nb}")
os.makedirs(os.path.dirname(save_path), exist_ok=True)
img.save(save_path)
print(f"清理后的图片已保存: {save_path}")
def draw_debug_boxes(image_path: str, coords_map: dict, save_path: str):
"""在原图上绘制预测的矩形框用于人工核对。"""
img = Image.open(image_path).convert('RGB')
draw = ImageDraw.Draw(img)
w, h = img.size
for key, bbox in coords_map.items():
nb = normalize_bbox(bbox, w, h)
if nb is None:
print(f"跳过无法解析的坐标: {key} -> {bbox}")
continue
x1, y1, x2, y2 = nb
draw.rectangle([x1, y1, x2, y2], outline=(255, 0, 0), width=3)
draw.text((x1, max(0, y1 - 16)), key, fill=(255, 0, 0))
os.makedirs(os.path.dirname(save_path), exist_ok=True)
img.save(save_path)
print(f"调试框已生成: {save_path}")
def images_to_pdf(image_paths, output_pdf):
os.makedirs(os.path.dirname(output_pdf), exist_ok=True)
pil_images = [Image.open(p).convert('RGB') for p in image_paths]
if not pil_images:
raise RuntimeError("没有需要写入PDF的图片")
first = pil_images[0]
rest = pil_images[1:]
first.save(output_pdf, save_all=True, append_images=rest)
print(f"已生成PDF: {output_pdf}")
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": f"图像分辨率为{img_w}x{img_h}像素。坐标系定义:以原始图像左上角为原点(0,0),x向右增加,y向下增加;不要使用任何预处理(缩放或加黑边)产生的坐标。请仅返回这两个文本的矩形框坐标,且必须是归一化到[0,1]的浮点数(相对于原始图像宽高),返回格式严格为压缩JSON、无任何解释:{{\"AGN\": [x1_rel, y1_rel, x2_rel, y2_rel], \"UCLINK LOGISITICS LTD\": [x3_rel, y3_rel, x4_rel, y4_rel]}}。"},
],
},
],
temperature=0.1,
)
raw_text = completion.choices[0].message.content
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")
draw_debug_boxes(image_paths[0], result, debug_first)
erase_regions_on_image(image_paths[0], result, cleaned_first)
# 合成PDF:第一页使用清理后的图片,其余页沿用原图
final_images = [cleaned_first] + image_paths[1:]
images_to_pdf(final_images, os.path.join(cleaned_dir, "cleaned.pdf"))
差异被折叠。
import os
from openai import OpenAI
import requests
import mimetypes
import base64
import fitz # PyMuPDF
import json
from PIL import Image, ImageDraw
client = OpenAI(
# 若没有配置环境变量,请用阿里云百炼API Key将下行替换为:api_key="sk-xxx",
# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
api_key='sk-e41914f0d9c94035a5ae1322e9a61fb1',
# 以下是北京地域base_url,如果使用新加坡地域的模型,需要将base_url替换为:https://dashscope-intl.aliyuncs.com/compatible-mode/v1
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
pdf_path = "C:/Users/Administrator/Desktop/43610281036.pdf"
def pdf_to_images(pdf_path, output_dir='./pdf_pages', dpi=150):
"""
将PDF文件逐页转为PNG图片
:param pdf_path: PDF文件路径
:param output_dir: 输出目录,默认当前目录下pdf_pages文件夹
:param dpi: 渲染分辨率,默认150
:return: 生成的图片路径列表
"""
os.makedirs(output_dir, exist_ok=True)
doc = fitz.open(pdf_path)
image_paths = []
for page_num in range(len(doc)):
page = doc.load_page(page_num)
mat = fitz.Matrix(dpi / 72, dpi / 72)
pix = page.get_pixmap(matrix=mat)
output_path = os.path.join(output_dir, f"page_{page_num + 1}.png")
pix.save(output_path)
image_paths.append(output_path)
doc.close()
return image_paths
def download_image(image_url, save_path='output.png'):
try:
response = requests.get(image_url, stream=True, timeout=300) # 设置超时
response.raise_for_status() # 如果HTTP状态码不是200,则引发异常
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"图像已成功下载到: {save_path}")
except requests.exceptions.RequestException as e:
print(f"图像下载失败: {e}")
def encode_file(file_path):
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type or not mime_type.startswith("image/"):
raise ValueError("不支持或无法识别的图像格式")
with open(file_path, "rb") as image_file:
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对象。"""
# 直接尝试解析
try:
return json.loads(text)
except Exception:
pass
# 尝试提取首尾花括号之间的内容
start = text.find('{')
end = text.rfind('}')
if start != -1 and end != -1 and end > start:
candidate = text[start:end+1]
try:
return json.loads(candidate)
except Exception:
# 将单引号替换为双引号再试一次(有些模型会返回单引号JSON样式)
candidate2 = candidate.replace("'", '"')
try:
return json.loads(candidate2)
except Exception:
return None
return None
def normalize_bbox(bbox, img_w, img_h):
"""
规范化bbox为像素坐标 [x1,y1,x2,y2],并保证边界有效。
支持:
- 像素坐标;
- 归一化坐标(0-1);
- [x1,y1,w,h] 形式(若x2<=x1或y2<=y1则按宽高处理)。
"""
if not isinstance(bbox, (list, tuple)) or len(bbox) != 4:
return None
x1, y1, x2, y2 = bbox
# 如果可能是归一化坐标
if 0 <= x1 <= 1 and 0 <= y1 <= 1 and 0 <= x2 <= 1 and 0 <= y2 <= 1:
x1 = int(round(x1 * img_w))
y1 = int(round(y1 * img_h))
x2 = int(round(x2 * img_w))
y2 = int(round(y2 * img_h))
else:
# 像素坐标或 [x1,y1,w,h]
x1 = int(round(x1))
y1 = int(round(y1))
x2 = int(round(x2))
y2 = int(round(y2))
# 如果x2<=x1或y2<=y1,尝试将其视作宽高
if x2 <= x1 or y2 <= y1:
x2 = x1 + max(1, x2)
y2 = y1 + max(1, y2)
# 修正边界并加上少量边距
if x1 > x2:
x1, x2 = x2, x1
if y1 > y2:
y1, y2 = y2, y1
# 边距按短边的2%(最少5px,最多30px)
short_side = min(img_w, img_h)
margin = max(5, min(30, int(0.02 * short_side)))
x1 = max(0, x1 - margin)
y1 = max(0, y1 - margin)
x2 = min(img_w - 1, x2 + margin)
y2 = min(img_h - 1, y2 + margin)
return [x1, y1, x2, y2]
def erase_regions_on_image(image_path: str, coords_map: dict, save_path: str):
img = Image.open(image_path).convert('RGB')
draw = ImageDraw.Draw(img)
w, h = img.size
for key, bbox in coords_map.items():
nb = normalize_bbox(bbox, w, h)
if nb is None:
print(f"坐标格式错误,跳过 {key}: {bbox}")
continue
x1, y1, x2, y2 = nb
draw.rectangle([x1, y1, x2, y2], fill=(255, 255, 255))
print(f"已抹除 {key} 区域: {nb}")
os.makedirs(os.path.dirname(save_path), exist_ok=True)
img.save(save_path)
print(f"清理后的图片已保存: {save_path}")
def draw_debug_boxes(image_path: str, coords_map: dict, save_path: str):
"""在原图上绘制预测的矩形框用于人工核对。"""
img = Image.open(image_path).convert('RGB')
draw = ImageDraw.Draw(img)
w, h = img.size
for key, bbox in coords_map.items():
nb = normalize_bbox(bbox, w, h)
if nb is None:
print(f"跳过无法解析的坐标: {key} -> {bbox}")
continue
x1, y1, x2, y2 = nb
draw.rectangle([x1, y1, x2, y2], outline=(255, 0, 0), width=3)
draw.text((x1, max(0, y1 - 16)), key, fill=(255, 0, 0))
os.makedirs(os.path.dirname(save_path), exist_ok=True)
img.save(save_path)
print(f"调试框已生成: {save_path}")
def images_to_pdf(image_paths, output_pdf):
os.makedirs(os.path.dirname(output_pdf), exist_ok=True)
pil_images = [Image.open(p).convert('RGB') for p in image_paths]
if not pil_images:
raise RuntimeError("没有需要写入PDF的图片")
first = pil_images[0]
rest = pil_images[1:]
first.save(output_pdf, save_all=True, append_images=rest)
print(f"已生成PDF: {output_pdf}")
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": f"图像分辨率为{img_w}x{img_h}像素。坐标系定义:以原始图像左上角为原点(0,0),x向右增加,y向下增加;不要使用任何预处理(缩放或加黑边)产生的坐标。请仅返回这两个文本的矩形框坐标,且必须是归一化到[0,1]的浮点数(相对于原始图像宽高),返回格式严格为压缩JSON、无任何解释:{{\"AGN\": [x1_rel, y1_rel, x2_rel, y2_rel], \"UCLINK LOGISITICS LTD\": [x3_rel, y3_rel, x4_rel, y4_rel]}}。"},
],
},
],
)
raw_text = completion.choices[0].message.content
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")
draw_debug_boxes(image_paths[0], result, debug_first)
erase_regions_on_image(image_paths[0], result, cleaned_first)
# 合成PDF:第一页使用清理后的图片,其余页沿用原图
final_images = [cleaned_first] + image_paths[1:]
images_to_pdf(final_images, os.path.join(cleaned_dir, "cleaned.pdf"))
......@@ -351,6 +351,7 @@ class CcBl(models.Model):
bl_sync_log_ids = fields.One2many('cc.bl.sync.log', 'bl_id', string='Bill Of Loading Sync Logs')
# 增加提单状态操作时间:取最新一条提单节点同步信息的操作时间
process_time = fields.Datetime(string='Process Time', compute='_compute_process_time', store=True)
push_remark = fields.Text('Push Remark')
def change_state_by_ship_package(self):
"""
......
......@@ -3,4 +3,5 @@
from . import batch_input_ship_package_statu_wizard
from . import update_bl_status_wizard
from . import batch_get_pod_info_wizard
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论