614 lines
23 KiB
Python
614 lines
23 KiB
Python
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 |