Stop Motion VR

Bringing traditional stop motion to the world of virtual reality.

Einleitung

Von einer der ältesten Filmtechniken zur Welt der virtuellen Stop-Motion Animation.

Was ist Stop Motion VR?

Das Projekt „Stop-Motion VR“ ist ein Projekt von Denny Koch und Johannes Schubert, das vom Medieninnovationszentrums Babelsberg (MIZ) gefördert wurde. Ziel war es ein einfaches Soft- und Hardwaresetup zu entwickeln, mit dem es möglich ist Stop-Trick Animationen zum großen Teil automatisiert für die Virtual Reality aufzubereiten. Dieses Handbuch schildert die Entwicklung aus Sicht der Projektbeteiligten sowie die technischen Details und Voraussetzungen.


Als Kerntechnologie verwenden wir die Photogrammetrie. Mit Hilfe der Photogrammetrie können mehrere Fotoaufnahmen eines Objektes aus verschiedenen Perspektiven zu einem 3D-Modell zusammengefügt und mit den Fotos texturiert werden. Da jeder Frame einer Stop-Motion Animation separat vom Animator eingestellt und fotografiert wird, bietet sich diese Filmtechnik im besonderen Maße für die Photogrammetrie an. Die Herausforderung bei der Umsetzung unseres Vorhabens bestand vor allem darin den Prozess von den Fotokameras bis hin zum fertigen 3D-Modell in einer 3D-Engine so weit wie möglich zu automatisieren und dabei den Animator möglichst nicht in seiner gewohnten Arbeit zu behindern. Um diese Aufgabe zu lösen haben wir eine Kombination aus Hard- und Software Setup entwickelt das die einzelnen Prozessabschnitte in großen Teilen autonom abarbeiten kann.

Als Projektpartner stand uns das Team des Stop-Motion Films Laika & Nemo zur Seite. So konnten wir unsere Techniken an einer realistischen Produktionssituation und mit echten Stop-Trick Puppen testen.

Wir möchten anderen Teams die Möglichkeit geben unser Verfahren nachzuvollziehen und stellen deswegen mit diesem OpenSource-Handbuch unsere Erkentnisse und Lösungen frei zur Verfügung.

Unser Ansatz

Wie bereits erwähnt haben wir uns in unsere Vorhaben auf die Photogrammetrie als Kerntechnologie konzentriert. Mit ihrer Hilfe können 3D-Modelle nicht nur ohne einen 3D-Artist erstellt werden, sondern diese mit den Fotos des echten Objektes texturiert werden. Dadurch entsteht ein fotorealistisches 3D-Modell das, im Falle der Stop-Motion Animation, in der Lage ist den Charm und Charakter der Handgemachten Stop-Trick Puppe in die virtuelle Realität transportiert.

Im Verlaufen des Projektes habe sich drei zentrale Problemstellungen herauskristallisiert:

  1. Wie muss ein Kamerasetup aussehen das für die Photogrammetrie geeignet ist, schnell genug für den produktiven Einsatz ist und den Animator nicht behindert?
  2. Wie kann der Photogrammetrie-Prozess möglichst weitgehend automatisiert werden um zu verhindern, dass in großem Umfang zusätzliche händische Arbeit anfällt?
  3. Wie müssen die fertigen 3D-Modelle aufbereitet werden um sie in einer der moderenen 3D-Engines nutzen zu können?

Im Folgenden zeigen wir wie wir diese Probleme gelöst haben und welche Verbesserungen wir in Zukunft noch erarbeiten möchten.

Hardware

Der Dreh- und Angelpunkt

Um aus Fotos ein 3D-Modell berechnen zu können, müssen Aufnahmen eines Objektes aus mehreren Perspektiven mit einer ausreichend hohen Überlappung erstellt werden.

Die Aufnahmen können dabei ohne Einhaltung von bestimmten Winkeln oder Abständen gemacht werden. Das bedeutet, der wichtigste Faktor beim Kamerasetup ist eine größtmögliche Abdeckung des Objektes und es kann auf einen langen Einrichtungs- und Kalilbrierungsprozess verzichtet werden. Es ist ferner möglich das gesamte Setup wärend der Aufnahmen zu bewegen und neu zu platzieren um beispielsweise dem Animator Platz zu machen.

Unserer Erfahrung nach eignen sich drei Kameras optimal für ein Grundsetup. Dabei ist jeweils eine Kamera von oben, eine aus der Mitte und eine von unten auf das Objekt gerichtet.

Der Photogrammetrie-Algorithmus sucht in den so entstandenen Fotos nach sich überlappenden Punkten und rekonstruiert daraus die jeweiligen Kamerapositionen und die Position dieser Punkte im dreidimensionalen Raum. Die daraus resultierende Punktewolke wird Parse-Cloud genannt. Sie ist der Ausgangpunkt für den weiteren Prozess.

Um das Objekt aus allen Richtungen erfassen zu können, fotografieren wir das Objekt auf einem Drehteller. Um eine hinreichend große Überlappung der einzelnen Fotos zu erreichen, fotografieren wir das Modell in 10° Schritten. Je nach fotografiertem Objekt sollten jedoch durchaus auch größere Schritte zwischen den einzelnen Drehungen ausreichend sein.

Sollte ein Drehteller als Aufnahmeplattform in einem konkreten Projekt nicht in Frage kommen oder der Prozess des Drehens des Tellers zu lange dauern, kann die Aufnahmezeit durch das Hinzufügen von weiteren Kameras oder das verringern der Aufnahmeschritte jederzeit gesenkt werden. Eine Berechnung für verschiedene Optimierungsoptionen ist im Abschnitt Optimierung zu finden.

Die Kameras

Um den Arbeitsprozess des Animators so wenig wie möglich zu stören oder gar zu verkomplizieren, mussten wir ein Kamerasetup entwickeln das weitgehend automatisiert funktioniert. Dazu gehört:

  1. Das Verbinden der Kameras mit einer zentralen Steuerung
  2. Das Auslösen der Aufnahmen
  3. Der Download der Bilder von den Kameras
  4. Das (Weiter-)drehen des Drehtellers um eine voreingestellte Gradzahl

Für die zentrale Steuerung des gesamten Aufbaus haben wir die Prozess-Steuerungssoftware Ray eingesetzt. Diese Software der Firma Scenid ist derzeit nur auf Nachfrage erhältlich und erlaubt es Soft- und Hardwaremodule in einem Prozess zu verknüpfen.

Zum Steuern der Kameras waren wir auf eine programmierbare Schnittstelle (API) des Kameraanbieters angewiesen. Um das Setup möglichst flexibel einsetzten zu können entschieden wir uns die Kamera Alpha 6000 von Sony einzusetzen, die über eine offene WLAN-API verfügt. Im Nachhinein würden wir allerdings eine kabelgebundene Lösung präferieren, da sich das WLAN als teilweise äußerst unstabil erwiesen hat und sich der Verbindungsvorgang an sich als unnötig kompliziert herausgestellt hat. Dennoch war es uns möglich ein Ray-Modul zu entwickeln, das sich automatisch mit dem Kamera-WLAN verbinden und anschließend die Fotos auslösen kann. Der Download der Bilder war aufgrund der Schwierigkeiten mit der WLAN-API nicht mehr im Projektrahmen zu lösen. Dafür konnten wir den Auslöseprozess jedoch sehr detailliert abbilden und steuern.

Jede Kamera strahlt ein eigenes WLAN-Signal aus, auf das sich verbunden werden muss. Dazu setzen wir pro Kamera jeweils einen Mini-Computer namens Raspberry PI ein, auf dem ein Linux-Derivat läuft. Damit stehen uns alle wichtigen Script- und Programmiersprachen, sowie der große Funktionsumfang des OS zur Verfügung.

Jedes Raspberry PI verbindet sich zunächst mit dem Kamera-WLAN. Anschließend erfolgt die Suche der Kamerabeschreibung im Netzwerk über das SSDP Protokoll. Ist die Kamerabeschreibung gefunden wird diese heruntergeladen, verarbeitet und der API-Endpunkt gespeichert. Da Linux korrekterweise die SSDP-Unicast Pakete nicht dupliziert, werden diese leider nur an das erste Netzwerkinterface (meistens das eth0) gesendet. Dies verhindert allerdings, dass das Raspberry PI die Kamera über den WLAN-Adapter suchen kann.

Unsere Lösung für dieses Problem ist ein vorübergehendes Deaktivieren des Ethernet-Inferfaces für den Zeitraum über den die Kamerabeschreibung gesucht wird. Diese Herangehensweise ist insgesamt nicht zufriedenstellend und bestärkt uns in dem Standpunkt, dass eine Kabellösung vorzuziehen ist.

Ist die Verbindung mit der Kamera hergestellt und der API-Endpunkt gefunden, stellt das Ray-Modul die Verbindung zum lokalen Netzwerk via Ethernet wieder her und meldet sich an der zentralen Steuereinheit von Ray (dem Controller) an. Ab diesem Moment werden alle Statusmeldungen der Kameras über den Controller gesteuert und visualisiert.

Unser Node Modul zum Verbinden mit der Sony Remote API

// process.env.DEBUG = 'node-ssdp*';

var os = require('os');
var exec = require('child_process').exec;
var request = require('request');

var parseString = require('xml2js').parseString;

var request = require('request');

var Client = require('node-ssdp').Client;
var wifi = require('node-wifi')

module.exports = function(userConfig) {

  this.defaultConfig = {
    maxMSearch : 10,
    cameraSchema : 'urn:schemas-sony-com:service:ScalarWebAPI:1',
		API_URL : null,
		ssid : null,
		psk : null
  };
  this.mClient = null;
  this.searchTimer = null;
  this.timerCounter = 0;

  this.config = Object.assign({}, this.defaultConfig, userConfig);

  this.sendRPCNoArgs = function(m, v, p) {
    p = p || [];
    return new Promise(function(resolve, reject) {
      v = v || "1.0";
      var c = { "method": m, "params": p, "id": 1, "version": v };
      request({
        method : "POST",
        uri  : this.config.API_URL + "/camera",
        json : true,
        body : c
      }, function(error, res, body) {
        if(error) return reject(error);
        else return resolve(body);
      });
    }.bind(this));
  };

  this.connectWifi = function() {
    return new Promise(function(resolve, reject) {
			console.log('Conntecting Wifi: ' + this.config.ssid + ' : ' + this.config.psk + '...')
			wifi.connect({ ssid : this.config.ssid, password : this.config.psk }, function(err) {
				if (err) {
					console.error(err)
					return reject(err)
				}
				console.log('Connected!')
				return resolve()
			});
    });
  };

  this.disconnectWifi = function() {
    return new Promise(function(resolve, reject) {
      if(os.platform() == 'win32') {
        exec('netsh wlan disconnect', resolve);
      } else {
        exec('nmcli device disconnect iface wlan0', resolve);
      }
    });
  };

  this.discoverCamera = function() {
    return new Promise(function(resolve, reject) {
      console.log('Starting Camera Discovery');

      this.timerCounter = 0;

      this.mClient = new Client();

      this.mClient.on('response', function (headers, statusCode, rinfo) {
        clearInterval(this.searchTimer);

        this.config.DEVICE_DEC = headers.LOCATION;

        console.log('Found Device!');
        console.log('Device Description at: ', this.config.DEVICE_DEC);

        resolve();
      }.bind(this));

      this.sendMSearch(reject);
      this.searchTimer = setInterval(function() { this.sendMSearch(reject); }.bind(this), 2000);
    }.bind(this));
  };

  this.sendMSearch = function(reject) {
    if(this.timerCounter == this.config.maxMSearch) {
      clearInterval(this.searchTimer);
      console.log('Maximum M-Search trys reached, stopping camera discovery now.');
      return reject('Maximum M-Search trys reached, stopping camera discovery now.');
    }
    console.log('Sending M-Search no. ' + (this.timerCounter + 1));
    this.mClient.search(this.config.cameraSchema);
    this.timerCounter ++;
  };

  this.parseDeviceDec = function() {
    return new Promise(function(resolve, reject) {
      console.log('Requesting Device Description');
      request.get(this.config.DEVICE_DEC, function (error1, response, deviceDecXML) {
        if(error1) {
          console.error(error1);
          return reject(error1);
        }

        parseString(deviceDecXML, function (error2, r) {
          if(error2) {
            console.error(error2);
            return reject(error2);
          }

          var device = r.root.device[0];

          this.config.CAMERA_NAME = device.friendlyName[0];
          this.config.API_URL = device['av:X_ScalarWebAPI_DeviceInfo'][0]['av:X_ScalarWebAPI_ServiceList'][0]['av:X_ScalarWebAPI_Service'][2]['av:X_ScalarWebAPI_ActionList_URL'][0];

          console.log("Device Info:");
          console.log("-----------------------");
          console.log("Device Name: ", this.config.CAMERA_NAME);
          console.log("API Endpoint: ", this.config.API_URL);
          console.log("-----------------------");

          resolve();

        }.bind(this));
      }.bind(this));
    }.bind(this));
  };

  this.initShooting = function() {
    return this.sendRPCNoArgs("startRecMode");
  };

  this.getSupportedCameraFunction = function(iso) {
    return this.sendRPCNoArgs("getSupportedCameraFunction");
  };

  this.setFNumber = function(f) {
    return this.sendRPCNoArgs("setFNumber", null, [f]);
  };

  this.setShutterSpeed = function(s) {
    return this.sendRPCNoArgs("setShutterSpeed", null, [s]);
  };

  this.setISO = function(iso) {
    return this.sendRPCNoArgs("setIsoSpeedRate", null, [iso]);
  };

  this.takePhoto = function() {
    return this.sendRPCNoArgs("actTakePicture");
  };

};

Der Drehteller

Nach eingehender Recherche haben wir uns entschlossen den Drehteller für unser Setup selbst zu entwickeln. Die kommerziellen Produkte basieren meistens auf proprietären Softwarelösungen ohne offene API und sind zudem recht kostspielig.

Unseren Drehteller haben wir auf der Grundlage maschinell gesägter MDF Platten und in einem maßgefertigten Flightcase realisiert. Als Motor verwendeten wir einen einfachen Schrittmotor, der wie die Kameras über ein Raspberry PI und mit einem MotorHAT von der Firma Adafruit gesteuert wird. Die Automatisierung erfolgt auch hier über ein speziell entwickeltes Ray-Modul, das eine zentrale Steuerung des Motors erlaubt. Das MotorHAT von Adafruit verfügt über eine eigene Python Bibliothek mittels der der Motor unkompliziert gesteuert werden kann.

Unser Node Modul zur Steuerung des MotorHATs von Adafruit

var PythonShell = require('python-shell')

module.exports = function(userConfig) {

	this.FORWARD = 1
	this.BACKWARD = 2

	this.SINGLE = 1
	this.DOUBLE = 2
	this.INTERLEAVE = 3
	this.MICROSTEP = 4

  this.defaultConfig = {
    logger : require('scenid-default-logger')
  }

  this.config = require('scenid-configure-module')(userConfig, this.defaultConfig)
  this.logger = this.config.logger

	this.on = function(which, f) {
		if(which == 'ready') {
			this.config.readyF = f
			this.config.readyF()
		}
	}

	this.step = function(speed, steps, direction, step_type) {
		var options = {
  		mode: 'text',
			scriptPath: __dirname,
  		args: [speed, steps, direction, step_type]
		}

		PythonShell.run('stepper.py', options, function (err, results) {
			if (err) console.error(err)
			// results is an array consisting of messages collected during execution
			console.log('results: %j', results)
			if(this.config.readyF) this.config.readyF()
		}.bind(this))
	}
}

Das dazugehörige Python Script

#!/usr/bin/python
#import Adafruit_MotorHAT, Adafruit_DCMotor, Adafruit_Stepper
from Adafruit_MotorHAT import Adafruit_MotorHAT, Adafruit_DCMotor, Adafruit_StepperMotor

import sys
import time
import atexit

# create a default object, no changes to I2C address or frequency
mh = Adafruit_MotorHAT()

# recommended for auto-disabling motors on shutdown!
def turnOffMotors():
	mh.getMotor(1).run(Adafruit_MotorHAT.RELEASE)
	mh.getMotor(2).run(Adafruit_MotorHAT.RELEASE)
	mh.getMotor(3).run(Adafruit_MotorHAT.RELEASE)
	mh.getMotor(4).run(Adafruit_MotorHAT.RELEASE)

	atexit.register(turnOffMotors)

speed = int(sys.argv[1])
steps = int(sys.argv[2])
direction = int(sys.argv[3])
step_type = int(sys.argv[4])

# step count issn't used by the library ... dunno why!?
myStepper = mh.getStepper(200, 1)
myStepper.setSpeed(speed)
myStepper.step(steps, direction, step_type)

Das Licht

