数字图像相关技术:材料应变测量的开源软件实践

# 数字图像相关技术:材料应变测量的开源软件实践


数字图像相关技术作为现代实验力学的重要工具,通过分析材料表面的图像变化来测量全场应变。这项非接触式测量方法广泛应用于材料测试、结构分析和生物力学研究。开源DIC软件的发展,使得这项专业技术对更多研究人员和工程师开放。


## 数字图像相关技术基础原理


DIC技术通过跟踪材料表面随机或人为添加的散斑图案,计算变形过程中的位移场和应变场:


```python

# 基础DIC计算框架

import numpy as np

import matplotlib.pyplot as plt

from scipy import ndimage

from scipy.optimize import minimize


class BasicDIC:

    def __init__(self, subset_size=21, grid_spacing=5):

        """

        初始化DIC分析器

        :param subset_size: 子集大小(像素)

        :param grid_spacing: 网格间距(像素)

        """

        self.subset_size = subset_size

        self.grid_spacing = grid_spacing

        self.half_subset = subset_size // 2

        

    def create_analysis_grid(self, image_shape):

        """创建分析网格"""

        height, width = image_shape

        grid_x = np.arange(self.half_subset, width - self.half_subset, self.grid_spacing)

        grid_y = np.arange(self.half_subset, height - self.half_subset, self.grid_spacing)

        

        # 生成网格点

        xx, yy = np.meshgrid(grid_x, grid_y)

        grid_points = np.vstack([xx.flatten(), yy.flatten()]).T

        

        return grid_points

    

    def extract_subset(self, image, center_x, center_y):

        """提取分析子集"""

        # 计算子集边界

        y_start = int(center_y - self.half_subset)

        y_end = int(center_y + self.half_subset + 1)

        x_start = int(center_x - self.half_subset)

        x_end = int(center_x + self.half_subset + 1)

        

        # 提取子集区域

        subset = image[y_start:y_end, x_start:x_end]

        

        return subset

```


## 开源DIC软件Nocturn实战


Nocturn作为开源DIC软件的代表,提供了完整的应变分析工作流:


```python

# Nocturn DIC分析流程示例

import noclib as noc

import cv2


class NocturnAnalysis:

    def __init__(self):

        self.detector = noc.FeatureDetector()

        self.tracker = noc.ParticleTracker()

        self.strain_calculator = noc.StrainCalculator()

    

    def analyze_tensile_test(self, image_sequence):

        """分析拉伸测试图像序列"""

        

        # 1. 图像预处理

        processed_images = self.preprocess_images(image_sequence)

        

        # 2. 特征点检测

        reference_image = processed_images[0]

        features = self.detector.detect_features(

            reference_image, 

            method='FAST',

            threshold=20

        )

        

        # 3. 特征点跟踪

        print("开始特征点跟踪...")

        displacements = []

        

        for i in range(1, len(processed_images)):

            current_image = processed_images[i]

            

            # 使用亚像素精度跟踪

            tracked_features = self.tracker.track_features(

                reference_image,

                current_image,

                features,

                method='Lucas-Kanade',

                window_size=(21, 21)

            )

            

            # 计算位移向量

            displacement = tracked_features - features

            displacements.append(displacement)

            

            print(f"图像 {i}: 跟踪到 {len(tracked_features)} 个特征点")

        

        # 4. 应变计算

        strains = self.strain_calculator.compute_green_lagrange_strain(

            displacements,

            grid_spacing=10,

            smoothing_kernel=5

        )

        

        return {

            'features': features,

            'displacements': displacements,

            'strains': strains

        }

    

    def preprocess_images(self, images):

        """图像预处理"""

        processed = []

        

        for img in images:

            # 转换为灰度图

            if len(img.shape) == 3:

                gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

            else:

                gray = img.copy()

            

            # 直方图均衡化增强对比度

            enhanced = cv2.equalizeHist(gray)

            

            # 高斯滤波降噪

            filtered = cv2.GaussianBlur(enhanced, (5, 5), 1.0)

            

            processed.append(filtered)

        

        return processed

```


## 散斑图案生成与优化


有效的散斑图案对DIC精度至关重要:


```python

class SpecklePatternGenerator:

    def __init__(self, pattern_size=(1024, 1024)):

        self.size = pattern_size

        

    def generate_random_speckle(self, speckle_size_range=(3, 15), 

                                density=0.4):

        """

        生成随机散斑图案

        :param speckle_size_range: 散斑大小范围(像素)

        :param density: 散斑密度

        """

        pattern = np.zeros(self.size, dtype=np.uint8)

        height, width = self.size

        

        # 计算散斑数量

        total_pixels = height * width

        speckle_pixels = int(total_pixels * density)

        

        # 生成随机散斑

        min_size, max_size = speckle_size_range

        

        placed_pixels = 0

        while placed_pixels < speckle_pixels:

            # 随机位置

            x = np.random.randint(0, width)

            y = np.random.randint(0, height)

            

            # 随机大小

            size = np.random.randint(min_size, max_size)

            

            # 绘制圆形散斑

            radius = size // 2

            cv2.circle(pattern, (x, y), radius, 255, -1)

            

            placed_pixels += np.pi * radius ** 2

        

        # 应用高斯模糊模拟真实散斑

        pattern_blurred = cv2.GaussianBlur(pattern, (5, 5), 1.5)

        

        return pattern_blurred

    

    def calculate_pattern_quality(self, pattern):

        """计算散斑图案质量指标"""

        

        # 计算对比度

        mean_intensity = np.mean(pattern)

        std_intensity = np.std(pattern)

        contrast = std_intensity / mean_intensity if mean_intensity > 0 else 0

        

        # 计算自相关函数峰值

        correlation = np.correlate(pattern.flatten(), pattern.flatten(), mode='same')

        peak_ratio = np.max(correlation) / np.mean(correlation)

        

        # 计算信息熵

        histogram = np.histogram(pattern, bins=256, range=(0, 255))[0]

        histogram = histogram / histogram.sum()

        entropy = -np.sum(histogram * np.log2(histogram + 1e-10))

        

        return {

            'contrast': contrast,

            'peak_ratio': peak_ratio,

            'entropy': entropy,

            'overall_quality': contrast * entropy

        }

```


## 亚像素位移计算算法


提高DIC精度需要亚像素级的位移计算:


```python

class SubpixelRegistration:

    def __init__(self, interpolation_order=3):

        self.interp_order = interpolation_order

        

    def parabolic_subpixel_fit(self, correlation_surface):

        """

        抛物线拟合亚像素定位

        :param correlation_surface: 相关曲面

        """

        # 找到整数峰值位置

        peak_y, peak_x = np.unravel_index(

            np.argmax(correlation_surface), 

            correlation_surface.shape

        )

        

        # 检查边界

        if (peak_x <= 0 or peak_x >= correlation_surface.shape[1] - 1 or

            peak_y <= 0 or peak_y >= correlation_surface.shape[0] - 1):

            return float(peak_x), float(peak_y)

        

        # X方向抛物线拟合

        x_values = correlation_surface[peak_y, peak_x-1:peak_x+2]

        x_coeff = self._parabola_coefficients(x_values)

        subpixel_x = peak_x - x_coeff[1] / (2 * x_coeff[0]) if x_coeff[0] != 0 else peak_x

        

        # Y方向抛物线拟合

        y_values = correlation_surface[peak_y-1:peak_y+2, peak_x]

        y_coeff = self._parabola_coefficients(y_values)

        subpixel_y = peak_y - y_coeff[1] / (2 * y_coeff[0]) if y_coeff[0] != 0 else peak_y

        

        return subpixel_x, subpixel_y

    

    def _parabola_coefficients(self, values):

        """计算抛物线系数"""

        x = np.array([-1, 0, 1])

        y = values

        

        # 拟合抛物线 y = ax² + bx + c

        A = np.vstack([x**2, x, np.ones(3)]).T

        coeffs = np.linalg.lstsq(A, y, rcond=None)[0]

        

        return coeffs

    

    def gradient_based_method(self, ref_subset, cur_subset):

        """基于梯度的方法进行亚像素配准"""

        

        # 计算梯度

        grad_x = cv2.Sobel(ref_subset, cv2.CV_64F, 1, 0, ksize=3)

        grad_y = cv2.Sobel(ref_subset, cv2.CV_64F, 0, 1, ksize=3)

        

        # 计算强度差

        intensity_diff = cur_subset - ref_subset

        

        # 构建线性系统

        height, width = ref_subset.shape

        n_pixels = height * width

        

        # 重排为向量

        grad_x_flat = grad_x.flatten()

        grad_y_flat = grad_y.flatten()

        diff_flat = intensity_diff.flatten()

        

        # 构建雅可比矩阵

        J = np.zeros((n_pixels, 2))

        J[:, 0] = grad_x_flat

        J[:, 1] = grad_y_flat

        

        # 计算位移增量

        try:

            displacement = np.linalg.lstsq(J, diff_flat, rcond=None)[0]

        except np.linalg.LinAlgError:

            displacement = np.zeros(2)

        

        return displacement

```


## 应变计算与场平滑


从位移场计算应变需要合适的微分和滤波方法:


```python

class StrainComputation:

    def __init__(self, strain_type='engineering'):

        self.strain_type = strain_type

        

    def compute_strain_field(self, displacement_field, grid_spacing=1.0):

        """

        计算应变场

        :param displacement_field: 位移场,形状为 (height, width, 2)

        :param grid_spacing: 网格间距(像素/物理单位)

        """

        u = displacement_field[..., 0]  # x方向位移

        v = displacement_field[..., 1]  # y方向位移

        

        # 计算位移梯度

        grad_ux, grad_uy = np.gradient(u, grid_spacing, edge_order=2)

        grad_vx, grad_vy = np.gradient(v, grid_spacing, edge_order=2)

     <"8s.yunruiwater.cn"><"2f.sxyicheng.cn"><"5p.jsnjz.cn">

        

        if self.strain_type == 'engineering':

            # 工程应变

            exx = grad_ux  # ε_xx

            eyy = grad_vy  # ε_yy

            exy = 0.5 * (grad_uy + grad_vx)  # γ_xy / 2

        

        elif self.strain_type == 'green_lagrange':

            # Green-Lagrange应变

            exx = grad_ux + 0.5 * (grad_ux**2 + grad_vx**2)

            eyy = grad_vy + 0.5 * (grad_uy**2 + grad_vy**2)

            exy = 0.5 * (grad_uy + grad_vx) + 0.5 * (grad_ux*grad_uy + grad_vx*grad_vy)

        

        # 计算主应变

        principal_strains = self.compute_principal_strains(exx, eyy, exy)

        

        return {

            'strain_xx': exx,

            'strain_yy': eyy,

            'strain_xy': exy,

            'principal_strains': principal_strains

        }

    

    def compute_principal_strains(self, exx, eyy, exy):

        """计算主应变和主方向"""

        # 平均应变

        mean_strain = (exx + eyy) / 2

        

        # 应变偏差

        dev_strain = np.sqrt(((exx - eyy) / 2)**2 + exy**2)

        

        # 主应变

        epsilon1 = mean_strain + dev_strain

        epsilon2 = mean_strain - dev_strain

        

        # 主方向(与x轴夹角)

        theta = 0.5 * np.arctan2(2 * exy, exx - eyy)

        

        return {

            'epsilon1': epsilon1,

            'epsilon2': epsilon2,

            'theta': theta,

            'max_shear': epsilon1 - epsilon2

        }

    

    def apply_strain_smoothing(self, strain_field, method='savgol', 

                               window_length=11, polyorder=3):

        """应用应变场平滑"""

        smoothed = {}

        

        for key, field in strain_field.items():

            if key == 'principal_strains':

                # 递归处理子字段

                smoothed[key] = self.apply_strain_smoothing(field, method, 

                                                           window_length, polyorder)

                continue

            

            if method == 'savgol':

                from scipy.signal import savgol_filter

                # 二维Savitzky-Golay滤波

                smoothed_2d = savgol_filter(

                    savgol_filter(field, window_length, polyorder, axis=0),

                    window_length, polyorder, axis=1

                )

                smoothed[key] = smoothed_2d

            

            elif method == 'gaussian':

                sigma = window_length / 6  # 将窗口长度转换为sigma

                smoothed[key] = ndimage.gaussian_filter(field, sigma=sigma)

        

        return smoothed

```


## 完整DIC分析工作流


整合各个模块的完整分析流程:


```python

class CompleteDICWorkflow:

    def __init__(self, config):

        self.config = config

        self.speckle_generator = SpecklePatternGenerator()

        self.dic_analyzer = NocturnAnalysis()

        self.strain_computer = StrainComputation()

        self.results = {}

    

    def run_analysis(self, reference_image, deformed_images, 

                     physical_scale=1.0):

        """运行完整DIC分析工作流"""

        

        print("开始DIC分析工作流...")

        

        # 1. 图像预处理

        print("步骤1: 图像预处理")

        processed_images = [reference_image] + deformed_images

        processed_images = self.dic_analyzer.preprocess_images(processed_images)

        

        # 2. DIC分析

        print("步骤2: 数字图像相关分析")

        dic_results = self.dic_analyzer.analyze_tensile_test(processed_images)

        

        # 3. 应变计算

        print("步骤3: 应变计算")

        strain_results = []

        

        for i, displacement in enumerate(dic_results['displacements']):

            # 重构位移场

            displacement_field = self.reconstruct_displacement_field(

                dic_results['features'],

                displacement,

                reference_image.shape

            )

            

            # 计算应变

            strain_field = self.strain_computer.compute_strain_field(

                displacement_field,

                grid_spacing=physical_scale

            )

            

            # 应用平滑

            if self.config.get('apply_smoothing', True):

                strain_field = self.strain_computer.apply_strain_smoothing(

                    strain_field,

                    method=self.config.get('smoothing_method', 'savgol'),

                    window_length=self.config.get('smoothing_window', 11)

                )

            

            strain_results.append(strain_field)

        

        # 4. 结果汇总

        self.results = {

            'displacements': dic_results['displacements'],

            'strains': strain_results,

            'features': dic_results['features'],

            'analysis_parameters': self.config

        }

        

        print("DIC分析完成")

        return self.results

    

    def reconstruct_displacement_field(self, features, displacements, image_shape):

        """从离散特征点重构连续位移场"""

        from scipy.interpolate import griddata

        

        height, width = image_shape

        

        # 创建网格

        grid_x, grid_y = np.meshgrid(

            np.arange(0, width, 1),

            np.arange(0, height, 1)

        )

        

        # 特征点位置

        points = features[:, :2]

        

        # 位移值

        values_u = displacements[:, 0]  # x方向位移

        values_v = displacements[:, 1]  # y方向位移

        

        # 插值得到全场位移

        field_u = griddata(points, values_u, (grid_x, grid_y), 

                          method='cubic', fill_value=0)

        field_v = griddata(points, values_v, (grid_x, grid_y),

                          method='cubic', fill_value=0)

        

        # 组合为位移场

        displacement_field = np.stack([field_u, field_v], axis=-1)

        

        return displacement_field

    

    def export_results(self, output_dir, format='h5'):

        """导出分析结果"""

        import h5py

        

        if format == 'h5':

            output_file = f"{output_dir}/dic_results.h5"

            

            with h5py.File(output_file, 'w') as f:

                # 保存位移数据

                for i, disp in enumerate(self.results['displacements']):

                    f.create_dataset(f'displacements/frame_{i}', data=disp)

                

                # 保存应变数据

                for i, strain in enumerate(self.results['strains']):

                    strain_group = f.create_group(f'strains/frame_{i}')

                    for key, value in strain.items():

                        if isinstance(value, dict):

                            subgroup = strain_group.create_group(key)

                            for subkey, subvalue in value.items():

                                subgroup.create_dataset(subkey, data=subvalue)

                        else:

                            strain_group.create_dataset(key, data=value)

                

                # 保存元数据

                f.attrs['analysis_date'] = np.string_(self.config.get('analysis_date', ''))

                f.attrs['physical_scale'] = self.config.get('physical_scale', 1.0)

        

        print(f"结果已导出到: {output_file}")

```


## 实践应用案例


以下是一个拉伸测试分析的完整示例:


