3/5 1.13 终于解决GIWAXS图像的转化问题

3/5 1.13 终于解决GIWAXS图像的转化问题

1. 第一性原理

前天大前天的迷迷糊糊,始终在辱骂AI,让他直接给我想要的结果,这似乎是不可能的,AI终究很难代替人类,人机协同vibecoding似乎才是时代的主流,当面对任何问题时,我们应该透过问题看本质,探究解决问题的本源,或者探索数据是怎样经过一步步处理达成我们想要的结果的。

收获:在使用AI助手的时候,我始终认为我们应该把他当做工具,而不是一个执行你主观想法的工具,只有我们抱有这种心态时,才会发现AI越来越好用,越用越好用,而不是一味地只求高级的模型,拥有恐怖上下文吞吐量和机制响应速度的模型。而是回归本源

今天,我让AI给我讲清楚了GIWAXS图像到底是怎么产生的,不仅帮助我理清了这个脚本的具体架构,更让我摆脱了库的束缚,从底层物理公式开始编写,一步步看着数据经过算法处理,成为我们想要的结果,具体的:
(1)GIWAXS图像tif格式的产生:每个像素点都对应像素强度,这是光子数目的一个映射,才有了我们看到的tif图像,但是这时候光子的坐标pixel_x,pixel_y仅仅是在探测器板上的一个没有物理含义的数值,因此如果改变了探测器的位置,或者换了X射线的波长,这时候同一个样品就会产生不同的图像,因此,我们还需要几何参数的矫正,将图像由无意义的像素空间转化为包含物理意义的倒空间

(2)我们首先要知道探测器像素长度,也就是每一个小格像素代表的真实物理长度;其次得知了真实的物理长度,我们需要结合X射线中心点Center_x和Center_y两个像素坐标(或者真实物理坐标),这样我们就能拿到光子的实际物理坐标,有了这个坐标;我们就可以结合空间几何的知识,结合探测器距离样品的距离,X射线的波长,X入射角,计算出每个光子的q_x,q_y,q_z,进而计算得到q_xy,q_z,完成倒空间的转换

(3)倒空间的坐标一旦确定,这是极坐标的转化纯粹就是矩阵运算,只取决于算法写的如何,跟参数设置一概没有关系,因此说清了如何计算得到横轴q,纵轴方位角,所有都可以行得通了

2. app.py

在搞清楚一切之后,我成功让AI辅助编辑了app.py

import os
import io
import base64
import numpy as np
import tifffile as tiff
from scipy.stats import binned_statistic_2d
import matplotlib
matplotlib.use('Agg')  # 启用 Web 服务器无头绘图模式,防止 GUI 崩溃
import matplotlib.pyplot as plt
from flask import Flask, request, render_template_string, jsonify

app = Flask(__name__)

