1

I wanted to reduce an image to a smaller size for easy sharing and faster uploading.

But I realized if I just reduce the size by its h&w, it doesn't really do the trick because a large image file may have a smaller h&w, and small image file may have a large h&w, so reduce image size by reduce its height & weight may not always shrink the size the way I wanted.

So now I have got the byte size using this:

import os
os.stat('myImage.jpg').st_size

Is it possible to reduce the image size by reducing its byte? And remain its ratio?

moomoochen
  • 355
  • 1
  • 7
  • 15
  • A useful Google search phrase might be *jpg compression*. – Ken White Oct 23 '18 at 03:00
  • You could always use let's say a fixed width, then calculate the according height by multiplying the target width with the aspect ratio of the original image. – Jeronimo Oct 23 '18 at 07:22
  • @Jeronimo, thanks for the suggestion, but I don't want to use width to reduce the size, because width and height aren't really relevant to the actually image size. Otherwise, I could have just used: _.resize((500, int(500/float(w) * h)), Image.ANTIALIAS)_ – moomoochen Oct 23 '18 at 07:29
  • @usr2564301, so basically, I don't want to reduce the image size by manipulating its height and weight, I want to reduce the image size simply by reduce its size(bytes). – moomoochen Oct 23 '18 at 09:09

2 Answers2

5

Here's a function I wrote with PIL. It does some iterative resizing and jpeg compression of an image to then look at the resulting file size and compare it to a target value, guessing the next best width/height combination from the size deviation ratio (basically some sort of a P controller).

It makes use of io.BytesIO which does all the resizing stuff in memory, so there's really only one read and one write access to files on the disk. Also, with this bruteforce approach, you can alter the target file format to let's say PNG, and it would work out of the box.

from PIL import Image
import os
import io

def limit_img_size(img_filename, img_target_filename, target_filesize, tolerance=5):
    img = img_orig = Image.open(img_filename)
    aspect = img.size[0] / img.size[1]

    while True:
        with io.BytesIO() as buffer:
            img.save(buffer, format="JPEG")
            data = buffer.getvalue()
        filesize = len(data)    
        size_deviation = filesize / target_filesize
        print("size: {}; factor: {:.3f}".format(filesize, size_deviation))

        if size_deviation <= (100 + tolerance) / 100:
            # filesize fits
            with open(img_target_filename, "wb") as f:
                f.write(data)
            break
        else:
            # filesize not good enough => adapt width and height
            # use sqrt of deviation since applied both in width and height
            new_width = img.size[0] / size_deviation**0.5    
            new_height = new_width / aspect
            # resize from img_orig to not lose quality
            img = img_orig.resize((int(new_width), int(new_height)))


limit_img_size(
    "test.jpg",   #  input file
    "test_with_limited_size.jpg",     #  target file
    50000,   # bytes    
    tolerance = 5    # percent of what the file may be bigger than target_filesize
)

EDIT:

With "in memory" I meant that when it saves the img to buffer in the loop, it saves it to a BytesIO object, which is not a file on the disk but in memory. And from that object I can then determine the resulting file size (which is just the length of that data buffer) without actually saving it to a file. In the end maybe that's just how you'd expect it to work, but I've seen too many codes that waste performance on saving files on disk due to a lack of knowledge about Python's io.BytesIO.

Only the final result will be saved to a file - and that's ofc where you want. Try using an absoulte filename for img_target_filename.

Jeronimo
  • 2,268
  • 2
  • 13
  • 28
  • I spent days trying to find a way to reduce image size by a given byte, and your code is exactly what I need and it works. Let me take some time and absorb this code! Thanks mate. – moomoochen Oct 23 '18 at 09:22
  • 1
    Glad it helps - would be nice if you could tag it as correct answer then. ;) – Jeronimo Oct 23 '18 at 15:37
  • With your code, I can even saved in different format(png to jpg), that's pretty awesome! Since I'm new to Python, could you please explain what did you mean by > resizing stuff "**in memory**"? And does that mean I can't save the altered image to another directory? – moomoochen Oct 24 '18 at 05:27
  • 1
    Answered in EDIT. – Jeronimo Oct 24 '18 at 07:17
  • 1
    I've another question, some of the images are not able to reduced its size properly, and the reason is that some of the images have significant different size when using `BytesIO`, whereas using `os.stat('xxx.jpg').st_size` will always return the real size. So, using this method, some of the images are unable to reduce the way it needs to be, and why is that? – moomoochen Nov 01 '18 at 07:49
  • Will this work with "TIF" files? I need to resize multiple pages of a "TIF" file... – Sardar Agabejli Sep 22 '20 at 11:50
1

it happened that a friend of mine needed to resize/rescale a personal image and it was required that the image to be at most 40KB. I wrote the following code to downscale/rescale the image considering the size on disk. I assume 40KB = 40000Bytes which is not exact but it can come handy

from skimage.io import imread, imshow
from skimage.transform import rescale, resize, downscale_local_mean
from skimage.io import imsave
import matplotlib.pyplot as plt
import os
import numpy as np

img = imread('original.jpg')
target_size = 40000
size = os.path.getsize('original.jpg')
factor = 0.9
while(size>=40000):
    image_rescaled = rescale(img, factor, anti_aliasing=False)
    imsave('new.jpg', image_rescaled)
    print('factor {} image of size {}'.format(factor,size))
    factor = factor - 0.05
    size = os.path.getsize('new.jpg')

end_size = os.path.getsize('new.jpg')
print(end_size)

Hope it helps!!!! You can follow me on GitHub as LeninGF

LeninGF
  • 332
  • 3
  • 10