# HG changeset patch # User Drew Perttula # Date 2017-04-20 07:18:00 # Node ID a30a73c1255474c059a48cb65acea32b60767c96 # Parent 23cabb70699bdccc372f4bae09f8669b2cde0895 more code and testing in solve & sim Ignore-this: cb72fb96cf4a46fa86ccf641c4703fdb diff --git a/bin/paintserver b/bin/paintserver --- a/bin/paintserver +++ b/bin/paintserver @@ -24,7 +24,8 @@ class Solve(PrettyErrorHandler, cyclone. 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): diff --git a/light9/paint/solve.py b/light9/paint/solve.py --- a/light9/paint/solve.py +++ b/light9/paint/solve.py @@ -3,13 +3,54 @@ 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""" @@ -19,8 +60,19 @@ class Solver(object): 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 @@ class Solver(object): 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 diff --git a/light9/paint/solve_test.py b/light9/paint/solve_test.py --- a/light9/paint/solve_test.py +++ b/light9/paint/solve_test.py @@ -4,24 +4,59 @@ from light9.namespaces import RDF, L9, D 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) diff --git a/light9/web/timeline/timeline-elements.html b/light9/web/timeline/timeline-elements.html --- a/light9/web/timeline/timeline-elements.html +++ b/light9/web/timeline/timeline-elements.html @@ -228,12 +228,12 @@ background: rgba(126, 52, 245, 0.0784313 /* outline: 2px solid red; */ } - - + diff --git a/show/dance2017/cam/test/bg.n3 b/show/dance2017/cam/test/bg.n3 --- a/show/dance2017/cam/test/bg.n3 +++ b/show/dance2017/cam/test/bg.n3 @@ -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 ] .