Загружать только часть изображения в python

Это может быть глупый вопрос, но...

У меня есть несколько тысяч изображений, которые я бы хотел загрузить в Python, а затем преобразовать в массивы numpy. Очевидно, это происходит медленно. Но на самом деле меня интересует только небольшая часть каждого изображения. (Та же часть, всего 100x100 пикселей в центре изображения.)

Есть ли способ загрузить только часть изображения, чтобы ускорить работу?

Вот пример кода, в котором я генерирую несколько образцовых изображений, сохраняю их и загружаю обратно.

import numpy as np
import matplotlib.pyplot as plt
import Image, time

#Generate sample images
num_images = 5

for i in range(0,num_images):
    Z = np.random.rand(2000,2000)
    print 'saving %i'%i
    plt.imsave('%03i.png'%i,Z)

%load the images
for i in range(0,num_images):
    t = time.time()

    im = Image.open('%03i.png'%i)
    w,h = im.size
    imc = im.crop((w-50,h-50,w+50,h+50))

    print 'Time to open: %.4f seconds'%(time.time()-t)

    #convert them to numpy arrays
    data = np.array(imc)

Ответы

Ответ 1

Сохраните ваши файлы как несжатые 24-битные BMP. Эти данные пикселей хранятся очень обычным способом. Просмотрите часть "Image Data" этой диаграммы из Wikipedia. Обратите внимание, что большая часть сложности диаграммы находится только из заголовков:

BMP file format

Например, скажем, вы сохраняете это изображение (здесь показано увеличенное изображение):

2x2 square image

Это то, что выглядит секция данных пикселей, если она хранится как 24-разрядный несжатый BMP. Обратите внимание, что данные хранятся снизу вверх по какой-либо причине и в форме BGR вместо RGB, поэтому первая строка в файле - это самая нижняя строка изображения, вторая строка - вторая по величине, и т.д.

00 00 FF    FF FF FF    00 00
FF 00 00    00 FF 00    00 00

Эти данные объясняются следующим образом:

           |  First column  |  Second Column  |  Padding
-----------+----------------+-----------------+-----------
Second Row |  00 00 FF      |  FF FF FF       |  00 00
-----------+----------------+-----------------+-----------
First Row  |  FF 00 00      |  00 FF 00       |  00 00
-----------+----------------+-----------------+-----------

или

           |  First column  |  Second Column  |  Padding
-----------+----------------+-----------------+-----------
Second Row |  red           |  white          |  00 00
-----------+----------------+-----------------+-----------
First Row  |  blue          |  green          |  00 00
-----------+----------------+-----------------+-----------

Прокладка должна содержать размер строки до 4 байтов.


Итак, все, что вам нужно сделать, это реализовать читателя для данного формата файла, а затем рассчитать смещение байта, где вы должны начать и прекратить чтение каждой строки:

def calc_bytes_per_row(width, bytes_per_pixel):
    res = width * bytes_per_pixel
    if res % 4 != 0:
        res += 4 - res % 4
    return res

def calc_row_offsets(pixel_array_offset, bmp_width, bmp_height, x, y, row_width):
    if x + row_width > bmp_width:
        raise ValueError("This is only for calculating offsets within a row")

    bytes_per_row = calc_bytes_per_row(bmp_width, 3)
    whole_row_offset = pixel_array_offset + bytes_per_row * (bmp_height - y - 1)
    start_row_offset = whole_row_offset + x * 3
    end_row_offset = start_row_offset + row_width * 3
    return (start_row_offset, end_row_offset)

Затем вам просто нужно обработать соответствующие байтовые смещения. Например, скажем, вы хотите прочитать кусок 400x400, начиная с позиции 500x500 в растровом изображении 10000x10000:

def process_row_bytes(row_bytes):
    ... some efficient way to process the bytes ...

bmpf = open(..., "rb")
pixel_array_offset = ... extract from bmp header ...
bmp_width = 10000
bmp_height = 10000
start_x = 500
start_y = 500
end_x = 500 + 400
end_y = 500 + 400

for cur_y in xrange(start_y, end_y):
    start, end = calc_row_offsets(pixel_array_offset, 
                                  bmp_width, bmp_height, 
                                  start_x, cur_y, 
                                  end_x - start_x)
    bmpf.seek(start)
    cur_row_bytes = bmpf.read(end - start)
    process_row_bytes(cur_row_bytes)

Обратите внимание, что важно, как вы обрабатываете байты. Вы, вероятно, можете сделать что-то умное с помощью PIL и просто сбрасывать в него данные пикселя, но я не совсем уверен. Если вы сделаете это неэффективно, это может не стоить того. Если скорость вызывает огромную озабоченность, вы можете написать ее с помощью pyrex или реализовать выше в C и просто вызвать ее из Python.

Ответ 2

Пока вы не можете получить намного быстрее, чем PIL-урожай в одном потоке, вы можете использовать несколько ядер, чтобы ускорить все!:)

Я запустил приведенный ниже код на моей 8-ядерной машине i7, а также на своем 7-летнем, двухъядерном, едва ли 2ГГц ноутбуке. Оба наблюдали значительные улучшения во время выполнения. Как и следовало ожидать, улучшение зависит от количества доступных ядер.

Ядро вашего кода одинаково, я просто отделил цикл от фактического вычисления, чтобы функция могла применяться к списку значений параллельно.

Итак, это:

for i in range(0,num_images):
    t = time.time()

    im = Image.open('%03i.png'%i)
    w,h = im.size
    imc = im.crop((w-50,h-50,w+50,h+50))

    print 'Time to open: %.4f seconds'%(time.time()-t)

    #convert them to numpy arrays
    data = np.array(imc)

Стал:

def convert(filename):  
    im = Image.open(filename)
    w,h = im.size
    imc = im.crop((w-50,h-50,w+50,h+50))
    return numpy.array(imc)

Ключом к ускорению является функция Pool библиотеки multiprocessing. Это делает тривиальным управлять вещами на нескольких процессорах.

Полный код:

import os 
import time
import numpy 
from PIL import Image
from multiprocessing import Pool 

# Path to where my test images are stored
img_folder = os.path.join(os.getcwd(), 'test_images')

# Collects all of the filenames for the images
# I want to process
images = [os.path.join(img_folder,f) 
        for f in os.listdir(img_folder)
        if '.jpeg' in f]

# Your code, but wrapped up in a function       
def convert(filename):  
    im = Image.open(filename)
    w,h = im.size
    imc = im.crop((w-50,h-50,w+50,h+50))
    return numpy.array(imc)

def main():
    # This is the hero of the code. It creates pool of 
    # worker processes across which you can "map" a function
    pool = Pool()

    t = time.time()
    # We run it normally (single core) first
    np_arrays = map(convert, images)
    print 'Time to open %i images in single thread: %.4f seconds'%(len(images), time.time()-t)

    t = time.time()
    # now we run the same thing, but this time leveraging the worker pool.
    np_arrays = pool.map(convert, images)
    print 'Time to open %i images with multiple threads: %.4f seconds'%(len(images), time.time()-t)

if __name__ == '__main__':
    main()

Довольно простой. Только несколько дополнительных строк кода и небольшой рефакторинг для перемещения бит преобразования в его собственную функцию. Результаты говорят сами за себя:

Результаты:

8-Core i7

Time to open 858 images in single thread: 6.0040 seconds
Time to open 858 images with multiple threads: 1.4800 seconds

Двухъядерный Intel Duo

Time to open 858 images in single thread: 8.7640 seconds
Time to open 858 images with multiple threads: 4.6440 seconds

Итак, иди! Даже если у вас есть супер-старая 2-ядерная машина, вы можете вдвое сократить время открытия и обработки ваших изображений.

Предостережения

Память. Если вы обрабатываете 1000 изображений, вы, вероятно, в какой-то момент собираетесь использовать Pythons Memory limit. Чтобы обойти это, вам просто нужно обработать данные в кусках. Вы все еще можете использовать все преимущества многопроцессорности, просто в меньших укусах. Что-то вроде:

for i in range(0, len(images), chunk_size): 
    results = pool.map(convert, images[i : i+chunk_size]) 
    # rest of code. 

Ответ 3

О, я просто понял, что может быть гораздо более простой способ, чем делать то, что я написал выше, относительно файлов BMP.

Если вы все равно генерируете файлы изображений, и вы всегда знаете, какую часть вы хотите читать, просто сохраните эту часть как другой файл изображения, когда вы его создаете:

import numpy as np
import matplotlib.pyplot as plt
import Image

#Generate sample images
num_images = 5

for i in range(0,num_images):
    Z = np.random.rand(2000, 2000)
    plt.imsave('%03i.png'%i, Z)
    snipZ = Z[200:300, 200:300]
    plt.imsave('%03i.snip.png'%i, snipZ)

#load the images
for i in range(0,num_images):
    im = Image.open('%03i.snip.png'%i)

    #convert them to numpy arrays
    data = np.array(im)

Ответ 4

Я провел несколько тестов времени, и мне жаль, что я не думаю, что вы можете получить намного быстрее, чем команда PIL crop. Даже при ручном поиске/чтении низкого уровня вам все равно нужно прочитать байты. Вот результаты синхронизации:

%timeit im.crop((1000-50,1000-50,1000+50,1000+50))
fid = open('003.png','rb')
%timeit fid.seek(1000000)
%timeit fid.read(1)
print('333*100*100/10**(9)*1000=%.2f ms'%(333*100*100/10**(9)*1000))


100000 loops, best of 3: 3.71 us per loop
1000000 loops, best of 3: 562 ns per loop
1000000 loops, best of 3: 330 ns per loop
333*100*100/10**(9)*1000=3.33 ms

Как можно видеть нижний расчет, мы имеем 1 байт * 10000 байт (100x100) * 333ns на байт = 3,33 мс, что совпадает с командой обрезки выше