☰
基于MATLAB的小鼠步态分析全流程:足印识别到时空参数
2026/10/28 14:49:07 网站建设 项目流程

做小鼠步态分析的人多半有过同一个卡点:高速相机拍了几百帧影像,跑道上的爪印清清楚楚,可要从这些像素一路推到能写进图表的步长、步宽、支撑相时长、占空比,中间那套“像素到毫米、足印到周期”的换算逻辑,往往没人完整讲过一遍。基于MATLAB的小鼠步态分析全流程实现,本质上是把一段动物运动视频(或一串足印图像)变成一张结构化参数表的过程:识别每一次落爪,判断它是哪条肢体的哪只爪,算出时空参数与协调性指标,再把这些数字交给统计模型。它解决的问题很具体——药物、损伤、基因改造之后,动物的运动功能到底变没变、变了多少、在哪个相位上变。适合三类人参考:刚接手行为学平台、需要从零搭一套流程的实验室新人;手头已有采集设备、但被足印重叠和左右肢误判折磨过一段时间的老手;以及想用现成脚本快速验证一批数据的分析岗。下面这套流程,是我在实际项目里反复改过几版的版本,代码骨架和参数计算都会摊开讲。

1. 整体方案设计与技术路线取舍

1.1 先把生物学问题翻译成可量化的指标

很多人一上手就急着写图像处理代码,结果做到一半发现采集的视角根本算不出想要的参数。我建议先倒着推:你要回答什么问题,它对应哪个量。运动功能评估常见的关切无非几类——整体行进能力(速度、步频、单位时间步数)、单肢的支撑能力(支撑相时长、足印面积、最大接触面积)、肢体间的配合(左右对称性、前后肢相位差)、以及精细的落爪姿态(足印角度、趾间距、爪长)。每一类背后对应的采集要求完全不同:算步宽必须有俯视视角并且能看到左右爪的横向分离;算足印角度要求爪子没有被身体阴影遮挡;算趾间距基本要求分辨率足够看清脚趾。

把问题拆到这一层,你会发现“步态分析”其实是一组子任务的集合,而不是一个单一指标。实际项目里我通常会给每只动物产出两张表:一张是逐个足印的明细表(每个足印一行,记录时刻、坐标、面积、角度、归属肢体),另一张是按步幅聚合的参数表(每个步幅一行,记录步长、周期、支撑时长、摆动时长、占空比)。明细表负责追溯,聚合表负责统计。这个双表结构后来救过我很多次——当导师质疑某个组的步长差异时,我可以直接回到明细表,看是不是某一只动物某个异常足印把均值拉偏了。

还有一点要想在前头:小鼠的步态强烈依赖行进速度。同一只动物在 15 cm/s 和 30 cm/s 下的步长、占空比可以差出一大截。这不是噪声,是真实的生理调节。所以设计阶段就要决定:是强迫动物在固定速度的跑带上走,还是让它在自由通道里自己走、然后事后按速度分层或把速度作为协变量放进模型。这两条路后面会反复提到,因为它直接决定了你的统计模型怎么写。

1.2 三条采集路线的取舍

采集方式决定了后面所有算法的难度上限。我在不同项目里用过三种路线,各自的代价差别很大,列个表更直观:

路线原理优点主要代价适合的参数
墨迹足印法爪底蘸无毒颜料,走过白纸留下足迹成本极低、图像干净、易归档只能拿到空间信息,无法测时间参数;动物需要保定上色步长、步幅、步宽、足印角度
透明跑道下视高速摄像玻璃或亚克力跑台,下方架相机仰拍时空参数全拿到、可长时间记录、非侵入反光与足垫接触判定是难点;设备成本高全部时空参数、接触面积
侧视高速摄像侧面拍摄四肢摆动能看关节角度、抬腿高度左右肢在投影上重叠,几乎无法分辨摆动轨迹、抬腿高度

我的经验是:如果只是想筛一批动物的基本步态特征,墨迹法加上一套成熟的图像分析脚本,一两周就能出结果,性价比最高;要做时间分辨的支撑相、摆动相分析,必须上高速摄像,而且最好选下视视角,因为侧视在左右肢区分上几乎无解。有个折中方案我挺推荐——透明跑道下视拍摄配合双色标记(前肢一种颜色、后肢另一种颜色),分割和归属两个难点一次性解决大半,后面会讲具体怎么用。

