Changeset - a30a73c12554
[Not reviewed]
default
0 5 0
Drew Perttula - 8 years ago 2017-04-20 07:18:00
drewp@bigasterisk.com
more code and testing in solve & sim
Ignore-this: cb72fb96cf4a46fa86ccf641c4703fdb
5 files changed with 157 insertions and 43 deletions:
0 comments (0 inline, 0 general)
bin/paintserver
Show inline comments
 
@@ -15,25 +15,26 @@ from light9.rdfdb import clientsession
 
import light9.paint.solve
 
from lib.cycloneerr import PrettyErrorHandler
 

	
 

	
 
class Solve(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def post(self):
 
        painting = json.loads(self.request.body)
 
        reload(light9.paint.solve)
 
        solver = light9.paint.solve.Solver(self.settings.graph)
 
        solver.loadSamples()
 
        with self.settings.stats.solve.time():
 
            out = solver.solve(painting)
 
        self.write(json.dumps(out))
 
            layers = solver.simulationLayers(out)
 
        self.write(json.dumps({'layers': layers, 'out': out}))
 

	
 
class App(object):
 
    def __init__(self, show, session):
 
        self.show = show
 
        self.session = session
 

	
 
        self.graph = SyncedGraph(networking.rdfdb.url, "paintServer")
 
        self.graph.initiallySynced.addCallback(self.launch)
 

	
 
        self.stats = scales.collection('/',
 
                                       scales.PmfStat('solve'),
 
                                       )
light9/paint/solve.py
Show inline comments
 
from __future__ import division
 
from light9.namespaces import RDF, L9
 
from PIL import Image
 
import decimal
 
import numpy
 
import scipy.misc
 
import scipy.misc, scipy.ndimage
 
import cairo
 

	
 
# numpy images in this file are (x, y, c) layout.
 

	
 
def numpyFromCairo(surface):
 
    w, h = surface.get_width(), surface.get_height()
 
    a = numpy.frombuffer(surface.get_data(), numpy.uint8)
 
    a.shape = h, w, 4
 
    a = a.transpose((1, 0, 2))
 
    return a[:w,:h,:3]
 

	
 
def numpyFromPil(img):
 
    return scipy.misc.fromimage(img, mode='RGB').transpose((1, 0, 2))
 

	
 
def saveNumpy(path, img):
 
    scipy.misc.imsave(path, img.transpose((1, 0, 2)))
 

	
 
def parseHex(h):
 
    if h[0] != '#': raise ValueError(h)
 
    return [int(h[i:i+2], 16) for i in 1, 3, 5]
 

	
 
def scaledHex(h, scale):
 
    rgb = parseHex(h)
 
    rgb8 = (rgb * scale).astype(numpy.uint8)
 
    return '#%02x%02x%02x' % tuple(rgb8)
 
    
 
def colorRatio(col1, col2):
 
    rgb1 = parseHex(col1)
 
    rgb2 = parseHex(col2)
 
    return tuple([round(a / b, 3) for a, b in zip(rgb1, rgb2)])
 

	
 
def brightest(img):
 
    return numpy.amax(img, axis=(0, 1))
 

	
 
def getVal(graph, subj):
 
    lit = graph.value(subj, L9['value']) or graph.value(subj, L9['scaledValue'])
 
    ret = lit.toPython()
 
    if isinstance(ret, decimal.Decimal):
 
        ret = float(ret)
 
    return ret
 

	
 
class Solver(object):
 
    def __init__(self, graph):
 
        self.graph = graph
 
        self.samples = {} # uri: Image array
 
        self.blurredSamples = {}
 
        self.sampleSettings = {} # (uri, path): { dev: { attr: val } }
 
        
 
    def loadSamples(self):
 
        """learn what lights do from images"""
 

	
 
        with self.graph.currentState() as g:
 
            for samp in g.subjects(RDF.type, L9['LightSample']):
 
                path = 'show/dance2017/cam/test/%s' % g.value(samp, L9['path'])
 
                img = Image.open(path)
 
                img.thumbnail((100, 100))
 
                self.samples[samp] = scipy.misc.fromimage(img, mode='RGB').transpose((1, 0, 2))
 
                self.samples[samp] = numpyFromPil(img)
 
                self.blurredSamples[samp] = self._blur(self.samples[samp])
 

	
 
                for s in g.objects(samp, L9['setting']):
 
                    d = g.value(s, L9['device'])
 
                    da = g.value(s, L9['deviceAttr'])
 
                    v = getVal(g, s)
 
                    key = (samp, g.value(samp, L9['path']).toPython())
 
                    self.sampleSettings.setdefault(key, {}).setdefault(d, {})[da] = v
 

	
 
    def _blur(self, img):
 
        return scipy.ndimage.gaussian_filter(img, 10, 0, mode='nearest')
 
                
 
    def draw(self, painting, w, h):
 
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
 
        ctx = cairo.Context(surface)
 
        ctx.rectangle(0, 0, w, h)
 
        ctx.fill()
 
        
 
        ctx.set_line_cap(cairo.LINE_CAP_ROUND)
 
        ctx.set_line_width(20)
 
        for stroke in painting['strokes']:
 
            for pt in stroke['pts']:
 
                op = ctx.move_to if pt is stroke['pts'][0] else ctx.line_to
 
                op(pt[0] / 4.0, pt[1] / 4.0) # todo scale
 
                op(pt[0] / 4, pt[1] / 4) # todo scale
 

	
 
            r,g,b = [int(stroke['color'][i:i+2], 16) / 255.0
 
                     for i in 1, 3, 5]
 
            ctx.set_source_rgba(r, g, b, 1)
 
            r,g,b = parseHex(stroke['color'])
 
            ctx.set_source_rgb(r / 255, g / 255, b / 255)
 
            ctx.stroke()
 
        # then blur?
 
        
 
        #surface.write_to_png('/tmp/surf.png')
 
        a = numpy.frombuffer(surface.get_data(), numpy.uint8)
 
        a.shape = (w, h, 4)
 
        return a[:w,:h,:3]
 
        return numpyFromCairo(surface)
 
        
 
    def solve(self, painting):
 
        """
 
        given strokes of colors on a photo of the stage, figure out the
 
        best light settings to match the image
 
        """
 
        pic0 = self.draw(painting, 100, 48)
 
        pic0 = self.draw(painting, 100, 48).astype(numpy.float)
 
        pic0Blur = self._blur(pic0)
 
        saveNumpy('/tmp/sample_paint_%s.png' % len(painting['strokes']),
 
                  pic0Blur)
 
        sampleDist = {}
 
        for sample, picSample in sorted(self.samples.items()):
 
            dist = numpy.sum(numpy.power(pic0.astype(numpy.float)
 
                                         - picSample, 2), axis=None)**.5
 
        for sample, picSample in sorted(self.blurredSamples.items()):
 
            #saveNumpy('/tmp/sample_%s.png' % sample.split('/')[-1],
 
            #          f(picSample))
 
            dist = numpy.sum(numpy.absolute(pic0Blur - picSample), axis=None)
 
            sampleDist[sample] = dist
 
        results = [(d, uri) for uri, d in sampleDist.items()]
 
        results.sort()
 
        import sys
 
        print >>sys.stderr, results, results[0][0] / results[1][0]
 

	
 
        sample = results[0][1]
 

	
 
        # this is wrong; some wrong-alignments ought to be dimmer than full
 
        brightest0 = brightest(pic0)
 
        brightestSample = brightest(self.samples[sample])
 
        
 
        if max(brightest0) < 1 / 255:
 
            return []
 

	
 
        scale = brightest0 / brightestSample
 
        
 
        out = []
 
        def getVal(obj):
 
            lit = g.value(obj, L9['value']) or g.value(obj, L9['scaledValue'])
 
            ret = lit.toPython()
 
            if isinstance(ret, decimal.Decimal):
 
                ret = float(ret)
 
            return ret
 
        with self.graph.currentState() as g:
 
            for obj in g.objects(sample, L9['setting']):
 
                out.append((g.value(obj, L9['device']),
 
                            g.value(obj, L9['deviceAttr']),
 
                            getVal(obj)))
 
                attr = g.value(obj, L9['deviceAttr'])
 
                val = getVal(g, obj)
 
                if attr == L9['color']:
 
                    val = scaledHex(val, scale)
 
                out.append((g.value(obj, L9['device']), attr, val))
 
                           
 
        return out
 

	
 

	
 
    def simulationLayers(self, settings):
 
        """
 
        how should a simulation preview approximate the light settings
 
        (device attribute values) by combining photos we have?
 
        """
 
    
 

	
 
        compiled = {} # dev: { attr: val }
 
        for row in settings:
 
            compiled.setdefault(row[0], {})[row[1]] = row[2]
 
        
 
        layers = []
 

	
 
        for (sample, path), s in self.sampleSettings.items():
 
            for d, dav in s.items():
 
                if d not in compiled:
 
                    continue
 
                requestedAttrs = compiled[d].copy()
 
                picAttrs = dav.copy()
 
                del requestedAttrs[L9['color']]
 
                del picAttrs[L9['color']]
 
                if requestedAttrs == picAttrs:
 
                    requestedColor = compiled[d][L9['color']]
 
                    picColor = dav[L9['color']]
 
                    layers.append({'path': path,
 
                                   'color': colorRatio(requestedColor,
 
                                                       picColor)})
 
        
 
        return layers
light9/paint/solve_test.py
Show inline comments
 
import unittest
 
import solve
 
from light9.namespaces import RDF, L9, DEV
 
from light9.rdfdb.localsyncedgraph import LocalSyncedGraph
 

	
 
class TestSolve(unittest.TestCase):
 
    def testBlack(self):
 
    def setUp(self):
 
        graph = LocalSyncedGraph(files=['show/dance2017/cam/test/bg.n3'])
 
        s = solve.Solver(graph)
 
        s.loadSamples()
 
        devAttrs = s.solve({'strokes': []})
 
        self.solver = solve.Solver(graph)
 
        self.solver.loadSamples()
 

	
 
    def testBlack(self):
 
        devAttrs = self.solver.solve({'strokes': []})
 
        self.assertEqual([], devAttrs)
 

	
 
    def testSingleLightCloseMatch(self):
 
        graph = LocalSyncedGraph(files=['show/dance2017/cam/test/bg.n3'])
 
        s = solve.Solver(graph)
 
        s.loadSamples()
 
        devAttrs = s.solve({'strokes': [{'pts': [[224, 141],
 
        devAttrs = self.solver.solve({'strokes': [{'pts': [[224, 141],
 
                                                 [223, 159]],
 
                                         'color': '#ffffff'}]})
 
        self.assertEqual(sorted([
 
        self.assertItemsEqual([
 
            (DEV['aura1'], L9['color'], u"#ffffff"),
 
            (DEV['aura1'], L9['rx'], 0.5 ),
 
            (DEV['aura1'], L9['ry'], 0.573),
 
        ]), sorted(devAttrs))
 
        ], devAttrs)
 
        
 
        
 
class TestSimulationLayers(unittest.TestCase):
 
    def setUp(self):
 
        graph = LocalSyncedGraph(files=['show/dance2017/cam/test/bg.n3'])
 
        self.solver = solve.Solver(graph)
 
        self.solver.loadSamples()
 
        
 
    def testBlack(self):
 
        self.assertEqual([], self.solver.simulationLayers(settings=[]))
 

	
 
    def testPerfect1Match(self):
 
        layers = self.solver.simulationLayers(settings=[
 
            (DEV['aura1'], L9['color'], u"#ffffff"),
 
            (DEV['aura1'], L9['rx'], 0.5 ),
 
            (DEV['aura1'], L9['ry'], 0.573)])
 
        self.assertEqual([{'path': 'bg2-d.jpg', 'color': (1, 1, 1)}], layers)
 

	
 
    def testPerfect1MatchTinted(self):
 
        layers = self.solver.simulationLayers(settings=[
 
            (DEV['aura1'], L9['color'], u"#304050"),
 
            (DEV['aura1'], L9['rx'], 0.5 ),
 
            (DEV['aura1'], L9['ry'], 0.573)])
 
        self.assertEqual([{'path': 'bg2-d.jpg', 'color': (.188, .251, .314)}], layers)
 
        
 
    def testPerfect2Matches(self):
 
        layers = self.solver.simulationLayers(settings=[
 
            (DEV['aura1'], L9['color'], u"#ffffff"),
 
            (DEV['aura1'], L9['rx'], 0.5 ),
 
            (DEV['aura1'], L9['ry'], 0.573),
 
            (DEV['aura2'], L9['color'], u"#ffffff"),
 
            (DEV['aura2'], L9['rx'], 0.7 ),
 
            (DEV['aura2'], L9['ry'], 0.573),
 
        ])
 
        self.assertItemsEqual([
 
            {'path': 'bg2-d.jpg', 'color': (1, 1, 1)},
 
            {'path': 'bg2-f.jpg', 'color': (1, 1, 1)},
 
                      ], layers)
light9/web/timeline/timeline-elements.html
Show inline comments
 
@@ -219,30 +219,30 @@ background: rgba(126, 52, 245, 0.0784313
 
     This element has the right Y coords.
 
     We compute X coords from the zoom setting.
 
     diagram-layer draws the note body. -->
 
<dom-module id="light9-timeline-note">
 
  <template>
 
    <style>
 
     :host {
 
         display: block;
 
         background: green;
 
         /* outline: 2px solid red; */
 
     }
 
    </style>
 
    <light9-timeline-note-inline-attrs rect="{{inlineRect}}"
 
    <xlight9-timeline-note-inline-attrs rect="{{inlineRect}}"
 
                                       graph="{{graph}}"
 
                                       song="{{song}}"
 
                                       uri="{{uri}}"
 
    >
 
    </light9-timeline-note-inline-attrs>
 
    </xlight9-timeline-note-inline-attrs>
 
  </template>
 
</dom-module>
 

	
 
<!-- All the adjusters you can edit or select. Tells a light9-adjusters-canvas how to draw them. Probabaly doesn't need to be an element.
 
     This element manages their layout and suppresion.
 
     Owns the selection.
 
     Maybe includes selecting things that don't even have adjusters.
 
     Maybe manages the layout of other labels and text too, to avoid overlaps.
 
   -->
 
<dom-module id="light9-timeline-adjusters">
 
  <template>
 
    <style>
show/dance2017/cam/test/bg.n3
Show inline comments
 
@@ -24,17 +24,18 @@
 
[ :device dev:aura1; :deviceAttr :ry;     :value 0.573 ] .
 

	
 
:sample3 a :LightSample; :path "bg2-d.jpg"; :setting 
 
[ :device dev:aura1; :deviceAttr :color;  :scaledValue "#ffffff" ],
 
[ :device dev:aura1; :deviceAttr :rx;     :value 0.5 ],
 
[ :device dev:aura1; :deviceAttr :ry;     :value 0.573 ] .
 

	
 
:sample4 a :LightSample; :path "bg2-e.jpg"; :setting 
 
[ :device dev:aura1; :deviceAttr :color;  :scaledValue "#ffffff" ],
 
[ :device dev:aura1; :deviceAttr :rx;     :value 0.6 ],
 
[ :device dev:aura1; :deviceAttr :ry;     :value 0.573 ] .
 

	
 
# note: different device
 
:sample5 a :LightSample; :path "bg2-f.jpg"; :setting 
 
[ :device dev:aura1; :deviceAttr :color;  :scaledValue "#ffffff" ],
 
[ :device dev:aura1; :deviceAttr :rx;     :value 0.7 ],
 
[ :device dev:aura1; :deviceAttr :ry;     :value 0.573 ] .
 
[ :device dev:aura2; :deviceAttr :color;  :scaledValue "#ffffff" ],
 
[ :device dev:aura2; :deviceAttr :rx;     :value 0.7 ],
 
[ :device dev:aura2; :deviceAttr :ry;     :value 0.573 ] .
 

	
0 comments (0 inline, 0 general)