calculator.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import cv2
  2. import numpy as np
  3. import matplotlib
  4. matplotlib.use('Agg')
  5. from matplotlib import pyplot as plt
  6. from matplotlib import rcParams
  7. import seaborn as sns
  8. import threading
  9. def set_chinese_font():
  10. rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei', 'Microsoft YaHei', 'SimSun']
  11. rcParams['axes.unicode_minus'] = False
  12. def region_ndti(img, mask, is_night):
  13. """对于可见光图像计算ndti,对于红外图像直接取值"""
  14. if len(img.shape) != 3:
  15. raise RuntimeError('请输入三通道彩色图像')
  16. if len(mask.shape) != 2:
  17. raise RuntimeError('请输入单通道掩膜图像')
  18. img = img.astype(np.float32)
  19. mask = mask.astype(np.float32)
  20. # 取通道
  21. g = img[:,:,1]
  22. r = img[:,:,2]
  23. # 判断是否为红外
  24. if is_night: # 红外直接取值
  25. ndti = r
  26. else: # 可见光计算ndti
  27. ndti = (r - g) / (r + g + 1e-6)
  28. # 掩膜归一化
  29. if np.max(mask) > 2:
  30. mask /= 255.
  31. # 仅保留掩膜区域的ndti
  32. ndti *= mask
  33. return ndti
  34. def plot_histogram_opencv(matrix, start, end, step, title):
  35. """使用OpenCV绘制直方图,避免GUI冲突"""
  36. # 计算直方图
  37. num_bins = int((end - start) / step)
  38. hist, bins = np.histogram(matrix.ravel(), bins=num_bins, range=(start, end))
  39. # 创建直方图图像(增加高度以便显示标签)
  40. hist_img = np.zeros((450, 600, 3), dtype=np.uint8)
  41. hist_img.fill(255) # 白色背景
  42. # 归一化直方图:确保高度在合理范围内
  43. if len(hist) > 0 and np.max(hist) > 0:
  44. # 保留10像素的边距
  45. hist_normalized = cv2.normalize(hist, None, 0, 350, cv2.NORM_MINMAX)
  46. else:
  47. hist_normalized = np.zeros_like(hist)
  48. # 计算合理的矩形宽度(确保至少1像素宽)
  49. if len(hist) > 0:
  50. bin_width = max(1, 600 // len(hist)) # 确保最小宽度为1
  51. else:
  52. bin_width = 1
  53. # 绘制直方图矩形
  54. for i in range(len(hist_normalized)):
  55. if i >= 600: # 防止索引越界
  56. break
  57. x1 = i * bin_width
  58. x2 = min(x1 + bin_width, 599) # 确保不超出图像边界
  59. y1 = 400 - int(hist_normalized[i]) # 从底部开始计算高度
  60. y2 = 400
  61. # 只绘制有高度的矩形
  62. if y1 < y2:
  63. cv2.rectangle(hist_img, (x1, y1), (x2, y2), (0, 0, 255), -1)
  64. # 添加坐标轴
  65. cv2.line(hist_img, (50, 400), (550, 400), (0, 0, 0), 2) # x轴
  66. cv2.line(hist_img, (50, 400), (50, 50), (0, 0, 0), 2) # y轴
  67. # 添加标题和标签(调整位置避免被裁剪)
  68. cv2.putText(hist_img, title, (10, 30),
  69. cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2)
  70. cv2.putText(hist_img, 'gray', (280, 430),
  71. cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
  72. cv2.putText(hist_img, 'fre', (5, 200),
  73. cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1)
  74. return hist_img
  75. def plot_histogram_seaborn_simple(matrix, start, end, step, title):
  76. """
  77. 使用Seaborn的histplot函数直接绘制(最简单的方法)
  78. """
  79. # 解决中文显示问题
  80. plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
  81. plt.rcParams['axes.unicode_minus'] = False
  82. plt.figure(figsize=(12, 6))
  83. # 直接使用Seaborn的histplot,设置bins参数[3](@ref)
  84. num_bins = int((end - start) / step)
  85. sns.histplot(matrix.ravel(), bins=num_bins, kde=True,
  86. color='skyblue', alpha=0.7,
  87. edgecolor='white', linewidth=0.5)
  88. plt.title(title+ '_histplot')
  89. plt.xlabel('灰度值')
  90. plt.ylabel('像素频率')
  91. plt.tight_layout()
  92. # plt.show()
  93. plt.savefig('temp_' + title +'.png') # 保存到文件而不是显示
  94. plt.close() # 关闭图形,释放资源
  95. # 如果需要显示,可以用cv2读取并显示
  96. hist_img = cv2.imread('temp_' + title +'.png')
  97. cv2.imshow('Histogram '+title, hist_img)
  98. def plot_histogram_seaborn(matrix, hist, bin_edges, start, end, step):
  99. """
  100. 使用Seaborn绘制更美观的直方图
  101. """
  102. # 设置Seaborn样式
  103. sns.set_style("whitegrid")
  104. plt.figure(figsize=(12, 6))
  105. # 将数据转换为适合Seaborn的格式
  106. flattened_data = matrix.ravel()
  107. # 使用Seaborn的histplot(会自动计算直方图,但我们用自定义的)
  108. # 这里我们手动绘制以确保使用我们的自定义bins
  109. bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
  110. # 创建条形图
  111. bars = plt.bar(bin_centers, hist, width=step * 0.8,
  112. alpha=0.7, color=sns.color_palette("viridis", 1)[0],
  113. edgecolor='white', linewidth=0.5)
  114. # 添加KDE曲线(核密度估计)
  115. sns.kdeplot(flattened_data, color='red', linewidth=2, label='密度曲线')
  116. plt.title(f'灰度分布直方图', fontsize=16)
  117. plt.xlabel('灰度值', fontsize=12)
  118. plt.ylabel('像素频率', fontsize=12)
  119. plt.legend()
  120. plt.tight_layout()
  121. plt.show()
  122. def colorful_ndti_matrix(ndti_matrix):
  123. """给NDTI矩阵着色"""
  124. # 存放着色后的NDTI矩阵
  125. color_ndti_matrix = np.zeros((ndti_matrix.shape[0], ndti_matrix.shape[1], 3), dtype=np.uint8)
  126. negative_mask = ndti_matrix < 0
  127. positive_mask = ndti_matrix > 0
  128. # 处理负值
  129. if np.any(negative_mask):
  130. blue_intensity = ndti_matrix.copy()
  131. blue_intensity[positive_mask] = 0
  132. blue_intensity[negative_mask] *= -255.
  133. blue_intensity = np.clip(blue_intensity, 0, 255).astype(np.uint8)
  134. color_ndti_matrix[:, :,0] = blue_intensity
  135. # 处理正值区域(红色渐变)
  136. if np.any(positive_mask):
  137. # 将0到+1映射到0-255的红色强度
  138. red_intensity = ndti_matrix.copy()
  139. red_intensity[negative_mask] = 0
  140. red_intensity[positive_mask] *= 255
  141. red_intensity = np.clip(red_intensity, 0, 255).astype(np.uint8)
  142. color_ndti_matrix[:, :, 2] = red_intensity
  143. # 返回着色后的ndti矩阵
  144. return color_ndti_matrix
  145. def img_add(img1, img2, img1_w=1, img2_w=0, gamma=0):
  146. if len(img1.shape) != len(img2.shape):
  147. raise ValueError('img1 and img2 must have the same shape')
  148. # 设置权重参数(透明度)
  149. alpha = img1_w # 第一张图像的权重
  150. beta = img2_w # 第二张图像的权重
  151. gamma =gamma # 亮度调节参数
  152. # 执行加权叠加
  153. result = cv2.addWeighted(img1, alpha, img2, beta, gamma)
  154. return result
  155. def callback(event, x, y, flags, param):
  156. if event == cv2.EVENT_LBUTTONDOWN:
  157. is_night_ = param['is_night']
  158. frame = param['Image']
  159. w_name = param['window_name']
  160. b = frame[:,:,0].astype(np.float32)
  161. g = frame[:,:,1].astype(np.float32)
  162. r = frame[:,:,2].astype(np.float32)
  163. if is_night_:
  164. ndti_value = r[y, x]
  165. else:
  166. ndti_value = (r[y,x] - g[y,x]) / (r[y,x] + g[y,x])
  167. cv2.putText(frame, f'{ndti_value:.2f}', (x, y), cv2.FONT_HERSHEY_DUPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA)
  168. cv2.putText(frame, f'paused', (10, 45), cv2.FONT_HERSHEY_DUPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA)
  169. cv2.imshow(w_name, frame)
  170. def single_img(img_path,mask_path, id=0):
  171. frame = cv2.imread(img_path)
  172. if frame is None:
  173. raise RuntimeError('img open failed')
  174. mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
  175. if mask is None:
  176. raise RuntimeError('mask open failed')
  177. scale = 4 if id != 3 else 2
  178. mask = cv2.resize(mask, (mask.shape[1] // scale, mask.shape[0] // scale))
  179. frame = cv2.resize(frame, (frame.shape[1] // scale, frame.shape[0] // scale))
  180. roi_ndti = region_ndti(frame, mask)
  181. # 给ndti着色
  182. color_ndti = colorful_ndti_matrix(roi_ndti)
  183. # 绘制直方图
  184. roi_index = mask > 0
  185. roi_ndti_fla = roi_ndti[roi_index] # 仅保留感兴趣区的计算结果
  186. hist_img = plot_histogram_opencv(roi_ndti[roi_index], -1, 1, 0.01, f'NDTI Histogram {id}')
  187. # 打印统计信息
  188. # 3σ原则
  189. mean_value = np.mean(roi_ndti_fla)
  190. std_value = np.std(roi_ndti_fla)
  191. min_value = np.min(roi_ndti_fla)
  192. max_value = np.max(roi_ndti_fla)
  193. up_limit = mean_value + 3 * std_value
  194. down_limit = mean_value - 3 * std_value
  195. # 调整
  196. roi_ndti_fla = roi_ndti_fla[(up_limit >= roi_ndti_fla) & (roi_ndti_fla >= down_limit)]
  197. text = f'pixs:{int(np.sum(roi_ndti_fla))} adj_mean:{roi_ndti_fla.mean():.3f} adj_std:{roi_ndti_fla.std():.3f}'
  198. cv2.putText(frame, text, (10, 25), cv2.FONT_HERSHEY_DUPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA)
  199. print(f"""统计信息:
  200. 总像素数: {np.sum(roi_ndti_fla):,}
  201. 灰度范围: {min_value:.1f} - {max_value:.1f}
  202. 平均值: {mean_value:.2f}
  203. 标准差: {std_value:.2f}
  204. 调整平均值:{roi_ndti_fla.mean():.2f}
  205. 调整标准差:{roi_ndti_fla.std():.2f}
  206. """
  207. )
  208. # 显示当前帧处理结果
  209. cv2.imshow('original', frame) # 原图
  210. cv2.imshow('mask'+ str(id), mask) # 掩膜
  211. roi_ndti = np.abs(roi_ndti*255.).astype(np.uint8)
  212. cv2.imshow('ndti'+ str(id), roi_ndti) # ndti黑白强度
  213. cv2.imshow('color_ndti'+ str(id), color_ndti) # # ndti彩色强度
  214. # 图像叠加
  215. add_img = img_add(frame, color_ndti)
  216. cv2.imshow('add_ori_ndti' + str(id), add_img)
  217. param = {'Image': frame}
  218. cv2.setMouseCallback('original', callback, param=param)
  219. cv2.waitKey(0)
  220. cv2.destroyAllWindows()
  221. def main(video_dir, mask_dir, id):
  222. # 视频分析浊度
  223. video_path = video_dir
  224. # 加载掩膜
  225. mask_path = mask_dir
  226. mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
  227. if mask is None:
  228. raise RuntimeError('mask open failed')
  229. # 除数倍率
  230. scale = 4 if id != 3 else 2
  231. mask = cv2.resize(mask, (mask.shape[1] // scale, mask.shape[0] // scale))
  232. # 视频读取
  233. cap = cv2.VideoCapture(video_path)
  234. pause = False
  235. # 检查视频是否成功打开
  236. if not cap.isOpened():
  237. print("错误:无法打开视频文件。")
  238. print("可能的原因:文件路径错误,或系统缺少必要的解码器。")
  239. exit()
  240. else:
  241. ret, frame = cap.read()
  242. # 成功打开后,逐帧读取和显示视频
  243. global_img = [None]
  244. is_night = False
  245. while True:
  246. if not pause:
  247. ret, frame = cap.read() # ret指示是否成功读取帧,frame是图像数据
  248. # 如果读取帧失败(可能是文件损坏或已到结尾),退出循环
  249. if not ret:
  250. print("视频播放完毕。")
  251. break
  252. # 判断是否为夜间模式
  253. if np.mean(np.abs(frame[:, :, 0] - frame[:, :, 1])) < 0.1:
  254. is_night = True
  255. # 处理当前帧
  256. # 缩放
  257. frame = cv2.resize(frame, (frame.shape[1] // scale, frame.shape[0] // scale))
  258. # cv2.imshow('original', frame)
  259. # 滤波
  260. # frame = cv2.GaussianBlur(frame, (5, 5), 1.5)
  261. # 计算掩膜区域的NDTI值
  262. roi_ndti = region_ndti(frame, mask, is_night)
  263. # 给ndti着色
  264. color_ndti = colorful_ndti_matrix(roi_ndti)
  265. # 绘制直方图
  266. roi_index = mask > 0
  267. roi_ndti_fla = roi_ndti[roi_index] # 仅保留感兴趣区的计算结果
  268. # plot_histogram_seaborn_simple(roi_ndti_fla,-1,1,0.01, str(id))
  269. # 打印统计信息
  270. # 3σ原则
  271. mean_value = np.mean(roi_ndti_fla)
  272. print('调整前平均值:', mean_value)
  273. std_value = np.std(roi_ndti_fla)
  274. print('调整前平均值:', std_value)
  275. min_value = np.min(roi_ndti_fla)
  276. max_value = np.max(roi_ndti_fla)
  277. up_limit = mean_value + 3 * std_value
  278. down_limit = mean_value - 3 * std_value
  279. # 调整
  280. roi_ndti_fla = roi_ndti_fla[(up_limit >= roi_ndti_fla) & (roi_ndti_fla >= down_limit)]
  281. text = f'pixs:{int(np.sum(roi_ndti_fla))} adj_mean:{roi_ndti_fla.mean():.3f} adj_std:{roi_ndti_fla.std():.3f}'
  282. cv2.putText(frame, text, (10, 25), cv2.FONT_HERSHEY_DUPLEX, 0.6, (0, 255, 0), 2, cv2.LINE_AA)
  283. # print(f"""统计信息:
  284. # 总像素数: {np.sum(roi_ndti_fla):,}
  285. # 灰度范围: {min_value:.1f} - {max_value:.1f}
  286. # 平均值: {mean_value:.2f}
  287. # 标准差: {std_value:.2f}
  288. # 调整平均值:{roi_ndti_fla.mean():.2f}
  289. # 调整标准差:{roi_ndti_fla.std():.2f}
  290. # """
  291. # )
  292. # 显示当前帧处理结果
  293. #cv2.imshow('original', frame) # 原图
  294. #cv2.imshow('mask'+ str(id), mask) # 掩膜
  295. #roi_ndti = np.abs(roi_ndti*255.).astype(np.uint8)
  296. #cv2.imshow('ndti'+ str(id), roi_ndti) # ndti黑白强度
  297. #cv2.imshow('color_ndti'+ str(id), color_ndti) # # ndti彩色强度
  298. # 图像叠加
  299. add_img = img_add(frame, color_ndti)
  300. global_img[0] = add_img
  301. cv2.imshow('add_ori_ndti'+ str(id), add_img)
  302. #cv2.imshow('Histogram', hist_img)
  303. # 播放帧率为25FPS,如果期间按下'q'键则退出循环
  304. key = cv2.waitKey(500) & 0xFF
  305. if key == ord(' '):
  306. pause = not pause
  307. status = "已暂停" if pause else "播放中"
  308. print(f"{id} 状态: {status}")
  309. if key == ord('q'):
  310. break
  311. if pause:
  312. if global_img[0] is not None:
  313. param = {'Image': global_img[0],'window_name': 'add_ori_ndti' + str(id), 'is_night': is_night}
  314. cv2.setMouseCallback('add_ori_ndti' + str(id), callback, param=param)
  315. # 释放资源并关闭所有窗口
  316. cap.release()
  317. if __name__ == '__main__':
  318. set_chinese_font()
  319. # single_img(r'D:\code\water_turbidity_det\data\day_202511211129\1_device_capture.jpg',
  320. # r'D:\code\water_turbidity_det\draw_mask\mask\1_main_20251119102036_@1000000.png')
  321. # pass
  322. # path1 = r'D:\code\water_turbidity_det\data\day_202511211129\1_video_202511211128.dav'
  323. # path2 = r'D:\code\water_turbidity_det\data\day_202511211129\2_video_202511211128.dav'
  324. # path3 = r'D:\code\water_turbidity_det\data\day_202511211129\3_video_202511211128.dav'
  325. # path4 = r'D:\code\water_turbidity_det\data\day_202511211129\4_video_202511211128.dav'
  326. #
  327. # path1 = r'D:\code\water_turbidity_det\data\night\1_video_20251120.dav'
  328. # path2 = r'D:\code\water_turbidity_det\data\night\2_video_20251120_1801.dav'
  329. # path3 = r'D:\code\water_turbidity_det\data\night\3_video_20251120_1759.dav'
  330. # path4 = r'D:\code\water_turbidity_det\data\night\4_video_20251120_1800.dav'
  331. #
  332. # t1 = threading.Thread(target=main, kwargs={'video_dir': path1,
  333. # 'mask_dir': r'D:\code\water_turbidity_det\draw_mask\mask\1_main_20251119102036_@1000000.png',
  334. # 'id':1})
  335. # t2 = threading.Thread(target=main, kwargs={'video_dir': path2,
  336. # 'mask_dir': r'D:\code\water_turbidity_det\draw_mask\mask\2_main_20251119102038_@1000000.png',
  337. # 'id':2})
  338. # t3 = threading.Thread(target=main, kwargs={'video_dir': path3,
  339. # 'mask_dir': r'D:\code\water_turbidity_det\draw_mask\mask\3_main_20251119102042_@1000000.png',
  340. # 'id':3})
  341. # t4 = threading.Thread(target=main, kwargs={'video_dir': path4,
  342. # 'mask_dir': r'D:\code\water_turbidity_det\draw_mask\mask\4_main_20251119102044_@1000000.png',
  343. # 'id':4})
  344. # # threads = [t1, t2, t3, t4]
  345. # threads = [t4]
  346. # for t in threads:
  347. # t.start()
  348. # for t in threads:
  349. # if t.is_alive():
  350. # t.join()
  351. main(video_dir=r'/video/records_202512031553\video4_20251129053218_20251129060528.mp4',
  352. mask_dir=r'D:\code\water_turbidity_det\draw_mask\mask\4_device_capture.png',
  353. id=4)
  354. cv2.destroyAllWindows()