1.3 为什么把分析放在 MATLAB 里

选 MATLAB 不是为了“显得专业”,而是几个很实际的原因。图像处理工具箱里的imbinarize、bwlabel、regionprops、bwmorph这套组合,配合Image Acquisition或直接VideoReader读帧,写出来的脚本短且可读;信号处理工具箱的sgolayfilt、findpeaks在事件检测上足够用;真正让我留下来的原因是统计端——fitlme处理重复测量数据几乎是一行代码的事,而步态数据天然是嵌套结构(步幅嵌套在肢体里、肢体嵌套在动物里、动物嵌套在组里),用普通方差分析会把自由度算高、p 值算小,这个坑后面单独说。

当然也有代价。MATLAB 处理长视频时内存吃紧,几百帧 1080p 的灰度图一次性载入就能把 16 GB 机器撑满;它的循环性能不如编译型语言,逐帧regionprops在大数据量下会明显变慢。我的处理办法是分块读帧、中间结果落盘为.mat或.csv,分析阶段只读参数表而不重跑图像。这套“图像阶段”与“统计阶段”分离的架构,后来在任何规模的项目里都没再改过。

2. 数据预处理与足印识别核心细节

2.1 帧率、分辨率与像素当量的确定

在写第一行代码之前,有两个数字必须定下来:帧率和像素当量。帧率决定你能不能抓到摆动相——小鼠正常行走时一个完整步态周期大约 200 到 300 ms,其中摆动相只占三分之一左右,也就是 70 到 100 ms。要以 ±10% 的精度定位摆动相的起止时刻,至少需要在这个窗口内采到 3 到 5 帧,换算下来帧率下限在 300 fps,我一般设 500 fps。低于这个数,测出来的支撑时长会被量化误差吃掉一大块,而且误差是系统性的——总是偏长,因为它会把相邻两帧的边界向外扩。

像素当量则决定空间参数的分辨能力。假设视野宽 20 cm、相机水平分辨率 1280 像素,那么每个像素对应 0.156 mm。小鼠后肢步宽大约 12 到 18 mm,换算成像素是 77 到 115 像素,这个量级下测量误差可以接受;但足印面积小一些的爪(前肢足印往往只有后肢的三分之二)就只有几百个像素,边界噪声带来的面积误差可能到 5%。如果预算允许,把分辨率提到 1920 像素宽,像素当量降到 0.104 mm,面积误差会明显下降。这个权衡要提前算,事后没办法补。

标定这一步千万别省。我的做法是在每次采集开始前,把一把已知长度的金属标尺放在跑道同一焦平面上拍一张,然后:

% 标定:在标定图上量取 100 mm 对应多少像素 calImg = imread('calib_100mm.png'); calGray = im2gray(calImg); calMask = imbinarize(calGray, 'adaptive', 'ForegroundPolarity','dark'); calProps = regionprops(calMask, 'BoundingBox'); % 取最长的那条连通域作为标尺 [~, idx] = max(cellfun(@(b) b(3), {calProps.BoundingBox})); rulerPx = calProps(idx).BoundingBox(3); pxPerMM = rulerPx / 100; % 像素/毫米 mmPerPx = 1 / pxPerMM;

这里有个细节:标尺必须和跑道面在同一高度。如果用的是玻璃跑台、相机在下方仰拍,标尺要贴在玻璃底面而不是放在玻璃上面,否则折射会带来 1% 到 2% 的系统偏差,看着不起眼,但所有空间参数会被同一个系数整体缩放,跨批次比较时就露馅了。

2.2 背景建模与足印分割

下视拍摄的画面里,背景是均匀的跑台底色,前景是深色的爪印。最简单的做法是固定阈值,但跑台用久了会有划痕、动物身上会掉毛、爪底沾的碎屑会留在玻璃上,固定阈值很快就会失效。我平时用背景差分加自适应阈值的组合:

