The personal website of Scott W Harden

Dynamic Logo Generation with Python

I'm working on a software project and I'd love the startup screen to be unique. I want the splash image to be generated programmatically. I have to generate it from the script, but how can I reliably print text on an image with fonts vary across operating systems? Keep in mind many OS's don't have "arial.ttf", or no truetype fonts at all!

First, I created a 2D binary array to represent the alphabet using pixel fonts such as this as a reference. The word I'm trying to create is "QRSS VD" (the name of my program). I store the data in strings, as seen below. I use 1's to mark pixels, and spaces to mark empty spaces.

data="""
1111 1111 1111 1111   1  1 111
1  1 1  1 1    1      1  1 1  1
1  1 1111 1111 1111   1  1 1  1
1 11 1 1     1    1   1 1  1  1
1111 1 11 1111 1111    1   111
"""

Once I think it looks nice, I replace the spaces with zeros to make it take up a lot of visual space...

data2="""
1111011110111101111000100101110
1001010010100001000000100101001
1001011110111101111000100101001
1011010100000100001000101001001
1111010110111101111000010001110
"""

Then I further obscure it by replacing linebreaks with a different symbol, such as the number 2, then break the lines so they're not lined up...

b="1111011110111101111000100101110210010100101000010000001001010012"
b+="1001011110111101111000100101001210110101000001000010001010010012"
b+="1111010110111101111000010001110"

The result is a pretty cool way to obscure the text. I don't know why you'd want to, but if you want to make sure that no one goes in and changes the letters around (at least without making them think pretty hard about it) you could look at ways to further encrypt this data stream. From here, I create an image using the Python Imaging Library, setting pixel values to 255*b[x,y] (so 0 stays 0 and 1 becomes 255, perfect for an 8-bit image). After enlarging, here's the result:

Now let's make it a little bit less pixelated. It's not a cure-all method, but blurring it up a little with a bilinear filter helps a lot...

Then I apply the code below which applies a cool colormap to the pixel values. I'll provide cleaner code for this later (I have a really cool way of generating colormaps and saving them as arrays of RGB tuples). Then I go through and plot some random sin wavs on top of it. Sweet! Here are 6 images generated from the program run 6 times. Notice the randomness of the sine wavs!

A different image is generated every time the script runs, and it requires no external files (bitmaps or fonts) and should work well on all operating systems.

from PIL import Image
from PIL import ImageOps
from PIL import ImageFilter
from random import randint
import scipy


def genLogo():
    colormap = [(0, 0, 129), (0, 0, 134), (0, 0, 139), (0, 0, 143), (0, 0, 148), (0, 0, 152), (0, 0, 157), (0, 0, 161), (0, 0, 166), (0, 0, 170), (0, 0, 175), (0, 0, 180), (0, 0, 184), (0, 0, 189), (0, 0, 193), (0, 0, 198), (0, 0, 202), (0, 0, 207), (0, 0, 211), (0, 0, 216), (0, 0, 220), (0, 0, 225), (0, 0, 230), (0, 0, 234), (0, 0, 239), (0, 0, 243), (0, 0, 248), (0, 0, 252), (0, 0, 255), (0, 0, 255), (0, 0, 255), (0, 0, 255), (0, 2, 255), (0, 7, 255), (0, 11, 255), (0, 14, 255), (0, 18, 255), (0, 23, 255), (0, 27, 255), (0, 31, 255), (0, 34, 255), (0, 39, 255), (0, 43, 255), (0, 47, 255), (0, 51, 255), (0, 54, 255), (0, 59, 255), (0, 63, 255), (0, 67, 255), (0, 71, 255), (0, 75, 255), (0, 79, 255), (0, 83, 255), (0, 87, 255), (0, 91, 255), (0, 95, 255), (0, 99, 255), (0, 103, 255), (0, 107, 255), (0, 111, 255), (0, 115, 255), (0, 119, 255), (0, 123, 255), (0, 127, 255), (0, 131, 255), (0, 135, 255), (0, 139, 255), (0, 143, 255), (0, 147, 255), (0, 151, 255), (0, 155, 255), (0, 159, 255), (0, 163, 255), (0, 167, 255), (0, 171, 255), (0, 175, 255), (0, 179, 255), (0, 183, 255), (0, 187, 255), (0, 191, 255), (0, 195, 255), (0, 199, 255), (0, 203, 255), (0, 207, 255), (0, 211, 255), (0, 215, 255), (0, 219, 254), (0, 223, 251), (0, 227, 248), (2, 231, 245), (5, 235, 241), (7, 239, 238), (11, 243, 235), (14, 247, 232), (18, 251, 228), (21, 255, 225), (23, 255, 222), (27, 255, 219), (31, 255, 215), (34, 255, 212), (37, 255, 208), (40, 255, 205), (44, 255, 203), (47, 255, 199), (50, 255, 195), (54, 255, 192), (57, 255, 189), (60, 255, 186), (63, 255, 183), (66, 255, 179), (70, 255, 176), (73, 255, 173), (76, 255, 170), (79, 255, 166), (83, 255, 163), (86, 255, 160), (89, 255, 157), (92, 255, 154), (95, 255, 150), (99, 255, 147), (102, 255, 144), (105, 255, 141), (108, 255, 137), (112, 255, 134), (115, 255, 131), (118, 255, 128), (121, 255, 125), (124, 255, 121),
                (128, 255, 118), (131, 255, 115), (134, 255, 112), (137, 255, 108), (141, 255, 105), (144, 255, 102), (147, 255, 99), (150, 255, 95), (154, 255, 92), (157, 255, 89), (160, 255, 86), (163, 255, 83), (166, 255, 79), (170, 255, 76), (173, 255, 73), (176, 255, 70), (179, 255, 66), (183, 255, 63), (186, 255, 60), (189, 255, 57), (192, 255, 54), (195, 255, 50), (199, 255, 47), (202, 255, 44), (205, 255, 41), (208, 255, 37), (212, 255, 34), (215, 255, 31), (218, 255, 28), (221, 255, 24), (224, 255, 21), (228, 255, 18), (231, 255, 15), (234, 255, 12), (238, 255, 8), (241, 252, 5), (244, 248, 2), (247, 244, 0), (250, 240, 0), (254, 236, 0), (255, 233, 0), (255, 229, 0), (255, 226, 0), (255, 221, 0), (255, 218, 0), (255, 215, 0), (255, 211, 0), (255, 207, 0), (255, 203, 0), (255, 199, 0), (255, 196, 0), (255, 192, 0), (255, 188, 0), (255, 184, 0), (255, 180, 0), (255, 177, 0), (255, 173, 0), (255, 169, 0), (255, 165, 0), (255, 162, 0), (255, 159, 0), (255, 155, 0), (255, 151, 0), (255, 147, 0), (255, 143, 0), (255, 140, 0), (255, 136, 0), (255, 132, 0), (255, 128, 0), (255, 125, 0), (255, 121, 0), (255, 117, 0), (255, 114, 0), (255, 110, 0), (255, 106, 0), (255, 102, 0), (255, 99, 0), (255, 95, 0), (255, 91, 0), (255, 88, 0), (255, 84, 0), (255, 80, 0), (255, 76, 0), (255, 73, 0), (255, 69, 0), (255, 65, 0), (255, 62, 0), (255, 58, 0), (255, 54, 0), (255, 51, 0), (255, 47, 0), (255, 43, 0), (255, 39, 0), (255, 36, 0), (255, 32, 0), (255, 28, 0), (255, 25, 0), (255, 21, 0), (253, 17, 0), (248, 14, 0), (244, 10, 0), (240, 6, 0), (235, 2, 0), (230, 0, 0), (225, 0, 0), (221, 0, 0), (217, 0, 0), (212, 0, 0), (207, 0, 0), (203, 0, 0), (198, 0, 0), (194, 0, 0), (189, 0, 0), (185, 0, 0), (180, 0, 0), (175, 0, 0), (171, 0, 0), (166, 0, 0), (162, 0, 0), (157, 0, 0), (152, 0, 0), (148, 0, 0), (144, 0, 0), (139, 0, 0), (134, 0, 0), (130, 0, 0), (134, 0, 0), (130, 0, 0)]

    def red(val):
        return colormap[val][0]

    def green(val):
        return colormap[val][1]

    def blue(val):
        return colormap[val][2]

    def colorize(im):
        r = Image.eval(im, red)
        g = Image.eval(im, green)
        b = Image.eval(im, blue)
        im = Image.merge("RGB", (r, g, b))
        return im
    b = "1111011110111101111000100101110210010100101000010000001001010012"
    b += "1001011110111101111000100101001210110101000001000010001010010012"
    b += "1111010110111101111000010001110"
    b = b.split("2")
    im = Image.new("L", (33+15, 7+13))
    data = im.load()
    for y in range(len(b)):
        for x in range(len(b[y])):
            data[x+6, y+6] = int(b[y][x])*255
    scale = 15
    im = im.resize((im.size[0]*scale, im.size[1]*scale))
    data = im.load()

    def drawSin(width, height, vertoffset, horizoffset, thickness, darkness):
        for x in range(im.size[0]):
            y = scipy.sin((x-horizoffset)/float(width))*height+vertoffset
            for i in range(thickness):
                if 0 <= y+i < im.size[1] and 0 <= x < im.size[0]:
                    # print x,im.size[0],y+i,im.size[1]
                    data[x, y+i] = data[x, y+i]+darkness

    for i in range(5):
        print "line", i
        drawSin(randint(5, 75), randint(-100, 200), randint(0, im.size[1]),
                randint(0, im.size[0]), randint(3, 15), 70)
    for i in range(10):
        im = im.filter(ImageFilter.SMOOTH_MORE)
    im = colorize(im)
    return im


