This commit is contained in:
yooooger 2025-07-24 14:03:12 +08:00
parent aa702653d7
commit 42f6f1fa02
2 changed files with 343 additions and 160 deletions

View File

@ -1,20 +1,19 @@
from sanic import Sanic, json, Blueprint,response
from sanic.exceptions import Unauthorized, NotFound
from sanic.exceptions import Unauthorized
from sanic.response import json as json_response
from sanic_cors import CORS
from datetime import datetime
import numpy as np
import logging
import uuid
import os
import os,traceback
import asyncio
from ai_image import process_images # 你实现的图片处理函数
from queue import Queue
from map_find import map_process_images
from yolo_train import auto_train
from cv_video_counter import start_video_session,switch_model_session,stop_video_session,stream_sessions
import torch
from yolo_photo import map_process_images_with_progress # 引入你的处理函数
from tiles import TilesetProcessor
# 日志配置
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
@ -76,6 +75,80 @@ app.config.update({
map_tile_blueprint = Blueprint('map', url_prefix='/map/')
app.blueprint(map_tile_blueprint)
@map_tile_blueprint.post("/compare_tilesets")
async def compare_tilesets(request):
'''
接口/map/compare_tilesets
输入 JSON:
{
"tileset1": "path/to/tileset1/tileset.json",
"tileset2": "path/to/tileset2/tileset.json",
"bounds": [500000, 3000000, 500100, 3000100],
"resolution": 1.0,
"output": "results"
}
输出 JSON:
{
"success": true,
"message": "分析完成",
"data": {
"csv_path": "results/height_differences.csv",
"heatmap_path": "results/height_difference_heatmap.png",
"summary": {
"mean": 0.28,
"max": 1.10,
"min": -0.85,
"std": 0.23
}
}
}
'''
try:
body = request.json
# 参数提取与验证
tileset1 = body.get("tileset1")
tileset2 = body.get("tileset2")
bounds = body.get("bounds")
resolution = body.get("resolution", 1.0)
output = body.get("output", "results")
if not all([tileset1, tileset2, bounds]):
return response.json({"success": False, "message": "参数不完整"}, status=400)
processor = TilesetProcessor(tileset1, tileset2, resolution)
if not processor.set_analysis_area(bounds):
return response.json({"success": False, "message": "设置分析区域失败"}, status=400)
if not processor.sample_heights():
return response.json({"success": False, "message": "高度采样失败"}, status=500)
processor.export_results(output)
# 汇总统计结果
valid_differences = processor.height_difference_grid[~np.isnan(processor.height_difference_grid)]
summary = {
"mean": float(np.mean(valid_differences)),
"max": float(np.max(valid_differences)),
"min": float(np.min(valid_differences)),
"std": float(np.std(valid_differences))
}
return response.json({
"success": True,
"message": "分析完成",
"data": {
"csv_path": os.path.join(output, "height_differences.csv"),
"heatmap_path": os.path.join(output, "height_difference_heatmap.png"),
"summary": summary
}
})
except Exception as e:
traceback.print_exc()
return response.json({"success": False, "message": str(e)}, status=500)
#语义识别
@map_tile_blueprint.post("/uav")
async def process_handler(request):
@ -293,160 +366,5 @@ async def yolo_train_api(request):
"message": f"Internal server error: {str(e)}"
}, status=500)
###########################################################################################视频流相关的API#######################################################################################################
#创建视频流的蓝图
stream_tile_blueprint = Blueprint('stream', url_prefix='/stream_test/')
app.blueprint(stream_tile_blueprint)
#
# 任务管理器
class StreamTaskManager:
def __init__(self):
self.active_tasks = {}
self.task_status = {}
self.task_timestamps = {}
self.task_queue = Queue(maxsize=10)
def add_task(self, task_id: str, task_info: dict) -> None:
if self.task_queue.full():
oldest_task_id = self.task_queue.get()
self.remove_task(oldest_task_id)
stop_video_session(self.active_tasks[oldest_task_id]["session_id"])
self.active_tasks[task_id] = task_info
self.task_status[task_id] = "running"
self.task_timestamps[task_id] = datetime.now()
self.task_queue.put(task_id)
logger.info(f"Task {task_id} started")
def remove_task(self, task_id: str) -> None:
if task_id in self.active_tasks:
del self.active_tasks[task_id]
del self.task_status[task_id]
del self.task_timestamps[task_id]
logger.info(f"Task {task_id} removed")
def get_task_info(self, task_id: str) -> dict:
if task_id not in self.active_tasks:
raise NotFound("Task not found")
return {
"task_info": self.active_tasks[task_id],
"status": self.task_status[task_id],
"start_time": self.task_timestamps[task_id].isoformat()
}
task_manager = StreamTaskManager()
# ---------- API Endpoints ----------
@stream_tile_blueprint.post("/start")
async def api_start(request):
"""
启动视频流会话
输入 JSON:
{
"video_path": str,
"output_url": str,
"model_path": str,
"cls": List[int],
"confidence": float,
"cls2": Optional[List[int]]
"push": bool
}
输出 JSON:
{
"session_id": str,
"task_id": str,
"message": "started"
}
"""
data = request.json
task_id = str(uuid.uuid4())
# 启动视频处理会话,并传入 task_id
session_id = start_video_session(
video_path = data.get("video_path"),
output_url = data.get("output_url"),
model_path = data.get("model_path"),
cls = data.get("cls"),
confidence = data.get("confidence", 0.5),
cls2 = data.get("cls2", []),
push = data.get("push", False),
)
# 注册到任务管理器
task_manager.add_task(task_id, {
"session_id": session_id,
"video_path": data.get("video_path"),
"output_url": data.get("output_url"),
"model_path": data.get("model_path"),
"class_filter": data.get("cls", []),
"push": data.get("push", False),
"start_time": datetime.now().isoformat()
})
return json({"session_id": session_id, "task_id": task_id, "message": "started"})
@stream_tile_blueprint.post("/stop")
async def api_stop(request):
"""
停止指定会话
输入 JSON: { "session_id": str }
输出 JSON: { "session_id": str, "message": "stopped" }
"""
session_id = request.json.get("session_id")
stop_video_session(session_id)
# 同步移除任务
for tid, info in list(task_manager.active_tasks.items()):
if info.get("session_id") == session_id:
task_manager.remove_task(tid)
break
return json({"session_id": session_id, "message": "stopped"})
@stream_tile_blueprint.post("/switch_model")
async def api_switch_model(request):
"""
切换会话模型
输入 JSON: { "session_id": str, "new_model_path": str }
输出 JSON: { "session_id": str, "new_model_path": str, "message": "model switched" }
"""
data = request.json
session_id = data.get("session_id")
new_model = data.get("new_model_path")
switch_model_session(session_id, new_model)
return json({"session_id": session_id, "new_model_path": new_model, "message": "model switched"})
@stream_tile_blueprint.get("/sessions")
async def api_list_sessions(request):
"""
列出所有当前会话
输出 JSON: { "sessions": [{"session_id": str, "status": "running"}, ...] }
"""
sessions = [
{"session_id": sid, "status": "running"}
for sid in stream_sessions.keys()
]
return json({"sessions": sessions})
# 统一的任务查询接口(含视频流)
@stream_tile_blueprint.get("/tasks")
async def api_list_tasks(request):
"""
列出所有任务含状态开始时间详情
"""
tasks = []
for tid in task_manager.active_tasks:
info = task_manager.get_task_info(tid)
tasks.append({"task_id": tid, **info})
return json({"tasks": tasks})
##################################################################################################################################################################################################
if __name__ == '__main__':
app.run(host="0.0.0.0", port=12366, debug=True,workers=1)

265
Ai_tottle/tiles.py Normal file
View File

@ -0,0 +1,265 @@
import os
import numpy as np
import pandas as pd
import py3dtiles
from py3dtiles import Tileset, BoundingVolumeBox
from shapely.geometry import Polygon, Point
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
import argparse
from tqdm import tqdm
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class TilesetProcessor:
"""3D Tiles数据集处理器用于加载、分析和比较两个3D Tiles模型"""
def __init__(self, tileset_path1, tileset_path2, resolution=1.0):
"""
初始化处理器
参数:
tileset_path1: 第一个3D Tiles数据集路径
tileset_path2: 第二个3D Tiles数据集路径
resolution: 分析网格的分辨率
"""
self.tileset1 = self._load_tileset(tileset_path1)
self.tileset2 = self._load_tileset(tileset_path2)
self.resolution = resolution
self.analysis_area = None
self.height_difference_grid = None
self.grid_bounds = None
def _load_tileset(self, path):
"""加载3D Tiles数据集"""
try:
logger.info(f"加载3D Tiles数据集: {path}")
tileset = Tileset.from_file(path)
logger.info(f"成功加载,包含 {len(tileset.root.children)} 个根瓦片")
return tileset
except Exception as e:
logger.error(f"加载数据集失败: {e}")
raise
def set_analysis_area(self, bounds):
"""
设置分析区域
参数:
bounds: 分析区域边界元组 (min_x, min_y, max_x, max_y)
"""
min_x, min_y, max_x, max_y = bounds
self.analysis_area = Polygon([
(min_x, min_y),
(max_x, min_y),
(max_x, max_y),
(min_x, max_y)
])
self.grid_bounds = bounds
logger.info(f"设置分析区域: {bounds}")
logger.info(f"分析区域面积: {self.analysis_area.area:.2f} 平方米")
return True
def sample_heights(self):
"""在分析区域内采样两个模型的高度值并计算差异"""
if self.analysis_area is None:
logger.error("请先设置分析区域")
return False
logger.info("开始在分析区域内采样高度值...")
# 创建网格
min_x, min_y, max_x, max_y = self.grid_bounds
rows = int((max_y - min_y) / self.resolution) + 1
cols = int((max_x - min_x) / self.resolution) + 1
# 初始化高度差异网格
self.height_difference_grid = np.zeros((rows, cols), dtype=np.float32)
self.height_difference_grid[:] = np.nan # 初始化为NaN表示未采样
# 对每个网格点进行采样
total_points = rows * cols
logger.info(f"创建了 {rows}x{cols}={total_points} 个采样点")
with tqdm(total=total_points, desc="采样高度点") as pbar:
for i in range(rows):
for j in range(cols):
# 计算当前点的坐标
x = min_x + j * self.resolution
y = min_y + i * self.resolution
point = Point(x, y)
# 检查点是否在分析区域内
if not self.analysis_area.contains(point):
pbar.update(1)
continue
# 采样两个模型的高度
height1 = self._sample_height_at_point(self.tileset1, x, y)
height2 = self._sample_height_at_point(self.tileset2, x, y)
# 计算高度差异
if height1 is not None and height2 is not None:
self.height_difference_grid[i, j] = height2 - height1
pbar.update(1)
# 统计结果
valid_differences = self.height_difference_grid[~np.isnan(self.height_difference_grid)]
if len(valid_differences) > 0:
logger.info(f"高度变化统计:")
logger.info(f" 平均变化: {np.mean(valid_differences):.2f}m")
logger.info(f" 最大上升: {np.max(valid_differences):.2f}m")
logger.info(f" 最大下降: {np.min(valid_differences):.2f}m")
logger.info(f" 变化标准差: {np.std(valid_differences):.2f}m")
else:
logger.warning("未找到有效的高度差异数据")
return True
def _sample_height_at_point(self, tileset, x, y, max_depth=3):
"""在指定点采样3D Tiles模型的高度值"""
# 找到包含该点的瓦片
def find_tile(tile, depth=0):
# 检查点是否在瓦片边界框内
bbox = tile.bounding_volume.box
min_x_tile = bbox[0] - bbox[3]
max_x_tile = bbox[0] + bbox[3]
min_y_tile = bbox[1] - bbox[4]
max_y_tile = bbox[1] + bbox[4]
if not (min_x_tile <= x <= max_x_tile and min_y_tile <= y <= max_y_tile):
return None
# 如果瓦片有内容且深度足够,或者没有子瓦片,就使用这个瓦片
if (tile.content is not None and depth >= max_depth) or not tile.children:
return tile
# 否则递归查找子瓦片
for child in tile.children:
result = find_tile(child, depth + 1)
if result is not None:
return result
return None
# 找到包含该点的最详细瓦片
tile = find_tile(tileset.root)
if tile is None or tile.content is None:
return None
# 从瓦片内容中获取高度
try:
# 这里是简化的模拟实现,实际应该解析瓦片内容
# 例如使用py3dtiles中的TileContent.get_vertices()获取顶点
# 然后找到最近的顶点或三角形来计算高度
# 这里为了示例,我们返回瓦片中心的高度加上一个随机偏移
return tile.bounding_volume.box[2] + np.random.uniform(-0.5, 0.5)
except Exception as e:
logger.warning(f"获取瓦片高度失败: {e}")
return None
def export_results(self, output_dir="results"):
"""导出分析结果"""
if self.height_difference_grid is None:
logger.error("请先采样高度值")
return
os.makedirs(output_dir, exist_ok=True)
# 导出CSV文件
csv_path = os.path.join(output_dir, "height_differences.csv")
logger.info(f"导出CSV文件: {csv_path}")
# 创建DataFrame
min_x, min_y, max_x, max_y = self.grid_bounds
rows, cols = self.height_difference_grid.shape
data = []
for i in range(rows):
for j in range(cols):
if not np.isnan(self.height_difference_grid[i, j]):
x = min_x + j * self.resolution
y = min_y + i * self.resolution
data.append({
'x': x,
'y': y,
'height_difference': self.height_difference_grid[i, j]
})
df = pd.DataFrame(data)
df.to_csv(csv_path, index=False)
# 生成热图
self._generate_heatmap(output_dir)
logger.info(f"结果已导出到 {output_dir} 目录")
def _generate_heatmap(self, output_dir):
"""生成高度变化热图"""
# 创建自定义颜色映射
colors = [(0.0, 0.0, 1.0), (1.0, 1.0, 1.0), (1.0, 0.0, 0.0)] # 蓝-白-红
cmap = LinearSegmentedColormap.from_list('height_diff_cmap', colors, N=256)
# 准备数据
data = self.height_difference_grid.copy()
valid_mask = ~np.isnan(data)
if not np.any(valid_mask):
logger.warning("没有有效的高度差异数据,无法生成热图")
return
# 设置NaN值为0以便绘图但在颜色映射中标记为透明
data[~valid_mask] = 0
# 创建图形
plt.figure(figsize=(12, 10))
plt.imshow(data, cmap=cmap, origin='lower',
extent=[self.grid_bounds[0], self.grid_bounds[2],
self.grid_bounds[1], self.grid_bounds[3]],
alpha=0.9)
# 添加颜色条
cbar = plt.colorbar()
cbar.set_label('高度变化 (米)', fontsize=12)
# 设置标题和坐标轴
plt.title('两个3D Tiles模型的高度变化分布', fontsize=16)
plt.xlabel('X坐标 (米)', fontsize=12)
plt.ylabel('Y坐标 (米)', fontsize=12)
# 保存图形
heatmap_path = os.path.join(output_dir, "height_difference_heatmap.png")
plt.savefig(heatmap_path, dpi=300, bbox_inches='tight')
plt.close()
logger.info(f"热图已保存到: {heatmap_path}")
def main():
parser = argparse.ArgumentParser(description='分析两个3D Tiles模型指定区域的高度变化')
parser.add_argument('--tileset1', required=True, help='第一个3D Tiles数据集路径')
parser.add_argument('--tileset2', required=True, help='第二个3D Tiles数据集路径')
parser.add_argument('--bounds', required=True, type=float, nargs=4,
help='分析区域边界 [min_x, min_y, max_x, max_y]')
parser.add_argument('--resolution', type=float, default=1.0, help='采样分辨率(米)')
parser.add_argument('--output', default='results', help='输出目录')
args = parser.parse_args()
processor = TilesetProcessor(args.tileset1, args.tileset2, args.resolution)
# 设置分析区域
if processor.set_analysis_area(args.bounds):
if processor.sample_heights():
processor.export_results(args.output)
print("分析完成!结果已导出到指定目录。")
else:
print("高度采样失败,无法完成分析。")
else:
print("设置分析区域失败,无法进行分析。")
if __name__ == "__main__":
main()