[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
------------------------------------------------------