im = genLogo()
im.save('logo.png', "PNG")
Markdown source code last modified on January 18th, 2021
---
title: Dynamic Logo Generation with Python
date: 2010-03-07 18:47:00
tags: python, old
---

# Dynamic Logo Generation with Python

__I'm working on a software project and I'd love the startup screen to be unique.__ I want the splash image to be generated programmatically. I have to generate it from the script, but how can I reliably print text on an image with fonts vary across operating systems? Keep in mind many OS's don't have "arial.ttf", or no truetype fonts at all!

__First, I created a 2D binary array to represent the alphabet__ using pixel fonts [such as this](http://img.nattawat.org/images/yus3i81of22r503eyjtw.png) as a reference. The word I'm trying to create is "QRSS VD" (the name of my program). I store the data in strings, as seen below. I use 1's to mark pixels, and spaces to mark empty spaces.

```python
data="""
1111 1111 1111 1111   1  1 111
1  1 1  1 1    1      1  1 1  1
1  1 1111 1111 1111   1  1 1  1
1 11 1 1     1    1   1 1  1  1
1111 1 11 1111 1111    1   111
"""
```

__Once I think it looks nice__, I replace the spaces with zeros to make it take up a lot of visual space...

```python
data2="""
1111011110111101111000100101110
1001010010100001000000100101001
1001011110111101111000100101001
1011010100000100001000101001001
1111010110111101111000010001110
"""
```

__Then I further obscure it__ by replacing linebreaks with a different symbol, such as the number 2, then break the lines so they're not lined up...

```python
b="1111011110111101111000100101110210010100101000010000001001010012"
b+="1001011110111101111000100101001210110101000001000010001010010012"
b+="1111010110111101111000010001110"
```

__The result is a pretty cool way__ to obscure the text. I don't know why you'd want to, but if you want to make sure that no one goes in and changes the letters around (at least without making them think pretty hard about it) you could look at ways to further encrypt this data stream. From here, I create an image using the Python Imaging Library, setting pixel values to 255\*b\[x,y\] (so 0 stays 0 and 1 becomes 255, perfect for an 8-bit image). After enlarging, here's the result:

<div class="text-center img-border">

[![](nearest_thumb.jpg)](nearest.png)

</div>

Now let's make it a little bit less pixelated. It's not a cure-all method, but blurring it up a little with a bilinear filter helps a lot...

<div class="text-center img-border">

[![](bilinear_thumb.jpg)](bilinear.png)

</div>

Then I apply the code below which applies a cool colormap to the pixel values. I'll provide cleaner code for this later (I have a really cool way of generating colormaps and saving them as arrays of RGB tuples). Then I go through and plot some random sin wavs on top of it. Sweet! Here are 6 images generated from the program run 6 times. Notice the randomness of the sine wavs!

<div class="text-center img-border img-micro">

[![](qrss-vd-logo-0_thumb.jpg)](qrss-vd-logo-0.png)
[![](qrss-vd-logo-1_thumb.jpg)](qrss-vd-logo-1.png)
[![](qrss-vd-logo-2_thumb.jpg)](qrss-vd-logo-2.png)
[![](qrss-vd-logo-3_thumb.jpg)](qrss-vd-logo-3.png)
[![](qrss-vd-logo-4_thumb.jpg)](qrss-vd-logo-4.png)
[![](qrss-vd-logo-5_thumb.jpg)](qrss-vd-logo-5.png)

</div>

__A different image is generated every time the script runs, __and it requires no external files (bitmaps or fonts) and should work well on all operating systems.

```python
from PIL import Image
from PIL import ImageOps
from PIL import ImageFilter
from random import randint
import scipy


def genLogo():
    colormap = [(0, 0, 129), (0, 0, 134), (0, 0, 139), (0, 0, 143), (0, 0, 148), (0, 0, 152), (0, 0, 157), (0, 0, 161), (0, 0, 166), (0, 0, 170), (0, 0, 175), (0, 0, 180), (0, 0, 184), (0, 0, 189), (0, 0, 193), (0, 0, 198), (0, 0, 202), (0, 0, 207), (0, 0, 211), (0, 0, 216), (0, 0, 220), (0, 0, 225), (0, 0, 230), (0, 0, 234), (0, 0, 239), (0, 0, 243), (0, 0, 248), (0, 0, 252), (0, 0, 255), (0, 0, 255), (0, 0, 255), (0, 0, 255), (0, 2, 255), (0, 7, 255), (0, 11, 255), (0, 14, 255), (0, 18, 255), (0, 23, 255), (0, 27, 255), (0, 31, 255), (0, 34, 255), (0, 39, 255), (0, 43, 255), (0, 47, 255), (0, 51, 255), (0, 54, 255), (0, 59, 255), (0, 63, 255), (0, 67, 255), (0, 71, 255), (0, 75, 255), (0, 79, 255), (0, 83, 255), (0, 87, 255), (0, 91, 255), (0, 95, 255), (0, 99, 255), (0, 103, 255), (0, 107, 255), (0, 111, 255), (0, 115, 255), (0, 119, 255), (0, 123, 255), (0, 127, 255), (0, 131, 255), (0, 135, 255), (0, 139, 255), (0, 143, 255), (0, 147, 255), (0, 151, 255), (0, 155, 255), (0, 159, 255), (0, 163, 255), (0, 167, 255), (0, 171, 255), (0, 175, 255), (0, 179, 255), (0, 183, 255), (0, 187, 255), (0, 191, 255), (0, 195, 255), (0, 199, 255), (0, 203, 255), (0, 207, 255), (0, 211, 255), (0, 215, 255), (0, 219, 254), (0, 223, 251), (0, 227, 248), (2, 231, 245), (5, 235, 241), (7, 239, 238), (11, 243, 235), (14, 247, 232), (18, 251, 228), (21, 255, 225), (23, 255, 222), (27, 255, 219), (31, 255, 215), (34, 255, 212), (37, 255, 208), (40, 255, 205), (44, 255, 203), (47, 255, 199), (50, 255, 195), (54, 255, 192), (57, 255, 189), (60, 255, 186), (63, 255, 183), (66, 255, 179), (70, 255, 176), (73, 255, 173), (76, 255, 170), (79, 255, 166), (83, 255, 163), (86, 255, 160), (89, 255, 157), (92, 255, 154), (95, 255, 150), (99, 255, 147), (102, 255, 144), (105, 255, 141), (108, 255, 137), (112, 255, 134), (115, 255, 131), (118, 255, 128), (121, 255, 125), (124, 255, 121),
                (128, 255, 118), (131, 255, 115), (134, 255, 112), (137, 255, 108), (141, 255, 105), (144, 255, 102), (147, 255, 99), (150, 255, 95), (154, 255, 92), (157, 255, 89), (160, 255, 86), (163, 255, 83), (166, 255, 79), (170, 255, 76), (173, 255, 73), (176, 255, 70), (179, 255, 66), (183, 255, 63), (186, 255, 60), (189, 255, 57), (192, 255, 54), (195, 255, 50), (199, 255, 47), (202, 255, 44), (205, 255, 41), (208, 255, 37), (212, 255, 34), (215, 255, 31), (218, 255, 28), (221, 255, 24), (224, 255, 21), (228, 255, 18), (231, 255, 15), (234, 255, 12), (238, 255, 8), (241, 252, 5), (244, 248, 2), (247, 244, 0), (250, 240, 0), (254, 236, 0), (255, 233, 0), (255, 229, 0), (255, 226, 0), (255, 221, 0), (255, 218, 0), (255, 215, 0), (255, 211, 0), (255, 207, 0), (255, 203, 0), (255, 199, 0), (255, 196, 0), (255, 192, 0), (255, 188, 0), (255, 184, 0), (255, 180, 0), (255, 177, 0), (255, 173, 0), (255, 169, 0), (255, 165, 0), (255, 162, 0), (255, 159, 0), (255, 155, 0), (255, 151, 0), (255, 147, 0), (255, 143, 0), (255, 140, 0), (255, 136, 0), (255, 132, 0), (255, 128, 0), (255, 125, 0), (255, 121, 0), (255, 117, 0), (255, 114, 0), (255, 110, 0), (255, 106, 0), (255, 102, 0), (255, 99, 0), (255, 95, 0), (255, 91, 0), (255, 88, 0), (255, 84, 0), (255, 80, 0), (255, 76, 0), (255, 73, 0), (255, 69, 0), (255, 65, 0), (255, 62, 0), (255, 58, 0), (255, 54, 0), (255, 51, 0), (255, 47, 0), (255, 43, 0), (255, 39, 0), (255, 36, 0), (255, 32, 0), (255, 28, 0), (255, 25, 0), (255, 21, 0), (253, 17, 0), (248, 14, 0), (244, 10, 0), (240, 6, 0), (235, 2, 0), (230, 0, 0), (225, 0, 0), (221, 0, 0), (217, 0, 0), (212, 0, 0), (207, 0, 0), (203, 0, 0), (198, 0, 0), (194, 0, 0), (189, 0, 0), (185, 0, 0), (180, 0, 0), (175, 0, 0), (171, 0, 0), (166, 0, 0), (162, 0, 0), (157, 0, 0), (152, 0, 0), (148, 0, 0), (144, 0, 0), (139, 0, 0), (134, 0, 0), (130, 0, 0), (134, 0, 0), (130, 0, 0)]

    def red(val):
        return colormap[val][0]

    def green(val):
        return colormap[val][1]

    def blue(val):
        return colormap[val][2]

    def colorize(im):
        r = Image.eval(im, red)
        g = Image.eval(im, green)
        b = Image.eval(im, blue)
        im = Image.merge("RGB", (r, g, b))
        return im
    b = "1111011110111101111000100101110210010100101000010000001001010012"
    b += "1001011110111101111000100101001210110101000001000010001010010012"
    b += "1111010110111101111000010001110"
    b = b.split("2")
    im = Image.new("L", (33+15, 7+13))
    data = im.load()
    for y in range(len(b)):
        for x in range(len(b[y])):
            data[x+6, y+6] = int(b[y][x])*255
    scale = 15
    im = im.resize((im.size[0]*scale, im.size[1]*scale))
    data = im.load()

    def drawSin(width, height, vertoffset, horizoffset, thickness, darkness):
        for x in range(im.size[0]):
            y = scipy.sin((x-horizoffset)/float(width))*height+vertoffset
            for i in range(thickness):
                if 0 <= y+i < im.size[1] and 0 <= x < im.size[0]:
                    # print x,im.size[0],y+i,im.size[1]
                    data[x, y+i] = data[x, y+i]+darkness

    for i in range(5):
        print "line", i
        drawSin(randint(5, 75), randint(-100, 200), randint(0, im.size[1]),
                randint(0, im.size[0]), randint(3, 15), 70)
    for i in range(10):
        im = im.filter(ImageFilter.SMOOTH_MORE)
    im = colorize(im)
    return im


im = genLogo()
im.save('logo.png', "PNG")
```

Smoothly Scroll an Image Across a Window with Tkinter vs. PyGame

The goal is simple: have a very large image (larger than the window) automatically scroll across a Python-generated GUI window. I already have the code created to generate spectrograph images in real time, now I just need a way to have them displayed in real time. At first I tried moving the coordinates of my images and even generating new images with create_image(), but everything I did resulted in a tacky "flickering" effect (not to mention it was slow). Thankfully I found that self.canv.move(self.imgtag,-1,0) can move a specific item (self.imgtag) by a specified amount and it does it smoothly (without flickering). Here's some sample code. Make sure "snip.bmp" is a big image in the same folder as this script

from Tkinter import *
import Image
import ImageTk


class scrollingImage(Frame):

    def go(self):
        self.canv.move(self.imgtag, -1, 0)
        self.canv.update()
        self.after(100, self.go)

    def __init__(self, parent=None):
        Frame.__init__(self, parent)
        self.master.title("Spectrogram Viewer")
        self.pack(expand=YES, fill=BOTH)
        self.canv = Canvas(self, relief=SUNKEN)
        self.canv.config(width=200, height=200)
        self.canv.config(highlightthickness=0)

        sbarV = Scrollbar(self, orient=VERTICAL)
        sbarH = Scrollbar(self, orient=HORIZONTAL)

        sbarV.config(command=self.canv.yview)
        sbarH.config(command=self.canv.xview)

        self.canv.config(yscrollcommand=sbarV.set)
        self.canv.config(xscrollcommand=sbarH.set)

        sbarV.pack(side=RIGHT, fill=Y)
        sbarH.pack(side=BOTTOM, fill=X)

        self.canv.pack(side=LEFT, expand=YES, fill=BOTH)
        self.im = Image.open("./snip.bmp")
        width, height = self.im.size
        # self.canv.config(scrollregion=(0,0,width,height))
        self.canv.config(scrollregion=(0, 0, 300, 300))
        self.im2 = ImageTk.PhotoImage(self.im)
        x, y = 0, 0
        self.imgtag = self.canv.create_image(x, y,
                                             anchor="nw", image=self.im2)
        self.go()

scrollingImage().mainloop()

Alternatively, I found a way to accomplish a similar thing with PyGame. I've decided not to use PyGame for my software package however, because it's too specific and can't be run well alongside Tk windows, and it would be insanely hard to add scrollbars to the window. However it's extremely effective at scrolling images smoothly. Anyhow, here's the code:

import pygame
from PIL import Image

im = Image.open("1hr_original.jpg")
graphic = pygame.image.fromstring(im.tostring(), im.size, im.mode)
screen = pygame.display.set_mode((400, 300))
clock = pygame.time.Clock()
running = 1
x, y = 0, 0
while running:
    clock.tick(30)
    for event in pygame.event.get():  # get user input
        if event.type == pygame.QUIT:  # if user clicks the close X
            running = 0  # make running 0 to break out of loop
    screen.blit(graphic, (x, y))
    pygame.display.flip()  # Update screen
    x -= 1
Markdown source code last modified on January 18th, 2021
---
title: Smoothly Scroll an Image Across a Window with Tkinter vs. PyGame
date: 2010-03-05 08:49:00
tags: python, old
---

# Smoothly Scroll an Image Across a Window with Tkinter vs. PyGame

<div class="text-center img-border">

[![](tk-scrolling_thumb.jpg)](tk-scrolling.png)

</div>

__The goal is simple: have a very large image (larger than the window) automatically scroll across a Python-generated GUI window.__ I already have the code created to generate spectrograph images in real time, now I just need a way to have them displayed in real time. At first I tried moving the coordinates of my images and even generating new images with create\_image(), but everything I did resulted in a tacky "flickering" effect (not to mention it was slow). Thankfully I found that `` self.canv.move(self.imgtag,-1,0) `` can move a specific item (self.imgtag) by a specified amount and it does it smoothly (without flickering). Here's some sample code. Make sure "snip.bmp" is a big image in the same folder as this script

```python
from Tkinter import *
import Image
import ImageTk


class scrollingImage(Frame):

    def go(self):
        self.canv.move(self.imgtag, -1, 0)
        self.canv.update()
        self.after(100, self.go)

    def __init__(self, parent=None):
        Frame.__init__(self, parent)
        self.master.title("Spectrogram Viewer")
        self.pack(expand=YES, fill=BOTH)
        self.canv = Canvas(self, relief=SUNKEN)
        self.canv.config(width=200, height=200)
        self.canv.config(highlightthickness=0)

        sbarV = Scrollbar(self, orient=VERTICAL)
        sbarH = Scrollbar(self, orient=HORIZONTAL)

        sbarV.config(command=self.canv.yview)
        sbarH.config(command=self.canv.xview)

        self.canv.config(yscrollcommand=sbarV.set)
        self.canv.config(xscrollcommand=sbarH.set)

        sbarV.pack(side=RIGHT, fill=Y)
        sbarH.pack(side=BOTTOM, fill=X)

        self.canv.pack(side=LEFT, expand=YES, fill=BOTH)
        self.im = Image.open("./snip.bmp")
        width, height = self.im.size
        # self.canv.config(scrollregion=(0,0,width,height))
        self.canv.config(scrollregion=(0, 0, 300, 300))
        self.im2 = ImageTk.PhotoImage(self.im)
        x, y = 0, 0
        self.imgtag = self.canv.create_image(x, y,
                                             anchor="nw", image=self.im2)
        self.go()

scrollingImage().mainloop()
```

__Alternatively, I found a way to accomplish a similar thing with PyGame.__ I've decided not to use PyGame for my software package however, because it's too specific and can't be run well alongside Tk windows, and it would be insanely hard to add scrollbars to the window. However it's extremely effective at scrolling images smoothly. Anyhow, here's the code:

```python
import pygame
from PIL import Image

im = Image.open("1hr_original.jpg")
graphic = pygame.image.fromstring(im.tostring(), im.size, im.mode)
screen = pygame.display.set_mode((400, 300))
clock = pygame.time.Clock()
running = 1
x, y = 0, 0
while running:
    clock.tick(30)
    for event in pygame.event.get():  # get user input
        if event.type == pygame.QUIT:  # if user clicks the close X
            running = 0  # make running 0 to break out of loop
    screen.blit(graphic, (x, y))
    pygame.display.flip()  # Update screen
    x -= 1
```

Realtime FFT Graph of Audio WAV File or Microphone Input with Python, Scipy, and WCKgraph

I'm stretching the limits of what these software platforms were designed to to, but I'm impressed such a hacked-together code produces fast, functional results. The code below is the simplest case code I could create which graphs the audio spectrum of the microphone input. It seems to run fine with about 30+ FPS on my modest machine. It should work on Windows and Linux. I chose not to go with matplotlib because I didn't think it was fast enough for my needs in this one case. Here's what the code below looks like running:

NOTE that this program was designed with the intent of recording the FFTs, therefore if the program "falls behind" the real time input, it will buffer the sound on its own and try to catch up (accomplished by two layers of threading). In this way, all audio gets interpreted. If you're just trying to create a spectrograph for simple purposes, have it only sample the audio when it needs to, rather than having it sample audio continuously.

import pyaudio
import scipy
import struct
import scipy.fftpack

from Tkinter import *
import threading
import time, datetime
import wckgraph
import math

#ADJUST THIS TO CHANGE SPEED/SIZE OF FFT
bufferSize=2**11
#bufferSize=2**8

# ADJUST THIS TO CHANGE SPEED/SIZE OF FFT
sampleRate=48100
#sampleRate=64000

p = pyaudio.PyAudio()
chunks=[]
ffts=[]
def stream():
        global chunks, inStream, bufferSize
        while True:
                chunks.append(inStream.read(bufferSize))

def record():
        global w, inStream, p, bufferSize
        inStream = p.open(format=pyaudio.paInt16,channels=1,
                rate=sampleRate,input=True,frames_per_buffer=bufferSize)
        threading.Thread(target=stream).start()

def downSample(fftx,ffty,degree=10):
        x,y=[],[]
        for i in range(len(ffty)/degree-1):
                x.append(fftx[i*degree+degree/2])
                y.append(sum(ffty[i*degree:(i+1)*degree])/degree)
        return [x,y]

def smoothWindow(fftx,ffty,degree=10):
        lx,ly=fftx[degree:-degree],[]
        for i in range(degree,len(ffty)-degree):
                ly.append(sum(ffty[i-degree:i+degree]))
        return [lx,ly]

def smoothMemory(ffty,degree=3):
        global ffts
        ffts = ffts+[ffty]
        if len(ffts)< =degree: return ffty
        ffts=ffts[1:]
        return scipy.average(scipy.array(ffts),0)

def detrend(fftx,ffty,degree=10):
        lx,ly=fftx[degree:-degree],[]
        for i in range(degree,len(ffty)-degree):
                ly.append(ffty[i]-sum(ffty[i-degree:i+degree])/(degree*2))
                #ly.append(fft[i]-(ffty[i-degree]+ffty[i+degree])/2)
        return [lx,ly]

def graph():
        global chunks, bufferSize, fftx,ffty, w
        if len(chunks)>0:
                data = chunks.pop(0)
                data=scipy.array(struct.unpack("%dB"%(bufferSize*2),data))
                #print "RECORDED",len(data)/float(sampleRate),"SEC"
                ffty=scipy.fftpack.fft(data)
                fftx=scipy.fftpack.rfftfreq(bufferSize*2, 1.0/sampleRate)
                fftx=fftx[0:len(fftx)/4]
                ffty=abs(ffty[0:len(ffty)/2])/1000
                ffty1=ffty[:len(ffty)/2]
                ffty2=ffty[len(ffty)/2::]+2
                ffty2=ffty2[::-1]
                ffty=ffty1+ffty2
                ffty=scipy.log(ffty)-2
                #fftx,ffty=downSample(fftx,ffty,5)
                #fftx,ffty=detrend(fftx,ffty,30)
                #fftx,ffty=smoothWindow(fftx,ffty,10)
                ffty=smoothMemory(ffty,3)
                #fftx,ffty=detrend(fftx,ffty,10)
                w.clear()
                #w.add(wckgraph.Axes(extent=(0, -1, fftx[-1], 3)))
                w.add(wckgraph.Axes(extent=(0, -1, 6000, 3)))
                w.add(wckgraph.LineGraph([fftx,ffty]))
                w.update()
        if len(chunks)>20:
                print "falling behind...",len(chunks)

def go(x=None):
        global w,fftx,ffty
        print "STARTING!"
        threading.Thread(target=record).start()
        while True:
                graph()

root = Tk()
root.title("SPECTRUM ANALYZER")
root.geometry('500x200')
w = wckgraph.GraphWidget(root)
w.pack(fill=BOTH, expand=1)
go()
mainloop()
Markdown source code last modified on January 18th, 2021
---
title: Realtime FFT Graph of Audio WAV File or Microphone Input with Python, Scipy, and WCKgraph
date: 2010-03-05 16:30:37
tags: python, old
---

# Realtime FFT Graph of Audio WAV File or Microphone Input with Python, Scipy, and WCKgraph

__I'm stretching the limits of what these software platforms were designed to to__, but I'm impressed such a hacked-together code produces fast, functional results. The code below is the simplest case code I could create which graphs the audio spectrum of the microphone input. It seems to run fine with about 30+ FPS on my modest machine. It should work on Windows and Linux. I chose not to go with matplotlib because I didn't think it was fast enough for my needs in this one case. Here's what the code below looks like running:

<div class="text-center">

![](python-real-time-tk-wav-fft.gif)

</div>

__NOTE__ that this program was designed with the intent of recording the FFTs, therefore if the program "falls behind" the real time input, it will buffer the sound on its own and try to catch up (accomplished by two layers of threading). In this way, all audio gets interpreted. If you're just trying to create a spectrograph for simple purposes, have it only sample the audio when it needs to, rather than having it sample audio continuously.

```python
import pyaudio
import scipy
import struct
import scipy.fftpack

from Tkinter import *
import threading
import time, datetime
import wckgraph
import math

#ADJUST THIS TO CHANGE SPEED/SIZE OF FFT
bufferSize=2**11
#bufferSize=2**8

# ADJUST THIS TO CHANGE SPEED/SIZE OF FFT
sampleRate=48100
#sampleRate=64000

p = pyaudio.PyAudio()
chunks=[]
ffts=[]
def stream():
        global chunks, inStream, bufferSize
        while True:
                chunks.append(inStream.read(bufferSize))

def record():
        global w, inStream, p, bufferSize
        inStream = p.open(format=pyaudio.paInt16,channels=1,
                rate=sampleRate,input=True,frames_per_buffer=bufferSize)
        threading.Thread(target=stream).start()

def downSample(fftx,ffty,degree=10):
        x,y=[],[]
        for i in range(len(ffty)/degree-1):
                x.append(fftx[i*degree+degree/2])
                y.append(sum(ffty[i*degree:(i+1)*degree])/degree)
        return [x,y]

def smoothWindow(fftx,ffty,degree=10):
        lx,ly=fftx[degree:-degree],[]
        for i in range(degree,len(ffty)-degree):
                ly.append(sum(ffty[i-degree:i+degree]))
        return [lx,ly]

def smoothMemory(ffty,degree=3):
        global ffts
        ffts = ffts+[ffty]
        if len(ffts)< =degree: return ffty
        ffts=ffts[1:]
        return scipy.average(scipy.array(ffts),0)

def detrend(fftx,ffty,degree=10):
        lx,ly=fftx[degree:-degree],[]
        for i in range(degree,len(ffty)-degree):
                ly.append(ffty[i]-sum(ffty[i-degree:i+degree])/(degree*2))
                #ly.append(fft[i]-(ffty[i-degree]+ffty[i+degree])/2)
        return [lx,ly]

def graph():
        global chunks, bufferSize, fftx,ffty, w
        if len(chunks)>0:
                data = chunks.pop(0)
                data=scipy.array(struct.unpack("%dB"%(bufferSize*2),data))
                #print "RECORDED",len(data)/float(sampleRate),"SEC"
                ffty=scipy.fftpack.fft(data)
                fftx=scipy.fftpack.rfftfreq(bufferSize*2, 1.0/sampleRate)
                fftx=fftx[0:len(fftx)/4]
                ffty=abs(ffty[0:len(ffty)/2])/1000
                ffty1=ffty[:len(ffty)/2]
                ffty2=ffty[len(ffty)/2::]+2
                ffty2=ffty2[::-1]
                ffty=ffty1+ffty2
                ffty=scipy.log(ffty)-2
                #fftx,ffty=downSample(fftx,ffty,5)
                #fftx,ffty=detrend(fftx,ffty,30)
                #fftx,ffty=smoothWindow(fftx,ffty,10)
                ffty=smoothMemory(ffty,3)
                #fftx,ffty=detrend(fftx,ffty,10)
                w.clear()
                #w.add(wckgraph.Axes(extent=(0, -1, fftx[-1], 3)))
                w.add(wckgraph.Axes(extent=(0, -1, 6000, 3)))
                w.add(wckgraph.LineGraph([fftx,ffty]))
                w.update()
        if len(chunks)>20:
                print "falling behind...",len(chunks)

def go(x=None):
        global w,fftx,ffty
        print "STARTING!"
        threading.Thread(target=record).start()
        while True:
                graph()

root = Tk()
root.title("SPECTRUM ANALYZER")
root.geometry('500x200')
w = wckgraph.GraphWidget(root)
w.pack(fill=BOTH, expand=1)
go()
mainloop()
```

Animated Realtime Spectrograph with Scrolling Waterfall Display in Python

My project is coming along nicely. This isn't an incredibly robust spectrograph program, but it sure gets the job done quickly and easily. The code below will produce a real time scrolling spectrograph entirely with Python! It polls the microphone (or default recording device), should work on any OS, and can be adjusted for vertical resolution / FFT frequency discretion resolution. It has some simple functions for filtering (check out the de-trend filter!) and might serve as a good start to a spectrograph / frequency analysis project. It took my a long time to reach this point! I've worked with Python before, and dabbled with the Python Imaging Library (PIL), but this is my first experience with real time linear data analysis and high-demand multi-threading. I hope it helps you. Below are screenshots of the program (two running at the same time) listening to the same radio signals (mostly Morse code) with standard output and with the "de-trending filter" activated.

import pyaudio
import scipy
import struct
import scipy.fftpack

from Tkinter import *
import threading
import time
import datetime
import wckgraph
import math

import Image
import ImageTk
from PIL import ImageOps
from PIL import ImageChops
import time
import random
import threading
import scipy

# ADJUST RESOLUTION OF VERTICAL FFT
bufferSize = 2**11
# bufferSize=2**8

# ADJUSTS AVERAGING SPEED NOT VERTICAL RESOLUTION
# REDUCE HERE IF YOUR PC CANT KEEP UP
sampleRate = 24000
# sampleRate=64000

p = pyaudio.PyAudio()
chunks = []
ffts = []


def stream():
    global chunks, inStream, bufferSize
    while True:
        chunks.append(inStream.read(bufferSize))


def record():
    global w, inStream, p, bufferSize
    inStream = p.open(format=pyaudio.paInt16, channels=1,
                      rate=sampleRate, input=True, frames_per_buffer=bufferSize)
    threading.Thread(target=stream).start()
    # stream()


def downSample(fftx, ffty, degree=10):
    x, y = [], []
    for i in range(len(ffty)/degree-1):
        x.append(fftx[i*degree+degree/2])
        y.append(sum(ffty[i*degree:(i+1)*degree])/degree)
    return [x, y]


def smoothWindow(fftx, ffty, degree=10):
    lx, ly = fftx[degree:-degree], []
    for i in range(degree, len(ffty)-degree):
        ly.append(sum(ffty[i-degree:i+degree]))
    return [lx, ly]


def smoothMemory(ffty, degree=3):
    global ffts
    ffts = ffts+[ffty]
    if len(ffts) < =degree:
        # ly.append(fft[i]-(ffty[i-degree]+ffty[i+degree])/2) return [lx,ly] def graph(): global chunks, bufferSize, fftx,ffty, w if len(chunks)>0:
        return ffty ffts = ffts[1:] return scipy.average(scipy.array(ffts), 0) def detrend(fftx, ffty, degree=10): lx, ly = fftx[degree:-degree], [] for i in range(degree, len(ffty)-degree): ly.append((ffty[i]-sum(ffty[i-degree:i+degree])/(degree*2)) * 2+128)
        data = chunks.pop(0)
        data = scipy.array(struct.unpack("%dB" % (bufferSize*2), data))
        ffty = scipy.fftpack.fft(data)
        fftx = scipy.fftpack.rfftfreq(bufferSize*2, 1.0/sampleRate)
        fftx = fftx[0:len(fftx)/4]
        ffty = abs(ffty[0:len(ffty)/2])/1000
        ffty1 = ffty[:len(ffty)/2]
        ffty2 = ffty[len(ffty)/2::]+2
        ffty2 = ffty2[::-1]
        ffty = ffty1+ffty2
        ffty = (scipy.log(ffty)-1)*120
        fftx, ffty = downSample(fftx, ffty, 2)
        updatePic(fftx, ffty)
        reloadPic()

    if len(chunks) > 20:
        print "falling behind...", len(chunks)


def go(x=None):
    global w, fftx, ffty
    print "STARTING!"
    threading.Thread(target=record).start()
    while True:
        # record()
        graph()


def updatePic(datax, data):
    global im, iwidth, iheight
    strip = Image.new("L", (1, iheight))
    if len(data) > iheight:
        data = data[:iheight-1]
    # print "MAX FREQ:",datax[-1]
    strip.putdata(data)
    # print "%03d, %03d" % (max(data[-100:]), min(data[-100:]))
    im.paste(strip, (iwidth-1, 0))
    im = im.offset(-1, 0)
    root.update()


def reloadPic():
    global im, lab
    lab.image = ImageTk.PhotoImage(im)
    lab.config(image=lab.image)


root = Tk()
im = Image.open('./ramp.tif')
im = im.convert("L")
iwidth, iheight = im.size
im = im.crop((0, 0, 500, 480))
# im=Image.new("L",(100,1024))
iwidth, iheight = im.size
root.geometry('%dx%d' % (iwidth, iheight))
lab = Label(root)
lab.place(x=0, y=0, width=iwidth, height=iheight)
go()

UPDATE: I'm not going to post the code for this yet (it's very messy) but I got this thing to display a spectrograph on a canvas. What's the advantage of that? Huge, massive spectrographs (thousands of pixels in all directions) can now be browsed in real time using scrollbars, and when you scroll it doesn't stop recording, and you don't lose any data! Super cool.

