Decode line codes in png graphs

Assumptions (format):

  • The clock is given and it is a red line on the top.
  • The signal line is black
  • ...

In [2]:
# Makes sure to install PyPNG image handling module
import sys
!{sys.executable} -m pip install pypng


Requirement already satisfied: pypng in /home/fedor1113/anaconda3/lib/python3.6/site-packages

In [3]:
import png

In [ ]:


In [4]:
r = png.Reader("ex.png")
t = r.asRGB()

img = list(t[2])
# print(img)

Outline

The outline of the idea is:

  1. Find the red lines that represent parallel synchronization signal above
  2. Calculate their size
  3. "Synchromize with rows below" (according to the rules of the code)
  4. ...
  5. PROFIT!

!!! Things to keep in mind:

  1. deviations of red
  2. deviations of black
  3. noise - it might just break everything!
  4. beginning and end of image...
  5. ...

A rather simple PNG we'll work with first:


In [5]:
# Let us first define colour red
# We'll work with RGB for colours
# So for accepted variants we'll make a list of 3-lists.

class colourlist(list):
    """Just lists of 3-lists with some fancy methods to work with RGB colours
    """
    
    def add_deviations(self, d=8): # Magical numbers are so magical!
        """Adds deviations for RGB colours to a given list.
           Warning! Too huge - it takes forever.
           
           Input: list of 3-lists
           Output: None (side-effects - changes the list)
        """
    
        #l = l[:] Nah, let's make it a method
        l = self
    
        v = len(l)
        max_deviation = d
    
        for i in range(v): # Iterate through the list of colours
        
            for j in range(-max_deviation, max_deviation+1): 
                # Actually it is the deviation.
                
                #for k in range(3): # RGB! (no "a"s here)
                    
                newcolour = self[i][:] # Take one of the original colours
                newcolour[0] = abs(newcolour[0]+j) # Create a deviation
                l.append(newcolour) 
                # Append new colour to the end of the list. 
                # <- Here it is changed!
                for j in range(-max_deviation, max_deviation+1): 
                    # Work with all the possibilities with this d
                    newcolour1 = newcolour[:]
                    newcolour1[1] = abs(newcolour1[1]+j)
                    l.append(newcolour1) 
                    # Append new colour to the end of the list. Yeah! 
                    # <- Here it is changed!
                
                    for j in range(-max_deviation, max_deviation+1): 
                        # Work with all the possibilities with this d
                        newcolour2 = newcolour1[:]
                        newcolour2[2] = abs(newcolour2[2]+j)
                        l.append(newcolour2) # Append new colour to the end of the list. Yeah! 
                        # <- Here it is changed!
    
        return None

def withinDeviation(colour, cl, d=20):
    """This is much more efficient!
       Input: 3-list (colour), colourlist, int
       Output: bool
    """
    for el in cl:
        if (abs(colour[0] - el[0]) <= d and 
            abs(colour[1] - el[1]) <= d and 
            abs(colour[2] - el[2]) <= d):
            return True
    return False



accepted_colours = colourlist([[118, 58, 57], [97, 71, 36], [132, 56, 46], [132, 46, 47], [141, 51, 53]]) # ...

#accepted_colours.add_deviations()

# print(accepted_colours) # -check it! - or better don't - it is a biiiig list....

# print(len(accepted_colours)) # That will take a while... Heh..

In [6]:
def find_first_pixel_of_colour(pixellist, accepted_deviations):
    """Returns the row and column of the pixel 
       in a converted to list with RGB colours PNG
       
       Input: ..., colourlist
       Output: 2-tuple of int (or None)
    """
    
    accepted_deviations = accepted_deviations[:]
    rows = len(pixellist)
    cols = len(pixellist[0])
    
    for j in range(rows):
        for i in range(0, cols, 3):
            # if [pixellist[j][i], pixellist[j][i+1], pixellist[j][i+2]] in accepted_deviations:
            if withinDeviation([pixellist[j][i], pixellist[j][i+1], pixellist[j][i+2]], accepted_deviations):
                return (j, i)
    
    return None



fr = find_first_pixel_of_colour(img, accepted_colours)

if fr is None:
    print("Warning a corrupt file or a wrong format!!!")

print(fr)
print(img[fr[0]][fr[1]], img[fr[0]][fr[1]+1], img[fr[0]][fr[1]+2])
print(img[fr[0]])


(1, 3)
118 58 57
array('B', [255, 245, 243, 118, 58, 57, 132, 46, 47, 133, 56, 46, 97, 71, 36, 141, 165, 105, 113, 186, 105, 103, 178, 96, 138, 168, 106, 95, 76, 36, 131, 55, 41, 130, 52, 40, 96, 72, 36, 140, 165, 108, 116, 184, 107, 107, 175, 98, 144, 165, 109, 98, 74, 38, 129, 55, 44, 127, 53, 40, 97, 71, 34, 141, 165, 105, 113, 186, 105, 103, 178, 96, 138, 168, 106, 95, 76, 36, 131, 55, 41, 130, 52, 40, 96, 72, 36, 144, 169, 112, 112, 180, 103, 111, 179, 102, 141, 162, 106, 98, 74, 38, 127, 53, 42, 131, 57, 44, 98, 72, 35, 140, 164, 104, 114, 187, 106, 103, 178, 96, 135, 165, 103, 94, 75, 35, 131, 55, 41, 132, 54, 42, 98, 74, 38, 134, 159, 102, 110, 178, 101, 110, 178, 101, 145, 166, 110, 98, 74, 38, 132, 58, 47, 127, 53, 40, 97, 71, 34, 145, 169, 109, 109, 182, 101, 107, 182, 100, 135, 165, 103, 95, 76, 36, 129, 53, 39, 134, 56, 44, 98, 74, 38, 134, 159, 102, 110, 178, 101, 110, 178, 101, 145, 166, 110, 98, 74, 38, 132, 58, 47, 127, 53, 40, 97, 71, 34, 141, 165, 105, 113, 186, 105, 103, 178, 96, 138, 168, 106, 95, 76, 36, 131, 55, 41, 130, 52, 40, 96, 72, 36, 140, 165, 108, 116, 184, 107, 107, 175, 98, 144, 165, 109, 98, 74, 38, 129, 55, 44, 127, 53, 40, 97, 71, 34, 145, 169, 109, 109, 182, 101, 107, 182, 100, 135, 165, 103, 95, 76, 36, 129, 53, 39, 134, 56, 44, 97, 73, 37, 139, 164, 107, 117, 185, 108, 107, 175, 98, 141, 162, 106, 97, 73, 37, 129, 55, 44, 129, 55, 42, 98, 72, 35, 140, 164, 104, 114, 187, 106, 103, 178, 96, 135, 165, 103, 94, 75, 35, 131, 55, 41, 132, 54, 42, 97, 73, 37, 139, 164, 107, 117, 185, 108, 107, 175, 98, 141, 162, 106, 97, 73, 37, 129, 55, 44, 129, 55, 42, 98, 72, 35, 140, 164, 104, 114, 187, 106, 103, 178, 96, 135, 165, 103, 94, 75, 35, 131, 55, 41, 132, 54, 42, 98, 74, 38, 134, 159, 102, 110, 178, 101, 110, 178, 101, 145, 166, 110, 98, 74, 38, 132, 58, 47, 127, 53, 40, 97, 71, 34, 141, 165, 105, 113, 186, 105, 103, 178, 96, 138, 168, 106, 95, 76, 36, 131, 55, 41, 130, 52, 40, 96, 72, 36, 140, 165, 108, 116, 184, 107, 107, 175, 98, 144, 165, 109, 98, 74, 38, 129, 55, 44, 127, 53, 40, 97, 71, 34, 145, 169, 109, 109, 182, 101, 107, 182, 100, 135, 165, 103, 95, 76, 36, 129, 53, 39, 134, 56, 44, 98, 74, 38, 134, 159, 102, 110, 178, 101, 110, 178, 101, 145, 166, 110, 98, 74, 38, 132, 58, 47, 127, 53, 40, 97, 71, 34, 141, 165, 105, 113, 186, 105, 103, 178, 96, 138, 168, 106, 95, 76, 36, 131, 55, 41, 130, 52, 40, 96, 72, 36, 144, 169, 112, 112, 180, 103, 111, 179, 102, 141, 162, 106, 98, 74, 38, 127, 53, 42, 131, 57, 44, 97, 73, 37, 140, 164, 106, 117, 186, 106, 107, 176, 96, 141, 162, 103, 98, 73, 33, 131, 55, 39, 129, 56, 39, 97, 75, 36, 133, 161, 103, 109, 181, 107, 99, 181, 107, 113, 170, 115, 234, 255, 232, 245, 255, 241])

