如何提高 Python 数组操作性能.

2022-09-23 16:02:57 +08:00
 jeeyong
最近正在写一个小系统. 涉及到把 CT 影像转换成 PNG.
CT 影像不是不是标准通用格式, 所以需要从读取 bytes 再转成数组..
这个过程中需要两次处理数组数据, 但是性能很慢..有相关经验的朋友可以给点建议嘛?
以下是详述:

我读出来的 bytes 数据处理成 uint8 后. 是这样的形式:
[208, 4, 208, 4, 208, 4...196, 8]
不懂图像处理的知识, 我的理解就是, 一个灰度, 一个 Alpha 通道值(透明值?).
第一次处理数组是要把上面的数组, 改成如下形式: <- 暂且叫 生成数组阶段.
[208, 208, 208, 4, 208, 208, 208, 4.......]
就是把 208 这个灰度值变成 rgb 的形式.
然后再通过一个循环变成 pillow 支持的格式, 如下: <- 暂且叫 数组转换阶段吧.
[
[ [r, g, b, a], [r, g, b, a], [r, g, b, a], ],
[ [r, g, b, a], [r, g, b, a], [r, g, b, a], ],
[ [r, g, b, a], [r, g, b, a], [r, g, b, a], ],
]

然后这个数组要通过 numpy 转换成 uint8 类型才能提交给 pillow 或者类似图片处理的库..

所以大致耗时分为三个阶段:
1. 生成数组
2. 转换数组
3. 通过 numpy 转换数组类型为 uint8.

数组大小为: 5022 * 4200 * 4 = 84,369,600

环境 A 介绍:
Win10, 10900K, 64GB 3600, NVME 2TB SSD
Python 3.10.4, pillow 最新版
阶段一耗时: 9 秒左右
阶段二耗时: 45 秒左右
阶段三耗时: 5 秒左右

觉得太慢, 尝试全部改成 numpy 操作.
但是阶段二慢的出奇, 后来查资料发现, numpy 在大量数组下标赋值操作的性能还不如 Python 原生 list.
阶段二, 上厕所, 洗把脸回来还没跑完, 就直接放弃了. 也尝试过 np.insert 操作, 一样慢.

后来尝试环境 B, 基于 pypy 的.
pypy3.8, numpy, pillow, 同样的硬件.
第一阶段耗时: 0.2 秒
第二阶段耗时: 9.7 秒
第三阶段耗时: 77 秒
查阅资料得知, pypy 的 C 扩展接口性能很差, 不如原生 python.
而 Numpy 就是 C 扩展的库..所以导致 Numpy 性能急剧下降.

好了..剩下的办法超出我的知识范畴了..
有朋友能分享一下经验或者给个可能的方向我去看也行.

需要转换的数据量大约有 23 万个 CT 文件. 按照一个文件 1 分钟, 我即便 4 个进程跑(文件转换的服务器 4 核 8GB 内存, 还要跑一些其他服务.), 5 万分钟 / 60 分钟 / 24 小时 一点意外没有的情况要跑 42 天...

顺便吐槽以下, 朋友给写了个 nodejs 的版本, 转换一个图 2.3 秒, WTF!!
5057 次点击
所在节点    Python
70 条回复
hsfzxjy
2022-09-23 16:34:02 +08:00
@hsfzxjy #19 修正 np.stack([...], axis=-1)
jeeyong
2022-09-23 16:34:15 +08:00
@dlsflh
我是直接写个 tensor 扔给 torch 跑,我知道它会自动用 SIMD 或者 CUDA (如果启动 gpu 的话)

@lmshl 这大哥的做法解释了你的疑问..
jeeyong
2022-09-23 16:35:27 +08:00
各位的代码, 够我消化一夜了....
太感谢了...太感谢了!
@hsfzxjy
@yoohwzy
@stein42
wcsjtu
2022-09-23 16:38:33 +08:00
应该是代码里出现了大量的 for 循环,以及大量的__getitem__/__setitem__操作才慢的。numpy.ndarray 的随机读取性能确实不如 builtins.list 。因为`ndarray[i]` 需要 new 一个 PyLongObject 出来,而`builtins.list[i]`只需要 refcnt++。

楼主这个问题, 如果用 numpy 的话, 就得摆脱面向过程的思想, 用函数式来做。numpy 的 broadcast 机制应该能实现楼主想要的功能。需要稍微学习一下。

如果不想用 numpy 的话, 只能用预编译或者 jit 方案来加速了。 既然楼主已经试过 numba 了, 我推荐另一个工具 pythran. 性能与 numba 差不多, 但是比 numba 好用
jeeyong
2022-09-23 16:40:34 +08:00
@wcsjtu 我先尝试 numpy 的方案....再看预编译或者 jit 类的...
谢谢~
lmshl
2022-09-23 16:43:13 +08:00
前段时间处理过 DICOM 格式,不过我是转换为 Voxel ,量也不大不需要注重性能。

但我不精通 numpy ,理论上 numpy 应该不太能把常用 operator 解释为向量指令集,如果是简单求和之类的操作还好。
stein42
2022-09-23 16:47:40 +08:00
@jeeyong
这里是举例,实际应该从文件读取。
buffer = open(name, 'rb').read()

或者直接用 np.fromfile

思路很直接:
先得到 numpy 的数组。
再提取灰度和 alpha 的数组。
再拼接成二维数组,灰度重复 3 次。
最后 reshape 成需要的形状。
vicalloy
2022-09-23 17:10:21 +08:00
用 numpy 就要全程使用 numpy ,不然数据处理过程中会大量构造和销毁 python 对象,速度会很慢。
newmlp
2022-09-23 17:20:34 +08:00
直接 cpp 一把梭。。。用什么 python
faterazer
2022-09-23 17:23:08 +08:00
听你的描述,用 numpy 去搞,应该不会有什么性能问题呀,最后的数据维度应该是( 4 通道数 * 高 * 宽),能否给一两个具体的 case 呢?就像 LeetCode 示例那样(数据不敏感的话可以放出一部分)。不过有几点要注意一下:

- 使用 numpy ,就多用向量化编程,避免 for 循环之类的( numpy 做了向量操作优化,底层是经过优化的计算库,不用担心性能问题)。
- 如果真的要频繁的修改,建议先在 Python 的 list 对象上修改好,再转成 numpy
faterazer
2022-09-23 17:26:10 +08:00
@newmlp numpy 可不代表 python 哦
Cy86
2022-09-23 18:26:31 +08:00
楼主方便给一张 CT 影像么, 这样大伙可以本地跑测下
CamD
2022-09-23 18:37:43 +08:00
numpy ,或者 torch 移到 gpu 里操作。
jdhao
2022-09-23 19:45:35 +08:00
直接给个样例文件,让网友帮你看看,你这么说不好定位问题
chashao
2022-09-23 19:47:29 +08:00
求楼主发个 CT 影像,我试试 c++
iloveios
2022-09-23 19:53:19 +08:00
php 才是王道
Cy86
2022-09-23 19:56:27 +08:00
from numpy import maximum,uint8
from pydicom import dcmread
from PIL.Image import fromarray
from time import perf_counter

def convert_dcm_jpg():
im = dcmread('test.DCM')
rescaled_image = im.pixel_array.astype(float)
# rescaled_image = (maximum(rescaled_image,0)/rescaled_image.max())*255 # float pixels
final_image = uint8(rescaled_image) # integers pixels
final_image = fromarray(final_image)
return final_image
if __name__ == '__main__':
t = perf_counter()
image = convert_dcm_jpg()
image.save('test1.jpg')
print(F'coast:{perf_counter() - t:.8f}s')
Cy86
2022-09-23 19:57:40 +08:00
from numpy import maximum,uint8
from pydicom import dcmread
from PIL.Image import fromarray
from time import perf_counter

def convert_dcm_jpg():
im = dcmread('test.DCM')
rescaled_image = im.pixel_array.astype(float)
# rescaled_image = (maximum(rescaled_image,0)/rescaled_image.max())*255 # float pixels
final_image = uint8(rescaled_image) # integers pixels
final_image = fromarray(final_image)
return final_image
if __name__ == '__main__':
t = perf_counter()
image = convert_dcm_jpg()
image.save('test1.jpg')
print(F'用时:{perf_counter() - t:.8f}s')

# 0.01024350s
Cy86
2022-09-23 19:58:21 +08:00
paopjian
2022-09-23 20:13:38 +08:00
有考虑提供一张图片和原始代码?这样可能做优化更快

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/882441

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX