use*_*217 1 python multithreading image-processing vips
我正在编写一个 Python(3.4.3) 程序,该程序在 Ubuntu 14.04 LTS 上使用 VIPS(8.1.1) 来使用多个线程读取许多小图块,并将它们组合成一个大图像。
在一个非常简单的测试中:
from concurrent.futures import ThreadPoolExecutor
from multiprocessing import Lock
from gi.repository import Vips
canvas = Vips.Image.black(8000,1000,bands=3)
def do_work(x):
img = Vips.Image.new_from_file('part.tif') # RGB tiff image
with lock:
canvas = canvas.insert(img, x*1000, 0)
with ThreadPoolExecutor(max_workers=8) as executor:
for x in range(8):
executor.submit(do_work, x)
canvas.write_to_file('complete.tif')
Run Code Online (Sandbox Code Playgroud)
我得到正确的结果。在我的完整程序中,每个线程的工作涉及从源文件读取二进制文件,将其转换为 tiff 格式,读取图像数据并插入到画布中。这似乎有效,但当我尝试检查结果时,我遇到了麻烦。由于图像非常大(〜50000 * 100000像素),我无法将整个图像保存在一个文件中,所以我尝试了
canvas = canvas.resize(.5)
canvas.write_to_file('test.jpg')
Run Code Online (Sandbox Code Playgroud)
这需要非常长的时间,并且生成的 jpeg 只有黑色像素。如果我调整大小三次,程序就会被终止。我也尝试过
canvas.extract_area(20000,40000,2000,2000).write_to_file('test.tif')
Run Code Online (Sandbox Code Playgroud)
这会导致错误消息segmentation fault(core dumped)
,但它确实保存了图像。里面有图片内容,但是好像放错地方了。
我想知道可能是什么问题?
以下是完整程序的代码。使用 OpenCV + sharemem(sharedmem 处理多处理部分)也实现了相同的逻辑,并且它的工作没有问题。
import os
import subprocess
import pickle
from multiprocessing import Lock
from concurrent.futures import ThreadPoolExecutor
import threading
import numpy as np
from gi.repository import Vips
lock = Lock()
def read_image(x):
with open(file_name, 'rb') as fin:
fin.seek(sublist[x]['dataStartPos'])
temp_array = np.fromfile(fin, dtype='int8', count=sublist[x]['dataSize'])
name_base = os.path.join(rd_path, threading.current_thread().name + 'tempimg')
with open(name_base + '.jxr', 'wb') as fout:
temp_array.tofile(fout)
subprocess.call(['./JxrDecApp', '-i', name_base + '.jxr', '-o', name_base + '.tif'])
temp_img = Vips.Image.new_from_file(name_base + '.tif')
with lock:
global canvas
canvas = canvas.insert(temp_img, sublist[x]['XStart'], sublist[x]['YStart'])
def assemble_all(filename, ramdisk_path, scene):
global canvas, sublist, file_name, rd_path, tilesize_x, tilesize_y
file_name = filename
rd_path = ramdisk_path
file_info = fetch_pickle(filename) # A custom function
# this info includes where to begin reading image data, image size and coordinates
tilesize_x = file_info['sBlockList_P0'][0]['XSize']
tilesize_y = file_info['sBlockList_P0'][0]['YSize']
sublist = [item for item in file_info['sBlockList_P0'] if item['SStart'] == scene]
max_x = max([item['XStart'] for item in file_info['sBlockList_P0']])
max_y = max([item['YStart'] for item in file_info['sBlockList_P0']])
canvas = Vips.Image.black((max_x+tilesize_x), (max_y+tilesize_y), bands=3)
with ThreadPoolExecutor(max_workers=4) as executor:
for x in range(len(sublist)):
executor.submit(read_image, x)
return canvas
Run Code Online (Sandbox Code Playgroud)
上述模块(作为 mcv 导入)在驱动程序脚本中调用:
canvas = mcv.assemble_all(filename, ramdisk_path, 0)
Run Code Online (Sandbox Code Playgroud)
为了检查内容,我使用了
canvas.extract_area(25000, 40000, 2000, 2000).write_to_file('test_vips1.jpg')
Run Code Online (Sandbox Code Playgroud)
我认为你的问题与 libvips 计算像素的方式有关。
在 OpenCV 这样的系统中,图像是巨大的内存区域。您执行一系列操作,每个操作都会以某种方式修改内存映像。
libvips 不是这样的,尽管界面看起来很相似。在 libvips 中,当您对图像执行操作时,实际上只是向管道添加一个新部分。只有当您最终将输出连接到某个接收器(磁盘上的文件,或者您想要填充图像数据的内存区域,或者显示器的区域)时,libvips 才会真正执行任何计算。然后,libvips 将使用递归算法在整个管道长度上运行大量工作线程,评估您同时创建的所有操作。
与编程语言进行类比,像 OpenCV 这样的系统是命令式的,libvips 是函数式的。
libvips 工作方式的好处是它可以立即查看整个管道,并且可以优化大部分内存使用并充分利用 CPU。不好的是,长的操作序列可能需要大量的堆栈来评估(而使用 OpenCV 这样的系统,您更有可能受到图像大小的限制)。特别是,libvips 用来评估的递归系统意味着管道长度受到 C 堆栈的限制,在许多操作系统上约为 2MB。
这是一个简单的测试程序,或多或少可以完成您正在做的事情:
#!/usr/bin/python3
import sys
import pyvips
if len(sys.argv) < 4:
print "usage: %s image-in image-out n" % sys.argv[0]
print " make an n x n grid of image-in"
sys.exit(1)
tile = pyvips.Image.new_from_file(sys.argv[1])
outfile = sys.argv[2]
size = int(sys.argv[3])
img = pyvips.Image.black(size * tile.width, size * tile.height, bands=3)
for y in range(size):
for x in range(size):
img = img.insert(tile, x * size, y * size)
# we're not interested in huge files for this test, just write a small patch
img.crop(10, 10, 100, 100).write_to_file(outfile)
Run Code Online (Sandbox Code Playgroud)
你像这样运行它:
time ./bigjoin.py ~/pics/k2.jpg out.tif 2
real 0m0.176s
user 0m0.144s
sys 0m0.031s
Run Code Online (Sandbox Code Playgroud)
它加载k2.jpg
(2k x 2k JPG 图像),将该图像重复到 2 x 2 网格中,并保存其中的一小部分。该程序可以很好地处理非常大的图像,请尝试删除crop
并运行为:
./bigjoin.py huge.tif out.tif[bigtiff] 10
Run Code Online (Sandbox Code Playgroud)
它会将巨大的 tiff 图像复制 100 次到一个非常巨大的 tiff 文件中。它会很快并且占用很少的内存。
然而,这个程序会因为小图像被复制多次而变得非常不满意。例如,在这台机器(Mac)上,我可以运行:
./bigjoin.py ~/pics/k2.jpg out.tif 26
Run Code Online (Sandbox Code Playgroud)
但这失败了:
./bigjoin.py ~/pics/k2.jpg out.tif 28
Bus error: 10
Run Code Online (Sandbox Code Playgroud)
如果输出为 28 x 28,则为 784 个图块。我们构建图像的方式是重复插入单个图块,这是一个 784 次操作的管道——长到足以导致堆栈溢出。在我的 Ubuntu 笔记本电脑上,我可以在管道开始失败之前很久就完成大约 2,900 个操作。
有一个简单的方法可以修复这个程序:构建一个宽而不是深的管道。不要每次都插入单个图像,而是制作一组条带,然后将这些条带连接起来。现在管道深度将与图块数量的平方根成正比。例如:
img = pyvips.Image.black(size * tile.width, size * tile.height, bands=3)
for y in range(size):
strip = pyvips.Image.black(size * tile.width, tile.height, bands=3)
for x in range(size):
strip = strip.insert(tile, x * size, 0)
img = img.insert(strip, 0, y * size)
Run Code Online (Sandbox Code Playgroud)
现在我可以运行:
./bigjoin2.py ~/pics/k2.jpg out.tif 200
Run Code Online (Sandbox Code Playgroud)
这是 40,000 张图像连接在一起的结果。