Difference between revisions of "CS110:Lab09"

From IPRE Wiki
Jump to: navigation, search
('''Making Music''')
Line 1: Line 1:
='''Lab 09: Exploring Music''' =
+
='''Lab 08: Graphics Part II: Making Photoshop Functions''' =
 +
 
  
 
=='''Objective'''==
 
=='''Objective'''==
* Learn some fundamentals of sound and music
+
* Create functions to manipulate images
* Create musical compositions
+
  
=='''Useful Resources'''==
+
== '''Image Transformations ''' ==
  
[[ChucK]]
+
You've probably used a variety of graphics editors such as ''Paint, Adobe Photoshop, GIMP'',
 +
etc. to manipulate or enhance your pictures. In this lab session, you will be exploring and
 +
creating functions to transform your images in the same way these image editors are able to. <br>
  
=='''Sound'''==
+
''Image transformations'' are operations that change the appearance of an image or its spatial location.
Having explored and used many of the robot commands by now, you have seen that your robot make beeps when you call the '''beep()''' function. You can also have Myro make a beep directly out of your computer, rather than the robot. For instance, if you execute the following command:
+
Common image transformations include '''resize (shrink''' or '''enlarge)''', '''rotate''', '''crop''',
 +
'''translate (i.e''' '''move'''), and '''flip'''. There are also functions to change the color or brightness
 +
of an image. <br>
  
<pre>
+
In this lab, we will explore four functions: resize, colorize, translate and paste. At the end of this session,
computer.beep(3, 880)
+
you will be able to use this functions (and create your own) to transform your images into beautiful mosaics. Once again, you are
</pre>
+
only limited by your imagination and creativity.
  
This command tells your computer to play a tone at 880 Hertz for 3 seconds. Hertz is
+
== '''Resizing an Image: Shrinking and Enlarging ''' ==
a unit that measures frequency. <br>
+
  
<b>1Hertz = 1cycle / second </b>
+
Below is a program that takes an image and shrink it by a specified factor, F.
 
+
For instance, if the original image is 1000x1000 pixels and you wanted to shrink
Therefore, a beep at 880 Hz represents 880 complete cycles per second.
+
it by a factor of 5, you would end up with an image of size 200x200.  
Humans can hear frequencies in the 20 Hz to 20000 Hz (or 20 Kilo Hertz) range and
+
are able to distinguish sounds that differ only by a few
+
Hertz (as little as 1 Hz). This ability varies from person to person.
+
 
+
Try the following commands and see if you can distinguish between the two tones:
+
  
 
<pre>
 
<pre>
computer.beep(1, 440)
+
def main():
computer.beep(1, 450)
+
    # read an image and display it
 +
    oldPic = makePicture(pickAFile())
 +
    show(oldPic, "Before")
 +
    X = getWidth(oldPic)
 +
    Y = getHeight(oldPic)
 +
    # Input the shrink factor and computer size of new image
 +
    F = int(ask("Enter the shrink factor."))
 +
    newx = X/F
 +
    newy = Y/F
 +
    # create the new image
 +
    newPic = makePicture(newx, newy)
 +
    for x in range(newx):
 +
        for y in range(newy):
 +
            setPixel(newPic, x, y, getPixel(oldPic, x*F, y*F))
 +
    show(newPic, "After")
 
</pre>
 
</pre>
  
To make the tones more distinctive, place the commands above in a loop so
+
The image below on the right is 425x400 pixels in dimension. After shrinking it by a factor of 2,
that you can repeatedly hear the alternating tones. <br>
+
the result is the image on the right with dimensions 212x200. <br>
  
 +
[[image:Asimo2.GIF|Original Image (425x400)]] [[Image:Asimo3.GIF|After Shrinking (212x200, F=2)]]
  
'''Do This:''' Program your computer to create a siren by repeating two
+
'''Exercise 1: ''' Write a function ('''resize''') that gives the user the option to '''enlarge'''
different tones. You will have to experiment
+
or '''shrink''' an image by some factor. The user should have the option to choose this factor as well.
with different pairs of frequencies (they may be close together or far apart) to
+
Your function should display the new image after it has been resized.
produce a realistic sounding siren. Write your program to play the siren for 15
+
seconds. The louder the better!
+
  
=='''Musical Scales'''==
+
== '''Changing Colors in your Image ''' ==
  
