I'm a hobbyist programmer.
What I try to do here is to read 8-bit grayscale PNG file. Then unfilter it. (That caused a headache). And after that perform Prewitt operator for "edge detection". I know that there is million and five libs for this but I want to learn why and how these things works.
I try follow OOP-principles, but it is hard.
There are still some features I intend to add, but I'd like a review of the existing code before going further.
So what I want to know:
Do I follow OOP-principles? If not, what to do better? (Link to good book would be amazing.)
Should I use bytearray instead of list? Does it affect performance?
Any comments, pointers etc are more than welcome.
And code:
PNG.py
import sys, binascii, math
from Chunk import *
from Filter import Filter
import time
class PNG:
def __init__(self, f):
self.data = []
self.f = open(f, "rb")
self.mod = sys.modules[__name__]
def readFile(self):
f = self.f
try:
self.head = f.read(8)
while True:
length = f.read(4)
if length == b'':
break
try:
c = getattr(self.mod, f.read(4).decode()) #Class
except Exception as e:
print("Chunk is not implemented yet")
raise
found = [(i, x) for (i,x) in enumerate(self.data) if isinstance(x, c)]
if len(found) > 0:
index, obj = found[0]
obj.append(f.read(int.from_bytes(length, byteorder='big')))
else:
i = c(data=f.read(int.from_bytes(length, byteorder='big'))) #Read data
self.data.append(i)
f.read(4)
finally:
f.close()
def getKernels(self, pixels, width):
kernels = []
pixels = [x for sublist in pixels for x in sublist]
for i, px in enumerate(pixels):
x = (i % width)
y = math.floor(i / width)
try:
kernel = [pixels[(x-1)+((y-1)*width)],pixels[(x)+((y-1)*width)],pixels[(x+1)+((y-1)*width)],pixels[(x-1)+(y*width)],pixels[(x)+(y*width)],pixels[(x+1)+(y*width)],pixels[(x-1)+((y+1)*width)],pixels[(x)+((y+1)*width)],pixels[(x+1)+((y+1)*width)]]
#TODO Function to generate single kernel with size variations
except Exception as e:
kernel = [0] #Bound of image
kernels.append(kernel)
return kernels
def edgeDetect(self, pixels, ihdr, idat):
kernels = self.getKernels(pixels, ihdr.width) #Generate kernel 3*3
pixels = self.prewittOperator(kernels) #Apply prewittOprator
data = b''
for i, px in enumerate(pixels):
if i % (ihdr.width) == 0: #New scanline
data += int(0).to_bytes(1, byteorder='big') #Filter method not implemented yet so "manually" set filtermethod to 0 (None)
pxbytes = bytes([px])
data += pxbytes
return data
def prewittOperator(self, kernels):
data = []
for kernel in kernels:
if len(kernel) != 9:
data.append(0)
else:
gx = sum([i*j for i, j in zip(kernel, [-1,0,1,-1,0,1,-1,0,1])])
gy = sum([i*j for i, j in zip(kernel, [-1,-1,-1,0,0,0,1,1,1])])
g = int(math.sqrt(int(math.pow(gx, 2))+int(math.pow(gy, 2))))%256
data.append(g)
return data
def editImage(self):
ihdr=self.getChunk(IHDR) #Get Image header instance
idat=self.getChunk(IDAT) #Get Image data instance
data = idat.getImage(ihdr) #Get filtered image data
pixels = Filter().unfilter(data, ihdr.width) #Unfilter data
newImage = self.edgeDetect(pixels, ihdr, idat);
idat.setImage(newImage) #Set new image data
def getChunk(self,classname):
found = [(i, x) for (i, x) in enumerate(self.data) if isinstance(x, classname)]
index, chunk = found[0]
return chunk;
def writeFile(self, filename):
f = open(filename, "wb")
f.write(self.head)
for c in self.data:
f.write(c.getLength())
f.write(bytes(c.__class__.__name__, 'ASCII'))
f.write(c.getData())
f.write(c.getCRC(bytes(c.__class__.__name__, 'ASCII')))
png = PNG("input.png")
png.readFile()
png.editImage()
png.writeFile("output.png")
Chunk.py
import zlib, binascii
class Chunk:
def __init__(self, data = b''):
self.data = data
def getLength(self):
return len(self.data).to_bytes(4, byteorder='big')
def getData(self):
return self.data
def setData(self, data):
self.data = data
def getCRC(self, name):
return (binascii.crc32(name+self.data)).to_bytes(4, byteorder='big') #TODO Own CRC calculation
class IHDR(Chunk):
def __init__(self, **kwds):
super().__init__(**kwds)
self.parse()
def parse(self):
data = super().getData()
self.width = int(data[:4].hex(), 16)
self.height = int(data[4:8].hex(), 16)
self.bit_depth = int(data[8:9].hex(), 16)
self.color_type = int(data[9:10].hex(), 16)
if(self.color_type != 0):
print("Color type not implemented yet")
raise
self.compression_method = int(data[10:11].hex(), 16)
if(self.compression_method != 0):
print("Compression method not implemented yet")
raise
self.filter_method = int(data[11:12].hex(), 16)
self.interlace_method = int(data[12:13].hex(), 16)
class IDAT(Chunk):
def __init__(self, **kwds):
super().__init__(**kwds)
def append(self, data):
super().setData(super().getData()+data)
def parseBytes(self, data, width, length, lines):
if len(data) > 0:
lines.append(data[:(1+width*length)])
return self.parseBytes(data[(1+width*length):], width, length, lines)
else:
return lines
def getImage(self, ihdr):
return self.parseBytes(zlib.decompress(super().getData()), ihdr.width, int(ihdr.bit_depth/8), [])
def setImage(self, data):
super().setData(zlib.compress(data))
class IEND(Chunk):
def __init__(self, **kwds):
super().__init__(**kwds)
Filter.py
import math
class Filter:
def filter(self):
print("Filter function not implemented yet")
raise
def unfilter(self, data, width):
unfiltered = []
for y, row in enumerate(data):
ft = row[0] #Filter method
nr = [] #new row
unfiltered.append(nr)
for x, px in enumerate(row[1:]):
try:
unfiltered[y].append(int(self.method(ft)(px, x, y, unfiltered)))
except Exception as e:
print(e)
print("ERROR Filter: "+str(ft)+" Cordinates: "+str(x)+":"+str(y))
raise
return unfiltered
def none(self,px,x,y,data):
return px
def sub(self,px,x,y,data):
return (px + self.reconA(x,y,data))%256
def up(self,px,x,y,data):
return (px + self.reconB(x,y,data))%256
def average(self,px,x,y,data):
return (px + math.floor((self.reconA(x,y,data)+self.reconB(x,y,data))/2))%256
def paeth(self,px,x,y,data):
a = self.reconA(x,y,data)
b = self.reconB(x,y,data)
c = self.reconC(x,y,data)
p = a + b - c
pa = math.fabs(p-a)
pb = math.fabs(p-b)
pc = math.fabs(p-c)
if pa <= pb and pa <= pc:
return (px + a)%256
if pb <= pc:
return (px + b)%256
return (px + c)%256
def reconA(self,x,y,data):
if x == 0:
return 0
return data[y][x-1]
def reconB(self,x,y,data):
if y == 0:
return 0
return data[y-1][x]
def reconC(self,x,y,data):
if y == 0 or x == 0:
return 0
return data[y-1][x-1]
def method(self, t):
return {
0: self.none,
1: self.sub,
2: self.up,
3: self.average,
4: self.paeth,
}[t]