Search code examples
pythonnumpymatplotlibcairo

Python cairo save figure as np array gives weird results


Try to run my example:

import numpy as np
import cairo
import matplotlib.pyplot as plt

img_size = 28
size = 0.8
color = (255, 0, 0)
thickness = 2

data = np.zeros((img_size, img_size, 3), dtype=np.uint8)
surface = cairo.ImageSurface.create_for_data(
  data, cairo.FORMAT_RGB16_565, img_size, img_size
)
cr = cairo.Context(surface)

# fill with solid white
cr.set_source_rgb(1, 1, 1)
cr.paint()


size = img_size * size - thickness
cr.rectangle((img_size - size) / 2, (img_size - size) / 2, size, size)
cr.set_line_width(thickness)
cr.set_source_rgb(*color)
cr.stroke()

surface.write_to_png("shape.png")
plt.imshow(data)
plt.savefig("shape_from_np.png")

shape.png looks like:

enter image description here

shape_from_np.png looks like:

enter image description here

I was thinking maybe I missed the format, or channels/shape, but if I print out the red layer like data[:,:,0], it shows weird 0 values at the bottom half as well, and I'm not sure why.


Solution

  • In documentation I found ImageSurface.create_from_png(...) so I used your 'shape.png' to test what values it will use for PNG image.

    import cairo
    
    surface = cairo.ImageSurface.create_from_png('shape.png')
    
    print('format:', surface.get_format())
    print('width :', surface.get_width())
    print('height:', surface.get_height())
    print('stride:', surface.get_stride())
    print('stride/width:', surface.get_stride()/surface.get_width())
    

    and this gives

    format: 1
    width : 28
    height: 28
    stride: 112
    stride/width: 4.0
    

    I found that format: 1 means cairo.Format.RGB24 and in doc cairo.Format I found it needs 32bit pixel which means 4 bytes in

    data = np.zeros((img_size, img_size, 4), dtype=np.uint8)
    

    and the same 4 gives me stride/width (stride/28)


    Using these values I can create correct image but BGR instead of RGB

    enter image description here

    So it still needs some changes.


    But cv2 uses BGR images and it saves it correctly without changes.

    import cv2
    cv2.imwrite('cv2.png', data)
    

    import numpy as np
    import cairo
    import matplotlib.pyplot as plt
    
    img_size = 28
    size = 0.8
    color = (255, 0, 0)
    thickness = 2
    
    data = np.zeros((img_size, img_size, 4), dtype=np.uint8)
    
    surface = cairo.ImageSurface.create_for_data(data, cairo.FORMAT_RGB24, img_size, img_size)
    cr = cairo.Context(surface)
    
    # fill with solid white
    cr.set_source_rgb(1, 1, 1)
    cr.paint()
    
    size = int(img_size * size) - thickness
    cr.rectangle((img_size - size) / 2, (img_size - size) / 2, size, size)
    cr.set_line_width(thickness)
    cr.set_source_rgb(*color)
    cr.stroke()
    
    plt.imshow(data)
    plt.savefig("shape_from_np.png")
    
    surface.write_to_png("shape.png")
    
    import cv2
    cv2.imwrite('cv2.png', data)
    

    Doc: ImageSurface, Format