Python Image Library
(a.k.a PIL
, or Pillow). Can be used to open and
manipulate image files from within Python computer
programs. We might say that it allows you
to programmatically manipulate images. This
offers functionality like Adobe Photoshop, but as
a library — so instead of a graphical
user interface (GUI), it offers an application
programming interface (API) that you can access
from code. This means that you can get Photoshop-like
functionality, but in a way that is dynamic and
scriptable. You can allow the user of your program to
specify arguments on the command line that affect image
manipulation: for example filenames, or other parameters.
Here are some of the basic operations:
from PIL import Image # open an image file: img = Image.open( "spaghetti.jpg" ) # access properties of the image, like width & height: (width, height) = img.size print( str(width) + "," + str(height) ) # get the color of a pixel by its x, y coordinates: (red, green, blue) = img.getpixel( (200,100) ) # set the color of a pixel using its x, y coordinates. # Here I'm using a blue color: img.putpixel( (200,100), (155,155,255) ) # save an image, specifying format with file name & extension: img.save("new-spaghetti.png")
modulo: an operator to calculate the remainder. A very powerful and useful operator when working with algorithms. Let's look at some examples:
for i in range(1000): # So this loop will repeat 1000 times, with i = 0, 1, 2, 3 ... 999. # Let's look at some different uses of modulo and what they would do: print( i % 5 ) # will display 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1 ... etc if i % 2 == 0: print("spaghetti") # This will print 'spaghetti' when i is an even number if i % 50 == 0: print("meatballs") # This will print 'meatballs' whenever i a multiple of 50 if i % 50 < 3: print("parmessan") # This will print 'parmessan' when i is: # 0, 1, 2, 50, 51, 52, 100, 101, 102, etc
Today we're going to build on our reading discussion this week about the aleatory in computation by learning some techniques in computer program code for working with randomness. One of the things that we read and discussed was where the aleatory comes from within the rigid, formal strictures of digital machinery; how can randomness be generated from deterministic processes? We will not be going into the technical details today around how to generate random and pseudo-random numbers. This topic involves complicated mathematics and we could spend an entire semester (or more!) just talking about those mathematic techniques. Instead, we will start with the fact that random numbers are in fact possible to generate, and we will learn the commands that Python provides to generate them.
Building on this, we will look at techniques for working with algorithms that incorporate randomness, or in other words, algorithms that are probabilistic: step-by-step procedures that incorporate and use values that are unpredictable, but within various ranges and likelihoods, which we can shape and control.
In spite of all our conversations about the complexity of
generating random numbers, Python implements various random
number generating algorithms and provides them to us as a
collection of easy-to-use commands. These are provided by
a library (in Python also known as
a module) aptly called random
.
To get started, simply import this module:
import random
So far, I have been sharing with you all the various commands and functions that you have been working with. But I want to point you to the Python documentation itself, so that you may explore by accessing it directly:
This is the authoritative standard for all things Python. It is comprehensive and accurate, describing all aspects of the language. There are many other guides out there (w3schools.com is good, as is this guide called Automate the Boring Stuff with Python, by Al Sweigart) and these can be great. But if you want to go straight to the source for definitive information, the official Python docs cannot be beat.Official programming language documentation like this can be hard to read. I often need to read slowly and carefuly, and think hard about the precise meanings of the terms that are used here. I usually have to read an explanation a few times before I completely wrap my head around all of its details and implications. But since this is the official documentation, that scrutiny is usually worth it for the thorough understanding that it can offer. Unlike much of what one might find when searching for programming help online, official documentation like this a trustworthy and reliable source of information.
In particular, I recommend you look at the tutorial for an instructive introduction, and most powerfully, the "Library Reference", which will tell you everything that you need to know (and more) about Python and its commands.
Scroll about 1-2 screens down, and in the "Numeric and Mathematic Modules" section, find the entry called random, which explains how to "Generate pseudo-random numbers."
To get started, let's look at the function
called random()
,
which is essentially the heart of this module. To get
acquainted, let's experiment with this in the Python shell:
>>> import random >>> random.random() 0.5536582109886334 >>> random.random() 0.519728888240708 >>> random.random() 0.21552893690804353As you can see,
random()
returns an unpredictable
decimal number between 0 and 1.
There are numerous other shortcuts to make working with randomness easier. But let's start by spending some time with this one.
Using only this function, if you wanted a random integer (a number with no decimal places) between 0 and 9, what might you do? Have a look at this code and think about what it's doing:
>>> int( random.random() * 10 )First of all, taking a decimal number between 0 and 1 and multiplying it by 10 is going to give us another number that is at minimum 0, and at maximum 10. Think about what you would get if you multiply 10 times .9999 (close to the largest value of
random()
). You'll get 9.999.
To summarize this: when you are starting with a number in the
range of 0 to 1, multiplying scales a range. You
are expanding that range of 0 to 1, into the range of 0 and
whatever you multiply it by. random.random() * 100
is going to give us a random decimal number somewhere between 0
and 100. This is a useful principle to keep in mind.
But I said I wanted an integer (whole
number). That is what the int()
command does. It
truncates (or cuts off) the decimal part of the number,
returning a whole number from a decimal point. If you'd prefer,
you could also use round()
, which doesn't simply
truncate, but actually rounds:
>>> int(.1) 0 >>> int(.6) 0 >>> round(.1) 0 >>> round(.6) 1
OK. Now, what if I wanted a number that was between 50 and 100?
What is the range here? 50. So I could use my scaling technique
like this: random.random() * 50
. But that is going
to give a number between 0 and 50, not between 50 and 100. What
can I do here? I could simply add 50: 50 + random.random()
* 50
. Now I'm taking a random number scaled to the range
of 0 to 50, and adding 50 to it, shifting that range to 50 to
100. To summarize this principle: when working with numbers like
this, addition shifts a range.
Multiplying scales a range, and adding shifts the
range. Using these techniques, you can shape the range
of values we get from random.random()
and
manipulate them to span whatever range of randomness we
wish. These are very useful principles for creative coding.
Fortunately, Python gives us some commands that we can use directly to achieve the same principles. (I think it is important and valuable later on to understand what is going on here, hence the above explanation before this reveal.)
If you want a randomly generated whole number (integer) from
within some range, you can
use: random.randrange(start,stop)
. (Python
docs for this.) This takes two arguments and returns a
random integer in between those arguments. So for example:
>>> random.randrange(5,10) 6 >>> random.randrange(5,10) 8 >>> random.randrange(5,10) 5If you only pass in one argument, it returns a number between 0 and that argument:
>>> random.randrange(10) 8 >>> random.randrange(10) 3 >>> random.randrange(10) 1
Another very useful idea in working with randomness is the idea
of a random sequence of numbers that are reproducible. This can
be useful for testing for example. Let's say you want to draw
something that looks random, but you want it to look random the
same way each time you run your code. You don't want it to be
different each time. There is a command for
this: random.seed()
When you call random.seed()
with one numerical
argument, every time you call random()
after that,
it will always return the same random-looking sequence. For
example, if I run Python twice, I get two different random
numbers each time:
$ python >>> import random >>> random.random() 0.9998781067887237
$ python >>> import random >>> random.randrange(1) 0.6638400757429336But if I call
random.seed()
, I still get a random number,
but it is the same each time:
$ python >>> import random >>> random.seed(42) >>> random.random() 0.7534071414623441
$ python >>> import random >>> random.seed(42) >>> random.random() 0.7534071414623441
This is called a pseudo-random number (it was discussed in the 10 PRINT book) because the numbers are drawn from a uniform distribution, so they appear random, but they are in fact deterministic.
This is very very useful in game development. For example, say
you want to generate a character or a natural terrain, and you
want its appearance to seem random. But you want that apperance
to be the same each time the game is run, not changing every
time. If that character has a name or ID number, you could use
that as the seed. Each time you want to render that character,
call seed()
with their ID number, and the calls
to random()
that come after will seem random, but
will be random in the same way each time.
As pointed out in class, this is precisely how the seed function works in Minecraft!
What if I want to generate some kind of digital object, say an
image with randomly placed colored pixels on a white background,
and I want to control how many colored pixels there are? You can
ask if
statements about random choices to control
the probability.
For example, have a look at this code, which creates a new blank image, loops over all the pixels, and sets some of them to a color based on a random value:
Example 1
from PIL import Image import random # let's make a 100x100 white image width = 100 height = 100 img = Image.new("RGB", (width,height), (255,255,255) ) for y in range(height): for x in range(width): r = random.random() if r > .5: img.putpixel( (x,y), (0,0,0) ) img.save("rando.png")
And what if I simply want there to be less black pixels in that image? Have a look at this:
Example 2
from PIL import Image
import random
# let's make a 100x100 white image
width = 100
height = 100
img = Image.new("RGB", (width,height), (255,255,255) )
for y in range(height):
for x in range(width):
r = random.random()
if r > .9:
img.putpixel( (x,y), (0,0,0) )
img.save("rando.png")
In Example 1, a pixel is drawn about 50% of the time: assuming a uniform distribution, a random number between 0 and 1 will be greater than .5 about half of the time. In Example 2 however, I am now only drawing the pixel if the random choice is greater than .9, so this will draw a pixel about 10% of the time. Notice the more sparse image.
As one last technique, let's start by considering this example:
Example 3
from PIL import Image import random # let's make a 100x100 white image width = 100 height = 100 img = Image.new("RGB", (width,height), (255,255,255) ) # loop 500 times, and each time, pick a random x and a random y # and draw a pixel there for n in range(500): x = int( random.random() * 100 ) y = int( random.random() * 100 ) img.putpixel( (x,y), (0,0,0) ) img.save("rando.png")
But what if we didn't want the pixels evenly spaced out. What if we wanted them randomized, but kind of more clustered at the top?
Example 4
from PIL import Image import random # let's make a 100x100 white image width = 100 height = 100 img = Image.new("RGB", (width,height), (255,255,255) ) # loop 500 times, and each time, pick a random x and a random y # and draw a pixel there for n in range(500): x = int( random.random() * 100 ) r = random.random() y = int( r * r * 100 ) img.putpixel( (x,y), (0,0,0) ) img.save("rando.png")
I'm sure this is probably a little hard to understand. What I'm doing here is choosing a random number for y, but multiplying it times itself. If you remember back to algebra, and graphing functions, remember that plotting x-squared (x to the second power) looks like a slightly curved shape. (See image.) What I'm doing here is taking the random number between 0 and 1 and squaring it, so it changes the shape of the probability. It means that there is a greater chance the numbers will be smaller. You can play around with a graphing calculator to think about how different functions have different shapes, and how you can use a random variable here as the input to shape the resulting distribution. In addition to x squared, think about x cubed, or x to the fourth power; you can also think about the square root of x, or 1 divided by x.
What if I want to do something with randomness that is not just generating a number, but rather randomly selecting something from a list? I could do that like this:
>>> import random >>> a = ['Birds', 'flying', 'high', 'you', 'know', 'how', 'I', 'feel'] >>> i = random.randrange( len(a) ) >>> a[i] 'know' >>> i = random.randrange( len(a) ) >>> a[i] 'Birds'What I'm doing here is creating a list called
a
,
then remember that len(a)
gives me the length of
that list. So I'm passing the length of the list in to the
command random.randrange()
. That will always give
me a value between 0 and the length of the list. I then use that
value as the index to my list.
Turns out this is such a common operation that Python makes a
shortcut for it: choice()
. If you pass a list to
this function, it will randomly select one item from the list:
>>> random.choice(a) 'know' >>> random.choice(a) 'how'
Let's use this principle to randomly select a file. But first, we have to take a detour and think about more ways to access files.
Let's say that I am in a directory (folder) called "Unit 1 exercise 4", and that it contains a subbolder called "images", like this:
From the command line, I can use ls
to
view the contents:
$ ls images $ ls images/ earth.jpg fire.jpg newspaper.png smoke.jpgNow I'm going to run Python and use a function called
listdir()
, which takes the name of a
directory, and returns a list of all the files in that
directory. This is in the os
library, so I must
import it first:
>>> from os import listdir >>> listdir("images") ['newspaper.png', 'fire.jpg', 'earth.jpg', 'smoke.jpg']Here I have passed in the name
images
,
the name of my subfolder, and listdir()
gives me an
array of all files within that. Then I could choose a random
file in that list like this:
>>> from os import listdir >>> import random >>> files = listdir("images") >>> file = random.choice(files)Now I could go and use
file
as I would any filename
— for example to open that file with
Pillow. This technique will be very useful in getting started with the homework for this week .