%% 背景建模:取 200 帧的中位数作为背景(动物走过的位置被中位数抹掉) vid = VideoReader('run_01.mp4'); nFrames = floor(vid.Duration * vid.FrameRate); bgStack = zeros(vid.Height, vid.Width, 200, 'uint8'); for k = 1:200 bgStack(:,:,k) = im2gray(readFrame(vid)); end bg = median(bgStack, 3); % 逐像素中位数 %% 逐帧分割 vid.CurrentTime = 0; fgMask = false(vid.Height, vid.Width, nFrames); for k = 1:nFrames gray = im2gray(readFrame(vid)); diffImg = imabsdiff(gray, bg); % 自适应阈值,抗背景亮度漂移 level = graythresh(diffImg); m = imbinarize(diffImg, max(level, 0.10)); % 形态学清理:先开后闭,去掉孤立噪点并补上爪内部的孔洞 m = bwareaopen(m, 40); % 去掉小于 40 像素的碎片 m = imopen(m, strel('disk', 1)); % 断开身体与爪之间的细连接 m = imclose(m, strel('disk', 2)); % 补足爪印内部的空洞 fgMask(:,:,k) = m; end

imopen那个步骤用的是 1 像素半径的圆盘,作用不是去噪,而是切断身体轮廓与爪子之间那一两条像素宽的连接。这一步很关键:小鼠腹部离跑台很近,俯拍时身体边缘常和爪印连成一个大连通域,如果不切断,后面的连通域分析会把整只动物当成一个“巨大足印”,面积过滤直接把它扔掉,结果就是整段数据静默丢失——没有任何报错,只是爪子少了。我吃过这个亏,后来养成习惯:分割完先抽几帧画出来肉眼看,确认爪子和身体是分开的再往下跑。

bwareaopen的面积阈值 40 像素也不是随手写的。按前面的 0.156 mm/像素算,40 像素大约是 1 mm²,比最小的前肢足印边缘碎片略大,比正常足印小很多。如果你的分辨率不同,这个阈值要按比例缩放,经验公式是面积阈值 ≈ 1 mm^2 × (pxPerMM)^2。

2.3 拖影、反光与接触判定的处理

曝光时间没调好是最常见的图像问题。相机帧率高、进光量不足,就得拉长曝光,结果运动中的爪子在画面上拖出一条尾巴,连通域的长宽比变得离谱,足印角度完全失真。判断方法很简单:找一个摆动相中段的帧,看爪子边界是否清晰。根治办法是提高照明强度、缩短曝光时间,而不是靠后期去掉拖影——网上流传的“用腐蚀把尾巴啃掉”的做法我不推荐,它同时也会把正常的足印边缘啃掉一圈,面积参数就系统性偏小了。

另一个麻烦是反光。玻璃跑台下方打光时,爪底的湿气会在玻璃上形成镜面反射,分割出来是爪印旁边一个亮斑,被阈值判成前景。实用解法有两个:一是在光源和跑台之间加一层漫射板,把点光源变成面光源;二是在镜头前加偏振片并调整光源偏振方向,把镜面反射压下去。这两个都是物理手段,比在代码里加一堆过滤条件省事得多。如果已经拍完了只能补救,可以在regionprops之后按“面积/外接矩形面积”的实心度过滤,镜面反射斑的实心度通常接近 1 且面积小,正常足印的实心度在 0.6 到 0.85 之间。

接触判定还有个容易忽略的点:爪掌在落地瞬间是从后缘向前铺开的,前一帧只有足跟接触,后一帧整个爪掌都贴上了玻璃。如果直接用二值掩膜的像素计数做阈值判断,落地时刻会判定得偏晚、离地时刻判定得偏早,支撑相被系统性压缩。我一般用“接触面积曲线”本身来做事件检测,而不是对每一帧做硬判定——把每只爪子逐帧的像素面积画成一条时间曲线,曲线上升沿的中点是接地时刻,下降沿中点是离地时刻。这样得到的时刻精度可以达到亚帧级别(用线性插值补),比逐帧硬阈值好得多。

2.4 前肢后肢、左侧右侧的归属判定

这是整个流程里最容易出错的一环。足印分出来了,但一堆斑点里哪个是左前、哪个是右后,机器并不知道。有三条思路,可靠性依次递增:

思路一:几何规则。小鼠正常行走时,同侧的后肢足印落在前肢足印的前方(注意是前方,因为四足动物的后肢跨步时会越过同侧前肢的落点),左右两侧的足印沿行进方向形成两条大致平行的链。可以先拟合躯干中线,再按足印到中线的有符号距离分左右,按前后顺序配对。这个方法在墨迹法数据上表现不错,但依赖“同侧前后肢配对”这条规则,一旦动物出现拖步或者不走直线就会失效。

思路二:尺寸与形状特征。后肢足印通常比前肢大 20% 到 40%,形状更狭长,趾印更分散;前肢足印小而圆。用面积和长宽比做二分类,配合一个简单的阈值或者训练好的分类器,准确率能到 90% 以上。但要注意,不同品系、不同体重的小鼠尺寸差异很大,阈值必须按批次自己标定,不能拿别人的数字直接用。

思路三:双色标记。前肢涂一种颜色、后肢涂另一种,分割时直接在色相通道上分开。这是最可靠的做法,代价是需要给动物上色、每次实验前多一个保定步骤,而且颜料必须无毒、易清洗、不干扰动物正常行走。我做过对比,双色方案把归属错误率从 8% 左右压到 1% 以下,如果你要做的正是前后肢协调性分析,这个投入完全值得。

左右侧的判定相对简单一些,但也有坑。有些动物会贴着跑道一侧走,躯干中线本身就偏,用全局中线分左右会在边缘处误判。稳妥的做法是逐帧估计躯干位置(用所有足印的质心或者身体掩膜的主轴),用局部中线做划分。

3. 参数计算的完整实现

3.1 步态事件检测:把足印序列变成周期

有了每只爪子逐帧的接触状态,接下来的任务是把连续的时间序列切成一个个步态周期。核心是找“接地时刻”和“离地时刻”。我通常先对接触面积曲线做平滑,再用导数符号变化定位边沿:

%% pawArea:1×nFrames 的接触面积序列(单位:像素) fs = 500; % 采样率,等于视频帧率 win = 2*round(fs/100)+1; % 约 20 ms 的窗,必须是奇数 areaSm = sgolayfilt(double(pawArea), 2, win); % 2 阶多项式平滑 % 归一化到 0~1,便于跨个体比较 areaNorm = (areaSm - min(areaSm)) / (max(areaSm) - min(areaSm) + eps); % 以 0.3 为阈值找上升沿与下降沿,并线性插值到亚帧精度 thr = 0.3; above = areaNorm > thr; d = diff([0 above 0]); onIdx = find(d == 1); offIdx = find(d == -1) - 1; % 亚帧插值 onT = interpCrossing(areaNorm, thr, onIdx, fs, 'rising'); offT = interpCrossing(areaNorm, thr, offIdx, fs, 'falling');

平滑窗口的选择有个取舍:窗口太短,噪声会让同一只爪子在一个支撑相内被检出多次;窗口太长,摆动相的窄峰会被抹平,支撑时长被高估。20 ms 对应 10 帧,是我在 500 fps 下试出来的平衡点,差不多是一个摆动相时长的四分之一。如果用 250 fps 采集,这个窗口要相应缩到 5 帧,否则会过度平滑。

阈值 0.3 也不是固定的。它是相对该爪子自身最大接触面积的比例,好处是自动适应不同大小的爪子。但如果某只爪子在整段视频里始终没有完全落地(比如动物有痛感、避免负重),它的最大值本身就偏小,归一化后仍然会跨过阈值,导致“假阳性支撑相”。这种情况要靠人工复核:把每只爪子的原始面积曲线画出来,看峰值是否明显低于其他爪子。负重回避本身就是重要的行为学发现,不能当成数据缺陷扔掉。

3.2 时空参数的计算公式与代码实现

有了每只爪子的接地/离地时刻序列,大部分参数都能直接算。为了说清楚,我用一组具体的数字走一遍。假设某只小鼠右后肢在 500 fps 下记录到连续三次接地时刻为第 100、225、350 帧,对应 0.200 s、0.450 s、0.700 s,每次支撑持续 90 帧(0.180 s)。

  • 步态周期(cycle duration):相邻两次接地的间隔,0.450 − 0.200 = 0.250 s。
  • 步频(cadence):周期倒数,1 / 0.250 = 4.0 步/秒,也常写成 240 步/分钟。
  • 支撑相时长(stance duration):0.180 s。
  • 摆动相时长(swing duration):周期减支撑,0.250 − 0.180 = 0.070 s。
  • 占空比(duty factor):支撑相 / 周期 = 0.180 / 0.250 = 0.72。这个值大于 0.5 说明同一时刻至少有一只爪子在地上,属于典型的 walk;如果接近 0.5,说明有腾空相,趋向 trot。
  • 步幅(stride length):同一只爪连续两次接地时质心在行进方向上的位移。若两次质心坐标的纵坐标分别为 320 像素和 178 像素,差值 142 像素,按 0.156 mm/像素换算得 22.2 mm。
  • 步长(step length):同一侧前后肢,或左右同名肢相邻落点之间的纵向距离,取决于你采用的配对定义,论文里一定要写清楚用的是哪一种,否则跨研究没法比。
  • 步宽(base of support):左右后肢同时支撑时,两个足印质心的横向距离。取所有“双侧同时接地”的帧求平均,比取单个步幅更可靠。