# ==========================================
# 前端 HTML 页面 (完美复刻你的暗黑风格)
# ==========================================
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>GIWAXS 终极物理转换器</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #1e1e1e; color: #d4d4d4; margin: 0; padding: 20px; display: flex; flex-direction: column; align-items: center; }
        h1 { color: #569cd6; }
        .container { display: flex; width: 100%; max-width: 1400px; gap: 20px; }
        .panel { background-color: #252526; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
        .controls { flex: 1; min-width: 350px; }
        .display { flex: 2; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; }
        .input-group { margin-bottom: 15px; display: flex; align-items: center; justify-content: space-between; }
        .input-group label { flex: 1; font-size: 14px; color: #9cdcfe; }
        .input-group input { width: 80px; padding: 5px; background: #3c3c3c; border: 1px solid #555; color: white; border-radius: 4px; text-align: right; }
        .input-group select { width: 80px; padding: 5px; background: #3c3c3c; border: 1px solid #555; color: white; border-radius: 4px; margin-left: 5px; }
        .btn-group { display: flex; gap: 10px; margin-top: 15px; }
        button { flex: 1; padding: 12px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; font-weight: bold; transition: background 0.2s; color: white;}
        .btn-qspace { background-color: #007acc; }
        .btn-qspace:hover { background-color: #0098ff; }
        .btn-polar { background-color: #c586c0; }
        .btn-polar:hover { background-color: #d197ce; }
        .file-upload { margin-bottom: 20px; padding: 15px; border: 2px dashed #555; text-align: center; border-radius: 4px; }
        #resultImg { max-width: 100%; border: 1px solid #555; border-radius: 4px; margin-top: 15px; display: none; }
        #loading { display: none; color: #ce9178; margin-top: 20px; font-weight: bold; }
    </style>
</head>
<body>
    <h1>🚀 GIWAXS 本地高精度 3D 转换服务器</h1>
    <div class="container">
        <div class="panel controls">
            <form id="calcForm">
                <div class="file-upload">
                    <input type="file" id="fileInput" accept=".tif,.tiff" style="width:100%; color:white;" required>
                </div>
                <div class="input-group">
                    <label>探测器距离 (SDD)</label>
                    <input type="number" id="sdd" value="809" step="any">
                    <select id="sdd_unit"><option value="mm">mm</option><option value="cm">cm</option></select>
                </div>
                <div class="input-group">
                    <label>X射线波长/能量</label>
                    <input type="number" id="wavelength" value="0.6888" step="any">
                    <select id="wave_unit"><option value="A">Å</option><option value="keV">keV</option></select>
                </div>
                <div class="input-group">
                    <label>掠入射角 (Alpha_i)</label>
                    <input type="number" id="alphai" value="0.5" step="any">
                    <select id="alphai_unit"><option value="deg">度 (°)</option></select>
                </div>
                <div class="input-group">
                    <label>像素物理尺寸</label>
                    <input type="number" id="pixelsize" value="200" step="any">
                    <select id="pixelsize_unit"><option value="um">μm</option><option value="mm">mm</option></select>
                </div>
                <div class="input-group">
                    <label>直射光中心 X (水平)</label>
                    <input type="number" id="centerx" value="545" step="any">
                </div>
                <div class="input-group">
                    <label>直射光中心 Y (垂直)</label>
                    <input type="number" id="centery" value="1826" step="any">
                </div>
                <div class="input-group">
                    <label>输出图像分辨率</label>
                    <input type="number" id="qbins" value="1000" step="100">
                </div>
                
                <div class="btn-group">
                    <button type="button" class="btn-qspace" onclick="processData('cartesian')">1. 倒空间映射 (q_xy, q_z)</button>
                    <button type="button" class="btn-polar" onclick="processData('polar')">2. 极坐标展开 (Cake Plot)</button>
                </div>
            </form>
            <div id="loading">⏳ 正在使用底层 C 引擎进行数百万像素的高精度张量计算,请稍候...</div>
        </div>

        <div class="panel display">
            <h3 style="margin-top:0;">科学映射结果</h3>
            <img id="resultImg" src="" alt="处理结果">
        </div>
    </div>

    <script>
        async function processData(mode) {
            const fileInput = document.getElementById('fileInput');
            if (!fileInput.files.length) { alert("请先上传 TIFF 图像!"); return; }
            
            document.getElementById('loading').style.display = 'block';
            document.getElementById('resultImg').style.display = 'none';

            const formData = new FormData();
            formData.append('file', fileInput.files[0]);
            formData.append('mode', mode);
            formData.append('sdd', document.getElementById('sdd').value);
            formData.append('sdd_unit', document.getElementById('sdd_unit').value);
            formData.append('wavelength', document.getElementById('wavelength').value);
            formData.append('wave_unit', document.getElementById('wave_unit').value);
            formData.append('alphai', document.getElementById('alphai').value);
            formData.append('alphai_unit', document.getElementById('alphai_unit').value);
            formData.append('px_size', document.getElementById('pixelsize').value);
            formData.append('px_size_unit', document.getElementById('pixelsize_unit').value);
            formData.append('cx', document.getElementById('centerx').value);
            formData.append('cy', document.getElementById('centery').value);
            formData.append('q_bins', document.getElementById('qbins').value);

            try {
                const response = await fetch('/process', { method: 'POST', body: formData });
                const result = await response.json();
                if(result.error) {
                    alert("计算错误: " + result.error);
                } else {
                    document.getElementById('resultImg').src = 'data:image/png;base64,' + result.image;
                    document.getElementById('resultImg').style.display = 'block';
                }
            } catch (err) {
                alert("请求失败: " + err);
            }
            document.getElementById('loading').style.display = 'none';
        }
    </script>
</body>
</html>
"""

# ==========================================
# 后端核心算法与路由
# ==========================================
@app.route('/')
def index():
    return render_template_string(HTML_TEMPLATE)

@app.route('/process', methods=['POST'])
def process_image():
    try:
        # 1. 接收文件与参数
        file = request.files['file']
        file_bytes = file.read()
        intensity = tiff.imread(io.BytesIO(file_bytes)).astype(np.float32)
        H, W = intensity.shape
        
        mode = request.form.get('mode')
        params = {
            'sdd': float(request.form.get('sdd')), 'sdd_unit': request.form.get('sdd_unit'),
            'lambda': float(request.form.get('wavelength')), 'wave_unit': request.form.get('wave_unit'),
            'alphai': float(request.form.get('alphai')), 'alphai_unit': request.form.get('alphai_unit'),
            'px_size': float(request.form.get('px_size')), 'px_size_unit': request.form.get('px_size_unit'),
            'cx': float(request.form.get('cx')), 'cy': float(request.form.get('cy')),
            'q_bins': int(request.form.get('q_bins'))
        }

        # 2. 统一单位并生成物理坐标
        sdd = params['sdd'] * (1e-3 if params['sdd_unit'] == 'mm' else 1e-2)
        lambda_A = 12.398 / params['lambda'] if params['wave_unit'] == 'keV' else params['lambda'] * (10 if params['wave_unit'] == 'nm' else 1)
        alpha_i = np.deg2rad(params['alphai'])
        px_size = params['px_size'] * (1e-6 if params['px_size_unit'] == 'um' else 1e-3)
        k0 = 2 * np.pi / lambda_A

        x, y = np.arange(W), np.arange(H)
        X, Y = np.meshgrid(x, y)
        X_phys = (X - params['cx']) * px_size
        Y_phys = (params['cy'] - Y) * px_size

        # 3. 绝对 3D 向量物理映射
        r_norm = np.sqrt(sdd**2 + X_phys**2 + Y_phys**2)
        ki_x = k0 * np.cos(alpha_i)
        ki_z = -k0 * np.sin(alpha_i)

        kf_x = k0 * (sdd * np.cos(alpha_i) + Y_phys * np.sin(alpha_i)) / r_norm
        kf_y = k0 * X_phys / r_norm
        kf_z = k0 * (-sdd * np.sin(alpha_i) + Y_phys * np.cos(alpha_i)) / r_norm

        qx, qy, qz = kf_x - ki_x, kf_y, kf_z - ki_z
        
        # 🌟 修复点1:加回 np.sign(qy),让 qxy 区分左右(不折叠)
        qxy = np.sign(qy) * np.sqrt(qx**2 + qy**2)
        q_total = np.sqrt(qx**2 + qy**2 + qz**2)
        
        plt.figure(figsize=(10, 7))
        q_bins = params['q_bins']

        # ==========================================
        # 4. 根据用户点击的按钮分配任务
        # ==========================================
        if mode == 'cartesian':
            q_max = 3.5
            # 🌟 修复点2:将 X 轴范围设为 [-q_max, q_max],展示左右全貌
            valid_mask = (qz >= 0) & (qxy >= -q_max) & (qxy <= q_max) & (qz <= q_max)
            # X 轴的分辨率翻倍以保持像素比例
            q_grid_data, _, _, _ = binned_statistic_2d(
                qz[valid_mask], qxy[valid_mask], values=intensity[valid_mask],
                statistic='mean', bins=[q_bins, q_bins*2], range=[[0, q_max], [-q_max, q_max]]
            )
            q_data = np.nan_to_num(q_grid_data)
            
            vmax = np.percentile(q_data[q_data > 0], 99.5) if np.any(q_data > 0) else 1
            # 更新 extent 范围
            im = plt.imshow(q_data, origin='lower', extent=[-q_max, q_max, 0, q_max], cmap='plasma', vmin=0, vmax=vmax)
            plt.colorbar(im, label='Intensity (a.u.)')
            plt.xlabel(r'$q_{xy}\ (\AA^{-1})$', fontsize=14)
            plt.ylabel(r'$q_z\ (\AA^{-1})$', fontsize=14)
            plt.title('Exact 3D Cartesian Mapping (Unfolded)', fontsize=16)

        elif mode == 'polar':
            # 🌟 修复点3:极坐标展开时,仅使用右半边的探测器数据 (qy > 0) 消除重影
            qxy_abs = np.abs(qxy)
            chi_deg = np.degrees(np.arctan2(qxy_abs, qz))
            
            horizon_mask = (qz >= 0) & (qy > 0) # 强制屏蔽左半边
            
            q_max_auto = np.max(q_total[horizon_mask])
            chi_max = 90.0
            polar_h = int(q_bins * 0.6)

            valid_mask = horizon_mask & (chi_deg <= chi_max) & (chi_deg >= 0)
            q_polar = np.zeros((polar_h, q_bins), dtype=np.float32)
            counts_polar = np.zeros((polar_h, q_bins), dtype=np.int32)

            col_idx = np.floor((q_total[valid_mask] / q_max_auto) * (q_bins - 1)).astype(np.int32)
            row_idx = np.floor(((chi_max - chi_deg[valid_mask]) / chi_max) * (polar_h - 1)).astype(np.int32)

            np.add.at(q_polar, (row_idx, col_idx), intensity[valid_mask])
            np.add.at(counts_polar, (row_idx, col_idx), 1)

            with np.errstate(divide='ignore', invalid='ignore'):
                q_polar /= counts_polar
                q_polar[np.isnan(q_polar)] = 0

            vmax = np.percentile(q_polar[q_polar > 0], 99.5) if np.any(q_polar > 0) else 1
            im = plt.imshow(q_polar, extent=[0, q_max_auto, 0, chi_max], cmap='plasma', origin='upper', aspect='auto', vmin=0, vmax=vmax)
            plt.colorbar(im, label='Intensity (a.u.)')
            plt.xlabel(r'Scattering Vector $q\ (\AA^{-1})$', fontsize=14)
            plt.ylabel(r'Azimuthal Angle $\chi\ (^\circ)$', fontsize=14)
            plt.title('Corrected GIWAXS Polar Map (Right-Side Only)', fontsize=16)
            plt.gca().invert_yaxis()

        # 5. 将图像写入内存并转换为 Base64 发送给浏览器
        plt.tight_layout()
        buf = io.BytesIO()
        plt.savefig(buf, format='png', dpi=150)
        plt.close()
        buf.seek(0)
        img_base64 = base64.b64encode(buf.read()).decode('utf-8')

        return jsonify({'image': img_base64})

    except Exception as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    print("🚀 本地 GIWAXS 图像处理服务器已启动!")
    print("👉 请在浏览器中打开: http://127.0.0.1:5000")
    app.run(debug=True, port=5000)

这个代码,在运行之前记得先:

i

导入包,然后python app.py,就可以得到这样一个页面:

或许emoji充满了ai味,但是功能可以运行,不用在乎这些细节熬

image-JnJo.png

之后上传图像tif格式的,找遍全网只有pygid对我好...服了,然后按照poni文件输入参数或者按照口头说明输入参数即可完成倒空间的转换

这效果真的无敌了,跟pygid包一个效果

然后是另一张图像的处理:

这是我首次实现两份来源不同GIWAXS图像的自动化倒易转化和极化,真的是进步一大步,明天我就拿着这个脚本去完善我的软件,同时我对于软件的功能或许有了新的思考,因为我明白了GIWAXS图像真正的优势是什么,明天去读一些文献考证一下这些作用,比如查看分子取向等。

3.好梦

困死我了!

GIWAXS图像的深度处理-3/4 2026-03-04
github开发初体验 2026-03-07

评论区