Markdown source code last modified on January 18th, 2021
---
title: Animated Realtime Spectrograph with Scrolling Waterfall Display in Python
date: 2010-03-05 22:51:21
tags: python, old
---

# Animated Realtime Spectrograph with Scrolling Waterfall Display in Python

__My project is coming along nicely.__ This isn't an incredibly robust spectrograph program, but it sure gets the job done quickly and easily. The code below will produce a real time scrolling spectrograph entirely with Python! It polls the microphone (or default recording device), should work on any OS, and can be adjusted for vertical resolution / FFT frequency discretion resolution. It has some simple functions for filtering (check out the de-trend filter!) and might serve as a good start to a spectrograph / frequency analysis project. It took my a long time to reach this point! I've worked with Python before, and dabbled with the Python Imaging Library (PIL), but this is my first experience with real time linear data analysis and high-demand multi-threading. I hope it helps you. Below are screenshots of the program (two running at the same time) listening to the same radio signals (mostly Morse code) with standard output and with the "de-trending filter" activated.

<div class="text-center img-border">

[![](spectrogram-scrollbars_thumb.jpg)](spectrogram-scrollbars.png)

</div>

<div class="text-center img-border img-small">

[![](nofilter_thumb.jpg)](nofilter.png)
[![](filter_thumb.jpg)](filter.png)

