"""
MOSAICS WITH A GENETIC ALGORITHM IN PYTHON+TKINTER
By: Aldo Gonzalez
Saint Norbert College Senior Capstone Project - Spring 2021
Sources:
-Pillow
-Slicer
-L subclass of list https://code.activestate.com/recipes/579103-python-addset-attributes-to-list/
-W3schools
-tutorialspoint
-Codemy (Tkinter tutorial videos)
-htmlview https://pypi.org/project/tkhtmlview/
-Dr. McVey
"""
import copy
import random
import time
import os
from tkinter import *
import PIL
from PIL import ImageTk,Image,ImageOps #need to include all modules
import image_slicer
#import io
#import zipfile
from tkhtmlview import HTMLLabel
root = Tk() #window
root.title('Mosaics')
#Frame.geometry("1000x850") #window size?
Frame=LabelFrame(root)
Frame.pack()
global ps1, ps2 #packetsize vars in fitness -> determine #s that go into calculating packetsize
ps1=1 #if 5 (this is kinda the half pt, vs ps2 is the full length -> assuming same w+l, so can only do 4x4,10x10,etc.)
ps2=2 #+10 -> 10x10 packets=100=500. good for basics w/ 25. probs need less w/ 49t (1500px)
#back w/ 500/3750=13%. could stick around that
#400t=300px= 4x4=26%, or 2x2=.7%
#note: keep even bc =/= half pixels
class L(list):
"""
A subclass of list that can accept additional attributes.
Should be able to be used just like a regular list.
The problem:
a = [1, 2, 4, 8]
a.x = "Hey!" # AttributeError: 'list' object has no attribute 'x'
The solution:
a = L(1, 2, 4, 8)
a.x = "Hey!"
print a # [1, 2, 4, 8]
print a.x # "Hey!"
print len(a) # 4
You can also do these:
a = L( 1, 2, 4, 8 , x="Hey!" ) # [1, 2, 4, 8]
a = L( 1, 2, 4, 8 )( x="Hey!" ) # [1, 2, 4, 8]
a = L( [1, 2, 4, 8] , x="Hey!" ) # [1, 2, 4, 8]
a = L( {1, 2, 4, 8} , x="Hey!" ) # [1, 2, 4, 8]
a = L( [2 ** b for b in range(4)] , x="Hey!" ) # [1, 2, 4, 8]
a = L( (2 ** b for b in range(4)) , x="Hey!" ) # [1, 2, 4, 8]
a = L( 2 ** b for b in range(4) )( x="Hey!" ) # [1, 2, 4, 8]
a = L( 2 ) # [2]
"""
def __new__(self, *args, **kwargs):
return super(L, self).__new__(self, args, kwargs)
def __init__(self, *args, **kwargs):
if len(args) == 1 and hasattr(args[0], '__iter__'):
list.__init__(self, args[0])
else:
list.__init__(self, args)
self.__dict__.update(kwargs)
def __call__(self, **kwargs):
self.__dict__.update(kwargs)
return self
"""
IMGINIT
-input parameters -> globals
-multi or single mode set (via a and adv vars)
-slicer slices into tiles to make final image tile list
-other pixel/sizing/related vars set here
-frame destroyed and replaced
"""
def imgInit(fN, a):
#INPUT GLOBALS
global tileNum #used for many funct loops
global mainCy #frequency
global eP
global mPopP, mTileP
global GenSize, ConstGenSize
global curr
curr=1
tileNum=int(tileNumE.get()) #rows, cols set pretty proportionally. 20,25,32,49. 100, 156, 210, 400
#cycles=1000 #T.TD: only for now. later will end when user wants, when not getting much better, etc.
GenSize=int(GenSizeE.get()) #Pops
eP=float(ePE.get()) #elites: 20% of top move on
mPopP=float(mPopPE.get()) #.5=50% of pops affected (out of the ones that make it past selection, bc this happens before crossover) (can include repeated ones due to randomness)
mTileP=float(mTilePE.get()) #.1=10% of tiles affected (can include repeated ones due to randomness)
mainCy=int(mainCyE.get())
ConstGenSize=copy.deepcopy(GenSize) #avoid data share -> deep copy from GenSize
global adv
if a: #if the option was for single (basic) or multi (adv)
adv=True
else:
adv=False
global tiles
#fN=takes filename, so, will take disk version (not the resized version from main)
#can always use paint and resize it in disk, or at least a copy -> now, when "join," won't display massive one
#or, since join returns an img, can just resize that one
tiles=image_slicer.slice(fN, tileNum, save=False)
#tiles=image_slicer.slice(fN, 20, 5, 4, False) #see documentation
tiles=image_slicer.save_tiles(tiles, prefix="tile", directory=os.getcwd(),format="png") #no prefix for now=imgs get replaced. + current dir. not sure how to do new folder.
#Note: column=row, row=column. careful w/ using those properties
tileNum=len(tiles) #the real # used is rounded up to be proportionate, so good to change now
global tilePixelX, tilePixelY #used in fitness funct
global tilePixelTotal #used in fitness funct
global popRows, popColumns #used in crossover bc going row by row, changing all columns in certain rows
tilePixelX=(tiles[1].coords)[0] #gives x distance from 0 to 1 (or, that's at least how I've used it)
popRows=(tiles[tileNum-1].position)[1]
"""
goal: determine # of y pixels in a tile (bc that varies when # of tiles change)
for x=super easy, bc tile[1] is always the next one x-wise
for y=not as simple. as I'm going thru tiles, need to detect when y coords change the first time -> that's the answer
"""
x=0
while True:
if (tiles[x].coords)[1] != 0: #once it changes
tilePixelY=(tiles[x].coords)[1]
popColumns=(tiles[x-1].position)[0] #guy before=in last column
break
x+=1
tilePixelTotal=tilePixelX*tilePixelY
print(tilePixelTotal*tileNum)
global Frame
Frame.destroy() #=/= make var disappear
Frame=LabelFrame(root) #comparable to initial one that was in main scope
Frame.pack()
randomT() #set up first gen and s2 displays
""" INITIAL GUI/TK SET-UP """
ParOptionsL = HTMLLabel(Frame, html="
Please Enter Desired Parameters
")
ParOptionsL.grid(row = 0, column = 0, columnspan=6)
ParOptionsL.fit_height()
#num of tiles
tileNumL = Label(Frame, text="Number of Tiles") #per Population")
tileNumL.grid(row = 1, column = 0)
tileNumE = Entry(Frame) #width=8)
tileNumE.grid(row=2, column=0)
tileNumE.insert(0, "25")
#update frequency
mainCyL = Label(Frame, text="Update Frequency") # (in Generations)")
mainCyL.grid(row = 1, column = 1)
mainCyE = Entry(Frame) #width=8)
mainCyE.grid(row=2, column=1)
mainCyE.insert(0, "150")
#num of pops per gen
GenSizeL = Label(Frame, text="Size of Generation") # (in Populations)")
GenSizeL.grid(row = 1, column = 2)
GenSizeE = Entry(Frame) #width=8)
GenSizeE.grid(row=2, column=2)
GenSizeE.insert(0, "4")
#elite percentage (pops per gen)
ePL = Label(Frame, text="Elite Decimal")
ePL.grid(row = 1, column = 3)
ePE = Entry(Frame) #width=8)
ePE.grid(row=2, column=3)
ePE.insert(0, ".3")
#mutation: pops affected
mPopPL = Label(Frame, text="Mutation: Populations Affected")
mPopPL.grid(row = 1, column = 4)
mPopPE = Entry(Frame) #width=8)
mPopPE.grid(row=2, column=4)
mPopPE.insert(0, ".3")
#mutation: tiles affected per pop
mTilePL = Label(Frame, text="Mutation: Tiles Affected")
mTilePL.grid(row = 1, column = 5)
mTilePE = Entry(Frame) #width=8)
mTilePE.grid(row=2, column=5)
mTilePE.insert(0, ".05")
OptionRunL=HTMLLabel(Frame, html="Please Select an Option to Run
")
OptionRunL.grid(row=3, column=0, columnspan=6)
OptionRunL.fit_height()
MonaLisaFN="MonaLisa.png" #fileName
MonaLisa=Image.open(MonaLisaFN) #img object
#MonaLisa=ImageOps.fit(MonaLisa,(300,200)) #600,400 #150,100
#MonaLisa=ImageOps.scale(MonaLisa, 0.5)
MonaLisaTK=ImageTk.PhotoImage(MonaLisa) #TK img object
MonaLisaLabel=Label(Frame, image=MonaLisaTK)
MonaLisaLabel.grid(row=4,column=0, columnspan=2) #.pack()
MonaLisaButton=Button(Frame, text="Single Image", command=lambda: imgInit(MonaLisaFN, False), bg="black", fg="white")
MonaLisaButton.grid(row=5,column=0, columnspan=2)
#FinalDog
FinalDogFN="adv/final.png" #"Garfield1.png"
FinalDog=Image.open(FinalDogFN)
#print(FinalDog)
#FinalDog=ImageOps.fit(FinalDog,(250,300))
#FinalDog=ImageOps.scale(FinalDog, 0.5)
FinalDogTK=ImageTk.PhotoImage(FinalDog) #for TK
FinalDogLabel=Label(Frame, image=FinalDogTK)
FinalDogLabel.grid(row=4, column=2, columnspan=2) #.pack() #pack what's in label now #Q: if reusing vars doesn't hurt, how to remove items?
FinalDogSingleButton=Button(Frame, text="Single Image", command=lambda: imgInit(FinalDogFN, False), bg="black", fg="white")
FinalDogSingleButton.grid(row=5, column=2)
FinalDogMultiButton=Button(Frame, text="Multi-Image", command=lambda: imgInit(FinalDogFN, True), bg="black", fg="white")
FinalDogMultiButton.grid(row=5, column=3)
#Sandler
SandlerFN="sandler.png"
Sandler=Image.open(SandlerFN)
#Sandler=ImageOps.fit(Sandler,(300,200))
#Sandler=ImageOps.scale(Sandler, 0.5)
SandlerTK=ImageTk.PhotoImage(Sandler) #for TK
SandlerLabel=Label(Frame, image=SandlerTK)
SandlerLabel.grid(row=4, column=4, columnspan=2) #.pack() #pack what's in label now #Q: if reusing vars doesn't hurt, how to remove items?
SandlerButton=Button(Frame, text="Single Image", command=lambda: imgInit(SandlerFN, False), bg="black", fg="white")
SandlerButton.grid(row=5, column=4, columnspan=2)
"""
-stores all disk tiles into imgList (for easily picking random ones to put in first gen)
ADV- =/= use imgs from disk from slicer (those are from the final), so the file names are different here
then builds first generation
-fills list of random imgs
-replaces tile imgs w/ those
-joins into one img
"""
def randomT():
""" only need to run once """
#1. list w/ all tile names + images from disk
ImgFNList=[]
global imgList #will use in mutation. basic=has "final" img tiles (bc same as initial), adv=has initial pics (diff from final)
imgList=[] #! image objects of all tiles -> for basic, will be final img tiles -> will compare random tiles to these
if(adv):
for x in range(40,51): #60,66 #40,51
curr="adv/"+str(x)+".png"
currImg=Image.open(curr) #less costly than replacing in list
#imgList.append(currImg)
imgList.append(ImageOps.fit(currImg,(tilePixelX, tilePixelY))) #now =/= need to update in paint every time diff tile # or diff pics
ImgFNList.append(curr) #no use for now. only care for img objects for pixels
else: #need rows and cols bc of slicer disk naming process
for x in range(1,popRows+1): #1,5=01-04 here for ROW
if x<10:
currX="tile_0"+str(x)+"_"
else:
currX="tile_"+str(x)+"_"
for y in range(1,popColumns+1): #1,6=01-05 on inner loop for COL
if y<10:
curr=currX+"0"+str(y)+".png"
else:
curr=currX+str(y)+".png"
currImg=Image.open(curr) #less costly than replacing in list
imgList.append(currImg)
#imgList.append(ImageOps.fit(currImg,(55,50)))
ImgFNList.append(curr) #no use for now. only care for img objects for pixels
global tilesList #might be using in diff functs
tilesList=copy.deepcopy(tiles) #not shallow
tilesList=list(tilesList) #tuple->list of tuples.data type can change, sure?
tilesList=L(tilesList) #now can store fitness
global Gen
Gen=[] #current generation. possibly =/= need diff final list
"""
SETS UP INITIAL GENERATION!
run multiple times
will run through all pops
append each pop (w/ fitness) to Gen
"""
for pop in range(0,ConstGenSize):
#2. loop to generate random # + make a list of random images (used to also display tiles)
#col=0
#cnt=0
RimgList=[] #! random img objects -> 1 population img. later: list w/in list for allowing multiple population imgs? or dict? or?
RFNList=[]
#TKList=[] #random TK img objects
#TKlabelList=[] #random TK img labels
#txtlabelList=[]
size=len(imgList)
for x in range(0,tileNum): #if 20 -> 0-19 iterations
num=random.randint(0, size-1) #now good for both basic+adv bc allows for a shorter set of imgs than tiles (you just run the random on those more times than the #of imgs)
#tileNum-1) #random # between 0-19 -> yes bc list
RimgList.append(imgList[num])
RFNList.append(ImgFNList[num])
#3. replace all the tile imgs w/ random ones
for x in range(0,tileNum):
tilesList[x].image = RimgList[x]
tilesList[x].filename = RFNList[x]
Gen.append(copy.deepcopy(tilesList)) #brings fitness along
#tilesList just has current pop -> temp var, bc L
screen2()
"""
SCREEN2
sets up second screen
all the labels, buttons, and initial process images
"""
def screen2():
global joinTKList, joinTKlabelList #bc will be calling main() from a button after this
joinTKList=[] #population TK img objects=generation
joinTKlabelList=[] #population TK img labels=generation
startL = Label(Frame, text = 'Start', font=10)
startL.grid(row = 0, column = 0)
currentL = Label(Frame, text = 'Current', font=10)
currentL.grid(row = 0, column = 1, columnspan=2)
endL = Label(Frame, text = 'End', font=10)
endL.grid(row = 0, column = 3)
#INITIAL
joinImg=image_slicer.join(Gen[0])
joinTKList.append(ImageTk.PhotoImage(joinImg))
joinTKlabelList.append(Label(Frame, image=joinTKList[0]))
joinTKlabelList[0].grid(row=1, column=0)
#CURRENT
joinImg=image_slicer.join(Gen[0])
joinTKList.append(ImageTk.PhotoImage(joinImg))
joinTKlabelList.append(Label(Frame, image=joinTKList[1]))
joinTKlabelList[1].grid(row=1, column=1, columnspan=2)
#FINAL
joinImg=image_slicer.join(tiles)
#print(joinImg.size)
joinTKList.append(ImageTk.PhotoImage(joinImg))
joinTKlabelList.append(Label(Frame, image=joinTKList[2]))
joinTKlabelList[2].grid(row=1, column=3)
global GoButton
GoButton = Button(Frame, text="GO. Keep going!", command=main, bg="black", fg="white") #KEEP ER MOVIN
GoButton.grid(row = 2, column = 1)
DoneButton = Button(Frame, text="DONE. Looks Good!", command=done, bg="black", fg="white")
DoneButton.grid(row = 2, column = 2)
""" DoneButton = Button(Frame, text="click", command=click)
DoneButton.grid(row = 2, column = 1) """
def done():
exit()
""" def click():
for x in range(0,cycles):
GoButton.invoke()
time.sleep(1) """
"""
MAIN
#Cycle
Each funct will run # of pop times
each have their own loops for this
here, only concerned w/ how many generations/cycles we want to run this process
then updates display! -> MAIN runs as many times as you click the button "GO"
"""
def main():
global Gen #bc of shuffle, sort
global joinTKList, joinTKlabelList #for new label space
global curr
for cycle in range(0,mainCy):
#print(Gen) #should have 4 Ls (lists w/ fitnesses)
fitness2()
print("GENERATION #",curr,": ",sep="")
for x in range(0,GenSize):
print(Gen[x].fitness)
selection()
mutation()
crossover()
random.shuffle(Gen)
curr+=1
Gen.sort(key=sortFitness, reverse=True)
#UPDATE DISPLAY (w/ best one as current)
joinImg=image_slicer.join(Gen[GenSize-1]) #best
joinTKList.append(ImageTk.PhotoImage(joinImg))
temp=len(joinTKList)-1 #new size of list. eff bc using twice
joinTKlabelList.append(Label(Frame, image=joinTKList[temp])) #all to keep clean vars
joinTKlabelList[temp].grid(row=1, column=1, columnspan=2) #replace. may destroy before, too.
random.shuffle(Gen)
"""
CROSSOVER
-pick random pop in Gen
-deep copy append into Gen (new spot)
-pick another random pop in Gen
-every other row, switch each tile w/ 2nd pop
=now a mix of both parents, each getting every other row
repeat till amt recovered
update GenSize (so, set as global... again)
"""
def crossover():
global GenSize
#stop if reached original amt? (need original var)
#could be good bc what if elitism is set low? need more -> keeps going
while GenSize != ConstGenSize:
p1=random.randint(0,GenSize-1)
p2=random.randint(0,GenSize-1)
tileCnt=0;
Gen.append(copy.deepcopy(Gen[p1]))
change=False #first row can stay. next row, I want it to change, grabbing tiles from other parent
GenSize=len(Gen) #=/= sure how many by end. but if stop pt works, can just set back to original var (TBD)
for r in range(0,popRows): #rows
if(change): #changing. pulling from other parent
#$: store Gen[p2] in a var to not continue to access list
for c in range(0,popColumns): #cols
Gen[GenSize-1][tileCnt].image=Gen[p2][tileCnt].image #swapping row, tile by tile
tileCnt+=1
else: #no tile swapping here
#do need current tileCnt for when I change. this=more efficient than looping
tileCnt+=popColumns
change= not change #toggle
"""
MUTATION
need:
-mPopP (currently half)
-Gen -Gen length -random loop
-pick a random pop
-change some random tiles to some random tiles (from original, global tiles?)
"""
def mutation():
size=len(imgList) #efficiency
mPopAmt=round(GenSize*mPopP)
mTileAmt=round(mTileP*tileNum)
for x in range(0, mPopAmt):
#$: can return right in [] w/o variables. currently like this for readability
pop=random.randint(0, GenSize-1) #0-3. which pop?
for y in range(0, mTileAmt):
tile=random.randint(0, tileNum-1)
tileO=random.randint(0, size-1)
Gen[pop][tile].image = imgList[tileO] #tiles[tileO].image
#basic/adv=imgList has all initial imgs. diff for each, but can use same var!
#print("GENERATION #1 AFTER MUTATION: ")
""" print(GenSize)
for x in range(0,3):
print(Gen[x].fitness) """
#used to sort messy Ls (that includes fitness property) by fitness!
def sortFitness(e):
return e.fitness #yes!!!
"""
ELITES: sort, get top %, throw into generation
BRACKET SELECTION: go 2x2, picking best, throwing into generation
slight variance if even or odd size of gen
Note: bracket selects from ALL. thus, possible it picks out some elites again, which is fine.
GenSize: Updated here!
"""
def selection():
E=copy.deepcopy(Gen)
S=copy.deepcopy(Gen)
Gen.clear() #now =/= need Gen, bc all items in copy lists
E.sort(key=sortFitness, reverse=True)
size=len(E)
Esize=round(size*eP)
#ELITES
for x in range(size-Esize,size):
Gen.append(copy.deepcopy(E[x]))
#BRACKET SELECTION
if(size % 2 == 0): #even. can do 2x2 whole way.
for x in range(0,size,2): #size depends on # of pops
if S[x].fitness < S[x+1].fitness: #throw best in there
Gen.append(copy.deepcopy(S[x]))
else:
Gen.append(copy.deepcopy(S[x+1]))
else: #odd. 2x2 until last guy, which is just thrown in, probs
for x in range(0,size-1,2): #size depends on # of pops
if S[x].fitness < S[x+1].fitness: #throw best in there
Gen.append(copy.deepcopy(S[x]))
else:
Gen.append(copy.deepcopy(S[x+1]))
Gen.append(copy.deepcopy(S[size-1])) #last guy
global GenSize #for this nested funct to affect the var for main... maybe if it wasn't nested, it'd work
GenSize=len(Gen) #changed. now diff from size, Esize
"""
yxy PACKET PIXEL BY PIXEL
5 yxy packets. top, left, mid, right, bottom
much faster than all pixels, but still catching much more detail difference than avg color of tile
the smaller the # -> the smaller the difference -> the closest in match
=/=pre-compute
so, need tiles of initial/original (global tiles) + tiles of manipulated/current (tilesList)
+ pixel arrays for all tiles of each (each loop pass=1 tile of each)
"""
def fitness2():
for pop in range(0,GenSize):
#cnt=0
#sum=0
imgSum=0
for t in range(0,tileNum):
#CURRENT TILE
pixO=tiles[t].image.load() #original
pixC=Gen[pop][t].image.load() #current
""" #each tile=5 packets of 10x10 -> 500. each 1 a nested loop after finding start pt """
for packet in range(0,5):
if packet==0: #TOP
startX=round((tilePixelX/2)-ps1)
endX=startX+ps2
startY=0
endY=startY+ps2
elif packet==1: #MID
startX=round((tilePixelX/2)-ps1)
endX=startX+ps2
startY=round((tilePixelY/2)-ps1)
endY=startY+ps2
elif packet==2: #BOTTOM
startX=round((tilePixelX/2)-ps1)
endX=startX+ps2
startY=tilePixelY-ps2
endY=startY+ps2
elif packet==3: #LEFT
startX=0
endX=ps2
startY=round((tilePixelY/2)-ps1)
endY=startY+ps2
else: #RIGHT
startX=tilePixelX-ps2
endX=startX+ps2
startY=round((tilePixelY/2)-ps1)
endY=startY+ps2
for x in range(startX, endX):
for y in range(startY, endY):
colorO=pixO[x,y] #current RGB tuple from original
colorC=pixC[x,y] #current RGB tuple from changed/current
#cnt+=1
pixDiff=0
for color in range(3): #color=R, G, B. goes to each to compare separately
pixDiff+=abs(colorO[color]-colorC[color]) #add differences up
imgSum+=pixDiff
avg=imgSum/tileNum #sum can work, too, but avg will be a more manageable number (smaller)
Gen[pop].fitness=avg
""" AVERAGE TILE COLOR
*not currently usable. assumes certain small additions*
requires pre-comp (could be a funct, but called before cycle)
+ need global tiles to be L as well? bc each tile needs an tileAvgColor
assume that for now
add up all Rs, Gs, and Bs? compare to all Rs, Gs, and Bs from final?
or wait, how to do avg tile color??? is that it?
can also divide each by # of pix
will probs be very diff if I get the precomp to help w/ current ones too
"""
def fitnessAvg():
for pop in range(0,GenSize):
for x in range(0,tileNum):
#CURRENT TILE
AvgO=tiles[x].tileAvgColor #original #TD: assuming this exists
pixC=Gen[pop][x].image.load() #current
sum=[0,0,0] #RGB sum for each tile. will compare w/ AvgO at end of this loop
#each tile=50x75, bc 250x300 -> 3750. TD: can put var later
for r in range(0,tilePixelX):
for c in range(0,tilePixelY):
#colorO=pixO[r,c] #current RGB tuple from original
colorC=pixC[r,c] #current RGB tuple from changed/current
for color in range(3): #color=R, G, B. goes to each to compare separately
sum[color]+=colorC[color] #
sum[0]/=tilePixelTotal #avg=sum/# of pixels
sum[1]/=tilePixelTotal
sum[2]/=tilePixelTotal
#now compare, do smth (wait, do I compare? or just send #?), and reset
#print(equalCnt) #how many pixels correct in this tile? should be 60x50=3,000
#prints 20x4=80 times, so not awful
#but can also have an if asking if it's the right # -> same -> many less prints
avg=sum/tileNum #sum can work, too, but avg will be a more manageable number (smaller)
Gen[pop].fitness=avg
#print("fitness score of population img #",pop+1,sep="") #sep by , . sep= indicates diff par
#print(Gen[pop].fitness) #T
#return avg
""" PIXEL BY PIXEL
=/=pre-compute
so, need tiles of initial/original (global tiles) + tiles of manipulated/current (tilesList)
+ pixel arrays for all tiles of each (each loop pass=1 tile of each)
calcs sum via all correct pixels of all tiles -> avg for whole img per tile -> returned
"""
def fitness():
for pop in range(0,GenSize):
sum=0
for x in range(0,tileNum):
#CURRENT TILE
pixO=tiles[x].image.load() #original
pixC=Gen[pop][x].image.load() #current
equalCnt=0
#each tile=50x75, bc 250x300 -> 3750.
for r in range(0,tilePixelX):
for c in range(0,tilePixelY):
colorO=pixO[r,c] #current RGB tuple from original
colorC=pixC[r,c] #current RGB tuple from changed/current
equal=True #assume RGBs are equal
for color in range(3): #color=R, G, B. goes to each to compare separately
if colorO[color]!=colorC[color]: #e- are the R's equal?
equal=False
break #if R differs=no match=why bother to keep going
if equal:
equalCnt+=1
sum+=equalCnt #add correct pixels
#print(equalCnt) #how many pixels correct in this tile? should be 60x50=3,000
#prints 20x4=80 times, so not awful
#but can also have an if asking if it's the right # -> same -> many less prints
avg=sum/tileNum #sum can work, too, but avg will be a more manageable number (smaller)
Gen[pop].fitness=avg
#print("fitness score of population img #",pop+1,sep="") #sep by ,. sep= indicates diff par
#print(Gen[pop].fitness) #T
#return avg
root.mainloop() #root fine? bc window. or does this only run @start to run window?