diff --git a/echoserver.py b/echoserver.py index 04d07f0..879d323 100644 --- a/echoserver.py +++ b/echoserver.py @@ -4,6 +4,7 @@ import tornado.ioloop import os web_dir = os.path.join(os.path.split(__file__)[0], 'www') +output_dir = os.path.join(os.path.split(__file__)[0], 'output') # This is our WebSocketHandler - it handles the messages # from the tornado server @@ -27,6 +28,7 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler): application = tornado.web.Application([ (r"/ws", WebSocketHandler), + (r"/output/(.*)", tornado.web.StaticFileHandler, {"path": output_dir}), (r"/(.*)", tornado.web.StaticFileHandler, {"path": web_dir, "default_filename": 'index.html'}), ],debug=True) diff --git a/portrait_compositor.py b/portrait_compositor.py index 9fb8884..5b1ddae 100644 --- a/portrait_compositor.py +++ b/portrait_compositor.py @@ -13,6 +13,9 @@ from websocket import create_connection import logging import json +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger('portraits') + camera = picamera.PiCamera() camera.rotation = 180 camera.resolution = (1920, 1080) @@ -24,6 +27,8 @@ outputResolution = (1000, 1000) genders = ['male', 'female', 'unknown'] perspectives = ['side', 'front'] gender_perspectives = [g+"_"+p for p in perspectives for g in genders] +# we also want dedicated composites for the perspectives +gender_perspectives.extend(perspectives) curdir = os.path.dirname(os.path.abspath(__file__)) tmpimage = '/tmp/piimage.jpg' @@ -42,6 +47,8 @@ environment = { 'LD_PRELOAD': '/usr/lib/arm-linux-gnueabihf/libopencv_core.so.2.4', } +updateGoesOk = True; + def updateStats(type, name, count, image_filename): params = { 'type': type, @@ -49,35 +56,41 @@ def updateStats(type, name, count, image_filename): 'time': int(time.time()), 'case_count': int(count), } - try: - ws = create_connection("ws://localhost:8888/ws") - js = json.dumps({ - 'type': type, - 'name': name, - 'img_src': os.path.basename(image_filename), - 'case_count': int(count), - }) - ws.send(js) - except Exception as e: - raise + # try: + # ws = create_connection("ws://localhost:8888/ws") + # js = json.dumps({ + # 'type': type, + # 'name': name, + # 'img_src': os.path.basename(image_filename), + # 'case_count': int(count), + # }) + # ws.send(js) + # except Exception as e: + # raise url = 'https://artstats.rubenvandeven.com/composites/views.php' - if count % 10 == 0: - # only send every one in x image, so that the server never can - # retrace _exact_ faces by comparing the sent frames. - with open(image_filename) as fp: - print('send request including image') + try: + if count % 10 == 0: + # only send every one in x image, so that the server never can + # retrace _exact_ faces by comparing the sent frames. + with open(image_filename) as fp: + logger.info('send request including image') + r = requests.post( + url , + files={'image': fp}, + params=params + ) + else: + logger.info('send request') r = requests.post( - url , - files={'image': fp}, + url, params=params ) - else: - print('send request') - r = requests.post( - url, - params=params - ) + updateGoesOk = True + except Exception as e: + updateGoesOk = False + logger.critical("Error when updating statistics") + logger.exception(e) class CompositeImage: def __init__(self, name, resolution): @@ -102,7 +115,7 @@ class CompositeImage: self.count = i name = self.get_frame_filename(self.count) img_file = os.path.join(dir, name) - print("\trestore {}".format(img_file)) + logger.info("\trestore {}".format(img_file)) self.image = np.array(Image.open(img_file)).astype('float64') self.state_dirty = False @@ -112,12 +125,15 @@ class CompositeImage: def get_frame_filename(self, i): return "{}-{}x{}-{}.png".format(self.name, self.resolution[0], self.resolution[1], i) + def get_current_filename(self): + return self.get_frame_filename(self.count) def save_image(self, dir): if self.state_dirty is False: + # don't save if no changes are made since last save return - name = self.get_frame_filename(self.count) + name = self.get_current_filename() filename = os.path.join(dir, name) self.get_image().save(filename) @@ -140,6 +156,9 @@ class CompositeCollection: def get_pickle_filename(self): return os.path.join(self.target_dir, self.id + ".p") + def get_json_filename(self): + return os.path.join(self.target_dir, "composites.json") + def load(self): pickle_file_name = self.get_pickle_filename() # if os.path.exists(pickle_file_name): @@ -151,7 +170,7 @@ class CompositeCollection: composites[name] = CompositeImage(name, self.size) composites[name].restore( data['c'][name], self.target_dir) except Exception as e: - print("Create new composite", e) + logger.info("Create new composite", e) for name in self.names: composites[name] = CompositeImage(name, self.size) @@ -163,9 +182,31 @@ class CompositeCollection: data['c'][name] = self.composites[name].count with open( self.get_pickle_filename(), "wb" ) as fp: - print("Save", data) + logger.info("Save", data) pickle.dump( data, fp ) + self.save_json() + + def save_json(self): + """ + Save statistics as json + """ + data = {} + for name in self.composites: + data[name] = { + 'count': self.composites[name].count, + 'img': self.composites[name].get_current_filename() + } + + with open( self.get_json_filename(), "w" ) as fp: + logger.debug("Json to {}".format(self.get_json_filename())) + json.dump(data, fp) + + ws = create_connection("ws://localhost:8888/ws") + ws.send("update") + + + def save_img(self, name): self.get(name).save_image(self.target_dir) @@ -192,19 +233,22 @@ class CompositeCollection: for i in range(start, end): filename = os.path.join(self.target_dir, c.get_frame_filename(i)) if os.path.exists(filename): - print("Clean {}".format(filename)) + logger.info("Clean {}".format(filename)) os.unlink(filename) def append_face(row, image, composites): # degrees to distinguish side (as we will never be able to use 90 :-( ) - suffix = 'side' if abs(float(row['yaw'])) > 20 else 'front' - # print('yaw:', float(row['yaw'])) - name = "{}_{}".format(row['gender'], suffix) - if name not in composites.names: - return - composite = composites.get(name) + # Plus, we now have a wide angle lens. + suffix = 'side' if abs(float(row['yaw'])) > 12 else 'front' + names = [suffix, "{}_{}".format(row['gender'], suffix)] + + compositesToUse = [] + for name in names: + if name not in composites.names: + return + compositesToUse.append(composites.get(name)) # TODO: matrix transform the image, to skew the face into being a flat-ish surface # This might yield less blurry composites @@ -228,16 +272,17 @@ def append_face(row, image, composites): # PIL.Image handles cropping outside the canvas by filling with black/transparent x = face_x + dx y = face_y + dy - print('crop') + logger.debug('crop') i = image.crop((x,y, x + size_x, y + size_y)) if suffix == 'side' and float(row['yaw']) < 0: - print('\tflip') + logger.debug('\tflip') i = i.transpose(Image.FLIP_LEFT_RIGHT) - print('add') - composite.addFace(i) - print('added') + for composite in compositesToUse: + logger.debug('add') + composite.addFace(i) + logger.debug('added') composites = CompositeCollection(gender_perspectives, outputResolution, os.path.join(curdir, 'output')) @@ -250,7 +295,7 @@ while True: img = Image.open(tmpimage) os.unlink(tmpimage) with open(tmpimageResults) as csvfile: - print("open csv") + logger.debug("open csv") data = csv.DictReader(csvfile) faces = 0 for row in data: @@ -258,15 +303,14 @@ while True: # not a valid face continue faces += 1 - print("append face") + logger.info("append face") append_face(row, img, composites) if faces > 0: - print("save :-)") + logger.info("save :-)") for name in composites.names: - print("\tsave img '{}'".format(name)) + logger.info("\tsave img '{}'".format(name)) c = composites.save_img(name) # save pickle after images, so they can be restored composites.save() composites.clean() - # TODO: trigger output update diff --git a/www/composite_loader.js b/www/composite_loader.js new file mode 100644 index 0000000..8dda78e --- /dev/null +++ b/www/composite_loader.js @@ -0,0 +1,100 @@ +class Face { + constructor(id) { + this.id = id; + this.value = 0; + // this.name = name; + this.imgEls = []; + this.valueEls = []; + + let els = document.querySelectorAll(`[data-face='${this.id}']`); + for(let el of els) { + if(el.tagName == 'IMG') { + this.imgEls.push(el); + } else { + this.valueEls.push(el); + } + } + } + + addImgEl(el) { + this.imgEls.push(el) + } + + addValueEl(el) { + this.valueEls.push(el) + } + + update(imgUrl, value) { + for(let el of this.valueEls) { + el.textContent = new Intl.NumberFormat("nl").format(value); + } + for(let el of this.imgEls) { + el.src = imgUrl; + } + } +} + +class Bar { + constructor(el) { + this.el = el; + this.values = {}; + } + + setValue(id, value) { + this.values[id] = value; + this.update(); + } + + update() { + for(let id in this.values) { + let el = document.getElementById('graph--' + id); + if(!el) continue; + + let nrEls = el.getElementsByClassName('nr'); + for(let nrEl of nrEls) { + nrEl.textContent = new Intl.NumberFormat("nl").format(this.values[id]); + } + + el.style.flex = parseInt(this.values[id]); + } + } + +} + +var bar = new Bar(document.getElementById('graph')); + +var faces = { + 'front': new Face('front'), + 'side': new Face('side'), + 'male_front': new Face('male_front'), + 'female_front': new Face('female_front'), + 'unknown_front': new Face('unknown_front'), + 'male_side': new Face('male_side'), + 'female_side': new Face('female_side'), + 'unknown_side': new Face('unknown_side') +} + +var update = function() { + let req = new XMLHttpRequest(); + req.addEventListener("load", function(e){ + let data = JSON.parse(this.responseText); + console.log(data); + for(let name in data){ + faces[name].update('/output/'+data[name]['img'], data[name]['count']); + } + + bar.setValue('male', data['male_front']['count'] + data['male_side']['count']) + bar.setValue('female', data['female_front']['count'] + data['female_side']['count']) + bar.setValue('unknown', data['unknown_front']['count'] + data['unknown_side']['count']) + }); + req.open("GET", "/output/composites.json?v=" + Date.now()); + req.send(); +} + +var socket = new ReconnectingWebSocket("ws://"+window.location.hostname+":"+window.location.port+"/ws") +socket.addEventListener('message', function (event) { + //trigger update + update(); +}); + +update(); diff --git a/www/index.html b/www/index.html index df9d567..44a5a4e 100644 --- a/www/index.html +++ b/www/index.html @@ -9,291 +9,164 @@
An examination of discriminatory traits in gender differentation.