Changeset - 5d2dcae1a7c6
[Not reviewed]
default
0 8 0
drewp@bigasterisk.com - 8 years ago 2017-06-10 02:02:33
drewp@bigasterisk.com
paint can now do best matches on multiple lights at once
Ignore-this: 3b8a333264edc9532f8c4ff48d92ac17
8 files changed with 206 insertions and 20 deletions:
0 comments (0 inline, 0 general)
bin/paintserver
Show inline comments
 
@@ -14,18 +14,20 @@ from rdflib import URIRef
 
from light9.rdfdb import clientsession
 
import light9.paint.solve
 
from lib.cycloneerr import PrettyErrorHandler
 
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)
 
            #layers = solver.simulationLayers(out)
 
            
 
        self.write(json.dumps({
 
@@ -36,32 +38,53 @@ class Solve(PrettyErrorHandler, cyclone.
 

	
 
    def reloadSolver(self):
 
        reload(light9.paint.solve)
 
        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):
 
        self.show = show
 
        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,
 
                                                  solver=self.solver,
 
                                                  stats=self.stats)
 
        reactor.listenTCP(networking.paintServer.port, self.cycloneApp)
light9/effect/settings.py
Show inline comments
 
@@ -13,14 +13,17 @@ import logging
 
log = logging.getLogger('settings')
 

	
 
def parseHex(h):
 
    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'])
 
    ret = lit.toPython()
 
    if isinstance(ret, decimal.Decimal):
 
        ret = float(ret)
 
@@ -80,12 +83,33 @@ class _Settings(object):
 
            for row in s.asList(): # could work straight from s._compiled
 
                if row[0] is None:
 
                    raise TypeError('bad row %r' % (row,))
 
                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']:
 
            return '#000000'
 
        return 0.0
 

	
 
@@ -114,22 +138,22 @@ class _Settings(object):
 
        return bool(self._compiled)
 
        
 
    def __repr__(self):
 
        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))
 
                    if len(words) > 5:
 
                        words.append('...')
 
                        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))
 

	
 
    def _vectorKeys(self, deviceAttrFilter=None):
 
        """stable order of all the dev,attr pairs for this type of settings"""
 
        raise NotImplementedError
 
@@ -147,13 +171,13 @@ class _Settings(object):
 
        
 
    def toVector(self, deviceAttrFilter=None):
 
        out = []
 
        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
 

	
 
    def byDevice(self):
 
        for dev, av in self._compiled.iteritems():
light9/effect/settings_test.py
Show inline comments
 
import unittest
 
from rdflib import Literal
 
from light9.rdfdb.patch import Patch
 
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',
 
                                             'test/cam/bg.n3'])
 

	
 
    def testToVectorZero(self):
 
@@ -109,6 +110,40 @@ class TestDeviceSettings(unittest.TestCa
 
        s2 = DeviceSettings(self.graph, [
 
            (DEV['aura1'], L9['rx'], 0.3),
 
            (DEV['aura1'], L9['ry'], 0.3),
 
        ])
 
        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')])),
 
            ]))
light9/paint/solve.py
Show inline comments
 
@@ -64,37 +64,49 @@ class ImageDistAbs(object):
 

	
 
    def distanceTo(self, img2):
 
        return numpy.sum(numpy.absolute(self.a - img2), axis=None) / self.maxDist
 

	
 
        
 
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']):
 
                    self._loadSample(g, cap)
 
        log.info('loaded %s samples', len(self.samples))
 

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

	
 
    def draw(self, painting):
 
        return self._draw(painting, self.imgSize[0], self.imgSize[1])
 
@@ -103,41 +115,86 @@ class Solver(object):
 
        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(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
 
                op(pt[0] * w, pt[1] * h)
 

	
 
            r,g,b = parseHex(stroke['color'])
 
            ctx.set_source_rgb(r / 255, g / 255, b / 255)
 
            ctx.stroke()
 
        
 
        #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):
 
        """
 
        given strokes of colors on a photo of the stage, figure out the
 
        best light DeviceSettings to match the image
 
        """
 
@@ -223,13 +280,14 @@ class Solver(object):
 
        assert isinstance(settings, DeviceSettings)
 
        layers = []
 

	
 
        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
 
                dist = devSettings.distanceTo(otherDevSettings)
 
                log.info('  candidate pic %s %s dist=%s', sample, path, dist)
 
                candidatePics.append((dist, path, s.getValue(dev, L9['color'])))
light9/web/paint/paint-elements.coffee
Show inline comments
 
log = console.log
 

	
 
class Painting
 
  constructor: (@svg) ->
 
    @strokes = []
 

	
 
  setSize: (@size) ->
 

	
 
@@ -173,19 +175,57 @@ Polymer
 
    throw new Error("no value for #{s}")
 
    
 
Polymer
 
  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)
light9/web/paint/paint-elements.html
Show inline comments
 
@@ -3,12 +3,13 @@
 
<link rel="import" href="/lib/iron-resizable-behavior/iron-resizable-behavior.html">
 
<link rel="import" href="/lib/iron-ajax/iron-ajax.html">
 
<link rel="import" href="/lib/paper-radio-group/paper-radio-group.html">
 
<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">
 
  <template>
 
    <style>
 
     :host {
 
@@ -95,13 +96,17 @@
 
  <template>
 
    <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph>
 

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

	
 
<script src="/lib/N3.js-pull61/browser/n3-browser.js"></script>
 
<script src="/lib/shortcut/index.js"></script>
light9/web/paint/paint-report-elements.html
Show inline comments
 
@@ -23,13 +23,13 @@
 

	
 
    <div id="solutions">
 
      <div id="single-light">
 
        <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>
 
        
 
        <light9-device-settings graph="{{graph}}" subj="{{solution.bestMatch.uri}}"></light9-device-settings>
 
      </div>
 

	
light9/web/rdfdb-synced-graph.html
Show inline comments
 
@@ -22,12 +22,13 @@
 
           graph: {type: Object, notify: true},
 
           status: {type: String, notify: true}
 
       },
 
       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#',
 
           }, function(s) { this.status = s; }.bind(this));
 
           window.graph = this.graph;
 
       }
0 comments (0 inline, 0 general)