In western music, a ''scale'' is divided into 12 notes (from 7 major notes:
+
In the previous labs, you've manipulated the pixels in the pictures your robot take
ABCDEFG). An ''octave'' in C comprises of the 12 notes shown below: <br>
+
to identify various objects such as the orange pyramid. In this section you will
 +
experiment with changing colors in other images (these do not have to be pictures your robots take).
 +
Recall that you can define your own colors using the makeColor function. For instance,
 +
to define a color called awesomeness with RGB values 156,120,47 you would use the following code:
  
<b> C C#/Db D D#/Eb E F F#/Gb G G#/Ab A A#/Bb B </b> <br><br>
+
<pre>
 +
awesomeness = makeColor(156,120,47)
 +
</pre>
  
C# (pronounced "C sharp") is the same tone as Db (pronounced "D flat").
+
Also, you can load pictures from your computer with the makePicture() function:
Frequencies corresponding to a specific note, for example C, are multiplied (or
+
divided) by 2 to generate the same note in a higher (or lower) octave. For instance
+
in the two tones shown below, the second tone is one octave higher than the first: <br><br>
+
computer.beep(1, 440)<br>
+
computer.beep(1, 880) <br> <br>
+
 
+
Therefore in order to raise a tone by 1 octave, you multiply the frequency by 2.
+
Likewise, to make a tone 1 octave lower, you divide by 2.
+
Notes indicating an octave can be denoted as follows: <br>
+
<b>C0 C1 C2 C3 C4 C5 C6 C7 C8 </b><br><br>
+
That is, C0 is the note for C in the lowest (or 0) octave. The fifth octave
+
(numbered 4) is commonly referred to as a middle octave. Thus C4 is the C
+
note in the middle octave. The frequency corresponding to C4 is 261.63 Hz. <br> <br>
+
 
+
'''Do This:'''
+
Try playing '''C4''' on the computer. Also try C5 (523.25) which is twice the
+
frequency of C4 and C3 (130.815). <br>
+
 
+
=='''Computing the Computer's Range of Tones'''==
+
 
+
In common tuning, the 12 notes are equidistant. Therfore, if the frequency doubles every octave, each successive note is 21 / 12 apart. That is, if C4 is 261.63 Hz, C# (or Db) will be:
+
 
+
C#4/Db4 = 261.63 *2 ^(1/12) = 277.18
+
 
+
We can then compute all successive note frequencies:
+
 
+
* D4 = 277.18 * 2 ^ (1/12) = 293.66
+
* D#4/Eb = 293.66 * 2 ^ (1/12) = 311.13 
+
* etc. 
+
 
+
'''Note: ''' In python, the characters that denote the exponent are '''**'''. Therefore to raise 2 by the exponent 3, you would type:
+
  
 
<pre>
 
<pre>
2 ** 3
+
mySavedPicture = makePicture("robots.jpg")
 +
show(mySavedPicture)
 
</pre>
 
</pre>
  
The lowest tone that the Computer can play is A0 and the highest tone is C8. A0 has a frequency of 27.5 Hz, and C8 has a frequency of 4186 Hz. That's quite a range! See if you can you hear the entire range. Try this:
+
To navigate and then select the image to load, use the following combination:
 
+
 
<pre>
 
<pre>
computer.beep(1, 27.5)
+
mySavedPicture = makePicture(pickAFile())
computer.beep(1, 4186)
+
show(mySavedPicture)
 
</pre>
 
</pre>
  
'''Do This:''' Write a program to play all the 12 notes in an octave
+
When you call the pickAFile command, a navigational dialog box is displayed. You can
using the above computation. You may assume in your program that C0 is
+
use this to navigate to any folder and select a file to open as any JPEG image.  
16.35 and then use that to compute all frequencies in a given octave (C4 is
+
16.35 * 24). Your program should input an octave (a number from 0 through
+
8), produce all the notes in that octave and also printout a frequency chart for
+
each note in that octave.
+
  
== '''Making Music''' ==
+
The red pixels in the first butterfly below have been changed to blue and its yellow pixels
 +
to red.
  
Now we turn from beeps to music. First, let's make an easy method of playing notes by name, rather than by frequencies:
+
[[Image:Butterfly1.JPG]]<--- '''Butterfly1''' [[Image:Butterfly2.JPG]]<--- '''Butterfly2'''
  