Ein äußerst wichtiger Aspekt bei der Fotografie für Photogrammetrie ist die Beleuchtung. Entgegen der Intuition der meisten Fotografen und Filmschaffenden müssen die Fotos für dieses Verfahren möglichst gleichmäßig und neutral beleuchtet sein. So wird einerseits sichergestellt dass der Algorithmus nicht durch einen wandernden Schattenwurf irritiert wird und andererseits ist es so möglich das resultierende 3D-Modell in der virtuellen Szene mit virtuellem Licht und damit realitätsnäher auszuleuchten. Da die Fotos auch als Textur für das fertige Modell verwendet werden, würde ein Schatten auf den Fotos zudem als falscher Schatten in der VR-Szene erscheinen.

Dank der Unterstützung eines professionellen Fotografen konnten wir ein minimales Lichtsetup entwickeln, das lediglich aus einem auf die dem Drehteller gegenüberliegende Decke gerichteten Blitzlicht besteht. Dabei werden die Kameras so eingestellt, dass sie nur das Blitzlicht aufnehmen. Um dies zu erreichen wird der ISO-Wert auf ein niedriges bis normales Level gesetzt (ca. 200 - 400) und die Blende so eingestellt, dass das resultierende Foto ohne das Blitzlicht praktisch schwarz ist. So können unerwünschte Umwelteinflüsse wie normale Raumbeleuchtung oder Sonnenlicht praktisch ausgeschaltet werden. Durch das Ausrichten des Blitzlichtes gegen die Decke oder eine dem Objekt gegenüberliegende Wand lässt sich eine möglichst breite Streuung des Lichtes erreichen. Die resultierenden Bilder sind schließlich neutral beleuchtet und nahezu schattenfrei.

Damit jede Kamera denselben Blitz nutzen kann, entschieden wir uns jede Kamera mit einem eigenen Funkauslöser zum Auslösen des Blitzes auszustatten.

Bei unseren Recherchen sind wir häufig auf Lichtsetups gestoßen, die auf Standlichtern basieren. Diese haben den großen Nachteil das man verhältnismäßig viele und zudem sehr helle Lampen benötigt um das Bild in einer vergleichbaren Qualität auszuleuchten. Das ist zum einen finanziell unattraktiv und würde zum anderen unnötige zusätzliche Komponenten am Filmset bedeuten, die den Animator in seiner Arbeit behindern könnten. Der einzige für uns ersichtliche Vorteil eines auf Standlichtern basierenden Lichtsetups ist, dass die Kameras potentiell schneller oder sogar simultan auslösen können, da nicht auf das Laden des Blitzlichtes gewartet werden muss. Unserer Erfahrung nach muss bei einem Blitzlicht-Setup mit einer Wartezeit von ca. 0,5 Sekunden zwischen den einzelnen Fotos gerechnet werden.

Optimierung

Je nach Bedarf der jeweiligen Filmproduktion kann es nötig sein die Aufnahmezeiten zu veringern. Dies lässt sich über das Hinzufügen von Kameras, das Verringern der Gradzahl der einzelnen Drehtellerschritte oder das Wechseln auf ein Standlicht-Setup erreichen. Im Folgenden sind vier Berechnungsbeispiele für die verschiedenen Optimierungsoptionen aufgeführt.

Blitzlicht-Setup: Berechnung der Sekunden pro Schritt und Pose und Filmsekunden am Tag in Abhängigkeit zu der Anzahl der Kameras

Angenommen werden folgende Konstanten:

  • Jede Kamera benötigt 0,5 Sekunden zum Auslösen (spA)
  • Der Drehteller benötigt 2 Sekunden zum erreichen der nächsten Position (sR)
  • Der Animator braucht 60 Sekunden zum einstellen der Pose (As)
  • Eine Filmsekunde hat 12 einzelne Frames (Posen)

Anzahl Kameras Schritt­grad Anzahl Schritte spA sR As Sekunden pro Pose Film­sekunden in 8h
3 10 36 0.5 2 60 186 12.90
6 10 18 0.5 2 60 150 16.00
9 10 12 0.5 2 60 138 17.39
12 10 9 0.5 2 60 132 18.18
18 10 6 0.5 2 60 126 19.05
27 10 4 0.5 2 60 122 19.67
36 10 3 0.5 2 60 120 20.00

Blitzlicht-Setup: Berechnung der Sekunden pro Schritt und Pose und Filmsekunden am Tag in Abhängigkeit des Schrittgrades

Angenommen werden folgende Konstanten:

  • Jede Kamera benötigt 0,5 Sekunden zum Auslösen (spA)
  • Der Drehteller benötigt 2 Sekunden zum erreichen der nächsten Position (sR)
  • Der Animator benötigt 60 Sekunden zum einstellen der Pose (As)
  • Eine Filmsekunde hat 12 einzelne Frames (Posen)

Anzahl Kameras Schritt­grad Anzahl Schritte spA sR As Sekunden pro Pose Film­sekunden in 8h
3 10 36 0.5 2 60 186 12.90
3 20 18 0.5 2 60 123 19.51
3 30 12 0.5 2 60 102 23.53
3 40 9 0.5 2 60 91.5 26.23
3 45 8 0.5 2 60 88 27.27
3 60 6 0.5 2 60 81 29.63

Standlicht-Setup: Berechnung der Sekunden pro Schritt und Pose und Filmsekunden am Tag in Abhängigkeit zu der Anzahl der Kameras

Angenommen werden folgende Konstanten:

  • Alle Kameras lösen innerhalb einer Sekunde aus (spA)
  • Der Drehteller benötigt 2 Sekunden zum erreichen der nächsten Position (sR)
  • Der Animator benötigt 60 Sekunden zum einstellen der Pose (As)
  • Eine Filmsekunde hat 12 einzelne Frames (Posen)

Anzahl Kameras Schritt­grad Anzahl Schritte spA sR As Sekunden pro Pose Film­sekunden in 8h
3 10 36 1 2 60 168 14.29
6 10 18 1 2 60 105 22.86
9 10 12 1 2 60 88 27.27
12 10 9 1 2 60 80.25 29.91
18 10 6 1 2 60 73 32.88
27 10 4 1 2 60 68.44 35.06
36 10 3 1 2 60 66.25 36.23

Standlicht-Setup: Berechnung der Sekunden pro Schritt und Pose und Filmsekunden am Tag in Abhängigkeit des Schrittgrades

Angenommen werden folgende Konstanten:

  • Alle Kameras lösen innerhalb einer Sekunde aus (spA)
  • Der Drehteller benötigt 2 Sekunden zum erreichen der nächsten Position (sR)
  • Der Animator benötigt 60 Sekunden zum einstellen der Pose (As)
  • Eine Filmsekunde hat 12 einzelne Frames (Posen)

Anzahl Kameras Schritt­grad Anzahl Schritte apA sR As Sekunden pro Pose Film­sekunden in 8h
3 10 36 1 2 60 168 14.29
3 20 18 1 2 60 114 21.05
3 30 12 1 2 60 96 25
3 40 9 1 2 60 87 27.59
3 45 8 1 2 60 84 28.57
3 60 6 1 2 60 78 30.77

Software

PhotoScan

Auf der Softwareebene ist das Photogrammetrieprogramm die zentrale Komponente. In unserem Fall haben wir Professional Edition der Software Agrisoft PhotoScan eingesetzt. PhotoScan ist in vielen Bereichen der Unterhaltungsindustrie, aber auch im wissenschaftlichen Bereich der Standard im Bereich der Photogrammtrielösungen und bietet in der Professional Edition die Möglichkeit eigene Automatisierungsskripte zu entwickeln. Das Scripting in PhotoScan basiert auf Python. Daher konnten wir auf der Grundlage des Python Moduls von Ray auch hier eine Anbindung an den Ray-Controller und zur zentralen Steuerung realisieren. Der Prozess in PhotoScan teilt sich in die folgenden Unterprozesse auf:

  1. Laden einer Fotoreihe
  2. Finden von zusammengehörigen Punkten (Alignment)
  3. Berechnen einer Punktewolke (Dense-Cloud)
  4. (Optional) Händisches Bereinigen der Punktewolke
  5. Triangulation der Dense-Cloud (Meshing)
  6. Rekonstruktion der Textur (Texturing)
  7. Export des 3D-Modells für den weiteren Prozess

Die von uns verwendeten Parameter

Alle diese Punkte werden durch das von uns entwickelte PhotoScan-Script eigenständig nacheinander abgearbeitet.

Die Möglichkeit den Prozess zu unterbrechen und die Punktewolke von Hand zu bereinigen ist derzeit noch nicht implementiert, kann aber ohne größeren Aufwand nachgepflegt werden.

Das Pipeline-Script zum automatischen Abarbeiten eines Fotoordners in PhotoScan


import PhotoScan
import sys
import os
import time
import json
import urllib.request
import socket
import threading
from threading import Thread

