jiyuhang 3 місяців тому
батько
коміт
9c57ea9ef3

+ 5 - 0
.idea/.gitignore

@@ -0,0 +1,5 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/

+ 6 - 0
.idea/inspectionProfiles/profiles_settings.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>

+ 7 - 0
.idea/misc.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Black">
+    <option name="sdkName" value="torch" />
+  </component>
+  <component name="ProjectRootManager" version="2" project-jdk-name="torch" project-jdk-type="Python SDK" />
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/water_turbidity_det.iml" filepath="$PROJECT_DIR$/.idea/water_turbidity_det.iml" />
+    </modules>
+  </component>
+</project>

+ 7 - 0
.idea/vcs.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 8 - 0
.idea/water_turbidity_det.iml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="jdk" jdkName="torch" jdkType="Python SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

BIN
__pycache__/main.cpython-313.pyc


+ 421 - 0
calculator.py

@@ -0,0 +1,421 @@
+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):
+    """
+
+    :param img: bgr image
+    :param mask:
+    :return:
+    """
+    if len(img.shape) != 3:
+        raise RuntimeError('请输入三通道彩色图像')
+
+    if len(mask.shape) != 2:
+        raise RuntimeError('请输入单通道掩膜图像')
+    img = img.astype(np.float32)
+    mask = mask.astype(np.float32)
+    #b = img[:,:,0]
+    g = img[:,:,1]
+    r = img[:,:,2]
+
+    ndti = (r - g) / (r + g + 1e-6)
+    if np.max(mask) > 2:
+        mask /= 255
+    ndti *= mask
+    return ndti
+
+def region_night(img, mask):
+    if len(img.shape) != 3:
+        raise RuntimeError('请输入三通道彩色图像')
+
+    if len(mask.shape) != 2:
+        raise RuntimeError('请输入单通道掩膜图像')
+    img = img.astype(np.float32)
+    mask = mask.astype(np.float32)
+    ir = img[:,:,2]
+
+    if np.max(mask) > 2:
+        mask /= 255
+    ir *= mask
+    return ir
+
+
+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))
+            # 滤波
+            # frame = cv2.GaussianBlur(frame, (5, 5), 1.5)
+            # 计算掩膜区域的NDTI值
+            if is_night:
+                roi_ndti = region_night(frame, mask)
+            else:
+                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]  # 仅保留感兴趣区的计算结果
+            plot_histogram_seaborn_simple(roi_ndti_fla,-1,1,0.01, str(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)
+            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_img_202511211412.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_2025_1120.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()
+
+    cv2.destroyAllWindows()
+
+

BIN
data/1_main_20251119102036_@1000000.jpg


BIN
data/2_main_20251119102038_@1000000.jpg


BIN
data/3_main_20251119102042_@1000000.jpg


BIN
data/4_main_20251119102044_@1000000.jpg


BIN
data/day_202511211129/1_img_202511211412.jpg


BIN
data/day_202511211129/1_video_202511211128.dav


BIN
data/day_202511211129/2_video_202511211128.dav


BIN
data/day_202511211129/3_video_202511211127.dav


BIN
data/day_202511211129/4_video_202511211127.dav


BIN
data/night/1_video_2025_1120.dav


BIN
data/night/2_video_20251120_1801.dav


BIN
data/night/3_video_20251120_1759.dav


BIN
data/night/4_video_20251120_1800.dav


+ 80 - 0
draw_mask/draw.py

@@ -0,0 +1,80 @@
+import cv2
+import numpy as np
+import os
+
+class DrawRectangle:
+
+    def __init__(self, div_scale):
+        self.mask_save_dir = './mask'
+        self.scale = div_scale
+        self.current_roi_points = []
+        self.rois = []
+        self.window_title = "Image - Select ROI"
+        self.draw_complete = False
+        pass
+
+    def callback(self, event, x, y, flags, param):
+
+        drawing_image = param['Image']
+        # 左键添加感兴趣点
+        if event == cv2.EVENT_LBUTTONDOWN:
+            # 添加感兴趣点
+            self.current_roi_points.append((x, y))
+            # 绘制标记点
+            cv2.circle(drawing_image, (x, y), 4, (0, 255, 0), -1)
+            # 绘制多边形
+            if len(self.current_roi_points) > 1:
+                cv2.line(drawing_image, self.current_roi_points[-2], self.current_roi_points[-1], (0, 255, 0), 2)
+            # 显示图像
+            cv2.imshow(self.window_title, drawing_image)
+            print(f'添加感兴趣点:{y}行, {x}列')
+        # 右键闭合感兴趣区域
+        if event == cv2.EVENT_RBUTTONDOWN:
+            if len(self.current_roi_points) < 3:
+                print("[提示] ROI 至少需要 3 个点构成多边形!")
+                return
+            cv2.line(drawing_image, self.current_roi_points[-1], self.current_roi_points[0], (0, 255, 0), 2)
+            cv2.imshow(self.window_title, drawing_image)
+            # 清理
+            self.rois.append(self.current_roi_points)
+            print(f'添加感兴趣区,包含点数:{len(self.current_roi_points)}个')
+            self.current_roi_points = []
+
+
+    def draw(self, img_path: str):
+        """在输入图像中绘制多边形区域,然后生成相应的mask图片"""
+        # 读取图像
+        ori_img = cv2.imread(img_path)
+        mask_base_name = os.path.splitext(os.path.basename(img_path))[0] + '.png'
+        img = cv2.resize(ori_img, (ori_img.shape[1] // self.scale, ori_img.shape[0] // self.scale))
+        if img is None:
+            raise RuntimeError('Cannot read the image!')
+        param = {'Image': img}
+        cv2.namedWindow(self.window_title)
+        cv2.setMouseCallback(self.window_title, self.callback, param=param)
+
+
+        # 显示图像并等待退出
+        while True:
+            cv2.imshow(self.window_title, img)
+            key = cv2.waitKey(1) & 0xFF
+            if key == ord('q') or key == 27:  # 按'q'或ESC键退出
+                break
+
+        # 为原图生成掩膜
+        mask = np.zeros((ori_img.shape[0], ori_img.shape[1]),dtype=np.uint8)  # shape等于原始输入图像
+        for roi in self.rois:
+            roi_points = np.array(roi, np.int32).reshape((-1, 1, 2)) * self.scale  # 兴趣点的缩放处理
+            cv2.fillPoly(mask, [roi_points], 255)
+
+        # 保存掩膜图像
+        if not os.path.exists(self.mask_save_dir):
+            os.makedirs(self.mask_save_dir)
+        cv2.imwrite(os.path.join(self.mask_save_dir, mask_base_name), mask)
+        # cv2.imshow("mask", mask)
+        # cv2.waitKey(0)
+        cv2.destroyAllWindows()
+
+if __name__ == '__main__':
+    drawer = DrawRectangle(2)
+    drawer.draw(r"D:\code\water_turbidity_det\data\4_main_20251119102044_@1000000.jpg")

BIN
draw_mask/mask/1_main_20251119102036_@1000000.png


BIN
draw_mask/mask/2_main_20251119102038_@1000000.png


BIN
draw_mask/mask/3_main_20251119102042_@1000000.png


BIN
draw_mask/mask/4_main_20251119102044_@1000000.png



+ 46 - 0
main.py

@@ -0,0 +1,46 @@
+import cv2
+
+
+img_path = 'qingche.jpg'
+
+bgr_img = cv2.resize(cv2.imread(img_path), (512, 512))
+if bgr_img is None:
+    raise ValueError(f"无法读取图像: {img_path}")
+b_channel, g_channel, r_channel = cv2.split(bgr_img)
+
+def mouse_callback(event, x, y, flags, param):
+    """鼠标回调函数:点击时显示灰度值"""
+    if event == cv2.EVENT_LBUTTONDOWN:
+        # 获取灰度值(灰度图中直接取值)
+        img = param.get('img')
+        window = param.get('window')
+        gray_value = img[y, x]
+        # 在彩色原图的副本上显示文本
+        text = f'Gray: {gray_value}'
+        cv2.putText(img, text, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
+        cv2.imshow(window, img)
+
+# 创建窗口并绑定回调函数
+for pic, name in [(b_channel, 'B Channel'),(g_channel, 'G Channel'),(r_channel, 'R Channel')]:
+    img = pic
+    window = name
+    params = {
+        'img':img,
+        'window':window
+    }
+    cv2.namedWindow(window)
+    cv2.setMouseCallback(window, mouse_callback, param=params)
+
+# 显示图像并等待退出
+while True:
+    cv2.imshow('B Channel', b_channel)
+    cv2.imshow('G Channel', g_channel)
+    cv2.imshow('R Channel', r_channel)
+    cv2.imshow('rgb', bgr_img)
+    key = cv2.waitKey(1) & 0xFF
+    if key == ord('q') or key == 27:  # 按'q'或ESC键退出
+        break
+
+cv2.destroyAllWindows()
+pass
+


+ 265 - 0
roi_creator_mp.py

@@ -0,0 +1,265 @@
+# -*- coding: utf-8 -*-
+"""
+代码一:定义数据并保存(支持多个ROI)
+
+功能:
+1. 加载图一。
+2. 允许用户交互式地绘制多个多边形 ROI,并为每个ROI命名。
+3. 允许用户交互式地绘制多个矩形屏蔽区。
+4. 将所有ROI坐标、名称、屏蔽区坐标和相关参数打包保存到一个 JSON 文件中。
+"""
+import cv2
+import numpy as np
+import json
+import os
+import base64
+
+# ========== 全局变量 ==========
+# --- ROI 绘制相关 ---
+current_roi_points = []
+all_rois = []  # 存储所有ROI及其名称
+drawing_complete = False
+drawing_image = None
+original_image = None
+# --- 屏蔽区绘制相关 ---
+ignore_polygons = []
+drawing_ignore = None
+drawing_rect = False
+rect_start_point = None
+
+# ========== 可配置参数 ==========
+CONFIG_PARAMS = {
+    "MARGIN_RATIO": 0.25,
+    "MIN_MATCH_COUNT": 10,
+    "USE_CLAHE": True
+}
+
+
+# -------------------------------------------------
+# 1. 回调函数:选择 ROI
+# -------------------------------------------------
+def select_roi(event, x, y, flags, param):
+    """用于绘制 ROI 多边形的回调函数"""
+    global current_roi_points, drawing_complete, drawing_image
+
+    if drawing_complete:
+        return
+
+    if event == cv2.EVENT_LBUTTONDOWN:
+        current_roi_points.append((x, y))
+        cv2.circle(drawing_image, (x, y), 4, (0, 255, 0), -1)
+        if len(current_roi_points) > 1:
+            cv2.line(drawing_image, current_roi_points[-2], current_roi_points[-1], (0, 255, 0), 2)
+        cv2.imshow("Image 1 - Select ROI", drawing_image)
+
+    elif event == cv2.EVENT_RBUTTONDOWN:
+        if len(current_roi_points) < 3:
+            print("[提示] ROI 至少需要 3 个点构成多边形!")
+            return
+        drawing_complete = True
+        cv2.line(drawing_image, current_roi_points[-1], current_roi_points[0], (0, 255, 0), 2)
+        cv2.imshow("Image 1 - Select ROI", drawing_image)
+        print("[提示] 当前ROI选择完毕,按任意键继续...")
+
+
+# -------------------------------------------------
+# 2. 回调函数:选择屏蔽区
+# -------------------------------------------------
+def select_ignore_rect(event, x, y, flags, param):
+    """用于绘制屏蔽矩形的回调函数"""
+    global rect_start_point, drawing_rect, ignore_polygons, drawing_ignore
+
+    if event == cv2.EVENT_LBUTTONDOWN:
+        rect_start_point = (x, y)
+        drawing_rect = True
+
+    elif event == cv2.EVENT_MOUSEMOVE:
+        if drawing_rect:
+            preview_img = drawing_ignore.copy()
+            cv2.rectangle(preview_img, rect_start_point, (x, y), (0, 0, 255), 2)
+            cv2.imshow("Image 1 - Ignore Area", preview_img)
+
+    elif event == cv2.EVENT_LBUTTONUP:
+        if not drawing_rect:
+            return
+        drawing_rect = False
+        x_start, y_start = rect_start_point
+        x1, y1 = min(x_start, x), min(y_start, y)
+        x2, y2 = max(x_start, x), max(y_start, y)
+
+        if x2 - x1 > 5 and y2 - y1 > 5:
+            cv2.rectangle(drawing_ignore, (x1, y1), (x2, y2), (0, 0, 255), 2)
+            rect_as_poly = [(x1, y1), (x2, y1), (x2, y2), (x1, y2)]
+            ignore_polygons.append(rect_as_poly)
+            print(f"[提示] 已新增屏蔽区 #{len(ignore_polygons)},可继续拉框,完成后按 Enter 键。")
+        cv2.imshow("Image 1 - Ignore Area", drawing_ignore)
+
+
+# -------------------------------------------------
+# 3. 工具函数
+# -------------------------------------------------
+def read_image_chinese(path):
+    """读取可能包含中文路径的图片"""
+    try:
+        with open(path, 'rb') as f:
+            data = np.frombuffer(f.read(), np.uint8)
+        return cv2.imdecode(data, cv2.IMREAD_COLOR)
+    except Exception as e:
+        print(f"读取图片失败: {e}")
+        return None
+
+
+def draw_all_rois(image, rois_list):
+    """在图像上绘制所有已保存的ROI"""
+    result = image.copy()
+    colors = [(0, 255, 0), (255, 0, 0), (0, 0, 255), (255, 255, 0),
+              (255, 0, 255), (0, 255, 255), (128, 255, 0), (255, 128, 0)]
+
+    for idx, roi_data in enumerate(rois_list):
+        color = colors[idx % len(colors)]
+        points = roi_data["points"]
+        name = roi_data["name"]
+
+        # 绘制多边形
+        cv2.polylines(result, [np.array(points, np.int32)], True, color, 2)
+
+        # 计算中心点并显示名称
+        center = np.mean(points, axis=0).astype(int)
+        cv2.putText(result, name, tuple(center), cv2.FONT_HERSHEY_SIMPLEX,
+                    0.7, color, 2, cv2.LINE_AA)
+
+    return result
+
+
+# -------------------------------------------------
+# 4. 主函数
+# -------------------------------------------------
+def define_and_save_data(image1_path, output_json_path):
+    """
+    主流程函数:加载图像,引导用户选择多个ROI并命名,最终保存数据。
+    """
+    global current_roi_points, drawing_complete, drawing_image, all_rois
+    global drawing_ignore, ignore_polygons, original_image
+
+    # --- 步骤 1: 加载图像 ---
+    img1 = read_image_chinese(image1_path)
+    if img1 is None:
+        print(f"错误:无法加载图像 '{image1_path}',请检查路径!")
+        return
+
+    original_image = img1.copy()
+
+    # --- 步骤 2: 循环选择多个ROI ---
+    print("\n========== 开始选择ROI ==========")
+    roi_count = 0
+
+    while True:
+        roi_count += 1
+        current_roi_points = []
+        drawing_complete = False
+
+        # 显示已有的ROI
+        if all_rois:
+            drawing_image = draw_all_rois(original_image, all_rois)
+        else:
+            drawing_image = original_image.copy()
+
+        cv2.namedWindow("Image 1 - Select ROI")
+        cv2.setMouseCallback("Image 1 - Select ROI", select_roi)
+
+        print(f"\n【ROI #{roi_count}】")
+        print("用鼠标左键多点圈出ROI,右键闭合完成当前ROI。")
+        print("按 ESC 键结束所有ROI选择,按其他键继续添加下一个ROI。")
+        cv2.imshow("Image 1 - Select ROI", drawing_image)
+
+        key = cv2.waitKey(0)
+
+        if key == 27:  # ESC键,结束ROI选择
+            cv2.destroyWindow("Image 1 - Select ROI")
+            if len(current_roi_points) >= 3:
+                # 保存最后一个ROI
+                roi_name = input(f"请输入ROI #{roi_count} 的名称: ").strip()
+                if not roi_name:
+                    roi_name = f"ROI_{roi_count}"
+                all_rois.append({
+                    "name": roi_name,
+                    "points": current_roi_points
+                })
+                print(f"已保存ROI: {roi_name}")
+            break
+        else:
+            if len(current_roi_points) >= 3:
+                # 为当前ROI输入名称
+                roi_name = input(f"请输入ROI #{roi_count} 的名称: ").strip()
+                if not roi_name:
+                    roi_name = f"ROI_{roi_count}"
+
+                all_rois.append({
+                    "name": roi_name,
+                    "points": current_roi_points
+                })
+                print(f"已保存ROI: {roi_name}")
+            else:
+                print("当前ROI点数不足,跳过保存。")
+                roi_count -= 1
+
+    if not all_rois:
+        print("错误:未选择任何ROI,程序退出。")
+        return
+
+    print(f"\n总共选择了 {len(all_rois)} 个ROI")
+
+    # --- 步骤 3: 交互式选择屏蔽区 ---
+    drawing_ignore = draw_all_rois(original_image, all_rois)
+    cv2.namedWindow("Image 1 - Ignore Area")
+    cv2.setMouseCallback("Image 1 - Ignore Area", select_ignore_rect)
+    print("\n【步骤 2】在弹窗中,用鼠标左键拖拽拉框来选择屏蔽区,可画多个。")
+    print("         所有屏蔽区画完后,按 Enter 键保存并结束,或按 Esc 键退出。")
+    cv2.imshow("Image 1 - Ignore Area", drawing_ignore)
+
+    while True:
+        key = cv2.waitKey(0)
+        if key in [13, 10]:  # Enter键
+            break
+        elif key == 27:  # Esc
+            cv2.destroyAllWindows()
+            print("用户取消操作。")
+            return
+    cv2.destroyAllWindows()
+
+    # --- 步骤 4: 封装数据并保存到 JSON ---
+    try:
+        with open(image1_path, 'rb') as f:
+            image_bytes = f.read()
+        image_base64 = base64.b64encode(image_bytes).decode('utf-8')
+        image_filename = os.path.basename(image1_path)
+    except Exception as e:
+        print(f"\n[失败] 读取并编码图像文件时出错: {e}")
+        return
+
+    # 封装数据
+    data_to_save = {
+        "image1_filename": image_filename,
+        "image1_data_base64": image_base64,
+        "rois": all_rois,  # 保存所有ROI及其名称
+        "ignore_polygons": ignore_polygons,
+        "parameters": CONFIG_PARAMS
+    }
+
+    try:
+        with open(output_json_path, 'w', encoding='utf-8') as f:
+            json.dump(data_to_save, f, indent=4, ensure_ascii=False)
+        print(f"\n[成功] {len(all_rois)} 个ROI、屏蔽区和图像数据已成功保存到: {output_json_path}")
+    except Exception as e:
+        print(f"\n[失败] 保存 JSON 文件时出错: {e}")
+
+
+# -------------------------------------------------
+if __name__ == "__main__":
+    IMG1_PATH = r"20250615000002.jpg"
+    OUTPUT_JSON = "roi_json_dir/matching_data_multi_roi.json"
+
+    # 创建输出目录
+    os.makedirs(os.path.dirname(OUTPUT_JSON), exist_ok=True)
+
+    define_and_save_data(IMG1_PATH, OUTPUT_JSON)