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
 
@@ -21,13 +21,14 @@ class Solve(PrettyErrorHandler, cyclone.
 
        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
 

	
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
 
@@ -225,18 +225,18 @@ background: rgba(126, 52, 245, 0.0784313
 
     :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.
show/dance2017/cam/test/bg.n3
Show inline comments
 
@@ -30,11 +30,12 @@
 

	
 
: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)