changeset 1626:5d2dcae1a7c6

paint can now do best matches on multiple lights at once Ignore-this: 3b8a333264edc9532f8c4ff48d92ac17
author drewp@bigasterisk.com
date Sat, 10 Jun 2017 02:02:33 +0000
parents 9fe3052f8ced
children 6e06a4955ef2
files bin/paintserver light9/effect/settings.py light9/effect/settings_test.py light9/paint/solve.py light9/web/paint/paint-elements.coffee light9/web/paint/paint-elements.html light9/web/paint/paint-report-elements.html light9/web/rdfdb-synced-graph.html
diffstat 8 files changed, 206 insertions(+), 20 deletions(-) [+]
line wrap: on
line diff
--- a/bin/paintserver	Sat Jun 10 02:00:29 2017 +0000
+++ b/bin/paintserver	Sat Jun 10 02:02:33 2017 +0000
@@ -17,12 +17,14 @@
 from light9.namespaces import RDF, L9, DEV
 
 
+
+
 class Solve(PrettyErrorHandler, cyclone.web.RequestHandler):
     def post(self):
         painting = json.loads(self.request.body)
         with self.settings.stats.solve.time():
             img = self.settings.solver.draw(painting)
-            sample, sampleDist = self.settings.solver.bestMatch(img)
+            sample, sampleDist = self.settings.solver.bestMatch(img, device=DEV['aura2'])
             with self.settings.graph.currentState() as g:
                 bestPath = g.value(sample, L9['imagePath']).replace(L9[''], '')
             #out = solver.solve(painting)
@@ -39,6 +41,17 @@
         self.settings.solver = light9.paint.solve.Solver(self.settings.graph)
         self.settings.solver.loadSamples()
 
+class BestMatches(PrettyErrorHandler, cyclone.web.RequestHandler):
+    def post(self):
+        body = json.loads(self.request.body)
+        painting = body['painting']
+        devs = [URIRef(d) for d in body['devices']]
+        with self.settings.stats.solve.time():
+            img = self.settings.solver.draw(painting)
+            outSettings = self.settings.solver.bestMatches(img, devs)
+            self.write(json.dumps({
+                'settings': outSettings.asList()
+                }))
         
 class App(object):
     def __init__(self, show, session):
@@ -46,19 +59,29 @@
         self.session = session
 
         self.graph = SyncedGraph(networking.rdfdb.url, "paintServer")
-        self.graph.initiallySynced.addCallback(self.launch)
+        self.graph.initiallySynced.addCallback(self.launch).addErrback(log.error)
         
         self.stats = scales.collection('/', scales.PmfStat('solve'),
                                        )
        
     def launch(self, *args):
 