<pre>
+
You can get the original butterfly (on the left) with this command:
from myro import *
+
  
def getFrequency(noteName):
+
>>> butterfly = makePicture("http://wiki.roboteducation.org/Image:Butterfly1.JPG")
    return media._frequency[noteName.lower()]
+
</pre>
+
  
To use this, you can give it the name of a note as a string, and get back the frequency. For example:
+
'''Exercise 2: ''' Write a program that takes butterfly1 and changes two or more of the colors in the image
 +
to produce an output like butterfly2. To use image butterfly1, right click on the image and select ''Save Image As''.
  
<pre>
+
== '''Translating an Image ''' ==
>>> getFrequency("C4")
+
261.60000000000002
+
  
>>> computer.beep(1, getFrequency("C4"))
+
In this section you will apply what you already know about Graphics to another type of image
[plays a lovely note at that frequency for 1 second]
+
transformation, translation. A ''translation'' is simply a movement in the x,y plane of your window.
</pre>
+
When you were introduced to graphics, you created a variety of polygons as well as points and lines,
 
+
drew these objects in a graphics window and then manipulated them
You can also transcribe an entire song as a string using the makeSong() function in Myro. For example, to get the frequencies and timings for "A4 1/4; C5 1/8; C5 1/8; A4 1/4" you could:
+
in various ways. You can also treat an image like an object and move it around in your canvas in the same way
 
+
you did with rectangles, circles etc. To convert a picture your robot takes into an ''image'', use the following:
>>> makeSong("A4 1/4; C5 1/8; C5 1/8; A4 1/4")
+
[(440.0, 0.25), (523.29999999999995, 0.125), (523.29999999999995, 0.125), (440.0, 0.25)]
+
 
+
To use this, you could:
+
  
 
<pre>
 
<pre>
for freq, note in makeSong("A4 1/4; C5 1/8; C5 1/8; A4 1/4"):
+
picture = takePicture()
    # convert notes (eq, 1/4) into seconds:
+
pixmap  = makePixmap(picture)
    beat = note * 4 * 0.4              # 1/4 = one beat, which lasts for 0.4 seconds
+
image = Image(Point(x, y), pixmap)
    computer.beep(beat, freq)
+
 
</pre>
 
</pre>
  
You can also put the song in a separate file and use '''readSong(filename)'''. For more details on this function, see [[Song File Format]].
+
If the picture was taken by your camera or downloaded from the web, you will need to use the
 
+
makePicture command first so that the Myro commands will work on it. The code below creates two pixmap
Now, explore the functions of [[ChucK]] i.e. '''Run the example commands you see!!''' ChucK is already installed on the machines in the lab so you can skip the section that deals with installation if you are doing these exercises in the lab. You will need to understand the basic operations to continue. <br>
+
images, draws these images on a canvas and then translates them within the canvas using the move command.  
  
After you have used the functions in ChucK, you will see that you can change the
 
frequencies of different instruments pretty easily by using the '''setFrequency''' command.
 
So instead of changing the frequency of the computer and making it beep, you could change the
 
frequency of a mandolin and pluck its strings using commands similar to the ones
 
you just executed:
 
  
 
<pre>
 
<pre>
man = Mandolin() #create a mandolin instrument
+
myCanvas = GraphWin("My BigCanvas", 400, 400)
man.connect()
+
oldPic = makePicture(pickAFile())
for freq, note in makeSong("A4 1/4; C5 1/8; C5 1/8; A4 1/4"):
+
#show(oldPic)
    man.setFrequency(freq)
+
pixmap  = makePixmap(oldPic)
    wait(0.3)
+
butterfly1 = Image(Point(50, 50), pixmap)
    m.pluck(note)
+
butterfly2 = Image(Point(300, 300), pixmap)
 +
butterfly1.draw(myCanvas)
 +
butterfly2.draw(myCanvas)
 +
butterfly1.move(50,50)
 +
butterfly2.move(-50,-50)
 
</pre>
 
</pre>
  
== Creating Instruments and Parts ==
+
The picture you choose must be smaller than your canvas. An ideal size is 100x100.
 +
The images below show the translation of two butterflies. As specified by the code above,
 +
butterfly1 starts out in the upper left corner and is translated 50 pixels along the x axis and the y axis.
 +
