diff --git a/Ai_tottle/ai_tottle_api.py b/Ai_tottle/ai_tottle_api.py index 1db7a69..ed733fb 100644 --- a/Ai_tottle/ai_tottle_api.py +++ b/Ai_tottle/ai_tottle_api.py @@ -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) diff --git a/Ai_tottle/tiles.py b/Ai_tottle/tiles.py new file mode 100644 index 0000000..1584373 --- /dev/null +++ b/Ai_tottle/tiles.py @@ -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() \ No newline at end of file