-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Saving PNG images with PIL is 4 times slower than saving them with OpenCV #5986
Comments
Here are some thoughts for you. We allow the compression level to be set when saving PNGs - if I change your code to We also allow setting the compression type when saving PNGs - |
Thanks for the hints, I tried to adapt my code and I'm getting the following results:
and
which both is still pretty far away from the OpenCV results and leading to resulting image sizes of 33KB (PIL - compress_type=3), and 147KB (PIL - compress_level=1) vs 38KB (OpenCV). Maybe it's a MacOS specific issue? |
No, it's not macOS specific. I am also a macOS user. |
I recall looking into this a while ago, so my memory may be a bit sketchy, but something I noticed. For whatever reason, PIL implements its own PNG filtering (as opposed to use something like libpng). It'd be nice if PNG encoding could use a more speed optimised library. |
May have spoke too soon in my previous comment. Considering how long PNG has been around, I thought that the popular libraries would be reasonably well optimised, however, looking into this, it seems like they're predominantly optimised for decode only, not encode. Somewhat surprising to me, but not completely unreasonable I guess. So I'm not sure why OpenCV is faster here - they appear to be using libpng for PNG creation, which doesn't use SIMD for encoding. Maybe the compiler's auto-vectorizer just happens to work there? Regardless, I did find two speed focused encoders, fpng and fpnge, which only surfaced relatively recently. I made some changes to the latter to make it more usable and made a quick-and-dirty Python module for it. Whilst writing this comment, I came across Python bindings for fpng. I haven't tried this myself, but it may also be worth checking out. |
Doing a basic investigation, I found that Pillow/src/libImaging/ZipEncode.c Line 279 in 7ff0592
I thought maybe changing a setting in |
Thanks for looking into it. If you set compression level to 0, is that still where most of the time is spent? |
Yes. |
That indeed is very surprising. Would be interesting to know what it's spending all its time on, even when it's doing no compression. |
Not sure if anything has changed in the meantime, but for me PIL is waaaay faster than OpenCV. Python 3.10.6 Using compression level 9, and I moved the image_array outside of the loop to make the benchmark more fair. import time
import cv2
import numpy
from PIL import Image
from PIL.ImageDraw import ImageDraw
if __name__ == '__main__':
image = Image.new("RGB", (4000, 2800))
image_draw = ImageDraw(image)
image_draw.rectangle((10, 20, 60, 120), fill=(230, 140, 25))
trials = 20
t1 = time.time()
for i in range(trials):
image.save("tmp1.png", compress_level=9)
t2 = time.time()
print(f"Total time for PIL: {t2 - t1}s ")
compression_level = [cv2.IMWRITE_PNG_COMPRESSION, 9]
image_array = numpy.array(image)
image_array = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR)
t1 = time.time()
for i in range(trials):
cv2.imwrite("tmp2.png", image_array, compression_level)
t2 = time.time()
print(f"Total time for OpenCV: {t2 - t1}s ")
img1 = cv2.imread("tmp1.png")
img2 = cv2.imread("tmp2.png")
print(f"Images are equal: {numpy.all(img1 == img2)}") Total time for PIL: 5.534168481826782s Compress level 3 (which is OpenCV standard) gives me this: |
Pillow is indeed faster in the code from the previous comment - but that isn't because anything changed, but just because the previous comment is using compression, whereas the original post isn't using compression. If the previous comment shows an acceptable comparison for compression, then Pillow slower without compression, but faster with. |
When I'm running the code from #5986 (comment), I still obtain the following result (averaged the numbers from three runs):
would be interesting to understand, why the results are so contradicting, when running the same code on different machines. |
That is simply not true. Doing so would result in a file size as big as a BMP image. I modified the drawing code slightly so it isn't just almost black only. The table below shows the file size for each compression option. Using the default option gives a file size that is 2.7x smaller with PIL. Also interesting to see that the default value (so not specifying compression at all) you can see clearly that PIL uses compression 6 by default because the file size is the same. OpenCV on the other hand is somewhere in between compression level 3 and 4. Compressing with level 0 gives an image that is about the size of the image (4000x2800x3=33600000 + some png headers).
image = Image.new("RGB", (4000, 2800))
image_draw = ImageDraw(image)
image_draw.rectangle((10, 20, 60, 120), fill=(230, 140, 25))
num_squares = 100
for _ in range(num_squares):
x1 = random.randint(0, 3950)
y1 = random.randint(0, 2750)
x2 = x1 + random.randint(10, 150)
y2 = y1 + random.randint(10, 150)
fill_color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
image_draw.rectangle((x1, y1, x2, y2), fill=fill_color)
As for the save times, there does seem to be a lot of speed difference between Pillow 9.0 and 9.2! I just noticed that I used that version in my earlier tests and that is probably the cause of the difference in time measurement. So my conclusion so far is that Pillow 9.0 does indeed have something odd going on with PNG save times, which can be resolved by updating to Pillow 9.1.0 or higher, just tested that as well. |
Perhaps I could have said that the code in the original post didn't specify compression. |
Just for another data point, when I was still using Adobe Photoshop last year, their "best compression" option which would try multiple compression parameters to try to find the optimal (or at least the best within some amount of time) for an image took something like 10s for relatively small (~2560x1600ish) images. Lossless JPEG2000 was actually a decent amount faster aside from being smaller. I remember reading a long time ago that finding the optimal PNG compression involved trying multiple parameters for compression within a range since they couldn't be predicted accurately in advance but things may have changed since then. |
Replacing zlib with zlib-ng makes huge speedup. Here are my quick and dirty benchmark. Original (with zlib):
With zlib-ng:
It's about 2x speed-up with almost no effort. Another candidate is zlib-cloudflare. But it does not support 32-bit CPUs. |
I made PR #8495 for this. |
We did drop 32-bit Windows wheels for a few releases, but there were too many people still depending on them: #7443 (comment) But if there is a benefit to using zlib-cloudflare over zlib-ng, I don't see a reason that we couldn't use zlib-cloudflare in the 64-bit wheels and zlib-ng (or even just zlib) in the 32-bit wheels. |
@nulano |
It does come at the cost of about 3% increased file size. Do you know if there are tweaks / settings for zlib-ng to get comparable file sizes? |
You can use the The Pillow default is 5, but even using the maximum 9 is faster with zlib-ng than the default 5 with regular zlib. See the benchmarks in my PR (comments at the end of the collapsed block): #8500 |
@nulano Thanks for linking your test results, impressive gains! |
What did you do?
I want to save an image to disk and noticed a severe performance bottleneck for the part of my code that used PIL for saving images compared to a similar part in my codebase that uses OpenCV to save the images.
What did you expect to happen?
I expected both methods to be somewhat similar in performance.
What actually happened?
PIL was at least four times slower than converting the PIL.Image into an numpy array and storing the array using cv2.imwrite.
What are your OS, Python and Pillow versions?
Here is the benchmark code that I used:
which produced
The produced images are slightly different in file-size so potentially there is a more sophisticated compression being used.
Here are the two (black) images I obtained
tmp1.png (PIL-image, 33KB)
tmp2.png (OpenCV-image, 38KB)
My questions are:
Interestingly, if I switch from PNG to JPG, the results are flipped and PIL is faster than OpenCV:
could this be a problem in the PNG encoding library?
The text was updated successfully, but these errors were encountered: