changeset 1524:a30a73c12554

more code and testing in solve & sim Ignore-this: cb72fb96cf4a46fa86ccf641c4703fdb
author Drew Perttula <drewp@bigasterisk.com>
date Thu, 20 Apr 2017 07:18:00 +0000
parents 23cabb70699b
children 1849713b0d73
files bin/paintserver light9/paint/solve.py light9/paint/solve_test.py light9/web/timeline/timeline-elements.html show/dance2017/cam/test/bg.n3
diffstat 5 files changed, 157 insertions(+), 43 deletions(-) [+]
line wrap: on
line diff
--- a/bin/paintserver	Wed Apr 19 08:26:08 2017 +0000
+++ b/bin/paintserver	Thu Apr 20 07:18:00 2017 +0000
@@ -24,7 +24,8 @@
         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):
--- a/light9/paint/solve.py	Wed Apr 19 08:26:08 2017 +0000
+++ b/light9/paint/solve.py	Thu Apr 20 07:18:00 2017 +0000
@@ -3,13 +3,54 @@
 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"""
@@ -19,8 +60,19 @@
                 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)
@@ -32,55 +84,80 @@
         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
--- a/light9/paint/solve_test.py	Wed Apr 19 08:26:08 2017 +0000
+++ b/light9/paint/solve_test.py	Thu Apr 20 07:18:00 2017 +0000
@@ -4,24 +4,59 @@
 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)
--- a/light9/web/timeline/timeline-elements.html	Wed Apr 19 08:26:08 2017 +0000
+++ b/light9/web/timeline/timeline-elements.html	Thu Apr 20 07:18:00 2017 +0000
@@ -228,12 +228,12 @@
          /* 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>
 
--- a/show/dance2017/cam/test/bg.n3	Wed Apr 19 08:26:08 2017 +0000
+++ b/show/dance2017/cam/test/bg.n3	Thu Apr 20 07:18:00 2017 +0000
@@ -33,8 +33,9 @@
 [ :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 ] .