2025-05-27 15:46:31 +08:00

614 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/<path:filename>')
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/<task_id>", 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/<task_id>", 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/<report_id>", 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