In [7]:
# [133, 56, 46] in accepted_colours

In [8]:
# Let us now find the length of the red lines that represent the sync signal

def find_next_pixel_in_row(pixel, row, accepted_deviations):
    """Returns the column of the next pixel of a given colour
       (with deviations) in a row from a converted to list with RGB 
       colours PNG
       
       Input: 2-tuple of int, list of int with len%3==0, colourlist
       Output: int (returns -1 specifically if none are found)
    """
    
    l = len(row)
    
    if pixel[1] >= l-1:
        return -1
    
    for i in range(pixel[1]+3, l, 3):
        # if [row[i], row[i+1], row[i+2]] in accepted_deviations:
        if withinDeviation([row[i], row[i+1], row[i+2]], accepted_deviations):
            return i
    
    return -1



def colour_line_length(pixels, start, colour, deviations=20):

    line_length = 1
    pr = start[:]
    r = (pr[0], 
         find_next_pixel_in_row(pr, pixels[pr[0]], colour[:]))
    # print(pr, r)
    if not(r[1] == pr[1]+3):
        print("Ooops! Something went wrong!")
    else:
        line_length += 1
        while (r[1] == pr[1]+3):
            pr = r
            r = (pr[0], 
                 find_next_pixel_in_row(pr, 
                                        pixels[pr[0]], colour[:]))
            line_length += 1
    
    return line_length



line_length = colour_line_length(img, fr, accepted_colours, deviations=20)
            
print(line_length) # !!!


5

We found the sync (clock) line length in our graph!


In [9]:
print("It is", line_length)


It is 5

Now the information transfer signal itself is ~"black", so we need to find the black colour range as well!


In [10]:
# Let's do just that

black = colourlist([[0, 0, 0], [0, 1, 0], [7, 2, 8]])
# black.add_deviations(60) # experimentally it is somewhere around that
# experimentally the max deviation is somewhere around 60
print(black)


[[0, 0, 0], [0, 1, 0], [7, 2, 8]]

The signal we are currently interested in is Manchester code (as per G.E. Thomas).

It is a self-clocking signal, but since we do have a clock with it - we use it)

Let us find the height of the Manchester signal in our PNG - just because...


In [11]:
fb = find_first_pixel_of_colour(img, black)

In [12]:
def signal_height(pxls, fib):
    signal_height = 1
    # if ([img[fb[0]+1][fb[1]], img[fb[0]+1][fb[1]+1], img[fb[0]+1][fb[1]+2]] in black):
    if withinDeviation([pxls[fib[0]+1][fib[1]], pxls[fib[0]+1][fib[1]+1]
                        , pxls[fib[0]+1][fib[1]+2]], black, 60):
        signal_height += 1
        i = 2
        rows = len(pxls)
        # while([img[fb[0]+i][fb[1]], img[fb[0]+i][fb[1]+1], img[fb[0]+i][fb[1]+2]] in black):
        while(withinDeviation([pxls[fib[0]+i][fib[1]]
                               , pxls[fib[0]+i][fib[1]+1]
                               , pxls[fib[0]+i][fib[1]+2]], black, 60)):
            signal_height += 1
            i += 1
            if (i >= rows):
                break
    else:
        print("") # TO DO
    return signal_height

sheight = signal_height(img, fb)-1

In [13]:
print(sheight)


8

In [14]:
# Let's quickly find the last red line
...

In [15]:
def manchester(pixels, start, clock, 
               line_colour, d=60, inv=False):
    """Decodes Manchester code (as per G. E. Thomas) 
       (or with inv=True Manchester code
       (as per IEEE 802.4)).
       
       Input: array of int with len%3==0 (- PNG pixels),
             int, int, colourlist, int, bool (optional)
       Output: str (of '1' and '0') or None
    """
    
    res = ""
    
    cols = len(pixels[0])
    fb = find_first_pixel_of_colour(pixels, line_colour)
    m = 2*clock*3-2*3 # Here be dragons!
    # Hack: only check it using the upper line 
    # (or lack thereof)
    
    if not(inv):
        for i in range(start, cols-2*3, m):
            fromUP = withinDeviation([pixels[fb[0]][i-6], 
                              pixels[fb[0]][i-5], 
                              pixels[fb[0]][i-4]], 
                             line_colour, d)
            if fromUP:
                res = res + "1"
            else:
                res = res + "0"
    else:
        for i in range(start, cols-2*3, m):
            fromUP = withinDeviation([pixels[fb[0]][i-6], 
                              pixels[fb[0]][i-5], 
                              pixels[fb[0]][i-4]], 
                             line_colour, d)
            if cond:
                res = res + "0"
            else:
                res = res + "1"
    
    return res

In [16]:
def nrz(pixels, start, clock, 
               line_colour, d=60, inv=False):
    """Decodes NRZ code
       (or with inv=True its inversed version).
       It is assumed that there is indeed a valid
       NRZ code with a valid message.
       
       Input: array of int with len%3==0 (- PNG pixels),
             int, int, colourlist, int, bool (optional)
       Output: str (of '1' and '0') or (maybe?) None
    """
    
    res = ""
    
    cols = len(pixels[0])
    fb = find_first_pixel_of_colour(pixels, line_colour)
    m = 2*clock*3-2*3 # Here be dragons!
    # Hack: only check it using the upper line 
    # (or lack thereof)
    
    if not(inv):
        for i in range(start, cols, m):
            UP = withinDeviation([pixels[fb[0]][i], 
                              pixels[fb[0]][i+1], 
                              pixels[fb[0]][i+2]], 
                             line_colour, d)
            if UP:
                res = res + "1"
            else:
                res = res + "0"
    else:
        for i in range(start, cols-2*3, m):
            UP = withinDeviation([pixels[fb[0]][i], 
                              pixels[fb[0]][i+1], 
                              pixels[fb[0]][i+2]], 
                             line_colour, d)
            if cond:
                res = res + "0"
            else:
                res = res + "1"
    
    return res

In [41]:
def code2B1Q(pixels, start, clock=None, 
               line_colour=[[0, 0, 0]], d=60, inv=False):
    """Decodes 2B1Q code. The clock is not used - it
       is for compatibility only - really, so put 
       anything there. Does _NOT_ always work!
       
       WARNING! Right now does not work AT ALL 
       (apart from one specific case)
       
       Input: array of int with len%3==0 (- PNG pixels),
             int, *, colourlist, int
       Output: str (of '1' and '0') or None
    """
    
    res = ""
    
    cols = len(pixels[0])
    fb = find_first_pixel_of_colour(pixels, line_colour) # (11, 33)
    # will only work if the first or second dibit is 0b11
    ll = colour_line_length(pixels, fb, line_colour, deviations=20) # 10
    sh = signal_height(pixels, fb) - 1 # 17 -1?
    m = ll*3-2*3 # will only work if there is a transition
    # (after the first dibit)
    # We only need to check if the line is
    # on the upper, middle upper or middle lower rows...
    
    for i in range(start, cols, m):
        UP = withinDeviation([pixels[fb[0]][i], 
                              pixels[fb[0]][i+1], 
                              pixels[fb[0]][i+2]], 
                             line_colour, d)
        DOWN = withinDeviation([pixels[fb[0]+sh][i], 
                              pixels[fb[0]+sh][i+1], 
                              pixels[fb[0]+sh][i+2]], 
                             line_colour, d)
        almostUP = UP
        # if UP:
        #     res = res + "10"
        if DOWN: # elif DOWN:
            res = res + "00"
            # print("00")
        elif almostUP:
            res = res + "11"
            # print("11")
        else:
            res = res + "01"
            # print("01")
    
    return res

In [18]:
# A-a-and... here is magic!

res = manchester(img, fr[1]+5*3, line_length, black, d=60, inv=False)

In [19]:
ans = []
for i in range(0, len(res), 8):
    ans.append(int('0b'+res[i:i+8], 2))
# print(ans)

In [20]:
for i in range(0, len(ans)):
    print(ans[i])


242
224
236

Huzzah!

And that is how we decode it.

Let us now look at some specific examples.


In [21]:
# Here is a helper function to automate all that

def parse_code(path_to_file, code, inv=False):
    """Guess what... Parses a line code PNG
    
       Input: str, function 
       (~coinsides with the name of the code)
       Output: str (of '1' and '0') or (maybe?) None
    """
    
    r1 = png.Reader(path_to_file)
    t1 = r1.asRGB()
    img1 = list(t1[2])
    fr1 = find_first_pixel_of_colour(img1, accepted_colours)
    line_length1 = colour_line_length(img1, 
                                  fr1, accepted_colours, deviations=20)
    
    res1 = code(img1, fr1[1]+5*3, line_length1, black, d=60, inv=inv)
    
    return res1

In [22]:
def print_nums(bitesstr):
    """I hope you get the gist...
       
       Input: str
       Output: list (side effects - prints...)
    """
    
    ans1 = []
    for i in range(0, len(bitesstr), 8):
        ans1.append(int('0b'+bitesstr[i:i+8], 2))
    
    for i in range(0, len(ans1)):
        print(ans1[i])
    
    return ans1

In [ ]:

Manchester Code

(a rather tricky example)

Here is a tricky example of Manchester code - where we have ASCII '0's and '1's with which a 3-letter "word" is encoded.


In [37]:
ans1 = print_nums(parse_code("Line_Code_PNGs/Manchester.png", manchester))


49
49
49
48
49
49
49
49
49
49
49
48
49
49
48
49
49
49
49
48
49
48
49
49

In [24]:
res2d = ""
for i in range(0, len(ans1)):
    res2d += chr(ans1[i])

ans2d = []
for i in range(0, len(res2d), 8):
    print(int('0b'+res2d[i:i+8], 2))


239
237
235

In [ ]:

NRZ


In [25]:
ans2 = print_nums(parse_code("Line_Code_PNGs/NRZ.png", nrz))


237
243
241

In [ ]:

2B1Q

Warning! 2B1Q is currently almost completely broken. Pull requests with correct solutions are welcome :)


In [42]:
ans3 = print_nums(parse_code("Line_Code_PNGs/2B1Q.png", code2B1Q))


49
49
49
48
49
49
48
49
49
49
49
49
48
48
48
48
49
49
49
48
49
49
48
48

In [43]:
res2d3 = ""
for i in range(0, len(ans3)):
    res2d3 += chr(ans3[i])

ans2d3 = []
for i in range(0, len(res2d3), 8):
    print(int('0b'+res2d3[i:i+8], 2))


237
240
236

In [ ]: