'commit'
This commit is contained in:
BIN
Apps/XinDianTu/Config/__pycache__/Setting.cpython-310.pyc
Normal file
BIN
Apps/XinDianTu/Config/__pycache__/Setting.cpython-310.pyc
Normal file
Binary file not shown.
@@ -16,18 +16,21 @@ if project_root not in sys.path:
|
||||
|
||||
import uiautomator2 as u2
|
||||
from Core.BaseCrawler import BaseCrawler
|
||||
from Util import Kit
|
||||
from Util.Kit import take_screenshot
|
||||
from Apps.XinDianTu import Kit
|
||||
from Apps.XinDianTu.Kit import take_screenshot
|
||||
from Util.ObsUtil import ObsUploader
|
||||
from Util.RedisKit import RedisKit
|
||||
from Util.PaddleOCRKit import get_ocr_kit
|
||||
import cv2
|
||||
from Apps.XinDianTu.Service import XinDianTuService
|
||||
from Config.Config import (
|
||||
OBS_TMP_PREFIX, CDN_DOMAIN, SCROLL_DISTANCE_RATIO,
|
||||
OBS_TMP_PREFIX, CDN_DOMAIN, TEMP_IMAGE_DIR
|
||||
)
|
||||
from Apps.XinDianTu.Config.Setting import (
|
||||
SCROLL_DISTANCE_RATIO,
|
||||
MAX_SCROLLS, REDIS_STATION_EXPIRE,
|
||||
WAIT_DETAIL_PAGE_LOAD, WAIT_BACK_TO_LIST, WAIT_AFTER_SCROLL,
|
||||
MAX_CRAWL_DISTANCE, TEMP_IMAGE_DIR
|
||||
MAX_CRAWL_DISTANCE
|
||||
)
|
||||
|
||||
# --- 用户配置区域 ---
|
||||
@@ -57,14 +60,15 @@ logger = logging.getLogger("StationList")
|
||||
|
||||
# 强制设置所有相关模块的日志级别为 INFO,防止被第三方库干扰
|
||||
logging.getLogger("PaddleOCRKit").setLevel(logging.INFO)
|
||||
logging.getLogger("Util.Kit").setLevel(logging.INFO)
|
||||
logging.getLogger("OpenXinDianTu").setLevel(logging.INFO)
|
||||
logging.getLogger("FullProcess").setLevel(logging.INFO)
|
||||
|
||||
|
||||
class XinDianTuCrawler(BaseCrawler):
|
||||
"""
|
||||
新电途小程序爬虫实现
|
||||
"""
|
||||
|
||||
def __init__(self, service=None):
|
||||
super().__init__(service)
|
||||
# 初始化配置参数
|
||||
@@ -90,6 +94,7 @@ class XinDianTuCrawler(BaseCrawler):
|
||||
async def crawl_detail(self, station_info):
|
||||
pass
|
||||
|
||||
|
||||
async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
"""
|
||||
获取场站列表并处理翻页
|
||||
@@ -140,7 +145,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
logger.info(f"截图实际尺寸: {img_w}x{img_h}")
|
||||
# 如果截图尺寸与窗口尺寸不符,更新 device_info 以适配坐标换算
|
||||
if img_w != w or img_h != h:
|
||||
logger.warning(f"检测到截图尺寸 ({img_w}x{img_h}) 与窗口尺寸 ({w}x{h}) 不一致,将以截图尺寸为准进行坐标换算。")
|
||||
logger.warning(
|
||||
f"检测到截图尺寸 ({img_w}x{img_h}) 与窗口尺寸 ({w}x{h}) 不一致,将以截图尺寸为准进行坐标换算。")
|
||||
device_info['width'] = img_w
|
||||
device_info['height'] = img_h
|
||||
except Exception as e:
|
||||
@@ -158,7 +164,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
vl_img_path = screenshot_path.replace(".jpg", "_vl.jpg")
|
||||
|
||||
if not os.path.exists(json_path) or not os.path.exists(vl_img_path):
|
||||
logger.error(f"❌ Step [2/6] 失败: 未生成有效的 JSON 或 VL 图片 (JSON: {os.path.exists(json_path)}, VL: {os.path.exists(vl_img_path)})")
|
||||
logger.error(
|
||||
f"❌ Step [2/6] 失败: 未生成有效的 JSON 或 VL 图片 (JSON: {os.path.exists(json_path)}, VL: {os.path.exists(vl_img_path)})")
|
||||
logger.warning("可能未识别到任何卡片,跳过当前页")
|
||||
continue
|
||||
|
||||
@@ -201,13 +208,13 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
cropped_card = original_img[y1:y2, x1:x2]
|
||||
|
||||
# 识别
|
||||
logger.info(f"正在识别第 {idx+1}/{num_cards} 个卡片 (区域: {rect})...")
|
||||
logger.info(f"正在识别第 {idx + 1}/{num_cards} 个卡片 (区域: {rect})...")
|
||||
t_card_start = time.time()
|
||||
parsed_data = ocr_kit.recognize(cropped_card)
|
||||
t_card_end = time.time()
|
||||
|
||||
# Log detailed OCR result
|
||||
logger.info(f"卡片 {idx+1} OCR 识别耗时: {t_card_end - t_card_start:.2f}s")
|
||||
logger.info(f"卡片 {idx + 1} OCR 识别耗时: {t_card_end - t_card_start:.2f}s")
|
||||
|
||||
if parsed_data and parsed_data.get("station_name"):
|
||||
# 格式化数据以匹配原有结构
|
||||
@@ -232,7 +239,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
"tags": parsed_data.get("tags", []),
|
||||
"parking_info": parsed_data.get("parking", "")
|
||||
}
|
||||
logger.info(f"✅ 成功解析场站: {station_info['station_name']} | 距离: {station_info['distance']}")
|
||||
logger.info(
|
||||
f"✅ 成功解析场站: {station_info['station_name']} | 距离: {station_info['distance']}")
|
||||
|
||||
# 立即记录场站基础状态信息 (电桩、价格等)
|
||||
try:
|
||||
@@ -242,12 +250,13 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
|
||||
stations.append(station_info)
|
||||
else:
|
||||
logger.warning(f"⚠️ 卡片 {idx+1} 未能识别出有效场站名称")
|
||||
logger.warning(f"⚠️ 卡片 {idx + 1} 未能识别出有效场站名称")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Local OCR processing failed: {e}", exc_info=True)
|
||||
|
||||
logger.info(f"Step [5/6] 本地 OCR 解析完成 (总耗时: {time.time() - t_ocr_total:.2f}s) | 成功识别: {len(stations)}/{len(json_metadata.get('cards', []))}")
|
||||
logger.info(
|
||||
f"Step [5/6] 本地 OCR 解析完成 (总耗时: {time.time() - t_ocr_total:.2f}s) | 成功识别: {len(stations)}/{len(json_metadata.get('cards', []))}")
|
||||
|
||||
if not stations:
|
||||
logger.warning("当前页面未检测到任何有效的场站信息")
|
||||
@@ -304,7 +313,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
dist_log_part = f" (解析: {dist_km:.2f}km / 限: {MAX_CRAWL_DISTANCE}km)"
|
||||
|
||||
if dist_km >= MAX_CRAWL_DISTANCE:
|
||||
logger.info(f"当前场站 {name} 距离 {dist_km}km (原始: {distance_str}) 已达到或超过限制 {MAX_CRAWL_DISTANCE}km。")
|
||||
logger.info(
|
||||
f"当前场站 {name} 距离 {dist_km}km (原始: {distance_str}) 已达到或超过限制 {MAX_CRAWL_DISTANCE}km。")
|
||||
max_distance_reached = True
|
||||
except Exception as e:
|
||||
logger.warning(f"解析距离出错: {distance_str}, {e}")
|
||||
@@ -312,7 +322,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
pro_info = f" | PRO会员价: {pro_price}" if pro_price else ""
|
||||
# 确保距离始终显示,如果为空则显示 "未知"
|
||||
display_distance = distance_str if distance_str else "未知"
|
||||
logger.info(f"正在处理第 {s_idx+1} 个场站: {name} | 价格: {price}{pro_info} | 枪: {piles_str} | 距离: {display_distance}{dist_log_part}")
|
||||
logger.info(
|
||||
f"正在处理第 {s_idx + 1} 个场站: {name} | 价格: {price}{pro_info} | 枪: {piles_str} | 距离: {display_distance}{dist_log_part}")
|
||||
|
||||
if max_distance_reached:
|
||||
break
|
||||
@@ -348,11 +359,13 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
if up_success:
|
||||
detail_url = f"https://{CDN_DOMAIN}/{detail_object_key}"
|
||||
# 3. 解析并保存地址 (异步后台处理)
|
||||
|
||||
# 定义异步任务函数
|
||||
async def _process_address_task(task_name, task_url, task_dev_info, task_redis_key):
|
||||
t_addr_task = time.time()
|
||||
try:
|
||||
addr_res = await service.process_station_address(task_name, task_url, device_info=task_dev_info)
|
||||
addr_res = await service.process_station_address(task_name, task_url,
|
||||
device_info=task_dev_info)
|
||||
logger.info(f"Step [解析并保存地址-后台] 耗时: {time.time() - t_addr_task:.4f}s")
|
||||
if addr_res.get("address"):
|
||||
logger.info(f"成功记录地址: {addr_res.get('address')}")
|
||||
@@ -387,7 +400,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
|
||||
# 尝试识别,将调试图片保存到 Images 目录
|
||||
t_find = time.time()
|
||||
pos = Kit.find_expand_button_position(screenshot_path, debug_dir=save_dir, debug_filename_prefix=name)
|
||||
pos = Kit.find_expand_button_position(screenshot_path, debug_dir=save_dir,
|
||||
debug_filename_prefix=name)
|
||||
logger.info(f"Step [识别展开按钮] 耗时: {time.time() - t_find:.4f}s")
|
||||
|
||||
# 清理截图
|
||||
@@ -435,7 +449,8 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
async def _process_price_task(task_name, task_urls, task_dev_info):
|
||||
t_price_task = time.time()
|
||||
try:
|
||||
await service.process_price_schedule(task_name, task_urls, device_info=task_dev_info)
|
||||
await service.process_price_schedule(task_name, task_urls,
|
||||
device_info=task_dev_info)
|
||||
logger.info(f"Step [解析电费时段数据-后台] 耗时: {time.time() - t_price_task:.4f}s")
|
||||
logger.info(f"电费时段数据解析完成: {task_name}")
|
||||
except Exception as e:
|
||||
@@ -491,6 +506,7 @@ async def get_station_list(d, service, uploader, max_scrolls=MAX_SCROLLS):
|
||||
logger.info(f"任务结束,共采集到 {len(all_stations)} 个场站。停止原因: {stop_reason}")
|
||||
return all_stations
|
||||
|
||||
|
||||
def clean_images_dir():
|
||||
"""清理 Images 目录下除保留文件外的所有文件"""
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -517,6 +533,7 @@ def clean_images_dir():
|
||||
|
||||
logger.info(f"清理完成,共删除 {deleted_count} 个文件。")
|
||||
|
||||
|
||||
async def clean_redis_data(redis_kit):
|
||||
"""清除 Redis 中的场站处理记录"""
|
||||
if TEST_CLEAR_REDIS:
|
||||
@@ -528,6 +545,7 @@ async def clean_redis_data(redis_kit):
|
||||
else:
|
||||
logger.info("未发现旧的场站处理记录")
|
||||
|
||||
|
||||
async def main(service=None, do_cleanup=True):
|
||||
# 连接设备
|
||||
d = u2.connect()
|
||||
@@ -572,6 +590,7 @@ async def main(service=None, do_cleanup=True):
|
||||
if should_close_service:
|
||||
await service.close_db()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -3,11 +3,13 @@ import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
import time
|
||||
from Config.Config import BOTTOM_SAFE_EXCLUDE_RATIO, TEMP_IMAGE_DIR
|
||||
from Apps.XinDianTu.Config.Setting import 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")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def read_image(path):
|
||||
"""读取图片,支持中文路径"""
|
||||
try:
|
||||
@@ -16,6 +18,7 @@ def read_image(path):
|
||||
logger.info(f"Error reading image {path}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def save_image(path, img):
|
||||
"""保存图片,支持中文路径"""
|
||||
try:
|
||||
@@ -28,6 +31,7 @@ def save_image(path, img):
|
||||
logger.error(f"Error saving image {path}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# 截图
|
||||
def take_screenshot(d, image_uuid, save_dir=TEMP_IMAGE_DIR):
|
||||
path = f"{save_dir}/{image_uuid}.jpg"
|
||||
@@ -35,6 +39,7 @@ def take_screenshot(d, image_uuid, save_dir=TEMP_IMAGE_DIR):
|
||||
d.screenshot(path)
|
||||
return path
|
||||
|
||||
|
||||
def click_image_template(d, template_path, timeout=5.0, threshold=0.8):
|
||||
"""
|
||||
使用 OpenCV 模板匹配查找并点击图片
|
||||
@@ -112,6 +117,7 @@ def click_image_template(d, template_path, timeout=5.0, threshold=0.8):
|
||||
logger.info(f"Image not found after {timeout}s (Best confidence: {best_val_overall:.2f})")
|
||||
return False
|
||||
|
||||
|
||||
def detect_black_agree_button(image_path, debug_dir=None):
|
||||
"""
|
||||
通过计算机图形学检测黑色的"同意"按钮 (Image 1 场景)
|
||||
@@ -186,7 +192,7 @@ def detect_black_agree_button(image_path, debug_dir=None):
|
||||
if debug_dir:
|
||||
os.makedirs(debug_dir, exist_ok=True)
|
||||
debug_img = img.copy()
|
||||
cv2.rectangle(debug_img, (x, y), (x+cw, y+ch), (0, 0, 255), 2)
|
||||
cv2.rectangle(debug_img, (x, y), (x + cw, y + ch), (0, 0, 255), 2)
|
||||
cv2.circle(debug_img, (center_x, center_y), 5, (0, 255, 0), -1)
|
||||
save_image(os.path.join(debug_dir, "debug_agree_btn.jpg"), debug_img)
|
||||
|
||||
@@ -194,6 +200,7 @@ def detect_black_agree_button(image_path, debug_dir=None):
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
|
||||
"""
|
||||
通过模板匹配检测"关闭(X)"按钮 (Image 2 场景)
|
||||
@@ -264,7 +271,8 @@ def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
|
||||
rel_x = center_x / target_w
|
||||
rel_y = center_y / target_h
|
||||
|
||||
logger.info(f"Ad Close Button Match: Confidence={max_val:.4f}, Scale={best_scale:.2f}, Pos=({center_x}, {center_y}), Rel=({rel_x:.2f}, {rel_y:.2f})")
|
||||
logger.info(
|
||||
f"Ad Close Button Match: Confidence={max_val:.4f}, Scale={best_scale:.2f}, Pos=({center_x}, {center_y}), Rel=({rel_x:.2f}, {rel_y:.2f})")
|
||||
|
||||
is_valid_pos = True
|
||||
|
||||
@@ -279,7 +287,8 @@ def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
|
||||
# 底部中间: Y > 0.6
|
||||
if is_valid_pos and max_val < 0.98: # 如果置信度非常高(0.98+),可能是极其标准的X,暂时放过(除非在禁区)
|
||||
if not ((rel_x > 0.7 and rel_y < 0.5) or (rel_y > 0.6)):
|
||||
logger.warning(f"Ignored match at ({center_x}, {center_y}) - not in typical Close Button regions (Top-Right or Bottom).")
|
||||
logger.warning(
|
||||
f"Ignored match at ({center_x}, {center_y}) - not in typical Close Button regions (Top-Right or Bottom).")
|
||||
is_valid_pos = False
|
||||
|
||||
if max_val >= threshold:
|
||||
@@ -287,12 +296,14 @@ def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
|
||||
# [Safety Check] 底部安全区排除
|
||||
# 如果检测到的关闭按钮位于屏幕底部 BOTTOM_SAFE_EXCLUDE_RATIO 区域内,认为是误判(如误触底部功能按钮)
|
||||
if center_y > (target_h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)):
|
||||
logger.warning(f"Ignored Ad Close Button at ({center_x}, {center_y}) - in Bottom Safety Zone ({int(BOTTOM_SAFE_EXCLUDE_RATIO*100)}%).")
|
||||
logger.warning(
|
||||
f"Ignored Ad Close Button at ({center_x}, {center_y}) - in Bottom Safety Zone ({int(BOTTOM_SAFE_EXCLUDE_RATIO * 100)}%).")
|
||||
# 也可以保存一下调试图
|
||||
if debug_dir:
|
||||
os.makedirs(debug_dir, exist_ok=True)
|
||||
debug_img = target.copy()
|
||||
cv2.rectangle(debug_img, top_left, (top_left[0] + best_w, top_left[1] + best_h), (0, 0, 128), 2) # Dark Red for Safety Ignored
|
||||
cv2.rectangle(debug_img, top_left, (top_left[0] + best_w, top_left[1] + best_h), (0, 0, 128),
|
||||
2) # Dark Red for Safety Ignored
|
||||
cv2.putText(debug_img, f"SAFETY IGNORED",
|
||||
(top_left[0], top_left[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 128), 1)
|
||||
save_image(os.path.join(debug_dir, "debug_ad_close_safety_ignored.jpg"), debug_img)
|
||||
@@ -315,7 +326,8 @@ def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
|
||||
if debug_dir:
|
||||
os.makedirs(debug_dir, exist_ok=True)
|
||||
debug_img = target.copy()
|
||||
cv2.rectangle(debug_img, top_left, (top_left[0] + best_w, top_left[1] + best_h), (0, 165, 255), 2) # Orange for ignored
|
||||
cv2.rectangle(debug_img, top_left, (top_left[0] + best_w, top_left[1] + best_h), (0, 165, 255),
|
||||
2) # Orange for ignored
|
||||
cv2.putText(debug_img, f"IGNORED Pos",
|
||||
(top_left[0], top_left[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 165, 255), 1)
|
||||
save_image(os.path.join(debug_dir, "debug_ad_close_ignored.jpg"), debug_img)
|
||||
@@ -332,6 +344,7 @@ def detect_ad_close_x(image_path, template_path, debug_dir=None, threshold=0.7):
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def detect_any_ad_close(image_path, template_dir, debug_dir=None):
|
||||
"""
|
||||
遍历模板目录下的所有 ad_close*.jpg 进行匹配
|
||||
@@ -348,6 +361,7 @@ def detect_any_ad_close(image_path, template_dir, debug_dir=None):
|
||||
return pos
|
||||
return None
|
||||
|
||||
|
||||
def detect_bottom_close_circle(image_path, debug_dir=None):
|
||||
"""
|
||||
通过几何特征检测底部的圆形关闭按钮 (常见于插屏广告)
|
||||
@@ -379,8 +393,8 @@ def detect_bottom_close_circle(image_path, debug_dir=None):
|
||||
# 2. 霍夫圆变换检测圆形
|
||||
# dp=1.2 (累加器分辨率), minDist=w/5 (圆心最小距离), param1=100 (Canny高阈值), param2=30 (圆心累加阈值), minR=w*0.04, maxR=w*0.1
|
||||
# 缩小最大半径范围,防止识别到过大的按钮(如扫码充电按钮)
|
||||
circles = cv2.HoughCircles(gray_blurred, cv2.HOUGH_GRADIENT, dp=1.2, minDist=w/5,
|
||||
param1=100, param2=30, minRadius=int(w*0.04), maxRadius=int(w*0.09))
|
||||
circles = cv2.HoughCircles(gray_blurred, cv2.HOUGH_GRADIENT, dp=1.2, minDist=w / 5,
|
||||
param1=100, param2=30, minRadius=int(w * 0.04), maxRadius=int(w * 0.09))
|
||||
|
||||
if circles is not None:
|
||||
circles = np.round(circles[0, :]).astype("int")
|
||||
@@ -394,7 +408,7 @@ def detect_bottom_close_circle(image_path, debug_dir=None):
|
||||
global_cx = cx
|
||||
|
||||
# 过滤1: 必须在水平中心附近 (容差 15%,收紧限制)
|
||||
if abs(global_cx - w//2) > (w * 0.15):
|
||||
if abs(global_cx - w // 2) > (w * 0.15):
|
||||
continue
|
||||
|
||||
# 过滤2: 垂直方向限制 (必须在屏幕 80% - 95% 之间)
|
||||
@@ -403,7 +417,7 @@ def detect_bottom_close_circle(image_path, debug_dir=None):
|
||||
if rel_y < 0.80 or rel_y > 0.95:
|
||||
continue
|
||||
|
||||
dist = abs(global_cx - w//2)
|
||||
dist = abs(global_cx - w // 2)
|
||||
if dist < min_dist_to_center:
|
||||
min_dist_to_center = dist
|
||||
best_circle = (global_cx, global_cy, r)
|
||||
@@ -416,7 +430,8 @@ def detect_bottom_close_circle(image_path, debug_dir=None):
|
||||
# [Safety Check] 底部安全区排除
|
||||
# 如果检测到的圆形按钮位于屏幕底部 BOTTOM_SAFE_EXCLUDE_RATIO 区域内,认为是误判(如误触底部扫码充电等)
|
||||
if cy > (h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)):
|
||||
logger.warning(f"Ignored Bottom Circle at ({cx}, {cy}) - in Bottom Safety Zone ({int(BOTTOM_SAFE_EXCLUDE_RATIO*100)}%).")
|
||||
logger.warning(
|
||||
f"Ignored Bottom Circle at ({cx}, {cy}) - in Bottom Safety Zone ({int(BOTTOM_SAFE_EXCLUDE_RATIO * 100)}%).")
|
||||
return None
|
||||
|
||||
logger.info(f"Found Bottom Circle Button via Hough: ({cx}, {cy}), r={r}")
|
||||
@@ -432,6 +447,7 @@ def detect_bottom_close_circle(image_path, debug_dir=None):
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_expand_button_position(image_path, debug_dir=None, debug_filename_prefix=None):
|
||||
"""
|
||||
通过几何特征识别"全部时段"按钮的位置
|
||||
@@ -572,7 +588,7 @@ def get_row_stats(gray):
|
||||
# 中央区域 (用于检测内容)
|
||||
center_x = w // 2
|
||||
strip_w = 100
|
||||
center_strip = gray[:, center_x - 50 : center_x + 50]
|
||||
center_strip = gray[:, center_x - 50: center_x + 50]
|
||||
|
||||
# 边缘区域 (用于检测背景/边距)
|
||||
# 假设边距至少有 10px
|
||||
@@ -584,6 +600,7 @@ def get_row_stats(gray):
|
||||
|
||||
return row_means, row_stds, edge_means
|
||||
|
||||
|
||||
def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
|
||||
"""
|
||||
从图片中裁剪场站卡片
|
||||
@@ -718,7 +735,7 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
|
||||
if segments:
|
||||
current_group = [segments[0]]
|
||||
for i in range(1, len(segments)):
|
||||
prev_end = segments[i-1][1]
|
||||
prev_end = segments[i - 1][1]
|
||||
curr_start = segments[i][0]
|
||||
gap = curr_start - prev_end
|
||||
|
||||
@@ -765,7 +782,7 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
|
||||
# 取中间一段行来分析
|
||||
mid_y = (y1 + y2) // 2
|
||||
sample_h = min(10, y2 - y1)
|
||||
sample_rows = gray[mid_y - sample_h//2 : mid_y + sample_h//2, :]
|
||||
sample_rows = gray[mid_y - sample_h // 2: mid_y + sample_h // 2, :]
|
||||
col_means = np.mean(sample_rows, axis=0)
|
||||
|
||||
# 从左向右找第一个非灰像素
|
||||
@@ -785,7 +802,7 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
|
||||
if x1 >= x2 or (x2 - x1) < w * 0.5:
|
||||
# 如果这个 segment 无法确定宽度,可能不是有效卡片,暂不参与宽度投票
|
||||
# 但为了不漏掉,暂时先记录,用默认值填充
|
||||
logger.warning(f" Warning: Segment {y1}-{y2} has weird width {x2-x1}.")
|
||||
logger.warning(f" Warning: Segment {y1}-{y2} has weird width {x2 - x1}.")
|
||||
pass
|
||||
else:
|
||||
candidate_x1.append(x1)
|
||||
@@ -831,7 +848,8 @@ def crop_cards_from_image(img_path, output_dir=None, save_debug=True):
|
||||
|
||||
for (y1, y2), h in zip(temp_valid_segments, heights):
|
||||
if h < threshold_h:
|
||||
logger.info(f" Filtering out segment Y={y1}-{y2} (H={h}) because it's too short (Threshold={threshold_h:.1f}).")
|
||||
logger.info(
|
||||
f" Filtering out segment Y={y1}-{y2} (H={h}) because it's too short (Threshold={threshold_h:.1f}).")
|
||||
else:
|
||||
final_cards.append((y1, y2, final_x1, final_x2))
|
||||
logger.info(f" Card: Y={y1}-{y2}, X={final_x1}-{final_x2}, H={h}")
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# coding=utf-8
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import os
|
||||
|
||||
import uiautomator2 as u2
|
||||
from Util.Kit import take_screenshot, detect_black_agree_button, click_image_template, detect_ad_close_x, detect_any_ad_close, detect_bottom_close_circle
|
||||
from Util.ObsUtil import ObsUploader
|
||||
from Util.XinDianTuReadImageKit import XinDianTuReadImageKit
|
||||
from Config.Config import OBS_TMP_PREFIX, CDN_DOMAIN, TEMP_IMAGE_DIR
|
||||
|
||||
from Apps.XinDianTu.Kit import take_screenshot, detect_black_agree_button, detect_any_ad_close, \
|
||||
detect_bottom_close_circle
|
||||
from Config.Config import TEMP_IMAGE_DIR
|
||||
|
||||
# pip install adbutils
|
||||
# 配置日志输出,方便调试和监控
|
||||
|
||||
@@ -12,8 +12,6 @@ if project_root not in sys.path:
|
||||
from Apps.XinDianTu.Service import XinDianTuService
|
||||
from Apps.XinDianTu import Opener
|
||||
from Apps.XinDianTu import Crawler
|
||||
from Apps.XinDianTu import Coord as Y4_Coord
|
||||
from Apps.XinDianTu import GetCookie as T2_GetAmapCookie
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
@@ -60,35 +58,6 @@ async def main():
|
||||
logger.error(f"❌ 步骤 2 执行异常: {e}")
|
||||
return
|
||||
|
||||
# 步骤 3: 补全坐标 (调用 Y4_Coord)
|
||||
logger.info(">>> 步骤 3: 启动高德地图坐标回填任务...")
|
||||
try:
|
||||
# 首次尝试,开启 Cookie 检查
|
||||
await Y4_Coord.main(check_cookie_first=True)
|
||||
except Y4_Coord.CookieInvalidError:
|
||||
logger.warning("⚠️ 检测到高德地图 Cookie 失效或缺失。")
|
||||
logger.info(">>> 正在启动 Cookie 获取工具 (T2)...")
|
||||
logger.info("请在弹出的浏览器窗口中完成登录,然后在控制台按回车确认。")
|
||||
|
||||
# 调用同步的 T2 工具
|
||||
# 由于 T2 包含 input() 阻塞,且这是客户端脚本,直接调用即可
|
||||
try:
|
||||
# 传入 force=True 跳过“文件已存在”的询问,因为我们确定它无效
|
||||
# run_in_executor 用于避免完全阻塞事件循环(虽然在这里影响不大)
|
||||
loop = asyncio.get_event_loop()
|
||||
success = await loop.run_in_executor(None, T2_GetAmapCookie.run_login_tool, True)
|
||||
|
||||
if success:
|
||||
logger.info("✅ Cookie 更新成功,正在重新启动坐标回填任务...")
|
||||
# 再次尝试执行,这次可以跳过检查(刚获取完)或者继续检查
|
||||
await Y4_Coord.main(check_cookie_first=False)
|
||||
else:
|
||||
logger.error("❌ Cookie 获取失败或用户取消,跳过坐标回填步骤。")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 调用 T2 工具失败: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 步骤 3 执行异常: {e}")
|
||||
|
||||
finally:
|
||||
# 确保数据库连接关闭
|
||||
if service:
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
|
||||
# Ensure sys path includes root for imports if not already
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
if project_root not in sys.path:
|
||||
sys.path.append(project_root)
|
||||
|
||||
from sqlalchemy import text
|
||||
from Util.XinDianTuReadImageKit import XinDianTuReadImageKit
|
||||
from Apps.XinDianTu.XinDianTuReadImageKit import XinDianTuReadImageKit
|
||||
from DbKit.Db import Db
|
||||
from Config.Config import DB_URL
|
||||
from Model.StationProfile import StationProfile
|
||||
|
||||
@@ -9,13 +9,16 @@ import logging
|
||||
import base64
|
||||
from openai import OpenAI, BadRequestError
|
||||
from Config.Config import (
|
||||
ALY_LLM_API_KEY, VL_MODEL_NAME, VL_MODEL_NAME_AD,
|
||||
ALY_LLM_API_KEY, VL_MODEL_NAME, VL_MODEL_NAME_AD
|
||||
)
|
||||
from Apps.XinDianTu.Config.Setting import (
|
||||
SAFE_EXCLUDE_RATIO, FALLBACK_WIDTH, FALLBACK_HEIGHT,
|
||||
BOTTOM_SAFE_EXCLUDE_RATIO
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XinDianTuReadImageKit:
|
||||
_client = OpenAI(
|
||||
api_key=ALY_LLM_API_KEY,
|
||||
@@ -152,16 +155,20 @@ class XinDianTuReadImageKit:
|
||||
return min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)
|
||||
|
||||
# 优先使用场站名称区域进行点击 (最安全)
|
||||
coords = get_pixel_coords(text_bn, display_width, display_height) or get_pixel_coords(text_bounds, display_width, display_height)
|
||||
coords = get_pixel_coords(text_bn, display_width, display_height) or get_pixel_coords(text_bounds,
|
||||
display_width,
|
||||
display_height)
|
||||
if coords:
|
||||
x1, y1, x2, y2 = coords
|
||||
uia_x = int(x1 + (x2 - x1) / 2)
|
||||
uia_y = int(y1 + (y2 - y1) / 2)
|
||||
logger.info(f"坐标计算: 使用文字区域 -> ({uia_x}, {uia_y}) | 区域: {coords} | 屏幕: {display_width}x{display_height}")
|
||||
logger.info(
|
||||
f"坐标计算: 使用文字区域 -> ({uia_x}, {uia_y}) | 区域: {coords} | 屏幕: {display_width}x{display_height}")
|
||||
|
||||
# 备选:使用整个卡片区域
|
||||
if uia_x is None:
|
||||
coords = get_pixel_coords(bn, display_width, display_height) or get_pixel_coords(bounds, display_width, display_height)
|
||||
coords = get_pixel_coords(bn, display_width, display_height) or get_pixel_coords(bounds, display_width,
|
||||
display_height)
|
||||
if coords:
|
||||
x1, y1, x2, y2 = coords
|
||||
w, h = x2 - x1, y2 - y1
|
||||
@@ -172,18 +179,21 @@ class XinDianTuReadImageKit:
|
||||
# 默认中心点,但稍微偏上一点,避开底部可能的按钮
|
||||
uia_x = int(x1 + w / 2)
|
||||
uia_y = int(y1 + h * 0.4)
|
||||
logger.info(f"坐标计算: 使用卡片区域 -> ({uia_x}, {uia_y}) | 区域: {coords} | 屏幕: {display_width}x{display_height}")
|
||||
logger.info(
|
||||
f"坐标计算: 使用卡片区域 -> ({uia_x}, {uia_y}) | 区域: {coords} | 屏幕: {display_width}x{display_height}")
|
||||
|
||||
# --- 安全过滤:过滤掉屏幕顶部的点击坐标 (通常是广告或菜单) ---
|
||||
if uia_y is not None:
|
||||
# 如果点击点在屏幕顶部 SAFE_EXCLUDE_RATIO 范围内,极大概率是误触广告位,将其排除
|
||||
if uia_y < (display_height * SAFE_EXCLUDE_RATIO):
|
||||
logger.warning(f"安全排除: 坐标 ({uia_x}, {uia_y}) 位于屏幕顶部 {int(SAFE_EXCLUDE_RATIO*100)}% 区域,疑似广告或菜单,已忽略。")
|
||||
logger.warning(
|
||||
f"安全排除: 坐标 ({uia_x}, {uia_y}) 位于屏幕顶部 {int(SAFE_EXCLUDE_RATIO * 100)}% 区域,疑似广告或菜单,已忽略。")
|
||||
return None
|
||||
|
||||
# 如果点击点在屏幕底部 BOTTOM_SAFE_EXCLUDE_RATIO 范围内,极大概率是误触底部功能区(如扫码充电),将其排除
|
||||
if uia_y > (display_height * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)):
|
||||
logger.warning(f"安全排除: 坐标 ({uia_x}, {uia_y}) 位于屏幕底部 {int(BOTTOM_SAFE_EXCLUDE_RATIO*100)}% 区域,疑似底部功能区,已忽略。")
|
||||
logger.warning(
|
||||
f"安全排除: 坐标 ({uia_x}, {uia_y}) 位于屏幕底部 {int(BOTTOM_SAFE_EXCLUDE_RATIO * 100)}% 区域,疑似底部功能区,已忽略。")
|
||||
return None
|
||||
|
||||
if uia_x is not None and uia_y is not None:
|
||||
@@ -293,7 +303,9 @@ class XinDianTuReadImageKit:
|
||||
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
if isinstance(data, dict) and (data.get("station_name_bounds") or data.get("station_name_bounds_norm") or data.get("bounds") or data.get("bounds_norm")):
|
||||
if isinstance(data, dict) and (
|
||||
data.get("station_name_bounds") or data.get("station_name_bounds_norm") or data.get(
|
||||
"bounds") or data.get("bounds_norm")):
|
||||
data = XinDianTuReadImageKit._add_click_point(data, device_info, anchor="station_text")
|
||||
return {
|
||||
"uia_center_x": data.get("uia_center_x"),
|
||||
@@ -339,7 +351,8 @@ class XinDianTuReadImageKit:
|
||||
data = json.loads(raw)
|
||||
if isinstance(data, list):
|
||||
# 过滤掉 None (即被 _add_click_point 排除掉的顶部项)
|
||||
data = [XinDianTuReadImageKit._add_click_point(x, device_info, anchor="station_text") for x in data if x.get("b_use", 1) == 1]
|
||||
data = [XinDianTuReadImageKit._add_click_point(x, device_info, anchor="station_text") for x in data if
|
||||
x.get("b_use", 1) == 1]
|
||||
data = [x for x in data if x is not None]
|
||||
elif isinstance(data, dict):
|
||||
if data.get("b_use", 1) == 1:
|
||||
@@ -389,7 +402,8 @@ class XinDianTuReadImageKit:
|
||||
if tasks:
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
for i, r in zip(idxs, results):
|
||||
if isinstance(r, dict) and r.get("uia_center_x") is not None and r.get("uia_center_y") is not None:
|
||||
if isinstance(r, dict) and r.get("uia_center_x") is not None and r.get(
|
||||
"uia_center_y") is not None:
|
||||
data[i]["uia_center_x"] = r.get("uia_center_x")
|
||||
data[i]["uia_center_y"] = r.get("uia_center_y")
|
||||
|
||||
@@ -491,12 +505,14 @@ class XinDianTuReadImageKit:
|
||||
|
||||
# 顶部安全排除
|
||||
if cy < (img_h * SAFE_EXCLUDE_RATIO):
|
||||
logger.warning(f"VL安全排除: 坐标 ({cx}, {cy}) 位于屏幕顶部 {int(SAFE_EXCLUDE_RATIO*100)}% 区域,已忽略。")
|
||||
logger.warning(
|
||||
f"VL安全排除: 坐标 ({cx}, {cy}) 位于屏幕顶部 {int(SAFE_EXCLUDE_RATIO * 100)}% 区域,已忽略。")
|
||||
continue
|
||||
|
||||
# 底部安全排除
|
||||
if cy > (img_h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO)):
|
||||
logger.warning(f"VL安全排除: 坐标 ({cx}, {cy}) 位于屏幕底部 {int(BOTTOM_SAFE_EXCLUDE_RATIO*100)}% 区域,疑似底部功能区,已忽略。")
|
||||
logger.warning(
|
||||
f"VL安全排除: 坐标 ({cx}, {cy}) 位于屏幕底部 {int(BOTTOM_SAFE_EXCLUDE_RATIO * 100)}% 区域,疑似底部功能区,已忽略。")
|
||||
continue
|
||||
|
||||
item["uia_center_x"] = cx
|
||||
@@ -694,7 +710,7 @@ class XinDianTuReadImageKit:
|
||||
# 计算左右边距的平均亮度,辅助判断(仅记录日志,不强行过滤)
|
||||
margin_w = max(5, int(width * 0.05))
|
||||
l_margin = img_data[y1:y2, 0:margin_w]
|
||||
r_margin = img_data[y1:y2, width-margin_w:width]
|
||||
r_margin = img_data[y1:y2, width - margin_w:width]
|
||||
l_mean = np.mean(l_margin)
|
||||
r_mean = np.mean(r_margin)
|
||||
|
||||
@@ -727,7 +743,7 @@ class XinDianTuReadImageKit:
|
||||
|
||||
# 如果裁剪后宽度太小,可能不是有效卡片
|
||||
if (x2 - x1) < width * 0.5:
|
||||
logger.info(f" [-] 忽略区域 {i}: 裁剪后宽度过小 ({x2-x1})")
|
||||
logger.info(f" [-] 忽略区域 {i}: 裁剪后宽度过小 ({x2 - x1})")
|
||||
continue
|
||||
|
||||
# 保存
|
||||
@@ -743,7 +759,8 @@ class XinDianTuReadImageKit:
|
||||
"center_y": center_y,
|
||||
"y_range": (y1, y2)
|
||||
})
|
||||
logger.info(f" [+] 发现卡片 {i}: Y({y1}-{y2}), H={h}, Crop X({x1}-{x2}), 边缘(L={l_mean:.1f}, R={r_mean:.1f}), 已保存")
|
||||
logger.info(
|
||||
f" [+] 发现卡片 {i}: Y({y1}-{y2}), H={h}, Crop X({x1}-{x2}), 边缘(L={l_mean:.1f}, R={r_mean:.1f}), 已保存")
|
||||
|
||||
logger.info(f"分析完成:识别到 {len(valid_segments)} 个区域")
|
||||
return valid_segments
|
||||
@@ -1284,7 +1301,7 @@ async def test3(station_name: str):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
#asyncio.run(test1())
|
||||
# asyncio.run(test1())
|
||||
station_name = '吉林省看守所充电站'
|
||||
#asyncio.run(test2(station_name))
|
||||
# asyncio.run(test2(station_name))
|
||||
asyncio.run(test3(station_name))
|
||||
|
||||
Binary file not shown.
BIN
Apps/XinDianTu/__pycache__/Kit.cpython-310.pyc
Normal file
BIN
Apps/XinDianTu/__pycache__/Kit.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Apps/XinDianTu/__pycache__/XinDianTuReadImageKit.cpython-310.pyc
Normal file
BIN
Apps/XinDianTu/__pycache__/XinDianTuReadImageKit.cpython-310.pyc
Normal file
Binary file not shown.
Reference in New Issue
Block a user