单细胞空间转录组:空间可视化指南
模块简介
本教程专为处理寻因生物 (SeekGene) 单细胞空间转录组产品的底层空间几何变换而设计,旨在帮助您深入理解并实现芯片坐标与组织图像的精准对齐。
在开始具体的代码操作前,我们需要先理解单细胞空间转录组产品的空间坐标体系。在分析产出的矩阵中,空间坐标(通常被称为“芯片像素”)是基于测序芯片自身的网格建立的。它与我们日常处理的光学图像像素截然不同,其本质上表征的是芯片底层的物理分辨率。例如,在寻因高分辨芯片中,1 个芯片像素大致对应 0.2653 微米 (μm) 的真实物理距离。
在真实的科研分析场景中,无论是为了将基因表达数据与组织形态学特征进行严丝合缝的整合,还是为了精确测量细胞微环境中的物理距离,研究人员都不可避免地需要对这些原始坐标进行几何转换。如果您关注的是生物学聚类、细胞分型等下游分析结果的空间可视化,请参考单细胞空间转录组专题下的其他教程。而本教程则聚焦于底层基石,专门为您拆解并演示坐标映射与图像对齐过程中最核心的 两项空间变换任务:
- 1. 空间坐标的转换与统一:这是空间可视化的基础,主要包含两个维度的转换:
- 空间坐标到物理距离 (Coordinate to Physical Distance):根据芯片的物理分辨率,将抽象的相对坐标换算为具有实际生物学意义的真实物理距离(um)。
- 空间坐标与图像像素的映射 (Coordinate to Pixel):建立测序数据物理坐标系与图像像素坐标系之间的数学映射。
- 2. 交互式高精度图像对齐 (Interactive Affine Alignment):在完成初步的坐标映射后,为了修正切片在实验过程中产生的偏移或旋转,本教程提供了一个离线的 HTML 交互式对齐工具。您可以通过网页中的按钮进行平移、缩放和旋转等视觉微调,从而实现对全分辨率原始图像的无损刚性校准。
输入文件准备
本模块需要以下输入文件:
- 必需细胞空间坐标文件:必须为标准流程产出的
_filtered_feature_bc_matrix/cell_locations.tsv.gz文件,该文件包含了每个细胞在测序芯片上的物理网格坐标。 - 必需组织切片图像文件:必须为经过 SeekSpaceTools 质控与对齐后的图像文件,通常命名为
*_aligned_HE.png或*_aligned_DAPI.png。该图像作为空间可视化的底层参照,确保了与测序数据的初步空间对齐关系。
依赖包导入
import pandas as pd
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
import os, base64, json
import cv2
import io
import plotly# === 全局参数配置 ===
# 芯片的数据物理边界
# 注意:
# 在 SeekSpaceTools v1.0.2 中,芯片的物理边界固定为 55050x19906。
# 如果您使用的软件版本在 v1.0.3 及以上,芯片的物理边界通常为 56500x20434。
# 具体参数可通过 rds 文件 或 h5ad 文件获取
CHIP_MAX_X = 55050
CHIP_MAX_Y = 19906
# 芯片物理分辨率 (每单位代表的微米数 um)
RESOLUTION_UM = 0.2653空间坐标的转换与统一
本节将引导您完成空间可视化的核心基础任务:将芯片网格坐标换算至真实的物理微米 (um) 距离,并在宏观层面上将测序数据与图像坐标系统一。
读取细胞坐标与图像数据
在进行任何空间变换前,我们首先需要加载两个核心的基础数据:
- 细胞空间坐标数据:通常为
cell_locations.tsv.gz,记录了每个细胞(Cell)在测序芯片网格上的原始相对位置(即芯片坐标)。 - 组织切片图像:加载经过初步对齐的 H&E 或 DAPI 图像,并提取其像素矩阵的形状 (Shape) 信息。图像的尺寸(宽度和高度)将为我们后续设定绘图坐标轴范围、以及计算像素映射缩放比例提供关键的尺度参考。
table = pd.read_csv("A5_filtered_feature_bc_matrix/cell_locations.tsv.gz", sep="\t")
table.head()| Cell_Barcode | X | Y | |
|---|---|---|---|
| 0 | AAACCCAATGACACGATACTGTG | 35293 | 6684 |
| 1 | AAACCCAATGCGCAGTGCTGCGC | 41981 | 2808 |
| 2 | AAACCCAATGCGGACATACTGTG | 3052 | 11748 |
| 3 | AAACCCAATGCTTCGACTAGTCA | 24843 | 6148 |
| 4 | AAACCCAATGGACCACGTAGTCA | 24279 | 4715 |
img = Image.open('A5_aligned_HE.png')
img_array = np.array(img)
img_array.shape初始坐标系叠加与可视化(基于芯片坐标)
在这一步,我们直接将读取的组织图像与细胞的原始芯片坐标进行叠加可视化,以进行初步的对齐检查。
注意:如果您使用的是 H&E 染色图像,它通常来自于实验使用的切片的 相邻切片 (Adjacent Slice)。由于生物学组织在三维空间上的连续变化,以及两张切片在制备过程中产生的独立微小形变,图像组织轮廓与细胞坐标之间存在轻微的局部不完全重合属于正常的实验现象,并非坐标映射错误
代码逻辑解析:
extent=[0, CHIP_MAX_X, 0, CHIP_MAX_Y]:这是matplotlib.pyplot.imshow中的关键参数。由于我们的细胞散点坐标 (X, Y) 是基于芯片的物理网格(如 0 ~ 55050),而读取进来的图像是像素坐标系(例如高度 1447 × 宽度 4000 像素),两者的尺度完全不同。通过设置extent,我们强制将这张组织图像在绘图时“拉伸”并覆盖到整个芯片的坐标边界范围内。- 注意:此时图像被同比例放大并强行 fit 在了芯片的相对坐标空间上。这是一种视觉层面上的“强制映射”,主要用于检查图像的宏观方向和细胞的分布轮廓是否大致吻合,尚未涉及真实的物理距离换算。
plt.figure(figsize=(20, 12))
# 先绘制背景图片
ax = plt.gca()
ax.imshow(img,
cmap='gray',
extent=[0, CHIP_MAX_X, 0, CHIP_MAX_Y], # 关键:设置图片坐标范围
aspect='auto',
alpha=1)
# 再绘制散点图
ax.scatter(
table['X'],
table['Y'],
s=5,
alpha=0.8,
edgecolors='none',
rasterized=True
)
ax.set_aspect('equal')
将细胞坐标转化为真实物理距离 (um)
为了进行具有实际生物学意义的下游分析(例如计算细胞间的真实微环境距离),我们需要将抽象的芯片坐标转换为真实的物理长度单位(微米,μm)。
核心逻辑:
- 物理尺度换算:直接将细胞的原始坐标乘以物理转换系数(即每个芯片像素对应的真实微米数),即可换算得到物理距离坐标。
- 可视化与性能提示:在绘制物理距离下的散点图时,只需在绘图工具层面调整背景图片的坐标覆盖范围即可。强烈建议不要直接去放大原图的实际像素矩阵,以免引发内存溢出并损失图像的高清细节。
table["X_um"] = table["X"] * RESOLUTION_UM
table["Y_um"] = table["Y"] * RESOLUTION_UMtable["X_um"].describe()mean 6158.961242
std 3163.637123
min 624.781500
25% 3458.981400
50% 6194.755000
75% 8962.629900
max 11734.749600
Name: X_um, dtype: float64
plt.figure(figsize=(20, 12))
# 先绘制背景图片
ax = plt.gca()
ax.imshow(img,
cmap='gray',
extent=[0, CHIP_MAX_X * RESOLUTION_UM, 0, CHIP_MAX_Y * RESOLUTION_UM], # 关键:设置图片坐标范围
aspect='auto',
alpha=1) # 设置透明度
# 再绘制散点图
ax.scatter(
table['X_um'],
table['Y_um'],
s=5,
alpha=0.8,
edgecolors='none',
rasterized=True
)
ax.set_aspect('equal')
将细胞坐标转化为光学图像像素
在某些分析场景下(例如需要从组织切片中提取局部图像特征),我们需要将细胞的空间坐标映射到真实的光学图像像素坐标系中。
核心逻辑:
- 坐标等比换算:利用前面计算出的缩放比例,直接将细胞的芯片坐标等比例缩小,转换到图像实际的像素维度下。
- 高效对齐策略:在绘图和对齐时,我们通过数学计算转换“一维的坐标点”,而不是强行去放大或缩放“高分辨率的背景图片”。这种方式不仅计算成本极低,避免了内存溢出,还能完美保留原图的图像特征与清晰度。
scale_ratio_x= CHIP_MAX_X/img_array.shape[1]
scale_ratio_y = CHIP_MAX_Y/img_array.shape[0]
print(scale_ratio_x, scale_ratio_y)table["X_img"] = table["X"]/scale_ratio_x
table["Y_img"] = table["Y"]/scale_ratio_yplt.figure(figsize=(20, 12))
# 先绘制背景图片
ax = plt.gca()
ax.imshow(img,
cmap='gray', # 如提供HE图像,注释掉这一行
extent=[0, img_array.shape[1], 0, img_array.shape[0]], # 关键:设置图片坐标范围
aspect='auto',
alpha=1) # 设置透明度
# 再绘制散点图
ax.scatter(
table['X_img'],
table['Y_img'],
s=5,
alpha=0.8,
edgecolors='none',
rasterized=True
)
ax.set_aspect('equal')
交互式高精度图像对齐
在完成初始的坐标映射后,我们通常会发现组织图像与细胞散点之间存在一定的偏差。为了修正切片在实验过程中产生的物理偏移或旋转,我们需要对图像进行仿射变换 (Affine Transformation)。
什么是仿射变换? 仿射变换是一种二维坐标到二维坐标的线性映射。它能够保持图像的“平直性”和“平行性”(即直线变换后依然是直线,平行线依然平行)。在空间转录组的对齐过程中,我们主要利用仿射变换的三个几何特性:
- 平移 (Translation):修正组织在 X 轴和 Y 轴上的绝对位置偏移。
- 缩放 (Scaling):修正由于光学镜头或分辨率差异导致的图像大小缩放。
- 旋转 (Rotation):修正切片贴在载玻片上时的角度倾斜。
核心变换参数
在接下来的操作中,我们将寻找一个
Sx, Sy(Scale): 图像在 X/Y 轴上的缩放比例。Tx, Ty(Translate): 图像在 X/Y 轴上平移的原始像素距离。degree(Rotate): 图像围绕中心点的旋转角度。
工作流总结
为了避免在代码中盲目地盲猜这些参数,本教程接下来将提供一种交互式解决方案:
- 利用代码自动生成一个本地 HTML 交互网页;
- 在网页中通过可视化点击,直观地将图像与细胞对齐;
- 网页会在后台自动通过矩阵乘法,将您的所有交互动作累加为一个标准的仿射矩阵,您只需将算出的
Sx, Sy, Tx, Ty, degree复制并应用到最终的 Python OpenCV 图像处理函数中,即可实现全图的精准校准。
展示图像对齐前的情况
plt.figure(figsize=(20, 12))
# 先绘制背景图片
ax = plt.gca()
ax.imshow(img,
cmap='gray',
extent=[0, CHIP_MAX_X * RESOLUTION_UM, 0, CHIP_MAX_Y * RESOLUTION_UM], # 关键:设置图片坐标范围
aspect='auto',
alpha=1) # 设置透明度
# 再绘制散点图
ax.scatter(
table['X_um'],
table['Y_um'],
s=5,
alpha=0.8,
edgecolors='none',
rasterized=True
)
ax.set_aspect('equal')
交互式对齐工具 (Interactive Alignment)
由于 Jupyter Notebook 环境在处理海量散点图和高频图像重绘时容易出现卡顿或刷新异常,为了保证对齐操作的精确度与流畅性,我们采用了一种更加高效的方案:利用 Python 代码自动生成一个独立运行的离线 HTML 交互工具。
技术实现与工作原理:
- 离线 Plotly.js:代码会首先提取本地环境中的
plotly.js库,避免网络环境对加载速度的影响。 - 图像智能降采样:为了确保浏览器极速渲染,代码会在后台对原始高分辨率图像进行等比例缩小,并转化为 Base64 格式嵌入网页。注意:这一步仅改变视觉展示的分辨率,工具后台计算矩阵时依然严格基于图像原始的物理像素尺寸。
使用指南:
- 生成工具:依次运行下方的代码块。执行完毕后,您会在当前目录看到新生成的
alignment_tool.html文件。 - 打开网页:在系统的文件管理器中双击该 HTML 文件,使用浏览器(推荐 Chrome 或 Edge)打开。
- 交互微调:在网页顶部的控制面板中调整每次操作的步长,通过点击方向、缩放和旋转按钮,观察底图与红色散点(细胞)的匹配程度。
- 参数提取:网页后台会实时将您的操作累加成一个标准的 3x3 仿射变换矩阵。当确认对齐后,点击绿色的 “📋 复制参数 (JSON)” 按钮。
- 应用参数:将剪贴板中的 JSON 参数粘贴到本教程后续章节对应的代码单元格中,即可对原始图像执行最终的裁剪与变换。
# 提取纯本地 Plotly JS
js_path = os.path.abspath("plotly_offline.min.js") # 此处需要替换plotly 文件的路径
with open(js_path, "w", encoding="utf-8") as f:
f.write(plotly.offline.get_plotlyjs())# 图像降采样并转 Base64
h_ori, w_ori = img_array.shape[:2]
max_dim = 1200
if max(h_ori, w_ori) > max_dim:
scale_down = max_dim / max(h_ori, w_ori)
img_small = cv2.resize(img_array, (int(w_ori * scale_down), int(h_ori * scale_down)))
else:
img_small = img_array
img_pil = Image.fromarray(img_small)
buffer = io.BytesIO()
img_pil.save(buffer, format="JPEG", quality=85)
img_b64 = "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode('utf-8')# 细胞坐标抽样
sample_size = 80000
if len(table) > sample_size:
table_sample = table.sample(sample_size)
else:
table_sample = table
# 细胞坐标
# 如果使用物理距离,参数如下
x_coords = [round(x, 2) for x in table_sample['X_um'].tolist()]
y_coords = [round(y, 2) for y in table_sample['Y_um'].tolist()]
W = float(CHIP_MAX_X * RESOLUTION_UM)
H = float(CHIP_MAX_Y * RESOLUTION_UM)
# 如果使用芯片坐标,参数如下
# x_coords = [round(x, 2) for x in table_sample['X'].tolist()]
# y_coords = [round(y, 2) for y in table_sample['Y'].tolist()]
# W = CHIP_MAX_X
# H = CHIP_MAX_Y
# 如果使用像素坐标,参数如下
# x_coords = [round(x, 2) for x in table_sample['X_img'].tolist()]
# y_coords = [round(y, 2) for y in table_sample['Y_img'].tolist()]
# W = img_array.shape[1]
# H = img_array.shape[0]# ==========================================
# 3. 构造 HTML (修复矩阵的 Scale Factor 逻辑并新增复制 JSON 功能)
# ==========================================
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SeekSpace 极速对齐工具</title>
<script src="./plotly_offline.min.js"></script>
<style> body {{ font-family: sans-serif; margin: 20px; background: #f8f9fa; }}
.panel {{ display: flex; gap: 15px; align-items: center; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); margin-bottom: 15px; flex-wrap: wrap; }}
button {{ padding: 6px 10px; cursor: pointer; border: 1px solid #ced4da; border-radius: 6px; background: #fff; font-size: 13px; transition: background 0.2s; }}
button:hover {{ background: #e9ecef; }}
input[type="number"] {{ width: 60px; padding: 4px; border: 1px solid #ced4da; border-radius: 4px; }}
.param-box {{ background: #212529; color: #61afef; padding: 10px 15px; border-radius: 6px; font-family: monospace; font-size: 14px; font-weight: bold; letter-spacing: 1px; }}
#plotly-div {{ width: 100%; height: 80vh; background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
table { border: 0; }</style>
</head>
<body>
<div class="panel">
<div>
<b>平移 (<input type="number" id="step-val" value="30"> um):</b>
<button onclick="moveImg('left')">⬅️ 左移</button>
<button onclick="moveImg('right')">➡️ 右移</button>
<button onclick="moveImg('up')">⬆️ 上移</button>
<button onclick="moveImg('down')">⬇️ 下移</button>
</div>
<div>
<b>缩放 (<input type="number" id="step-s" value="0.01" step="0.01">):</b>
<button onclick="scaleImg('shrink')">➖ 缩小</button>
<button onclick="scaleImg('enlarge')">➕ 放大</button>
</div>
<div>
<b>旋转 (<input type="number" id="step-deg" value="0.25" step="0.25"> °):</b>
<button onclick="rotateImg(-1)">🔄 逆时针</button>
<button onclick="rotateImg(1)">🔃 顺时针</button>
</div>
<!-- 新增:右侧对齐的复制按钮与参数显示面板 -->
<div style="margin-left: auto; display: flex; gap: 10px; align-items: center;">
<button onclick="copyParamsToClipboard()" style="background: #4caf50; color: white; border: none; font-weight: bold;">📋 复制参数 (JSON)</button>
<div class="param-box" id="param-display">加载中...</div>
</div>
</div>
<div id="plotly-div"></div>
<script>
const x_coords = {json.dumps(x_coords)};
const y_coords = {json.dumps(y_coords)};
const original_img_b64 = "{img_b64}";
const W = {W};
const H = {H};
const scale_xy = H / W;
// 1. Vue 里的变量定义
let pos_x = 0;
let pos_y = H;
let degree = 0;
let img_size_x = W;
let img_size_y = H;
// 核心:纯净的仿射矩阵
let affine_matrix = [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0]
];
// 矩阵左乘函数 (复刻 mathjs multiply)
function multiplyMatrix(A, B) {{
let C = [[0,0,0],[0,0,0],[0,0,0]];
for(let i=0; i<3; i++)
for(let j=0; j<3; j++)
for(let k=0; k<3; k++)
C[i][j] += A[i][k] * B[k][j];
return C;
}}
function updateDisplay() {{
let Sx = affine_matrix[0][0];
let Sy = affine_matrix[1][1];
let Tx = affine_matrix[0][2];
let Ty = affine_matrix[1][2];
document.getElementById('param-display').innerText =
`Sx: ${{Sx.toFixed(4)}} | Sy: ${{Sy.toFixed(4)}} | Tx: ${{Tx.toFixed(2)}} | Ty: ${{Ty.toFixed(2)}} | Deg: ${{degree.toFixed(2)}}°`;
}}
// 新增:复制参数到剪贴板功能
window.copyParamsToClipboard = function() {{
let Sx = affine_matrix[0][0];
let Sy = affine_matrix[1][1];
let Tx = affine_matrix[0][2];
let Ty = affine_matrix[1][2];
// 构造标准 JSON 对象
let params = {{
"Sx": Number(Sx.toFixed(4)),
"Sy": Number(Sy.toFixed(4)),
"Tx": Number(Tx.toFixed(2)),
"Ty": Number(Ty.toFixed(2)),
"degree": Number(degree.toFixed(2))
}};
// 转换为格式化的字符串
let jsonStr = JSON.stringify(params, null, 2);
// 写入剪贴板并弹窗提示
navigator.clipboard.writeText(jsonStr).then(() => {{
alert("✅ 仿射矩阵参数已成功复制到剪贴板!\\n\\n" + jsonStr);
}}).catch(err => {{
alert("❌ 复制失败,请手动复制:\\n" + jsonStr);
}});
}};
// 2. 移动逻辑修复:引入 scale factor
window.moveImg = function(dir) {{
let s = parseFloat(document.getElementById('step-val').value) || 30;
let M = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
let current_scale = img_size_x / W;
let matrix_shift = s / current_scale;
if (dir === 'left') {{
M[0][2] = -matrix_shift;
pos_x -= s;
}} else if (dir === 'right') {{
M[0][2] = matrix_shift;
pos_x += s;
}} else if (dir === 'up') {{
M[1][2] = -matrix_shift;
pos_y += s;
}} else if (dir === 'down') {{
M[1][2] = matrix_shift;
pos_y -= s;
}}
affine_matrix = multiplyMatrix(M, affine_matrix);
Plotly.relayout('plotly-div', {{
'images[0].x': pos_x,
'images[0].y': pos_y
}});
updateDisplay();
}};
window.scaleImg = function(dir) {{
let s = parseFloat(document.getElementById('step-s').value) || 0.01;
let scale_factor = 1.0;
let delta_size = W * s;
if (dir === 'enlarge') {{
scale_factor = (img_size_x + delta_size) / img_size_x;
img_size_x += delta_size;
img_size_y += delta_size * scale_xy;
}} else if (dir === 'shrink') {{
scale_factor = (img_size_x - delta_size) / img_size_x;
img_size_x -= delta_size;
img_size_y -= delta_size * scale_xy;
}}
let M = [
[scale_factor, 0, 0],
[0, scale_factor, 0],
[0, 0, 1]
];
affine_matrix = multiplyMatrix(M, affine_matrix);
Plotly.relayout('plotly-div', {{
'images[0].sizex': img_size_x,
'images[0].sizey': img_size_y
}});
updateDisplay();
}};
window.rotateImg = function(sign) {{
const step = parseFloat(document.getElementById('step-deg').value) || 0.25;
degree += sign * step;
rotateBase64Image(original_img_b64, degree).then(rotatedB64 => {{
Plotly.relayout('plotly-div', {{'images[0].source': rotatedB64}});
}});
updateDisplay();
}};
function rotateBase64Image(base64data, degrees) {{
return new Promise((resolve, reject) => {{
const img = new Image();
img.src = base64data;
img.onload = function () {{
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.translate(img.width / 2, img.height / 2);
ctx.rotate(degrees * (Math.PI / 180));
ctx.drawImage(img, -img.width / 2, -img.height / 2, img.width, img.height);
resolve(canvas.toDataURL());
}};
img.onerror = reject;
}});
}}
// 初始渲染
const data = [{{
x: x_coords,
y: y_coords,
mode: 'markers',
type: 'scattergl',
marker: {{ size: 2, color: 'red', opacity: 0.5 }},
name: 'Cells',
hoverinfo: 'none'
}}];
const layout = {{
xaxis: {{ range: [0, W], title: "X (um)", scaleanchor: "y", scaleratio: 1, showgrid: false }},
yaxis: {{ range: [0, H], title: "Y (um)", showgrid: false }},
margin: {{ l: 50, r: 50, t: 50, b: 50 }},
hovermode: false,
plot_bgcolor: 'black',
images: [{{
source: original_img_b64,
xref: "x",
yref: "y",
x: pos_x,
y: pos_y,
sizex: img_size_x,
sizey: img_size_y,
sizing: "stretch",
opacity: 0.6,
layer: "below"
}}]
}};
Plotly.newPlot('plotly-div', data, layout, {{responsive: true, displayModeBar: true}}).then(() => {{
updateDisplay();
}});
</script>
</body>
</html>
"""
html_filename = "alignment_tool.html"
html_path = os.path.abspath(html_filename)
with open(html_path, "w", encoding="utf-8") as f:
f.write(html_content)
print(f"✅ HTML 工具已在当前目录生成,请双击打开文件:{html_filename}")对图像进行仿射变换
请将你在配准软件中获得的仿射变换参数粘贴到下方params字典中。 必须包含以下 5 个键:Sx, Sy, Tx, Ty, degree
def affine_transformation(img, Sx, Sy, Tx, Ty, degree):
h, w, c = img.shape
x0 = w/2
y0 = h/2
M_rotate = np.array([[np.cos(np.radians(degree)), -np.sin(np.radians(degree)), x0 * (1 - np.cos(np.radians(degree))) + y0 * np.sin(np.radians(degree))],
[np.sin(np.radians(degree)), np.cos(np.radians(degree)), y0 * (1 - np.cos(np.radians(degree))) - x0 * np.sin(np.radians(degree))],
[0, 0, 1]])
M_trans = np.array([[Sx, 0, Tx],
[0, Sy, Ty],
[0, 0, 1]])
M_f = np.dot(M_trans, M_rotate)
# 使用 warpAffine 进行严格的仿射变换 (比 warpPerspective 性能更好)
# img_transformed = cv2.warpAffine(img, M_trans, (w, h), borderValue=(0, 0, 0))
return cv2.warpPerspective(img, M_f, (w, h), borderValue=(255, 255, 255))
params = {
"Sx": 0.994,
"Sy": 0.994,
"Tx": 79.86,
"Ty": 39.46,
"degree": -0.45
}
img_trans = affine_transformation(img_array, **params)展示转换后的图片和细胞的对齐程度
最后,我们将应用了最终仿射矩阵的图像 (img_trans) 与细胞散点坐标进行叠加展示,以验证对齐效果。
⚠️ 重要提示:关于刚性变换的局限性
请注意,本教程中提供的方法使用的是仿射变换 (Affine Transformation)。仿射变换属于刚性/半刚性变换,它只能处理图像整体的平移、旋转和缩放。它假设整个组织切片像一块坚硬的木板,内部各个细胞点之间的相对位置关系是保持绝对不变的。
然而,在真实的生物学实验中(特别是相邻连续切片之间的匹配,或者切片面积较大时),组织经常会发生非刚性形变 (Non-rigid Deformation)
如果您的图像存在这种局部的非刚性形变,仅仅通过本教程中的全局刚性仿射变换是无法实现完美对齐的。
针对这种情况,我们强烈建议您: 在进行下游的空间转录组坐标对齐之前,先将您的原始切片图像导入到专业的图像配准软件中进行非刚性配准 (Elastic/Non-linear Registration) 处理。常用的专业工具包括:
- Fiji/ImageJ(例如使用 TrakEM2 或 bUnwarpJ 插件)
- QuPath
- 基于配准算法的独立工具,如 Elastix 等。
plt.figure(figsize=(20, 12))
# 先绘制背景图片
ax = plt.gca()
ax.imshow(img_trans,
cmap='gray',
extent=[0, CHIP_MAX_X * RESOLUTION_UM, 0, CHIP_MAX_Y * RESOLUTION_UM], # 关键:设置图片坐标范围
aspect='auto',
alpha=1) # 设置透明度
# 再绘制散点图
ax.scatter(
table['X_um'],
table['Y_um'],
s=5,
alpha=0.8,
edgecolors='none',
rasterized=True
)
ax.set_aspect('equal')
保存调整好的图像
output_filename = "aligned_HE_transformed.png"
final_img = Image.fromarray(img_trans.astype('uint8'))
final_img.save(output_filename)