Project: Using Genetic Algorithms to Create DNA Art
I decided to do a follow-up on one of my previous projects . This time, I planned to apply genetic algorithms onto DNA to create art. It just seemed somehow fitting to me.
Here is the reference image:
My first task was to adapt my previous code to this new image. I also changed the resolution of it so that the algorithms could progress faster without comprimising the final quality.
I changed the base shape from a pentagon to a circle, so that rather than having the colours being filled by the shape, it would instead be circles connected by lines (much like a connect the dots game.)
I also restored the genetic algorithm to its full form so that it properly mutates. I had previously modified it because it didn’t suit the problem then.
Here is the bulk of the code:
from PIL import Image
import sys , pygame
from pygame.locals import *
import random
import math
import pickle
target = Image . open ( "dna.jpg" )
totalx = target . size [ 0 ]
totaly = target . size [ 1 ]
pop_no = 5
circles_no = 1000
mutation_rate = 0.01
pygame . init ()
DISPLAYSURF = pygame . display . set_mode (( totalx , totaly ))
pygame . display . set_caption ( 'DNA' )
WHITE = ( 255 , 255 , 255 )
BLACK = ( 0 , 0 , 0 )
def maxfit ( thing ):
return thing . fitness
def diff ( first , second ):
a = abs ( first [ 0 ] - second [ 0 ])
b = abs ( first [ 1 ] - second [ 1 ])
c = abs ( first [ 2 ] - second [ 2 ])
return ( a + b + c )
class Circle :
def __init__ ( self ):
self . color = random . choice ([ BLACK , WHITE ])
self . pos = ( random . randint ( 0 , totalx ),
random . randint ( 0 , totaly ))
self . radius = 2
class Drawing :
def __init__ ( self ):
self . circles = []
for _ in xrange ( circles_no ):
t = Circle ()
self . circles . append ( t )
def calcfitness ( self ):
totaldiff = 0
for x in xrange ( target . size [ 0 ]):
for y in xrange ( target . size [ 1 ]):
totaldiff += diff ( target . getpixel (( x , y )),
DISPLAYSURF . get_at (( x , y ))[: 3 ])
self . fitness = ( 20173671.0 / totaldiff ) ** 3
def crossover ( self , partner ):
child = Drawing ()
for i in xrange ( circles_no ):
child . circles [ i ] . color = random . choice ([ BLACK , WHITE ])
child . circles [ i ] . pos = random . choice ([ self . circles [ i ] . pos ,
partner . circles [ i ] . pos ])
return child
def mutate ( self ):
index = random . randint ( 0 , circles_no - 1 )
for x in self . circles :
if random . random () < mutation_rate :
x . color = random . choice ([ BLACK , WHITE ])
for x in self . circles :
if random . random () < mutation_rate :
x . pos = ( random . randint ( 0 , totalx ),
random . randint ( 0 , totaly ))
population = []
for _ in xrange ( pop_no ):
t = Drawing ()
population . append ( t )
gen = 0
while True :
print "Generation: " + str ( gen )
# calculate fitness
for d in population :
DISPLAYSURF . fill ( BLACK )
for x in d . circles :
surface = pygame . Surface (( totalx , totaly ))
surface . set_colorkey (( 0 , 0 , 0 ))
pygame . draw . circle ( surface , x . color , x . pos , x . radius )
DISPLAYSURF . blit ( surface , ( 0 , 0 ))
d . calcfitness ()
print d . fitness
best = max ( population , key = maxfit )
DISPLAYSURF . fill ( BLACK )
for x in best . circles :
surface = pygame . Surface (( totalx , totaly ))
surface . set_colorkey (( 0 , 0 , 0 ))
pygame . draw . circle ( surface , x . color , x . pos , x . radius )
DISPLAYSURF . blit ( surface , ( 0 , 0 ))
# mate
st = best . circles
c1 = Drawing ()
for i in xrange ( circles_no ):
c1 . circles [ i ] . color = st [ i ] . color
c1 . circles [ i ] . radius = st [ i ] . radius
c1 . circles [ i ] . pos = st [ i ] . pos
c1 . mutate ()
c2 = Drawing ()
for i in xrange ( circles_no ):
c2 . circles [ i ] . color = st [ i ] . color
c2 . circles [ i ] . radius = st [ i ] . radius
c2 . circles [ i ] . pos = st [ i ] . pos
c2 . mutate ()
c3 = Drawing ()
for i in xrange ( circles_no ):
c3 . circles [ i ] . color = st [ i ] . color
c3 . circles [ i ] . radius = st [ i ] . radius
c3 . circles [ i ] . pos = st [ i ] . pos
c3 . mutate ()
c4 = Drawing ()
for i in xrange ( circles_no ):
c4 . circles [ i ] . color = st [ i ] . color
c4 . circles [ i ] . radius = st [ i ] . radius
c4 . circles [ i ] . pos = st [ i ] . pos
c4 . mutate ()
population = []
population . append ( best )
population . append ( c1 )
population . append ( c2 )
population . append ( c3 )
population . append ( c4 )
pygame . display . update ()
if gen % 5 == 0 :
pygame . image . save ( DISPLAYSURF , str ( gen ) + ".jpeg" )
if gen % 1000 == 0 :
pickle . dump ( best . circles , open ( "save.p" , "wb" ))
gen += 1
At first the output is a random distribution of tiny circles as shown here:
But then, by the 16725th generation, the tiny circles closely resemble the reference image:
Using another modified and longer version of remake.py from the previous project, I can enlarge the quality of the image, and “connect the dots.”
from PIL import Image
import sys , pygame
from pygame.locals import *
import random
import math
import pickle
from collections import Counter
target = Image . open ( "dna.jpg" )
totalx = 3000
totaly = 6000
pop_no = 5
circles_no = 1000
mutation_rate = 0.01
pygame . init ()
DISPLAYSURF = pygame . display . set_mode (( totalx , totaly ))
pygame . display . set_caption ( 'DNA' )
WHITE = ( 255 , 255 , 255 )
BLACK = ( 0 , 0 , 0 )
def maxfit ( thing ):
return thing . fitness
def diff ( first , second ):
a = abs ( first [ 0 ] - second [ 0 ])
b = abs ( first [ 1 ] - second [ 1 ])
c = abs ( first [ 2 ] - second [ 2 ])
return ( a + b + c )
class Circle :
def __init__ ( self ):
self . color = random . choice ([ BLACK , WHITE ])
self . pos = ( random . randint ( 0 , totalx ),
random . randint ( 0 , totaly ))
self . radius = 2
class Drawing :
def __init__ ( self ):
self . circles = []
for _ in xrange ( circles_no ):
t = Circle ()
self . circles . append ( t )
def calcfitness ( self ):
totaldiff = 0
for x in xrange ( target . size [ 0 ]):
for y in xrange ( target . size [ 1 ]):
totaldiff += diff ( target . getpixel (( x , y )),
DISPLAYSURF . get_at (( x , y ))[: 3 ])
self . fitness = ( 20173671.0 / totaldiff ) ** 3
def crossover ( self , partner ):
child = Drawing ()
for i in xrange ( circles_no ):
child . circles [ i ] . color = random . choice ([ BLACK , WHITE ])
child . circles [ i ] . pos = random . choice ([ self . circles [ i ] . pos ,
partner . circles [ i ] . pos ])
return child
def mutate ( self ):
index = random . randint ( 0 , circles_no - 1 )
for x in self . circles :
if random . random () < mutation_rate :
x . color = random . choice ([ BLACK , WHITE ])
for x in self . circles :
if random . random () < mutation_rate :
x . pos = ( random . randint ( 0 , totalx ),
random . randint ( 0 , totaly ))
best = pickle . load ( open ( "save.p" , "rb" ))
DISPLAYSURF . fill ( BLACK )
t = []
RANGE = 2.5
for x in best :
maxx = x . pos [ 0 ] + RANGE
minx = x . pos [ 0 ] - RANGE
maxy = x . pos [ 1 ] + RANGE
miny = x . pos [ 1 ] - RANGE
for y in best :
if ( y != x ):
if ( y . pos [ 0 ] < maxx ) and ( y . pos [ 0 ] > minx ):
if ( y . pos [ 1 ] < maxy ) and ( y . pos [ 1 ] > miny ):
t . append ( y )
best = list ( set ( t ))
print best
def multiply ( x ):
return 36 * x
for x in best :
surface = pygame . Surface (( totalx , totaly ))
surface . set_colorkey (( 0 , 0 , 0 ))
pygame . draw . circle ( surface , x . color , tuple ( map ( multiply , x . pos )), x . radius * 7 )
for y in best :
if ( abs ( map ( multiply , x . pos )[ 0 ] - map ( multiply , y . pos )[ 0 ]) < 300 ) and ( abs ( map ( multiply , x . pos )[ 1 ] - map ( multiply , y . pos )[ 1 ]) < 300 ) and ( y != x ):
pygame . draw . line ( surface , WHITE , max ( [ tuple ( map ( multiply , x . pos )), tuple ( map ( multiply , y . pos ))]), min ( [ tuple ( map ( multiply , y . pos )) , tuple ( map ( multiply , y . pos )) ]), 1 )
DISPLAYSURF . blit ( surface , ( 0 , 0 ))
pygame . display . update ()
pygame . image . save ( DISPLAYSURF , "highres.jpeg" )
In the end, I get the following image. Personally, I think it looks really good :)
I also played around with the hues and saturation, which results in this: