Image Buffer - Pixel level access to images

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

# <pep8 compliant>

# Requires Blender SVN Revision 36007 (2.57rc2) or later

class Pixel:
    def __init__(self, r=0.0, g=0.0, b=0.0, a=None, colour=None):
        self.r = r
        self.g = g
        self.b = b
        self.a = a
        if colour:
            self.r = colour[0]
            self.g = colour[1]
            self.b = colour[2]
            if len(colour) > 3:
                self.a = colour[3]
        if self.a is None:
                self.a = 1.0

    def __sub__(self, other):
        return Pixel(
            self.r - other.r,
            self.g - other.g,
            self.b - other.b,
            self.a - other.a)

    def __add__(self, other):
        return Pixel(
            self.r + other.r,
            self.g + other.g,
            self.b + other.b,
            self.a + other.a)

    def __mul__(self, scalar):
        return Pixel(
            self.r * scalar,
            self.g * scalar,
            self.b * scalar,
            self.a * scalar)

    def __neg__(self):
        return self * -1

    def __repr__(self):
        return "Pixel(%s, %s, %s, %s)" % (self.r, self.g, self.b, self.a)

    def as_tuple(self):
        return (self.r, self.g, self.b, self.a)


class ImageBuffer:
    def __init__(self, image, clear=False):
        self.image = image
        self.x, self.y = self.image.size
        if clear:
            self.clear()
        else:
            self.buffer = list(self.image.pixels)

    def update(self):
        self.image.pixels = self.buffer

    def _index(self, x, y):
        if x < 0 or y < 0 or x >= self.x or y >= self.y:
            return None
        return (x + y * self.x) * 4

    def clear(self):
        self.buffer = [0.0 for i in range(self.x * self.y * 4)]

    def set_pixel(self, x, y, colour):
        index = self._index(x, y)
        if index is not None:
            self.buffer[index:index + 4] = colour.as_tuple()

    def get_pixel(self, x, y):
        index = self._index(x, y)
        if index is not None:
            return Pixel(colour=self.buffer[index:index + 4])
        else:
            return None

    def uv_to_xy(self, u, v):
        x = int(u * self.x)
        y = int(v * self.y)
        return x, y

    def draw_line(self, u1, v1, c1, u2, v2, c2, ends=True):
        '''Draws a gradient line'''
        if type(u1) == int:
            x1, y1, x2, y2 = u1, v1, u2, v2
        else:
            x1, y1 = self.uv_to_xy(u1, v1)
            x2, y2 = self.uv_to_xy(u2, v2)
        steep = abs(y2 - y1) > abs(x2 - x1)
        if steep:
            x1, y1 = y1, x1
            x2, y2 = y2, x2
        if x1 > x2:
            x1, x2 = x2, x1
            y1, y2 = y2, y1
            c1, c2 = c2, c1
        deltax = x2 - x1
        deltay = abs(y2 - y1)
        error = deltax / 2
        mix = c2 - c1
        y = y1
        if y1 < y2:
            ystep = 1
        else:
            ystep = -1
        for x in range(x1, x2 + 1):
            draw = ends or (x != x1 and x != x2)
            if not draw:
                #make sure we can skip this pixel
                if steep:
                    s, t = y, x
                else:
                    s, t = x, y
                c = self.get_pixel(s, t)
                if c is not None:
                    if sum(c.as_tuple(4)) == 0:
                        draw = True
            if draw:
                d = x - x1
                if d:
                    if d == deltax:
                        colour = c2 * 1.0  # makes sure we are using copy
                    else:
                        colour = c1 + mix * (d / deltax)
                else:
                    colour = c1 * 1.0  # makes sure we are using copy
                if steep:
                    self.set_pixel(y, x, colour)
                else:
                    self.set_pixel(x, y, colour)
            error = error - deltay
            if error < 0:
                y = y + ystep
                error = error + deltax

EDIT: New code above no longer has the speed issues. If you got the early version of this script it will no longer work on current builds of Blender, please update to the one above.

============== Original outdated post continues below ===================
Warning it’s VERY slow due to the size of the buffer to hold the pixels in Python. Currently this is the recommended way to do it though. http://cia.vc/stats/project/Blender/.message/3ca215b

If you know you are only going to write to the image, specify clear=True when you create the buffer as this will slightly improve the performance.

So is there a way to convert a procedural texture to an image buffer so we can get the pixel of a texture (not just image)?

No. You’d have to bake to an image and access it that way.

This approach is really only useful for low resolution images though. Get to 256 x 256 and you’ll start noticing the delays. Hopefully the speed will improve significantly when we can update image.pixels directly rather than using a Python list. I don’t know when that will be done by the Blender devs though.

can this be use to read a JPG color image and then you can change colorof some pixel or other things
or need to go with special lib to do that ?

Yes, you can use it to read and write pixels on a jpg.

