Python生物医学专业案例 – 细胞计数

在上公共的编程基础课时,我们经常受到学生的质疑: 我们学这玩艺儿有什么用? 学生的疑问来自于“他没有从课程中得到通过程序设计来解决本专业问题的体验”。重庆大学的教学团队设计了很多与各专业紧密相关的程序设计案例,我们会陆续分享出来,供大家参考。

本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载,但需要注明原作者”海洋饼干叔
叔”;本文不允许以纸质及电子出版为目的进行抄摘或改编。
1.《Python编程基础及应用》,陈波,刘慧君,高等教育出版社。免费授课视频 Python编程基础及应用
2.《Python编程基础及应用实验教程》, 陈波,熊心志,张全和,刘慧君,赵恒军,高等教育出版社Python编程基础及应用实验教程
3. 《简明C及C++语言教程》,陈波,待出版书稿。免费授课视频

(在原始案例中,代码中间留空让学生填写,这里的代码是完整的)

一幅细胞照片经过二值化以后可以视为像素值为0或1的矩阵,如图21-1所示。在该矩阵中,值为1为元素表示该处是细胞或细胞的一部分,该元素的上、下、左、右的相邻元素如果也是1,则相邻元素与该元素位于同一个细胞内;矩阵中值为0的元素表示该处无细胞。识别并统计显微镜下一幅细胞照片中的细胞数量,是血液常规检查的最基本任务之一。

对于图21-1所示的细胞照片,按上述规则,容易数出该幅照片中包含7个细胞。注意,第3行第3列是一个孤立细胞(图中已用底纹区分),它与第2行第5列的细胞并非同一个,因为它位于第2行第4列元素的左下方,而不是上下左右的位置。
图21-1 细胞计数示例
在本实验对应的实验子目录中有一名为cellpicture.txt的文本文件,其内容为细胞照片的二值化矩阵。请编写程序,从该文件读取矩阵内容并统计该矩阵中的细胞数量。

该文件的内容如下:

12 14
10111000011100
01100110001101
00000111000011
00110000001000
00011000111000
00111100010011
10011101100111
11000100000001
00000000011000
00000000000000
10001100110000
10001000011111

其中,第1行的1214以空格分隔,表示该矩阵有12行14列。接下来,则是12行元素数据,每行14个0/1字符。

21.1 从文件读入二值化矩阵

下述程序负责从文件cellpicture.txt中读取二值化矩阵,并将其组织在嵌套列表d中。请将程序补充完整并运行,确保矩阵d的内容与文件一致。注意,矩阵元素的类型应为整数。

d = []
with open('cellpicture.txt') as f:
    m,n = map(int,f.readline().split())
    for x in range(m):
        r = list(map(int, f.readline()[:n]))
        d.append(r)

for x in d:
    print(x)

【解题提示】

  • f.readline( )从文本文件f中读取一行内容,其返回值为一个字符串,注意该字符串包括了每行末尾的换行符。
  • map(func,iterable)称之为映射函数,该函数逐一将可迭代对象iterable中的元素作为参数提供给由func指定的函数来执行,映射出一系列的新元素。map()函数返回一个类型为map的可迭代对象,可以通过list()函数将其转换成列表。下述代码中的map()函数,将列表元素1,3,5,7逐一交给square()函数运行产生其平方数,再借助于list()函数转换得到包含4个平方数的结果列表。
def square(x):
    return x*x 
print(list(map(square,[1,3,5,7])))

上述代码的执行结果为:

[1, 9, 25, 49]

21.2图的宽度优先遍历

细胞的计数依赖于对矩阵元素的遍历。在逐行逐列遍历矩阵元素的过程中,发现一个值为1的元素,则意味着新发现了一个细胞。此时,需要从该元素出发,往多个方向搜索,找出该元素归属细胞所包含的全部像素/元素,并将这些像素/元素标记为“已探索”,以避免在后续遍历过程中,这些像素被错误地认为属于一个“新细胞”。

这个任务可以通过一个称为“图的宽度优先遍历”的算法来解决,该算法可用函数explorePixel()来描述。其中,d为代表二值矩阵的嵌套列表,该矩阵有m行n列,搜索出发点为i行j列。请在阅读完解题提示后,将下述代码补充完整。

def explorePixel(d,m,n,i,j):
        q = [(i,j)]
        while q:
            x,y = q.pop(0)
            if d[x][y] < 0:
                continue 
            d[x][y] = 0-d[x][y]
            if x>0 and d[x-1][y]>0:
                q.append((x-1,y))
            if x<(m-1) and d[x+1][y]>0:
                q.append((x+1,y))
            if y>0 and d[x][y-1]>0:
                q.append((x,y-1))
            if y<(n-1) and d[x][y+1]>0:
                q.append((x,y+1))

