'Pandas - image to DataFrame

I want to convert an RGB image into a DataFrame, so that I have the co-ordinates of each pixel and their RGB value.

         x   y   red  green  blue
0        0   0   154      0     0
1        1   0   149    111     0
2        2   0   153      0     5
3        0   1   154      0     9
4        1   1   154     10    10
5        2   1   154      0     0

I can extract the RGB into a DataFrame quite easily

colourImg = Image.open("test.png")
colourPixels = colourImg.convert("RGB")
colourArray = np.array(colourPixels.getdata())

df = pd.DataFrame(colourArray, columns=["red","green","blue"])

But I don't know how to get the X & Y coordinates in there. I could write a loop, but on a large image that takes a long time.



Solution 1:[1]

Try using np.indices unfortunately it ends up with a array where the coordinate is the first dimension, but you can do a bit of np.moveaxis to fix that.

colourImg = Image.open("test.png")
colourPixels = colourImg.convert("RGB")
colourArray = np.array(colourPixels.getdata()).reshape(colourImg.size + (3,))
indicesArray = np.moveaxis(np.indices(colourImg.size), 0, 2)
allArray = np.dstack((indicesArray, colourArray)).reshape((-1, 5))


df = pd.DataFrame(allArray, columns=["y", "x", "red","green","blue"])

It's not the pretiest, but it seems to work (edit: fixed x,y being the wrong way around).

Solution 2:[2]

I've named the coordinates 'col' and 'row' to be explicit and avoid confusion if the x-coordinate is reffering to the column number or row number of your original pixel array:

A = colourArray

# Create the multiindex we'll need for the series
index = pd.MultiIndex.from_product(
    (*map(range, A.shape[:2]), ('r', 'g', 'b')),
    names=('row', 'col', None)
)

# Can be chained but separated for use in explanation
df = pd.Series(A.flatten(), index=index)
df = df.unstack()
df = df.reset_index().reindex(columns=['col', 'row', 'r', 'g', 'b'])

Explanation:

pd.Series(A.flatten(), index=index) will create a multiindex series where each channel intensity is accessible via df[row_n, col_n][channel_r_g_or_b]. The df variable (currently a series) will now look something like this:

row  col   
0    0    r    116
          g     22
          b    220
     1    r     75
          g    134
          b     43
              ... 
255  246  r     79
          g      9
          b    218
     247  r    225
          g    172
          b    172

unstack() will pivot the third index (channel index), returning a dataframe with columns b, g, r with each row indexed by a multiindex of (row_n, col_n). The df now looks like this:

           b    g    r
row col               
0   0    220   22  116
    1     43  134   75
    2    187   97   33
... ...  ...  ...  ...
255 226  156  242  128
    227  221   63  212
    228   75  110  193

We then call reset_index() to get rid of the (row_n, col_n) multiindex and just have a flat 0..?(n_pixels-1) index. The df is now:

       row  col    b    g    r
0        0    0  220   22  116
1        0    1   43  134   75
2        0    2  187   97   33
...    ...  ...  ...  ...  ...
65506  255  226  156  242  128
65507  255  227  221   63  212
65508  255  228   75  110  193

And then a simple reindex() to rearrange the columns into col, row, r, g, b order.


Timings:

Now as for how fast this runs, well... for a 3-channel image, here are the timings:

Size       Time
  250x250  58.2 ms
  500x500   251 ms
1000x1000  1.03 s
2500x2500  8.14 s

Admittedly not great on images > 1 MP. unstack() can take a while after the df gets very large.

I've tried @davidsheldon's solution and it ran a lot quicker, for a 2500x2500 image, it took 244 ms, and a 10000x10000 image took 9.04 s.

Solution 3:[3]

I created a little package using the davidsheldon's answer. Thank you! You can use

pip install pd2img

to install it. It is also possible to reverse the process (save the dataframe as an image). By the way: If the image has an alpha channel, the dataframe will have an additional column for that!

Here is everything you need to know:

from pd2img import Pd2Img
df = Pd2Img(r"C:\Users\Gamer\Documents\Downloads\WhatsApp 
Image 2022-04-21 at 5.07.14 PM.jpeg")  # creating an instance
df.to_file_rgb('f:\\testimagefile1.png')  # save the 
dataframe to an RGB image
df.to_file_rgba('f:\\testimagefile2.png')  # save the dataframe to an RGBA image 
np3 = df.to_numpy_rgb()  # convert the image to an numpy array (RGB, not BGR!)
np4 = df.to_numpy_rgba()  # convert the image to an numpy array (RGBA)
print(df.df)  # printing the dataframe
           y     x  red  green  blue
0          0     0  155    150   144
1          0     1  155    150   144

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Hans