import os import re from dataclasses import datacl...

बनाया गया: 8 अक्तूबर 2026

का उपयोग करके उत्तर दिया गया GPT-6 Thinking द्वारा Chat01

प्रश्न

image.png

import os
import re
from dataclasses import dataclass

import cv2
import numpy as np
import pandas as pd

============================================================

1. 用户配置区

============================================================

图片文件夹:把带编号的图片放在这里

IMAGE_FOLDER = r"D:\Test1Photo\Calibration01\Cam31894-FromFile\9.10侧视铺展直径-160℃\光滑\侧视-光滑_20℃-2.5cm.1"

输出 Excel 路径

OUTPUT_EXCEL = os.path.join(IMAGE_FOLDER, "spreading_diameter_results.xlsx")

图片扩展名

IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff")

可选:期望分辨率检查。不想检查时设为 None

EXPECTED_VIDEO_WIDTH = 640
EXPECTED_VIDEO_HEIGHT = 384

长度标定:px 对应真实长度。暂时只需要 px 时,两项都设为 None

CALIBRATION_PIXEL_DISTANCE = 551
CALIBRATION_REAL_DISTANCE = 10
LENGTH_UNIT = "mm"

基板边缘清晰时自动吸附到水平边缘

AUTO_REFINE_SURFACE = True
MIN_SURFACE_EDGE_DROP = 8.0

============================================================

2. 基准参数(基于 640 x 384 标定,会自动缩放)

============================================================

REFERENCE_WIDTH = 640
REFERENCE_HEIGHT = 384

DARK_THRESHOLD = 200
BACKGROUND_DIFF = 60
MIN_TRACK_AREA = 350
TRACK_EDGE_MARGIN = 6
TRACK_MAX_DISTANCE = 200
SURFACE_EXCLUDE = 4
IMPACT_TOLERANCE = 5
IMPACT_CONFIRM_FRAMES = 2
MEASURE_HEIGHT = 200
CONTACT_BAND_HEIGHT = 10
MIN_COLUMN_PIXELS = 1
MIN_DIAMETER = 10
MAX_DIAMETER_RATIO = 0.90
MEASURE_EDGE_MARGIN = 0
MAX_CENTER_DISTANCE = 200
MAX_GROWTH_FACTOR = 100
MAX_GROWTH_PIXELS = 100000
DISPLAY_WIDTH = 1500
DELAY = 20
SURFACE_REFINE_RADIUS = 10
BLUR_KERNEL = (5, 5)
TRACK_CLOSE_KERNEL = (7, 7)
TRACK_OPEN_KERNEL = (3, 3)
DIFF_DILATE_KERNEL = (5, 5)
NEAR_OPEN_KERNEL = (3, 2)
COLUMN_OPEN_WIDTH = 3

def scaled_int(value, scale, minimum=1):
return max(minimum, int(round(value * scale)))

def scaled_odd(value, scale, minimum=1):
result = scaled_int(value, scale, minimum)
if result % 2 == 0:
result += 1
return result

def configure_for_resolution(width, height):
"""按照实际分辨率,从 640 x 384 基准参数生成当前图片参数。"""
global MIN_TRACK_AREA, TRACK_EDGE_MARGIN, TRACK_MAX_DISTANCE
global SURFACE_EXCLUDE, IMPACT_TOLERANCE, MEASURE_HEIGHT
global CONTACT_BAND_HEIGHT, MIN_DIAMETER, MEASURE_EDGE_MARGIN
global MAX_CENTER_DISTANCE, MAX_GROWTH_PIXELS, SURFACE_REFINE_RADIUS
global BLUR_KERNEL, TRACK_CLOSE_KERNEL, TRACK_OPEN_KERNEL
global DIFF_DILATE_KERNEL, NEAR_OPEN_KERNEL, COLUMN_OPEN_WIDTH

text
scale_x = width / REFERENCE_WIDTH scale_y = height / REFERENCE_HEIGHT scale_distance = (scale_x + scale_y) / 2.0 MIN_TRACK_AREA = scaled_int(350, scale_x * scale_y, 20) TRACK_EDGE_MARGIN = scaled_int(6, scale_x) TRACK_MAX_DISTANCE = scaled_int(120, scale_distance) SURFACE_EXCLUDE = scaled_int(4, scale_y) IMPACT_TOLERANCE = scaled_int(5, scale_y) MEASURE_HEIGHT = scaled_int(110, scale_y) CONTACT_BAND_HEIGHT = scaled_int(3, scale_y) MIN_DIAMETER = scaled_int(20, scale_x) MEASURE_EDGE_MARGIN = scaled_int(7, scale_x) MAX_CENTER_DISTANCE = scaled_int(45, scale_x) MAX_GROWTH_PIXELS = scaled_int(85, scale_x) SURFACE_REFINE_RADIUS = scaled_int(10, scale_y) BLUR_KERNEL = (scaled_odd(5, scale_x), scaled_odd(5, scale_y)) TRACK_CLOSE_KERNEL = (scaled_odd(7, scale_x), scaled_odd(7, scale_y)) TRACK_OPEN_KERNEL = (scaled_odd(3, scale_x), scaled_odd(3, scale_y)) DIFF_DILATE_KERNEL = (scaled_odd(5, scale_x), scaled_odd(5, scale_y)) NEAR_OPEN_KERNEL = (scaled_odd(3, scale_x), scaled_int(2, scale_y)) COLUMN_OPEN_WIDTH = scaled_odd(3, scale_x) return scale_x, scale_y

def length_per_pixel():
if CALIBRATION_PIXEL_DISTANCE is None or CALIBRATION_REAL_DISTANCE is None:
return None
if CALIBRATION_PIXEL_DISTANCE <= 0 or CALIBRATION_REAL_DISTANCE <= 0:
raise ValueError("长度标定值必须大于 0。")
return CALIBRATION_REAL_DISTANCE / CALIBRATION_PIXEL_DISTANCE

def format_diameter(diameter):
if diameter is None:
return "--"
text = f"{diameter} px"
conversion = length_per_pixel()
if conversion is not None:
text += f" / {diameter * conversion:.4f} {LENGTH_UNIT}"
return text

============================================================

3. 数据结构

============================================================

@dataclass
class MeasureResult:
diameter: int
x_left: int
x_right: int

============================================================

4. 手动点击基板上表面 & 液滴中心

============================================================

surface_y = None
mouse_y = None

def mouse_callback(event, x, y, flags, param):
global surface_y, mouse_y
if event == cv2.EVENT_MOUSEMOVE:
mouse_y = y
elif event == cv2.EVENT_LBUTTONDOWN:
surface_y = y
print(f"已选择基板上表面:y = {surface_y} px")

def select_surface(frame):
global surface_y, mouse_y
surface_y = None
mouse_y = None

text
window_name = "Select substrate upper surface" cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) cv2.setMouseCallback(window_name, mouse_callback) print("\n" + "=" * 62) print("请在基板真正的上表面单击,然后按 Enter 确认") print("R:重选 ESC:退出") print("=" * 62) while True: display = frame.copy() height, width = display.shape[:2] if mouse_y is not None: cv2.line(display, (0, mouse_y), (width - 1, mouse_y), (255, 0, 0), 1) if surface_y is not None: cv2.line(display, (0, surface_y), (width - 1, surface_y), (0, 255, 0), 2) cv2.putText( display, f"Surface y = {surface_y} | ENTER: confirm R: reset", (18, 34), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 150, 0), 2, cv2.LINE_AA, ) cv2.imshow(window_name, display) key = cv2.waitKey(20) & 0xFF if key in (10, 13) and surface_y is not None: break if key in (ord("r"), ord("R")): surface_y = None print("已清除,请重新选择。") elif key == 27: cv2.destroyAllWindows() raise SystemExit cv2.destroyWindow(window_name) return surface_y

def refine_surface_y(frame, clicked_y):
"""在用户点击附近寻找最强的白到黑水平边缘,统一基板坐标。"""
if not AUTO_REFINE_SURFACE:
return clicked_y

text
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, BLUR_KERNEL, 0) row_mean = np.mean(gray, axis=1) gradient = row_mean[1:] - row_mean[:-1] start = max(1, clicked_y - SURFACE_REFINE_RADIUS) stop = min(len(gradient) - 2, clicked_y + SURFACE_REFINE_RADIUS) edge_y = start + int(np.argmin(gradient[start:stop + 1])) edge_drop = -float(gradient[edge_y]) if edge_drop < MIN_SURFACE_EDGE_DROP: print( f"基板边缘较模糊(强度差 {edge_drop:.2f})," f"保留手动点击 y = {clicked_y}" ) return clicked_y refined_y = int(np.clip(edge_y + 2, 1, frame.shape[0] - 1)) print(f"基板位置自动校正:点击 y = {clicked_y} -> 使用 y = {refined_y}") return refined_y

用于点击液滴中心的回调

drop_center_x = None
drop_center_y = None
drop_mouse_x = None
drop_mouse_y = None

def drop_mouse_callback(event, x, y, flags, param):
global drop_center_x, drop_center_y, drop_mouse_x, drop_mouse_y
if event == cv2.EVENT_MOUSEMOVE:
drop_mouse_x = x
drop_mouse_y = y
elif event == cv2.EVENT_LBUTTONDOWN:
drop_center_x = x
drop_center_y = y
print(f"已选择液滴中心:x = {drop_center_x}, y = {drop_center_y}")

def select_drop_center(frame):
"""在 01 图片上手动点击液滴中心,返回 x。"""
global drop_center_x, drop_center_y, drop_mouse_x, drop_mouse_y
drop_center_x = None
drop_center_y = None
drop_mouse_x = None
drop_mouse_y = None

text
window_name = "Select droplet center on frame 01" cv2.namedWindow(window_name, cv2.WINDOW_NORMAL) cv2.setMouseCallback(window_name, drop_mouse_callback) print("\n" + "=" * 62) print("请在 01 图片上单击液滴中心,然后按 Enter 确认") print("R:重选 ESC:退出") print("=" * 62) while True: display = frame.copy() height, width = display.shape[:2] if drop_mouse_x is not None and drop_mouse_y is not None: cv2.circle(display, (drop_mouse_x, drop_mouse_y), 4, (255, 0, 0), 1) if drop_center_x is not None and drop_center_y is not None: cv2.circle(display, (drop_center_x, drop_center_y), 6, (0, 0, 255), 2) cv2.line(display, (drop_center_x, 0), (drop_center_x, height - 1), (0, 255, 0), 1) cv2.putText( display, f"Drop center x = {drop_center_x} | ENTER: confirm R: reset", (18, 34), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 150, 0), 2, cv2.LINE_AA, ) cv2.imshow(window_name, display) key = cv2.waitKey(20) & 0xFF if key in (10, 13) and drop_center_x is not None: break if key in (ord("r"), ord("R")): drop_center_x = None drop_center_y = None print("已清除,请重新选择液滴中心。") elif key == 27: cv2.destroyAllWindows() raise SystemExit cv2.destroyWindow(window_name) return drop_center_x

============================================================

5. 撞击后:去除静态基板,只保留近壁动态液滴

============================================================

def build_near_wall_mask(frame, background_gray, surface_y_value):
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, BLUR_KERNEL, 0)

text
difference = cv2.absdiff(gray, background_gray) changed = cv2.threshold(difference, BACKGROUND_DIFF, 255, cv2.THRESH_BINARY)[1] changed = cv2.dilate( changed, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, DIFF_DILATE_KERNEL), iterations=1, ) dark = cv2.threshold(gray, DARK_THRESHOLD, 255, cv2.THRESH_BINARY_INV)[1] moving_dark = cv2.bitwise_and(dark, changed) height, width = gray.shape # 基板上方全部保留,不再限制带高 y_bottom = max(1, surface_y_value - SURFACE_EXCLUDE) near_wall = np.zeros((height, width), dtype=np.uint8) near_wall[:y_bottom, :] = moving_dark[:y_bottom, :] near_wall = cv2.morphologyEx( near_wall, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_RECT, NEAR_OPEN_KERNEL), iterations=1, ) return near_wall

def contiguous_runs(values):
padded = np.pad(values.astype(np.uint8), (1, 1), constant_values=0)
changes = np.diff(padded.astype(np.int16))
starts = np.flatnonzero(changes == 1)
ends = np.flatnonzero(changes == -1) - 1
return list(zip(starts.tolist(), ends.tolist()))

def measure_near_wall(mask, impact_center_x, surface_y_value, previous_diameter=None):
height, width = mask.shape

text
count, labels, stats, _ = cv2.connectedComponentsWithStats( (mask > 0).astype(np.uint8), connectivity=4, ) components = [] for label in range(1, count): x, y, w, h, area = stats[label] if area < MIN_DIAMETER or w < MIN_DIAMETER: continue right = x + w - 1 if x <= impact_center_x <= right: center_distance = 0 else: center_distance = min(abs(impact_center_x - x), abs(impact_center_x - right)) if center_distance <= MAX_CENTER_DISTANCE: # 面积优先、距离次之 components.append((-int(area), center_distance, label)) if not components: return None, "no center-connected near-wall region" _, _, selected_label = min(components) main_component = labels == selected_label y_limit = max(1, surface_y_value - SURFACE_EXCLUDE) band = main_component[:y_limit, :].astype(np.uint8) band = cv2.erode(band, np.ones((1, 3), np.uint8), iterations=1) col_counts = np.count_nonzero(band, axis=0) cols = np.flatnonzero(col_counts > 0) if cols.size == 0: return None, "no center-connected near-wall region" x_left = int(cols.min()) x_right = int(cols.max()) diameter = x_right - x_left if x_left <= MEASURE_EDGE_MARGIN or x_right >= width - 1 - MEASURE_EDGE_MARGIN: return None, "region touches image edge" if diameter >= int(MAX_DIAMETER_RATIO * width): return None, "abnormally wide region" if previous_diameter is not None: growth_limit = max( int(previous_diameter * MAX_GROWTH_FACTOR), previous_diameter + MAX_GROWTH_PIXELS, ) if diameter > growth_limit: return None, "implausible one-frame width jump" return MeasureResult(diameter, x_left, x_right), "ok"

============================================================

6. 显示辅助

============================================================

def put_status(image, frame_number, state, diameter, max_diameter, max_frame):
status_color = (0, 170, 255) if state == "FALLING" else (0, 220, 0)

text
cv2.putText( image, f"Frame = {frame_number}", (22, 34), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2, cv2.LINE_AA, ) cv2.putText( image, f"State = {state}", (22, 68), cv2.FONT_HERSHEY_SIMPLEX, 0.75, status_color, 2, cv2.LINE_AA, ) diameter_text = f"D = {format_diameter(diameter)}" if max_diameter is None: max_text = "Dmax = --" else: max_text = f"Dmax = {format_diameter(max_diameter)} @ frame {max_frame}" cv2.putText( image, diameter_text, (22, 102), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2, cv2.LINE_AA, ) cv2.putText( image, max_text, (22, 136), cv2.FONT_HERSHEY_SIMPLEX, 0.68, (180, 0, 180), 2, cv2.LINE_AA, )

