diff --git a/bin/captureDevice b/bin/captureDevice new file mode 100644 --- /dev/null +++ b/bin/captureDevice @@ -0,0 +1,137 @@ +#!bin/python +from __future__ import division +from rdflib import URIRef, Literal +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks, Deferred + +import logging +import optparse +import os +import time +import traceback +import treq +import cyclone.web, cyclone.websocket, cyclone.httpclient +from greplin import scales + +from run_local import log +from lib.cycloneerr import PrettyErrorHandler + +from light9.namespaces import L9 +from light9 import networking, showconfig +from light9.rdfdb.syncedgraph import SyncedGraph +from light9.paint.capture import writeCaptureDescription +from light9.greplin_cyclone import StatsForCyclone +from light9.effect.settings import DeviceSettings +from light9.effect.sequencer import sendToCollector + +stats = scales.collection('/webServer', scales.PmfStat('setAttr')) + +class Attrs(PrettyErrorHandler, cyclone.web.RequestHandler): + def put(self): + with stats.setAttr.time(): + client, clientSession, settings, sendTime = parseJsonMessage(self.request.body) + self.set_status(202) + + +class Camera(object): + def __init__(self, imageUrl): + self.imageUrl = imageUrl + + def takePic(self, uri, writePath): + log.info('takePic %s', uri) + return treq.get(self.imageUrl).addCallbacks(lambda r: self._done(writePath, r), log.error) + + @inlineCallbacks + def _done(self, writePath, response): + jpg = yield response.content() + try: + os.makedirs(os.path.dirname(writePath)) + except OSError: + pass + with open(writePath, 'w') as out: + out.write(jpg) + log.info('wrote %s', writePath) + + + +settleTime = .5 + +camera = Camera('http://dash:8200/picamserve/pic?res=480&resize=480&rotation=180&iso=800&redgain=1.6&bluegain=2&shutter=60000') + +def launch(graph): + + def steps(a, b, n): + return [round(a + (b - a) * i / n, 5) for i in range(n)] + + startTime = time.time() + toGather = [] + + row = 0 + for ry in steps(0.85, .92, 6): + xSteps = steps(.24, .45, 12) + if row % 2: + xSteps.reverse() + row += 1 + for rx in xSteps: + toGather.append(DeviceSettings(graph, [ + (L9['device/moving1'], L9['rx'], rx), + (L9['device/moving1'], L9['ry'], ry), + ])) + + numPics = [0] + settingsCache = set() + @inlineCallbacks + def step(): + if not toGather: + reactor.stop() + return + settings = toGather.pop() + + log.info('move to %r', settings) + yield sendToCollector(client='captureDevice', session='main', settings=settings) + + d = Deferred() + reactor.callLater(settleTime, d.callback, None) + yield d + dev = settings.devices()[0] + + devTail = dev.rsplit('/')[-1] + captureId = 'cap%s' % (int(startTime) - 1495170000) + picId = 'pic%s' % numPics[0] + path = '/'.join(['capture', devTail, captureId, picId]) + ctx = URIRef('/'.join([showconfig.showUri(), 'capture', devTail, captureId, 'index'])) + uri = URIRef('/'.join([showconfig.showUri(), 'capture', devTail, captureId, picId])) + + relOutPath = path + '.jpg' + + yield camera.takePic(uri, os.path.join(showconfig.root(), relOutPath)) + numPics[0] += 1 + + writeCaptureDescription(graph, ctx, uri, dev, relOutPath, settingsCache, settings) + + reactor.callLater(0, step) + step().addErrback(log.error) + + reactor.listenTCP(networking.captureDevice.port, + cyclone.web.Application(handlers=[ + (r'/()', cyclone.web.StaticFileHandler, + {"path" : "light9/web", "default_filename" : "captureDevice.html"}), + (r'/stats', StatsForCyclone), + ]), + interface='::') + log.info('serving http on %s', networking.captureDevice.port) + +def main(): + parser = optparse.OptionParser() + parser.add_option("-v", "--verbose", action="store_true", + help="logging.DEBUG") + (options, args) = parser.parse_args() + log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + + graph = SyncedGraph(networking.rdfdb.url, "captureDevice") + + graph.initiallySynced.addCallback(lambda _: launch(graph)).addErrback(log.error) + reactor.run() + +if __name__ == '__main__': + main() diff --git a/light9/networking.py b/light9/networking.py --- a/light9/networking.py +++ b/light9/networking.py @@ -36,6 +36,7 @@ class ServiceAddress(object): def path(self, more): return self.url + str(more) +captureDevice = ServiceAddress(L9['captureDevice']) curveCalc = ServiceAddress(L9['curveCalc']) dmxServer = ServiceAddress(L9['dmxServer']) dmxServerZmq = ServiceAddress(L9['dmxServerZmq']) diff --git a/light9/paint/capture.py b/light9/paint/capture.py new file mode 100644 --- /dev/null +++ b/light9/paint/capture.py @@ -0,0 +1,34 @@ +import os +from rdflib import URIRef +from light9 import showconfig +from light9.rdfdb.patch import Patch +from light9.namespaces import L9 +from light9.paint.solve import loadNumPy + +def writeCaptureDescription(graph, ctx, uri, dev, relOutPath, settingsSubgraphCache, settings): + settings.addStatements( + uri, ctx=ctx, + settingRoot=URIRef('/'.join([showconfig.showUri(), 'capture', dev.rsplit('/')[1]])), + settingsSubgraphCache=settingsSubgraphCache) + graph.patch(Patch(addQuads=[ + (dev, L9['capture'], uri, ctx), + (uri, L9['imagePath'], URIRef('/'.join([showconfig.showUri(), relOutPath])), ctx), + ])) + +class CaptureLoader(object): + def __init__(self, graph): + self.graph = graph + + def loadImage(self, pic, thumb=(100, 100)): + ip = self.graph.value(pic, L9['imagePath']) + if not ip.startswith(showconfig.show()): + raise ValueError(repr(ip)) + diskPath = os.path.join(showconfig.root(), ip[len(self.show):]) + return loadNumPy(diskPath, thumb) + + def devices(self): + """devices for which we have any captured data""" + + def capturedSettings(self, device): + """list of (pic, settings) we know for this device""" + diff --git a/light9/paint/solve.py b/light9/paint/solve.py --- a/light9/paint/solve.py +++ b/light9/paint/solve.py @@ -1,7 +1,6 @@ from __future__ import division from light9.namespaces import RDF, L9, DEV from PIL import Image -import decimal import numpy import scipy.misc, scipy.ndimage, scipy.optimize import cairo @@ -18,6 +17,11 @@ def numpyFromCairo(surface): 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): scipy.misc.imsave(path, img.transpose((1, 0, 2))) diff --git a/show/dance2017/networking.n3 b/show/dance2017/networking.n3 --- a/show/dance2017/networking.n3 +++ b/show/dance2017/networking.n3 @@ -6,6 +6,7 @@ show:dance2017 :networking sh:netHome . sh:netHome :webServer ; :patchReceiverUpdateHost "dash"; + :captureDevice ; :curveCalc ; :collector ; :collectorZmq ; @@ -14,13 +15,14 @@ sh:netHome :keyboardComposer ; :musicPlayer ; :oscDmxServer ; - :paintServer ; - :picamserve ; + :paintServer ; + :picamserve ; :rdfdb ; :subComposer ; :subServer ; :vidref . +:captureDevice :urlPath "captureDevice" . :curveCalc :urlPath "curveCalc" . :dmxServer :urlPath "dmxServer" . :effectEval :urlPath "effectEval" .