对应的代码:

function param = calcGaitParams(onT, offT, centroidXY, mmPerPx) % onT, offT: 接地/离地时刻(秒),等长 % centroidXY: n×2,每个足印的质心坐标(像素) cycle = diff(onT); % 周期序列 stance = offT - onT; % 支撑相 swing = cycle - stance(1:end-1); % 摆动相 param.cycleMean = mean(cycle); param.cadence = 1 / param.cycleMean; % 步/秒 param.stanceMean = mean(stance); param.swingMean = mean(swing); param.dutyFactor = param.stanceMean / param.cycleMean; % 步幅:连续足印的欧氏距离乘以像素当量 d = hypot(diff(centroidXY(:,1)), diff(centroidXY(:,2))); param.strideMm = mean(d) * mmPerPx; % 变异系数,反映步态规整程度 param.cycleCV = std(cycle) / mean(cycle); param.strideCV = std(d) / mean(d); end

有个参数值得多说一句:变异系数(CV)。步态参数的平均值变化往往不明显,但节律的规整程度会先变差。我在几个模型里都观察到,处理组和对照组的平均步长差异不到 5%,但周期 CV 从 0.08 升到 0.20,肉眼都能看出走路节奏乱了。所以别只报均值,把 CV 一起报上,信息量更大。

3.3 足印形态参数:角度、面积与趾间距

形态参数对分辨率最敏感,也最容易被忽略。足印角度(paw angle)定义为足印长轴与行进方向的夹角,正常后肢在 10° 到 25° 之间外展。计算方法是先对单个足印做主轴分析:

props = regionprops(mask, 'Orientation','MajorAxisLength',... 'MinorAxisLength','Area','Centroid','Eccentricity'); % Orientation 是与水平方向夹角,需减去行进方向角 pawAngle = props.Orientation - headingAngle; % 归一到 [-90, 90] pawAngle = mod(pawAngle + 90, 180) - 90;

regionprops的Orientation对形状很敏感,爪子边缘只要多出一个小凸起,角度就能跳十几度。我的做法是先做一次imclose加imfill,再算主轴;如果同一只爪子的角度在相邻步幅间跳动超过 15°,基本可以判定是分割质量问题,需要回头查阈值。

足印面积反映接触与负重情况,但要注意它和“最大接触面积”不是一回事。单帧面积只是瞬时值,真正有意义的是整个支撑相内的最大值,以及面积上升的斜率(反映落爪速度)。趾间距(toe spread)需要分辨率支撑:小鼠脚趾间距在 2 到 4 mm 量级,按 0.1 mm/像素算只有 20 到 40 像素,边缘检测误差就能吃掉 10%。如果这不是你的核心指标,我建议直接放弃,把精力放在时空参数上,性价比高得多。

3.4 协调性指标与对称性指数

单肢参数只能说明“这条腿怎么样”,协调性才能说明“几条腿配合得怎么样”。最常用的是相位差(phase dispersion):把同侧前肢和同侧后肢的接地时刻都换算成相对于该侧后肢周期的相位(0 到 1 之间),再求差值的均值。正常行走时,同侧前后肢的相位差接近 0.5(交替落地);如果接近 0 或 1,说明同侧前后肢同时落地,属于异常模式。

