Search code examples
pythonimageubuntupython-imaging-library

PIL library Image.fromarray() causes AttributeError: 'tuple' object has no attribute '__array_interface__'


I'm using the physics simulator pybullet, and want to save an image from a 'virtual camera' within my simulation, using the following two lines of code.

camera1 = pybullet.getCameraImage(900,600)
im1 = Image.fromarray(camera1[2])

The first line uses pybullet's getCameraImage function to return an unprocessed image. camera1[2] is a list of pixel colors in RGBA format, in range [0..255] for each color.
The second line should take that array and convert it into an image which I can then save and view.

When I run the code I get the following error message:

Traceback (most recent call last):
  File "generate_dataset.py", line 43, in <module>
    im1 = Image.fromarray(camera1[2], "RGBA")
  File "/usr/lib/python3/dist-packages/PIL/Image.py", line 2140, in 
fromarray
    arr = obj.__array_interface__
AttributeError: 'tuple' object has no attribute '__array_interface__'

The code was working yesterday on Ubuntu 14.04, but today I upgraded to Ubuntu 16.04 and the code has stopped working. I have tried running it with Python 2.7.12 and Python 3.5.2, both versions get the same error.

Things I have tried:
Adding another line to convert the list into a numpy array:
camera1 = p.getCameraImage(900,600) imarray = np.asarray(camera1[2]) im1 = Image.fromarray(imarray)
Resulted in:

Traceback (most recent call last):
  File "generate_dataset.py", line 42, in <module>
    im1 = Image.fromarray(imarray)
  File "/usr/local/lib/python2.7/dist-packages/PIL/Image.py", line 2431, in fromarray
    raise TypeError("Cannot handle this data type")
TypeError: Cannot handle this data type

Changing the last line to:
im1 = Image.fromarray(imarray.astype('uint8'))
Resulted in:

Traceback (most recent call last):
  File "generate_dataset.py", line 42, in <module>
    im1 = Image.fromarray(imarray.astype('uint8'))
  File "/usr/lib/python3/dist-packages/PIL/Image.py", line 2165, in fromarray
    size = shape[1], shape[0]
IndexError: tuple index out of range

Extra info, if needed

Pybullet documentation: https://docs.google.com/document/d/10sXEhzFRSnvFcl3XxNGhnD4N2SedqwdAvK3dsihxVUA/edit#heading=h.2ye70wns7io3

PIL documentation: https://pillow.readthedocs.io/en/3.1.x/reference/Image.html

My full code:

import pybullet as p
import pybullet_data
import time
from PIL import Image
from random import *
import numpy as np

nExamples = 200

for n in range(0, nExamples):

  print ("Running example " + str(n))

  physicsClient = p.connect(p.DIRECT) #or p.GUI for graphical version
  p.setAdditionalSearchPath(pybullet_data.getDataPath()) #optionally
  p.setGravity(0,0,-10)
  planeId = p.loadURDF("ground.urdf")
  p.resetDebugVisualizerCamera( cameraDistance=1, cameraYaw=0, cameraPitch=-30, cameraTargetPosition=[0,0,0])

  x1 = uniform(-0.03,0.03)
  y1 = uniform(-0.03,0.03)
  x2 = x1 + uniform(-0.03,0.03)
  y2 = y1 + uniform(-0.03,0.03)
  cubeStartPos = [0.0,0,0.025]
  cubeStartPos1 = [x1,y1,0.075]
  cubeStartPos2 = [x2,y2,0.125]
  yaw0 = uniform(0,np.pi/2)
  yaw1 = uniform(0,np.pi/2)
  yaw2 = uniform(0,np.pi/2)
  cubeStartOrientation0 = p.getQuaternionFromEuler([0,0,yaw0])
  cubeStartOrientation1 = p.getQuaternionFromEuler([0,0,yaw1])
  cubeStartOrientation2 = p.getQuaternionFromEuler([0,0,yaw2])
  boxId = p.loadURDF("red_block.urdf",cubeStartPos, cubeStartOrientation0)
  boxId1 = p.loadURDF("green_block.urdf",cubeStartPos1, cubeStartOrientation1)
  boxId2 = p.loadURDF("blue_block.urdf",cubeStartPos2, cubeStartOrientation2)

  #saving the initial image...
  camera1 = p.getCameraImage(900,600)
  imarray = np.asarray(camera1[2])
  im1 = Image.fromarray(imarray.astype('uint8'))

  for i in range (250):
    p.stepSimulation()
    time.sleep(1./20.)

  camera2 = p.getCameraImage(900,600)

  #saving the image after blocks movement --> if stable this image is equal to the initial...
  im2 = Image.fromarray(camera2[2])

  #Are the images different? (Is it unstable?) --> if yes then diff is large, otherwise, diff is negligible
  diff = (camera2[2] - camera1[2]).sum()
  print("DIFFERENCE =", diff)
  if abs(diff) < 100000:
    im1.save("images/stable/image_%d.png" % n)
  else:
    im1.save("images/unstable/image_%d.png" % n)

  #cropping images
  cropped = im1.crop((350,200,550,400))
  cropped.save("images/cropped/image_%d.png" % n)

  p.disconnect()

  print ("Reached end of loop\n")