-        self.solver = light9.paint.solve.Solver(self.graph, sessions=[L9['show/dance2017/capture/moving1/cap961804']])
+        self.solver = light9.paint.solve.Solver(self.graph, sessions=[
+            L9['show/dance2017/capture/aura1/cap1876596'],
+            L9['show/dance2017/capture/aura2/cap1876792'],
+            L9['show/dance2017/capture/aura3/cap1877057'],
+            L9['show/dance2017/capture/aura4/cap1877241'],
+            L9['show/dance2017/capture/aura5/cap1877406'],
+            L9['show/dance2017/capture/q1/cap1874255'],
+            L9['show/dance2017/capture/q2/cap1873665'],
+            L9['show/dance2017/capture/q3/cap1876223'],
+        ])
         self.solver.loadSamples()
         
         self.cycloneApp = cyclone.web.Application(handlers=[
             (r'/stats', StatsForCyclone),
             (r'/solve', Solve),
+            (r'/bestMatches', BestMatches),
         ],
                                                   debug=True,
                                                   graph=self.graph,
--- a/light9/effect/settings.py	Sat Jun 10 02:00:29 2017 +0000
+++ b/light9/effect/settings.py	Sat Jun 10 02:02:33 2017 +0000
@@ -16,8 +16,11 @@
     if h[0] != '#': raise ValueError(h)
     return [int(h[i:i+2], 16) for i in 1, 3, 5]
 
+def parseHexNorm(h):
+    return [x / 255 for x in parseHex(h)]
+    
 def toHex(rgbFloat):
-    return '#%02x%02x%02x' % tuple(int(v * 255) for v in rgbFloat)
+    return '#%02x%02x%02x' % tuple(max(0, min(255, int(v * 255))) for v in rgbFloat)
 
 def getVal(graph, subj):
     lit = graph.value(subj, L9['value']) or graph.value(subj, L9['scaledValue'])
@@ -83,6 +86,27 @@
                 out._compiled.setdefault(row[0], {})[row[1]] = row[2]
         out._delZeros()
         return out
+
+    @classmethod
+    def fromBlend(cls, graph, others):
+        """others is a list of (weight, Settings) pairs"""
+        out = cls(graph, [])
+        for weight, s in others:
+            if not isinstance(s, cls):
+                raise TypeError(s)
+            for row in s.asList(): # could work straight from s._compiled
+                if row[0] is None:
+                    raise TypeError('bad row %r' % (row,))
+                dd = out._compiled.setdefault(row[0], {})
+
+                if isinstance(row[2], basestring):
+                    prev = parseHexNorm(dd.get(row[1], '#000000'))
+                    newVal = toHex(prev + weight * numpy.array(parseHexNorm(row[2])))
+                else:
+                    newVal = dd.get(row[1], 0) + weight * row[2]
+                dd[row[1]] = newVal
+        out._delZeros()
+        return out
         
     def _zeroForAttr(self, attr):
         if attr == L9['color']:
@@ -117,7 +141,7 @@
         words = []
         def accum():
             for dev, av in self._compiled.iteritems():
-                for attr, val in av.iteritems():
+                for attr, val in sorted(av.iteritems()):
                     words.append('%s.%s=%s' % (dev.rsplit('/')[-1],
                                                attr.rsplit('/')[-1],
                                                val))
@@ -126,7 +150,7 @@
                         return
         accum()
         return '<%s %s>' % (self.__class__.__name__, ' '.join(words))
-        
+
     def getValue(self, dev, attr):
         return self._compiled.get(dev, {}).get(attr, self._zeroForAttr(attr))
 
@@ -150,7 +174,7 @@
         for dev, attr in self._vectorKeys(deviceAttrFilter):
             v = self.getValue(dev, attr)
             if attr == L9['color']:
-                out.extend([x / 255 for x in parseHex(v)])
+                out.extend(parseHexNorm(v))
             else:
                 out.append(v)
         return out
--- a/light9/effect/settings_test.py	Sat Jun 10 02:00:29 2017 +0000
+++ b/light9/effect/settings_test.py	Sat Jun 10 02:02:33 2017 +0000
@@ -4,7 +4,8 @@
 from light9.rdfdb.localsyncedgraph import LocalSyncedGraph
 from light9.namespaces import RDF, L9, DEV
 from light9.effect.settings import DeviceSettings
-
+             
+        
 class TestDeviceSettings(unittest.TestCase):
     def setUp(self):
         self.graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3',
@@ -112,3 +113,37 @@
         ])
         self.assertEqual(0.36055512754639896, s1.distanceTo(s2))
         
+
+L1 = L9['light1']
+ZOOM = L9['zoom']
+class TestFromBlend(unittest.TestCase):
+    def setUp(self):
+        self.graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3',
+                                             'test/cam/bg.n3'])
+    def testSingle(self):
+        self.assertEqual(
+            DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]),
+            DeviceSettings.fromBlend(self.graph, [
+                (1, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))]))
+
+    def testScale(self):
+        self.assertEqual(
+            DeviceSettings(self.graph, [(L1, ZOOM, 0.1)]),
+            DeviceSettings.fromBlend(self.graph, [
+                (.2, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))]))
+
+    def testMixFloats(self):
+        self.assertEqual(
+            DeviceSettings(self.graph, [(L1, ZOOM, 0.4)]),
+            DeviceSettings.fromBlend(self.graph, [
+                (.2, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)])),
+                (.3, DeviceSettings(self.graph, [(L1, ZOOM, 1.0)])),
+            ]))
+
+    def testMixColors(self):
+        self.assertEqual(
+            DeviceSettings(self.graph, [(L1, ZOOM, '#503000')]),
+            DeviceSettings.fromBlend(self.graph, [
+                (.25, DeviceSettings(self.graph, [(L1, ZOOM, '#800000')])),
+                (.5, DeviceSettings(self.graph, [(L1, ZOOM, '#606000')])),
+            ]))
--- a/light9/paint/solve.py	Sat Jun 10 02:00:29 2017 +0000
+++ b/light9/paint/solve.py	Sat Jun 10 02:02:33 2017 +0000
@@ -67,18 +67,22 @@
 
         
 class Solver(object):
-    def __init__(self, graph, sessions=None, imgSize=(100, 75)):
+    def __init__(self, graph, sessions=None, imgSize=(100, 53)):
         self.graph = graph
         self.sessions = sessions # URIs of capture sessions to load
         self.imgSize = imgSize
         self.samples = {} # uri: Image array (float 0-255)
         self.fromPath = {} # imagePath: image array
+        self.path = {} # sample: path
         self.blurredSamples = {}
-        self.sampleSettings = {} # (uri, path): DeviceSettings
+        self.sampleSettings = {} # sample: DeviceSettings
+        self.samplesForDevice = {} # dev : [(sample, img)]
         
     def loadSamples(self):
         """learn what lights do from images"""
 
+        log.info('loading...')
+
         with self.graph.currentState() as g:
             for sess in self.sessions:
                 for cap in g.objects(sess, L9['capture']):
@@ -87,11 +91,19 @@
 
     def _loadSample(self, g, samp):
         pathUri = g.value(samp, L9['imagePath'])
-        self.samples[samp] = self.fromPath[pathUri] = loadNumpy(pathUri.replace(L9[''], '')).astype(float)
-        self.blurredSamples[samp] = self._blur(self.samples[samp])
+        img = loadNumpy(pathUri.replace(L9[''], '')).astype(float)
+        settings = DeviceSettings.fromResource(self.graph, samp)
+        
+        self.samples[samp] = img
+        self.fromPath[pathUri] = img
+        self.blurredSamples[samp] = self._blur(img)
 
-        key = (samp, pathUri)
-        self.sampleSettings[key] = DeviceSettings.fromResource(self.graph, samp)
+        self.path[samp] = pathUri
+        assert samp not in self.sampleSettings
+        self.sampleSettings[samp] = settings
+        devs = settings.devices()
+        if len(devs) == 1:
+            self.samplesForDevice.setdefault(devs[0], []).append((samp, img))
         
     def _blur(self, img):
         return scipy.ndimage.gaussian_filter(img, 10, 0, mode='nearest')
@@ -106,7 +118,7 @@
         ctx.fill()
         
         ctx.set_line_cap(cairo.LINE_CAP_ROUND)
-        ctx.set_line_width(w / 5) # ?
+        ctx.set_line_width(w / 15) # ?
         for stroke in painting['strokes']:
             for pt in stroke['pts']:
                 op = ctx.move_to if pt is stroke['pts'][0] else ctx.line_to
@@ -119,22 +131,67 @@
         #surface.write_to_png('/tmp/surf.png')
         return numpyFromCairo(surface)
 
-    def bestMatch(self, img):
+    def bestMatch(self, img, device=None):
         """the one sample that best matches this image"""
         #img = self._blur(img)
         results = []
         dist = ImageDist(img)
-        for uri, img2 in sorted(self.samples.items()):
+        if device is None:
+            items = self.samples.items()
+        else:
+            items = self.samplesForDevice[device]
+        for uri, img2 in sorted(items):
             if img.shape != img2.shape:
+                log.warn("mismatch %s %s", img.shape, img2.shape)
                 continue
             results.append((dist.distanceTo(img2), uri, img2))
         results.sort()
         topDist, topUri, topImg = results[0]
+        print 'tops2'
+        for row in results[:4]:
+            print '%.5f' % row[0], row[1][-20:], self.sampleSettings[row[1]]
+        
        
         #saveNumpy('/tmp/best_in.png', img)
         #saveNumpy('/tmp/best_out.png', topImg)
         #saveNumpy('/tmp/mult.png', topImg / 255 * img)
         return topUri, topDist
+
+    def bestMatches(self, img, devices=None):
+        """settings for the given devices that point them each
+        at the input image"""
+        dist = ImageDist(img)
+        devSettings = []
+        for dev in devices:
+            results = []
+            for samp, img2 in self.samplesForDevice[dev]:
+                results.append((dist.distanceTo(img2), samp))
+            results.sort()
+            
+            s = self.blendResults([(d, self.sampleSettings[samp])
+                                   for d, samp in results[:8]])
+            devSettings.append(s)
+        return DeviceSettings.fromList(self.graph, devSettings)
+
+    def blendResults(self, results):
+        """list of (dist, settings)"""
+        
+        dists = [d for d, sets in results]
+        hi = max(dists)
+        lo = min(dists)
+        n = len(results)
+        remappedDists = [1 - (d - lo) / (hi - lo) * n / (n + 1)
+                         for d in dists]
+        total = sum(remappedDists)
+        
+        #print 'blend'
+        #for o,n in zip(dists, remappedDists):
+        #    print o,n, n / total
+        blend = DeviceSettings.fromBlend(
+            self.graph,
+            [(d / total, sets) for
+             d, (_, sets) in zip(remappedDists, results)])
+        return blend
         
     def solve(self, painting):
         """
@@ -226,7 +283,8 @@
         for dev, devSettings in settings.byDevice():
             requestedColor = devSettings.getValue(dev, L9['color'])
             candidatePics = [] # (distance, path, picColor)
-            for (sample, path), s in self.sampleSettings.items():
+            for sample, s in self.sampleSettings.items():
+                path = self.path[sample]
                 otherDevSettings = s.ofDevice(dev)
                 if not otherDevSettings:
                     continue
--- a/light9/web/paint/paint-elements.coffee	Sat Jun 10 02:00:29 2017 +0000
+++ b/light9/web/paint/paint-elements.coffee	Sat Jun 10 02:02:33 2017 +0000
@@ -1,3 +1,5 @@
+log = console.log
+
 class Painting
   constructor: (@svg) ->
     @strokes = []
@@ -176,16 +178,54 @@
   is: "light9-paint"
   properties: {
     painting: { type: Object }
+    client: { type: Object }
+    graph: { type: Object }
   }
 
   ready: () ->
     # couldn't make it work to bind to painting's notifyPath events
     @$.canvas.addEventListener('paintingChanged', @paintingChanged.bind(@))
     @$.solve.addEventListener('response', @onSolve.bind(@))
+
+    @clientSendThrottled = _.throttle(@client.send.bind(@client), 60)
+    @bestMatchPending = false
     
   paintingChanged: (ev) ->
+    U = (x) => @graph.Uri(x)
+
     @painting = ev.detail
     @$.solve.body = JSON.stringify(@painting.getDoc())
-    @$.solve.generateRequest()
+    #@$.solve.generateRequest()
+
+    @$.bestMatches.body = JSON.stringify({
+      painting: @painting.getDoc(),
+      devices: [
+        U('dev:aura1'), U('dev:aura2'), U('dev:aura3'), U('dev:aura4'), U('dev:aura5'),
+        U('dev:q1'), U('dev:q2'), U('dev:q3'),
+        ]})
+
+    send = =>
+      @$.bestMatches.generateRequest().completes.then (r) =>
+        @clientSendThrottled(r.response.settings)
+        if @bestMatchPending
+          @bestMatchPending = false
+          send()
+    
+    if @$.bestMatches.loading
+      @bestMatchPending = true
+    else
+      send()
 
   onSolve: (response) ->
+    U = (x) => @graph.Uri(x)
+
+    sample = @$.solve.lastResponse.bestMatch.uri
+    settingsList = []
+    for s in @graph.objects(sample, U(':setting'))
+      try
+        v = @graph.floatValue(s, U(':value'))
+      catch
+        v = @graph.stringValue(s, U(':scaledValue'))
+      row = [@graph.uriValue(s, U(':device')), @graph.uriValue(s, U(':deviceAttr')), v]
+      settingsList.push(row)
+    @client.send(settingsList)
--- a/light9/web/paint/paint-elements.html	Sat Jun 10 02:00:29 2017 +0000
+++ b/light9/web/paint/paint-elements.html	Sat Jun 10 02:02:33 2017 +0000
@@ -6,6 +6,7 @@
 <link rel="import" href="/lib/paper-radio-button/paper-radio-button.html">
 <link rel="import" href="paint-report-elements.html">
 <link rel="import" href="../rdfdb-synced-graph.html">
+<link rel="import" href="../light9-collector-client.html">
 
 
 <dom-module id="light9-paint-canvas">
@@ -98,7 +99,11 @@
     <light9-paint-canvas id="canvas" bg="bg2.jpg" painting="{{painting}}"></light9-paint-canvas>
 
     <iron-ajax id="solve" method="POST" url="../paintServer/solve" last-response="{{solve}}"></iron-ajax>
+
+    <iron-ajax id="bestMatches" method="POST" url="../paintServer/bestMatches"></iron-ajax>
     
+    <div>To collector: <light9-collector-client self="{{client}}"></light9-collector-client></div>
+
     <light9-simulation graph="{{graph}}" solution="{{solve}}" layers="{{layers}}"></light9-simulation>
   </template>
 </dom-module>
--- a/light9/web/paint/paint-report-elements.html	Sat Jun 10 02:00:29 2017 +0000
+++ b/light9/web/paint/paint-report-elements.html	Sat Jun 10 02:02:33 2017 +0000
@@ -26,7 +26,7 @@
         <div>Single pic best match:</div>
 
         <!-- drag this img to make an effect out of just it -->
-        <light9-capture-image name="mac2" path="{{solution.bestMatch.path}}"></light9-capture-image>
+        <light9-capture-image name="lighhtnamehere" path="{{solution.bestMatch.path}}"></light9-capture-image>
 
         <div>Error: {{solution.bestMatch.dist}}</div>
         
--- a/light9/web/rdfdb-synced-graph.html	Sat Jun 10 02:00:29 2017 +0000
+++ b/light9/web/rdfdb-synced-graph.html	Sat Jun 10 02:02:33 2017 +0000
@@ -25,6 +25,7 @@
        ready: function() {
            this.graph = new SyncedGraph('/rdfdb/syncedGraph', {
                '': 'http://light9.bigasterisk.com/',
+               'dev': 'http://light9.bigasterisk.com/device/',
                'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
                'rdfs': 'http://www.w3.org/2000/01/rdf-schema#',
                'xsd': 'http://www.w3.org/2001/XMLSchema#',