Fast way to import and crop a jpeg in python lib - python

I have a python app that imports 200k+ images, crops them, and presents the cropped image to pyzbar to interpret a barcode. Cropping helps because there are multiple barcodes on the image and, presumably pyzbar is a little faster when given smaller images.
Currently I am using Pillow to import and crop the image.
On the average importing and cropping an image takes 262 msecs and pyzbar take 8 msecs.
A typical run is about 21 hours.
I wonder if a library other than Pillow might offer substantial improvements in loading/cropping. Ideally the library should be available for MacOS but I could also run the whole thing in a virtual Ubuntu machine.
I am working on a version that can run in parallel processes which will be a big improvement but if I could get 25% or more speed increase from a different library I would also add that.

As you didn't provide a sample image, I made a dummy file with dimensions 2544x4200 at 1.1MB in size and it is provided at the end of the answer. I made 1,000 copies of that image and processed all 1,000 images for each benchmark.
As you only gave your code in the comments area, I took it, formatted it and made the best I could of it. I also put it in a loop so it can process many files for just one invocation of the Python interpreter - this becomes important when you have 20,000 files.
That looks like this:
#!/usr/bin/env python3
import sys
from PIL import Image
# Process all input files so we only incur Python startup overhead once
for filename in sys.argv[1:]:
print(f'Processing: {filename}')
imgc = Image.open(filename).crop((0, 150, 270, 1050))
My suspicion is that I can make that faster using:
GNU Parallel, and/or
pyvips
Here is a pyvips version of your code:
#!/usr/bin/env python3
import sys
import pyvips
import numpy as np
# Process all input files so we only incur Python startup overhead once
for filename in sys.argv[1:]:
print(f'Processing: {filename}')
img = pyvips.Image.new_from_file(filename, access='sequential')
roi = img.crop(0, 150, 270, 900)
mem_img = roi.write_to_memory()
# Make a numpy array from that buffer object
nparr = np.ndarray(buffer=mem_img, dtype=np.uint8,
shape=[roi.height, roi.width, roi.bands])
Here are the results:
Sequential original code
./orig.py bc*jpg
224 seconds, i.e. 224 ms per image, same as you
Parallel original code
parallel ./orig.py ::: bc*jpg
55 seconds
Parallel original code but passing as many filenames as possible
parallel -X ./orig.py ::: bc*jpg
42 seconds
Sequential pyvips
./vipsversion bc*
30 seconds, i.e. 7x as fast as PIL which was 224 seconds
Parallel pyvips
parallel ./vipsversion ::: bc*
32 seconds
Parallel pyvips but passing as many filenames as possible
parallel -X ./vipsversion ::: bc*
5.2 seconds, i.e. this is the way to go :-)
Note that you can install GNU Parallel on macOS with homebrew:
brew install parallel

You might take a look on PyTurboJPEG which is a Python wrapper of libjpeg-turbo with insanely fast rescaling (1/2, 1/4, 1/8) while decoding large JPEG image, the returning numpy.ndarray is handy for image cropping. Moreover, JPEG image encoding speed is also remarkable.
from turbojpeg import TurboJPEG
# specifying library path explicitly
# jpeg = TurboJPEG(r'D:\turbojpeg.dll')
# jpeg = TurboJPEG('/usr/lib64/libturbojpeg.so')
# jpeg = TurboJPEG('/usr/local/lib/libturbojpeg.dylib')
# using default library installation
jpeg = TurboJPEG()
# direct rescaling 1/2 while decoding input.jpg to BGR array
in_file = open('input.jpg', 'rb')
bgr_array_half = jpeg.decode(in_file.read(), scaling_factor=(1, 2))
in_file.close()
# encoding BGR array to output.jpg with default settings.
out_file = open('output.jpg', 'wb')
out_file.write(jpeg.encode(bgr_array))
out_file.close()
libjpeg-turbo prebuilt binaries for macOS and Linux are also available here.

Related

set imageio compression level in python

I'm using imageio in Python to read in jpg images and write them as a gif, using something resembling the code below.
import imageio
with imageio.get_writer('mygif.gif', mode='I') as writer:
for filename in framefiles: # iterate over names of jpg files I want to turn into gif frames
frame = imageio.imread(filename)
writer.append_data(frame)
I'm noticing that the image quality in the gifs I produce is quite poor; I suspect this is due to some form of compression. Is there a way to tell imageio not to use any compression? Or maybe a way to do this with opencv instead?
Real problem is that GIF can display only 256 colors (8-bits color) so it has to reduce 24-bits colors (RGB) to 256 colors or it has emulate more colors using dots with different colors - ditherring.
As for options:
Digging in source code I found that it can get two parameters quantizer, palettesize which can control image/animation quality. (There is also subrectangles to reduce file size)
But there are two plugins for GIF which use different modules Pillow or FreeImage and they need different value for quantizer
PIL needs integer 0, 1 or 2.
FI needs string 'wu' or 'nq' (but later it converts it to integer 0 or 1)
They also keep these values in different way so if you want get current value or change it after get_writer() then you also need different code.
You can select module with format='GIF-PIL' or format='GIF-FI'
with imageio.get_writer('mygif.gif', format='GIF-PIL', mode='I',
quantizer=2, palettesize=32) as writer:
print(writer)
#print(dir(writer))
#print(writer._writer)
#print(dir(writer._writer))
print('quantizer:', writer._writer.opt_quantizer)
print('palette_size:', writer._writer.opt_palette_size)
#writer._writer.opt_quantizer = 1
#writer._writer.opt_palette_size = 256
#print('quantizer:', writer._writer.opt_quantizer)
#print('palette_size:', writer._writer.opt_palette_size)
with imageio.get_writer('mygif.gif', format='GIF-FI', mode='I',
quantizer='nq', palettesize=32) as writer:
print(writer)
#print(dir(writer))
print('quantizer:', writer._quantizer)
print('palette_size:', writer._palettesize)
#writer._quantizer = 1
#writer._palettesize = 256
#print('quantizer:', writer._quantizer)
#print('palette_size:', writer._palettesize)
I tried to create animations with different settings but they don't look much better.
I get better result using external program ImageMagick in console/terminal
convert image*.jpg mygif.gif
but still it wasn't as good as video or static images.
You can run it in Python
os.system("convert image*.jpg mygif.gif")
subprocess.run("convert image*.jpg mygif.gif", shell=True)
Or you can try to do it with module Wand which is a wrapper on ImageMagick
Source code: GifWriter in pillowmulti.py and in freeimagemulti.py
* wu - Wu, Xiaolin, Efficient Statistical Computations for Optimal Color Quantization
* nq (neuqant) - Dekker A. H., Kohonen neural networks for optimal color quantization
Doc: GIF-PIL Static and animated gif (Pillow), GIF-FI Static and animated gif (FreeImage)

how can I load a single tif image in parts into numpy array without loading the whole image into memory?

so There is a 4GB .TIF image that needs to be processed, as a memory constraint I can't load the whole image into numpy array so I need to load it lazily in parts from hard disk.
so basically I need and that needs to be done in python as the project requirement. I also tried looking for tifffile library in PyPi tifffile but I found nothing useful please help.
pyvips can do this. For example:
import sys
import numpy as np
import pyvips
image = pyvips.Image.new_from_file(sys.argv[1], access="sequential")
for y in range(0, image.height, 100):
area_height = min(image.height - y, 100)
area = image.crop(0, y, image.width, area_height)
array = np.ndarray(buffer=area.write_to_memory(),
dtype=np.uint8,
shape=[area.height, area.width, area.bands])
The access option to new_from_file turns on sequential mode: pyvips will only load pixels from the file on demand, with the restriction that you must read pixels out top to bottom.
The loop runs down the image in blocks of 100 scanlines. You can tune this, of course.
I can run it like this:
$ vipsheader eso1242a-pyr.tif
eso1242a-pyr.tif: 108199x81503 uchar, 3 bands, srgb, tiffload_stream
$ /usr/bin/time -f %M:%e ./sections.py ~/pics/eso1242a-pyr.tif
273388:479.50
So on this sad old laptop it took 8 minutes to scan a 108,000 x 82,000 pixel image and needed a peak of 270mb of memory.
What processing are you doing? You might be able to do the whole thing in pyvips. It's quite a bit quicker than numpy.
import pyvips
img = pyvips.Image.new_from_file("space.tif", access='sequential')
out = img.resize(0.01, kernel = "linear")
out.write_to_file("resied_image.jpg")
if you want to convert the file to other format have a smaller size this code will be enough and will help you do it without without any memory spike and in very less time...

get a numpy array from a sequence of images in a folder

I have a folder, say video1 with bunch of images in order frame_00.png, frame_01.png, ...
What I want is a 4D numpy array in the format (number of frames, w, h, 3)
This is what I did, but I think it is quite slow, is there any faster or more effecient method to achieve the same thing?
folder = "video1/"
import os
images = sorted(os.listdir(folder)) #["frame_00", "frame_01", "frame_02", ...]
from PIL import Image
import numpy as np
video_array = []
for image in images:
im = Image.open(folder + image)
video_array.append(np.asarray(im)) #.transpose(1, 0, 2))
video_array = np.array(video_array)
print(video_array.shape)
#(75, 50, 100, 3)
There's an older SO thread that goes into a great deal of detail (perhaps even a bit too much) on this very topic. Rather than vote to close this question as a dup, I'm going to give a quick rundown of that thread's top bullet points:
The fastest commonly available image reading function is imread from the cv2 package.
Reading the images in and then adding them to a plain Python list (as you are already doing) is the fastest approach for reading in a large number of images.
However, given that you are eventually converting the list of images to an array of images, every possible method of building up an array of images is almost exactly as fast as any other
Although, interestingly enough, if you take the approach of assigning images directly to a preallocated array, it actually matters which indices (ie which dimension) you assign to in terms of getting optimal performance.
So basically, you're not going to be able to get much faster while working in pure, single-threaded Python. You might get a boost from switching to cv2.imread (in place of PIL.Image.open).
PNG is an extremely slow format, so if you can use almost anything else, you'll see a big speedup.
For example, here's an opencv version of your program that gets the filenames from command-line args:
#!/usr/bin/python3
import sys
import cv2
import numpy as np
video_array = []
for filename in sys.argv[1:]:
im = cv2.imread(filename)
video_array.append(np.asarray(im))
video_array = np.array(video_array)
print(video_array.shape)
I can run it like this:
$ mkdir sample
$ for i in {1..100}; do cp ~/pics/k2.png sample/$i.png; done
$ time ./readframes.py sample/*.png
(100, 2048, 1450, 3)
real 0m6.063s
user 0m5.758s
sys 0m0.839s
So 6s to read 100 PNG images. If I try with TIFF instead:
$ for i in {1..100}; do cp ~/pics/k2.tif sample/$i.tif; done
$ time ./readframes.py sample/*.tif
(100, 2048, 1450, 3)
real 0m1.532s
user 0m1.060s
sys 0m0.843s
1.5s, so four times faster.
You might get a small speedup with pyvips:
#!/usr/bin/python3
import sys
import pyvips
import numpy as np
# map vips formats to np dtypes
format_to_dtype = {
'uchar': np.uint8,
'char': np.int8,
'ushort': np.uint16,
'short': np.int16,
'uint': np.uint32,
'int': np.int32,
'float': np.float32,
'double': np.float64,
'complex': np.complex64,
'dpcomplex': np.complex128,
}
# vips image to numpy array
def vips2numpy(vi):
return np.ndarray(buffer=vi.write_to_memory(),
dtype=format_to_dtype[vi.format],
shape=[vi.height, vi.width, vi.bands])
video_array = []
for filename in sys.argv[1:]:
vi = pyvips.Image.new_from_file(filename, access='sequential')
video_array.append(vips2numpy(vi))
video_array = np.array(video_array)
print(video_array.shape)
I see:
$ time ./readframes.py sample/*.tif
(100, 2048, 1450, 3)
real 0m1.360s
user 0m1.629s
sys 0m2.153s
Another 10% or so.
Finally, as other posters have said, you could load frames in parallel. That wouldn't help TIFF much, but it would certainly boost PNG.

What is causing my Python program to run out of memory using opencv?

I wrote a program to read images using Python's opencv and tried to load 3 GB images, but the program aborted.
There is 32 GB of memory on my PC, but when I run this program it will run out of it. What is the cause?
The error message is not issued and the PC becomes abnormally heavy. I confirmed it with Ubuntu's System Monitor, and it ran out of memory and swap.
I import images into one array to pass to tensorflow deep learning program. The size of the images are 200 x 200 color images.
I use 64 bit version of Python.
import os
import numpy as np
import cv2
IMG_SIZE = 200
def read_images(path):
dirnames = sorted(os.listdir(path))
files = [sorted(os.listdir(path+dirnames[i]))\
for i in range(len(dirnames))]
i = 0
images = []
for fs in files:
tmp_images = []
for f in fs:
img = cv2.imread(path +dirnames[i] + "/" + f)
img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))
img = img.flatten().astype(np.float32)/255.0
tmp_images.append(img)
i = i + 1
images.append(tmp_images)
return np.asarray(images)
Reasons for running out of memory:
Image file size and the size of corresponding array in memory are different. Images, e.g., PNG and JPEG formats, are compressed. The size of a corresponding uncompressed BMP image is more relevant here. Also, ndarray holds some meta-information that makes it a bit larger.
Converting to float32 from uint8 multiplies the size by 4. Try to avoid this if possible (I recognize uint8 imposes some limitations, like being unable to normalize and center the data).
Possible remedies:
Use numpy.memmap to create an array stored on disk
Reduce the quality of images, by converting to grayscale and/or reducing the resolution.
Train the model on a smaller number of images.

python set maximum file size when converting (pdf) to jpeg using e.g. Wand

For subsequent processing purposes, in python I am converting a multi-page PDF (f) into JPEGs (temp?.jpg):
import os
from wand.image import Image as wimage
with wimage(filename=f,resolution=300) as img:
for i in range(len(img.sequence)):
ftemp=os.path.abspath('temp%i.jpg'%i)
img_to_save=wimage(img.sequence[i])
img_to_save.compression_quality = 100
img_to_save.format='jpeg'
img_to_save.save(filename=ftemp)
I am using wand because of its ability to sequence the PDF pages, but am open to PIL etc.
I need the resolution and compression_quality to be as high as possible, but I want each JPEG to be no larger than (say) 300 kb in size.
How can I set a limit to the size of the JPEG file?
On the command line I would just do (see https://stackoverflow.com/a/11920384/1021819):
convert original.jpeg -define jpeg:extent=300kb -scale 50% output.jpg
Thanks!
The wand library has wand.image.OptionDict for managing -define attributes, but unfortunately all options are locked by wand.image.Option frozenset. IMHO, this renders the whole feature as unusable.
Luckily, you can create a quick sub-class to handle this via the wand.api.
import os
from wand.image import Image
from wand.api import library
from wand.compat import binary
class wimage(Image):
def myDefine(self, key, value):
""" Skip over wand.image.Image.option """
return library.MagickSetOption(self.wand, binary(key), binary(value))
with wimage(filename=f, resolution=300) as img:
for i in range(len(img.sequence)):
ftemp=os.path.abspath('temp%i.jpg'%i)
with wimage(img.sequence[i]) as img_to_save:
img_to_save.myDefine('jpeg:extent', '300kb')
img_to_save.compression_quality = 100
img_to_save.format='jpeg'
img_to_save.save(filename=ftemp)
In the near future. The wand.image.Option would be deprecated, and you could simply call img_to_save.options['jpeg:extent'] = '300kb'.

Categories

Resources