# threading test
def fancyFunction(it):
				response = urllib.request.urlopen()

t = Thread(target = fancyFunction)
t.start()

# basic stuff
rootDirectory = "c:/"
processFolderPath = "/"
exportFolderPath = "/"
photoList = []
cmdArgumentAnnouncementString = "-"
cmdArgumentDefinitionString = "="
cmdArgumentPathSpaceEscapeString = "§§"
scriptStartTimestamp = time.time()
scriptDuration = 0.0



##########################################################
# config variables (declaration and default fallback)
##########################################################
# Alignment accuracy in [HighestAccuracy, HighAccuracy, MediumAccuracy, LowAccuracy, LowestAccuracy]
qualityLevels_MatchingAccuracy = [PhotoScan.LowestAccuracy, PhotoScan.LowAccuracy, PhotoScan.MediumAccuracy, PhotoScan.HighAccuracy, PhotoScan.HighestAccuracy]
configQuality_MatchingAccuracy = PhotoScan.MediumAccuracy

# Image pair preselection in [ReferencePreselection, GenericPreselection, NoPreselection]
qualityLevels_MatchingPreselection = [PhotoScan.ReferencePreselection, PhotoScan.GenericPreselection, PhotoScan.NoPreselection]
configQuality_MatchingPreselection = PhotoScan.GenericPreselection

# Dense point cloud quality in [UltraQuality, HighQuality, MediumQuality, LowQuality, LowestQuality]
qualityLevels_DenseCloudQuality = [PhotoScan.LowestQuality, PhotoScan.LowQuality, PhotoScan.MediumQuality, PhotoScan.HighQuality, PhotoScan.UltraQuality]
configQuality_DenseCloudQuality = PhotoScan.LowQuality

# Interpolation mode in [EnabledInterpolation, DisabledInterpolation, Extrapolated]
qualityLevels_Interpolation = [PhotoScan.EnabledInterpolation, PhotoScan.DisabledInterpolation, PhotoScan.Extrapolated]
configQuality_Interpolation = PhotoScan.EnabledInterpolation

# UV mapping mode in [GenericMapping, OrthophotoMapping, AdaptiveOrthophotoMapping, SphericalMapping, CameraMapping]
qualityLevels_UVMappingMode = [PhotoScan.GenericMapping, PhotoScan.OrthophotoMapping, PhotoScan.AdaptiveOrthophotoMapping, PhotoScan.SphericalMapping, PhotoScan.CameraMapping]
configQuality_UVMappingMode = PhotoScan.SphericalMapping

# Diffuse texture size (pixel, must be power-of-two resolution)
configQuality_DiffuseTextureSize = 256

# Blending mode in [AverageBlending, MosaicBlending, MinBlending, MaxBlending, DisabledBlending]
qualityLevels_DiffuseBlendingMode = [PhotoScan.AverageBlending, PhotoScan.MosaicBlending, PhotoScan.MinBlending, PhotoScan.MaxBlending, PhotoScan.DisabledBlending]
configQuality_DiffuseBlendingMode = PhotoScan.MosaicBlending



##########################################################
# parsing command line parameter
##########################################################
print("Parsing command line arguments...")
for cmdArgument in sys.argv:
	# is there a correct command line parameter?
	if cmdArgument.find(cmdArgumentAnnouncementString) >= 0:
		cmdArgument = cmdArgument.replace(cmdArgumentAnnouncementString, "") # get rid of the announcing string
		definitionIndex = cmdArgument.find(cmdArgumentDefinitionString)
		if definitionIndex < 0:
			print("ERROR: Parameter found but no definition was given!")

		# parsing key and value
		key = cmdArgument[:definitionIndex]
		value = cmdArgument[(definitionIndex + len(cmdArgumentDefinitionString)):]
		print("PARAMETER: '"+key+"' -> '"+value+"'")

		if key == "MatchingAccuracy":
			configQuality_MatchingAccuracy = qualityLevels_MatchingAccuracy[int(value)]
		elif key == "MatchingPreselection":
			configQuality_MatchingPreselection = qualityLevels_MatchingPreselection[int(value)]
		elif key == "DenseCloudQuality":
			configQuality_DenseCloudQuality = qualityLevels_DenseCloudQuality[int(value)]
		elif key == "Interpolation":
			configQuality_Interpolation = qualityLevels_Interpolation[int(value)]
		elif key == "UVMappingMode":
			configQuality_UVMappingMode = qualityLevels_UVMappingMode[int(value)]
		elif key == "DiffuseTextureSize":
			configQuality_DiffuseTextureSize = int(value)
		elif key == "DiffuseBlendingMode":
			configQuality_DiffuseBlendingMode = qualityLevels_DiffuseBlendingMode[int(value)]
		elif key == "ProcessFolder":
			processFolderPath = value.replace(cmdArgumentPathSpaceEscapeString, " ")
		elif key == "ExportFolder":
			exportFolderPath = value.replace(cmdArgumentPathSpaceEscapeString, " ")
		else:
			print("Ignoring unknown parameter.")



##########################################################
# crawl photo directory
##########################################################
print("Crawling processing directory...")
for file in os.listdir(processFolderPath):
	if file.endswith(".jpg") or file.endswith(".JPG") or file.endswith(".jpeg") or file.endswith(".JPEG"):
		photoList += [str(file)]
print(str(len(photoList)) + " photos found.")

print("Reading absolute base paths...")
for i in range(len(photoList)):
	photoList[i] = os.path.join(processFolderPath, '') + photoList[i]



##########################################################
# start calculation process
##########################################################
print(str(len(photoList)) + " photos in queue!")

doc = PhotoScan.app.document

print("Creating chunk...")
chunk = doc.addChunk()
print(photoList)
chunk.addPhotos(photoList)

print(str(len(chunk.cameras)) + " cameras created!")

print("Matching photos...")
chunk.matchPhotos(accuracy = configQuality_MatchingAccuracy, preselection = configQuality_MatchingPreselection)

print("Align cameras...")
chunk.alignCameras()

print("Building dense cloud...")
chunk.buildDenseCloud(quality = configQuality_DenseCloudQuality)

print("Generating model...")
chunk.buildModel(PhotoScan.Arbitrary, interpolation = configQuality_Interpolation)

print("Generating UV map...")
chunk.buildUV(mapping = configQuality_UVMappingMode)

print("Generate diffuse map...")
chunk.buildTexture(blending = configQuality_DiffuseBlendingMode, size = configQuality_DiffuseTextureSize)

print("Exporting model...")
chunk.exportModel(path = os.path.join(exportFolderPath, " ") + "export.fbx", cameras = False, format = "fbx")

print("Cleaning project...")
doc.clear()

scriptDuration = time.time() - scriptStartTimestamp
print("Script ended. ("+str(scriptDuration)+" seconds processing time)")

Unreal Engine 4

Wir setzen für Stop Motion VR die Unreal Engine 4 (UE4) ein. Sie ist im Vergleich zu anderen 3D-Engines sehr einfach zu bedienen, für zahlreiche Anwendungsgebiete kostenlos verfügbar und stellt mit Blueprint eine zugängliche visuelle Programmiersprache bereit. Die Unreal Engine import und transformiert viele gängige 3D-Formate und die dazugehörigen Texturen automatisch derart, dass uns hier die Möglichkeit eines nahtlosen Transfers der PhotoScan-Modelle in die Engine bereits vorlag. Für längere Szenen ist es allerdings notwendig, die Anzahl der Polygone der 3D-Modelle zu reduzieren und diese dementsprechend zu transformieren. Für diese Transformierung haben wir im Verlaufe des Projektes mehrere Programme getestet. Besonders hinsichtlich des Funktionsumfangs hat uns dabei am meisten die Open Source Software Blender zugesagt. Das 3D-Programm bietet eine Python-Schnittstelle zur Automatisierung und war in der Lage für uns verwertbare Modelle zu erzeugen. Wir würden jedoch empfehlen diesen Schritt von Hand durchzuführen, da hier ein künstlerisches Auge auf das Ergebnis gefragt sein kann.

Um die fertigen 3D-Modelle in der VR-Szene schließlich wieder zu einer Animation zusammführen zu können, haben wir in der Unreal Engine einen Blueprint-Actor entwickelt. Diesem Actor können mehrere Modelle hinzugefügt werden, die eine fortlaufende Sequenz ergeben. Über die Eingabe verschiedener Parameter können dann die Animationsgeschwindigkeit und die Art der Animation gesteuert werden. Außerdem bietet der Actor die Möglichkeit zur Kontrolle einzelner Frames und somit diese einzeln anzusteuern.

Downloads

Kontakt