% 同侧前后肢相位差 phaseHind = mod((onT_hind - onT_hind(1)) / meanCycleHind, 1); phaseFore = mod((onT_fore - onT_fore(1)) / meanCycleFore, 1); % 对每个前肢落地时刻,找相位最接近的后肢周期 phaseDiff = min(abs(phaseFore' - phaseHind), [], 2);

对称性指数更简单,直接用左右同名肢的参数对比:

symIdx = abs(L - R) ./ ((L + R)/2); % 0 表示完全对称

这个指标的好处是无量纲,可以跨参数、跨个体比较。经验上,健康小鼠后肢步长的对称性指数在 0.05 以下,超过 0.15 就值得警惕,但要注意这只是一条参考线,不同品系、不同年龄段差别不小,最好在本实验室内部先建立自己的基线。

3.5 平滑、统计模型与结果输出

原始参数序列一定要平滑,但平滑的对象要想清楚。我的原则是:时刻序列不平滑,派生参数才平滑。接地时刻是事件的本质,平滑它会人为制造出并不存在的精度;而周期序列在求均值前做一次中位数滤波(去掉个别异常步幅)是合理的,比如周期突然变成均值的 2 倍,多半是动物停了一下或者检测漏掉一次落地,这种点必须剔除,不能进入统计。

统计端是很多人的短板。步态数据的嵌套结构意味着同一只动物的多个步幅并不独立,如果直接把几百个步幅当成独立样本做 t 检验,自由度会被严重高估。正确做法是用线性混合效应模型,把动物作为随机截距:

% data 表包含:stride, cycle, duty, group, animalID, speed lme = fitlme(data, 'stride ~ group + speed + (1|animalID)'); disp(lme);

把速度作为协变量放进模型是必须的,它解释了很大一部分方差,不放进去组间差异很容易被速度差异掩盖。如果你的设计是强迫固定速度,速度项可以去掉,但仍建议保留一个速度校验列,确认所有动物实际走的速度确实一致——跑带速度不等于动物实际步速,这个偏差在小鼠身上可以到 20%。

4. 常见问题排查与避坑实录

4.1 足印识别类问题速查

下面这张表是我这几年遇到问题后攒下来的,按现象查原因最快:

现象最可能的原因排查与处理
整段数据里爪子数量明显偏少足印与身体轮廓连成大连通域,被面积过滤掉加大imopen半径,或先分割身体再擦除
同一只爪在一个支撑相里被检出多次平滑窗口过短,接触面积曲线有抖动窗口加长到摆动相时长的四分之一左右
足印角度跳变超过 15°边缘小凸起干扰主轴计算先imclose再imfill,或改用椭圆拟合
前后肢识别准确率忽高忽低尺寸阈值是按别的批次定的每批重新统计面积分布,重新设阈值
支撑相普遍偏短用了逐帧硬阈值,没有做亚帧插值改用以面积曲线过阈值的方式定位事件
摆动相完全测不出来帧率低于 300 fps提高帧率重新采集,软件补不回来

4.2 三个我踩过的坑

坑一:跑道速度和动物步速不是一回事。我早期做过一批跑带实验,设定 20 cm/s,跑完发现不同动物的步频差异极大。后来在跑带上方加了标记线做人工帧计数,才意识到有些动物其实是“滑步”——爪子在地上被跑带带着走,支撑相被拉长,步频被迫降低。判断方法是看单个爪子在支撑相内的质心位移:如果是负值(向后移动),说明存在滑步。改进办法是给跑带加粗糙涂层,或者干脆改用自由通道加事后速度分层。

坑二:习惯化训练没做够。小鼠第一次上跑台,紧张状态下步态参数会全线偏离,步长缩短、周期变长、CV 变大,看起来像是“运动功能障碍”,其实只是应激。我的经验是至少连续训练三天,每天让它在跑道上自由走 5 到 10 分钟,等到它的行进速度和路径长度趋于日常水平再开始正式采集。更严格的做法是把训练期数据也记下来,看第几次开始参数不再漂移,用数据决定开始采集的时间点。

坑三:识别异常值只看参数不看原始帧。有个批次的步长均值比对照组短了 12%,看着很显著。我逐个回看才发现,实验组有两只动物的嗅探行为特别多,走走停停,导致周期序列里出现了很多 2 倍周期的点。把这些点剔除后差异从 12% 降到 3%。这件事之后我定了一条规矩:任何超过 2 倍标准差的参数值,必须回过头去看它对应的原始帧,确认是动物行为还是算法错误,两边都不能想当然。

4.3 尺度与光学的系统性偏差

有些误差不出现在单次测量里,而是以系统偏差的形式藏在全部数据中。最典型的是标定平面与跑道平面不重合——前面提过,玻璃跑台仰拍时标尺贴在玻璃上面而不是下面,所有空间参数会整体缩小 1% 到 3%。另一种是镜头畸变:广角镜头在画面边缘的放大率与中心不同,如果动物习惯走跑道一侧,那边的尺寸就系统性偏大。解决办法有二,要么用远心镜头或长焦镜头减小畸变,要么做一次棋盘格标定,用径向畸变系数对坐标做校正。

还有一种隐蔽的偏差来自透视。相机没有严格垂直于跑道平面时,画面不同位置的像素当量不同。判断方法很简单:把标尺放在视野的左、中、右三个位置各拍一张,如果测得的像素长度相差超过 1%,说明机位不正,需要重新调整。这五分钟的检查能省掉后面几周的返工。

5. 实验设计与结果解读的实操经验

5.1 样本量、重复次数与数据取舍

步态参数的个体间变异往往大于组间差异,这是行为学数据的固有特点。我的经验值是每个实验组至少 10 到 12 只动物,每只动物至少取 3 个完整步态序列(每个序列包含 4 到 6 个连续步幅),这样既保证了个体内平均的可靠性,又不至于让采集时间过长导致动物疲劳。如果一个序列里超过两个步幅被判定为异常,我倾向整段丢弃而不是修补,因为修补过程本身带有主观性。

还有一个容易忽略的问题:多只动物在同一天采集,如果第一只在跑道里留下了气味,后面的动物行为会受影响。所以每只动物采完都要清洁跑道,并且不同组之间穿插着采,避免“上午全是对照组、下午全是实验组”这种时间混杂。时间效应在行为学实验里是真实存在的,尤其在昼夜节律敏感的小鼠上。

5.2 结果怎么读才不误判

拿到一张参数表,不要急着看组间 p 值。先看几个内部一致性检查:占空比应该落在 0.6 到 0.75 之间(行走状态),步频和速度之间应该正相关,左右对称性指数应该小于 0.15。如果这些基本关系都不成立,说明数据本身有问题,统计再漂亮也是假的。

解读时要区分“能力下降”和“策略改变”。步长变短可以是力量下降(真的走不远),也可以是动物主动降低速度以求稳定(考虑到平衡)。区分这两个最直接的办法是看速度归一化后的步长——如果按速度分层后差异消失,那多半是策略改变;如果分层后差异依然存在,才更可能是能力受损。这种分层分析在 MATLAB 里用groupsummary或者把速度分箱后做交互项都非常方便。

另外提醒一句,任何涉及活体动物的操作都要在开始前走完机构内的伦理审查流程,并遵守动物福利相关要求,包括麻醉、保定时长、训练强度、人道终点这些细节。这些不是形式,它们直接影响数据质量——一只长期处于应激状态的动物,测出来的步态参数没有参考价值。

5.3 流程的可复用化组织

最后说说工程层面。这套流程跑通之后,一定要把它整理成可复用的结构,否则每来一批新数据就得重写一次。我的目录组织是这样的:每只动物一个文件夹,里面放原始视频、标定图、分割后的掩膜缓存、以及两个 CSV(足印明细表和步幅参数表);顶层放一个runAll.m,遍历所有文件夹批量执行,输出一张汇总表。函数层面拆成五个:calibrate、segment、attribute(归属判定)、calcParams、report,每个只做一件事。这样拆的好处是,当某一批数据的背景特别脏时,我只需要换掉segment里的阈值策略,其他四个函数原封不动。

参数配置我建议单独写成一个结构体文件,把所有阈值集中在一处:

cfg.pxPerMM = 6.4; cfg.minAreaPx = 1 * cfg.pxPerMM^2; % 约 1 mm^2 cfg.smoothWin = 2*round(cfg.frameRate/100)+1; cfg.onThreshold = 0.30; cfg.maxCycleCV = 0.25; % 超过则标记为异常序列

这样换人接手时,不用读几千行代码就能知道所有可调参数在哪。我在项目交接时最深的体会是:一套流程的价值不在于它跑得多快,而在于别人拿到它之后能不能在没有你的情况下改对参数。

个人体会是,小鼠步态分析最难的部分从来不是写代码,而是想清楚每一个数字背后的生理含义——为什么这只动物的占空比突然从 0.68 掉到 0.55,为什么它某一侧的步宽变窄了,为什么周期 CV 在给药后第三天开始升高。算法能给你数字,但把数字和行为对上,还得靠一帧一帧地回看原始录像。我现在每批数据分析完,都会随机抽两只动物,把它们的完整视频和参数曲线并排放在一起看一遍,这个习惯帮我抓到过好几次算法层面的系统性问题。如果你打算长期做这套流程,建议也把这个复核环节固定下来,它是整条链路上最后一道、也是最可靠的一道校验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询