import os import re from dataclasses import datacl...
Criado em: 8 de outubro de 2026
Respondido usando GPT-6 Thinking por Chat01
Criado em: 8 de outubro de 2026
Respondido usando GPT-6 Thinking por Chat01
import os
import re
from dataclasses import dataclass
import cv2
import numpy as np
import pandas as pd
IMAGE_FOLDER = r"D:\Test1Photo\Calibration01\Cam31894-FromFile\9.10侧视铺展直径-160℃\光滑\侧视-光滑_20℃-2.5cm.1"
OUTPUT_EXCEL = os.path.join(IMAGE_FOLDER, "spreading_diameter_results.xlsx")
IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff")
EXPECTED_VIDEO_WIDTH = 640
EXPECTED_VIDEO_HEIGHT = 384
CALIBRATION_PIXEL_DISTANCE = 551
CALIBRATION_REAL_DISTANCE = 10
LENGTH_UNIT = "mm"
AUTO_REFINE_SURFACE = True
MIN_SURFACE_EDGE_DROP = 8.0
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
textscale_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
@dataclass
class MeasureResult:
diameter: int
x_left: int
x_right: int
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
textwindow_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
textgray = 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
textwindow_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
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)
textdifference = 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
textcount, 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"
def put_status(image, frame_number, state, diameter, max_diameter, max_frame):
status_color = (0, 170, 255) if state == "FALLING" else (0, 220, 0)
textcv2.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)
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}")
textfiles = [] 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}")
textif image is None: raise SystemExit(f"无法解码图片(可能格式不支持或文件损坏):{path}") return image
def main():
image_paths = list_image_files(IMAGE_FOLDER)
textprint("\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()这个代码把图中主液滴旁边未连接的破碎液滴也算进去了,怎么帮我改一下代码,别把破碎液滴识别进去,其他的参数和内容保持不变
你这段代码的问题可以定位到:主你这段代码的问题可以定位到: 主液滴与旁边的破碎液滴在二值化处理后,可能被错误地合并成了同一个连通区域**,导致测液滴与旁边的破碎液滴在二值化处理后,可能被错误地合并成了同一个连通区域**,导致测量出来的铺展直径偏大。
虽然量出来的铺展直径偏大。
虽然你已经使用了 cv2.connectedComponentsWithStats(),但前你已经使用了 cv2.connectedComponentsWithStats(),但前面的膨胀操作面的膨胀操作可能让原本分离可能让原本分离的液滴在掩膜中产生的液滴在掩膜中产生细小连接。
我建议只修改液细小连接。
我建议只修改液滴识别部分,采用以下原则:
**以滴识别部分,采用以下原则:
以你在第你在第 01 帧手动点击的液滴中心为基 01 帧手动点击的液滴中心为基准,优先选择主液滴。 准**,优先选择主液滴。
排除主液滴附近独2. 排除主液滴附近独立的破碎液滴。
对二立的破碎液滴。
对二值图中因细值图中因细小连接而粘在小连接而粘在一起的液滴进行分离。
保留原来的参数、一起的液滴进行分离。
保留原来的参数、标定换算、基板选择标定换算、基板选择、逐帧显示、最大铺、逐帧显示、最大铺展直径计算和 Excel 导出逻展直径计算和 Excel 导出逻辑。
这样可以尽辑。
这样可以尽量减少对你量减少对你现有程序的影响。
现有程序的影响。
只需要在原## 具体修改方法
只需要在原来第 5 部分的 measure_ne来第 5 部分的 measure_near_wall() 函数中,找到下面ar_wall() 函数中,找到下面这段:
pythonx_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
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 应以原始图片序列重新运行的始图片序列重新运行的结果为准。结果为准。