如何提高 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!!
5022 次点击
所在节点    Python
70 条回复
dlsflh
2022-09-23 16:09:32 +08:00
我不太懂技术,不过那么多深度学习医学影像相关的项目是怎么做的呢?是不是可以参考他们的方法?
hsfzxjy
2022-09-23 16:10:09 +08:00
建议直接从 bytes 读成一个 numpy array ,这段用 cython 来写,会快得很多,而且不是很麻烦
hsfzxjy
2022-09-23 16:15:42 +08:00
用 cython 的话,你相当于可以直接用类 python 的语法写个多重循环,把读出来的 byte 放到 array 的对应位置。不用去想什么向量操作,但编译出来会接近 C 的速度。一些越界检查什么的需要关掉以获得最佳性能
MoYi123
2022-09-23 16:17:28 +08:00
你的问题我估计肯定是能直接调 numpy 的 api 或者其他的一些图像处理库解决的, 但是大家都不一定了解你的 CT 影像具体的细节, 所以你要先说明一下慢的那一步你具体是怎么做的. 而不是一个简单的“数组转换阶段”就概括了.
jeeyong
2022-09-23 16:20:22 +08:00
@dlsflh 感谢回复..
我猜..他们只是读...并不涉及到数组大范围的修改, 或者大量的下标赋值操作吧?所以并没有那么慢..
哦对, 另外可能还直接调用了 GPU 做计算...而我目前不能这么做..服务器的显卡很垃圾.


@hsfzxjy cython....是不是不如直接 nodejs 搞一个了...研究以下打包发布, python 直接拉起一个 node 的进程后台转换..这个东西有开发周期的约束, 快到了..
lizytalk
2022-09-23 16:22:24 +08:00
可以试一下 numba?
jeeyong
2022-09-23 16:22:50 +08:00
@MoYi123
我读出来的 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], ],
]

我补充了点文字..你看一下能理解吗?
yoohwzy
2022-09-23 16:23:09 +08:00
不要用 for 循环来处理

```Python
w = 5022
h = 4200

img = np.asarray(img).reshape((w * h, -1))

rgba_img = np.stack((img[:, 0],
lookStupiToForce
2022-09-23 16:23:19 +08:00
看你的意思,是转换 5022*4200 像素的灰度图片,到 rgb 色彩空间?

往这方面搜搜,看有没有结果
www.google.com/search?q=python+图像+色彩空间+转换

上面第一条搜索结果 https://blog.csdn.net/hallobike/article/details/120385129 里,有“读入灰度图片”

中文搜不到也可以去搜英文
www.google.com/search?q=python+color+space+convert
jeeyong
2022-09-23 16:24:42 +08:00
@lizytalk 试过.. 仅限于 @jit 装饰. 一堆警告和报错.
主要是不支持一些方法. 包括不限于 numpy 的...
网上翻的教程, 所有的数组转 uint8 类型, 都是 numpy 直接干的...
其他我就不会了...有其他方法我也想试试.... 不用 numpy 转换, 或许可以直接上 pypy
hsfzxjy
2022-09-23 16:24:58 +08:00
@jeeyong Cython 不难的,花一个小时看看文档就能上手,写出来肯定比 nodejs 快,因为只涉及简单的内存存储。你用 nodejs 写还要考虑进程间通信,以及两个语言中格式的切换,这部分开销也不小。
threebr
2022-09-23 16:25:41 +08:00
不懂 numpy 为什么会慢。用 numpy 的话要用向量操作代替循环,也就是说你处理一张图片的时候(你的三个阶段)是不写 for 循环的,然后底层 numpy 可能是用 c 编译的库,可能直接用 Intel 或者 AMD 专门处理向量的二进制库,不应该比你自己拿 c 写更慢
jeeyong
2022-09-23 16:26:53 +08:00
@yoohwzy
@lookStupiToForce 感谢回复..先尝试 np.stack 的用法...
再看 google 结果...
对..如果不用转换, 直接读取生成, 是正确的思路..只是当时没找到方法, 就先硬上了..
先解决,再优化~
jeeyong
2022-09-23 16:28:58 +08:00
@hsfzxjy 是吗? 一直很怕上 C 相关的东西...哈哈
nodejs 可以直接读数据库获取路径, 去转换完了写回对应字段...
解决问题来说, 应该这个方法是最快的..
但是不服气啊...60 秒和 2.3 秒的差距...
stein42
2022-09-23 16:29:49 +08:00
直接用 numpy 就可以了,python 的循环太慢。
```
import numpy as np

width = 5022
height = 4200

# buffer 直接从文件读取
buffer = bytes([208, 4]) * (width * height)
assert len(buffer) == width * height * 2

b = np.frombuffer(buffer, dtype=np.uint8)
grey = b[::2][..., None]
alpha = b[1::2][..., None]
image = np.hstack((grey, grey, grey, alpha)).reshape((width, height, 4))
```
jeeyong
2022-09-23 16:30:08 +08:00
@threebr 我的理解是, python 要通过对应的接口和 numpy 通信, 而 numpy 本身对于下标赋值这种操作的过程是很复杂的.
这基本就是昨天看到的一篇文章的原话. 作者还列出了对应的处理过程, 因为是 C, 直接放弃阅读了..
yoohwzy
2022-09-23 16:30:32 +08:00
不要用 for 循环来处理

```Python
w = 5022
h = 4200
c = 4

img = np.asarray(img).reshape((w * h, -1))

# 转换 GA 图片到 RGBA
rgba_img = np.stack((img[:, 0], img[:, 0], img[:, 0], img[:, 1]), axis=1)
# 转换数据格式为 np.uint8 并生成 HWC 排列的图片
rgba_img = rgba_img.astype(np.uint8).reshape((h, w, c))

# 如果需要 CHW 排列的
rgba_img = rgba_img.transpose((2, 0, 1))
```
jeeyong
2022-09-23 16:32:30 +08:00
@stein42 卧槽卧槽, 果然大神多....
我需要消化一下代码...
这之前都没用过 numpy...感谢感谢..

另外请教一下,
buffer = bytes([208, 4]) * (width * height)
文件内容不只是 [208, 4] 还有其他的灰度值和通道值, 这一步不理解..
hsfzxjy
2022-09-23 16:32:35 +08:00
看你新的描述应该用不到 cython ,我盲写了一段,你可以根据文件格式调一下

```python
import numpy as np

with open("file", "rb") as f:
<TAB>img = f.read()

img = np.frombuffer(img, dtype=np.uint8) # 2HW
img = img.reshape(H, W, 2) # H x W x 2
img = np.stack([img[..., 0], img[..., 0], img[..., 0], img[..., 1]]) # H x W x 4
```
lmshl
2022-09-23 16:32:58 +08:00
我是直接写个 tensor 扔给 torch 跑,我知道它会自动用 SIMD 或者 CUDA (如果启动 gpu 的话)

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

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

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

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

© 2021 V2EX