def resize_for_display(image):
if image.shape[1] <= DISPLAY_WIDTH:
return image
scale = DISPLAY_WIDTH / image.shape[1]
return cv2.resize(image, None, fx=scale, fy=scale, interpolation=cv2.INTER_AREA)

============================================================

7. 图片读取与排序

============================================================

def natural_sort_key(filename):
"""
自然排序:让 00, 01, 02, 10 按数字顺序排列,
而不是按字符串排序成 00, 01, 10, 02。
"""
name = os.path.splitext(filename)[0]
parts = re.split(r"(\d+)", name)
return [int(p) if p.isdigit() else p.lower() for p in parts]

def list_image_files(folder):
if not os.path.isdir(folder):
raise SystemExit(f"图片文件夹不存在:{folder}")

text
files = [] for name in os.listdir(folder): ext = os.path.splitext(name)[1].lower() if ext in IMAGE_EXTENSIONS: files.append(name) if not files: raise SystemExit(f"文件夹中没有找到图片:{folder}") files.sort(key=natural_sort_key) return [os.path.join(folder, name) for name in files]

def read_image(path):
# 用 numpy 先读二进制,再用 imdecode 解码,避免中文路径问题
try:
data = np.fromfile(path, dtype=np.uint8)
if data.size == 0:
raise SystemExit(f"文件为空或无法读取:{path}")
image = cv2.imdecode(data, cv2.IMREAD_COLOR)
except Exception as e:
raise SystemExit(f"读取图片失败:{path}\n错误:{e}")

text
if image is None: raise SystemExit(f"无法解码图片(可能格式不支持或文件损坏):{path}") return image

============================================================

8. 主程序

============================================================

def main():
image_paths = list_image_files(IMAGE_FOLDER)

text
print("\n" + "=" * 62) print(f"图片文件夹:{IMAGE_FOLDER}") print(f"共找到 {len(image_paths)} 张图片") print("第一张图片将作为背景/基底参考帧") print("=" * 62) first_frame = read_image(image_paths[0]) height, width = first_frame.shape[:2] if EXPECTED_VIDEO_WIDTH is not None and width != EXPECTED_VIDEO_WIDTH: print(f"警告:填写的宽度为 {EXPECTED_VIDEO_WIDTH},图片实际宽度为 {width}。") if EXPECTED_VIDEO_HEIGHT is not None and height != EXPECTED_VIDEO_HEIGHT: print(f"警告:填写的高度为 {EXPECTED_VIDEO_HEIGHT},图片实际高度为 {height}。") scale_x, scale_y = configure_for_resolution(width, height) # 提前检查标定值 length_per_pixel() # 1) 点击基板上表面 clicked_surface_y = select_surface(first_frame) selected_surface_y = refine_surface_y(first_frame, clicked_surface_y) background_gray = cv2.cvtColor(first_frame, cv2.COLOR_BGR2GRAY) # ---------- 预处理:抹掉 00 里基板上方的干扰液滴 ---------- # 1) 检测 00 里基板上方的暗区(= 干扰液滴) bg_dark = cv2.threshold( background_gray, DARK_THRESHOLD, 255, cv2.THRESH_BINARY_INV )[1] # 只在基板上方处理,基板及其下方不动 y_bottom = max(1, selected_surface_y - SURFACE_EXCLUDE) above = np.zeros_like(bg_dark) above[:y_bottom, :] = bg_dark[:y_bottom, :] # 2) 轻微膨胀,把液滴边缘也覆盖上 above = cv2.dilate( above, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)), iterations=1, ) # 3) 把这块区域用 inpaint 填成周围亮背景 # inpaint 需要 BGR 或单通道 8U,这里直接用灰度 background_gray = cv2.inpaint( background_gray, above, 5, cv2.INPAINT_TELEA ) # ------------------------------------------------------- background_gray = cv2.GaussianBlur(background_gray, BLUR_KERNEL, 0) print("\n" + "=" * 62) print(f"分辨率:{width} x {height}") print(f"自动缩放比例:x = {scale_x:.3f}, y = {scale_y:.3f}") print( f"自动参数:接触带 = {CONTACT_BAND_HEIGHT} px, " f"测量高度 = {MEASURE_HEIGHT} px, " f"撞击容差 = {IMPACT_TOLERANCE} px, " f"最小追踪面积 = {MIN_TRACK_AREA} px^2" ) if length_per_pixel() is None: print("长度标定:未设置,仅输出 px。") else: print( f"长度标定:1 px = {length_per_pixel():.8f} {LENGTH_UNIT}," f"由 {CALIBRATION_PIXEL_DISTANCE} px = " f"{CALIBRATION_REAL_DISTANCE} {LENGTH_UNIT} 得到。" ) print(f"基板上表面:y = {selected_surface_y}") print("=" * 62) # 2) 如果存在 01,则在 01 上点击液滴中心 impact_center_x = None if len(image_paths) >= 2: frame_01 = read_image(image_paths[1]) impact_center_x = select_drop_center(frame_01) print(f"液滴中心 x = {impact_center_x}") else: impact_center_x = width // 2 print(f"没有 01 图片,自动使用画面中心 x = {impact_center_x}") print("\n" + "=" * 62) print("开始逐张识别铺展直径") print("每张图片显示后,按任意键继续下一张;Q/ESC 提前结束") print("=" * 62) state = "IMPACTED" previous_diameter = None max_result = None max_frame_number = None max_frame_image = None max_mask = None impact_frame = None records = [] cv2.namedWindow("Droplet maximum spreading diameter", cv2.WINDOW_NORMAL) for idx, image_path in enumerate(image_paths): frame = read_image(image_path) frame_number = idx file_name = os.path.basename(image_path) original_display = frame.copy() mask = np.zeros((height, width), dtype=np.uint8) measured_mask = mask current_result = None rejection_reason = "" # 第 0 张只作为背景/基底参考,不参与测量 if idx == 0: records.append( { "file": file_name, "frame": frame_number, "state": "BACKGROUND", "diameter_px": None, "x_left": None, "x_right": None, "valid": False, "reject_reason": "background/reference frame", "is_max": False, } ) continue if impact_frame is None: impact_frame = frame_number # 从 01 开始,直接做近壁测量 mask = build_near_wall_mask(frame, background_gray, selected_surface_y) current_result, rejection_reason = measure_near_wall( mask, impact_center_x, selected_surface_y, previous_diameter, ) if current_result is not None: measured_mask = np.zeros_like(mask) measured_mask[ :, current_result.x_left:current_result.x_right + 1, ] = mask[ :, current_result.x_left:current_result.x_right + 1, ] previous_diameter = current_result.diameter if max_result is None or current_result.diameter > max_result.diameter: max_result = current_result max_frame_number = frame_number max_frame_image = frame.copy() max_mask = measured_mask.copy() cv2.line( original_display, (0, selected_surface_y), (width - 1, selected_surface_y), (255, 0, 0), 1, ) if impact_center_x is not None: cv2.line( original_display, (impact_center_x, max(0, selected_surface_y - MEASURE_HEIGHT - 10)), (impact_center_x, selected_surface_y), (255, 0, 255), 1, ) if current_result is not None: x_left = current_result.x_left x_right = current_result.x_right y_line = max(18, selected_surface_y - MEASURE_HEIGHT - 8) cv2.line(original_display, (x_left, y_line), (x_right, y_line), (0, 0, 255), 2) cv2.line( original_display, (x_left, y_line - 7), (x_left, selected_surface_y), (0, 0, 255), 2, ) cv2.line( original_display, (x_right, y_line - 7), (x_right, selected_surface_y), (0, 0, 255), 2, ) current_diameter = None if current_result is None else current_result.diameter max_diameter = None if max_result is None else max_result.diameter put_status( original_display, frame_number, state, current_diameter, max_diameter, max_frame_number, ) mask_display = cv2.cvtColor(measured_mask, cv2.COLOR_GRAY2BGR) y_top = max(0, selected_surface_y - MEASURE_HEIGHT) y_bottom = max(y_top + 1, selected_surface_y - SURFACE_EXCLUDE) cv2.line(mask_display, (0, y_top), (width - 1, y_top), (255, 0, 0), 1) cv2.line(mask_display, (0, y_bottom), (width - 1, y_bottom), (255, 0, 0), 1) cv2.putText( mask_display, "Near-wall moving droplet", (20, 34), cv2.FONT_HERSHEY_SIMPLEX, 0.72, (0, 255, 0), 2, cv2.LINE_AA, ) if state == "IMPACTED" and current_result is None and rejection_reason: cv2.putText( mask_display, "Rejected:" + rejection_reason, (20, 68), cv2.FONT_HERSHEY_SIMPLEX, 0.52, (0, 0, 255), 2, cv2.LINE_AA, ) if current_result is not None: cv2.rectangle( mask_display, (current_result.x_left, y_top), (current_result.x_right, y_bottom), (0, 255, 0), 2, ) combined = resize_for_display(np.hstack((original_display, mask_display))) cv2.imshow("Droplet maximum spreading diameter", combined) # 每张图片暂停,等待用户按键后再继续下一张 print(f"已显示 {file_name},按任意键继续下一张(Q/ESC 提前结束)") while True: key = cv2.waitKey(30) & 0xFF if key == 255: continue break if key in (ord("q"), ord("Q"), 27): print("用户提前结束。") break records.append( { "file": file_name, "frame": frame_number, "state": state, "diameter_px": None if current_result is None else current_result.diameter, "x_left": None if current_result is None else current_result.x_left, "x_right": None if current_result is None else current_result.x_right, "valid": current_result is not None, "reject_reason": rejection_reason if current_result is None else "", "is_max": ( current_result is not None and max_result is not None and current_result.diameter == max_result.diameter and frame_number == max_frame_number ), } ) cv2.destroyAllWindows() print("\n" + "=" * 62) print("最大铺展直径测量结果") print("=" * 62) print(f"Impact frame = {impact_frame if impact_frame is not None else '--'}") if max_result is None: print("Dmax = --(没有得到有效近壁连续区域)") else: print(f"Dmax = {format_diameter(max_result.diameter)}") print(f"Frame = {max_frame_number}") print(f"Range = [{max_result.x_left}, {max_result.x_right}]") print("=" * 62) # 导出 Excel df = pd.DataFrame(records) # 增加可读列 if length_per_pixel() is not None: df["diameter_real"] = df["diameter_px"].apply( lambda x: None if pd.isna(x) else x * length_per_pixel() ) df["unit"] = LENGTH_UNIT else: df["diameter_real"] = None df["unit"] = "px" try: df.to_excel(OUTPUT_EXCEL, index=False) print(f"结果已导出:{OUTPUT_EXCEL}") except Exception as e: print(f"导出 Excel 失败:{e}") csv_path = os.path.splitext(OUTPUT_EXCEL)[0] + ".csv" df.to_csv(csv_path, index=False, encoding="utf-8-sig") print(f"已改存为 CSV:{csv_path}") # 显示最大铺展帧 if max_frame_image is not None: result = max_frame_image.copy() y_line = max(18, selected_surface_y - MEASURE_HEIGHT - 8) cv2.line(result, (0, selected_surface_y), (width - 1, selected_surface_y), (255, 0, 0), 1) cv2.line(result, (max_result.x_left, y_line), (max_result.x_right, y_line), (0, 0, 255), 3) cv2.line( result, (max_result.x_left, y_line - 8), (max_result.x_left, selected_surface_y), (0, 0, 255), 2, ) cv2.line( result, (max_result.x_right, y_line - 8), (max_result.x_right, selected_surface_y), (0, 0, 255), 2, ) cv2.putText( result, f"Dmax = {format_diameter(max_result.diameter)}", (20, 38), cv2.FONT_HERSHEY_SIMPLEX, 0.85, (0, 0, 255), 2, cv2.LINE_AA, ) cv2.putText( result, f"Frame = {max_frame_number}", (20, 76), cv2.FONT_HERSHEY_SIMPLEX, 0.82, (0, 170, 0), 2, cv2.LINE_AA, ) max_mask_display = cv2.cvtColor(max_mask, cv2.COLOR_GRAY2BGR) final_display = resize_for_display(np.hstack((result, max_mask_display))) cv2.namedWindow("Maximum spreading diameter", cv2.WINDOW_NORMAL) cv2.imshow("Maximum spreading diameter", final_display) print("最大铺展帧已显示,按任意键关闭。") cv2.waitKey(0) cv2.destroyAllWindows()

if name == "main":
main()这个代码把图中主液滴旁边未连接的破碎液滴也算进去了,怎么帮我改一下代码,别把破碎液滴识别进去,其他的参数和内容保持不变

विचार

Filtering connected droplets

I’m focusing the mask on the component connected to the seed point, then applying morphological closing before selecting it. This preserves the main droplet’s irregular shape while excluding nearby disconnected fragments, without changing the remaining parameters.

Inspecting segmentation issues

I’m checking whether unwanted droplets are truly disconnected or joined by thin mask bridges. If they’re connected artificially, I’ll separate them with targeted morphological cleanup while keeping the existing thresholds, measurements, and other settings unchanged.

उत्तर

你这段代码的问题可以定位到:主你这段代码的问题可以定位到: 主液滴与旁边的破碎液滴在二值化处理后,可能被错误地合并成了同一个连通区域**,导致测液滴与旁边的破碎液滴在二值化处理后,可能被错误地合并成了同一个连通区域**,导致测量出来的铺展直径偏大。

虽然量出来的铺展直径偏大。

虽然你已经使用了 cv2.connectedComponentsWithStats(),但前你已经使用了 cv2.connectedComponentsWithStats(),但前面的膨胀操作面的膨胀操作可能让原本分离可能让原本分离的液滴在掩膜中产生的液滴在掩膜中产生细小连接。

我建议只修改液细小连接。

我建议只修改液滴识别部分,采用以下原则:

  1. **以滴识别部分,采用以下原则:

  2. 以你在第你在第 01 帧手动点击的液滴中心为基 01 帧手动点击的液滴中心为基准,优先选择主液滴。 准**,优先选择主液滴。

  3. 排除主液滴附近独2. 排除主液滴附近独立的破碎液滴。

  4. 对二立的破碎液滴。

  5. 对二值图中因细值图中因细小连接而粘在小连接而粘在一起的液滴进行分离。

  6. 保留原来的参数、一起的液滴进行分离。

  7. 保留原来的参数、标定换算、基板选择标定换算、基板选择、逐帧显示、最大铺、逐帧显示、最大铺展直径计算和 Excel 导出逻展直径计算和 Excel 导出逻辑。

这样可以尽辑。

这样可以尽量减少对你量减少对你现有程序的影响。

现有程序的影响。

विचार

Separating false bridges

I’m refining the cleanup so a nearby fragment joined by a thin vertical bridge gets removed before component selection. I’ll preserve the main droplet’s original mask and avoid altering unrelated thresholds or measurement settings.

Finalizing mask cleanup

I’m choosing an adaptive separation step: identify the main component after removing only thin bridges, then measure using the original pixels from that component. This excludes nearby fragments while preserving the droplet’s genuine outline.

Refining component selection

I’m narrowing recognition to the seed-connected main droplet and removing thin artificial bridges created during preprocessing. The displayed mask will then exclude detached fragments while preserving the existing measurements and thresholds.

Isolating the main droplet

I’m selecting the seed-centered component after removing only small, peripheral regions joined by thin bridges. The original mask remains unchanged unless this cleanup identifies a clearly detached fragment.

Finalizing the filter

I’m keeping existing thresholds and measurements intact, while adding adaptive bridge removal only when a small peripheral component separates from the seed-centered droplet. Otherwise, the original mask is retained.

Finalized image filtering to isolate the main droplet

विचार

Applying adaptive separation

I’m using the existing component selection, then testing progressively stronger vertical openings. Only small peripheral fragments separated by thin bridges are excluded; all original thresholds, measurements, and other settings remain unchanged.

Providing the replacement

I’m keeping the modification limited to measure_near_wall. Replace that function with the adaptive component-filtering version; all other code, parameters, and measurements stay unchanged.

Applied adaptive separation and provided the replacement

विचार

完成自适应分离

我会把修改限制在 measure_near_wall:先保留中心主液滴,再逐步去除由细桥连接的小型外围碎滴;没有检测到碎滴时,继续使用原始掩膜和参数。

Completed adaptive separation

Implemented adaptive morphology and boundary corrections for image segmentation

Worked for 12m 2s

उत्तर

具体修改方法

只需要在原## 具体修改方法

只需要在原来第 5 部分的 measure_ne来第 5 部分的 measure_near_wall() 函数中,找到下面ar_wall() 函数中,找到下面这段:

python
x_left = int(cols.min()) x这段: _right = int(cols.max()) diameter = x_right - x_left

将它替将它替换成下面这段代码。**其他换成下面这段代码。其他代码和原有参数全部不变。

