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