# HG changeset patch
# User drewp@bigasterisk.com
# Date 2017-06-10 02:02:33
# Node ID 5d2dcae1a7c6c1e0069fab7e62bd7a30dbdabb6f
# Parent 9fe3052f8ced88ea7697ec852c5d05457ea0cb2b
paint can now do best matches on multiple lights at once
Ignore-this: 3b8a333264edc9532f8c4ff48d92ac17
diff --git a/bin/paintserver b/bin/paintserver
--- a/bin/paintserver
+++ b/bin/paintserver
@@ -17,12 +17,14 @@ from lib.cycloneerr import PrettyErrorHa
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 @@ class Solve(PrettyErrorHandler, cyclone.
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 @@ class App(object):
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,
diff --git a/light9/effect/settings.py b/light9/effect/settings.py
--- a/light9/effect/settings.py
+++ b/light9/effect/settings.py
@@ -16,8 +16,11 @@ 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'])
@@ -83,6 +86,27 @@ class _Settings(object):
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 @@ class _Settings(object):
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 @@ class _Settings(object):
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 @@ class _Settings(object):
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
diff --git a/light9/effect/settings_test.py b/light9/effect/settings_test.py
--- a/light9/effect/settings_test.py
+++ b/light9/effect/settings_test.py
@@ -4,7 +4,8 @@ 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',
@@ -112,3 +113,37 @@ class TestDeviceSettings(unittest.TestCa
])
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')])),
+ ]))
diff --git a/light9/paint/solve.py b/light9/paint/solve.py
--- a/light9/paint/solve.py
+++ b/light9/paint/solve.py
@@ -67,18 +67,22 @@ class ImageDistAbs(object):
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 @@ class Solver(object):
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 @@ class Solver(object):
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 @@ class Solver(object):
#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 @@ class Solver(object):
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
diff --git a/light9/web/paint/paint-elements.coffee b/light9/web/paint/paint-elements.coffee
--- a/light9/web/paint/paint-elements.coffee
+++ b/light9/web/paint/paint-elements.coffee
@@ -1,3 +1,5 @@
+log = console.log
+
class Painting
constructor: (@svg) ->
@strokes = []
@@ -176,16 +178,54 @@ 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)
diff --git a/light9/web/paint/paint-elements.html b/light9/web/paint/paint-elements.html
--- a/light9/web/paint/paint-elements.html
+++ b/light9/web/paint/paint-elements.html
@@ -6,6 +6,7 @@
+