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味,但是功能可以运行,不用在乎这些细节熬

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

这效果真的无敌了,跟pygid包一个效果
然后是另一张图像的处理:

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