Changeset - 9bfd2303f011
[Not reviewed]
default
0 5 1
Drew Perttula - 8 years ago 2017-05-28 07:48:04
drewp@bigasterisk.com
fix bestMatch solve method
Ignore-this: 2e7f5af86e2e54b36dfb27a6b52e26a5
6 files changed with 47 insertions and 11 deletions:
0 comments (0 inline, 0 general)
light9/effect/settings.py
Show inline comments
 
from __future__ import division
 
"""
 
Data structure and convertors for a table of (device,attr,value)
 
rows. These might be effect attrs ('strength'), device attrs ('rx'),
 
or output attrs (dmx channel).
 
"""
 
import decimal
 
import numpy
 
from rdflib import URIRef, Literal
 
from light9.namespaces import RDF, L9, DEV
 
from light9.rdfdb.patch import Patch
 

	
 
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 toHex(rgbFloat):
 
    return '#%02x%02x%02x' % tuple(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):
 
@@ -87,25 +88,27 @@ class _Settings(object):
 
        itemed = tuple([(d, tuple([(a, v) for a, v in sorted(av.items())]))
 
                        for d, av in sorted(self._compiled.items())])
 
        return hash(itemed)
 

	
 
    def __eq__(self, other):
 
        if not issubclass(other.__class__, self.__class__):
 
            raise TypeError("can't compare %r to %r" % (self.__class__, other.__class__))
 
        return self._compiled == other._compiled
 

	
 
    def __ne__(self, other):
 
        return not self == other
 

	
 

	
 
    def __nonzero__(self):
 
        return bool(self._compiled)
 
        
 
    def __repr__(self):
 
        words = []
 
        def accum():
 
            for dev, av in self._compiled.iteritems():
 
                for attr, val in av.iteritems():
 
                    words.append('%s.%s=%s' % (dev.rsplit('/')[-1],
 
                                               attr.rsplit('/')[-1],
 
                                               val))
 
                    if len(words) > 5:
 
                        words.append('...')
 
                        return
 
        accum()
 
@@ -142,24 +145,25 @@ class _Settings(object):
 

	
 
    def byDevice(self):
 
        for dev, av in self._compiled.iteritems():
 
            yield dev, self.__class__._fromCompiled(self.graph, {dev: av})
 

	
 
    def ofDevice(self, dev):
 
        return self.__class__._fromCompiled(self.graph,
 
                                            {dev: self._compiled.get(dev, {})})
 
        
 
    def distanceTo(self, other):
 
        diff = numpy.array(self.toVector()) - other.toVector()
 
        d = numpy.linalg.norm(diff, ord=None)
 
        log.info('distanceTo %r - %r = %g', self, other, d)
 
        return d
 

	
 
    def statements(self, subj, ctx, settingRoot, settingsSubgraphCache):
 
        """
 
        settingRoot can be shared across images (or even wider if you want)
 
        """
 
        # ported from live.coffee
 
        add = []
 
        for i, (dev, attr, val) in enumerate(self.asList()):
 
            # hopefully a unique number for the setting so repeated settings converge
 
            settingHash = hash((dev, attr, val)) % 9999999
 
            setting = URIRef('%sset%s' % (settingRoot, settingHash))
light9/effect/settings_test.py
Show inline comments
 
@@ -21,24 +21,28 @@ class TestDeviceSettings(unittest.TestCa
 
        ])
 
        s2 = DeviceSettings(self.graph, [
 
            (L9['light1'], L9['attr2'], 0.3),
 
            (L9['light1'], L9['attr1'], 0.5),
 
        ])
 
        self.assertTrue(s1 == s2)
 
        self.assertFalse(s1 != s2)
 

	
 
    def testMissingFieldsEqZero(self):
 
        self.assertEqual(
 
            DeviceSettings(self.graph, [(L9['aura1'], L9['rx'], 0),]),
 
            DeviceSettings(self.graph, []))
 

	
 
    def testFalseIfZero(self):
 
        self.assertTrue(DeviceSettings(self.graph, [(L9['aura1'], L9['rx'], 0.1)]))
 
        self.assertFalse(DeviceSettings(self.graph, []))
 
        
 
    def testFromResource(self):
 
        ctx = L9['']
 
        self.graph.patch(Patch(addQuads=[
 
            (L9['foo'], L9['setting'], L9['foo_set0'], ctx),
 
            (L9['foo_set0'], L9['device'], L9['light1'], ctx),
 
            (L9['foo_set0'], L9['deviceAttr'], L9['brightness'], ctx),
 
            (L9['foo_set0'], L9['value'], Literal(0.1), ctx),
 
            (L9['foo'], L9['setting'], L9['foo_set1'], ctx),
 
            (L9['foo_set1'], L9['device'], L9['light1'], ctx),
 
            (L9['foo_set1'], L9['deviceAttr'], L9['speed'], ctx),
 
            (L9['foo_set1'], L9['scaledValue'], Literal(0.2), ctx),
light9/paint/solve.py
Show inline comments
 
from __future__ import division
 
from light9.namespaces import RDF, L9, DEV
 
from PIL import Image
 
import numpy
 
import scipy.misc, scipy.ndimage, scipy.optimize
 
import cairo
 
import logging
 

	
 
from light9.effect.settings import DeviceSettings, parseHex, toHex
 

	
 
log = logging.getLogger('solve')
 

	
 
# 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 loadNumpy(path, thumb=(100, 100)):
 
    img = Image.open(path)
 
    img.thumbnail(thumb)
 
    return numpyFromPil(img)
 

	
 
def saveNumpy(path, img):
 
    # maybe this should only run if log level is debug?
 
    scipy.misc.imsave(path, img.transpose((1, 0, 2)))
 

	
 
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 div(x, y):
 
        if y == 0:
 
            return 0
 
        return round(x / y, 3)
 
    return tuple([div(a, b) for a, b in zip(rgb1, rgb2)])
 

	
 
def brightest(img):
 
    return numpy.amax(img, axis=(0, 1))
 

	
 

	
 
class Solver(object):
 
    def __init__(self, graph):
 
        self.graph = graph
 
        self.samples = {} # uri: Image array
 
        self.fromPath = {} # basename: image array
 
        self.blurredSamples = {}
 
        self.sampleSettings = {} # (uri, path): DeviceSettings
 
@@ -66,51 +74,52 @@ class Solver(object):
 
        return scipy.ndimage.gaussian_filter(img, 10, 0, mode='nearest')
 

	
 
    def draw(self, painting):
 
        return self._draw(painting, 100, 48)
 
        
 
    def _draw(self, painting, w, h):
 
        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(20) # ?
 
        ctx.set_line_width(w / 5) # ?
 
        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 _imgDist(self, a, b):
 
        return numpy.sum(numpy.absolute(a - b), axis=None)
 
        
 
    def bestMatch(self, img):
 
        """the one sample that best matches this image"""
 
        results = []
 
        for uri, img2 in self.samples.iteritems():
 
            results.append((self._imgDist(img, img2), uri, img2))
 
        results.sort()
 
        print 'results:'
 
        for r in results:
 
            print r
 
        saveNumpy('/tmp/bestsamp.png', results[-1][2])
 
        return results[-1][1]
 
        log.info('results:')
 
        for d,u,i in results:
 
            log.info('%s %g', u, d)
 
        saveNumpy('/tmp/bestsamp.png', results[0][2])
 
        return results[0][1]
 
        
 
    def solve(self, painting):
 
        """
 
        given strokes of colors on a photo of the stage, figure out the
 
        best light DeviceSettings to match the image
 
        """
 
        pic0 = self.draw(painting).astype(numpy.float)
 
        pic0Blur = self._blur(pic0)
 
        saveNumpy('/tmp/sample_paint_%s.png' % len(painting['strokes']),
 
                  pic0Blur)
 
        sampleDist = {}
 
        for sample, picSample in sorted(self.blurredSamples.items()):
 
@@ -183,23 +192,28 @@ class Solver(object):
 
    def simulationLayers(self, settings):
 
        """
 
        how should a simulation preview approximate the light settings
 
        (device attribute values) by combining photos we have?
 
        """
 
        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():
 
                dist = devSettings.distanceTo(s.ofDevice(dev))
 
                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'])))
 
            candidatePics.sort()
 
            # we could even blend multiple top candidates, or omit all
 
            # of them if they're too far
 
            bestDist, bestPath, bestPicColor = candidatePics[0]
 

	
 
            log.info('  device best d=%g path=%s color=%s', bestDist, bestPath, bestPicColor)
 
            
 
            layers.append({'path': bestPath,
 
                           'color': colorRatio(requestedColor, bestPicColor)})
 
        
 
        return layers
light9/paint/solve_test.py
Show inline comments
 
@@ -78,12 +78,25 @@ class TestCombineImages(unittest.TestCas
 
                                        'show/dance2017/cam/test/bg.n3'])
 
        self.solver = solve.Solver(graph)
 
        self.solver.loadSamples()
 
    def test(self):
 
        out = self.solver.combineImages(layers=[
 
            {'path': 'bg2-d.jpg', 'color': (.2, .2, .3)},
 
            {'path': 'bg2-a.jpg', 'color': (.888, 0, .3)},
 
        ])
 
        solve.saveNumpy('/tmp/t.png', out)
 
        golden = solve.loadNumpy('show/dance2017/cam/test/layers_out1.png')
 
        numpy.testing.assert_array_equal(golden, out)
 

	
 
class TestBestMatch(unittest.TestCase):
 
    def setUp(self):
 
        graph = LocalSyncedGraph(files=['show/dance2017/cam/test/lightConfig.n3',
 
                                        'show/dance2017/cam/test/bg.n3'])
 
        self.solver = solve.Solver(graph)
 
        self.solver.loadSamples()
 
        
 
    def testRightSide(self):
 
        drawingOnRight = {"strokes":[{"pts":[[0.875,0.64],[0.854,0.644]],
 
                                      "color":"#aaaaaa"}]}
 
        drawImg = self.solver.draw(drawingOnRight)
 
        match = self.solver.bestMatch(drawImg)
 
        self.assertEqual(L9['sample5'], match)
show/dance2017/cam/test/lightConfig.n3
Show inline comments
 
new file 100644
show/dance2017/deviceClass.n3
Show inline comments
 
@@ -9,24 +9,25 @@
 
:zoom               a :DeviceAttr; :dataType :scalar ;
 
  rdfs:comment "maybe make this a separate 'wide to narrow' type" .
 
:focus              a :DeviceAttr; :dataType :scalar .
 
:iris               a :DeviceAttr; :dataType :scalar .
 
:prism              a :DeviceAttr; :dataType :scalar .
 
:strobe             a :DeviceAttr; :dataType :scalar;
 
  rdfs:comment "0=none, 1=fastest" .
 
:goboSpeed          a :DeviceAttr; :dataType :scalar ;
 
  rdfs:comment "0=stopped, 1=rotate the fastest".
 
:quantumGoboChoice  a :DeviceAttr; :dataType :choice;
 
  :choice :open, :spider, :windmill, :limbo, :brush, :whirlpool, :stars .
 

	
 

	
 
:SimpleDimmer a :DeviceClass;
 
  :deviceAttr :brightness;
 
  :attr
 
    [ :outputAttr :level; :dmxOffset 0 ] .
 

	
 
:ChauvetColorStrip a :DeviceClass;
 
  :deviceAttr :color;
 
  :attr
 
    [ :outputAttr :mode;  :dmxOffset 0 ],
 
    [ :outputAttr :red;   :dmxOffset 1 ],
 
    [ :outputAttr :green; :dmxOffset 2 ],
 
    [ :outputAttr :blue;  :dmxOffset 3 ] .
0 comments (0 inline, 0 general)