Version one of Alles Wat ik Voel
This commit is contained in:
commit
528f4016b0
7 changed files with 1293 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
__pycache__/
|
||||
env/
|
||||
|
||||
|
89
colour.py
Normal file
89
colour.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
import struct
|
||||
import scipy
|
||||
import scipy.misc
|
||||
import scipy.cluster
|
||||
import codecs
|
||||
# from IPython.display import Markdown, display, HTML
|
||||
import colorsys
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
NUM_CLUSTERS = 64
|
||||
|
||||
def getColourAsHex(colour):
|
||||
return '#' + ''.join(format(c, '02x') for c in colour.astype(int))
|
||||
|
||||
def getColoursForImageByClusters(image):
|
||||
"""
|
||||
Adapted on answers by
|
||||
Peter Hansen (http://stackoverflow.com/a/3244061)
|
||||
& Johan Mickos (http://stackoverflow.com/a/34140327)
|
||||
"""
|
||||
im = image.copy().resize((150, 150)) # optional, to reduce time
|
||||
ar = scipy.misc.fromimage(im)
|
||||
shape = ar.shape
|
||||
ar = ar.reshape(scipy.product(shape[:2]), shape[2])
|
||||
|
||||
# print( 'finding clusters')
|
||||
codes, dist = scipy.cluster.vq.kmeans(ar.astype(float), NUM_CLUSTERS)
|
||||
# print ('cluster centres:\n', codes)
|
||||
|
||||
vecs, dist = scipy.cluster.vq.vq(ar, codes) # assign codes
|
||||
counts, bins = scipy.histogram(vecs, len(codes)) # count occurrences
|
||||
|
||||
# When only looking for single color:
|
||||
# index_max = scipy.argmax(counts) # find most frequent
|
||||
# peak = codes[index_max]
|
||||
# colour = ''.join(chr(c) for c in peak).encode('hex')
|
||||
# print( 'most frequent is %s (#%s)' % (peak, colour))
|
||||
|
||||
percentages = 100 * counts / sum(counts)
|
||||
# print("Percentages", percentages)
|
||||
# colours = [ in codes]
|
||||
# print(colours)
|
||||
return list(zip(codes, percentages))
|
||||
|
||||
def getColoursForImageByPxAvg(image):
|
||||
im = image.copy().resize((8, 8))
|
||||
pixels = np.concatenate(scipy.misc.fromimage(im))
|
||||
# colours = ['#' + ''.join(format(c, '02x') for c in color.astype(int)) for color in pixels]
|
||||
percentages = np.zeros(len(pixels)) + (100 / len(pixels))
|
||||
return list(zip(pixels, percentages))
|
||||
|
||||
def getColoursAsHTML(colours):
|
||||
return " ".join(['<span style="background:%s">%s - (%s %%)</span>' % (getColourAsHex(colour[0]), getColourAsHex(colour[0]), colour[1]) for colour in colours]);
|
||||
|
||||
def loadColoursFromDbImages(images):
|
||||
return [i.colours for i in images]
|
||||
|
||||
|
||||
def getSvgFromDbImages(images, elId = ""):
|
||||
# sum concatenates all colour arrays
|
||||
allColours = []
|
||||
for c in loadColoursFromDbImages(images):
|
||||
allColours += c
|
||||
# box 160, because center or circle = 100 => +/- 50 => + r of colour circle (max: 10) => 160
|
||||
svg = '<svg viewBox="-160 -160 320 320" xmlns="http://www.w3.org/2000/svg" id="%s">' % elId
|
||||
|
||||
radius = 100
|
||||
|
||||
for colour in allColours:
|
||||
rgb, percentage = colour
|
||||
rgbNorm = rgb/255
|
||||
hsv = colorsys.rgb_to_hsv(rgbNorm[0], rgbNorm[1], rgbNorm[2])
|
||||
# find position on circle
|
||||
radians = 2 * math.pi * hsv[0]
|
||||
x = math.cos(radians)
|
||||
y = math.sin(radians)
|
||||
|
||||
# based on saturation, we move inwards/outwards
|
||||
# min = 0.5, max = 1.5 (dus + 0.5)
|
||||
pos = np.array([x,y]) * (0.5 + hsv[1]) * radius
|
||||
# Posibilitiy: determine position based on avg(saturation, value) => dark & grey inside, shiney and colourful outside
|
||||
# pos = np.array([x,y]) * (0.5 + (hsv[1]+hsv[2])/2) * radius
|
||||
r = max(1,-10/percentage+10) # as r, we converge to maximum radius 10, but don't want to get smaller radi then 1
|
||||
c = '<circle cx="%s" cy="%s" r="%s" style="fill:%s" />' % (pos[0], pos[1], r, getColourAsHex(rgb))
|
||||
svg += c
|
||||
|
||||
svg += "</svg>"
|
||||
return svg
|
BIN
images.db
Normal file
BIN
images.db
Normal file
Binary file not shown.
49
models.py
Normal file
49
models.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
from peewee import *
|
||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||
import datetime
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
def coloursToJson(colours):
|
||||
colours2 = [(list(colour[0]), colour[1]) for colour in colours]
|
||||
return json.dumps(colours2)
|
||||
|
||||
def jsonToColours(string):
|
||||
data = json.loads(string)
|
||||
return [(np.array(d[0]), d[1]) for d in data]
|
||||
|
||||
db = SqliteExtDatabase('images.db')
|
||||
|
||||
class ColoursField(TextField):
|
||||
# db_field = 'colour'
|
||||
|
||||
def db_value(self, value):
|
||||
return coloursToJson(value)
|
||||
|
||||
def python_value(self, value):
|
||||
return jsonToColours(value) # convert str to UUID
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
class Meta:
|
||||
database = db
|
||||
|
||||
class Emotion(BaseModel):
|
||||
name = CharField(unique=True)
|
||||
|
||||
class Group(BaseModel):
|
||||
name = CharField(unique=True)
|
||||
|
||||
class Artwork(BaseModel):
|
||||
author = CharField()
|
||||
age = SmallIntegerField(index=True)
|
||||
gender = FixedCharField(max_length=1) # we should not really use this one
|
||||
group = ForeignKeyField(Group, related_name='artworks', index=True)
|
||||
emotion = ForeignKeyField(Emotion, related_name='artworks', index=True)
|
||||
created_date = DateTimeField(default=datetime.datetime.now)
|
||||
filename = CharField()
|
||||
colours = ColoursField() # serialised colours + percentages: [([r,g,b], percentage), ...]
|
||||
|
||||
def getAges():
|
||||
r = Artwork.select(fn.Distinct(Artwork.age)).dicts()
|
||||
return [a['age'] for a in r]
|
55
server.py
Normal file
55
server.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
import models
|
||||
import os
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
import colour
|
||||
|
||||
models.db.connect()
|
||||
|
||||
curpath = os.path.dirname(os.path.abspath(__file__))
|
||||
tplpath = os.path.join(curpath, "templates");
|
||||
|
||||
loader = tornado.template.Loader(tplpath, autoescape=None)
|
||||
|
||||
config = {
|
||||
"port": 8881,
|
||||
"conn": models.db
|
||||
}
|
||||
|
||||
emotions = [ e for e in models.Emotion.select()]
|
||||
|
||||
|
||||
class MainHandler(tornado.web.RequestHandler):
|
||||
def get(self):
|
||||
loader.reset()
|
||||
html = loader.load('index.html').generate(host=self.request.host, emotions=emotions, ages=models.getAges(), groups=models.Group.select())
|
||||
self.write(html)
|
||||
|
||||
class ColourHandler(tornado.web.RequestHandler):
|
||||
def get(self):
|
||||
req_group = self.get_query_argument("group", default=None)
|
||||
req_age = self.get_query_argument("age", default=None)
|
||||
req_emotion = self.get_query_argument("emotion", default=None)
|
||||
req_elId = self.get_query_argument("elId", default="")
|
||||
|
||||
q = models.Artwork.select()
|
||||
if req_group is not None:
|
||||
group = models.Group.get(models.Group.id == req_group)
|
||||
q = q.where(models.Artwork.group == group)
|
||||
elif req_age is not None:
|
||||
q = q.where(models.Artwork.age == req_age)
|
||||
elif req_emotion is not None:
|
||||
emotion = models.Emotion.get(models.Emotion.id == req_emotion)
|
||||
q = q.where(models.Artwork.emotion == emotion)
|
||||
images = q
|
||||
svg = colour.getSvgFromDbImages(images, elId = req_elId)
|
||||
self.write(svg)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Start server", config)
|
||||
app = tornado.web.Application([
|
||||
(r"/", MainHandler),
|
||||
(r"/colours", ColourHandler),
|
||||
])
|
||||
app.listen(config['port'])
|
||||
tornado.ioloop.IOLoop.current().start()
|
173
templates/index.html
Normal file
173
templates/index.html
Normal file
|
@ -0,0 +1,173 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<style type="text/css">
|
||||
body{
|
||||
background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABmJLR0QAdwB3AHctbh3qAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QMFFQkN1CxOCgAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAXElEQVRo3u3YQRHAIAxFwVAT1L85gopgoNdc6D4DmZ1/y8hcFc3N+XafiCcuCQQEBAQEBAQEBAQEBAQEBATkV5BRVe0Pur3TIiAgICAgICAgICAgICAgICAgIB8dr84LFaZAYuYAAAAASUVORK5CYII=');
|
||||
font-family: monospace;
|
||||
font-size: 12pt;
|
||||
}
|
||||
body.loading #colourImage{
|
||||
opacity: 0;
|
||||
transition: opacity .4s;
|
||||
}
|
||||
#main svg{
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
left:0;top:0;right:0;bottom: 0;
|
||||
}
|
||||
aside ul{
|
||||
list-style: none;
|
||||
}
|
||||
aside ul li{
|
||||
padding: 0.1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
aside ul li span{
|
||||
background:#ff0;
|
||||
transition: max-width 0.5s;
|
||||
max-width: 0%;
|
||||
display: inline-block;
|
||||
overflow: visible;
|
||||
}
|
||||
aside ul li:hover span{
|
||||
max-width: 100%;
|
||||
}
|
||||
aside ul li.selected span{
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
aside#selects{
|
||||
float:left;
|
||||
}
|
||||
|
||||
aside#stats{
|
||||
float:right;
|
||||
}
|
||||
|
||||
#spinner{
|
||||
display:none;
|
||||
}
|
||||
body.loading #spinner{
|
||||
display: block;
|
||||
animation: rotate 3s infinite linear;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Standard syntax */
|
||||
@keyframes rotate {
|
||||
from {transform: rotate(0deg);}
|
||||
to {transform: rotate(360deg);}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<aside id='selects'>
|
||||
<h3>Toon op basis van...</h3>
|
||||
<h4>groep</h4>
|
||||
<ul>
|
||||
{% for group in groups %}
|
||||
<li data-param="group" data-id="{{group.id}}"><span>{{group.name.replace(" ", " ")}}</span></li>
|
||||
{% end %}
|
||||
</ul>
|
||||
<h4>leeftijd</h4>
|
||||
<ul>
|
||||
{% for age in ages %}
|
||||
<li data-param="age" data-id="{{age}}"><span>{{age}} jaar</span></li>
|
||||
{% end %}
|
||||
</ul>
|
||||
<h4>emotie</h4>
|
||||
<ul>
|
||||
{% for emotion in emotions %}
|
||||
<li data-param="emotion" data-id="{{emotion.id}}"><span>{{emotion.name}}</span></li>
|
||||
{% end %}
|
||||
</ul>
|
||||
<ul>
|
||||
<li data-param="no" data-id="thing"><span>toon alles</span></li>
|
||||
</ul>
|
||||
<!-- <h4>Kleur???</h4> -->
|
||||
</aside>
|
||||
<aside id='works'>
|
||||
</aside>
|
||||
<aside id='stats'>
|
||||
<h4>Emoties</h4>
|
||||
|
||||
<h4>Licht/donker</h4>
|
||||
|
||||
</aside>
|
||||
<div id='main'>
|
||||
<svg viewBox="-160 -160 320 320" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%">
|
||||
<defs>
|
||||
<mask id="mask1" x="0" y="0" width="100" height="100" >
|
||||
<circle r='155' cx="0" cy="0" style='fill:white;' />
|
||||
<circle r='50' cx="0" cy="0" style="fill:black" />
|
||||
</mask>
|
||||
</defs>
|
||||
<circle r='155' cx="0" cy="0" style='stroke:none;fill:rgba(0,0,0,0.1);mask: url(#mask1);' />
|
||||
</svg>
|
||||
|
||||
<svg id="spinner" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 1000 1000" style="width:5%;height:5%;top:47.5%;left:47.5%;">
|
||||
<g><path d="M500,964.2c-130.9,0-253.9-51-346.5-143.5C61,728.1,10,605.1,10,474.2c0-92.7,26-182.9,75.2-261C133.1,137.3,200.7,76,280.8,35.8l41.2,82.1c-65.1,32.6-120.1,82.5-159,144.2c-40,63.4-61.1,136.6-61.1,212c0,219.5,178.6,398.1,398.1,398.1c219.5,0,398.1-178.6,398.1-398.1c0-75.3-21.1-148.6-61.1-212c-38.9-61.7-93.9-111.6-159-144.2l41.2-82.1C799.3,76,866.9,137.3,914.8,213.2c49.2,78.1,75.2,168.3,75.2,261c0,130.9-51,253.9-143.5,346.5C753.9,913.2,630.9,964.2,500,964.2z"/></g>
|
||||
</svg>
|
||||
|
||||
<svg viewBox="-160 -160 320 320" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%" id="colourImage">
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function updateProgress(evt)
|
||||
{
|
||||
console.log(evt);
|
||||
}
|
||||
function transferComplete(evt, r)
|
||||
{
|
||||
// console.log(evt);
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = evt.target.response;
|
||||
document.getElementById('colourImage').innerHTML = div.children[0].innerHTML;
|
||||
document.body.classList.remove('loading');
|
||||
}
|
||||
function transferFailed(evt)
|
||||
{
|
||||
console.log(evt);
|
||||
}
|
||||
function transferCanceled(evt)
|
||||
{
|
||||
console.log(evt);
|
||||
}
|
||||
|
||||
var links = document.getElementById('selects').getElementsByTagName("li");
|
||||
for(let link of links)
|
||||
{
|
||||
link.onclick = function() {
|
||||
document.body.classList.add('loading');
|
||||
// document.getElementById('colourImage').innerHTML = "";
|
||||
console.log('click',this.dataset.param, this.dataset.id);
|
||||
var oReq = new XMLHttpRequest();
|
||||
oReq.addEventListener("progress", updateProgress);
|
||||
oReq.addEventListener("load", transferComplete);
|
||||
oReq.addEventListener("error", transferFailed);
|
||||
oReq.addEventListener("abort", transferCanceled);
|
||||
oReq.open("GET", "/colours?"+this.dataset.param+"="+this.dataset.id+"&elId=colourImage");
|
||||
oReq.send()
|
||||
|
||||
for(let unsetLink of links) {
|
||||
unsetLink.classList.remove('selected');
|
||||
}
|
||||
this.classList.add('selected');
|
||||
}.bind(link)
|
||||
}
|
||||
// var colourImage = document.getElementById('colourImage');
|
||||
// var i = 0;
|
||||
// for(let circle of colourImage.children) {
|
||||
// i++;
|
||||
// // console.log(circle);
|
||||
// setTimeout(function() {
|
||||
// this.remove();
|
||||
// }.bind(circle), i * 3);
|
||||
// }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
923
ways_of_seeing_images.ipynb
Normal file
923
ways_of_seeing_images.ipynb
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue