CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Huffman Encoding Accessibility Application - Typing interface


  1. Accessibility Keyboard Demo
  2. Efficiency Analysis
  1. Accessibility Keyboard Demo

    Note: This work was inspired by a similar feature of Android's switchaccess. The original code is here. You will want to in particular look at HuffmanTreeBuilder.java and PPMTrie.java

    ###############################################################################
    # An implementation of a very basic typing interface designed for users with
    # motor imparment who are not able to use a traditional keyboard but are rather
    # rather constrained to using only one or two switches. 
    #
    # In paricular this typing interface is designed to show how Huffman encoding
    # can improve the typing efficiency of these users in particular if a good 
    # language model is utilized. Here we're using a naive language model that is 
    # based only on English letter frequencies. A better model would further 
    # illustrate how powerful Huffman encoding can be in improving the typing 
    # efficiency. 
    # 
    # Author: Rudina Morina (rmorina@andrew.cmu.edu)
    ##############################################################################
    
    from Animation import Animation
    from tkinter import *
    import string
    import heapq
    
    def rgbString(red, green, blue):
        return "#%02x%02x%02x" % (red, green, blue)
    
    # A class utilized to build the huffman and the linear tree 
    class TreeNode(object):
        def __init__(self, freq=None, char=None, left=None, right=None):
            self.char = char
            self.freq = freq
            self.left = left
            self.right = right
    
        def isLeaf(self):
            return (self.left == None and self.right == None)
    
        # needed mainly for debugging purposes 
        def __repr__(self):
            return "TreeNode(char=%s, freq=%s)" % (self.char, self.freq)
    
        def __lt__(self, other):
            return self.freq < other.freq  
    
    class TextBox(object):
        def __init__(self, font, canvas, width, height):
            self.font = font
            self.canvas = canvas
            self.width = width
            self.height = height
            self.typedSequence = ""
            self.textMargin = 10
            self.boxXMargin, self.boxYMargin = 50, 60
            self.textBoxHeight = self.height/2 - 3*self.boxYMargin
            self.textBoxWidth = self.width - 2*self.boxXMargin
    
        def textSize(self, text):
            temp = self.canvas.create_text(0, 0, text=text, anchor=NW, font=self.font)
            (x0, y0, x1, y1) = self.canvas.bbox(temp)
            self.canvas.delete(temp)
            return (x1-x0, y1-y0)
    
        def draw(self):
            # draw the text box
            textBoxLeft, textBoxTop = self.boxXMargin, self.boxYMargin
            textBoxRight = textBoxLeft + self.textBoxWidth
            textBoxBottom = textBoxTop + self.textBoxHeight
            self.canvas.create_rectangle(textBoxLeft, textBoxTop, textBoxRight, 
                textBoxBottom)
    
            # draw the text
            textXStart = self.boxXMargin + self.textMargin
            textYStart = self.boxYMargin + self.textMargin
            maxCharHeight = 0
            for char in self.typedSequence:
                charWidth, charHeight = self.textSize(char)
                maxCharHeight = max(charHeight, maxCharHeight)
                if (textXStart + charWidth > textBoxRight - self.textMargin):
                    # filled the current line, move to the next line
                    textYStart += (maxCharHeight + self.textMargin)
                    textXStart = self.boxXMargin + self.textMargin
                if (textYStart > textBoxBottom - self.textMargin):
                    print("The current page is filled")
                    return
                self.canvas.create_text(textXStart, textYStart, text=char,
                    font=self.font, anchor=NW)
                textXStart += charWidth
    
        def addChar(self, char):
            if (char == "_"):
                # add space
                self.typedSequence += " "
            else:
                self.typedSequence += char
    
    class Keyboard(object):
    
        KEYS = list(string.ascii_lowercase) + ["_"]
        # scanning methods: linear and probability-based utilizing huffman encoding
        HUFFMAN = "huffman"
        LINEAR = "linear"
        # we're utilizing binary trees to create the huffman and linear encoding so
        # the keys of a keyboard are seperated into two buckets. 
        LEFT = "left"
        RIGHT = "right"
    
        def __init__(self, canvas, width, height, languageModel=None):
            self.languageModel = languageModel
            self.keyboardContent = [
                                    Keyboard.KEYS[0 : 10], 
                                    Keyboard.KEYS[10 : 19],
                                    Keyboard.KEYS[19 : ]
                                    ]
            self.canvas = canvas
            self.width = width
            self.height = height
            self.unreachableKeys = set()
            self.rightKeys = set()
            self.leftKeys = set()
            self.scanningMethod = Keyboard.LINEAR
            self.leftKeysColor = rgbString(204, 255, 204) # light green
            self.rightKeysColor = rgbString(255, 255, 255) # white
    
        def draw(self):
            xMargin, yMargin, keyMargin = 50, 100, 10
            keyboardX, keyboardY = xMargin, self.height/2 + yMargin
            #TODO: fix the magic numbers
            keySize = (self.width - 2*xMargin - 9*keyMargin) / 10
            rowMargin = keySize / 3
            for row in range(len(self.keyboardContent)):
                keyboardX += row*rowMargin
                for col in range(len(self.keyboardContent[row])):
                    curChar = self.keyboardContent[row][col]
                    curKeyX = keyboardX + (keySize + keyMargin)*col
                    curKeyY = keyboardY + (keySize + keyMargin)*row
                    rectFill = rgbString(255, 255, 255)
                    border = rgbString(0, 0, 0)
                    if curChar in self.leftKeys:
                        rectFill = self.leftKeysColor
                    elif curChar in self.rightKeys:
                        rectFill = self.rightKeysColor
                    elif curChar in self.unreachableKeys:
                        border = rgbString(136,136,136)  # light gray
                    self.canvas.create_rectangle(curKeyX, curKeyY, curKeyX + keySize, 
                        curKeyY + keySize, fill=rectFill, outline=border)
                    self.canvas.create_text(curKeyX + keySize/2, curKeyY + keySize/2, 
                        text=curChar, fill=border)
    
        def addUnreachableKeys(self, keys):
            self.unreachableKeys = self.unreachableKeys.union(keys)
    
        def resetKeys(self):
            self.unreachableKeys = set()
            self.rightKeys = set()
            self.leftKeys = set()
    
        def setRightKeys(self, keys):
            self.rightKeys = keys
    
        def setLeftKeys(self, keys):
            self.leftKeys = keys
    
        def setScanningMethod(self, scanningMethod):
            self.resetKeys()
            self.scanningMethod = scanningMethod
            if (self.scanningMethod == Keyboard.LINEAR):
                self.leftKeysColor = rgbString(204, 255, 204) # light green
                self.rightKeysColor = rgbString(255, 255, 255) # white
            else:
                self.leftKeysColor = rgbString(204, 255, 204) # light green
                self.rightKeysColor = rgbString(255,239,213) # light orange
    
    class Editor(Animation):
    
        def __init__(self, font="Arial 30", languageModel=None):
            self.languageModel = languageModel
            self.font = font
    
        def buildLinearTree(self):
            tree = None
            for i in range(len(Keyboard.KEYS) - 1, -1, -1):
                char = Keyboard.KEYS[i]
                charNode = TreeNode(char=char)
                tree = TreeNode(left=charNode, right=tree)
            return tree
    
        def buildHuffmanTree(self):
            huffmanNodes = []
            for char in self.languageModel:
                huffmanNodes.append(TreeNode(self.languageModel[char], char))
            heapq.heapify(huffmanNodes)
            while (len(huffmanNodes) > 1):
                # obtain the two minimum-frequency Huffman nodes 
                child1 = heapq.heappop(huffmanNodes)
                child2 = heapq.heappop(huffmanNodes)
                parent = TreeNode(child1.freq + child2.freq, left=child1, right=child2)
                heapq.heappush(huffmanNodes, parent)
            return None if huffmanNodes == [] else heapq.heappop(huffmanNodes)
    
        def getNodes(self, path):
            # get all the node to the left or to the right of the current node
            nodesToHighlight = set()
            if (self.curNode == None):
                return nodesToHighlight
            def walkTree(tree):
                if (tree == None): return
                if (tree.left == None and tree.right == None):
                    nodesToHighlight.add(tree.char)
                walkTree(tree.left)
                walkTree(tree.right)
            if (path == Keyboard.LEFT):
                walkTree(self.curNode.left)
            else:
                walkTree(self.curNode.right)
            return nodesToHighlight
    
        def drawScanPrefButtons(self):
            if (self.keyboard.scanningMethod == Keyboard.HUFFMAN):
                hBoxFill = rgbString(192,192,192)
                lBoxFill = rgbString(255, 255, 255)
            else:
                hBoxFill = rgbString(255, 255, 255)
                lBoxFill = rgbString(192,192,192)
    
            hBoxLeft = self.boxLeft
            self.canvas.create_rectangle(hBoxLeft, self.boxTop, hBoxLeft + self.boxWidth,
                self.boxTop + self.boxHeight, fill=hBoxFill)
            self.canvas.create_text(hBoxLeft + self.boxWidth/2, 
                self.boxTop + self.boxHeight/2, text="Huffman Scan")
    
            lBoxLeft = hBoxLeft + self.boxWidth + self.boxMargin
            self.canvas.create_rectangle(lBoxLeft, self.boxTop, lBoxLeft + self.boxWidth,
                self.boxTop + self.boxHeight, fill=lBoxFill)
            self.canvas.create_text(lBoxLeft + self.boxWidth/2, 
                self.boxTop + self.boxHeight/2, text="Linear Scan")
    
        def redrawAll(self):
            self.keyboard.draw()
            self.textBox.draw()
            self.drawScanPrefButtons()
            if (self.keyboard.scanningMethod == Keyboard.HUFFMAN):
                text  = "Press Space to select the orange keys and Enter to select"\
                        " the green keys." 
            else:
                text  = "Press Space to move to the next key and Enter to select"\
                        " the currently highlighted key"
            self.canvas.create_text(self.instructionLeft, self.instructionTop, 
                text=text)
    
        def init(self):
            self.boxTop, self.boxLeft = self.height/2, 50
            self.boxWidth = 150
            self.boxHeight = 30
            self.boxMargin = 20
            self.instructionLeft = self.width/2
            self.instructionTop = 20
            self.keyboard = Keyboard(self.canvas, self.width, self.height, 
                self.languageModel)
            self.textBox = TextBox(self.font, self.canvas, self.width, self.height)
            self.treeRoot = self.buildLinearTree()
            self.curNode = None
    
        def mousePressed(self, event):
            # We don't have a language model so the only scanning method we can use
            # is linear
            if (self.languageModel == None): return
            hBoxLeft = self.boxLeft
            lBoxLeft = hBoxLeft + self.boxWidth + self.boxMargin
            boxBottom = self.boxTop + self.boxHeight 
            if (event.x >= hBoxLeft and event.x <= hBoxLeft + self.boxWidth and 
                event.y >= self.boxTop and event.y <= boxBottom):
                self.keyboard.setScanningMethod(Keyboard.HUFFMAN)
                self.treeRoot = self.buildHuffmanTree()
                self.curNode = None
            elif (event.x >= lBoxLeft and event.x <= lBoxLeft + self.boxWidth and 
                event.y >= self.boxTop and event.y <= boxBottom):
                self.keyboard.setScanningMethod(Keyboard.LINEAR)
                self.treeRoot = self.buildLinearTree()
    
        def keyPressed(self, event):
            # only recognize 2 keys
            if(event.keysym != "space" and event.keysym != "Return"): return
            if (event.keysym == "space"):
                if (self.curNode == None):
                    self.curNode = self.treeRoot
                else:
                    # if we move to the right of the current node, all the nodes to 
                    # the left of the current node won't be reachable anymore
                    newUnreachableKeys = self.getNodes(Keyboard.LEFT)
                    self.keyboard.addUnreachableKeys(newUnreachableKeys)
                    self.curNode = self.curNode.right
            elif (event.keysym == "Return"):
                if (self.curNode == None):
                    self.curNode = self.treeRoot
                else:
                    # if we move to the left of the current node, all the nodes to 
                    # the right of the current node won't be reachable anymore
                    newUnreachableKeys = self.getNodes(Keyboard.LEFT)
                    self.keyboard.addUnreachableKeys(newUnreachableKeys)
                    self.curNode = self.curNode.left
            if (self.curNode == None):
                self.keyboard.resetKeys()
                self.curNode = self.treeRoot
            elif (self.curNode.isLeaf()):
                self.textBox.addChar(self.curNode.char)
                self.keyboard.resetKeys()
                self.curNode = self.treeRoot
            # based on the curNode, divide the reachable keys into two buckets, the
            # one to the left of the current node and the one to the right of the 
            # current node
            self.keyboard.setLeftKeys(self.getNodes(Keyboard.LEFT))
            self.keyboard.setRightKeys(self.getNodes(Keyboard.RIGHT))
    
    # a language model based on frequency of English letters
    languageModel = {
      "e": 10.02, "t": 9.10, "a": 8.12, "o": 7.68, "i": 7.31, "n": 6.95, 
      "s": 6.28, "r": 6.02, "h": 5.92, "d": 4.32, "l": 3.98, "u": 2.88,
      "c": 2.71, "m": 2.61, "f": 2.30, "y": 2.11, "w": 2.09, "g": 2.03,
      "_": 2.0, "p": 1.82, "b": 1.49, "v": 1.11, "k": 0.69, "x": 0.17, "q": 0.11,
      "j": 0.10, "z": 0.07
      }
    
    editor = Editor(languageModel=languageModel)
    editor.run(800, 800)


  2. Efficiency Analysis

    How many key presses does it take for each character when using linear vs. huffman scanning?

    Character Linear Huffman
    'a' 1 4
    'b' 2 6
    'c' 3 5
    'd' 4 5
    'e' 5 3
    'f' 6 5
    'g' 7 6
    'h' 8 4
    'i' 9 4
    'j' 10 10
    'k' 11 7
    'l' 12 5
    'm' 13 5
    'n' 14 4
    'o' 15 4
    'p' 16 6
    'q' 17 9
    'r' 18 4
    's' 19 4
    't' 20 3
    'u' 21 5
    'v' 22 6
    'w' 23 6
    'x' 24 8
    'y' 25 6
    'z' 26 10
    'space' 27 6