Skip to content
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

Add feature to convert python list of numpy images to grid images #23

Merged
merged 3 commits into from Apr 3, 2017

Conversation

kylehounslow
Copy link

@kylehounslow kylehounslow commented Mar 25, 2017

Hi Adrian,
I really like what you're doing with imutils and your blog, keep it up!
I'd like to submit a feature make_grids_of_images to imutils as part of the convenience.py module if you approve:

Brief:
Often I need to crawl image datasets which have large, convoluted folder structures. I find it useful to create some grid images to visualise the dataset as well and share with my colleagues.

Example usage - "The Perfect Desktop Background":

from imutils import convenience
import urllib
# get image from url, convert to numpy array
req = urllib.urlopen('https://avatars0.githubusercontent.com/u/7102778?v=3&s=460')
arr = np.asarray(bytearray(req.read()), dtype=np.uint8)
img = cv2.imdecode(arr, -1)
# duplicate image 'num_imgs' times
num_imgs = 35
img_list = []
for i in xrange(num_imgs):
    img_list.append(img)
# convert image list into a grid of 256x256 images tiled in a 7x5 grid
grids = convenience.make_grids_of_images(img_list, (256, 256), (7, 5))
# iterate through grids and display
for grid in grids:
    cv2.imshow('grid image', grid)
    cv2.waitKey(0)

Let me know what you think and feel free to refine/use as you wish.
Cheers,
Kyle

@jrosebr1
Copy link
Contributor

Hi @kylehounslow, thanks for contributing. Can you tell me a little more about this functionality?

Specifically, it would be a nice to see a screenshot of the output of your code. I imagine the intended functionality is to create a fixed size montage of images, but I just want to make sure. Often it's really helpful to see a screenshot of the input and output to ensure I'm understanding the intended functionality matches the code.

@kylehounslow
Copy link
Author

Hi @jrosebr1, you bet.
The expected input is a python list of images, where the list can be of any length and the images of any width/height.
The user specifies the how many "tiles" in the x and y direction as well as the height/width of each "tile".
The output will then be a single image containing all of the input images laid out in a grid.
If the amount of images in the python list is greater than the number of tiles in the grid (which is usually the case), then the list simply spills into a new grid image of same dimensions. And since this is usually the case, the output is always a python list of the output grid images, even if a single image is output. Any empty space left over on a grid image is filled with black pixels.

The following is the code to be added to convenience.py , followed by an example usage:

def make_grids_of_images(image_list, image_shape, grid_shape):
    """
    ---------------------------------------------------------------------------------------------
    author: Kyle Hounslow
    ---------------------------------------------------------------------------------------------
    Converts a list of single images into a list of 'grid' images of specified rows and columns.
    A new grid image is started once rows and columns of grid image is filled.
    Empty space of incomplete grid images are filled with black pixels
    ---------------------------------------------------------------------------------------------
    :param image_list: python list of input images
    :param image_shape: tuple, size each image will be resized to for display (width, height)
    :param grid_shape: tuple, shape of image grid (width, height)
    :return: list of grid images in numpy array format
    ---------------------------------------------------------------------------------------------

    example usage:

    # load single image
    img = cv2.imread('lena.jpg')
    # duplicate image 25 times
    num_imgs = 25
    img_list = []
    for i in xrange(num_imgs):
        img_list.append(img)
    # convert image list into a grid of 256x256 images tiled in a 5x5 grid
    grids = make_grids_of_images(img_list, (256, 256), (5, 5))
    # iterate through grids and display
    for grid in grids:
        cv2.imshow('grid image', grid)
        cv2.waitKey(0)

    ----------------------------------------------------------------------------------------------
    """
    if len(image_shape) != 2:
        raise Exception('image shape must be list or tuple of length 2 (rows, cols)')
    if len(grid_shape) != 2:
        raise Exception('grid shape must be list or tuple of length 2 (rows, cols)')
    image_grids = []
    # start with black canvas to draw images onto
    grid_image = np.zeros(shape=(image_shape[1] * (grid_shape[1]), image_shape[0] * grid_shape[0], 3),
                          dtype=np.uint8)
    cursor_pos = [0, 0]
    start_new_img = False
    for img in image_list:
        if type(img).__module__ != np.__name__:
            raise Exception('input of type {} is not a valid numpy array'.format(type(img)))
        start_new_img = False
        img = cv2.resize(img, image_shape)
        # draw image to black canvas
        grid_image[cursor_pos[1]:cursor_pos[1] + image_shape[1], cursor_pos[0]:cursor_pos[0] + image_shape[0]] = img
        cursor_pos[0] += image_shape[0]  # increment cursor x position
        if cursor_pos[0] >= grid_shape[0] * image_shape[0]:
            cursor_pos[1] += image_shape[1]  # increment cursor y position
            cursor_pos[0] = 0
            if cursor_pos[1] >= grid_shape[1] * image_shape[1]:
                cursor_pos = [0, 0]
                image_grids.append(grid_image)
                # reset black canvas
                grid_image = np.zeros(shape=(image_shape[1] * (grid_shape[1]), image_shape[0] * grid_shape[0], 3),
                                      dtype=np.uint8)
                start_new_img = True
    if start_new_img is False:
        image_grids.append(grid_image)  # add unfinished grid
    return image_grids

Example:
In this example I'll use a single image (my github avatar), and simply duplicate it.
image

from imutils import convenience
import urllib
# get image from url, convert to numpy array
req = urllib.urlopen('https://avatars0.githubusercontent.com/u/7102778?v=3&s=460')
arr = np.asarray(bytearray(req.read()), dtype=np.uint8)
img = cv2.imdecode(arr, -1)
# duplicate image 'num_imgs' times
num_imgs = 35
img_list = []
for i in xrange(num_imgs):
    img_list.append(img)
# convert image list into a grid of 256x256 images tiled in a 7x5 grid
grids = convenience.make_grids_of_images(img_list, (256, 256), (7, 5))
# iterate through grids and display
for grid in grids:
    cv2.imshow('grid image', grid)
    cv2.waitKey(0)

Output:
image
In this case, the number of images in the input fit perfectly into a single grid image (7x5 == 35), let's try a less pretty number, perhaps 77.

from imutils import convenience
import urllib
# get image from url, convert to numpy array
req = urllib.urlopen('https://avatars0.githubusercontent.com/u/7102778?v=3&s=460')
arr = np.asarray(bytearray(req.read()), dtype=np.uint8)
img = cv2.imdecode(arr, -1)
# duplicate image 'num_imgs' times
num_imgs = 77
img_list = []
for i in xrange(num_imgs):
    img_list.append(img)
# convert image list into a grid of 256x256 images tiled in a 7x5 grid
grids = convenience.make_grids_of_images(img_list, (256, 256), (7, 5))
# iterate through grids and display
for grid in grids:
    cv2.imshow('grid image', grid)
    cv2.waitKey(0)

Output:
Image 1:
image
Image 2:
image
Image 3:
image

Let me know your thoughts.
Best,
Kyle

@jrosebr1
Copy link
Contributor

jrosebr1 commented Apr 2, 2017

@kylehounslow Thanks for sharing the example input and output, that is super helpful. I'm ready to go ahead and merge this in, I would just request changing the name of the function from make_grids_of_images to simply 'build_mosaics`.

In OpenCV "lingo" the word "mosaic" more accurately reflects what this function does.

@kylehounslow
Copy link
Author

@jrosebr1 "mosaic" sounds way better. Go ahead and make the change. Glad to have this merged in and will be back to contribute when I can.

@kylehounslow
Copy link
Author

@jrosebr1 I changed the function signature to build_mosaics. Branch should be good to merge.

@jrosebr1 jrosebr1 merged commit d6b0d70 into PyImageSearch:master Apr 3, 2017
@jrosebr1
Copy link
Contributor

jrosebr1 commented Apr 3, 2017

Done! Thank you for your contribution @kylehounslow. I also think this will make a nice PyImageSearch blog post in the future -- I'll likely demo this functionality on the blog.

@kylehounslow kylehounslow deleted the feature/grid_images branch April 3, 2017 19:16
@kylehounslow
Copy link
Author

@jrosebr1 cool! Will stay tuned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants