This commit is contained in:
HuangHai
2026-01-17 10:52:13 +08:00
parent 552f3238d3
commit 24380767a4
9 changed files with 240 additions and 61 deletions

View File

@@ -156,12 +156,10 @@ async def get_station_list(d, service, uploader, max_stations_count=MAX_STATIONS
image_uuid = str(uuid.uuid4())
screenshot_path = take_screenshot(d, image_uuid, save_dir=TEMP_IMAGE_DIR)
# 【优化】不再在每页滚动时检查兔子广告,仅在 Opener 进入时检查一次
# 如果后续发现有其它非兔子广告弹出,可在此处恢复非兔子广告的检测逻辑
ad_top_y_norm = 0.78 # 默认的点击边界 (0.78)
# 新策略:滚动后的页面完全依赖“速度躲兔子”,不再因为靠近底部而丢弃卡片
if scroll_count > 1:
ad_top_y_norm = 1.0
logger.info(f"当前列表页广告安全下边界: {ad_top_y_norm:.2f} (scroll_count={scroll_count})")
# 检查是否已经滚动到底部 (排除状态栏后,内容与上次一致)
current_md5 = Kit.get_image_content_md5(

View File

@@ -4,7 +4,7 @@ import cv2
import numpy as np
import time
import hashlib
from Apps.XinDianTu.Config.Setting import BOTTOM_SAFE_EXCLUDE_RATIO
from Apps.XinDianTu.Config.Setting import SAFE_EXCLUDE_RATIO, BOTTOM_SAFE_EXCLUDE_RATIO
from Config.Config import TEMP_IMAGE_DIR
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
@@ -748,26 +748,7 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
logger.info(f" Found {len(groups)} segment groups.")
# Select the Target Group
# Strategy: Select the LAST group that has at least one 'substantial' segment (H > 150).
# This assumes the Station List is the main content and usually at the bottom (scrolling area).
target_group = []
for g in reversed(groups):
# Check if group has substantial content
has_content = False
for (y1, y2) in g:
if (y2 - y1) > 150:
has_content = True
break
if has_content:
target_group = g
break
if not target_group and segments:
# Fallback to all segments if no substantial group found
target_group = segments
target_group = segments
logger.info(f" Selected group with {len(target_group)} segments.")
# 2. 确定每个卡片的左右边界 (Width Refinement)
@@ -881,10 +862,7 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
max_h = max(heights)
median_h = np.median(heights)
# 策略:如果高度差异较大,丢弃过小的卡片
# 阈值设定为 Median 的 75% (允许一定程度的偏差,如 220 vs 258 是允许的,但 150 vs 250 应被过滤)
# 使用 Median 比 Max 更稳健,避免被单个超高卡片(如广告)带偏
threshold_h = median_h * 0.70
threshold_h = median_h * 0.80
for (y1, y2), card_h in zip(temp_valid_segments, heights):
if card_h < threshold_h:
@@ -914,6 +892,19 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
"cards": []
}
# 顶部安全过滤:丢弃中心落在 SAFE_EXCLUDE_RATIO 以上区域的卡片
filtered_cards = []
for y1, y2, x1, x2 in final_cards:
center_y = (y1 + y2) / 2.0
if center_y < h * SAFE_EXCLUDE_RATIO:
logger.info(
f" Dropping card Y={y1}-{y2} (center={center_y:.1f}) "
f"because it is within top safe area ({SAFE_EXCLUDE_RATIO*100:.0f}%)."
)
continue
filtered_cards.append((y1, y2, x1, x2))
final_cards = filtered_cards
# 准备 _vl.jpg (只画框,不画红点)
vl_img = img.copy()

View File

@@ -1144,40 +1144,89 @@ class ReadImageKit:
# 寻找按钮坐标任务
button_task = asyncio.create_task(ReadImageKit.find_all_time_button_coordinate(image_url_or_path, device_info))
prompt = (
"仅输出JSON对象不含任何说明文字"
"任务1识别图片中充电站的完整名称full_station_name"
f"提示:列表中看到的名称可能是截断的(例如“{station_name}”),请在图片上方找到最匹配的完整名称。"
"任务2识别充电站的详细地址address"
"寻找规则:地址通常紧跟在场站名称下方,或者在距离图标(定位小图)附近,或者在带有导航按钮的同一行。"
"返回包含 full_station_name 和 address 字段的JSON对象例如 {\"full_station_name\": \"完整名称\", \"address\": \"详细地址\"}"
"如果找不到,对应字段返回空字符串"
"严格返回纯JSON格式"
)
prompt = (
"仅输出JSON对象不含任何说明文字"
"任务1识别图片中充电站的完整名称full_station_name"
f"提示:列表中看到的名称可能是截断的(例如“{station_name}”),请在图片上方找到最匹配的完整名称。"
"任务2识别充电站的详细地址address"
"寻找规则:地址通常紧跟在场站名称下方,或者在距离图标(定位小图)附近,或者在带有导航按钮的同一行。"
"若地址跨多行,请合并为一行返回,不要省略"
"返回包含 full_station_name 和 address 字段的JSON对象例如 {\"full_station_name\": \"完整名称\", \"address\": \"详细地址\"}"
"如果图片中能看到任何疑似地址的文字(如街道名、路名、园区名),请尽量提取出来并填入 address而不是返回空字符串"
"实在无法确定时,对应字段返回空字符串。"
"严格返回纯JSON格式。"
)
result = {}
try:
resp = await ReadImageKit._client.chat.completions.create(
model=VL_MODEL_NAME,
timeout=30,
messages=[
{
"role": "user",
"content": [
{"type": "image_url", "image_url": img_input},
{"type": "text", "text": json.dumps(device_info, ensure_ascii=False)},
{"type": "text", "text": prompt},
],
},
],
)
content = resp.choices[0].message.content or ""
raw = ReadImageKit._extract_json(content)
result = json.loads(raw)
except Exception as e:
logger.error(f"解析地址失败 ({station_name}): {e}")
# 首次结果兜底与必要时的重试
if not isinstance(result, dict):
result = {}
full_name = str(result.get("full_station_name") or "").strip()
address = str(result.get("address") or "").strip()
if not full_name and not address:
try:
logger.info(f"地址首次解析为空,针对场站“{station_name}”进行重试。")
retry_prompt = (
"仅输出JSON对象不含任何说明文字"
"请再次仔细查找图片中的充电站名称和详细地址,尽量不要返回空字符串。"
"即便地址不完整,也请返回你能看到的主要关键信息(如道路名、园区名、街道名等)。"
"返回格式仍为 {\"full_station_name\": \"完整名称或列表中的截断名称\", \"address\": \"尽量完整的详细地址\"}。"
"严格返回纯JSON格式。"
)
resp = await ReadImageKit._client.chat.completions.create(
model=VL_MODEL_NAME,
timeout=30,
messages=[
{
"role": "user",
"content": [
{"type": "image_url", "image_url": img_input},
{"type": "text", "text": json.dumps(device_info, ensure_ascii=False)},
{"type": "text", "text": retry_prompt},
],
},
],
)
content = resp.choices[0].message.content or ""
raw = ReadImageKit._extract_json(content)
retry_result = json.loads(raw)
if isinstance(retry_result, dict):
result = retry_result
full_name = str(result.get("full_station_name") or "").strip()
address = str(result.get("address") or "").strip()
except Exception as e:
logger.error(f"地址重试解析失败 ({station_name}): {e}")
# 最终兜底:至少回填一个名称,避免 None
if not full_name:
result["full_station_name"] = station_name or ""
if "address" not in result:
result["address"] = address
result = {}
try:
resp = await ReadImageKit._client.chat.completions.create(
model=VL_MODEL_NAME,
timeout=30,
messages=[
{
"role": "user",
"content": [
{"type": "image_url", "image_url": img_input},
{"type": "text", "text": json.dumps(device_info, ensure_ascii=False)},
{"type": "text", "text": prompt},
],
},
],
)
content = resp.choices[0].message.content or ""
raw = ReadImageKit._extract_json(content)
result = json.loads(raw)
except Exception as e:
logger.error(f"解析地址失败 ({station_name}): {e}")
# 合并按钮坐标
# 合并按钮坐标
try:
button_result = await button_task
if button_result: