| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- 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()
|