【解题提示】

  • explorePixel( )函数的任务可以描述为:从位于i行j列的像素出发,向其周边进行搜索,将与该像素有连接关系的其他像素全部找出,并标记为“已探索”。

  • 当像素值=0时,该像素不属于细胞;像素值=1时,该像素属于细胞且“待探索”,像素值=-1时,该像素属于细胞且“已探索”。

  • 列表q的行为表现为一个先进先出队列,其中存储那些已发现,但其自身及其相邻像素尚未被探索的像素。最初,q仅包含像素(i, j)。然后,程序将一直循环,直至队列空为止,每次循环的过程如下:
    (1)从队列中取出第0个待探索的像素(x, y);
    (2)检查(x,y)是否为已探索,如是,continue进入下一轮循环;
    (3)将(x,y)标记为“已探索”,即将其值变更为 0– d[x][y];
    (4)按上、下、左、右的顺序检查(x,y)的相邻像素,如果相邻像素的值>0,说明该相邻像素也属于当前细胞,将其加入待探索像素队列q。

  • 函数中的x表示行坐标,y表示列坐标。即与平面坐标系的通常习惯有所不同,这里的x表示上下方向,y表示左右方向。

为帮助读者理解上述“图的宽度优先遍历”算法,我们手工模拟一遍explorePixel()函数的执行过程,从像素(0,4)出发。请见图21-2,为了与程序一致,我们的行号、列号改为从下标0开始。
图21-2 图的宽度优先遍历过程
当程序遍历矩阵元素到第0行第4列时,发现像素(0,4)值为1,即该像素状态为待探索且属于一个新细胞,执行explorePixel(d,m,n,0,4)从像素(0,4)出发搜索与该像素连接的全部细胞像素:
(1) 队列q被初始化为只包含像素(0,4),其值为:[ (0,4) ]。
(2) 像素(0,4)出队列,其值等于1待探索,像素(0,4)被赋值-1。
此时,q = 空列表。
(3) 像素(0,4)位于第1行,其上方元素不存在。
(4) 像素(0,4)的下方元素(1,4)的值为1,属于同一细胞,将(1,4)加入队列q。
此时,q = [ (1,4) ]。
(5) 像素(0,4)的左方和右方元素值为0,不属于细胞。
(6) 像素(1,4)出队列,其值等于1待探索,赋值-1。
此时,q = 空列表。
(7) 像素(1,4)的上方元素(0,4)此时值为-1已探索。
(8) 像素(1,4)的下方元素(2,4)值为1,属于同一细胞,加入队列q。
此时,q = [ (2,4) ]。
(9) 像素(1,4)的左方和右方元素值均为1,属于同一细胞,将左方元素(1,3)和右方元素(1,5)加入队列q。
此时,q = [ (2,4), (1,3), (1,5) ]。
(10) 像素(2,4)出队列,其值为1待探索,赋值-1。
此时,q = [ (1,3), (1,5) ]。
(11) 像素(2,4)的上方元素已探索,下方、左方、右方元素为0,故未发现新的待探索像素。
(12) 像素(1,3)出队列,其值为1待探索,赋值-1;(1,3)的右方元素已探索 ,上方、左方、下方均为0,故未发现新的待探索像素。注意,(2,2)位于(1,3)的左下方,并不属于题目定义的相邻像素。
此时,q = [ (1,5) ]。
(13) 像素(1,5)出队列,其值为待探索,赋值-1;(1,5)的左方元素已探索,其它三个方向均为0,未发现新的待探索元素。
(14) 此时,q = 空列表,循环结束。在循环过程中,(0,4)、(1,4)、(2,4)、(1,3)、(1,5)共5个像素被探索并标记为已探索,这5个像素构成了一个完整的细胞。

            if d[x][y] < 0:
                continue 

读者可能会上述两行代码感到疑惑:既然一个待探索元素在加入队列前其值确定为1即待探索,那么在该像素出队后,其值可能变为负数(已探索)吗?
图21-3 重复加入队列的待探索元素
我们考虑图21-3所展示的情况。当我们从(1,1)出发搜索该细胞的全部像素时,(2,2)作为像素(2,1)的相邻元素,在探索(2,1)时会被加入队列。同时,作为(1,2)的下方元素,在探索(1,2)时也会被加入队列。这样,队列中就会存在两个(2,2),当第1个(2,2)被取出时,其像素值为1未探索,而当第2个(2,2)被取出时,其像素值已经是-1已探索。此时,再去考虑(2,2)的相邻元素已不具实践意义,故略过。

21.3 循环遍历与搜索

在实现了函数explorePixel( )之后,下述程序逐行逐列地遍历全部矩阵元素,如果发现值>0的像素,说明遇到了一个属于细胞且“待探索”的元素,将细胞计数变量iCellCount加1,然后再调用explorePixel()函数从该像素出发探索整个细胞。由于explorePixel( )函数会将所有探索过的细胞像素置为已探索,所以当下述程序第2行、第3行的主循环遍历到已探索的细胞像素时,该像素值为-1,不会将其视为一个新细胞。

请将下述程序补充完整,并与前两小节的程序合并,运行,识别并统计cellpixel.txt文件中的细胞个数。

iCellCount = 0
for i in range(m):
    for j in range(n):
        if d[i][j] <= 0:
            continue
        iCellCount += 1
        explorePixel(d,m,n,i,j)

print(iCellCount)

为了帮助更多的年轻朋友们学好编程,作者在B站上开了两门免费的网课,一门零基础讲Python,一门零基础C和C++一起学,拿走不谢!

简洁的C及C++
由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造
Python编程基础及应用
由编程界擅长教书,教书界特能编程的海洋饼干叔叔打造
如果你觉得纸质书看起来更顺手,目前Python有两本,C和C++在出版过程中。

Python编程基础及应用

Python编程基础及应用实验教程

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年6月12日
下一篇 2023年6月12日

相关推荐