```python

# 材料拉伸测试DIC分析

def analyze_tensile_test_specimen():

<"9q.csxthr.com"><"3k.zhaiLimao.com"><"6d.yunruiwater.cn">

    # 配置分析参数

    config = {

        'subset_size': 29,

        'grid_spacing': 7,

        'strain_type': 'green_lagrange',

        'apply_smoothing': True,

        'smoothing_method': 'savgol',

        'smoothing_window': 15,

        'physical_scale': 0.01  # 像素到毫米的转换系数

    }

    

    # 加载测试图像

    image_files = sorted(glob.glob('tensile_test/*.tif'))

    images = [cv2.imread(f, cv2.IMREAD_GRAYSCALE) for f in image_files]

    

    # 创建分析工作流

    workflow = CompleteDICWorkflow(config)

    

    # 运行分析

    reference_image = images[0]

    deformed_images = images[1:]

    

    results = workflow.run_analysis(

        reference_image, 

        deformed_images,

        physical_scale=config['physical_scale']

    )

    

    # 导出结果

    workflow.export_results('output_results')

    

    # 可视化结果

    visualize_results(results)

    

    return results


def visualize_results(results):

    """可视化分析结果"""

    fig, axes = plt.subplots(2, 2, figsize=(12, 10))

    

    # 显示最后一个时间步的应变

    last_strains = results['strains'][-1]

    

    # 应变分量可视化

    im1 = axes[0, 0].imshow(last_strains['strain_xx'], cmap='jet', 

                           vmin=-0.1, vmax=0.1)

    axes[0, 0].set_title('ε_xx (横向应变)')

    plt.colorbar(im1, ax=axes[0, 0])

    

    im2 = axes[0, 1].imshow(last_strains['strain_yy'], cmap='jet',

                           vmin=-0.1, vmax=0.1)

    axes[0, 1].set_title('ε_yy (纵向应变)')

    plt.colorbar(im2, ax=axes[0, 1])

    

    # 主应变可视化

    im3 = axes[1, 0].imshow(last_strains['principal_strains']['epsilon1'], 

                           cmap='jet')

    axes[1, 0].set_title('最大主应变 ε₁')

    plt.colorbar(im3, ax=axes[1, 0])

    

    # 剪切应变可视化

    im4 = axes[1, 1].imshow(last_strains['principal_strains']['max_shear'],

                           cmap='jet')

    axes[1, 1].set_title('最大剪切应变')

    plt.colorbar(im4, ax=axes[1, 1])

    

    plt.tight_layout()

    plt.savefig('output_results/strain_fields.png', dpi=300, bbox_inches='tight')

    plt.show()

```


## 误差分析与验证


为确保DIC测量结果的可靠性,需要进行误差分析:


```python

class DICErrorAnalysis:

    def __init__(self):

        self.error_metrics = {}

    

    def perform_virtual_test(self, synthetic_deformation, measured_deformation):

        """通过虚拟测试评估系统误差"""

        

        # 计算位移误差

        displacement_error = measured_deformation - synthetic_deformation

        rmse_displacement = np.sqrt(np.mean(displacement_error**2))

        

        # 计算应变误差

        synthetic_strain = self.compute_strain_from_displacement(synthetic_deformation)

        measured_strain = self.compute_strain_from_displacement(measured_deformation)

        strain_error = measured_strain - synthetic_strain

        rmse_strain = np.sqrt(np.mean(strain_error**2))

        

        # 计算相关系数

        correlation_coefficient = np.corrcoef(

            synthetic_deformation.flatten(),

            measured_deformation.flatten()

        )[0, 1]

        

        self.error_metrics = {

            'displacement_rmse': rmse_displacement,

            'strain_rmse': rmse_strain,

            'correlation': correlation_coefficient,

            'bias': np.mean(displacement_error),

            'precision': np.std(displacement_error)

        }

        

        return self.error_metrics

```


数字图像相关技术通过开源软件的实践变得更为可及。从散斑图案生成到亚像素位移计算,再到应变场分析,每个环节都需要细致的算法实现和参数优化。开源DIC软件不仅降低了技术门槛,还促进了方法的标准化和验证。随着计算能力的提升和算法的改进,DIC技术将继续在材料科学和工程应用中发挥重要作用,为研究人员提供可靠的应变测量工具。


请使用浏览器的分享功能分享到微信等