[Image-SIG] Scaling image pixels by a variable amount.

Terry Hancock hancock@anansispaceworks.com
Fri, 01 Mar 2002 15:08:45 -0800


Hi all,
This was a major pain to figure out, so I thought I'd
share my experience:

The PIL API doesn't appear to provide a simple means of
scaling the value of image pixels (e.g. I want to multiply
every pixel by some value S -- and I don't know what S
is when I write the program).  This is not necessarily
a bad thing, and it's clear that you're supposed to use
the ".point()" method to do this, though it can be
a little tricky, as I want to demonstrate here.

Naively, you might follow the Image.point examples, just
substituting a variable for the constant scale factor used
in the examples:

def return_scaled_image( S ):
	im = Image.open('example.png')
	im.point( lambda x : x * S )
	return im

But this doesn't work -- lambda defines a function, and
therefore a new name space -- and remember that Python
namespaces do not nest! Thus, we can only use S if it's
global, e.g.:

def return_scaled_image( S ):
	global S_g
	S_g = S
	im.point( lambda x : x * S_g )
	return im
	
This *does* work, but it has a subtle problem, which I found
out about only after I embedded the generated images into
a Zope-based webpage.  That problem is that this isn't
thread-safe!  I've made S_g global, so it is a state variable
in the module. If different instances of return_scaled_image()
are running in parallel, there will be a race-condition on
S_g and the results will be somewhat random.  I knew I didn't
like it, but now I know why.

I next tried replacing the lambda with a callable class, but
that doesn't work, because the isSequenceType() test inside
PIL (incorrectly?) identifies the class as a sequence (I don't
know if this should be considered a bug, but I don't like it),
and it then tries to use the mapping interpretation, which
fails, of course. I also tried doing the mapping myself, but
this also failed, for reasons I never really discovered. Perhaps
some more specific test is possible?

Just for grins, here's what that implementation looked like
(though remember that this *doesn't* work):

class scale_operator_class:
	def __init__(self):
		self.scaleby = 1.0
	def __call__(self, pixel):
		return pixel * self.scaleby

def return_scaled_image( S ):
	im = Image.open('example.png')
	scale_operator = scale_operator_class()
	scale_operator.scaleby = S
	im.point( scale_operator )
	return im

Finally, I came up with this, which works, and is thread-safe:

class scale_operator_class:
	def __init__(self):
		self.scaleby = 1.0
	def applyscale(self, pixel):
		return pixel * self.scaleby

def return_scaled_image( S ):
	im = Image.open('example.png')
	scale_operator = scale_operator_class()
	scale_operator.scaleby = S
	im.point( scale_operator.applyscale )
	return im

Perhaps this should have been obvious -- but it took me
awhile to figure it out.  Or perhaps it's clever, and so
it's a good idea to share it. But anyway, there you have it.

I didn't put the scaleby attribute in the __init__()
function because I actually wanted to reuse the same
instance several times in the same function (just
change scaleby each time), instead of defining a new
one each time.

If I have overlooked some better way to do this, or am
missing some obvious shortcoming of this approach, I'd be
interested in comments.  Thanks!

Terry

-- 
------------------------------------------------------
Terry Hancock
hancock@anansispaceworks.com       
Anansi Spaceworks                 
http://www.anansispaceworks.com 
P.O. Box 60583                     
Pasadena, CA 91116-6583
------------------------------------------------------