</div>

```python
import pyaudio
import scipy
import struct
import scipy.fftpack

from Tkinter import *
import threading
import time
import datetime
import wckgraph
import math

import Image
import ImageTk
from PIL import ImageOps
from PIL import ImageChops
import time
import random
import threading
import scipy

# ADJUST RESOLUTION OF VERTICAL FFT
bufferSize = 2**11
# bufferSize=2**8

# ADJUSTS AVERAGING SPEED NOT VERTICAL RESOLUTION
# REDUCE HERE IF YOUR PC CANT KEEP UP
sampleRate = 24000
# sampleRate=64000

p = pyaudio.PyAudio()
chunks = []
ffts = []


def stream():
    global chunks, inStream, bufferSize
    while True:
        chunks.append(inStream.read(bufferSize))


def record():
    global w, inStream, p, bufferSize
    inStream = p.open(format=pyaudio.paInt16, channels=1,
                      rate=sampleRate, input=True, frames_per_buffer=bufferSize)
    threading.Thread(target=stream).start()
    # stream()


def downSample(fftx, ffty, degree=10):
    x, y = [], []
    for i in range(len(ffty)/degree-1):
        x.append(fftx[i*degree+degree/2])
        y.append(sum(ffty[i*degree:(i+1)*degree])/degree)
    return [x, y]


def smoothWindow(fftx, ffty, degree=10):
    lx, ly = fftx[degree:-degree], []
    for i in range(degree, len(ffty)-degree):
        ly.append(sum(ffty[i-degree:i+degree]))
    return [lx, ly]


def smoothMemory(ffty, degree=3):
    global ffts
    ffts = ffts+[ffty]
    if len(ffts) < =degree:
        # ly.append(fft[i]-(ffty[i-degree]+ffty[i+degree])/2) return [lx,ly] def graph(): global chunks, bufferSize, fftx,ffty, w if len(chunks)>0:
        return ffty ffts = ffts[1:] return scipy.average(scipy.array(ffts), 0) def detrend(fftx, ffty, degree=10): lx, ly = fftx[degree:-degree], [] for i in range(degree, len(ffty)-degree): ly.append((ffty[i]-sum(ffty[i-degree:i+degree])/(degree*2)) * 2+128)
        data = chunks.pop(0)
        data = scipy.array(struct.unpack("%dB" % (bufferSize*2), data))
        ffty = scipy.fftpack.fft(data)
        fftx = scipy.fftpack.rfftfreq(bufferSize*2, 1.0/sampleRate)
        fftx = fftx[0:len(fftx)/4]
        ffty = abs(ffty[0:len(ffty)/2])/1000
        ffty1 = ffty[:len(ffty)/2]
        ffty2 = ffty[len(ffty)/2::]+2
        ffty2 = ffty2[::-1]
        ffty = ffty1+ffty2
        ffty = (scipy.log(ffty)-1)*120
        fftx, ffty = downSample(fftx, ffty, 2)
        updatePic(fftx, ffty)
        reloadPic()

    if len(chunks) > 20:
        print "falling behind...", len(chunks)


def go(x=None):
    global w, fftx, ffty
    print "STARTING!"
    threading.Thread(target=record).start()
    while True:
        # record()
        graph()


def updatePic(datax, data):
    global im, iwidth, iheight
    strip = Image.new("L", (1, iheight))
    if len(data) > iheight:
        data = data[:iheight-1]
    # print "MAX FREQ:",datax[-1]
    strip.putdata(data)
    # print "%03d, %03d" % (max(data[-100:]), min(data[-100:]))
    im.paste(strip, (iwidth-1, 0))
    im = im.offset(-1, 0)
    root.update()


def reloadPic():
    global im, lab
    lab.image = ImageTk.PhotoImage(im)
    lab.config(image=lab.image)


root = Tk()
im = Image.open('./ramp.tif')
im = im.convert("L")
iwidth, iheight = im.size
im = im.crop((0, 0, 500, 480))
# im=Image.new("L",(100,1024))
iwidth, iheight = im.size
root.geometry('%dx%d' % (iwidth, iheight))
lab = Label(root)
lab.place(x=0, y=0, width=iwidth, height=iheight)
go()
```

__UPDATE: I'm not going to post the code for this yet__ (it's very messy) but I got this thing to display a spectrograph on a canvas. What's the advantage of that? Huge, massive spectrographs (thousands of pixels in all directions) can now be browsed in real time using scrollbars, and when you scroll it doesn't stop recording, and you don't lose any data! Super cool.

Display large Images with Scrollbars with Python, Tk, and PIL

I wrote a program to display extremely large images in Python using TK. It's interesting how simple this program is, yet frustrating how long it took me to figure out.

This little Python program will load an image (pretty much any format) using the Python Imaging Library (PIL, which must be installed) and allows you to see it on a scrollable canvas (in two directions) with Tkinter and ImageTk. The above screenshot is of the program viewing the image below:

What is that image? I won't get ahead of myself, but it's about 5kHz of audio from 10.140mHz which includes a popular QRSS calling frequency. The image displays an hour of data. My ultimate goal is to have it scroll in the TK window, with slide-adjustable brightness/contrast/etc.

from Tkinter import *
import Image, ImageTk

class ScrolledCanvas(Frame):
     def __init__(self, parent=None):
          Frame.__init__(self, parent)
          self.master.title("Spectrogram Viewer")
          self.pack(expand=YES, fill=BOTH)
          canv = Canvas(self, relief=SUNKEN)
          canv.config(width=400, height=200)
          canv.config(highlightthickness=0)

          sbarV = Scrollbar(self, orient=VERTICAL)
          sbarH = Scrollbar(self, orient=HORIZONTAL)

          sbarV.config(command=canv.yview)
          sbarH.config(command=canv.xview)

          canv.config(yscrollcommand=sbarV.set)
          canv.config(xscrollcommand=sbarH.set)

          sbarV.pack(side=RIGHT, fill=Y)
          sbarH.pack(side=BOTTOM, fill=X)

          canv.pack(side=LEFT, expand=YES, fill=BOTH)
          self.im=Image.open("./1hr_original.jpg")
          width,height=self.im.size
          canv.config(scrollregion=(0,0,width,height))
          self.im2=ImageTk.PhotoImage(self.im)
          self.imgtag=canv.create_image(0,0,anchor="nw",image=self.im2)