Solution

  • It's not clear from your description whether camera1[2] is a flat list of consecutive R, G, B, A values, or whether it's a list of RGBA tuples. So I'll show you how to read both options. ;)

    Your main problem is that your data doesn't contain width and height information, so we need to supply that info, somehow. One way to do that would be to read the data into a 3D Numpy array of the correct shape. But we can also do it directly in PIL by using the appropriate Image methods.

    For my demonstrations I use Python loops to create some simple RGBA data.

    This script creates a list of RGBA tuples.

    from PIL import Image
    
    maxval = 255
    width, height = 400, 300
    
    # Display size info
    size = width * height
    fmt = 'Width: {}, Height: {}, Pixels: {}, Bytes: {}'
    print(fmt.format(width, height, size, size * 4))
    
    # Make a 2D gradient that starts at black in the top left corner,
    # with red & green increasing horizontally, blue increasing vertically.
    # This would be much faster using Numpy instead of Python loops.
    pixels = []
    # Make all pixels fully opaque
    alpha = maxval
    for y in range(height):
        blu = maxval * y // height
        for x in range(width):
            red = gre = maxval * x // width
            # Make a single RGBA pixel as a tuple
            pix = red, gre, blu, alpha
            # And save it
            pixels.append(pix)
    
    # Show that the size of `pixels` is correct and show the first few pixels
    print('Size:', len(pixels))
    print(pixels[:8])
    
    # Make a new image object. All pixels are set to black. 
    img = Image.new('RGBA', (width, height))
    # Copy the pixel data to the Image
    img.putdata(pixels)
    img.show()
    img.save('test1.png') 
    

    output

    Width: 400, Height: 300, Pixels: 120000, Bytes: 480000
    Size: 120000
    [(0, 0, 0, 255), (0, 0, 0, 255), (1, 1, 0, 255), (1, 1, 0, 255), (2, 2, 0, 255), (3, 3, 0, 255), (3, 3, 0, 255), (4, 4, 0, 255)]
    

    test1.png

    2D fade from black to yellow, blue, white


    This script creates a flat list of R, G, B, A values. It uses a Python 3 bytes object, so it won't work properly on Python 2.

    from PIL import Image
    
    maxval = 255
    width, height = 400, 300
    
    # Display size info
    size = width * height
    fmt = 'Width: {}, Height: {}, Pixels: {}, Bytes: {}'
    print(fmt.format(width, height, size, size * 4))
    
    # Make a 2D gradient that starts at black in the top left corner,
    # with red & green increasing horizontally, blue increasing vertically.
    # This would be much faster using Numpy instead of Python loops.
    rgba = []
    # Make all pixels fully opaque
    alpha = maxval
    for y in range(height):
        blu = maxval * y // height
        for x in range(width):
            red = gre = maxval * x // width
            # Make a single RGBA pixel as a tuple
            pix = red, gre, blu, alpha
            # And save each of red, gre, blu, alpha to rgba. 
            # By using `.extend` we create a flat list
            rgba.extend(pix)
    
    # Show that the size of `rgba` is correct and show the first few values.
    print('Size:', len(rgba))
    print(rgba[:32])
    
    # Convert the rgba list to bytes.
    rgba = bytes(rgba)
    # Make a new image object from the bytes
    img = Image.frombytes('RGBA', (width, height), rgba)
    img.show()
    img.save('test2.png')
    

    output

    Width: 400, Height: 300, Pixels: 120000, Bytes: 480000
    Size: 480000
    [0, 0, 0, 255, 0, 0, 0, 255, 1, 1, 0, 255, 1, 1, 0, 255, 2, 2, 0, 255, 3, 3, 0, 255, 3, 3, 0, 255, 4, 4, 0, 255]
    

    The file 'test2.png' is identical to 'test1.png'.