From 9b93c8ec2a633ef18d1626d5b3eee772eec10875 Mon Sep 17 00:00:00 2001 From: j <0x006A@0x2620.org> Date: Wed, 6 Jun 2012 21:49:32 +0200 Subject: [PATCH] add sequence backend --- pandora/sequence/__init__.py | 0 pandora/sequence/extract.py | 79 +++++++++++++ pandora/sequence/managers.py | 163 +++++++++++++++++++++++++++ pandora/sequence/models.py | 56 +++++++++ pandora/sequence/tasks.py | 31 +++++ pandora/sequence/views.py | 124 ++++++++++++++++++++ pandora/settings.py | 1 + static/js/pandora/sequencesDialog.js | 18 +-- 8 files changed, 465 insertions(+), 7 deletions(-) create mode 100644 pandora/sequence/__init__.py create mode 100644 pandora/sequence/extract.py create mode 100644 pandora/sequence/managers.py create mode 100644 pandora/sequence/models.py create mode 100644 pandora/sequence/tasks.py create mode 100644 pandora/sequence/views.py diff --git a/pandora/sequence/__init__.py b/pandora/sequence/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pandora/sequence/extract.py b/pandora/sequence/extract.py new file mode 100644 index 00000000..dd7ad63f --- /dev/null +++ b/pandora/sequence/extract.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import division +import Image +import os + +ZONE_INDEX = [] +for pixel_index in range(64): + x, y = pixel_index % 8, int(pixel_index / 8) + ZONE_INDEX.append(int(x / 2) + int(y / 4) * 4) + +def get_hash(image, mode, debug=False): + if mode == 'color': + # divide the image into 8 zones: + # 0 0 1 1 2 2 3 3 + # 0 0 1 1 2 2 3 3 + # 0 0 1 1 2 2 3 3 + # 0 0 1 1 2 2 3 3 + # 4 4 5 5 6 6 7 7 + # 4 4 5 5 6 6 7 7 + # 4 4 5 5 6 6 7 7 + # 4 4 5 5 6 6 7 7 + image_data = image.getdata() + image_hash = 0 + zone_values = [] + for zone_index in range(8): + zone_values.append([]) + for pixel_index, pixel_value in enumerate(image_data): + zone_values[ZONE_INDEX[pixel_index]].append(pixel_value) + for zone_index, pixel_values in enumerate(zone_values): + # get the mean for each color channel + mean = map(lambda x: int(round(sum(x) / 8)), zip(*pixel_values)) + # store the mean color of each zone as an 8-bit value: + # RRRGGGBB + color_index = sum(( + int(mean[0] / 32) << 5, + int(mean[1] / 32) << 2, + int(mean[2] / 64) + )) + image_hash += color_index * pow(2, zone_index * 8) + elif mode == 'shape': + image_data = image.convert('L').getdata() + image_mean = sum(image_data) / 64 + image_hash = 0 + for pixel_index, pixel_value in enumerate(image_data): + if pixel_value > image_mean: + image_hash += pow(2, pixel_index) + #return hash as 16 character hex string + h = hex(image_hash)[2:].upper() + if h.endswith('L'): h = h[:-1] + return '0' * (16-len(h)) + h + +def get_sequences(path): + modes = ['color', 'shape'] + sequences = {} + for mode in modes: + sequences[mode] = [] + fps = 25 + position = 0 + file_names = filter(lambda x: 'timelinedata8p' in x, os.listdir(path)) + file_names = sorted(file_names, key=lambda x: int(x[14:-4])) + file_names = map(lambda x: path + x, file_names) + for file_name in file_names: + timeline_image = Image.open(file_name) + timeline_width = timeline_image.size[0] + for x in range(0, timeline_width, 8): + frame_image = timeline_image.crop((x, 0, x + 8, 8)) + for mode in modes: + frame_hash = get_hash(frame_image, mode) + if position == 0 or frame_hash != sequences[mode][-1]['hash']: + if position > 0: + sequences[mode][-1]['out'] = position + sequences[mode].append({'in': position, 'hash': frame_hash}) + position += 1 / fps + for mode in modes: + if sequences[mode]: + sequences[mode][-1]['out'] = position + return sequences + diff --git a/pandora/sequence/managers.py b/pandora/sequence/managers.py new file mode 100644 index 00000000..272330de --- /dev/null +++ b/pandora/sequence/managers.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from django.db.models import Q, Manager +from django.conf import settings + +from ox.django.query import QuerySet + +from item.utils import decode_id + + +def parseCondition(condition, user): + ''' + condition: { + value: "war" + } + or + condition: { + key: "year", + value: "1970-1980, + operator: "!=" + } + ''' + k = condition.get('key', 'name') + k = { + 'id': 'public_id', + 'in': 'start', + 'out': 'end' + }.get(k, k) + if not k: + k = 'name' + v = condition['value'] + op = condition.get('operator') + if not op: + op = '' + + if op.startswith('!'): + op = op[1:] + exclude = True + else: + exclude = False + if op == '-': + q = parseCondition({'key': k, 'value': v[0], 'operator': '>='}, user) \ + & parseCondition({'key': k, 'value': v[1], 'operator': '<'}, user) + if exclude: + return ~q + else: + return q + if (not exclude and op == '=' or op in ('$', '^', '>=', '<')) and v == '': + return Q() + + if k.endswith('__id'): + v = decode_id(v) + if isinstance(v, bool): #featured and public flag + key = k + else: + key = "%s%s" % (k, { + '>': '__gt', + '>=': '__gte', + '<': '__lt', + '<=': '__lte', + '==': '__iexact', + '=': '__icontains', + '^': '__istartswith', + '$': '__iendswith', + }.get(op, '__icontains')) + + key = str(key) + if exclude: + q = ~Q(**{key: v}) + else: + q = Q(**{key: v}) + return q + +def parseConditions(conditions, operator, user): + ''' + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + ''' + conn = [] + for condition in conditions: + if 'conditions' in condition: + q = parseConditions(condition['conditions'], + condition.get('operator', '&'), user) + if q: + conn.append(q) + pass + else: + conn.append(parseCondition(condition, user)) + if conn: + q = conn[0] + for c in conn[1:]: + if operator == '|': + q = q | c + else: + q = q & c + return q + return None + + + +class SequenceManager(Manager): + + def get_query_set(self): + return QuerySet(self.model) + + def find(self, data, user): + ''' + query: { + conditions: [ + { + value: "war" + } + { + key: "year", + value: "1970-1980, + operator: "!=" + }, + { + key: "country", + value: "f", + operator: "^" + } + ], + operator: "&" + } + ''' + + #join query with operator + qs = self.get_query_set() + + conditions = parseConditions(data.get('query', {}).get('conditions', []), + data.get('query', {}).get('operator', '&'), + user) + if conditions: + qs = qs.filter(conditions) + + #anonymous can only see public items + if not user or user.is_anonymous(): + allowed_level = settings.CONFIG['capabilities']['canSeeItem']['guest'] + qs = qs.filter(sort__rightslevel__lte=allowed_level) + #users can see public items, there own items and items of there groups + else: + allowed_level = settings.CONFIG['capabilities']['canSeeItem'][user.get_profile().get_level()] + q = Q(sort__rightslevel__lte=allowed_level)|Q(user=user.id) + if user.groups.count(): + q |= Q(item__groups__in=user.groups.all()) + qs = qs.filter(q) + #admins can see all available items + return qs diff --git a/pandora/sequence/models.py b/pandora/sequence/models.py new file mode 100644 index 00000000..9b487f46 --- /dev/null +++ b/pandora/sequence/models.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import division, with_statement + +import re + +from django.db import models, transaction +from django.contrib.auth.models import User, Group +from django.db.models import Q +from django.conf import settings +import ox + +import managers +from annotation.models import Annotation, get_matches, get_super_matches +from item.models import Item, ItemSort +from changelog.models import Changelog + +from django.db import models + +class Sequence(models.Model): + public_id = models.CharField(max_length=128, unique=True) + item = models.ForeignKey(Item, null=True, related_name='sequences') + sort = models.ForeignKey(ItemSort, null=True, related_name='sequences') + user = models.IntegerField(db_index=True, null=True) + + mode = models.CharField(max_length=255) + hash = models.CharField(db_index=True, max_length=16, default='') + start = models.FloatField(default=-1, db_index=True) + end = models.FloatField(default=-1) + + objects = managers.SequenceManager() + + def save(self, *args, **kwargs): + self.public_id = u"%s/%s/%s-%s" % ( + self.item.itemId, self.mode, float(self.start), float(self.end) + ) + if self.item: + self.user = self.item.user and self.item.user.id + self.sort = self.item.sort + super(Sequence, self).save(*args, **kwargs) + + def __unicode__(self): + return self.public_id + + def json(self, keys=None, user=None): + j = { + 'id': self.public_id, + 'hash': self.hash, + 'in': self.start, + 'out': self.end, + } + if keys: + for key in keys: + if key not in j: + j[key] = self.item.json.get(key) + return j diff --git a/pandora/sequence/tasks.py b/pandora/sequence/tasks.py new file mode 100644 index 00000000..c09976da --- /dev/null +++ b/pandora/sequence/tasks.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +import os +from datetime import timedelta, datetime +import gzip +import random +random + +from django.conf import settings +from django.db import connection, transaction +from ox.utils import ET +from celery.task import task, periodic_task + +import models +import extract + +@task(ignore_results=True, queue='default') +def get_sequences(itemId): + i = models.Item.objects.get(itemId=itemId) + models.Sequence.objects.filter(item=i).delete() + data = extract.get_sequences(i.timeline_prefix) + with transaction.commit_on_success(): + for mode in data: + for seq in data[mode]: + s = models.Sequence() + s.item = i + s.mode = mode + s.start = seq['in'] + s.end = seq['out'] + s.hash = seq['hash'] + s.save() diff --git a/pandora/sequence/views.py b/pandora/sequence/views.py new file mode 100644 index 00000000..a1e1c1bf --- /dev/null +++ b/pandora/sequence/views.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# vi:si:et:sw=4:sts=4:ts=4 +from __future__ import division + +from django.conf import settings +from ox.utils import json +from ox.django.shortcuts import render_to_json_response, json_response + +from ox.django.api import actions + +from annotation.models import Annotation +from item.models import Item +from item import utils + +import models + +def parse_query(data, user): + query = {} + query['range'] = [0, 100] + query['sort'] = [{'key':'in', 'operator':'+'}] + for key in ('keys', 'group', 'range', 'sort', 'query'): + if key in data: + query[key] = data[key] + query['qs'] = models.Sequence.objects.find(query, user) + if 'itemsQuery' in data and data['itemsQuery'].get('conditions'): + item_query = Item.objects.find({'query': data['itemsQuery']}, user) + query['qs'] = query['qs'].filter(item__in=item_query) + return query + +def order_query(qs, sort): + order_by = [] + sort += [ + {'key': 'in', 'operator': '-'}, + ] + for e in sort: + operator = e['operator'] + if operator != '-': + operator = '' + key = { + 'in': 'start', + 'out': 'end', + }.get(e['key'], e['key']) + if key not in ('start', 'end', 'mode', 'hash'): + key = 'sort__%s' % key + order = '%s%s' % (operator, key) + order_by.append(order) + if order_by: + qs = qs.order_by(*order_by, nulls_last=True) + return qs + +def findSequences(request): + ''' + param data { + query: ... + itemsQuery: ... + } + + return { + 'status': {'code': int, 'text': string} + 'data': { + items = [{..}, {...}, ...] + } + } + ''' + data = json.loads(request.POST['data']) + response = json_response() + + query = parse_query(data, request.user) + qs = query['qs'] + if 'keys' in data: + qs = qs[query['range'][0]:query['range'][1]] + response['data']['items'] = [p.json(data['keys'], request.user) for p in qs] + elif 'position' in query: + qs = order_query(qs, query['sort']) + ids = [i['public_id'] for i in qs.values('public_id')] + data['conditions'] = data['conditions'] + { + 'value': data['position'], + 'key': query['sort'][0]['key'], + 'operator': '^' + } + query = parse_query(data, request.user) + qs = order_query(query['qs'], query['sort']) + if qs.count() > 0: + response['data']['position'] = utils.get_positions(ids, [qs[0].itemId])[0] + elif 'positions' in data: + qs = order_query(qs, query['sort']) + ids = [i['public_id'] for i in qs.values('public_id')] + response['data']['positions'] = utils.get_positions(ids, data['positions']) + else: + response['data']['items'] = qs.count() + return render_to_json_response(response) +actions.register(findSequences) + +def getSequence(request): + ''' + param data { + id + mode + position + } + + return { + 'status': {'code': int, 'text': string} + 'data': { + id + mode + in + out + } + } + ''' + data = json.loads(request.POST['data']) + response = json_response() + qs = models.Sequence.objects.filter( + item__itemId=data['id'], + mode=data['mode'], + start__lte=data['position'], + end__gt=data['position'] + ).order_by('start', 'end') + for sequence in qs: + response['data'] = sequence.json() + break + return render_to_json_response(response) +actions.register(getSequence) diff --git a/pandora/settings.py b/pandora/settings.py index 1223b7fd..dfa35d19 100644 --- a/pandora/settings.py +++ b/pandora/settings.py @@ -109,6 +109,7 @@ INSTALLED_APPS = ( 'annotation', 'clip', + 'sequence', 'archive', 'event', 'changelog', diff --git a/static/js/pandora/sequencesDialog.js b/static/js/pandora/sequencesDialog.js index 0639606d..2e398ed3 100644 --- a/static/js/pandora/sequencesDialog.js +++ b/static/js/pandora/sequencesDialog.js @@ -25,7 +25,7 @@ pandora.ui.sequencesDialog = function(id, position) { pandora.api.getSequence({id: id, mode: mode, position: position}, function(result) { // result.data: {hash, in, out} var fixedRatio = 16/9, - hash = result.data.hash; + hash = result.data.hash, $sidebar = Ox.Element(), // add video player $list = Ox.IconList({ fixedRatio: fixedRatio, @@ -45,7 +45,10 @@ pandora.ui.sequencesDialog = function(id, position) { items: function(data, callback) { pandora.api.findSequences(Ox.extend(data, { query: { - conditions: [{key: mode, value: hash, operator: '=='}], + conditions: [ + {key: 'mode', value: mode, operator: '=='}, + {key: 'hash', value: hash, operator: '=='} + ], operator: '&' } }), callback); @@ -68,13 +71,14 @@ pandora.ui.sequencesDialog = function(id, position) { // ... } }); - $splitPanel.replaceElements([$sidebar, $list]); + $splitPanel.replaceElement(0, $sidebar); + $splitPanel.replaceElement(1, $list); }); return $splitPanel; }, tabs: [ - {id: 'shapes', title: 'Similar Shapes'}, - {id: 'colors', title: 'Similar Colors'} + {id: 'shape', title: 'Similar Shapes'}, + {id: 'color', title: 'Similar Colors'} ] }), @@ -85,7 +89,7 @@ pandora.ui.sequencesDialog = function(id, position) { title: 'Close' }).bindEvent({ click: function() { - $dialog.close; + $dialog.close(); } }) ], @@ -102,4 +106,4 @@ pandora.ui.sequencesDialog = function(id, position) { return $dialog; -}; \ No newline at end of file +};