'commit'
This commit is contained in:
@@ -11,7 +11,7 @@ if project_root not in sys.path:
|
||||
|
||||
from Util.VLMKit import VLMKit
|
||||
from Apps.TeLaiDian.Kit import draw_rectangles, detect_cards_cv, setup_logger, read_image, detect_wide_rounded_card_cv
|
||||
from Apps.TeLaiDian.Config.Setting import SAFE_EXCLUDE_RATIO, BOTTOM_SAFE_EXCLUDE_RATIO
|
||||
from Apps.TeLaiDian.Config.Setting import SAFE_EXCLUDE_RATIO, BOTTOM_SAFE_EXCLUDE_RATIO, MIN_CARD_HEIGHT
|
||||
|
||||
# 初始化日志
|
||||
logger = setup_logger("ReadImageKit")
|
||||
@@ -242,36 +242,30 @@ class ReadImageKit:
|
||||
"""
|
||||
分析场站列表页图片,提取场站位置和基本信息
|
||||
"""
|
||||
cv_bboxes = detect_cards_cv(image_path, top_ratio=SAFE_EXCLUDE_RATIO, bottom_ratio=BOTTOM_SAFE_EXCLUDE_RATIO)
|
||||
prompt = """
|
||||
分析这张充电站列表截图,提取所有真实的充电站卡片。
|
||||
|
||||
if cv_bboxes:
|
||||
img = read_image(image_path)
|
||||
h, w = img.shape[:2] if img is not None else (2400, 1080)
|
||||
y_threshold = h * 0.58
|
||||
# 在最终绘制阶段再生成可视化,避免早期绘制产生误导
|
||||
prompt = f"""
|
||||
图片下半部分可能包含多个充电站卡片,请按从上到下识别这些卡片的名称与地址。
|
||||
|
||||
输出格式为 JSON 数组。
|
||||
每个对象包含:
|
||||
- "name": 场站名称
|
||||
- "address": 场站地址
|
||||
- "is_valid": true/false (是否为真实的场站卡片)
|
||||
"""
|
||||
else:
|
||||
prompt = """
|
||||
分析这张充电站列表截图,提取所有充电站卡片信息。
|
||||
|
||||
**重要限制**:
|
||||
1. 忽略页面上半部分(坐标 y < 500)的所有内容,包括顶部的搜索框、广告 Banner 和“PLUS会员/我的卡券”等图标。
|
||||
2. 仅提取下方重复出现的、包含“充电站”名称的矩形卡片。
|
||||
|
||||
输出格式为 JSON 数组,每个对象包含:
|
||||
- "name": 场站名称
|
||||
- "address": 场站地址
|
||||
- "point": 场站卡片的中心点击坐标 [x, y]
|
||||
- "bbox": 场站卡片的边界框 [x1, y1, x2, y2]
|
||||
"""
|
||||
要求:
|
||||
1. 忽略页面上半部分(如顶部导航栏、搜索框、广告 Banner、筛选标签等)。
|
||||
2. 仅识别下半部分一条条“充电站卡片”,每张卡片通常包含:场站名称、评分、最近充电时间、距离、价格、快/慢空闲数量等。
|
||||
3. 不要把同一张卡片拆成多块;每条场站只对应一个矩形框。
|
||||
|
||||
对于每张卡片,请输出:
|
||||
- name: 场站名称
|
||||
- address: 场站地址(如果无法确定可置为 null)
|
||||
- point: 卡片中心点击坐标 [x, y],使用归一化坐标 [0-1000](0 表示最左/最上,1000 表示最右/最下)
|
||||
- bbox: 卡片外接矩形边界 [x1, y1, x2, y2],同样使用归一化坐标 [0-1000]
|
||||
|
||||
以 JSON 数组形式输出,例如:
|
||||
[
|
||||
{
|
||||
"name": "某某充电站",
|
||||
"address": "某某路 100 号",
|
||||
"point": [500, 750],
|
||||
"bbox": [50, 600, 950, 820]
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
try:
|
||||
res_text = await self.vlm.analyze_image(image_path, prompt)
|
||||
@@ -279,82 +273,79 @@ class ReadImageKit:
|
||||
vlm_results = json.loads(json_str)
|
||||
|
||||
final_stations = []
|
||||
if cv_bboxes and isinstance(vlm_results, list):
|
||||
img = read_image(image_path)
|
||||
h, w = img.shape[:2] if img is not None else (2400, 1080)
|
||||
y_threshold = h * 0.58
|
||||
bottom_threshold = int(h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO))
|
||||
use_bboxes = []
|
||||
for b in cv_bboxes:
|
||||
cy = (b[1] + b[3]) // 2
|
||||
if cy >= y_threshold:
|
||||
use_bboxes.append(b)
|
||||
for res in vlm_results:
|
||||
if not res:
|
||||
vlm_list = vlm_results if isinstance(vlm_results, list) else []
|
||||
img = read_image(image_path)
|
||||
h, w = img.shape[:2] if img is not None else (2400, 1080)
|
||||
y_threshold = h * SAFE_EXCLUDE_RATIO
|
||||
bottom_threshold = int(h * (1 - BOTTOM_SAFE_EXCLUDE_RATIO))
|
||||
|
||||
def to_pixel(v, max_len):
|
||||
if v is None:
|
||||
return None
|
||||
try:
|
||||
fv = float(v)
|
||||
except:
|
||||
return None
|
||||
if fv <= 1000.0:
|
||||
return int(fv * max_len / 1000.0)
|
||||
return int(fv)
|
||||
|
||||
for res in vlm_list:
|
||||
if not res:
|
||||
continue
|
||||
p = res.get("point")
|
||||
b = res.get("bbox")
|
||||
actual_p = None
|
||||
actual_bbox = None
|
||||
|
||||
if p and len(p) == 2:
|
||||
px = to_pixel(p[0], w)
|
||||
py = to_pixel(p[1], h)
|
||||
if px is not None and py is not None:
|
||||
actual_p = [px, py]
|
||||
|
||||
if b and len(b) == 4:
|
||||
x1 = to_pixel(b[0], w)
|
||||
y1 = to_pixel(b[1], h)
|
||||
x2 = to_pixel(b[2], w)
|
||||
y2 = to_pixel(b[3], h)
|
||||
if None not in (x1, y1, x2, y2):
|
||||
actual_bbox = [max(0, int(x1)), max(0, int(y1)), min(w, int(x2)), min(h, int(y2))]
|
||||
|
||||
if actual_p is None and actual_bbox:
|
||||
actual_p = [(actual_bbox[0] + actual_bbox[2]) // 2, (actual_bbox[1] + actual_bbox[3]) // 2]
|
||||
|
||||
if actual_p is None and actual_bbox is None:
|
||||
continue
|
||||
|
||||
if actual_p and (actual_p[1] < y_threshold or actual_p[1] > bottom_threshold):
|
||||
logger.warning(f"过滤掉可能的误点击点 (y={actual_p[1]}): {res.get('name')}")
|
||||
continue
|
||||
|
||||
if actual_bbox is None and actual_p:
|
||||
half_w = int(w * 0.4)
|
||||
half_h = max(MIN_CARD_HEIGHT // 2, 90)
|
||||
x1 = max(0, actual_p[0] - half_w)
|
||||
x2 = min(w, actual_p[0] + half_w)
|
||||
y1 = max(0, actual_p[1] - half_h)
|
||||
y2 = min(h, actual_p[1] + half_h)
|
||||
actual_bbox = [x1, y1, x2, y2]
|
||||
|
||||
if actual_bbox is not None:
|
||||
bx1, by1, bx2, by2 = actual_bbox
|
||||
current_h = by2 - by1
|
||||
if current_h < MIN_CARD_HEIGHT * 0.8 or by2 > bottom_threshold:
|
||||
continue
|
||||
p = res.get("point")
|
||||
actual_p = None
|
||||
if p and len(p) == 2:
|
||||
actual_p = p
|
||||
if p[0] <= 1000 and p[1] <= 1000:
|
||||
actual_p = [int(p[0] * w / 1000), int(p[1] * h / 1000)]
|
||||
# 顶部与底部安全过滤
|
||||
if actual_p and (actual_p[1] < y_threshold or actual_p[1] > bottom_threshold):
|
||||
logger.warning(f"过滤掉可能的误点击点 (y={actual_p[1]} < {y_threshold}): {res.get('name')}")
|
||||
continue
|
||||
matched_bbox = None
|
||||
if actual_p:
|
||||
for b in use_bboxes:
|
||||
if b[0] <= actual_p[0] <= b[2] and b[1] <= actual_p[1] <= b[3]:
|
||||
matched_bbox = b
|
||||
break
|
||||
# 如果没有匹配到 CV 框,则根据点击点生成一个虚拟框
|
||||
if matched_bbox is None and actual_p is not None:
|
||||
half_w = int(w * 0.4)
|
||||
half_h = 90
|
||||
x1 = max(0, actual_p[0] - half_w)
|
||||
x2 = min(w, actual_p[0] + half_w)
|
||||
y1 = max(0, actual_p[1] - half_h)
|
||||
y2 = min(h, actual_p[1] + half_h)
|
||||
matched_bbox = [x1, y1, x2, y2]
|
||||
# 如果既没有 point 也没有匹配框,跳过
|
||||
if actual_p is None and matched_bbox is None:
|
||||
continue
|
||||
if actual_p is None and matched_bbox:
|
||||
actual_p = [(matched_bbox[0] + matched_bbox[2]) // 2, (matched_bbox[1] + matched_bbox[3]) // 2]
|
||||
final_stations.append({
|
||||
"name": res.get("name"),
|
||||
"address": res.get("address"),
|
||||
"point": actual_p,
|
||||
"bbox": matched_bbox
|
||||
})
|
||||
elif not cv_bboxes:
|
||||
vlm_list = vlm_results if isinstance(vlm_results, list) else []
|
||||
w, h = 0, 0
|
||||
for res in vlm_list:
|
||||
p = res.get("point")
|
||||
if p and len(p) == 2:
|
||||
# 1. 坐标转换逻辑
|
||||
actual_p = p
|
||||
if p[0] <= 1000 and p[1] <= 1000:
|
||||
if w == 0:
|
||||
img = read_image(image_path)
|
||||
if img is not None:
|
||||
h, w = img.shape[:2]
|
||||
else:
|
||||
w, h = 1080, 2400 # 兜底
|
||||
actual_p = [int(p[0] * w / 1000), int(p[1] * h / 1000)]
|
||||
|
||||
# 2. 坐标安全过滤:忽略页面上半部分的误点击(通常是 Logo 或广告)
|
||||
# 即使 VLM 没听话,我们也在这里硬性过滤
|
||||
# 根据主屏幕截图,顶部广告区和功能图标区占用了约 55% 的空间
|
||||
y_threshold = h * 0.58 if h > 0 else 1400
|
||||
if actual_p[1] < y_threshold:
|
||||
logger.warning(f"过滤掉可能的误点击点 (y={actual_p[1]} < {y_threshold}): {res.get('name')}")
|
||||
continue
|
||||
|
||||
res["point"] = actual_p
|
||||
final_stations.append(res)
|
||||
|
||||
final_stations.append({
|
||||
"name": res.get("name"),
|
||||
"address": res.get("address"),
|
||||
"point": actual_p,
|
||||
"bbox": actual_bbox
|
||||
})
|
||||
|
||||
if final_stations:
|
||||
final_stations.sort(key=lambda s: (s.get("point")[1] if s.get("point") else (s.get("bbox")[1] if s.get("bbox") else 0)))
|
||||
|
||||
# 保存诊断图片 (_vl.jpg, _flag.jpg)
|
||||
if final_stations:
|
||||
|
||||
Reference in New Issue
Block a user