Some notes on reading ImageData

259 views
Skip to first unread message

dio...@gmail.com

unread,
Aug 12, 2008, 10:55:20 AM8/12/08
to pyglet-users
I'm writing a drawing program in pyglet to kill time before classes
start. (Imagine "Kid Pix: The Second Coming" or something.) I had to
write flood fill and eyedropper tools, and that required that I get
the current canvas in some kind of readable format.

The code I ended up with is simple, but I had to do more hunting than
I would have liked, so I'm posting the results of my efforts here so
that the next poor guy who needs to read pixel data can find it.

I also have a color picker class if anyone's interested.

First bit of code, for the eyedropper tool (syntax-colored version at
http://rafb.net/p/8YVxys46.html):
def get_pixel_from_image(image, x, y):
#Grab 1x1-pixel image. Converting entire image to ImageData takes
much longer than just
#grabbing the single pixel with get_region() and converting just
that.
image_data = image.get_region(x,y,1,1).get_image_data()
#Get (very small) image as a string. The magic number '4' is just
len('RGBA').
data = image_data.get_data('RGBA',4)
#Convert Unicode strings to integers. Provided by Alex Holkner on the
mailing list.
components = map(ord, list(data))
#components only contains one pixel. I want to return a color that I
can pass to
#pyglet.gl.glColor4f(), so I need to put it in the 0.0-1.0 range.
return [float(c) / 255.0 for c in components]

While I was experimenting with a flood fill tool, I just called
get_pixel_from_image() every time I wanted to read in a new pixel. It
was Very Slow. To speed it up, I converted to image data all at once,
then just read values from the list.
def init(self):
#Get canvas as image. Essentially an alias for
image.get_buffer_manager().get_color_buffer().get_image_data().
self.canvas_pre = graphics.get_snapshot()
#Convert to array
data = self.canvas_pre.get_data('RGBA',self.canvas_pre.width*4)
#Convert Unicode to integer
self.pixel_data = map(ord, list(data))

Then, to read a pixel:
def get_pixel(self, x, y):
#Image data array is one-dimensional, so we need to find pixel's
position in it
pos = y * self.canvas_pre.width * 4 + x * 4
#Get pixel as an array slice and convert it to a float in the proper
range
return [float(c)/255.0 for c in self.pixel_data[pos:pos+4]]

I should mention that calling get_data() on an ImageData object using
the format in ImageData.format can be orders of magnitude faster than
other formats. I used 'RGB' format for a long time thinking that the
space saved from ignoring the alpha would matter, but even small
conversions took more than three seconds. I checked the ImageData
object's existing format, and it was RGBA. When I changed my format to
that, the function took less than a second to call rather than four or
five seconds.

I should also mention that writing a paint program in a double-
buffered environment can be interesting. I was frustrated for a few
hours before I came up with easy ways to draw everything twice.

Alex Holkner

unread,
Aug 12, 2008, 7:20:57 PM8/12/08
to pyglet...@googlegroups.com
On Wed, Aug 13, 2008 at 12:55 AM, dio...@gmail.com <dio...@gmail.com> wrote:
>
> I'm writing a drawing program in pyglet to kill time before classes
> start. (Imagine "Kid Pix: The Second Coming" or something.) I had to
> write flood fill and eyedropper tools, and that required that I get
> the current canvas in some kind of readable format.
>
> The code I ended up with is simple, but I had to do more hunting than
> I would have liked, so I'm posting the results of my efforts here so
> that the next poor guy who needs to read pixel data can find it.

There's some useful tips here -- thanks. Just some notes...

> #Convert Unicode strings to integers. Provided by Alex Holkner on the
> mailing list.
> components = map(ord, list(data))

Actually this converts 8-bit strings to integers. If you ever find
the need to map Unicode strings, use uniord instead of ord.

> #components only contains one pixel. I want to return a color that I
> can pass to
> #pyglet.gl.glColor4f(), so I need to put it in the 0.0-1.0 range.
> return [float(c) / 255.0 for c in components]

All of pyglet's new graphics and text features use colors in the range
0-255 now, so this conversion hopefully won't be necessary too often.

> I should also mention that writing a paint program in a double-
> buffered environment can be interesting. I was frustrated for a few
> hours before I came up with easy ways to draw everything twice.

I think the majority of your issues (both in ease of implementation
and performance) are due to you using the framebuffer(s) as the main
source of image data. There might be good reasons for this that I'm
unaware of, but for an "ordinary" paint program such as Photoshop or
The Gimp, it will be much easier to keep your image data in an
offscreen buffer of your choosing.

For example, you could use an array of ints, an a ctypes array of
structs, a Numpy array, ... all types that are easier to manipulate
than strings. It's much faster to convert any of these types into
texture data (e.g., using blit_into) than vice-versa. This is due not
only to the limited bandwidth in pulling image data off the video
card, but also because pyglet isn't optimised to produce image data in
any type except strings (whereas it will accept image data in several
other formats). You also won't have to worry about the
double-buffered issue.

Having said that, congratulations on getting such a difficult program working!

Alex.

dio...@gmail.com

unread,
Aug 12, 2008, 10:46:13 PM8/12/08
to pyglet-users
> Actually this converts 8-bit strings to integers.  If you ever find
> the need to map Unicode strings, use uniord instead of ord.

Thanks for correcting me on the Unicode/8-bit thing. As long as it
works...

> All of pyglet's new graphics and text features use colors in the range
> 0-255 now, so this conversion hopefully won't be necessary too often.

The conversion is more personal taste than anything else. When I
started writing this, I stored my colors as 0.0-1.0 out of habit. I'll
probably change everything to 0-255 before we release.

> I think the majority of your issues (both in ease of implementation
> and performance) are due to you using the framebuffer(s) as the main
> source of image data.  There might be good reasons for this that I'm
> unaware of, but for an "ordinary" paint program such as Photoshop or
> The Gimp, it will be much easier to keep your image data in an
> offscreen buffer of your choosing.
>
> For example, you could use an array of ints, an a ctypes array of
> structs, a Numpy array, ... all types that are easier to manipulate
> than strings.  It's much faster to convert any of these types into
> texture data (e.g., using blit_into) than vice-versa.  This is due not
> only to the limited bandwidth in pulling image data off the video
> card, but also because pyglet isn't optimised to produce image data in
> any type except strings (whereas it will accept image data in several
> other formats).  You also won't have to worry about the
> double-buffered issue.

It would be nice to have an offscreen buffer for everything, but how
would I draw primitives to it? Pyglet and OpenGL simplify a lot of
drawing code. Also, it seems to me that there would be a lot of back-
and-forth copying involved, whereas right now we're just grabbing the
canvas whenever we need to draw something temporary over it. String
conversions only happen with the paint fill tool and eyedropper.

> Having said that, congratulations on getting such a difficult program working!

Thanks! Josh and I are still on a PyOhio high, and we're trying to
carry it as far as possible before classes start again. Today, we
reached parity with MS Paint. Hooray! (Well, almost. The important
parts, anyway.)

We will probably release the source under a liberal license some time
in September. Since this program is primarily aimed at children, we
hope to inspire people to use our simple plug-in architecture to make
crazy tools. (Write a class that implements three methods, set about
five module properties, make a toolbar image, and done.) I will also
try to wrap up the juicy bits as examples.

-Steve Johnson
Reply all
Reply to author
Forward
0 new messages