在 XRD实验中,传统点探测器直接输出强度-角度曲线,而现代设备越来越多采用二维面探测器。面探带来了更高的采集效率,但也引入一个核心问题:

  • 如何把二维图像数据转换为标准的 2θ 衍射数据?

本文通过列角度对齐,将大量.tif的面探数据拼接成连续的XRD数据。
地址:https://gitee.com/zhang_jie_sc/xrd-data-analysis

1.背景

实验扫描流程如下:

  • 每次曝光得到一张 256×256 面探图像
  • 每张图对应一个 中心角度(mid angle)
  • 图像的 每一列 实际对应不同的 2θ
  • 多张图覆盖完整扫描范围(例如 10°–90°)

数据格式、数据内容如下图所示:
数据格式
数据内容
拼接完成效果:
在这里插入图片描述
即:

  • X轴:2θ角度
  • Y轴:探测器行
  • 值:强度

最终形成类似连续衍射图谱的 mosaic。

2.核心物理关系

面探测器中:

  • 中心列对应扫描角 mid
  • 每个像素存在固定角度偏移

角度换算:

PIX_ANGLE = 0.055 / 225
DEG_PER_PX = PIX_ANGLE / np.pi * 180.0
#列角度
col_angles = mid + (np.arange(W) - c0) * DEG_PER_PX

这一步本质是:

  • 像素坐标 → 物理角度坐标

3.算法整体思路

整个流程可以理解为:

读取图像
   ↓
计算每列真实角度
   ↓
找到该角度属于哪个bin
   ↓
将该列强度累加到目标矩阵

示例:

原始图像 (256×256)
      ↓
逐列投影
      ↓
角度bin对齐
      ↓
累加到全局矩阵

4.角度Bin设计

为了得到连续谱,需要建立统一角度网格:

angle_edges = np.arange(angle_min,angle_max + angle_step,angle_step)

例如:

10° → 90°
步长 0.02°

这相当于创建:

4000 个角度通道

最终 accumulator:

acc = np.zeros((H, n_bins))

5.关键函数解析

1️⃣ 文件名解析角度

def get_angle(filename):
    m = re.search(r"\d+\.\d{3}", filename)
    return float(m.group()) if m else 0.0

2️⃣ 列数据累加(核心)

def add_by_column(acc, angle_edges, angle, col):
    bin_idx = np.searchsorted(angle_edges, angle, side="right") - 1
    if bin_idx < acc.shape[1]:
        acc[:, bin_idx] += col

这里做了三件事:

  1. 找到角度属于哪个 bin
  2. 取出该列强度
  3. 累加进全局矩阵

3️⃣ 主拼接流程

for fp in paths:
    mid = get_angle(fp)
    img = tifffile.imread(fp)
    img = np.rot90(img, k=1)

    col_angles = mid + (np.arange(W)-c0)*DEG_PER_PX

    for j, angle_ in enumerate(col_angles):
        if 10 <= angle_ <= 90:
            add_by_column(acc, angle_edges, angle_, img[:, j])

6.优点

  1. 思路简单,先准备一个空白矩阵,再扫描每张图中的每一列数据,找到对应的列数累加即可
  2. 易于移植,没有复杂的numpy方法,可轻松移植到C++平台

7.完整代码

import re, time
from pathlib import Path
import numpy as np
import tifffile
import matplotlib.pyplot as plt

PIX_ANGLE = 0.055 / 225
DEG_PER_PX = PIX_ANGLE / np.pi * 180.0


def get_angle(filename: str) -> float:
    m = re.search(r"\d+\.\d{3}", filename)
    return float(m.group()) if m else 0.0


def add_by_column(acc: np.ndarray, angle_edges, angle, col):
    bin_idx = np.searchsorted(angle_edges, angle, side="right") - 1
    h, w = acc.shape
    if bin_idx < w:
        acc[:, bin_idx] += col


def stitch_bins_aligned(folder: Path, angle_min=10.0, angle_max=90.0, angle_step=0.02):
    paths = sorted([str(p.resolve()) for p in folder.glob("*.tif")])
    # bins
    angle_edges = np.arange(angle_min, angle_max + angle_step, angle_step)
    n_bins = len(angle_edges-1)

    H, W = 256, 256
    c0 = 128  # 128

    acc = np.zeros((H, n_bins), dtype=np.float64)

    for i, fp in enumerate(paths, 1):
        mid = get_angle(fp)
        img = tifffile.imread(fp).astype(np.float64)  # (256,256)
        img = np.rot90(img, k=1)
        col_angles = mid + (np.arange(W) - c0) * DEG_PER_PX  # (256,)

        for j, angle_ in enumerate(col_angles, 0):
            if 10 <= angle_ <= 90:
                add_by_column(acc, angle_edges, angle_, img[:, j])

        if i % 300 == 0 or i == len(paths):
            print(f"已完成 {i}/{len(paths)} ({i / len(paths):.0%})")

    acc = np.column_stack((acc, acc[:, -1]))
    return acc, angle_edges


if __name__ == "__main__":
    folder = Path(r'./MiniPixData/MiniPixData2')
    mosaic, angle_edges = stitch_bins_aligned(folder)
    print("max:",mosaic.max(),"min:",mosaic.min())
    plt.figure(figsize=(10, 4))
    plt.imshow(
        mosaic,
        aspect="auto",
        origin="lower",
        extent=[angle_edges[0], angle_edges[-1], 0, mosaic.shape[0]],
        cmap="jet",
        vmin=0,
        vmax=mosaic.max()
    )
    plt.xlabel("2Theta (deg)")
    plt.ylabel("Row (px)")
    plt.title("Stitched mosaic (bins-aligned, no integration)")
    plt.colorbar(label="Intensity")
    plt.tight_layout()
    plt.show()

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