Butterfly2 starts out near the lower right corner and is translated -50 pixels along the x axis and the y axis.
  
For this example, we will create a 7 measure song, each measure having three beats. Let's define a beat to be 0.4 seconds long.
+
[[Image:Translate1.JPG]] <--- Before Translation [[Image:Translate2.JPG]] <--- After Translation
  
First, let's start with a percussion background. If we make a quarter note be one beat, then there are 3 quarter notes in a measure. This is known as 3 4 time.
+
== '''Copying and Pasting an Image ''' ==
  
Let's have our percussion play a quarter note, eighth note, eighth note, quarter.  
+
Copying an image can prove useful in image transformations and other applications. Below you will find functions for copying and pasting an image onto a canvas.
  
To make this a little easier, let's create a generic function that will tell an instrument to play a note at a given strength, and wait a certain amount of time. This is very similar to how we move a robot:
+
<pre>
 +
from myro import *
  
# helper function to play one sound / note
+
def createCanvas():
def playOnce(instrument, time, strength):
+
     canvas = GraphWin("My BigCanvas", 200, 200)
     instrument.noteOn(strength)
+
     pic = makePicture(pickAFile())
     wait(time)
+
    return [pic, canvas]
  
Next, we define the percussion instrument (a Shaker) and its 7-measure part:
+
def makeGray(pic):
 +
    pic2 = copyPicture(pic)
 +
    for pix in getPixels(pic2):
 +
        r, g, b = getRGB(pix)
 +
        gray = (r + g + b) / 3
 +
        setRGB(pix, [gray, gray, gray])
 +
    return pic2
  
def playShakers():
+
def paste(pic1, x, y, pic2):
     shakers = Shakers()
+
     newpic = copyPicture(pic1)
    shakers.connect()
+
     for col in range(getWidth(pic2)):
    beat = 0.4
+
         for row in range(getHeight(pic2)):
     for i in range(7):
+
            setPixel(newpic, col + x, row + y,  
         playOnce(shakers, beat, 1)
+
                    getColor(getPixel(pic2, col, row)))
        playOnce(shakers, beat/2, .8)
+
     return newpic
        playOnce(shakers, beat/2, .8)
+
        playOnce(shakers, beat, 1)      
+
     shakers.noteOn(1)
+
  
Notice that this repeats 7 measure of the 1/4 1/8 1/8 1/4 pattern. To leave the shakers in a normal state, we issue the command '''shakers.noteOn(1)'''. Also, we define the instrument as a function of no parameters. This is important for later.
 
  
You can test it with:
+
def main():
 
+
  butterfly = makePicture("http://wiki.roboteducation.org/Image:Butterfly1.JPG")
>>> playShakers()
+
  bigpic = makePicture(500, 500)
 
+
  bigpic = paste(bigpic, 10, 10, butterfly)
And let's define another instrument, and its part:
+
  bigpic = paste(bigpic, 200, 200, makeGray(butterfly))
 
+
  show(bigpic)
def playBar():
+
</pre>
    bar = StruckBar()
+
    bar.connect()
+
    beat = 0.4
+
    wait(beat * 6) # wait for 2 measures / loop iterations
+
    for i in range(5):
+
        playOnce(bar, beat/2, .8)       
+
        playOnce(bar, beat/2, .8)
+
        playOnce(bar, beat, 1)
+
        playOnce(bar, beat, 1)
+
    bar.noteOn(1)
+
 
+
and test it:
+
 
+
>>> playBar()
+
 
+
Notice how it waits for two measures (6 beats) and then plays 5 measures.
+
 
+
And finally, a Mandolin part:
+
 
+
def playMandolin():
+
    m = Mandolin()
+
    m.connect()
+
    m.setGain(0.3)
+
    beat = 0.4
+
    wait(beat * 12) # wait for 4 measures / loop iterations
+
    for i in range(3):
+
        playOnce(m, beat, 1)
+
        playOnce(m, beat, 1)
+
        playOnce(m, beat/2, .8)
+
        playOnce(m, beat/2, .8)
+
    m.noteOn(1)
+
 
+
Notice that it waits 4 measures (12 beats) and then plays 3 measures.
+
 
+
This Mandolin part plays all the same note. You can change the frequency of such instruments using the '''setFrequency()''' method. See [[ChucK]] for more details. Combined with the '''makeSong()''' function (above) you can create beautiful duets between say, a Violin and a Piano.
+
 