ScrolledCanvas().mainloop()
Markdown source code last modified on January 18th, 2021
---
title: Display large Images with Scrollbars with Python, Tk, and PIL
date: 2010-03-03 19:56:55
tags: python, old
---

# Display large Images with Scrollbars with Python, Tk, and PIL

__I wrote a program to display extremely large images in Python using TK. __It's interesting how simple this program is, yet frustrating how long it took me to figure out.

<div class="text-center img-border">

[![](specview_thumb.jpg)](specview.png)

</div>

__This little Python program__ will load an image (pretty much any format) using the Python Imaging Library (PIL, which must be installed) and allows you to see it on a scrollable canvas (in two directions) with Tkinter and ImageTk. The above screenshot is of the program viewing the image below:

<div class="text-center img-border large">

[![](1hr_original_thumb.jpg)](1hr_original.jpg)

</div>

__What is that image?__ I won't get ahead of myself, but it's about 5kHz of audio from 10.140mHz which includes a popular QRSS calling frequency. The image displays an hour of data. My ultimate goal is to have it scroll in the TK window, with slide-adjustable brightness/contrast/etc.

```python
from Tkinter import *
import Image, ImageTk

class ScrolledCanvas(Frame):
     def __init__(self, parent=None):
          Frame.__init__(self, parent)
          self.master.title("Spectrogram Viewer")
          self.pack(expand=YES, fill=BOTH)
          canv = Canvas(self, relief=SUNKEN)
          canv.config(width=400, height=200)
          canv.config(highlightthickness=0)

          sbarV = Scrollbar(self, orient=VERTICAL)
          sbarH = Scrollbar(self, orient=HORIZONTAL)

          sbarV.config(command=canv.yview)
          sbarH.config(command=canv.xview)

          canv.config(yscrollcommand=sbarV.set)
          canv.config(xscrollcommand=sbarH.set)

          sbarV.pack(side=RIGHT, fill=Y)
          sbarH.pack(side=BOTTOM, fill=X)

          canv.pack(side=LEFT, expand=YES, fill=BOTH)
          self.im=Image.open("./1hr_original.jpg")
          width,height=self.im.size
          canv.config(scrollregion=(0,0,width,height))
          self.im2=ImageTk.PhotoImage(self.im)
          self.imgtag=canv.create_image(0,0,anchor="nw",image=self.im2)

ScrolledCanvas().mainloop()
```
Pages