Search code examples
pythonimagepython-imaging-librarybokeh

Python image multiple crops with Pillow and grouped and displayed in a row with Bokeh


I have the following task: generate multiple image crops, and then display them grouped in rows next to each other. So far I managed to generates crops based on coordinates. I have no clue how to display the images in rows, grouped by 'User' and ordered by 'Time'. I should use Bokeh for plotting the images, because there will be more plots to integrate. Please help!!!!

The dataframe:

#Import libraries
import numpy as np
from numpy import asarray
import pandas as pd
from PIL import Image
import matplotlib as plt

#Create the dataframe
data = {'Time':  ['2586', '2836', '2986', '3269', '3702'],
        'Map': ['Buc.jpg', 'Buc.jpg', 'Buc.jpg', 'Buc.jpg', 'Buc.jpg'],
        'Index': ['9', '10', '11', '12', '13'],
        'PSize': ['250', '150', '283', '433', '183'],
        'X': ['751', '673', '542', '762', '624'],
        'Y': ['458', '316', '287', '303', '297'],
        'User': ['u1', 'u1', 'u2', 'u2', 'u2'],
        }

columns = ['Time','Map','Index','PSize','X','Y','User']

df = pd.DataFrame (data, columns = columns)
df = df.astype(dtype= {"Time":"int64", "Map":"object","Index":"int64", 'PSize':'int64', 'X': 'int64', 'Y':"int64", 'User':'object'})
df.head()

Image file: enter image description here

Generate the crops based on coordinates:

#Create coordinate and crop the image
imagefile = 'Buc.jpg'
coordinates = list(df[['X', 'Y']].itertuples(index=False, name=None))
psize = 100

img = Image.open(imagefile)
for x, y in coordinates:
      box = (x-psize/2, y-psize/2, x+psize/2, y+psize/2)
      img.crop(box).show('%s.x%03d.y%03d.jpg'% (imagefile.replace('.jpg',''), x, y))

Output example:

enter image description here


Solution

  • Something like this. Note that Buc.jpg must be available within the current working directory when you run the script.

    import numpy as np
    import pandas as pd
    from PIL import Image
    
    from bokeh.io import show
    from bokeh.models import FixedTicker, FuncTickFormatter, ColumnDataSource
    from bokeh.plotting import figure
    from bokeh.transform import dodge
    
    df = pd.DataFrame({'Time': [2586, 2836, 2986, 3269, 3702],
                       'X': [751, 673, 542, 762, 624],
                       'Y': [458, 316, 287, 303, 297],
                       'User': ['u1', 'u1', 'u2', 'u2', 'u2']})
    
    imagefile = 'Buc.jpg'
    coordinates = list(df[['X', 'Y']].itertuples(index=False, name=None))
    psize = 100
    
    img = Image.open(imagefile).convert('RGBA')
    cropped_images = []
    for x, y in coordinates:
        box = (x - psize / 2, y - psize / 2, x + psize / 2, y + psize / 2)
        cropped_images.append(np.array(img.crop(box)).view(np.uint32)[::-1])
    
    df['Image'] = cropped_images
    
    # There's probably a better method to populate `TimeCoord` which I don't know.
    df = df.sort_values('Time')
    df['TimeCoord'] = 0
    for u in df['User'].unique():
        udf = (df['User'] == u)
        df.loc[udf, 'TimeCoord'] = np.arange(udf.sum())
    
    user_coords = dict(zip(df['User'].unique(), range(df.shape[0])))
    df['UserCoord'] = df['User'].replace(user_coords)
    
    p = figure(match_aspect=True)
    for r in [p.xaxis, p.xgrid, p.ygrid]:
        r.visible = False
    
    # Manually creating a categorical-like axis to make sure that we can use `dodge` below.
    p.yaxis.ticker = FixedTicker(ticks=list(user_coords.values()))
    p.yaxis.formatter = FuncTickFormatter(args=dict(rev_user_coords={v: k for k, v in user_coords.items()}),
                                          code="return rev_user_coords[tick];")
    
    
    ds = ColumnDataSource(df)
    img_size = 0.8
    p.image_rgba(image='Image',
                 x=dodge('TimeCoord', -img_size / 2), y=dodge('UserCoord', -img_size / 2),
                 dw=img_size, dh=img_size, source=ds)
    p.rect(x='TimeCoord', y='UserCoord', width=img_size, height=img_size, source=ds,
           line_dash='dashed', fill_alpha=0)
    show(p)