>>> img=bpy.data.images.new("test", width=128, height=128, alpha=True)
>>> from image_buffer import *
>>> buff = ImageBuffer(img)
>>> red = Pixel(1.0, 0.0, 0.0)
>>> green = Pixel(0.0, 1.0, 0.0)
>>> blue = Pixel(0.0, 0.0, 1.0)
>>> buff.draw_line(0, 0, red, 127, 127, red)
>>> buff.draw_line(0,127, green, 127,127, red)
>>> buff.draw_line(0, 0, red, 127, 0, blue)
>>> buff.set_pixel(32, 64, red)
>>> buff.set_pixel(96, 64, red)
>>> buff.update()

so you’d just use img=bpy.data.images[‘myjpeg.jpg’] instead of creating a new image…

Note that even at 128 x 128 there’s a good 5 - 10 seconds pause when doing the buff.update(), so it’s only useful for very small images currently :frowning:

You’ll also need to force a screen refresh if the image is showing. It works fine in an operator, but from console like this you’ll need to zoom in and out or something to refresh the image display.

well just like to read and let say change some color in pic
so dont’ really need to see the pic itself while working on it
that can be done after the script has done it’s changes to thepic

but would this be faster to use external lib for files
don’t remember the lib name here but there is one specifically for files
but don’t know if this is working for 2.5 yet

The problem with this approach is reading pixels. If you start with a clear buffer, then it’s reasonably quick. I’ve been doing some hacking on the Blender code and have found a way to speed this up. I’ve just posted to bf-committers about it. I’ll have to rewrite the ImageBuffer class if the change is accepted as it’ll change the format of the pixel access.

Yeah using an external image library like PIL would be faster but comes with it’s own issues if it’s not pure python. Users of my scripts are on Windows, Linux and Macs, so it’s a support nightmare if anything non-standard is needed :slight_smile:

do you remember the script in 2.49
that was reading an image then making up a displacement map

that was an interesting script
hope to see this one in 2.5 soon ?

Speed issues are sorted with Blender 2.57rc2 - minimum SVN revision for the updated script is 36007 - enjoy!

just upated

does it means it will work a lot faster
by how much ?

thanks for the update

I’ve not done any exact timings, but before a 128 x 128 took over 10 seconds to update, now a 1024 x 1024 takes less than a second… So LOTS faster :slight_smile:

Basically it’s now usable for whatever you want to try :slight_smile:

that’s not a step it’s a giant leap man

fantastic speed improvement

like to see the same for other things in 2.5 !LOL

thanks for your insight

jsut did a test in 36007

is there a pth thing also here to be set may be

got this error
on file in lcoal folder

File “C:\Users\RJ\0blend25\0beta33908\files529\imagepixel\ima
agepixel1.py”, line 4, in <module>
KeyError: ‘bpy_prop_collection[key]: key “cy32.jpg” not found’

with these lines

img=bpy.data.images[‘cy32.jpg’]
from image_buffer import *

also tried with
‘\cy32.jpg’
but error again

image is 512 X 512

thanks for any help

Have you loaded the image already into the UV editor? If not you can load it in Python with

img = bpy.data.images.load(filepath)

where filepath is the full path and filename of the image. You wouldn’t need to do “img=bpy.data.images[‘cy32.jpg’]” then as img would already be set correctly.

i put the image in my c root
shorter path i guess

error now
imagepixel1.py", line 11
buff = ImageBuffer(img)

but there ust be a short cut to get the pic from local folder
like \ for vista
but on 2.5 i know there was a lot of problem before with these path!

i touhg this was for an image from local folder but it need to be loaded in the UV editor
but still interesting

img = bpy.data.images.load(‘cy32.jpg’)

That should work with the file in the current directory. The shortcut for current directory is // so this should also work:

img = bpy.data.images.load(’//cy32.jpg’)

If you already have the image loaded in blender then you can select it with

img = bpy.data.images[‘cy32.jpg’]

cannot find the module image Buff

is there something special about this module is it an external one
and where can i get it ?

got that for now


import bpy
from image_buffer import *
#img=bpy.data.images.new("test", width=128, height=128, alpha=True)
#img=bpy.data.images['\\cy32.jpg'] 
img = bpy.data.images.load(['\\cy32.jpg'])
 
buff = ImageBuffer(img)
red = Pixel(1.0, 0.0, 0.0)
green = Pixel(0.0, 1.0, 0.0)
blue = Pixel(0.0, 0.0, 1.0)
buff.draw_line(0, 0, red, 127, 127, red)
buff.draw_line(0,127, green, 127,127, red)
buff.draw_line(0, 0, red, 127, 0, blue)
buff.set_pixel(32, 64, red)
buff.set_pixel(96, 64, red)
buff.update()
 

should i also change the image size to fit the image from too!

by “image Buff” do you mean image_buffer? If so that’s just what I called the script in the first post when I saved it. So if you have saved that in a file called image_buffer.py in the same directory as your script, it should work. Also take the square brackets out of the load statement…

img = bpy.data.images.load([’\cy32.jpg’])

should be

img = bpy.data.images.load(’//cy32.jpg’)

i see i did not include the first part for image buffer
but now i did

and it runs

now what does it do ?

don’t see any results

i can see the image in uv editor

but then how do i know what has been change
or is there something in the first part of the code i can change to see some results

Did you force a screen refresh by zooming in and out on the image? The draw_lines in the example above draw a Z shape, the diagonal of the Z is all red coming up from bottom left of image so should be easy to spot.