'commit'
This commit is contained in:
Binary file not shown.
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -1149,9 +1149,11 @@ class ReadImageKit:
|
||||
"任务1:识别图片中充电站的完整名称(full_station_name)。"
|
||||
f"提示:列表中看到的名称可能是截断的(例如“{station_name}”),请在图片上方找到最匹配的完整名称。"
|
||||
"任务2:识别充电站的详细地址(address)。"
|
||||
"寻找规则:地址通常紧跟在场站名称下方,或者在‘距离’图标(定位小图表)附近,或者在带有‘导航’按钮的同一行。"
|
||||
"寻找规则:地址通常紧跟在场站名称下方,或者在“距离”图标(定位小图标)附近,或者在带有“导航”按钮的同一行。"
|
||||
"若地址跨多行,请合并为一行返回,不要省略。"
|
||||
"返回包含 full_station_name 和 address 字段的JSON对象,例如 {\"full_station_name\": \"完整名称\", \"address\": \"详细地址\"}。"
|
||||
"如果找不到,对应字段返回空字符串。"
|
||||
"如果图片中能看到任何疑似地址的文字(如街道名、路名、园区名),请尽量提取出来并填入 address,而不是返回空字符串。"
|
||||
"实在无法确定时,对应字段返回空字符串。"
|
||||
"严格返回纯JSON格式。"
|
||||
)
|
||||
|
||||
@@ -1177,6 +1179,53 @@ class ReadImageKit:
|
||||
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
|
||||
|
||||
# 合并按钮坐标
|
||||
try:
|
||||
button_result = await button_task
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
141
Tools/T_XinDianTu_ListDebug.py
Normal file
141
Tools/T_XinDianTu_ListDebug.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
import uiautomator2 as u2
|
||||
|
||||
from Apps.XinDianTu.Kit import setup_logger, take_screenshot, crop_cards_from_image, clear_temp_dir
|
||||
from Apps.XinDianTu.ReadImageKit import ReadImageKit
|
||||
from Apps.XinDianTu.Config.Setting import SCROLL_DISTANCE_RATIO
|
||||
from Config.Config import TEMP_IMAGE_DIR
|
||||
|
||||
|
||||
logger = setup_logger("XinDianTu.ListDebug")
|
||||
|
||||
|
||||
async def _debug_from_list_image(d, device_info, list_image_path: str, step_prefix: str):
|
||||
if not os.path.exists(list_image_path):
|
||||
logger.error(f"[{step_prefix}] 列表截图不存在: {list_image_path}")
|
||||
return
|
||||
|
||||
logger.info(f"[{step_prefix}] 使用列表截图: {list_image_path}")
|
||||
|
||||
crop_cards_from_image(list_image_path)
|
||||
|
||||
json_path = list_image_path.replace(".jpg", ".json")
|
||||
vl_img_path = list_image_path.replace(".jpg", "_vl.jpg")
|
||||
|
||||
if not os.path.exists(json_path) or not os.path.exists(vl_img_path):
|
||||
logger.error(f"[{step_prefix}] 未生成 JSON 或 _vl 调试图: {json_path}, {vl_img_path}")
|
||||
return
|
||||
|
||||
logger.info(f"[{step_prefix}] 绿框调试图路径: {vl_img_path}")
|
||||
|
||||
with open(json_path, "r", encoding="utf-8") as f:
|
||||
json_metadata = json.load(f)
|
||||
|
||||
cards = json_metadata.get("cards") or []
|
||||
if not cards:
|
||||
logger.error(f"[{step_prefix}] JSON 中未找到任何卡片元数据")
|
||||
return
|
||||
|
||||
stations = await ReadImageKit.parse_vl_image(vl_img_path, json_metadata, device_info=device_info)
|
||||
if not stations:
|
||||
logger.error(f"[{step_prefix}] VL 未识别出任何场站")
|
||||
return
|
||||
|
||||
target_station = None
|
||||
for st in stations:
|
||||
if st and st.get("station_name"):
|
||||
target_station = st
|
||||
break
|
||||
|
||||
if not target_station:
|
||||
logger.error(f"[{step_prefix}] 未找到带有效名称的场站")
|
||||
return
|
||||
|
||||
station_name = target_station.get("station_name")
|
||||
click_x = target_station.get("uia_center_x")
|
||||
click_y = target_station.get("uia_center_y")
|
||||
|
||||
if click_x is None or click_y is None:
|
||||
logger.error(f"[{step_prefix}] 目标场站缺少坐标信息: {target_station}")
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"[{step_prefix}] 准备点击场站: '{station_name}', 点击坐标: ({click_x}, {click_y})"
|
||||
)
|
||||
|
||||
d.click(int(click_x), int(click_y))
|
||||
|
||||
await asyncio.sleep(2.0)
|
||||
|
||||
detail_uuid = f"xdt_debug_{step_prefix}_detail_{int(time.time())}"
|
||||
detail_path = take_screenshot(d, detail_uuid, save_dir=TEMP_IMAGE_DIR)
|
||||
logger.info(f"[{step_prefix}] 二级详情页截图路径: {detail_path}")
|
||||
|
||||
address_data = await ReadImageKit.parse_address(station_name, detail_path, device_info=device_info)
|
||||
full_name = address_data.get("full_station_name", "")
|
||||
address = address_data.get("address", "")
|
||||
|
||||
logger.info(
|
||||
f"[{step_prefix}] VLM 地址识别结果:"
|
||||
f" full_station_name='{full_name}', address='{address}'"
|
||||
)
|
||||
|
||||
d.press("back")
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
|
||||
async def run_debug():
|
||||
logger.info("=== 新电途 列表页兔子与地址识别调试脚本启动 ===")
|
||||
logger.info("请先手动打开微信并进入新电途小程序的场站列表第一页。")
|
||||
|
||||
clear_temp_dir()
|
||||
|
||||
d = u2.connect()
|
||||
w, h = d.window_size()
|
||||
|
||||
device_info = d.info or {}
|
||||
device_info["width"] = w
|
||||
device_info["height"] = h
|
||||
|
||||
logger.info(f"当前设备: {device_info.get('productName')} | 分辨率: {w}x{h}")
|
||||
|
||||
image_uuid_1 = f"xdt_debug_step1_{int(time.time())}"
|
||||
list_path_1 = take_screenshot(d, image_uuid_1, save_dir=TEMP_IMAGE_DIR)
|
||||
logger.info(f"[step1] 列表第一页原始截图路径: {list_path_1}")
|
||||
|
||||
await _debug_from_list_image(d, device_info, list_path_1, step_prefix="step1")
|
||||
|
||||
logger.info("[step2] 向上滑动,让兔子躲起来,然后 1 秒后截图...")
|
||||
d.swipe_ext("up", scale=SCROLL_DISTANCE_RATIO)
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
image_uuid_2 = f"xdt_debug_step2_{int(time.time())}"
|
||||
list_path_2 = take_screenshot(d, image_uuid_2, save_dir=TEMP_IMAGE_DIR)
|
||||
logger.info(f"[step2] 滑动后 1 秒截图(预计兔子已躲藏): {list_path_2}")
|
||||
|
||||
await _debug_from_list_image(d, device_info, list_path_2, step_prefix="step2")
|
||||
|
||||
logger.info("[step3] 再次向上滑动,进入第3页列表,然后 1 秒后截图...")
|
||||
d.swipe_ext("up", scale=SCROLL_DISTANCE_RATIO)
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
image_uuid_3 = f"xdt_debug_step3_{int(time.time())}"
|
||||
list_path_3 = take_screenshot(d, image_uuid_3, save_dir=TEMP_IMAGE_DIR)
|
||||
logger.info(f"[step3] 第3页列表截图路径: {list_path_3}")
|
||||
|
||||
await _debug_from_list_image(d, device_info, list_path_3, step_prefix="step3")
|
||||
|
||||
logger.info("=== 调试脚本完成:已按步骤执行三次进入详情并做地址识别,已包含第3页 ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(run_debug())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("用户手动中断调试脚本。")
|
||||
except Exception as e:
|
||||
logger.exception(f"调试脚本运行异常: {e}")
|
||||
BIN
Tools/__pycache__/T_XinDianTu_ListDebug.cpython-310.pyc
Normal file
BIN
Tools/__pycache__/T_XinDianTu_ListDebug.cpython-310.pyc
Normal file
Binary file not shown.
Reference in New Issue
Block a user