import cv2 import numpy as np import matplotlib matplotlib.use('Agg') from matplotlib import pyplot as plt from matplotlib import rcParams import seaborn as sns import threading def set_chinese_font(): rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei', 'Microsoft YaHei', 'SimSun'] rcParams['axes.unicode_minus'] = False def region_ndti(img, mask, is_night): """对于可见光图像计算ndti,对于红外图像直接取值""" if len(img.shape) != 3: raise RuntimeError('请输入三通道彩色图像') if len(mask.shape) != 2: raise RuntimeError('请输入单通道掩膜图像') img = img.astype(np.float32) mask = mask.astype(np.float32) # 取通道 g = img[:,:,1] r = img[:,:,2] # 判断是否为红外 if is_night: # 红外直接取值 ndti = r else: # 可见光计算ndti ndti = (r - g) / (r + g + 1e-6) # 掩膜归一化 if np.max(mask) > 2: mask /= 255. # 仅保留掩膜区域的ndti ndti *= mask return ndti def plot_histogram_opencv(matrix, start, end, step, title): """使用OpenCV绘制直方图,避免GUI冲突""" # 计算直方图 num_bins = int((end - start) / step) hist, bins = np.histogram(matrix.ravel(), bins=num_bins, range=(start, end)) # 创建直方图图像(增加高度以便显示标签) hist_img = np.zeros((450, 600, 3), dtype=np.uint8) hist_img.fill(255) # 白色背景 # 归一化直方图:确保高度在合理范围内 if len(hist) > 0 and np.max(hist) > 0: # 保留10像素的边距 hist_normalized = cv2.normalize(hist, None, 0, 350, cv2.NORM_MINMAX) else: hist_normalized = np.zeros_like(hist) # 计算合理的矩形宽度(确保至少1像素宽) if len(hist) > 0: bin_width = max(1, 600 // len(hist)) # 确保最小宽度为1 else: bin_width = 1 # 绘制直方图矩形 for i in range(len(hist_normalized)): if i >= 600: # 防止索引越界 break x1 = i * bin_width x2 = min(x1 + bin_width, 599) # 确保不超出图像边界 y1 = 400 - int(hist_normalized[i]) # 从底部开始计算高度 y2 = 400 # 只绘制有高度的矩形 if y1 < y2: cv2.rectangle(hist_img, (x1, y1), (x2, y2), (0, 0, 255), -1) # 添加坐标轴 cv2.line(hist_img, (50, 400), (550, 400), (0, 0, 0), 2) # x轴 cv2.line(hist_img, (50, 400), (50, 50), (0, 0, 0), 2) # y轴 # 添加标题和标签(调整位置避免被裁剪) cv2.putText(hist_img, title, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2) cv2.putText(hist_img, 'gray', (280, 430), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) cv2.putText(hist_img, 'fre', (5, 200), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1) return hist_img def plot_histogram_seaborn_simple(matrix, start, end, step, title): """ 使用Seaborn的histplot函数直接绘制(最简单的方法) """ # 解决中文显示问题 plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False plt.figure(figsize=(12, 6)) # 直接使用Seaborn的histplot,设置bins参数[3](@ref) num_bins = int((end - start) / step) sns.histplot(matrix.ravel(), bins=num_bins, kde=True, color='skyblue', alpha=0.7, edgecolor='white', linewidth=0.5) plt.title(title+ '_histplot') plt.xlabel('灰度值') plt.ylabel('像素频率') plt.tight_layout() # plt.show() plt.savefig('temp_' + title +'.png') # 保存到文件而不是显示 plt.close() # 关闭图形,释放资源 # 如果需要显示,可以用cv2读取并显示 hist_img = cv2.imread('temp_' + title +'.png') cv2.imshow('Histogram '+title, hist_img) def plot_histogram_seaborn(matrix, hist, bin_edges, start, end, step): """ 使用Seaborn绘制更美观的直方图 """ # 设置Seaborn样式 sns.set_style("whitegrid") plt.figure(figsize=(12, 6)) # 将数据转换为适合Seaborn的格式 flattened_data = matrix.ravel() # 使用Seaborn的histplot(会自动计算直方图,但我们用自定义的) # 这里我们手动绘制以确保使用我们的自定义bins bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2 # 创建条形图 bars = plt.bar(bin_centers, hist, width=step * 0.8, alpha=0.7, color=sns.color_palette("viridis", 1)[0], edgecolor='white', linewidth=0.5) # 添加KDE曲线(核密度估计) sns.kdeplot(flattened_data, color='red', linewidth=2, label='密度曲线') plt.title(f'灰度分布直方图', fontsize=16) plt.xlabel('灰度值', fontsize=12) plt.ylabel('像素频率', fontsize=12) plt.legend() plt.tight_layout() plt.show() def colorful_ndti_matrix(ndti_matrix): """给NDTI矩阵着色""" # 存放着色后的NDTI矩阵 color_ndti_matrix = np.zeros((ndti_matrix.shape[0], ndti_matrix.shape[1], 3), dtype=np.uint8) negative_mask = ndti_matrix < 0 positive_mask = ndti_matrix > 0 # 处理负值 if np.any(negative_mask): blue_intensity = ndti_matrix.copy() blue_intensity[positive_mask] = 0 blue_intensity[negative_mask] *= -255. blue_intensity = np.clip(blue_intensity, 0, 255).astype(np.uint8) color_ndti_matrix[:, :,0] = blue_intensity # 处理正值区域(红色渐变) if np.any(positive_mask): # 将0到+1映射到0-255的红色强度 red_intensity = ndti_matrix.copy() red_intensity[negative_mask] = 0 red_intensity[positive_mask] *= 255 red_intensity = np.clip(red_intensity, 0, 255).astype(np.uint8) color_ndti_matrix[:, :, 2] = red_intensity # 返回着色后的ndti矩阵 return color_ndti_matrix def img_add(img1, img2, img1_w=1, img2_w=0, gamma=0): if len(img1.shape) != len(img2.shape): raise ValueError('img1 and img2 must have the same shape') # 设置权重参数(透明度) alpha = img1_w # 第一张图像的权重 beta = img2_w # 第二张图像的权重 gamma =gamma # 亮度调节参数 # 执行加权叠加 result = cv2.addWeighted(img1, alpha, img2, beta, gamma) return result def callback(event, x, y, flags, param): if event == cv2.EVENT_LBUTTONDOWN: is_night_ = param['is_night'] frame = param['Image'] w_name = param['window_name'] b = frame[:,:,0].astype(np.float32) g = frame[:,:,1].astype(np.float32) r = frame[:,:,2].astype(np.float32) if is_night_: ndti_value = r[y, x] else: ndti_value = (r[y,x] - g[y,x]) / (r[y,x] + g[y,x]) cv2.putText(frame, f'{ndti_value:.2f}', (x, y), cv2.FONT_HERSHEY_DUPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA) cv2.putText(frame, f'paused', (10, 45), cv2.FONT_HERSHEY_DUPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA) cv2.imshow(w_name, frame) def single_img(img_path,mask_path, id=0): frame = cv2.imread(img_path) if frame is None: raise RuntimeError('img open failed') mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if mask is None: raise RuntimeError('mask open failed') scale = 4 if id != 3 else 2 mask = cv2.resize(mask, (mask.shape[1] // scale, mask.shape[0] // scale)) frame = cv2.resize(frame, (frame.shape[1] // scale, frame.shape[0] // scale)) roi_ndti = region_ndti(frame, mask) # 给ndti着色 color_ndti = colorful_ndti_matrix(roi_ndti) # 绘制直方图 roi_index = mask > 0 roi_ndti_fla = roi_ndti[roi_index] # 仅保留感兴趣区的计算结果 hist_img = plot_histogram_opencv(roi_ndti[roi_index], -1, 1, 0.01, f'NDTI Histogram {id}') # 打印统计信息 # 3σ原则 mean_value = np.mean(roi_ndti_fla) std_value = np.std(roi_ndti_fla) min_value = np.min(roi_ndti_fla) max_value = np.max(roi_ndti_fla) up_limit = mean_value + 3 * std_value down_limit = mean_value - 3 * std_value # 调整 roi_ndti_fla = roi_ndti_fla[(up_limit >= roi_ndti_fla) & (roi_ndti_fla >= down_limit)] text = f'pixs:{int(np.sum(roi_ndti_fla))} adj_mean:{roi_ndti_fla.mean():.3f} adj_std:{roi_ndti_fla.std():.3f}' cv2.putText(frame, text, (10, 25), cv2.FONT_HERSHEY_DUPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA) print(f"""统计信息: 总像素数: {np.sum(roi_ndti_fla):,} 灰度范围: {min_value:.1f} - {max_value:.1f} 平均值: {mean_value:.2f} 标准差: {std_value:.2f} 调整平均值:{roi_ndti_fla.mean():.2f} 调整标准差:{roi_ndti_fla.std():.2f} """ ) # 显示当前帧处理结果 cv2.imshow('original', frame) # 原图 cv2.imshow('mask'+ str(id), mask) # 掩膜 roi_ndti = np.abs(roi_ndti*255.).astype(np.uint8) cv2.imshow('ndti'+ str(id), roi_ndti) # ndti黑白强度 cv2.imshow('color_ndti'+ str(id), color_ndti) # # ndti彩色强度 # 图像叠加 add_img = img_add(frame, color_ndti) cv2.imshow('add_ori_ndti' + str(id), add_img) param = {'Image': frame} cv2.setMouseCallback('original', callback, param=param) cv2.waitKey(0) cv2.destroyAllWindows() def main(video_dir, mask_dir, id): # 视频分析浊度 video_path = video_dir # 加载掩膜 mask_path = mask_dir mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if mask is None: raise RuntimeError('mask open failed') # 除数倍率 scale = 4 if id != 3 else 2 mask = cv2.resize(mask, (mask.shape[1] // scale, mask.shape[0] // scale)) # 视频读取 cap = cv2.VideoCapture(video_path) pause = False # 检查视频是否成功打开 if not cap.isOpened(): print("错误:无法打开视频文件。") print("可能的原因:文件路径错误,或系统缺少必要的解码器。") exit() else: ret, frame = cap.read() # 成功打开后,逐帧读取和显示视频 global_img = [None] is_night = False while True: if not pause: ret, frame = cap.read() # ret指示是否成功读取帧,frame是图像数据 # 如果读取帧失败(可能是文件损坏或已到结尾),退出循环 if not ret: print("视频播放完毕。") break # 判断是否为夜间模式 if np.mean(np.abs(frame[:, :, 0] - frame[:, :, 1])) < 0.1: is_night = True # 处理当前帧 # 缩放 frame = cv2.resize(frame, (frame.shape[1] // scale, frame.shape[0] // scale)) # cv2.imshow('original', frame) # 滤波 # frame = cv2.GaussianBlur(frame, (5, 5), 1.5) # 计算掩膜区域的NDTI值 roi_ndti = region_ndti(frame, mask, is_night) # 给ndti着色 color_ndti = colorful_ndti_matrix(roi_ndti) # 绘制直方图 roi_index = mask > 0 roi_ndti_fla = roi_ndti[roi_index] # 仅保留感兴趣区的计算结果 # plot_histogram_seaborn_simple(roi_ndti_fla,-1,1,0.01, str(id)) # 打印统计信息 # 3σ原则 mean_value = np.mean(roi_ndti_fla) print('调整前平均值:', mean_value) std_value = np.std(roi_ndti_fla) print('调整前平均值:', std_value) min_value = np.min(roi_ndti_fla) max_value = np.max(roi_ndti_fla) up_limit = mean_value + 3 * std_value down_limit = mean_value - 3 * std_value # 调整 roi_ndti_fla = roi_ndti_fla[(up_limit >= roi_ndti_fla) & (roi_ndti_fla >= down_limit)] text = f'pixs:{int(np.sum(roi_ndti_fla))} adj_mean:{roi_ndti_fla.mean():.3f} adj_std:{roi_ndti_fla.std():.3f}' cv2.putText(frame, text, (10, 25), cv2.FONT_HERSHEY_DUPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA) # print(f"""统计信息: # 总像素数: {np.sum(roi_ndti_fla):,} # 灰度范围: {min_value:.1f} - {max_value:.1f} # 平均值: {mean_value:.2f} # 标准差: {std_value:.2f} # 调整平均值:{roi_ndti_fla.mean():.2f} # 调整标准差:{roi_ndti_fla.std():.2f} # """ # ) # 显示当前帧处理结果 #cv2.imshow('original', frame) # 原图 #cv2.imshow('mask'+ str(id), mask) # 掩膜 #roi_ndti = np.abs(roi_ndti*255.).astype(np.uint8) #cv2.imshow('ndti'+ str(id), roi_ndti) # ndti黑白强度 #cv2.imshow('color_ndti'+ str(id), color_ndti) # # ndti彩色强度 # 图像叠加 add_img = img_add(frame, color_ndti) global_img[0] = add_img cv2.imshow('add_ori_ndti'+ str(id), add_img) #cv2.imshow('Histogram', hist_img) # 播放帧率为25FPS,如果期间按下'q'键则退出循环 key = cv2.waitKey(500) & 0xFF if key == ord(' '): pause = not pause status = "已暂停" if pause else "播放中" print(f"{id} 状态: {status}") if key == ord('q'): break if pause: if global_img[0] is not None: param = {'Image': global_img[0],'window_name': 'add_ori_ndti' + str(id), 'is_night': is_night} cv2.setMouseCallback('add_ori_ndti' + str(id), callback, param=param) # 释放资源并关闭所有窗口 cap.release() if __name__ == '__main__': set_chinese_font() # single_img(r'D:\code\water_turbidity_det\data\day_202511211129\1_device_capture.jpg', # r'D:\code\water_turbidity_det\draw_mask\mask\1_main_20251119102036_@1000000.png') # pass # path1 = r'D:\code\water_turbidity_det\data\day_202511211129\1_video_202511211128.dav' # path2 = r'D:\code\water_turbidity_det\data\day_202511211129\2_video_202511211128.dav' # path3 = r'D:\code\water_turbidity_det\data\day_202511211129\3_video_202511211128.dav' # path4 = r'D:\code\water_turbidity_det\data\day_202511211129\4_video_202511211128.dav' # # path1 = r'D:\code\water_turbidity_det\data\night\1_video_20251120.dav' # path2 = r'D:\code\water_turbidity_det\data\night\2_video_20251120_1801.dav' # path3 = r'D:\code\water_turbidity_det\data\night\3_video_20251120_1759.dav' # path4 = r'D:\code\water_turbidity_det\data\night\4_video_20251120_1800.dav' # # t1 = threading.Thread(target=main, kwargs={'video_dir': path1, # 'mask_dir': r'D:\code\water_turbidity_det\draw_mask\mask\1_main_20251119102036_@1000000.png', # 'id':1}) # t2 = threading.Thread(target=main, kwargs={'video_dir': path2, # 'mask_dir': r'D:\code\water_turbidity_det\draw_mask\mask\2_main_20251119102038_@1000000.png', # 'id':2}) # t3 = threading.Thread(target=main, kwargs={'video_dir': path3, # 'mask_dir': r'D:\code\water_turbidity_det\draw_mask\mask\3_main_20251119102042_@1000000.png', # 'id':3}) # t4 = threading.Thread(target=main, kwargs={'video_dir': path4, # 'mask_dir': r'D:\code\water_turbidity_det\draw_mask\mask\4_main_20251119102044_@1000000.png', # 'id':4}) # # threads = [t1, t2, t3, t4] # threads = [t4] # for t in threads: # t.start() # for t in threads: # if t.is_alive(): # t.join() main(video_dir=r'/video/records_202512031553\video4_20251129053218_20251129060528.mp4', mask_dir=r'D:\code\water_turbidity_det\draw_mask\mask\4_device_capture.png', id=4) cv2.destroyAllWindows()