+
== '''Creating an Orchestra'''==
+
 
+
To play multiple parts together, we'll use the '''doTogether''' function. It takes a series of function names, and plays them together.
+
  
>>> doTogether(playShakers, playBar, playMandolin)
+
[[Image:CopyPaste.JPG]]
  
NOTE: don't put this line in your file. Instead, type it in the Python Shell.
+
= Assignment 08: Adding More Functions to your Photoshop Library =
  
= Assignment 09 =
+
'''Part I '''<br>
 +
Exercises 1 and 2 of Lab 8 <br>
  
'''Write a piece of music and perform it:''' The composition should be at least 1 minute 30 seconds in length. You should use at least three instruments, and one of those should use different frequencies. Your assignment will be graded on style of code, and style of music. Demos will be done the following week. HINT: you might want to find some musical compositions on the web to play some nice music. For example, a search for "notes to ode to joy" found this: [http://wiki.answers.com/Q/What_are_the_notes_ode_to_joy ode_to_joy].
+
'''Part II'''<br>
 +
'''1. Horizontal Flip: ''' Write a function that will flip an image horizontally as displayed below: <br>
 +
[[Image:HorizonFlip.jpg]] <br>
  
'''Bonus points''' will be given for extra components: more than three parts, use of frequencies, harmony, variety of instruments, fast parts/slow parts, etc.
+
'''2. Collage: ''' Write a program to create a mosaic of images on your canvas. Your functions should utilize the translate, horizontal flip and the colorize function you create in class. ''The colorize function can be found on the link to Photolib on the main course page.''
  
 
= Links to Course-Related Pages =
 
= Links to Course-Related Pages =
 
* Back to [http://wiki.roboteducation.org/CS110_Lab Lab Home Page]  
 
* Back to [http://wiki.roboteducation.org/CS110_Lab Lab Home Page]  
 
* Back to [http://cs.brynmawr.edu/Courses/cs110/fall2009/ Course Home Page]
 
* Back to [http://cs.brynmawr.edu/Courses/cs110/fall2009/ Course Home Page]

Revision as of 15:37, 18 February 2010

Lab 08: Graphics Part II: Making Photoshop Functions

Objective

  • Create functions to manipulate images

Image Transformations

You've probably used a variety of graphics editors such as Paint, Adobe Photoshop, GIMP, etc. to manipulate or enhance your pictures. In this lab session, you will be exploring and creating functions to transform your images in the same way these image editors are able to.

Image transformations are operations that change the appearance of an image or its spatial location. Common image transformations include resize (shrink or enlarge), rotate, crop, translate (i.e move), and flip. There are also functions to change the color or brightness of an image.

In this lab, we will explore four functions: resize, colorize, translate and paste. At the end of this session, you will be able to use this functions (and create your own) to transform your images into beautiful mosaics. Once again, you are only limited by your imagination and creativity.

Resizing an Image: Shrinking and Enlarging

Below is a program that takes an image and shrink it by a specified factor, F. For instance, if the original image is 1000x1000 pixels and you wanted to shrink it by a factor of 5, you would end up with an image of size 200x200.

def main():
    # read an image and display it
    oldPic = makePicture(pickAFile())
    show(oldPic, "Before")
    X = getWidth(oldPic)
    Y = getHeight(oldPic)
    # Input the shrink factor and computer size of new image
    F = int(ask("Enter the shrink factor."))
    newx = X/F
    newy = Y/F
    # create the new image
    newPic = makePicture(newx, newy)
    for x in range(newx):
        for y in range(newy):
            setPixel(newPic, x, y, getPixel(oldPic, x*F, y*F))
    show(newPic, "After")

The image below on the right is 425x400 pixels in dimension. After shrinking it by a factor of 2, the result is the image on the right with dimensions 212x200.

Original Image (425x400) After Shrinking (212x200, F=2)

Exercise 1: Write a function (resize) that gives the user the option to enlarge or shrink an image by some factor. The user should have the option to choose this factor as well. Your function should display the new image after it has been resized.

Changing Colors in your Image

In the previous labs, you've manipulated the pixels in the pictures your robot take to identify various objects such as the orange pyramid. In this section you will experiment with changing colors in other images (these do not have to be pictures your robots take). Recall that you can define your own colors using the makeColor function. For instance, to define a color called awesomeness with RGB values 156,120,47 you would use the following code:

awesomeness = makeColor(156,120,47)

Also, you can load pictures from your computer with the makePicture() function:

mySavedPicture = makePicture("robots.jpg")
show(mySavedPicture)

To navigate and then select the image to load, use the following combination:

mySavedPicture = makePicture(pickAFile())
show(mySavedPicture)

When you call the pickAFile command, a navigational dialog box is displayed. You can use this to navigate to any folder and select a file to open as any JPEG image.

The red pixels in the first butterfly below have been changed to blue and its yellow pixels to red.

Butterfly1.JPG<--- Butterfly1 Butterfly2.JPG<--- Butterfly2

You can get the original butterfly (on the left) with this command:

>>> butterfly = makePicture("http://wiki.roboteducation.org/Image:Butterfly1.JPG")

Exercise 2: Write a program that takes butterfly1 and changes two or more of the colors in the image to produce an output like butterfly2. To use image butterfly1, right click on the image and select Save Image As.

Translating an Image

In this section you will apply what you already know about Graphics to another type of image transformation, translation. A translation is simply a movement in the x,y plane of your window. When you were introduced to graphics, you created a variety of polygons as well as points and lines, drew these objects in a graphics window and then manipulated them in various ways. You can also treat an image like an object and move it around in your canvas in the same way you did with rectangles, circles etc. To convert a picture your robot takes into an image, use the following:

picture = takePicture()
pixmap  = makePixmap(picture)
image = Image(Point(x, y), pixmap)

If the picture was taken by your camera or downloaded from the web, you will need to use the makePicture command first so that the Myro commands will work on it. The code below creates two pixmap images, draws these images on a canvas and then translates them within the canvas using the move command.


myCanvas = GraphWin("My BigCanvas", 400, 400)
oldPic = makePicture(pickAFile())
#show(oldPic)
pixmap  = makePixmap(oldPic)
butterfly1 = Image(Point(50, 50), pixmap)
butterfly2 = Image(Point(300, 300), pixmap)
butterfly1.draw(myCanvas)
butterfly2.draw(myCanvas)
butterfly1.move(50,50)
butterfly2.move(-50,-50)

The picture you choose must be smaller than your canvas. An ideal size is 100x100. The images below show the translation of two butterflies. As specified by the code above, butterfly1 starts out in the upper left corner and is translated 50 pixels along the x axis and the y axis. Butterfly2 starts out near the lower right corner and is translated -50 pixels along the x axis and the y axis.

Translate1.JPG <--- Before Translation Translate2.JPG <--- After Translation

Copying and Pasting an Image

Copying an image can prove useful in image transformations and other applications. Below you will find functions for copying and pasting an image onto a canvas.

from myro import *

def createCanvas():
    canvas = GraphWin("My BigCanvas", 200, 200)
    pic = makePicture(pickAFile())
    return [pic, canvas]

def makeGray(pic):
    pic2 = copyPicture(pic)
    for pix in getPixels(pic2):
        r, g, b = getRGB(pix)
        gray = (r + g + b) / 3
        setRGB(pix, [gray, gray, gray])
    return pic2

def paste(pic1, x, y, pic2):
    newpic = copyPicture(pic1)
    for col in range(getWidth(pic2)):
        for row in range(getHeight(pic2)):
            setPixel(newpic, col + x, row + y, 
                     getColor(getPixel(pic2, col, row)))
    return newpic


def main():
   butterfly = makePicture("http://wiki.roboteducation.org/Image:Butterfly1.JPG")
   bigpic = makePicture(500, 500)
   bigpic = paste(bigpic, 10, 10, butterfly)
   bigpic = paste(bigpic, 200, 200, makeGray(butterfly))
   show(bigpic)

CopyPaste.JPG

Assignment 08: Adding More Functions to your Photoshop Library

Part I
Exercises 1 and 2 of Lab 8

Part II
1. Horizontal Flip: Write a function that will flip an image horizontally as displayed below:
HorizonFlip.jpg

2. Collage: Write a program to create a mosaic of images on your canvas. Your functions should utilize the translate, horizontal flip and the colorize function you create in class. The colorize function can be found on the link to Photolib on the main course page.

Links to Course-Related Pages