from flask import Blueprint, jsonify, request, send_from_directory, render_template from flask_socketio import SocketIO from datetime import datetime import os import json from PIL import Image from io import BytesIO import threading import uuid from .thermal_process import ThermalProcessor from .thermal_vision import ThermalVision # 创建Blueprint thermal_bp = Blueprint('thermal', __name__) # 获取全局的socketio实例 socketio = None def init_thermal_socketio(socket_io_instance): global socketio socketio = socket_io_instance # 存储最后上传的文件信息 last_uploaded_files = { "csv_path": None, "image_path": None, "save_dir": None, "timestamp": None, "system_image_path": None } # 存储正在运行的分析任务 thermal_analysis_tasks = {} @thermal_bp.route("/upload_thermal_data", methods=["POST"]) def upload_thermal_data(): try: print("开始处理热成像数据上传") # 获取时间戳 timestamp = request.form.get("timestamp", datetime.now().strftime("%Y%m%d_%H%M%S")) # 创建保存数据的目录 save_dir = os.path.join("/home/jsfb/jsfb_ws/collected_data/thermal_data", timestamp) os.makedirs(save_dir, exist_ok=True) # 更新最后上传的文件信息 last_uploaded_files["save_dir"] = save_dir last_uploaded_files["timestamp"] = timestamp image_filename = None system_image_filename = None # 保存CSV文件 if 'csvFile' in request.files: csv_file = request.files['csvFile'] if csv_file.filename: csv_path = os.path.join(save_dir, csv_file.filename) csv_file.save(csv_path) last_uploaded_files["csv_path"] = csv_path print(f"保存CSV文件到: {csv_path}") # 保存图像文件 if 'imageFile' in request.files: image_file = request.files['imageFile'] if image_file.filename: try: # 读取图像并旋转180度 image_data = image_file.read() image = Image.open(BytesIO(image_data)) rotated_image = image.rotate(180) # 保存旋转后的图像 image_filename = image_file.filename image_path = os.path.join(save_dir, image_filename) rotated_image.save(image_path) last_uploaded_files["image_path"] = image_path print(f"保存旋转后的图像到: {image_path}") except Exception as img_error: print(f"处理图像时出错: {str(img_error)}") raise # 保存系统相机图片 if 'systemCameraImage' in request.files: system_image = request.files['systemCameraImage'] if system_image.filename: try: # 读取原始图片 system_img = Image.open(system_image) # 计算新的尺寸,保持宽高比 MAX_SIZE = (1280, 960) # 设置最大分辨率 system_img.thumbnail(MAX_SIZE, Image.Resampling.LANCZOS) # 如果是RGBA格式,转换为RGB if system_img.mode == 'RGBA': system_img = system_img.convert('RGB') # 保存处理后的系统相机图片,使用JPEG格式和适当的压缩质量 system_image_filename = f"system_camera_{timestamp}.jpg" system_image_path = os.path.join(save_dir, system_image_filename) system_img.save(system_image_path, 'JPEG', quality=85, optimize=True) last_uploaded_files["system_image_path"] = system_image_path print(f"保存压缩后的系统相机图片到: {system_image_path}") # 输出处理后的图片信息 final_size = os.path.getsize(system_image_path) / 1024 # 转换为KB print(f"处理后的图片大小: {final_size:.2f}KB, 分辨率: {system_img.size}") except Exception as sys_img_error: print(f"处理系统相机图片时出错: {str(sys_img_error)}") raise # 通过WebSocket发送上传完成事件 if image_filename: event_data = { 'timestamp': timestamp, 'image_filename': image_filename, 'system_image_filename': system_image_filename } print(f"准备发送WebSocket事件: thermal_upload_complete, 数据: {event_data}") socketio.emit('thermal_upload_complete', event_data) print(f"已发送WebSocket事件: thermal_upload_complete") else: print("没有图像文件,未发送WebSocket事件") return jsonify({ "status": "success", "message": "星耀慧眼数据上传成功", "timestamp": timestamp, "save_directory": save_dir, "image_filename": image_filename, "system_image_filename": system_image_filename }), 200 except Exception as e: print(f"上传热成像数据时出错: {str(e)}") return jsonify({"status": "error", "message": str(e)}), 500 @thermal_bp.route('/thermal_data/') def thermal_data(filename): print(f"请求访问文件: {filename}") parts = filename.split('/') if len(parts) != 2: print(f"无效的文件路径格式: {filename}") return "Invalid path", 400 timestamp_dir = parts[0] file_name = parts[1] base_dir = "/home/jsfb/jsfb_ws/collected_data/thermal_data" file_path = os.path.join(base_dir, timestamp_dir) full_path = os.path.join(file_path, file_name) print(f"完整文件路径: {full_path}") if not os.path.exists(file_path): print(f"目录不存在: {file_path}") return "Directory not found", 404 if not os.path.exists(full_path): print(f"文件不存在: {full_path}") return "File not found", 404 try: print(f"尝试发送文件: {file_name} 从目录: {file_path}") response = send_from_directory(file_path, file_name) print(f"文件发送成功: {file_name}") return response except Exception as e: print(f"提供文件失败: {str(e)}") print(f"错误类型: {type(e).__name__}") return f"Error serving file: {str(e)}", 500 @thermal_bp.route('/thermal') def thermal_page(): return render_template('thermal_analysis.html') def analyze_thermal_data(task_id): try: print("开始分析热成像数据") if not last_uploaded_files["csv_path"]: raise Exception("没有找到要分析的CSV文件") data_dir = last_uploaded_files["save_dir"] csv_path = last_uploaded_files["csv_path"] processor = ThermalProcessor() vision = ThermalVision() # 处理CSV文件 heatmap_0, metadata = processor.process_csv(csv_path) # 保存主热图 output_main = os.path.join(data_dir, "heatmap_0.png") processor.save_image(heatmap_0, output_main) # 发送主热图路径 print("发送基准热图WebSocket事件") socketio.emit('new_heatmap', { 'type': '0', 'path': f'/thermal_data/{os.path.basename(data_dir)}/heatmap_0.png' }) # 生成额外的热图用于比较 heatmap_1, _ = processor.process_csv( csv_path, min_temp=metadata["min_temp"]+1, max_temp=metadata["max_temp"]+1, focus_temp=metadata["max_temp"]-3 ) output_1 = os.path.join(data_dir, "heatmap_1.png") processor.save_image(heatmap_1, output_1) # 发送对比热图1路径 print("发送对比热图1 WebSocket事件") socketio.emit('new_heatmap', { 'type': '1', 'path': f'/thermal_data/{os.path.basename(data_dir)}/heatmap_1.png' }) heatmap_2, _ = processor.process_csv( csv_path, min_temp=metadata["min_temp"]+2, max_temp=metadata["max_temp"]+2, focus_temp=metadata["max_temp"]-2 ) output_2 = os.path.join(data_dir, "heatmap_2.png") processor.save_image(heatmap_2, output_2) # 发送对比热图2路径 print("发送对比热图2 WebSocket事件") socketio.emit('new_heatmap', { 'type': '2', 'path': f'/thermal_data/{os.path.basename(data_dir)}/heatmap_2.png' }) # 通知前端开始VLM分析 print("发送VLM分析状态WebSocket事件") socketio.emit('analysis_status', { 'status': 'vlm_analyzing', 'message': '正在进行图像分析...' }) # 读取VLM提示 vlm_prompt_path = os.path.join(os.path.dirname(__file__), "vlm_prompt.txt") with open(vlm_prompt_path, 'r', encoding='utf-8') as f: vlm_prompt = f.read() # 进行VLM分析 print("开始VLM分析") vlm_result = "" # 检查是否存在系统相机图片 system_image = None if last_uploaded_files["system_image_path"] and os.path.exists(last_uploaded_files["system_image_path"]): try: system_image = Image.open(last_uploaded_files["system_image_path"]) print("成功读取系统相机图片") except Exception as e: print(f"读取系统相机图片失败: {str(e)}") system_image = None # 根据系统相机图片是否存在来决定分析图片的顺序 if system_image: print("使用系统相机图片作为主要分析图片") vlm_stream = vision.analyze( image=system_image, extra_images=[heatmap_0, heatmap_1, heatmap_2], prompt="请你对这张图进行分析描述,其中第一张图片为正常相机图片(非热成像热图),你可以根据此图片分析出拍摄的部位后再对应到热图进行分析,如果正常图像中可以看出的明显病症也请加入描述中", system_prompt=vlm_prompt, stream=True ) else: print("使用热成像图片作为主要分析图片") vlm_stream = vision.analyze( image=heatmap_0, extra_images=[heatmap_1, heatmap_2], prompt="请你对这张图进行分析描述", system_prompt=vlm_prompt, stream=True ) # 计算预期的总字符数(估算值) expected_chars = 800 # 预计VLM会输出800个字符 current_chars = 0 for chunk in vlm_stream: if task_id not in thermal_analysis_tasks: raise Exception("分析任务被取消") chunk_message = chunk.choices[0].delta.content if chunk_message: vlm_result += chunk_message current_chars += len(chunk_message) # 计算进度(最多到98%) progress = min(98, int((current_chars / expected_chars) * 100)) # 发送进度更新 socketio.emit('analysis_status', { 'status': 'vlm_analyzing', 'message': '正在分析图像...', 'progress': progress }) # VLM分析完成,发送100%进度 socketio.emit('analysis_status', { 'status': 'vlm_analyzing', 'message': '图像分析完成', 'progress': 100 }) # 通知前端VLM分析完成,开始生成总结 print("发送生成总结状态WebSocket事件") socketio.emit('analysis_status', { 'status': 'generating_summary', 'message': '正在生成分析报告...' }) # 读取总结提示 summarize_prompt_path = os.path.join(os.path.dirname(__file__), "summarize_prompt.txt") with open(summarize_prompt_path, 'r', encoding='utf-8') as f: summarize_prompt = f.read() # 生成总结(流式输出) summary_result = "" summary_stream = vision.summarize( prompt=f"VLM结果如下:{vlm_result},请你进行分析并且按照格式输出", system_prompt=summarize_prompt, stream=True ) print("开始流式发送总结文本块") for chunk in summary_stream: if task_id not in thermal_analysis_tasks: raise Exception("分析任务被取消") chunk_message = chunk.choices[0].delta.content if chunk_message: summary_result += chunk_message # 实时发送每个文本块 socketio.emit('summary_chunk', { 'text': chunk_message }) # 保存分析结果 results = { "metadata": metadata, "vlm_analysis": vlm_result, "summary": summary_result, "status": "completed", "analyzed_files": { "csv": os.path.basename(csv_path), "heatmaps": { "main": os.path.basename(output_main), "compare_1": os.path.basename(output_1), "compare_2": os.path.basename(output_2) } } } with open(os.path.join(data_dir, "analysis_results.json"), 'w', encoding='utf-8') as f: json.dump(results, f, ensure_ascii=False, indent=2) # 发送完成信号 print("发送分析完成状态WebSocket事件") socketio.emit('analysis_status', { 'status': 'completed', 'message': '分析完成' }) # 获取最新的报告列表并发送给前端 try: print("获取最新报告列表") # 使用现有的get_available_reports函数获取第一页报告 from flask import current_app with current_app.test_request_context(): reports_response = get_available_reports() reports_data = json.loads(reports_response.get_data(as_text=True)) if reports_data["status"] == "success": print("发送最新报告列表WebSocket事件") socketio.emit('reports_update', { 'reports': reports_data["reports"], 'pagination': reports_data["pagination"] }) else: print(f"获取报告列表失败: {reports_data.get('message', '未知错误')}") except Exception as e: print(f"更新报告列表时出错: {str(e)}") except Exception as e: error_message = str(e) print(f"热成像分析出错: {error_message}") socketio.emit('analysis_error', {'error': error_message}) @thermal_bp.route("/analyze_thermal_data", methods=["POST"]) def start_thermal_analysis(): try: if not last_uploaded_files["csv_path"]: return jsonify({ "status": "error", "message": "没有找到可分析的文件,请先上传热成像数据" }), 400 # 生成唯一的任务ID task_id = str(uuid.uuid4()) # 创建新的分析任务 thermal_analysis_tasks[task_id] = { "csv_path": last_uploaded_files["csv_path"], "save_dir": last_uploaded_files["save_dir"], "status": "running", "started_at": datetime.now().isoformat(), "finished": False } # 启动后台线程进行分析 thread = threading.Thread( target=analyze_thermal_data, args=(task_id,) ) thread.daemon = True thread.start() return jsonify({ "status": "success", "message": "热成像分析任务已启动", "task_id": task_id, "file_info": { "csv": os.path.basename(last_uploaded_files["csv_path"]), "directory": last_uploaded_files["save_dir"] } }) except Exception as e: return jsonify({"status": "error", "message": str(e)}), 500 @thermal_bp.route("/get_analysis_status/", methods=["GET"]) def get_analysis_status(task_id): if task_id not in thermal_analysis_tasks: return jsonify({"status": "error", "message": "任务不存在"}), 404 task = thermal_analysis_tasks[task_id] response = { "status": task["status"], "started_at": task["started_at"], "finished": task["finished"] } if task["status"] == "completed" and "results" in task: response["results"] = task["results"] elif task["status"] == "error" and "error" in task: response["error"] = task["error"] return jsonify(response) @thermal_bp.route("/cancel_analysis/", methods=["POST"]) def cancel_analysis(task_id): if task_id not in thermal_analysis_tasks: return jsonify({"status": "error", "message": "任务不存在"}), 404 if thermal_analysis_tasks[task_id]["finished"]: return jsonify({"status": "error", "message": "任务已完成,无法取消"}), 400 # 移除任务将导致分析线程退出 del thermal_analysis_tasks[task_id] return jsonify({ "status": "success", "message": "分析任务已取消" }) @thermal_bp.route("/get_available_reports", methods=["GET"]) def get_available_reports(): try: # 获取分页参数 page = int(request.args.get('page', 1)) per_page = int(request.args.get('per_page', 10)) base_dir = "/home/jsfb/jsfb_ws/collected_data/thermal_data" # 检查基础目录是否存在 if not os.path.exists(base_dir): return jsonify({ "status": "success", "reports": [], "pagination": { "total": 0, "page": page, "per_page": per_page, "total_pages": 0 } }) all_reports = [] # 获取所有有效的报告目录 for timestamp_dir in os.listdir(base_dir): dir_path = os.path.join(base_dir, timestamp_dir) if not os.path.isdir(dir_path): continue # 检查必需文件是否都存在 required_files = [ f"thermal_image_{timestamp_dir}.jpg", "analysis_results.json", "heatmap_0.png", "heatmap_1.png", "heatmap_2.png", f"thermal_data_{timestamp_dir}.csv" ] has_all_files = all(os.path.exists(os.path.join(dir_path, f)) for f in required_files) if has_all_files: # 获取报告创建时间 try: timestamp = datetime.strptime(timestamp_dir, "%Y%m%d_%H%M%S") formatted_time = timestamp.strftime("%Y年%m月%d日 %H:%M:%S") except ValueError: formatted_time = timestamp_dir # 读取分析结果作为摘要 summary = "" try: with open(os.path.join(dir_path, "analysis_results.json"), 'r', encoding='utf-8') as f: analysis_data = json.load(f) # 这里可以根据JSON的结构提取适当的摘要信息 summary = "热成像分析报告" # 临时使用固定文本 except: summary = "无法读取分析摘要" all_reports.append({ "id": timestamp_dir, "timestamp": formatted_time, "summary": summary, "thumbnail": f"/thermal_data/{timestamp_dir}/thermal_image_{timestamp_dir}.jpg" }) # 按时间戳倒序排序 all_reports.sort(key=lambda x: x["id"], reverse=True) # 计算分页 total = len(all_reports) start_idx = (page - 1) * per_page end_idx = start_idx + per_page # 获取当前页的报告 current_page_reports = all_reports[start_idx:end_idx] return jsonify({ "status": "success", "reports": current_page_reports, "pagination": { "total": total, "page": page, "per_page": per_page, "total_pages": (total + per_page - 1) // per_page } }) except Exception as e: print(f"获取报告列表出错: {str(e)}") return jsonify({ "status": "error", "message": f"获取报告列表失败: {str(e)}" }), 500 @thermal_bp.route("/load_report/", methods=["GET"]) def load_report(report_id): base_dir = os.path.join("/home/jsfb/jsfb_ws/collected_data/thermal_data", report_id) try: # 检查目录是否存在 if not os.path.exists(base_dir): return jsonify({ "status": "error", "message": "报告不存在" }), 404 # 读取分析结果 analysis_file = os.path.join(base_dir, "analysis_results.json") if not os.path.exists(analysis_file): return jsonify({ "status": "error", "message": "分析结果文件不存在" }), 404 with open(analysis_file, 'r', encoding='utf-8') as f: analysis_data = json.load(f) # 构建响应数据 response_data = { "status": "success", "data": { "timestamp": report_id, "images": { "original": f"/thermal_data/{report_id}/thermal_image_{report_id}.jpg", "heatmap_0": f"/thermal_data/{report_id}/heatmap_0.png", "heatmap_1": f"/thermal_data/{report_id}/heatmap_1.png", "heatmap_2": f"/thermal_data/{report_id}/heatmap_2.png" }, "analysis_results": analysis_data.get("summary", "无分析结果") # 从summary字段获取分析文本 } } return jsonify(response_data) except Exception as e: print(f"加载报告出错: {str(e)}") return jsonify({ "status": "error", "message": f"加载报告失败: {str(e)}" }), 500