利用nanoscope模块可以快速地将AFM成像原始数据转换为便于后续自己写代码分析的数据类型。

安装 nanoscope 模块可以使用 pip install nanoscope 命令即可。

然后这个模块附带了示例数据和代码,可以到该模块的安装目录中找到。

{anaconda3}\envs{afm}\Lib\site-packages\nanoscope
{anaconda3} 是 anaconda3 的安装目录, {afm} 是我创建的专门用于AFM成像数据分析的python虚拟环境

2025-03-10T01:33:23.png

实际上这个部分内容要完成对AFM成像数据的预处理并导出为8-bit灰度图,具体效果如上。

获取高度数据的基础函数我已经封装如下:

import matplotlib.pyplot as plt
import numpy as np
from PIL import Image


def getHeightData(fp):
    from nanoscope import files
    from nanoscope.constants import METRIC
    data = {}
    with files.ImageFile(fp) as f:
        height = f[0]  #默认AFM成像第一个通道就是height
        image, ax_properties = height.create_image(METRIC)
        assert ax_properties['title']=='Height'
        data['data'] = image
        data['aspect_ratio'] = height.aspect_ratio
        data['scan_size'] = height.scan_size
        data['scan_size_unit'] = height.scan_size_unit
        data['z_sens_units'] = height.z_sens_units
    return data 

这里面 .create_image 是 nanoscope 模块自带的方法把最原始的数值转换为了高度,高度的单位可以查看 z_sens_units,然后创建的 image,每一条 row 就是 AFM针尖扫描的 line,然后每条 line 上采样多少 number就对应了图像的 col。所以 scan_size 就对应着 x_range。然后根据 aspect_ratio(横纵比,一般为1) 可以计算 y_range。

由于实际采集样品数据的类型比较单一(一般是正方形,扫描区域通常是微米级别,高度是纳米级别,可统一单位到纳米),所以进一步简化这个函数,避免各种单位判断和比例转换。

def getHeightData(fp):
    from nanoscope import files
    from nanoscope.constants import METRIC
    data = {}
    with files.ImageFile(fp) as f:
        height = f[0]  
        image, ax_properties = height.create_image(METRIC)
        assert ax_properties['title']=='Height'
        assert abs(height.aspect_ratio-1)<0.01
        assert height.z_sens_units=='nm'
        assert height.scan_size_unit==r'µm'
        data['data'] = image[::-1]
        # y axis invert,保持和在 NanoscopeAnalysis中看到的效果一样
        data['row'], data['col'] = image.shape
        data['pixelsize'] = height.scan_size*1000/data['row']
    return data   

需要注意的是,此时得到的高度数据没有对齐基线,所以还需要进行 flatten 处理。这里提供了两种方法。一个是 median 中值对齐方法,适用于视野中样品颗粒较低的情况。一种是 polyfit 对齐方法,一般情况下都能获得不错的抚平效果。

def flat_median(arr):
    box = []
    for line in arr:
        line_ = line - np.median(line)
        box.append(line_)
    box = np.array(box)
    return box


def flat_polyfit(arr, order=1):
    box = []
    for line in arr:
        coeff = np.polyfit(range(len(line)),
                           line, 
                           order)
        correction = np.array(
        [sum([pow(i, n) * c
        for n, c in enumerate(reversed(coeff))])
        for i in range(len(line))])
        line_ = line - correction
        box.append(line_)
    box = np.array(box)
    return box

完成 AFM 高度数据的抚平之后,为了导出为图像,还需要一些工作,主要涉及高度数据到 8-bit 像素的映射,以及导出图像的 pixelsize有一个 rescale 的过程。具体函数代码如下:

def convert(hmap, z_range=[-10, 10]):
    data = hmap.copy()
    low, high = z_range
    data = (data - low)/(high - low)
    inds = np.where(data<0)
    data[inds] = 0
    inds = np.where(data>1)
    data[inds] = 1
    data = data*255
    data = data.astype('uint8')
    return data


def resize(data, pixelsize=2):
    # resize image, 1 pixel = 2 nm
    image = data['convert_flat_data']
    img = Image.fromarray(image)
    pixelsize0 = data["pixelsize"]
    width = data["col"]
    height = data["row"]
    w = int(width*pixelsize0/pixelsize)
    h = int(height*pixelsize0/pixelsize)
    img2 = img.resize((w,h))
    data['export_image'] = img2
    data["export_image_pixelsize"] = pixelsize
    return data

注意在这个地方,我根据实际情况,将高度映射的区间定到了 -10 到 10 nm,然后导出图像的 pixelsize 设置为了 1 pixel = 2 nm。

基于上述函数,对于单个AFM成像的原始数据,其预处理和导出函数再集成封装一下,就变成这样:

def single(fp, flat='median', pixelsize=2, z_range=[-10, 10]):
    assert fp.endswith(".spm")  
    # 目前仅对Bruker Multimode VIII的采集到的spm数据文件进行过测试
    # 发现部分以 `.001`, `.002` 之类结尾的数据不能nanoscope模块不能正确解析
    # 后续可尝试另外一个 nanoscope 的开源项目以获得更好的兼容性
    # https://github.com/jmarini/nanoscope/
    data = getHeightData(fp)
    if flat=='polyfit':
        data['flat_data'] = flat_polyfit(data['data'])
    else:
        data['flat_data'] = flat_median(data['data'])
    data['convert_flat_data'] = convert(data['flat_data'], 
                                        z_range=z_range)
    data = resize(data, pixelsize=pixelsize)
    fp2 = fp+".png"
    data['export_image'].save(fp2)
    return data

如果是要对比较多的数据文件进行批量处理,可以考虑使用 joblib 模块来并行:

from glob import glob
from joblib import Parallel, delayed

fps = glob("*.spm")
# 获取所有 spm 文件列表
flat_method='median'
pixelsize=2
z_range=[-10, 10]

res = Parallel(n_jobs=-1)\
      (delayed(single)(fp, flat_method, pixelsize, z_range)\
       for idx,fp in enumerate(fps))
最后修改:2025 年 03 月 10 日
请大力赞赏以支持本站持续运行!