text
```python x_left = int(cols.min()) x_right = intpython x_left = int(cols.min()) x_right = int(cols.max()) # ================================================== # 新增:排(cols.max()) # ================================================== # 新增:排除主液滴除主液滴两侧误连接的破碎液滴 # ================================================== # 两侧误连接的破碎液滴 # ================================================== # 只对已经选中的主液滴连只对已经选中的主液滴连通域进行处理 main_binary = main_component.astype(np.uint8) * 255 last_kernel_height = 0 left_trimmed通域进行处理 main_binary = main_component.astype(np.uint8) * 255 last_kernel_height = 0 left_trimmed = False right_trimmed = False # 逐步增 = False right_trimmed = False # 逐步增大纵向开运算核,尝大纵向开运算核,尝试断开细小连接 for base试断开细小连接 for base_height in range(3, 18, 2): kernel_height =_height in range(3, 18, 2): kernel_height = scaled_odd( base_height, height / REFERENCE_HEIGHT, scaled_odd( base_height, height / REFERENCE_HEIGHT, minimum=3, ) if kernel_height == last minimum=3, ) if kernel_height == last_kernel_height: continue last_kernel_kernel_height: continue last_kernel_height = kernel_height opened = cv2.morphologyEx( _height = kernel_height opened = cv2.morphologyEx( main_binary, cv2.MORPH_OPEN, cv2.getStruct main_binary, cv2.MORPH_OPEN, cv2.getStructuringElement( cv2.MORPH_RECT, (1, kerneluringElement( cv2.MORPH_RECT, (1, kernel_height), ), ) n_height), ), ) n_open, _, open_stats, _ = ( cv2.connectedComponentsWithStats_open, _, open_stats, _ = ( cv2.connectedComponentsWithStats( opened, connectivity=4, ) ) if n( opened, connectivity=4, ) ) if n_open <= 2: continue # 根据原_open <= 2: continue # 根据原来点击的液滴中心重新选来点击的液滴中心重新选取主液滴 candidates = [] 取主液滴 candidates = [] for label in range(1, n_open): ox, oy, for label in range(1, n_open): ox, oy, ow, oh, oarea = open_stats[label] if oarea < ow, oh, oarea = open_stats[label] if oarea < MIN_DIAMETER: continue o_right = ox + ow - MIN_DIAMETER: continue o_right = ox + ow - 1 if ox <= impact_center_x <= o_right: distance1 if ox <= impact_center_x <= o_right: distance = 0 else: distance = min( abs(impact = 0 else: distance = min( abs(impact_center_x - ox), abs(impact_center_x - o_right), ) _center_x - ox), abs(impact_center_x - o_right), ) if distance <= MAX_CENTER_DISTANCE: candidates.append( (-int if distance <= MAX_CENTER_DISTANCE: candidates.append( (-int(oarea), distance, label) ) if not candidates: continue (oarea), distance, label) ) if not candidates: continue # 主 # 主液滴区域 main_label = min(candidates液滴区域 main_label = min(candidates)[2] core_x, core_y, core_w, core_h, core_area = ( map(int)[2] core_x, core_y, core_w, core_h, core_area = ( map(int, open_stats[main_label]) ) , open_stats[main_label]) ) core_left = core_x core_right = core_x + core_w - core_left = core_x core_right = core_x + core_w - 1 satellite_on_left = False satellite_on_right = False 1 satellite_on_left = False satellite_on_right = False # 判断分离出来 # 判断分离出来的区域是否为外围破碎液滴 for label in range(1,的区域是否为外围破碎液滴 for label in range(1, n_open): if label == main_label: continue sx, sy, sw, sh n_open): if label == main_label: continue sx, sy, sw, sh, sarea = ( map(int, open_stats[label]) ) , sarea = ( map(int, open_stats[label]) ) # 小面积区域才作为破碎液 # 小面积区域才作为破碎液滴候选 if ( sarea < MIN_DIAMETER滴候选 if ( sarea < MIN_DIAMETER or sarea > 0.20 * core_area ): or sarea > 0.20 * core_area ): continue # 左侧破碎液滴 if sx + continue # 左侧破碎液滴 if sx + sw - 1 < core_left: satellite_on_left = True # sw - 1 < core_left: satellite_on_left = True # 右侧破碎液滴 elif sx > core_right: 右侧破碎液滴 elif sx > core_right: satellite_on_right = True # 只修正 satellite_on_right = True # 只修正存在破存在破碎液滴的一侧 if satellite_on_left and not left_trimmed碎液滴的一侧 if satellite_on_left and not left_trimmed: x_left = max(x_left, core_left) left_trimmed: x_left = max(x_left, core_left) left_trimmed = True if satellite_on_right and not right_trimmed: x_right = = True if satellite_on_right and not right_trimmed: x_right = min(x_right, core_right) right_trimmed = True min(x_right, core_right) right_trimmed = True if left_trimmed and right_trimmed: break # ================================================== if left_trimmed and right_trimmed: break # ================================================== # 新增结束:以下 # 新增结束:以下继续执行继续执行原有的直径计算 # ================================================== 原有的直径计算 # ================================================== diameter = x_right - x_left

修改 diameter = x_right - x_left

text
## 修改后的效果 <box gap={2}> <row后的效果 <box gap={2}> <row align="center" gap={2}> align="center" gap={2}> <icon name="check-circle" color="success" size="lg"/> **主 <icon name="check-circle" color="success" size="lg"/> **主液滴识别方式液滴识别方式不变:** 仍然使用第不变:** 仍然使用第 01 帧手动选择 01 帧手动选择的中心定位主的中心定位主液滴。 </row> <row align="center" gap={液滴。 </row> <row align="center" gap={2}> <icon name="check-circle" color="success" size="2}> <icon name="check-circle" color="success" size="lg"/> **排除旁边碎滴:** 对因lg"/> **排除旁边碎滴:** 对因细小连接被误细小连接被误合并的外围小合并的外围小液滴进行分离。 </row> <row align="center液滴进行分离。 </row> <row align="center" gap={2}> <icon name="check-circle" color="success" gap={2}> <icon name="check-circle" color="success" size="lg"/> **避免无故缩" size="lg"/> **避免无故缩小直径:** 没有小直径:** 没有检测到外围碎滴时,继续使用原来的检测到外围碎滴时,继续使用原来的测量边界。 </row> 测量边界。 </row> <row align="center" gap={2}> <icon name="check <row align="center" gap={2}> <icon name="check-circle" color="success" size="lg"/> **其他功能不-circle" color="success" size="lg"/> **其他功能不变:** 包括原来的长度标定、D变:** 包括原来的长度标定、Dmax 追踪、红色测量框和 Excel 导出。 max 追踪、红色测量框和 Excel 导出。 </row> </box> 我也将 </row> </box> 我也将完整的替换函数整理成了 Python完整的替换函数整理成了 Python 文件,方便直接复制,不需要 文件,方便直接复制,不需要自己拼自己拼接代码。 <row align="center" gap={2}> 接代码。 <row align="center" gap={2}> <icon name="file-code" color="secondary" size="lg <icon name="file-code" color="secondary" size="lg"/> [下载修改后的 measure"/> [下载修改后的 measure_near_wall 函数] </row> 这个文件包含_fixed.py) </row> 这个文件包含完整的替换函数,不是独立运行的完整的替换函数,不是独立运行的主程序。 我用你提供的截图中的白主程序。 我用你提供的截图中的白色掩膜及模拟测试图验证过色掩膜及模拟测试图验证过修改:截图中左侧的小液滴可以修改:截图中左侧的小液滴可以从主液滴的测量范围中排除,单从主液滴的测量范围中排除,单独主液滴的宽独主液滴的宽度仍然得到保留。<CodeC度仍然得到保留。<CodeCite tool="container" citation_id="5c80AA" line_range_start={ite tool="container" citation_id="5c80AA" line_range_start={1} line_range_end={4} /> **需要注意1} line_range_end={4} /> **需要注意:** 这里使用的是针对细颈:** 这里使用的是针对细颈误连接的处理误连接的处理方法,并不能保证所有复杂破碎形方法,并不能保证所有复杂破碎形态都能正确分开。如果碎滴与主液滴在态都能正确分开。如果碎滴与主液滴在二值图中形成很宽的连接二值图中形成很宽的连接,仍然可能需要进一步处理。最终的,仍然可能需要进一步处理。最终的铺展直径和 Dmax 应以原铺展直径和 Dmax 应以原始图片序列重新运行的始图片序列重新运行的结果为准。结果为准。

इस प्रश्नोत्तर को साझा करें