Up: Multimedia Programming

Counting Stars

September 18th, 2011 Leave a comment Go to comments
slide 001 Hello, and welcome to the second episode of the Software Carpentry lecture on multimedia programming. In this episode, we’ll look at how to count how many stars there are in a picture of the night sky.
slide 002 As we saw in the previous episode, we can use the Python Imaging Library, or PIL, to convert a color image like this…
slide 003 …into a monochrome image like this, where black represents bright pixels and white represents background.
slide 004 Our question is, how many stars are there? Counting the number of black pixels would be easy, but we want to know how many joined-up blobs of black pixels we have.
slide 005 A blob is just a set of pixels that are adjacent to one another.
slide 006 We could use several rules to determine what “adjacent” means, but the simplest is 4-way connectivity: pixels are adjacent if they are directly above, below, left, or right from each other, not if they are touching diagonally.
slide 007 We want to count each such blob once.
slide 008 So we’ll scan the image left to right, top to bottom
slide 009 Each time we find a new blob, we’ll increment our count.
slide 010 But how do we tell whether the pixel we’re looking at is part of a new blob or not?
slide 011 One way is to mark the pixels we’ve already seen by turning them red.
slide 012 If the pixel we’re looking at touches one that has already been turned red, then it’s part of a blob we have already counted. We’ll turn it red to show that we have looked at it, but we won’t count it as a star, since it belongs to a star we’ve already counted.
slide 013 A picture will make this clearer. We scan the image until we find a black pixel…
slide 014 …and mark it as seen by turning it red.
slide 015 Nothing above it or to its left is already red. It must be the first pixel in a new blob, so we increment our star counter.
slide 016 And keep scanning.
slide 017 This pixel does have a red predecessor (the one we just colored in), so it’s not the start of a new star.
slide 018 Eventually, we have colored in all of this star, and we move on to the next.
slide 019 But there’s a problem: the pixel shown here is definitely part of our second star, but neither of the pixels above it or to its left has already been colored in—the pixel that connects it to the rest of its star is to its right.
slide 020 We can fix this by looking in more directions, as shown here: if there are any colored pixels in the row above this one, or to its right, it isn’t the start of a new star.
slide 021 But our new rule still gives the wrong answer in cases like this one, where two stars touch on a diagonal corner.
slide 022 We could try to patch our algorithm again by changing our rule and considering blobs that are connected on diagonals as part of a single star…
slide 023 But this still gives the wrong answer in cases like this. In fact, any algorithm that only looks at the immediate neighbors of a pixel will give the wrong answer in some cases: we have to back up and reconsider our approach entirely.
slide 024 Instead of trying to decide whether pixels are connected one by one, let’s use flood fill to color in all the pixels in a star when it is first encountered.
slide 025 As before, we scan the image until we find a black pixel, then mark it red.
slide 026 Instead of continuing our scan right away, we look at the newly-filled pixel’s four neighbors.
slide 027 For each one that is currently black—i.e., that is part of this star, but hasn’t yet been colored in—we fill it in…
slide 028 …then immediately look at its neighbors to find other pixels that need coloring in.
slide 029 We keep doing this until the whole star is colored…
slide 030 …and only then re-start our scan to find other blobs.
slide 031 Here’s our main function. As before, it scans the image top to bottom and left to right using a nested loop.
slide 032 When it finds a black pixel…
slide 033 …it increments the count of the number of stars seen…
slide 034 …then calls ‘fill’ to fill in all the pixels connected to that one.
slide 035 The ‘fill’ function works by keeping a list of pixels that need to be looked at, but haven’t yet been filled in.
Such a list is often called a job list or work queue.
slide 036 It is simply a list of the (x,y) coordinates of pixels that are neighbors of ones we have already examined.
slide 037 The ‘fill’ function keeps looping until there’s nothing left in this list.
slide 038 On each pass through the loop, it takes the (x,y) coordinates of a pixel from the queue…
slide 039 If that pixel is black, ‘fill’ colors it red and adds its neighbors to the queue.
slide 040 Here’s what the function looks like.
slide 041 It begins by putting the (x,y) coordinates of the first pixel into the queue.
slide 042 While there are still pixels to process…
slide 043 …it gets the (x,y) coordinates of the next pixel in the queue and removes that pair from the queue.
slide 044 If the pixel is currently black, ‘fill’ turns it red to mark it as having been seen.
slide 045 Then adds the pixel to its left to the work queue (unless this pixel is on the left border of the image).
slide 046 ‘fill’ then does the same for the pixel to the right, above, and below the current one. This ensures that everything next to this pixel will eventually be looked at.
slide 047 Here’s the ‘fill’ algorithm in action. Our initial pixel is (1, 2). We turn it red…
slide 048 …and put its four neighbors in the queue.
slide 049 The next three loops aren’t very interesting, since the pixels left, above, and right of our starting point aren’t black.
On the fourth pass, though, when our queue only contains one lonely pixel, we find that it is black, so we fill it in…
slide 050 …and put its neighbors in the queue.
slide 051 The next time around the loop, we will look at our starting point (1, 2) a second time.
slide 052 This is wasteful, so as an exercise, rewrite ‘fill’ so that it never examines a pixel twice.
There are at least two ways to do this; see if you can find and implement both.
slide 053 Another way to write flood fill is to use a recursive algorithm.
slide 054 This keeps the work to be done on the call stack, instead of maintaining an explicit work queue.
slide 055 Here’s the recursive version of fill.
slide 056 It starts by checking the pixel it has been asked to look at.
If that pixel is not black, this ‘fill’ returns immediately: there is nothing for it to do.
slide 057 If the pixel is black, ‘fill’ turns it red…
slide 058 …and then calls ‘fill’ on its left-hand neighbor (if it has one).
slide 059 It then goes on to call ‘fill’ for the right-hand, top, and bottom neighbors as well.
slide 060 This has the same effect as using an explicit work queue; each call to ‘fill’ takes care of one pixel, then calls ‘fill’ again to take care of the neighbors.
While novices find the work queue approach more intuitive, experienced programmers tend to prefer recursion, since it is usually simpler to reason about.
We’ll have a closer look at recursive algorithms in future episodes.

  1. Michael
    December 26th, 2010 at 20:43 | #1

    What a great video!

    Although on the internet there is an abundance of materials on Python’s syntax and general usage, there are comparatively few like this video which offer insight into creative ways to solve problems, which for me is what computing is all about.

    Thanks very much.

  2. Andrew Dalke
    July 12th, 2011 at 11:55 | #2

    As an algorithmic point, using a Python list like this to implement a queue gives an O(n**2) performance for some large blobs because of the queue = queue[1:]. (BTW, x,y=queue.pop(0) would also work, and collections.deque().popleft() would be even better.) Since the algorithm works equally well with a queue or a stack, then using the list as a stack and doing a stack.pop() would give you the expected amortized O(1) performance and would map directly to the call stack version. On the other hand, pictures of stars are rarely more than a few pixels in size, so this isn’t a real concern for this problem.

  1. No trackbacks yet.