diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4c3bf601a8d2f8548adee0544438e0bf250a0f5d..6b053a2e686f36693295c847420318ebaf241299 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,15 +1,26 @@
-image: ubuntu:18.04
-
-before_script:
- - export DEBIAN_FRONTEND=noninteractive
- - apt-get update -qq && apt-get install -y -qq g++ cmake git
- - apt-get install -y -qq libopencv-dev libgoogle-glog-dev liburiparser-dev libreadline-dev libmsgpack-dev uuid-dev
+windows job:
+  tags:
+    - win
+  script:
+    - 'call "C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC/Auxiliary/Build/vcvars64.bat"'
+    - mkdir build
+    - cd build
+    - 'cmake -DWITH_PCL=FALSE -DCMAKE_GENERATOR_PLATFORM=x64 -DOpenCV_DIR="D:/opencv-4.0.1/build/install" -DCUDA_TOOLKIT_ROOT_DIR="C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v10.1" ..'
+    - devenv ftl.utu.fi.sln /build Release
+  
+linux job:
+  tags:
+    - docker
 
-ftl:
+  image: ubuntu:18.04
+  
+  before_script:
+    - export DEBIAN_FRONTEND=noninteractive
+    - apt-get update -qq && apt-get install -y -qq g++ cmake git
+    - apt-get install -y -qq libopencv-dev libgoogle-glog-dev liburiparser-dev libreadline-dev libmsgpack-dev uuid-dev libpcl-dev
   script:
     - mkdir build
     - cd build
     - cmake ..
     - make
-    - make test
-
+    - ctest --output-on-failure
diff --git a/CMakeLists.txt b/CMakeLists.txt
index dc83c57d6b51db9789afabb25f755820dcaac9d4..6eaf7b6d5304fca33e7009e98e14653de458d535 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -11,17 +11,40 @@ include(GNUInstallDirs)
 include(CTest)
 enable_testing()
 
+option(WITH_PCL "Use PCL if available" ON)
+option(WITH_FIXSTARS "Use Fixstars libSGM if available" ON)
+option(BUILD_VISION "Enable the vision component" ON)
+option(BUILD_RECONSTRUCT "Enable the reconstruction component" ON)
+option(BUILD_RENDERER "Enable the renderer component" ON)
+
 set(THREADS_PREFER_PTHREAD_FLAG ON)
 set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
 
 include(Findglog)
-#find_package( glog REQUIRED )
+
 find_package( OpenCV REQUIRED )
 find_package( Threads REQUIRED )
 find_package( URIParser REQUIRED )
 find_package( MsgPack REQUIRED )
-find_package( LibSGM )
-#find_package( ZLIB REQUIRED )
+
+if (WITH_FIXSTARS)
+	find_package( LibSGM )
+endif()
+
+if(${CMAKE_VERSION} VERSION_GREATER "3.12.0") 
+	cmake_policy(SET CMP0074 NEW)
+endif()
+
+if (WITH_PCL)
+	find_package( PCL QUIET COMPONENTS io common visualization registration segmentation)
+endif()
+
+set(CMAKE_CXX_STANDARD 17) # For PCL/VTK https://github.com/PointCloudLibrary/pcl/issues/2686
+set(HAVE_OPENCV TRUE)
+
+if (PCL_FOUND)
+	set(HAVE_PCL TRUE)
+endif()
 
 # Readline library is not required on Windows
 # May also entirely remove dependence on this... it should be optional at least.
@@ -93,7 +116,7 @@ check_include_file_cxx("opencv2/cudastereo.hpp" HAVE_OPENCVCUDA)
 find_program(CPPCHECK_FOUND cppcheck)
 if (CPPCHECK_FOUND)
 	message(STATUS "Found cppcheck: will perform source checks")
-	set(CMAKE_CXX_CPPCHECK "cppcheck" "--enable=style" "--suppress=*:*catch.hpp" "--suppress=*:*json.hpp")
+	set(CMAKE_CXX_CPPCHECK "cppcheck" "--enable=style" "--suppress=*:*catch.hpp" "--suppress=*:*elas*" "--suppress=*:*json.hpp")
 endif()
 
 include_directories(${PROJECT_SOURCE_DIR}/common/cpp/include)
@@ -116,10 +139,20 @@ endif()
 SET(CMAKE_USE_RELATIVE_PATHS ON)
 
 add_subdirectory(common/cpp)
-add_subdirectory(renderer)
 add_subdirectory(net)
-add_subdirectory(vision)
-add_subdirectory(reconstruct)
+
+if (BUILD_RENDERER)
+	add_subdirectory(renderer)
+	set(HAVE_RENDER)
+endif()
+
+if (BUILD_VISION)
+	add_subdirectory(vision)
+endif()
+
+if (BUILD_RECONSTRUCT)
+	add_subdirectory(reconstruct)
+endif()
 
 ### Generate Build Configuration Files =========================================
 
@@ -131,3 +164,13 @@ configure_file(${CMAKE_SOURCE_DIR}/common/cpp/src/config.cpp.in
                ${CMAKE_SOURCE_DIR}/common/cpp/src/config.cpp
 )
 
+# For issue #17
+# https://gitlab.kitware.com/cmake/cmake/issues/16915#note_456382
+if ( TARGET Qt5::Core )
+	get_property( core_options TARGET Qt5::Core PROPERTY INTERFACE_COMPILE_OPTIONS )
+	string( REPLACE "-fPIC" "" new_core_options "${core_options}" )
+	set_property( TARGET Qt5::Core PROPERTY INTERFACE_COMPILE_OPTIONS ${new_core_options} )
+	set_property( TARGET Qt5::Core PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE "ON" )
+	set( CMAKE_CXX_COMPILE_OPTIONS_PIE "-fPIC" )
+endif()
+
diff --git a/common/config/config.json b/common/config/config.json
index 4c0ec32253deeac8ac77c89a1f6659f8f437ee2d..186f3821754d22a57367c832415d18263810d622 100644
--- a/common/config/config.json
+++ b/common/config/config.json
@@ -10,7 +10,10 @@
 			"nostereo": false,
 			"scale": 1.0,
 			"flip_vert": false,
-			"max_fps": 25
+			"max_fps": 25,
+			"width": 640,
+			"height": 480,
+			"crosshair": false
 		},
 		"calibrate": false,
 		"calibration": {
@@ -55,7 +58,8 @@
 			"points": true,
 			"depth": false,
 			"left": false,
-			"right": false
+			"right": false,
+			"crosshair": false
 		},
 		"net": {
 			"listen": "tcp://*:9001",
@@ -69,7 +73,7 @@
 		"net": {
 			"peers": ["tcp://localhost:9001"]
 		},
-		"source": "ftl://utu.fi/dummy/rgb-d",
+		"sources": ["ftl://utu.fi/node1/rgb-d"],
 		"display": {
 			"flip_vert": false,
 			"disparity": false,
@@ -77,6 +81,15 @@
 			"depth": false,
 			"left": false,
 			"right": false
+		},
+		"registration": {
+			"reference-source" : "ftl://utu.fi/node1/rgb-d",
+			"calibration" : {
+				"run": false,
+				"iterations" : 10,
+				"delay" : 1000,
+				"patternsize" : [9, 6]
+				}
 		}
 	}
 }
diff --git a/common/cpp/CMakeLists.txt b/common/cpp/CMakeLists.txt
index 2a9be0f3b3f32963d088d6af06809db57281d3b7..c8248564d8629021ce24b40f7952a20a4d7337bc 100644
--- a/common/cpp/CMakeLists.txt
+++ b/common/cpp/CMakeLists.txt
@@ -1,13 +1,15 @@
 set(COMMONSRC
 	src/config.cpp
 	src/configuration.cpp
+	src/opencv_to_pcl.cpp
 )
 
 add_library(ftlcommon ${COMMONSRC})
 
 target_include_directories(ftlcommon PUBLIC
+	${PCL_INCLUDE_DIRS}
 	$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
 	$<INSTALL_INTERFACE:include>
 	PRIVATE src)
-target_link_libraries(ftlcommon glog::glog)
+target_link_libraries(ftlcommon glog::glog ${OpenCV_LIBS} ${PCL_LIBRARIES})
 
diff --git a/common/cpp/include/ftl/config.h.in b/common/cpp/include/ftl/config.h.in
index b03e4eeeff946c3e8c282500dd4717fa11fd3539..ca51cebb894ed858580b823201f859aaa5e1f675 100644
--- a/common/cpp/include/ftl/config.h.in
+++ b/common/cpp/include/ftl/config.h.in
@@ -5,6 +5,9 @@
 #cmakedefine HAVE_OPENCVCUDA
 #cmakedefine HAVE_URIPARSESINGLE
 #cmakedefine HAVE_CUDA
+#cmakedefine HAVE_OPENCV
+#cmakedefine HAVE_PCL
+#cmakedefine HAVE_RENDER
 
 extern const char *FTL_VERSION_LONG;
 extern const char *FTL_VERSION;
diff --git a/common/cpp/include/ftl/utility/opencv_to_pcl.hpp b/common/cpp/include/ftl/utility/opencv_to_pcl.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..f273d5e23e460bc70b4fb35ecfd16ce97308ffef
--- /dev/null
+++ b/common/cpp/include/ftl/utility/opencv_to_pcl.hpp
@@ -0,0 +1,22 @@
+#ifndef _FTL_COMMON_OPENCV_TO_PCL_HPP_
+#define _FTL_COMMON_OPENCV_TO_PCL_HPP_
+
+#include <ftl/config.h>
+#include <opencv2/opencv.hpp>
+
+#if defined HAVE_PCL
+#include <pcl/common/common_headers.h>
+
+namespace ftl {
+namespace utility {
+
+/**
+ * Convert an OpenCV point cloud matrix and RGB image to a PCL XYZRGB point cloud.
+ */
+pcl::PointCloud<pcl::PointXYZRGB>::Ptr matToPointXYZ(const cv::Mat &cvcloud, const cv::Mat &rgbimage);
+
+};
+};
+#endif  // HAVE_PCL
+
+#endif  // _FTL_COMMON_OPENCV_TO_PCL_HPP_
diff --git a/common/cpp/src/configuration.cpp b/common/cpp/src/configuration.cpp
index d42d487e9cbb7db6d3351d57a74d42c1342dc4dd..a2674dcb08d101b73a4835dde91d31b97f6865e8 100644
--- a/common/cpp/src/configuration.cpp
+++ b/common/cpp/src/configuration.cpp
@@ -129,20 +129,36 @@ static bool mergeConfig(const string &path) {
  */
 static bool findConfiguration(const string &file, const vector<string> &paths,
 		const std::string &app) {
+	bool f = false;
 	bool found = false;
 	
-	found |= mergeConfig(FTL_GLOBAL_CONFIG_ROOT "/config.json");
-	found |= mergeConfig(FTL_LOCAL_CONFIG_ROOT "/config.json");
-	found |= mergeConfig("./config.json");
+	f = mergeConfig(FTL_GLOBAL_CONFIG_ROOT "/config.json");
+	found |= f;
+	if (f) LOG(INFO) << "Loaded config: " << FTL_GLOBAL_CONFIG_ROOT "/config.json";
+	f = mergeConfig(FTL_LOCAL_CONFIG_ROOT "/config.json");
+	found |= f;
+	if (f) LOG(INFO) << "Loaded config: " << FTL_LOCAL_CONFIG_ROOT "/config.json";
+	f = mergeConfig("./config.json");
+	found |= f;
+	if (f) LOG(INFO) << "Loaded config: " << "./config.json";
 	
 	for (auto p : paths) {
 		if (is_directory(p)) {
-			found |= mergeConfig(p+"/config.json");
+			f = mergeConfig(p+"/config.json");
+			found |= f;
+			if (f) LOG(INFO) << "Loaded config: " << p << "/config.json";
 		}
 	}
 	
 	if (file != "") {
-		found |= mergeConfig(file);
+		f = mergeConfig(file);
+		found |= f;
+
+		if (!f) {
+			LOG(ERROR) << "Specific config file (" << file << ") was not found";
+		} else {
+			LOG(INFO) << "Loaded config: " << file;
+		}
 	}
 
 	if (found) {
diff --git a/common/cpp/src/opencv_to_pcl.cpp b/common/cpp/src/opencv_to_pcl.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7fb1eabe0e762677b46df8a91e029f7867e8dd1a
--- /dev/null
+++ b/common/cpp/src/opencv_to_pcl.cpp
@@ -0,0 +1,35 @@
+#include <ftl/utility/opencv_to_pcl.hpp>
+
+#if defined HAVE_PCL
+pcl::PointCloud<pcl::PointXYZRGB>::Ptr ftl::utility::matToPointXYZ(const cv::Mat &cvcloud, const cv::Mat &rgbimage) {
+	pcl::PointCloud<pcl::PointXYZRGB>::Ptr point_cloud_ptr(new pcl::PointCloud<pcl::PointXYZRGB>);
+	point_cloud_ptr->width = cvcloud.cols * cvcloud.rows;
+	point_cloud_ptr->height = 1;
+
+	for(int i=0;i<cvcloud.rows;i++) {
+	for(int j=0;j<cvcloud.cols;j++) {
+		//std::cout<<i<<endl;
+
+		pcl::PointXYZRGB point;
+		cv::Vec3f cvpoint = cvcloud.at<cv::Vec3f>(i,j);
+		point.x = cvpoint[0];
+		point.y = cvpoint[1];
+		point.z = cvpoint[2];
+
+		if (point.x == INFINITY || point.y == INFINITY || point.z == INFINITY || point.z < 600.0f) {
+			point.x = 0; point.y = 0; point.z = 0;
+		}
+
+		cv::Point3_<uchar> prgb = rgbimage.at<cv::Point3_<uchar>>(i, j);
+
+		// when color needs to be added:
+		uint32_t rgb = (static_cast<uint32_t>(prgb.z) << 16 | static_cast<uint32_t>(prgb.y) << 8 | static_cast<uint32_t>(prgb.x));
+		point.rgb = *reinterpret_cast<float*>(&rgb);
+
+		point_cloud_ptr -> points.push_back(point);
+	}
+	}
+
+	return point_cloud_ptr;
+}
+#endif  // HAVE_PCL
\ No newline at end of file
diff --git a/components/rgbd-sources/src/calibrate.cpp b/components/rgbd-sources/src/calibrate.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..becacbf57317bc96a19d2d4904f0107160ff3324
--- /dev/null
+++ b/components/rgbd-sources/src/calibrate.cpp
@@ -0,0 +1,614 @@
+/*
+ * Copyright 2019 Nicolas Pope
+ */
+
+#include <glog/logging.h>
+#include <ftl/config.h>
+#include <ftl/configuration.hpp>
+
+#include <iostream>
+#include <sstream>
+#include <string>
+#include <ctime>
+#include <cstdio>
+
+#include "calibrate.hpp"
+
+#include <opencv2/core.hpp>
+#include <opencv2/core/utility.hpp>
+#include <opencv2/imgproc.hpp>
+#include <opencv2/calib3d.hpp>
+#include <opencv2/imgcodecs.hpp>
+#include <opencv2/videoio.hpp>
+#include <opencv2/highgui.hpp>
+
+using ftl::Calibrate;
+using cv::FileStorage;
+using cv::CALIB_FIX_PRINCIPAL_POINT;
+using cv::CALIB_ZERO_TANGENT_DIST;
+using cv::CALIB_FIX_ASPECT_RATIO;
+using cv::CALIB_FIX_K1;
+using cv::CALIB_FIX_K2;
+using cv::CALIB_FIX_K3;
+using cv::CALIB_FIX_K4;
+using cv::CALIB_FIX_K5;
+using cv::CALIB_ZERO_DISPARITY;
+using cv::CALIB_USE_INTRINSIC_GUESS;
+using cv::CALIB_SAME_FOCAL_LENGTH;
+using cv::CALIB_RATIONAL_MODEL;
+using cv::CALIB_CB_ADAPTIVE_THRESH;
+using cv::CALIB_CB_NORMALIZE_IMAGE;
+using cv::CALIB_CB_FAST_CHECK;
+using cv::CALIB_USE_LU;
+using cv::NORM_L2;
+using cv::INTER_LINEAR;
+using cv::COLOR_BGR2GRAY;
+using cv::FileNode;
+using cv::FileNodeIterator;
+using cv::Mat;
+using cv::Size;
+using cv::Point2f;
+using cv::Point3f;
+using cv::Matx33d;
+using cv::Scalar;
+using cv::Rect;
+using cv::TermCriteria;
+using cv::waitKey;
+using std::string;
+using std::vector;
+
+void Calibrate::Settings::write(FileStorage& fs) const {
+    fs << "{"
+		<< "BoardSize_Width"  << boardSize.width
+		<< "BoardSize_Height" << boardSize.height
+		<< "Square_Size"         << squareSize
+		<< "Calibrate_Pattern" << patternToUse
+		<< "Calibrate_NrOfFrameToUse" << nrFrames
+		<< "Calibrate_FixAspectRatio" << aspectRatio
+		<< "Calibrate_AssumeZeroTangentialDistortion" << calibZeroTangentDist
+		<< "Calibrate_FixPrincipalPointAtTheCenter" << calibFixPrincipalPoint
+		<< "Write_DetectedFeaturePoints" << writePoints
+		<< "Write_extrinsicParameters"   << writeExtrinsics
+		<< "Write_gridPoints" << writeGrid
+		<< "Input_FlipAroundHorizontalAxis" << flipVertical
+		<< "Input_Delay" << delay
+		<< "}";
+}
+
+void Calibrate::Settings::read(const nlohmann::json& node) {
+    boardSize.width = node["board_size"][0];
+    boardSize.height = node["board_size"][1];
+    squareSize = node["square_size"];
+    nrFrames = node["num_frames"];
+    aspectRatio = node["fix_aspect_ratio"];
+    calibZeroTangentDist = node["assume_zero_tangential_distortion"];
+    calibFixPrincipalPoint = node["fix_principal_point_at_center"];
+    useFisheye =  node["use_fisheye_model"];
+    flipVertical = node["flip_vertical"];
+    delay = node["frame_delay"];
+    fixK1 = node["fix_k1"];
+    fixK2 = node["fix_k2"];
+    fixK3 = node["fix_k3"];
+    fixK4 = node["fix_k4"];
+    fixK5 = node["fix_k5"];
+
+    validate();
+}
+
+void Calibrate::Settings::validate() {
+    goodInput = true;
+    if (boardSize.width <= 0 || boardSize.height <= 0) {
+        LOG(ERROR) << "Invalid Board size: " << boardSize.width << " " <<
+        		boardSize.height;
+        goodInput = false;
+    }
+
+    if (squareSize <= 10e-6) {
+        LOG(ERROR) << "Invalid square size " << squareSize;
+        goodInput = false;
+    }
+
+    if (nrFrames <= 0) {
+        LOG(ERROR) << "Invalid number of frames " << nrFrames;
+        goodInput = false;
+    }
+
+    flag = 0;
+    if (calibFixPrincipalPoint) flag |= CALIB_FIX_PRINCIPAL_POINT;
+    if (calibZeroTangentDist)   flag |= CALIB_ZERO_TANGENT_DIST;
+    if (aspectRatio)            flag |= CALIB_FIX_ASPECT_RATIO;
+    if (fixK1)                  flag |= CALIB_FIX_K1;
+    if (fixK2)                  flag |= CALIB_FIX_K2;
+    if (fixK3)                  flag |= CALIB_FIX_K3;
+    if (fixK4)                  flag |= CALIB_FIX_K4;
+    if (fixK5)                  flag |= CALIB_FIX_K5;
+
+    if (useFisheye) {
+        // the fisheye model has its own enum, so overwrite the flags
+        flag = cv::fisheye::CALIB_FIX_SKEW |
+        		cv::fisheye::CALIB_RECOMPUTE_EXTRINSIC;
+        if (fixK1)                   flag |= cv::fisheye::CALIB_FIX_K1;
+        if (fixK2)                   flag |= cv::fisheye::CALIB_FIX_K2;
+        if (fixK3)                   flag |= cv::fisheye::CALIB_FIX_K3;
+        if (fixK4)                   flag |= cv::fisheye::CALIB_FIX_K4;
+        if (calibFixPrincipalPoint)
+        	flag |= cv::fisheye::CALIB_FIX_PRINCIPAL_POINT;
+    }
+
+    atImageList = 0;
+}
+
+Mat Calibrate::_nextImage(size_t cam) {
+    Mat result;
+    if (cam == 0) {
+    	local_->left(result);
+    } else if (cam == 1 && local_->isStereo()) {
+    	local_->right(result);
+    }
+    return result;
+}
+
+bool Calibrate::Settings::readStringList(const string& filename,
+		vector<string>& l) {
+    l.clear();
+    FileStorage fs(filename, FileStorage::READ);
+    if ( !fs.isOpened() )
+        return false;
+    FileNode n = fs.getFirstTopLevelNode();
+    if ( n.type() != FileNode::SEQ )
+        return false;
+    FileNodeIterator it = n.begin(), it_end = n.end();
+    for ( ; it != it_end; ++it )
+        l.push_back((string)*it);
+    return true;
+}
+
+bool Calibrate::Settings::isListOfImages(const string& filename) {
+    string s(filename);
+    // Look for file extension
+    if ( s.find(".xml") == string::npos && s.find(".yaml") == string::npos &&
+    		s.find(".yml") == string::npos )
+        return false;
+    else
+        return true;
+}
+
+enum { DETECTION = 0, CAPTURING = 1, CALIBRATED = 2 };
+
+bool runCalibration(const Calibrate::Settings& s, Size imageSize,
+		Mat&  cameraMatrix, Mat& distCoeffs,
+		vector<vector<Point2f> > imagePoints, float grid_width,
+		bool release_object);
+
+
+Calibrate::Calibrate(ftl::LocalSource *s, nlohmann::json &config) : local_(s) {
+    /*FileStorage fs(cal, FileStorage::READ); // Read the settings
+    if (!fs.isOpened())
+    {
+        LOG(ERROR) << "Could not open the configuration file: \"" << cal << "\"";
+        return;
+    }*/
+    // fs["Settings"] >> settings_;
+    settings_.read(config);
+    // fs.release();
+
+    if (!settings_.goodInput) {
+        LOG(ERROR) << "Invalid input detected. Application stopping.";
+        return;
+    }
+
+    map1_.resize(2);
+    map2_.resize(2);
+
+    calibrated_ = _loadCalibration();
+
+    if (calibrated_) {
+    	LOG(INFO) << "Calibration loaded from file";
+    }
+}
+
+bool Calibrate::_loadCalibration() {
+	// Capture one frame to get size;
+	Mat l, r;
+	local_->get(l, r);
+	Size img_size = l.size();
+	float scale = 1.0f;
+
+    Rect roi1, roi2;
+    FileStorage fs;
+
+    // reading intrinsic parameters
+    auto ifile = ftl::locateFile("intrinsics.yml");
+    if (ifile) {
+		fs.open((*ifile).c_str(), FileStorage::READ);
+		if (!fs.isOpened()) {
+		    LOG(WARNING) << "Could not open intrinsics file";
+		    return false;
+		}
+		
+		LOG(INFO) << "Intrinsics from: " << *ifile;
+    } else {
+    	LOG(WARNING) << "Calibration intrinsics file not found";
+		return false;
+    }
+
+    Mat M1, D1, M2, D2;
+    fs["M1"] >> M1;
+    fs["D1"] >> D1;
+    fs["M2"] >> M2;
+    fs["D2"] >> D2;
+
+    M1 *= scale;
+    M2 *= scale;
+
+	auto efile = ftl::locateFile("extrinsics.yml");
+	if (efile) {
+		fs.open((*efile).c_str(), FileStorage::READ);
+		if (!fs.isOpened()) {
+		    LOG(WARNING) << "Could not open extrinsics file";
+		    return false;
+		}
+		
+		LOG(INFO) << "Extrinsics from: " << *efile;
+    } else {
+    	LOG(WARNING) << "Calibration extrinsics file not found";
+		return false;
+    }
+
+    Mat R, T, R1, P1, R2, P2;
+    fs["R"] >> R;
+    fs["T"] >> T;
+    fs["R1"] >> R1;
+    fs["R2"] >> R2;
+    fs["P1"] >> P1;
+    fs["P2"] >> P2;
+    fs["Q"] >> Q_;
+
+    stereoRectify(M1, D1, M2, D2, img_size, R, T, R1, R2, P1, P2, Q_,
+    		CALIB_ZERO_DISPARITY, -1, img_size, &roi1, &roi2);
+
+    Mat map11, map12, map21, map22;
+    initUndistortRectifyMap(M1, D1, R1, P1, img_size, CV_16SC2,
+    		map1_[0], map2_[0]);
+    initUndistortRectifyMap(M2, D2, R2, P2, img_size, CV_16SC2,
+    		map1_[1], map2_[1]);
+    return true;
+}
+
+bool Calibrate::recalibrate() {
+	vector<vector<Point2f>> imagePoints[2];
+    Mat cameraMatrix[2], distCoeffs[2];
+    Size imageSize[2];
+
+	bool r = _recalibrate(imagePoints, cameraMatrix, distCoeffs, imageSize);
+
+	if (r) calibrated_ = true;
+
+	if (r && local_->isStereo()) {
+		int nimages = static_cast<int>(imagePoints[0].size());
+		auto squareSize = settings_.squareSize;
+		vector<vector<Point3f>> objectPoints;
+		objectPoints.resize(nimages);
+
+		for (auto i = 0; i < nimages; i++) {
+		    for (auto j = 0; j < settings_.boardSize.height; j++)
+		        for (auto  k = 0; k < settings_.boardSize.width; k++)
+		            objectPoints[i].push_back(Point3f(k*squareSize,
+		            		j*squareSize, 0));
+		}
+
+		Mat R, T, E, F;
+
+		LOG(INFO) << "Running stereo calibration...";
+
+		double rms = stereoCalibrate(objectPoints, imagePoints[0], imagePoints[1],
+                cameraMatrix[0], distCoeffs[0],
+                cameraMatrix[1], distCoeffs[1],
+                imageSize[0], R, T, E, F,
+                CALIB_FIX_ASPECT_RATIO +
+                CALIB_ZERO_TANGENT_DIST +
+                CALIB_USE_INTRINSIC_GUESS +
+                CALIB_SAME_FOCAL_LENGTH +
+                CALIB_RATIONAL_MODEL +
+                CALIB_FIX_K3 + CALIB_FIX_K4 + CALIB_FIX_K5,
+                TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 100, 1e-5));
+		LOG(INFO) << "... done with RMS error=" << rms;
+
+		// save intrinsic parameters
+		FileStorage fs(FTL_LOCAL_CONFIG_ROOT "/intrinsics.yml", FileStorage::WRITE);
+		if (fs.isOpened()) {
+		    fs << "M1" << cameraMatrix[0] << "D1" << distCoeffs[0] <<
+		        "M2" << cameraMatrix[1] << "D2" << distCoeffs[1];
+		    fs.release();
+		} else {
+		    LOG(ERROR) << "Error: can not save the intrinsic parameters";
+		}
+
+		Mat R1, R2, P1, P2;
+		Rect validRoi[2];
+
+		stereoRectify(cameraMatrix[0], distCoeffs[0],
+		              cameraMatrix[1], distCoeffs[1],
+		              imageSize[0], R, T, R1, R2, P1, P2, Q_,
+		              CALIB_ZERO_DISPARITY, 1, imageSize[0],
+		              &validRoi[0], &validRoi[1]);
+
+		fs.open(FTL_LOCAL_CONFIG_ROOT "/extrinsics.yml", FileStorage::WRITE);
+		if (fs.isOpened()) {
+		    fs << "R" << R << "T" << T << "R1" << R1 << "R2" << R2 << "P1" <<
+		    		P1 << "P2" << P2 << "Q" << Q_;
+		    fs.release();
+		} else {
+		    LOG(ERROR) << "Error: can not save the extrinsic parameters";
+		}
+
+		// Precompute maps for cv::remap()
+    	initUndistortRectifyMap(cameraMatrix[0], distCoeffs[0], R1, P1,
+    			imageSize[0], CV_16SC2, map1_[0], map2_[0]);
+    	initUndistortRectifyMap(cameraMatrix[1], distCoeffs[1], R2, P2,
+    			imageSize[0], CV_16SC2, map1_[1], map2_[1]);
+
+	} else {
+		int cam = 0;
+		if (settings_.useFisheye) {
+		    Mat newCamMat;
+		    cv::fisheye::estimateNewCameraMatrixForUndistortRectify(
+		    		cameraMatrix[cam], distCoeffs[cam], imageSize[cam],
+		            Matx33d::eye(), newCamMat, 1);
+		    cv::fisheye::initUndistortRectifyMap(
+		    		cameraMatrix[cam], distCoeffs[cam], Matx33d::eye(),
+		    		newCamMat, imageSize[cam],
+		            CV_16SC2, map1_[cam], map2_[cam]);
+		} else {
+		    initUndistortRectifyMap(
+		        cameraMatrix[cam], distCoeffs[cam], Mat(),
+		        getOptimalNewCameraMatrix(cameraMatrix[cam], distCoeffs[cam],
+		        		imageSize[cam], 1, imageSize[cam], 0),
+		        imageSize[cam], CV_16SC2, map1_[cam], map2_[cam]);
+		}
+	}
+
+	return r;
+}
+
+bool Calibrate::_recalibrate(vector<vector<Point2f>> *imagePoints,
+		Mat *cameraMatrix, Mat *distCoeffs, Size *imageSize) {
+	int winSize = 11;  // parser.get<int>("winSize");
+
+    float grid_width = settings_.squareSize * (settings_.boardSize.width - 1);
+    bool release_object = false;
+    double prevTimestamp = 0.0;
+    const Scalar RED(0, 0, 255), GREEN(0, 255, 0);
+
+    int chessBoardFlags = CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE;
+
+    if (!settings_.useFisheye) {
+        // fast check erroneously fails with high distortions like fisheye
+        chessBoardFlags |= CALIB_CB_FAST_CHECK;
+    }
+
+    for (;;) {
+        Mat view[2];
+		local_->get(view[0], view[1]);
+        LOG(INFO) << "Grabbing calibration image...";
+
+        if (view[0].empty() || (local_->isStereo() && view[1].empty()) ||
+        		imagePoints[0].size() >= (size_t)settings_.nrFrames) {
+        	settings_.outputFileName = FTL_LOCAL_CONFIG_ROOT "/cam0.xml";
+        	bool r = runCalibration(settings_, imageSize[0],
+        					cameraMatrix[0], distCoeffs[0], imagePoints[0],
+        					grid_width, release_object);
+
+        	if (local_->isStereo()) {
+		    	settings_.outputFileName = FTL_LOCAL_CONFIG_ROOT "/cam1.xml";
+		    	r &= runCalibration(settings_, imageSize[1],
+		    					cameraMatrix[1], distCoeffs[1], imagePoints[1],
+		    					grid_width, release_object);
+        	}
+
+        	if (!r && view[0].empty()) {
+        		LOG(ERROR) << "Not enough frames to calibrate";
+        		return false;
+        	} else if (r) {
+        		LOG(INFO) << "Calibration successful";
+        		break;
+        	}
+        }
+
+        imageSize[0] = view[0].size();  // Format input image.
+        imageSize[1] = view[1].size();
+        // if( settings_.flipVertical )    flip( view[cam], view[cam], 0 );
+
+        vector<Point2f> pointBuf[2];
+        bool found1, found2;
+		found1 = findChessboardCorners(view[0], settings_.boardSize,
+				pointBuf[0], chessBoardFlags);
+		found2 = !local_->isStereo() || findChessboardCorners(view[1],
+				settings_.boardSize, pointBuf[1], chessBoardFlags);
+
+        if (found1 && found2 &&
+        		local_->getTimestamp()-prevTimestamp > settings_.delay) {
+			prevTimestamp = local_->getTimestamp();
+            // improve the found corners' coordinate accuracy for chessboard
+                Mat viewGray;
+                cvtColor(view[0], viewGray, COLOR_BGR2GRAY);
+                cornerSubPix(viewGray, pointBuf[0], Size(winSize, winSize),
+                    Size(-1, -1), TermCriteria(
+                    	TermCriteria::EPS+TermCriteria::COUNT, 30, 0.0001));
+                imagePoints[0].push_back(pointBuf[0]);
+
+                if (local_->isStereo()) {
+                    cvtColor(view[1], viewGray, COLOR_BGR2GRAY);
+                    cornerSubPix(viewGray, pointBuf[1], Size(winSize, winSize),
+                        Size(-1, -1), TermCriteria(
+                        	TermCriteria::EPS+TermCriteria::COUNT, 30, 0.0001));
+					imagePoints[1].push_back(pointBuf[1]);
+                }
+
+                // Draw the corners.
+                drawChessboardCorners(view[0], settings_.boardSize,
+                		Mat(pointBuf[0]), found1);
+                if (local_->isStereo()) drawChessboardCorners(view[1],
+                		settings_.boardSize, Mat(pointBuf[1]), found2);
+        } else {
+        	LOG(WARNING) << "No calibration pattern found";
+        }
+
+        imshow("Left", view[0]);
+        if (local_->isStereo()) imshow("Right", view[1]);
+        char key = static_cast<char>(waitKey(50));
+
+        if (key  == 27)
+            break;
+    }
+
+	return true;
+}
+
+bool Calibrate::undistort(cv::Mat &l, cv::Mat &r) {
+	local_->get(l, r);
+	if (!calibrated_) return false;
+	if (l.empty()) return false;
+	remap(l, l, map1_[0], map2_[0], INTER_LINEAR);
+	if (local_->isStereo()) remap(r, r, map1_[1], map2_[1], INTER_LINEAR);
+	return true;
+}
+
+bool Calibrate::rectified(cv::Mat &l, cv::Mat &r) {
+	return undistort(l, r);
+}
+
+bool Calibrate::isCalibrated() {
+	return calibrated_;
+}
+
+//! [compute_errors]
+static double computeReprojectionErrors(
+		const vector<vector<Point3f> >& objectPoints,
+		const vector<vector<Point2f> >& imagePoints,
+		const vector<Mat>& rvecs, const vector<Mat>& tvecs,
+		const Mat& cameraMatrix , const Mat& distCoeffs,
+		vector<float>& perViewErrors, bool fisheye) {
+    vector<Point2f> imagePoints2;
+    size_t totalPoints = 0;
+    double totalErr = 0;
+    perViewErrors.resize(objectPoints.size());
+
+    for (size_t i = 0; i < objectPoints.size(); ++i) {
+        if (fisheye) {
+            cv::fisheye::projectPoints(objectPoints[i], imagePoints2, rvecs[i],
+            		tvecs[i], cameraMatrix, distCoeffs);
+        } else {
+            projectPoints(objectPoints[i], rvecs[i], tvecs[i], cameraMatrix,
+            		distCoeffs, imagePoints2);
+        }
+        double err = norm(imagePoints[i], imagePoints2, NORM_L2);
+
+        size_t n = objectPoints[i].size();
+        perViewErrors[i] = static_cast<float>(std::sqrt(err*err/n));
+        totalErr        += err*err;
+        totalPoints     += n;
+    }
+
+    return std::sqrt(totalErr/totalPoints);
+}
+//! [compute_errors]
+//! [board_corners]
+static void calcBoardCornerPositions(Size boardSize, float squareSize,
+		vector<Point3f>& corners, Calibrate::Settings::Pattern patternType) {
+    corners.clear();
+
+    switch (patternType) {
+    case Calibrate::Settings::CHESSBOARD:
+    case Calibrate::Settings::CIRCLES_GRID:
+        for (int i = 0; i < boardSize.height; ++i)
+            for (int j = 0; j < boardSize.width; ++j)
+                corners.push_back(Point3f(j*squareSize, i*squareSize, 0));
+        break;
+
+    case Calibrate::Settings::ASYMMETRIC_CIRCLES_GRID:
+        for (int i = 0; i < boardSize.height; i++)
+            for (int j = 0; j < boardSize.width; j++)
+                corners.push_back(Point3f((2*j + i % 2)*squareSize,
+                		i*squareSize, 0));
+        break;
+    default:
+        break;
+    }
+}
+//! [board_corners]
+static bool _runCalibration(const Calibrate::Settings& s, Size& imageSize,
+		Mat& cameraMatrix, Mat& distCoeffs,
+		vector<vector<Point2f> > imagePoints, vector<Mat>& rvecs,
+		vector<Mat>& tvecs, vector<float>& reprojErrs,  double& totalAvgErr,
+		vector<Point3f>& newObjPoints, float grid_width, bool release_object) {
+    cameraMatrix = Mat::eye(3, 3, CV_64F);
+    if (s.flag & CALIB_FIX_ASPECT_RATIO)
+        cameraMatrix.at<double>(0, 0) = s.aspectRatio;
+
+    if (s.useFisheye) {
+        distCoeffs = Mat::zeros(4, 1, CV_64F);
+    } else {
+        distCoeffs = Mat::zeros(8, 1, CV_64F);
+    }
+
+    vector<vector<Point3f> > objectPoints(1);
+    calcBoardCornerPositions(s.boardSize, s.squareSize, objectPoints[0],
+    		Calibrate::Settings::CHESSBOARD);
+    objectPoints[0][s.boardSize.width - 1].x = objectPoints[0][0].x +
+    		grid_width;
+    newObjPoints = objectPoints[0];
+
+    objectPoints.resize(imagePoints.size(), objectPoints[0]);
+
+    // Find intrinsic and extrinsic camera parameters
+    double rms;
+
+    if (s.useFisheye) {
+        Mat _rvecs, _tvecs;
+        rms = cv::fisheye::calibrate(objectPoints, imagePoints, imageSize,
+        		cameraMatrix, distCoeffs, _rvecs, _tvecs, s.flag);
+
+        rvecs.reserve(_rvecs.rows);
+        tvecs.reserve(_tvecs.rows);
+        for (int i = 0; i < static_cast<int>(objectPoints.size()); i++) {
+            rvecs.push_back(_rvecs.row(i));
+            tvecs.push_back(_tvecs.row(i));
+        }
+    } else {
+        rms = calibrateCamera(objectPoints, imagePoints, imageSize,
+                                cameraMatrix, distCoeffs, rvecs, tvecs,
+                                s.flag | CALIB_USE_LU);
+    }
+
+    LOG(INFO) << "Re-projection error reported by calibrateCamera: "<< rms;
+
+    bool ok = checkRange(cameraMatrix) && checkRange(distCoeffs);
+
+    objectPoints.clear();
+    objectPoints.resize(imagePoints.size(), newObjPoints);
+    totalAvgErr = computeReprojectionErrors(objectPoints, imagePoints, rvecs,
+    		tvecs, cameraMatrix, distCoeffs, reprojErrs, s.useFisheye);
+
+    return ok;
+}
+
+//! [run_and_save]
+bool runCalibration(const Calibrate::Settings& s, Size imageSize,
+		Mat& cameraMatrix, Mat& distCoeffs,
+		vector<vector<Point2f> > imagePoints, float grid_width,
+		bool release_object) {
+    vector<Mat> rvecs, tvecs;
+    vector<float> reprojErrs;
+    double totalAvgErr = 0;
+    vector<Point3f> newObjPoints;
+
+    bool ok = _runCalibration(s, imageSize, cameraMatrix, distCoeffs,
+    		imagePoints, rvecs, tvecs, reprojErrs, totalAvgErr, newObjPoints,
+    		grid_width, release_object);
+    LOG(INFO) << (ok ? "Calibration succeeded" : "Calibration failed")
+         << ". avg re projection error = " << totalAvgErr;
+
+    return ok;
+}
+//! [run_and_save]
diff --git a/net/cpp/include/ftl/net/peer.hpp b/net/cpp/include/ftl/net/peer.hpp
index da30ee52011406d2ee8c3550aae6999c003ec718..bf0aae4c9c937d02aef8566a1cc365e373b9c416 100644
--- a/net/cpp/include/ftl/net/peer.hpp
+++ b/net/cpp/include/ftl/net/peer.hpp
@@ -86,6 +86,11 @@ class Peer {
 	bool isConnected() const {
 		return sock_ != INVALID_SOCKET && status_ == kConnected;
 	};
+
+	/**
+	 * Block until the connection and handshake has completed.
+	 */
+	bool waitConnection();
 	
 	bool isValid() const {
 		return status_ != kInvalid && sock_ != INVALID_SOCKET;
@@ -298,7 +303,7 @@ int Peer::asyncCall(
 	
 	// Register the CB
 	callbacks_[rpcid] = std::make_unique<caller<T>>(cb);
-	
+
 	_send();
 	return rpcid;
 }
diff --git a/net/cpp/include/ftl/net/universe.hpp b/net/cpp/include/ftl/net/universe.hpp
index b29dc10ac5f2c39964ba43b54ba8cfa3bbe9e0a4..c8b10b7651a9cc9cf64ec8237156b0fcf0d0596b 100644
--- a/net/cpp/include/ftl/net/universe.hpp
+++ b/net/cpp/include/ftl/net/universe.hpp
@@ -56,9 +56,11 @@ class Universe {
 	 *
 	 * @param addr URI giving protocol, interface and port
 	 */
-	bool connect(const std::string &addr);
+	Peer *connect(const std::string &addr);
 	
 	int numberOfPeers() const { return peers_.size(); }
+
+	int waitConnections();
 	
 	Peer *getPeer(const ftl::UUID &pid) const;
 	
@@ -126,6 +128,8 @@ class Universe {
 	bool createResource(const std::string &uri);
 
 	std::optional<ftl::UUID> findOwner(const std::string &res);
+
+	void setLocalID(const ftl::UUID &u) { this_peer = u; };
 	
 	private:
 	void _run();
@@ -138,6 +142,7 @@ class Universe {
 	
 	private:
 	bool active_;
+	ftl::UUID this_peer;
 	nlohmann::json config_;
 	std::mutex net_mutex_;
 	fd_set sfderror_;
@@ -199,7 +204,7 @@ std::optional<R> Universe::findOne(const std::string &name, ARGS... args) {
 
 	std::map<Peer*, int> record;
 	for (auto p : peers_) {
-		record[p] = p->asyncCall<std::optional<R>>(name, handler, std::forward<ARGS>(args)...);
+		record[p] = p->asyncCall<std::optional<R>>(name, handler, args...);
 	}
 	
 	{  // Block thread until async callback notifies us
diff --git a/net/cpp/src/dispatcher.cpp b/net/cpp/src/dispatcher.cpp
index 269db7a6d38de8be0ac8fe25d82b222db63f0c04..a2dfd5ad64ddc9f84b719f633fcd3ef3ab54d5f6 100644
--- a/net/cpp/src/dispatcher.cpp
+++ b/net/cpp/src/dispatcher.cpp
@@ -136,8 +136,8 @@ void ftl::net::Dispatcher::dispatch_notification(Peer &s, msgpack::object const
     if (binding) {
         try {
             auto result = (*binding)(args);
-        } catch (int e) {
-			throw e;
+        } catch (const int &e) {
+			throw &e;
 		}
     } else {
     	LOG(ERROR) << "Missing handler for incoming message";
diff --git a/net/cpp/src/peer.cpp b/net/cpp/src/peer.cpp
index 25a1721cb1af4efbb1341e2c43aeb28e61910f30..fb69ce699850bed6550098fdfae0e3a652ba3ae6 100644
--- a/net/cpp/src/peer.cpp
+++ b/net/cpp/src/peer.cpp
@@ -39,6 +39,7 @@
 #include <memory>
 #include <algorithm>
 #include <tuple>
+#include <chrono>
 
 using std::tuple;
 using std::get;
@@ -46,6 +47,7 @@ using ftl::net::Peer;
 using ftl::URI;
 using ftl::net::ws_connect;
 using ftl::net::Dispatcher;
+using std::chrono::seconds;
 
 /*static std::string hexStr(const std::string &s)
 {
@@ -178,7 +180,8 @@ Peer::Peer(const char *pUri, Dispatcher *d) : uri_(pUri) {
 	scheme_ = uri.getProtocol();
 	if (uri.getProtocol() == URI::SCHEME_TCP) {
 		sock_ = tcpConnect(uri);
-		status_ = kConnecting;
+		if (sock_ != INVALID_SOCKET) status_ = kConnecting;
+		else status_ = kReconnecting;
 	} else if (uri.getProtocol() == URI::SCHEME_WS) {
 		LOG(INFO) << "Websocket connect " << uri.getPath();
 		sock_ = tcpConnect(uri);
@@ -480,6 +483,20 @@ void Peer::_sendResponse(uint32_t id, const msgpack::object &res) {
 	_send();
 }
 
+bool Peer::waitConnection() {
+	if (status_ == kConnected) return true;
+	
+	std::unique_lock<std::mutex> lk(send_mtx_);
+	std::condition_variable cv;
+
+	onConnect([&](Peer &p) {
+		cv.notify_all();
+	});
+
+	cv.wait_for(lk, seconds(5));
+	return status_ == kConnected;
+}
+
 void Peer::onConnect(const std::function<void(Peer&)> &f) {
 	if (status_ == kConnected) {
 		f(*this);
diff --git a/net/cpp/src/universe.cpp b/net/cpp/src/universe.cpp
index e2bb8d8caca3e4f693a86c1aa27d93ee83b53d2e..194b9d32687bf224fdefaf891b6dff90d4c03fa4 100644
--- a/net/cpp/src/universe.cpp
+++ b/net/cpp/src/universe.cpp
@@ -18,12 +18,12 @@ using std::optional;
 using std::unique_lock;
 using std::mutex;
 
-Universe::Universe() : active_(true), thread_(Universe::__start, this) {
+Universe::Universe() : active_(true), this_peer(ftl::net::this_peer), thread_(Universe::__start, this) {
 	_installBindings();
 }
 
 Universe::Universe(nlohmann::json &config) :
-		active_(true), config_(config), thread_(Universe::__start, this) {
+		active_(true), this_peer(ftl::net::this_peer), config_(config), thread_(Universe::__start, this) {
 	if (config["listen"].is_array()) {
 		for (auto &l : config["listen"]) {
 			listen(l);
@@ -66,9 +66,9 @@ bool Universe::listen(const string &addr) {
 	return l->isListening();
 }
 
-bool Universe::connect(const string &addr) {
+Peer *Universe::connect(const string &addr) {
 	auto p = new Peer(addr.c_str(), &disp_);
-	if (!p) return false;
+	if (!p) return nullptr;
 	
 	if (p->status() != Peer::kInvalid) {
 		unique_lock<mutex> lk(net_mutex_);
@@ -81,7 +81,15 @@ bool Universe::connect(const string &addr) {
 		peer_ids_[p.id()] = &p;
 	});
 	
-	return p->status() == Peer::kConnecting;
+	return p;
+}
+
+int Universe::waitConnections() {
+	int count = 0;
+	for (auto p : peers_) {
+		if (p->waitConnection()) count++;
+	}
+	return count;
 }
 
 int Universe::_setDescriptors() {
@@ -133,7 +141,7 @@ void Universe::_installBindings() {
 	
 	bind("__owner__", [this](const std::string &res) -> optional<UUID> {
 		LOG(INFO) << "SOMEONE ASKS FOR " << res;
-		if (owned_.count(res) > 0) return ftl::net::this_peer;
+		if (owned_.count(res) > 0) return this_peer;
 		else return {};
 	});
 }
@@ -179,7 +187,7 @@ bool Universe::_subscribe(const std::string &res) {
 	optional<UUID> pid = findOwner(res);
 	
 	if (pid) {
-		return call<bool>(*pid, "__subscribe__", ftl::net::this_peer, res);
+		return call<bool>(*pid, "__subscribe__", this_peer, res);
 	} else {
 		// No resource found
 		LOG(WARNING) << "Subscribe to unknown resource: " << res;
@@ -219,7 +227,7 @@ void Universe::_run() {
 
 		//Some kind of error occured, it is usually possible to recover from this.
 		if (selres < 0) {
-			std::cout << "SELECT ERROR " << selres << std::endl;
+			std::cout << "SELECT ERROR " << selres << " - " << strerror(errno) << std::endl;
 			//return false;
 			continue;
 		} else if (selres == 0) {
diff --git a/net/cpp/test/net_integration.cpp b/net/cpp/test/net_integration.cpp
index aa698dbe8668fd7082cba84d26ca4f7b3713c2d7..3532c5b7f85fd1f49959a733119506ddf82b4b4e 100644
--- a/net/cpp/test/net_integration.cpp
+++ b/net/cpp/test/net_integration.cpp
@@ -17,27 +17,31 @@ TEST_CASE("Universe::connect()", "[net]") {
 	Universe b;
 	
 	a.listen("tcp://localhost:7077");
+	//sleep_for(milliseconds(100));
 
 	SECTION("valid tcp connection using ipv4") {
-		REQUIRE( b.connect("tcp://127.0.0.1:7077") );
+		auto p = b.connect("tcp://127.0.0.1:7077");
+		REQUIRE( p );
 		
-		sleep_for(milliseconds(100));
+		p->waitConnection();
 		
 		REQUIRE( a.numberOfPeers() == 1 );
 		REQUIRE( b.numberOfPeers() == 1 );
 	}
 
 	SECTION("valid tcp connection using hostname") {
-		REQUIRE( b.connect("tcp://localhost:7077") );
+		auto p = b.connect("tcp://localhost:7077");
+		REQUIRE( p );
 		
-		sleep_for(milliseconds(100));
+		p->waitConnection();
 		
 		REQUIRE( a.numberOfPeers() == 1 );
 		REQUIRE( b.numberOfPeers() == 1 );
 	}
 
 	SECTION("invalid protocol") {
-		REQUIRE( !b.connect("http://127.0.0.1:7077") );
+		auto p = b.connect("http://127.0.0.1:7077");
+		REQUIRE( !p->isValid() );
 		
 		sleep_for(milliseconds(100));
 		
@@ -87,11 +91,11 @@ TEST_CASE("Universe::broadcast()", "[net]") {
 		b.broadcast("done");
 		
 		sleep_for(milliseconds(100));
+		REQUIRE( !done );
 	}
 	
 	SECTION("no arguments to one peer") {
-		b.connect("tcp://localhost:7077");
-		while (a.numberOfPeers() == 0) sleep_for(milliseconds(20));
+		b.connect("tcp://localhost:7077")->waitConnection();
 		
 		bool done = false;
 		a.bind("hello", [&done]() {
@@ -106,8 +110,7 @@ TEST_CASE("Universe::broadcast()", "[net]") {
 	}
 	
 	SECTION("one argument to one peer") {
-		b.connect("tcp://localhost:7077");
-		while (a.numberOfPeers() == 0) sleep_for(milliseconds(20));
+		b.connect("tcp://localhost:7077")->waitConnection();
 		
 		int done = 0;
 		a.bind("hello", [&done](int v) {
@@ -124,9 +127,8 @@ TEST_CASE("Universe::broadcast()", "[net]") {
 	SECTION("one argument to two peers") {
 		Universe c;
 		
-		b.connect("tcp://localhost:7077");
-		c.connect("tcp://localhost:7077");
-		while (a.numberOfPeers() < 2) sleep_for(milliseconds(20));
+		b.connect("tcp://localhost:7077")->waitConnection();
+		c.connect("tcp://localhost:7077")->waitConnection();
 		
 		int done1 = 0;
 		b.bind("hello", [&done1](int v) {
@@ -151,8 +153,7 @@ TEST_CASE("Universe::findOwner()", "") {
 	Universe a;
 	Universe b;
 	a.listen("tcp://localhost:7077");
-	b.connect("tcp://localhost:7077");
-	while (a.numberOfPeers() == 0) sleep_for(milliseconds(20));
+	b.connect("tcp://localhost:7077")->waitConnection();
 
 	SECTION("no owners exist") {
 		REQUIRE( !b.findOwner("ftl://test") );
@@ -165,11 +166,22 @@ TEST_CASE("Universe::findOwner()", "") {
 	
 	SECTION("three peers and one owner") {
 		Universe c;
-		c.connect("tcp://localhost:7077");
-		while (a.numberOfPeers() < 2) sleep_for(milliseconds(20));
+		c.connect("tcp://localhost:7077")->waitConnection();
+		b.setLocalID(ftl::UUID(7));
 
 		b.createResource("ftl://test");
-		REQUIRE( *(a.findOwner("ftl://test")) == ftl::net::this_peer );
+		REQUIRE( *(a.findOwner("ftl://test")) == ftl::UUID(7) );
+	}
+
+	SECTION("three peers and one owner (2)") {
+		Universe c;
+		c.connect("tcp://localhost:7077")->waitConnection();
+		c.setLocalID(ftl::UUID(7));
+
+		c.createResource("ftl://test");
+		auto r = a.findOwner("ftl://test");
+		REQUIRE( r );
+		REQUIRE( *r == ftl::UUID(7) );
 	}
 }
 
@@ -177,8 +189,7 @@ TEST_CASE("Universe::subscribe()", "") {
 	Universe a;
 	Universe b;
 	a.listen("tcp://localhost:7077");
-	b.connect("tcp://localhost:7077");
-	while (a.numberOfPeers() == 0) sleep_for(milliseconds(20));
+	b.connect("tcp://localhost:7077")->waitConnection();
 
 	SECTION("no resource exists") {
 		REQUIRE( !b.subscribe("ftl://test", []() {}) );
@@ -196,8 +207,7 @@ TEST_CASE("Universe::publish()", "") {
 	Universe a;
 	Universe b;
 	a.listen("tcp://localhost:7077");
-	b.connect("tcp://localhost:7077");
-	while (a.numberOfPeers() == 0) sleep_for(milliseconds(20));
+	b.connect("tcp://localhost:7077")->waitConnection();
 
 	SECTION("no subscribers") {
 		a.createResource("ftl://test");
diff --git a/net/cpp/test/peer_unit.cpp b/net/cpp/test/peer_unit.cpp
index b7bebbcb1cf3b17db40d4dbc5250b792ec2164e2..583f615d4b9c77735342972116bb15a3bac461fb 100644
--- a/net/cpp/test/peer_unit.cpp
+++ b/net/cpp/test/peer_unit.cpp
@@ -185,7 +185,7 @@ TEST_CASE("Peer(int)", "[]") {
 		
 		sleep_for(milliseconds(50));
 		
-		REQUIRE( s.getFTLVersion() ==  (8 << 16) + (5 << 8) + 2 );
+		REQUIRE( (s.getFTLVersion() ==  (8 << 16) + (5 << 8) + 2) );
 	}
 	
 	SECTION("has correct peer id on full handshake") {
@@ -342,7 +342,7 @@ TEST_CASE("Peer::bind()", "[rpc]") {
 		sleep_for(milliseconds(50));
 		
 		REQUIRE( (done == 55) );
-		REQUIRE( readRPCReturn<int>(0) == 55 );
+		REQUIRE( (readRPCReturn<int>(0) == 55) );
 	}
 
 	SECTION("vector return value") {		
@@ -361,7 +361,7 @@ TEST_CASE("Peer::bind()", "[rpc]") {
 		REQUIRE( (done == 55) );
 
 		auto res = readRPCReturn<vector<int>>(0);
-		REQUIRE( res[1] == 45 );
+		REQUIRE( (res[1] == 45) );
 	}
 }
 
diff --git a/net/cpp/test/uri_unit.cpp b/net/cpp/test/uri_unit.cpp
index e11b320f4c18d599f64ff7361a2e91fcda6c5223..a9fde4e8080ef98732c15149e6bd824dd1b31723 100644
--- a/net/cpp/test/uri_unit.cpp
+++ b/net/cpp/test/uri_unit.cpp
@@ -21,8 +21,8 @@ SCENARIO( "URI() can parse valid URIs", "[utility]" ) {
 		REQUIRE( uri.isValid() );
 		REQUIRE( uri.getScheme() == URI::SCHEME_HTTP );
 		REQUIRE( uri.getHost() == "localhost" );
-		REQUIRE( uri.getPort() == 8080 );
-		REQUIRE (uri.getPath() == "/test/case.html" );
+		REQUIRE( (uri.getPort() == 8080) );
+		REQUIRE( uri.getPath() == "/test/case.html" );
 	}
 
 	GIVEN( "a valid scheme with path and query" ) {
@@ -85,7 +85,7 @@ SCENARIO( "URI::getAttribute() from query" ) {
 
 	GIVEN( "an integer value" ) {
 		URI uri("http://localhost:1000/hello?x=56");
-		REQUIRE( uri.getAttribute<int>("x") == 56 );
+		REQUIRE( (uri.getAttribute<int>("x") == 56) );
 	}
 }
 
diff --git a/reconstruct/CMakeLists.txt b/reconstruct/CMakeLists.txt
index 3d851d469c66f19ce084a3bf0bbbf1f390adc636..2bbdfa4976d21e4323847580347d7ba87e8d7318 100644
--- a/reconstruct/CMakeLists.txt
+++ b/reconstruct/CMakeLists.txt
@@ -2,9 +2,9 @@
 include_directories(${PROJECT_SOURCE_DIR}/reconstruct/include)
 #include_directories(${PROJECT_BINARY_DIR})
 
-
 set(REPSRC
 	src/main.cpp
+	src/registration.cpp
 )
 
 add_executable(ftl-reconstruct ${REPSRC})
diff --git a/reconstruct/include/ftl/registration.hpp b/reconstruct/include/ftl/registration.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..58893623e483152618af8eab88fc7d5bd0619769
--- /dev/null
+++ b/reconstruct/include/ftl/registration.hpp
@@ -0,0 +1,34 @@
+#ifndef _FTL_RECONSTRUCT_REGISTRATION_HPP_
+#define _FTL_RECONSTRUCT_REGISTRATION_HPP_
+
+#include <ftl/config.h>
+#include <opencv2/opencv.hpp>
+
+#ifdef HAVE_PCL
+
+#include <pcl/common/common_headers.h>
+#include <pcl/point_cloud.h>
+
+namespace ftl {
+namespace registration {
+
+/* Find transformation matrix for transforming clouds_source to clouds_target
+ * Assumes that corresponding points in clouds_source[i] and clouds_target[i] have same indices
+ */
+Eigen::Matrix4f findTransformation(	std::vector<pcl::PointCloud<pcl::PointXYZ>::Ptr> clouds_source,
+									std::vector<pcl::PointCloud<pcl::PointXYZ>::Ptr> clouds_target);
+
+
+/* Convert chessboard corners found with OpenCV's findChessboardCorners to PCL point cloud.
+ */
+pcl::PointCloud<pcl::PointXYZ>::Ptr cornersToPointCloud(const std::vector<cv::Point2f> &corners, const cv::Mat &disp, const cv::Mat &Q);
+
+/* Find chessboard corners from image.
+ */
+bool findChessboardCorners(cv::Mat &rgb, const cv::Mat &disp, const cv::Mat &Q, const cv::Size pattern_size, pcl::PointCloud<pcl::PointXYZ>::Ptr &out);
+
+};
+};
+
+#endif  // HAVE_PCL
+#endif  // _FTL_RECONSTRUCT_REGISTRATION_HPP_
diff --git a/reconstruct/src/main.cpp b/reconstruct/src/main.cpp
index 51c346da25e0f367247cc33234a63bbac957e251..9c9400e3a2ea05f3d88b1142de444ee3d4a071d4 100644
--- a/reconstruct/src/main.cpp
+++ b/reconstruct/src/main.cpp
@@ -7,6 +7,7 @@
 #include <glog/logging.h>
 #include <ftl/config.h>
 #include <ftl/configuration.hpp>
+
 // #include <zlib.h>
 // #include <lz4.h>
 
@@ -21,100 +22,372 @@
 #include <ftl/display.hpp>
 #include <nlohmann/json.hpp>
 
-#include "opencv2/imgproc.hpp"
-#include "opencv2/imgcodecs.hpp"
-#include "opencv2/highgui.hpp"
-#include "opencv2/core/utility.hpp"
+#include <opencv2/imgproc.hpp>
+#include <opencv2/imgcodecs.hpp>
+#include <opencv2/highgui.hpp>
+#include <opencv2/core/utility.hpp>
+
+#include <ftl/utility/opencv_to_pcl.hpp>
+#include <ftl/registration.hpp>
 
 #ifdef WIN32
 #pragma comment(lib, "Rpcrt4.lib")
 #endif
 
+#ifdef HAVE_PCL
+#include <pcl/point_cloud.h>
+#include <pcl/common/transforms.h>
+#include <pcl/filters/uniform_sampling.h>
+#endif
+
 using ftl::net::Universe;
 using ftl::Display;
 using ftl::config;
 using std::string;
 using std::vector;
-using cv::Mat;
+
 using json = nlohmann::json;
 using std::this_thread::sleep_for;
 using std::chrono::milliseconds;
 using std::mutex;
 using std::unique_lock;
 
-static void run() {
-	Universe net(config["net"]);
-	Mat rgb, depth, Q;
-	mutex m;
+using std::vector;
+
+#ifdef HAVE_PCL
+using pcl::PointCloud;
+using pcl::PointXYZ;
+using pcl::PointXYZRGB;
+#endif
+
+using cv::Mat;
+
+class InputStereo {
+private:
+	string uri_;
+	Mat rgb_, disp_;
+	Mat q_;
+	std::mutex m_;
+
+#ifdef HAVE_PCL
+	static PointCloud<PointXYZRGB>::Ptr _getPointCloud(Mat rgb, Mat disp, Mat q) {
+		cv::Mat_<cv::Vec3f> XYZ(disp.rows, disp.cols);
+		reprojectImageTo3D(disp, XYZ, q, false);
+		return ftl::utility::matToPointXYZ(XYZ, rgb);
+	}
+#endif
+
+public:
+	InputStereo(string uri, const Mat Q): uri_(uri), q_(Q) {};
 	
-	Display disp(config["display"]);
+	void set(Mat &rgb, Mat &disp) {
+		unique_lock<mutex> lk(m_);
+		rgb_ = rgb;
+		disp_ = disp;
+	}
 	
-	// Make sure connections are complete
-	sleep_for(milliseconds(500));
+	string getURI() { return uri_; }
+	Mat getQ() { return q_; }
+
+	/* thread unsafe, use lock() */
+#ifdef HAVE_PCL	
+	PointCloud<PointXYZRGB>::Ptr getPointCloud() { return _getPointCloud(rgb_, disp_, q_); }
+#endif
+	Mat getRGB() { return rgb_; }
+	Mat getDisparity() { return disp_; }
+	/* Mat getDepth() {} */
+	/* Mat getLeftRGB() {} */
+	/* Mat getRightRGB() {} */
+	unique_lock<mutex> lock() { return unique_lock<mutex>(m_); } // use recursive mutex instead (and move locking to methods)?
+};
 
-	// TODO(nick) Allow for many sources
-	net.subscribe(config["source"], [&rgb,&m,&depth](const vector<unsigned char> &jpg, const vector<unsigned char> &d) {
-		unique_lock<mutex> lk(m);
-		cv::imdecode(jpg, cv::IMREAD_COLOR, &rgb);
-		//LOG(INFO) << "Received JPG : " << rgb.cols;
+#ifdef HAVE_PCL
+#include <pcl/filters/uniform_sampling.h>
+
+/*
+class InputMerged {
+private:
+	// todo: Abstract input systems, can also use other 3D inputs (like InputMerged)
+	vector<InputStereo> inputs_;
+	vector<Eigen::Matrix4f> T_;
+	
+public:
+	PointCloud<PointXYZRGB>::Ptr getPointCloud() {
+		PointCloud<PointXYZRGB>::Ptr result(new PointCloud<PointXYZRGB>);
+		for (size_t i = 0; i < T_.size(); i++) {
+			inputs_[i].lock();
+			PointCloud<PointXYZRGB>::Ptr cloud = inputs_[i].getPointCloud();
+			// Documentation: Can be used with cloud_in equal to cloud_out 
+			pcl::transformPointCloud(*cloud, *cloud, T_[i]);
+			*result += *cloud;
+		}
 		
-		depth = Mat(rgb.size(), CV_16UC1);
+		PointCloud<PointXYZRGB>::Ptr result_sampled(new PointCloud<PointXYZRGB>);
+		pcl::UniformSampling<PointXYZRGB> uniform_sampling;
+		uniform_sampling.setInputCloud(result);
+		uniform_sampling.setRadiusSearch(0.1f); // todo parametrize
+		uniform_sampling.filter(*result_sampled);
 		
-		/*z_stream infstream;
-		infstream.zalloc = Z_NULL;
-		infstream.zfree = Z_NULL;
-		infstream.opaque = Z_NULL;
-		// setup "b" as the input and "c" as the compressed output
-		infstream.avail_in = (uInt)d.size(); // size of input
-		infstream.next_in = (Bytef *)d.data(); // input char array
-		infstream.avail_out = (uInt)depth.step*depth.rows; // size of output
-		infstream.next_out = (Bytef *)depth.data; // output char array
-		 
-		// the actual DE-compression work.
-		inflateInit(&infstream);
-		inflate(&infstream, Z_NO_FLUSH);
-		inflateEnd(&infstream);*/
+		return result_sampled;
+	}
+};
+*/
+std::map<string, Eigen::Matrix4f> loadRegistration() {
+	std::map<string, Eigen::Matrix4f> registration;
+	std::ifstream file(string(FTL_LOCAL_CONFIG_ROOT) + "/registration.json");
+
+	// Use identity transform if no registration
+	if (!file.is_open()) {
+		Eigen::Matrix4f T;
+		registration["default"] = T.setIdentity();
+		return registration;
+	}
+
+	nlohmann::json load;
+	file >> load;
+	
+	for (auto it = load.begin(); it != load.end(); ++it) {
+		Eigen::Matrix4f m;
+		auto data = m.data();
 		
-		//LZ4_decompress_safe((char*)d.data(), (char*)depth.data, d.size(), depth.step*depth.rows);
+		for(size_t i = 0; i < 16; i++) {;
+			data[i] = it.value()[i];
+		}
 		
-		cv::imdecode(d, cv::IMREAD_UNCHANGED, &depth);
-		depth.convertTo(depth, CV_32FC1, 1.0f/16.0f);
-	});
+		registration[it.key()] = m;
+		
+	}
+	return registration;
+}
+
+void saveRegistration(std::map<string, Eigen::Matrix4f> registration) {
+	nlohmann::json save;
+	for (auto &item : registration) {
+		auto val = nlohmann::json::array();
+		for(size_t j = 0; j < 16; j++) { val.push_back((float) item.second.data()[j]); }
+		save[item.first] = val;
+	}
+
+	std::ofstream file(string(FTL_LOCAL_CONFIG_ROOT) + "/registration.json");
+	file << save;
+}
+
+template <template<class> class Container>
+std::map<string, Eigen::Matrix4f> runRegistration(Container<InputStereo> &inputs) {
+	
+	std::map<string, Eigen::Matrix4f> registration;
+	
+	// NOTE: uses config["registration"]
+	
+	if (!config["registration"].is_object()) {
+		LOG(FATAL) << "Configuration missing \"registration\" entry!";
+		return registration;
+	}
+	
+	int iter = (int) config["registration"]["calibration"]["iterations"];
+	int delay = (int) config["registration"]["calibration"]["delay"];
+	string ref_uri = (string) config["registration"]["reference-source"];
+	cv::Size pattern_size(	(int) config["registration"]["calibration"]["patternsize"][0],
+							(int) config["registration"]["calibration"]["patternsize"][1]);
+	
+	// config["registration"] done
+	
+	size_t ref_i;
+	bool found = false;
+	for (size_t i = 0; i < inputs.size(); i++) {
+		if (inputs[i].getURI() == ref_uri) {
+			ref_i = i;
+			found = true;
+			break;
+		}
+	}
+	
+	if (!found) { LOG(ERROR) << "Reference input not found!"; return registration; }
 	
-	while (disp.active()) {
-		Mat idepth;
+	for (auto &input : inputs) { 
+		cv::namedWindow("Registration: " + input.getURI(), cv::WINDOW_KEEPRATIO|cv::WINDOW_NORMAL);
+	}
+	
+	// vector for every input: vector of point clouds of patterns
+	vector<vector<PointCloud<PointXYZ>::Ptr>> patterns(inputs.size());
+	
+	while (iter > 0) {
+		bool retval = true; // set to false if pattern not found in one of the sources
+		vector<PointCloud<PointXYZ>::Ptr> patterns_iter(inputs.size());
 		
-		unique_lock<mutex> lk(m);
-		if (depth.cols > 0) {
-			// If no calibration data then get it from the remote machine
-			if (Q.rows == 0) {
-				// Must unlock before calling findOne to prevent net block!!
+		for (size_t i = 0; i < inputs.size(); i++) {
+			InputStereo &input = inputs[i];
+			Mat rgb, disp, Q;
+			
+			while(true) {
+				auto lk = input.lock();
+				rgb = input.getRGB();
+				disp = input.getDisparity();
+				Q = input.getQ();
 				lk.unlock();
-				auto buf = net.findOne<vector<unsigned char>>((string)config["source"]+"/calibration");
-				lk.lock();
-				if (buf) {
-					Q = Mat(cv::Size(4,4), CV_32F);
-					memcpy(Q.data, (*buf).data(), (*buf).size());
-					if (Q.step*Q.rows != (*buf).size()) LOG(ERROR) << "Corrupted calibration";
-					disp.setCalibration(Q);
-				}
+				
+				// todo solve this somewhere else
+				if ((rgb.cols > 0) && (disp.cols > 0)) { break; }
+				else { sleep_for(milliseconds(500)); }
 			}
+			
+			retval &= ftl::registration::findChessboardCorners(rgb, disp, Q, pattern_size, patterns_iter[i]);
+			
+			cv::imshow("Registration: " + input.getURI(), rgb);
+		}
+		cv::waitKey(delay);
+		
+		// every camera found the pattern
+		if (retval) {
+			for (size_t i = 0; i < patterns_iter.size(); i++) {
+				patterns[i].push_back(patterns_iter[i]);
+			}
+			iter--;
+		}
+		else { LOG(WARNING) << "Pattern not detected on all inputs";}
+	}
+	
+	for (auto &input : inputs) { cv::destroyWindow("Registration: " + input.getURI()); }
+	
+	for (size_t i = 0; i < inputs.size(); i++) {
+		Eigen::Matrix4f T;
+		if (i == ref_i) {
+			T.setIdentity();
+		}
+		else {
+			T = ftl::registration::findTransformation(patterns[i], patterns[ref_i]);
+		}
+		registration[inputs[i].getURI()] = T;
+	}
+	saveRegistration(registration);
+	return registration;
+}
+#endif
+
+bool getCalibration(Universe &net, string src, Mat &Q) {
+	Q = Mat(cv::Size(4,4), CV_32F);
+	while(true) {
+		auto buf = net.findOne<vector<unsigned char>>((string) src +"/calibration");
+		if (buf) {
+			memcpy(Q.data, (*buf).data(), (*buf).size());
+			
+			if (Q.step*Q.rows != (*buf).size()) {
+				LOG(ERROR) << "Corrupted calibration";
+				return false;
+			}
+			
+			return true;
+		}
+		else {
+			LOG(INFO) << "Could not get calibration, retrying";
+			sleep_for(milliseconds(500));
+		}
+	}
+}
+
+static void run() {
+	Universe net(config["net"]);
+	
+	// Make sure connections are complete
+	sleep_for(milliseconds(500));
+	
+	if (!config["sources"].is_array()) {
+		LOG(ERROR) << "No sources configured!";
+		return;
+	}
+	
+	std::deque<InputStereo> inputs;
+	std::vector<Display> displays;
+
+	// todo networking and initilization for input should possibly be implemented in their own class
+	//      with error handling etc.
+	//
+	
+	for (auto &src : config["sources"]) {
+		Mat Q;
+		if (!getCalibration(net, src, Q)) {	continue; } // skips input if calibration bad
+		
+		InputStereo &in = inputs.emplace_back(src, Q);
+		displays.emplace_back(config["display"], src);
+		
+		LOG(INFO) << src << " loaded";
+		
+		net.subscribe(src, [&in](const vector<unsigned char> &jpg, const vector<unsigned char> &d) {
+			Mat rgb, disp;
+			cv::imdecode(jpg, cv::IMREAD_COLOR, &rgb);
+			Mat(rgb.size(), CV_16UC1);
+			cv::imdecode(d, cv::IMREAD_UNCHANGED, &disp);
+			disp.convertTo(disp, CV_32FC1, 1.0f/16.0f);
+			in.set(rgb, disp);
+		});
+	}
+	
+	// Displays and Inputs configured
+	
+	// load point cloud transformations
+	
+#ifdef HAVE_PCL
+	std::map<string, Eigen::Matrix4f> registration;
+	if (config["registration"]["calibration"]["run"]) {
+		registration = runRegistration(inputs);
+	}
+	else {
+		registration = loadRegistration();
+	}
+	vector<Eigen::Matrix4f> T;
+	for (auto &input : inputs) {
+		Eigen::Matrix4f RT = (registration.count(input.getURI()) > 0) ? registration[input.getURI()] : registration["default"];
+		T.push_back(RT);
+	}
+	
+	//
+	vector<PointCloud<PointXYZRGB>::Ptr> clouds(inputs.size());
+	Display display_merged(config["display"], "Merged"); // todo
+
+	int active = displays.size();
+	while (active > 0) {
+		active = 0;
+
+		net.broadcast("grab");  // To sync cameras
+		
+		PointCloud<PointXYZRGB>::Ptr cloud(new PointCloud<PointXYZRGB>);
+		
+		for (size_t i = 0; i < inputs.size(); i++) {
+			Display &display = displays[i];
+			InputStereo &input = inputs[i];
+			
+			if (!display.active()) continue;
+			active += 1;
+			
+			auto lk = input.lock();
+			//Mat rgb = input.getRGB();
+			//Mat disparity = input.getDisparity();
+			clouds[i] = input.getPointCloud();
+			lk.unlock();
+			
+			display.render(clouds[i]);
+			//display.render(rgb, disparity);
+			display.wait(50);
 		}
 		
-		if (rgb.cols > 0) {
-			//cv::imshow("RGB", rgb);
-			disp.render(rgb,depth);
+		for (size_t i = 0; i < clouds.size(); i++) {
+			pcl::transformPointCloud(*clouds[i], *clouds[i], T[i]);
+			*cloud += *clouds[i];
 		}
 		
-		// TODO(nick) Use a swap buffer so this can be unlocked earlier
-		lk.unlock();
-		//if (cv::waitKey(40) == 27) break;
-		disp.wait(40);
+		pcl::UniformSampling<PointXYZRGB> uniform_sampling;
+		uniform_sampling.setInputCloud(cloud);
+		uniform_sampling.setRadiusSearch(0.1f);
+		uniform_sampling.filter(*cloud);
+		
+		display_merged.render(cloud);
+		display_merged.wait(50);
 	}
+#endif
+	// TODO non-PCL (?)
 }
 
 int main(int argc, char **argv) {
 	ftl::configure(argc, argv, "representation"); // TODO(nick) change to "reconstruction"
 	run();
 }
-
diff --git a/reconstruct/src/registration.cpp b/reconstruct/src/registration.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7020db090142332364769ea3c1d899f95c220d82
--- /dev/null
+++ b/reconstruct/src/registration.cpp
@@ -0,0 +1,168 @@
+#include <ftl/registration.hpp>
+
+#ifdef HAVE_PCL
+
+#include <glog/logging.h>
+#include <pcl/common/transforms.h>
+
+#include <pcl/registration/transformation_estimation_svd.h>
+#include <pcl/registration/transformation_estimation_svd_scale.h>
+
+#include <pcl/segmentation/sac_segmentation.h>
+#include <pcl/sample_consensus/method_types.h>
+#include <pcl/sample_consensus/model_types.h>
+#include <pcl/filters/project_inliers.h>
+#include <pcl/ModelCoefficients.h>
+
+#include <pcl/io/pcd_io.h>
+
+#include <pcl/registration/transformation_validation.h>
+#include <pcl/registration/transformation_validation_euclidean.h>
+
+//#include <pcl/registration/icp_nl.h>
+
+namespace ftl {
+namespace registration {
+
+using std::vector;
+using pcl::PointCloud;
+using pcl::PointXYZ;
+using pcl::PointXYZRGB;
+
+using cv::Mat;
+
+// todo template: fitPlane<typename T>(PointCloud<T> cloud_in, PointCloud<T> cloud_out)
+//
+// Fit calibration pattern into plane using RANSAC + project points
+//
+void fitPlane(PointCloud<PointXYZ>::Ptr cloud_in, PointCloud<PointXYZ>::Ptr cloud_out) {
+	// TODO: include pattern in model (find best alignment of found points and return transformed reference?)
+	
+	pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
+	pcl::PointIndices::Ptr inliers(new pcl::PointIndices);
+	
+	// Estimate plane with RANSAC
+	pcl::SACSegmentation<PointXYZ> seg;
+	
+	seg.setOptimizeCoefficients(true);
+	seg.setModelType(pcl::SACMODEL_PLANE);
+	seg.setMethodType(pcl::SAC_RANSAC);
+	seg.setDistanceThreshold(0.01);
+	seg.setInputCloud(cloud_in);
+	seg.segment(*inliers, *coefficients);
+	
+	// Project points into plane
+	pcl::ProjectInliers<PointXYZ> proj;
+	proj.setModelType(pcl::SACMODEL_PLANE);
+	proj.setInputCloud(cloud_in);
+	proj.setModelCoefficients(coefficients);
+	proj.filter(*cloud_out);
+}
+
+//template<typename T = PointXYZ> typename
+PointCloud<PointXYZ>::Ptr cornersToPointCloud(const vector<cv::Point2f> &corners, const Mat &disp, const Mat &Q) {
+	
+	cv::Matx44d Q_;
+	Q.convertTo(Q_, CV_64F);
+	
+	int corners_len = corners.size();
+	vector<cv::Vec3f> points(corners_len);
+	
+	// Output point cloud
+	PointCloud<PointXYZ>::Ptr cloud(new PointCloud<PointXYZ>);
+	cloud->width = corners_len;
+	cloud->height = 1;
+	
+	// Follows cv::reprojectImageTo3D(..)
+	// https://github.com/opencv/opencv/blob/371bba8f54560b374fbcd47e7e02f015ac4969ad/modules/calib3d/src/calibration.cpp#L2998
+	// documentation suggests using cv::perspectiveTransform(...) with sparse set of points
+	
+	for (int i = 0; i < corners_len; i++) {
+		double x = corners[i].x;
+		double y = corners[i].y;
+		double d = disp.at<float>((int) y, (int) x); // todo: better estimation
+		
+		cv::Vec4d homg_pt = Q_ * cv::Vec4d(x, y, d, 1.0);
+		cv::Vec3d p = cv::Vec3d(homg_pt.val) / homg_pt[3];
+		
+		// no check since disparities assumed to be good in the calibration pattern
+		// if (fabs(d-minDisparity) <= FLT_EPSILON)
+		
+		PointXYZ point;
+		point.x = p[0];
+		point.y = p[1];
+		point.z = p[2];
+		cloud->push_back(point);
+	}
+	
+	return cloud;
+}
+
+bool findChessboardCorners(Mat &rgb, const Mat &disp, const Mat &Q, const cv::Size pattern_size, PointCloud<PointXYZ>::Ptr &out) {
+	vector<cv::Point2f> corners(pattern_size.width * pattern_size.height);
+	bool retval = cv::findChessboardCorners(rgb, pattern_size, corners); // (todo) change to findChessboardCornersSB (OpenCV > 4)
+	// todo: verify that disparities are good
+	cv::drawChessboardCorners(rgb, pattern_size, Mat(corners), retval);
+	if (!retval) { return false; }
+	auto corners_cloud = cornersToPointCloud(corners, disp, Q);
+	if (out) { *out += *corners_cloud; } // if cloud is valid, add the points
+	else { out = corners_cloud; }
+	return true;
+}
+
+Eigen::Matrix4f findTransformation(vector<PointCloud<PointXYZ>::Ptr> clouds_source, vector<PointCloud<PointXYZ>::Ptr> clouds_target) {
+	size_t n_clouds = clouds_source.size();
+	
+	Eigen::Matrix4f T, T_tmp, T_new;
+	T.setIdentity();
+	
+	if ((clouds_source.size() != clouds_target.size()) || (n_clouds == 0)) {
+		LOG(ERROR) << "Input vectors have invalid sizes: clouds_source " << clouds_source.size()
+					<< ", clouds_target " << clouds_target.size() << ", transformation can not be estimated";
+		
+		return T; // identity
+	}
+	
+	// corresponding points have same indices (!!!)
+	int n_points = clouds_source[0]->width * clouds_source[0]->height;
+	vector<int> idx(n_points);
+	for (int i = 0; i < n_points; i++) { idx[i] = i; }
+	
+	pcl::registration::TransformationValidationEuclidean<PointXYZ, PointXYZ> validate;
+	pcl::registration::TransformationEstimationSVD<PointXYZ,PointXYZ> svd;
+	
+	double score_prev = std::numeric_limits<float>::max();
+	
+	for (size_t i = 0; i < n_clouds; ++i) {
+		PointCloud<PointXYZ> source;
+		PointCloud<PointXYZ> target = *clouds_target[i];
+		
+		pcl::transformPointCloud(*clouds_source[i], source, T);
+		svd.estimateRigidTransformation(source, idx, target, idx, T_new);
+		
+		// calculate new transformation
+		T_tmp = T_new * T;
+		
+		// score new transformation
+		double score = 0.0;
+		for (size_t j = 0; j < n_clouds; ++j) {
+			score += validate.validateTransformation(clouds_source[j], clouds_target[j], T);
+		}
+		score /= n_clouds;
+		
+		// if score doesn't improve, do not use as T, otherwise update T and score
+		if (score < score_prev) {
+			T = T_tmp;
+			score_prev = score;
+		}
+		
+		LOG(INFO) << "Validation score: " << score;
+	}
+	
+	return T;
+}
+
+} // namespace registration
+} // namespace ftl
+
+#endif // HAVE_PCL
\ No newline at end of file
diff --git a/renderer/cpp/CMakeLists.txt b/renderer/cpp/CMakeLists.txt
index d87223c42bb5637d83f2c8de25d04a86c3144a69..6ef0af73b9e9f21de44ab64c6e9ecbe30b6ee6a8 100644
--- a/renderer/cpp/CMakeLists.txt
+++ b/renderer/cpp/CMakeLists.txt
@@ -1,18 +1,15 @@
-# Need to include staged files and libs
-#include_directories(${PROJECT_SOURCE_DIR}/net/cpp/include)
-#include_directories(${PROJECT_BINARY_DIR})
-
-
 add_library(ftlrender
 	src/display.cpp
 )
 
+# These cause errors in CI build and are being removed from PCL in newer versions
+# target_compile_options(ftlrender PUBLIC ${PCL_DEFINITIONS})
+
 target_include_directories(ftlrender PUBLIC
+	${PCL_INCLUDE_DIRS}
 	$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
 	$<INSTALL_INTERFACE:include>
 	PRIVATE src)
-target_link_libraries(ftlrender Threads::Threads glog::glog ${OpenCV_LIBS})
-
-
+target_link_libraries(ftlrender ftlcommon Threads::Threads glog::glog ${OpenCV_LIBS} ${PCL_LIBRARIES})
 
 #ADD_SUBDIRECTORY(test)
diff --git a/renderer/cpp/include/ftl/display.hpp b/renderer/cpp/include/ftl/display.hpp
index c6d63ca4f8685c1c0cfa751c2fb87b5b10fb412e..04ff003de8b723f0af5d0475a1946fc6f7a4d302 100644
--- a/renderer/cpp/include/ftl/display.hpp
+++ b/renderer/cpp/include/ftl/display.hpp
@@ -11,31 +11,50 @@
 #include <opencv2/opencv.hpp>
 #include "opencv2/highgui.hpp"
 
+#if defined HAVE_PCL
+#include <pcl/common/common_headers.h>
+#include <pcl/visualization/pcl_visualizer.h>
+#endif  // HAVE_PCL
+
 namespace ftl {
 
 /**
  * Multiple local display options for disparity or point clouds.
  */
 class Display {
+	private:
+		std::string name_;
 	public:
-	explicit Display(nlohmann::json &config);
+	enum style_t {
+		STYLE_NORMAL, STYLE_DISPARITY, STYLE_DEPTH
+	};
+
+	public:
+	explicit Display(nlohmann::json &config, std::string name);
 	~Display();
 	
-	void setCalibration(const cv::Mat &q) { q_ = q; }
+	inline bool render(const cv::Mat &rgb, const cv::Mat &depth, const cv::Mat &q) { return render(rgb, cv::Mat(), q); }
+	bool render(const cv::Mat &rgb, const cv::Mat &rgbr, const cv::Mat &depth, const cv::Mat &q);
 
-	bool render(const cv::Mat &rgb, const cv::Mat &depth);
+#if defined HAVE_PCL
+	bool render(pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr);
+#endif  // HAVE_PCL
+	bool render(const cv::Mat &img, style_t s=STYLE_NORMAL);
 
 	bool active() const;
 	
 	void wait(int ms);
 
 	private:
-	cv::Mat q_;
 	nlohmann::json config_;
 
-	#if defined HAVE_VIZ
+#if defined HAVE_VIZ
 	cv::viz::Viz3d *window_;
-	#endif  // HAVE_VIZ
+#endif  // HAVE_VIZ
+
+#if defined HAVE_PCL
+	pcl::visualization::PCLVisualizer::Ptr pclviz_;
+#endif  // HAVE_PCL
 
 	bool active_;
 };
diff --git a/renderer/cpp/src/display.cpp b/renderer/cpp/src/display.cpp
index 7ee8efe3c7a5099656bd89bc680049a0e8949f82..4a64fbb85988d8584b023f4ff65e73b5e3f5fcda 100644
--- a/renderer/cpp/src/display.cpp
+++ b/renderer/cpp/src/display.cpp
@@ -5,16 +5,77 @@
 #include <glog/logging.h>
 
 #include <ftl/display.hpp>
+#include <ftl/utility/opencv_to_pcl.hpp>
 
 using ftl::Display;
 using cv::Mat;
 using cv::Vec3f;
 
-Display::Display(nlohmann::json &config) : config_(config) {
-	#if defined HAVE_VIZ
-	window_ = new cv::viz::Viz3d("FTL");
+Display::Display(nlohmann::json &config, std::string name) : config_(config) {
+	name_ = name;
+#if defined HAVE_VIZ
+	window_ = new cv::viz::Viz3d("FTL: " + name);
 	window_->setBackgroundColor(cv::viz::Color::white());
-	#endif  // HAVE_VIZ
+#endif  // HAVE_VIZ
+
+#if defined HAVE_PCL
+	pclviz_ = pcl::visualization::PCLVisualizer::Ptr(new pcl::visualization::PCLVisualizer ("FTL Cloud: " + name));
+	pclviz_->setBackgroundColor (255, 255, 255);
+	pclviz_->addCoordinateSystem (1.0);
+	pclviz_->setShowFPS(true);
+	pclviz_->initCameraParameters ();
+
+	pclviz_->registerPointPickingCallback(
+		[](const pcl::visualization::PointPickingEvent& event, void* viewer_void) {
+			if (event.getPointIndex () == -1) return;
+			float x, y, z;
+			event.getPoint(x, y, z);
+			LOG(INFO) << "( " << x << ", " << y << ", " << z << ")";
+		}, (void*) &pclviz_);
+	
+	pclviz_->registerKeyboardCallback (
+		[](const pcl::visualization::KeyboardEvent &event, void* viewer_void) {
+			auto viewer = *static_cast<pcl::visualization::PCLVisualizer::Ptr*>(viewer_void);
+			pcl::visualization::Camera cam;
+			viewer->getCameraParameters(cam);
+
+			Eigen::Vector3f pos(cam.pos[0], cam.pos[1], cam.pos[2]);
+			Eigen::Vector3f focal(cam.focal[0], cam.focal[1], cam.focal[2]);
+			Eigen::Vector3f dir = focal - pos; //.normalize();
+			dir.normalize();
+
+			const float speed = 40.0f;
+
+			if (event.getKeySym() == "Up") {
+				pos += speed*dir;
+				focal += speed*dir;
+			} else if (event.getKeySym() == "Down") {
+				pos -= speed*dir;
+				focal -= speed*dir;
+			} else if (event.getKeySym() == "Left") {
+				Eigen::Matrix3f m = Eigen::AngleAxisf(-0.5f*M_PI, Eigen::Vector3f::UnitY()).toRotationMatrix();
+				dir = m*dir;
+				pos += speed*dir;
+				focal += speed*dir;
+			} else if (event.getKeySym() == "Right") {
+				Eigen::Matrix3f m = Eigen::AngleAxisf(0.5f*M_PI, Eigen::Vector3f::UnitY()).toRotationMatrix();
+				dir = m*dir;
+				pos += speed*dir;
+				focal += speed*dir;
+			}
+
+
+			cam.pos[0] = pos[0];
+			cam.pos[1] = pos[1];
+			cam.pos[2] = pos[2];
+			cam.focal[0] = focal[0];
+			cam.focal[1] = focal[1];
+			cam.focal[2] = focal[2];
+			viewer->setCameraParameters(cam);
+
+		}, (void*)&pclviz_);
+#endif  // HAVE_PCL
+
 	active_ = true;
 }
 
@@ -24,15 +85,26 @@ Display::~Display() {
 	#endif  // HAVE_VIZ
 }
 
-bool Display::render(const cv::Mat &rgb, const cv::Mat &depth) {
+bool Display::render(const cv::Mat &rgb, const cv::Mat &rgbr, const cv::Mat &depth, const cv::Mat &q) {
 	Mat idepth;
 
-	if (config_["points"] && q_.rows != 0) {
-		#if defined HAVE_VIZ
+	if (config_["points"] && q.rows != 0) {
+#if defined HAVE_PCL
+		cv::Mat_<cv::Vec3f> XYZ(depth.rows, depth.cols);   // Output point cloud
+		reprojectImageTo3D(depth, XYZ, q, false);
+		auto pc = ftl::utility::matToPointXYZ(XYZ, rgb);
+
+		pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb(pc);
+		if (!pclviz_->updatePointCloud<pcl::PointXYZRGB> (pc, rgb, "reconstruction")) {
+			pclviz_->addPointCloud<pcl::PointXYZRGB> (pc, rgb, "reconstruction");
+			pclviz_->setCameraPosition(-878.0, -71.0, -2315.0, -0.1, -0.99, 0.068, 0.0, -1.0, 0.0);
+			pclviz_->setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, "reconstruction");
+		}
+#elif defined HAVE_VIZ
 		//cv::Mat Q_32F;
 		//calibrate_.getQ().convertTo(Q_32F, CV_32F);
 		cv::Mat_<cv::Vec3f> XYZ(depth.rows, depth.cols);   // Output point cloud
-		reprojectImageTo3D(depth+20.0f, XYZ, q_, true);
+		reprojectImageTo3D(depth+20.0f, XYZ, q, true);
 
 		// Remove all invalid pixels from point cloud
 		XYZ.setTo(Vec3f(NAN, NAN, NAN), depth == 0.0f);
@@ -45,15 +117,28 @@ bool Display::render(const cv::Mat &rgb, const cv::Mat &depth) {
 
 		//window_->spinOnce(40, true);
 
-		#else  // HAVE_VIZ
+#else  // HAVE_VIZ
 
 		LOG(ERROR) << "Need OpenCV Viz module to display points";
 
-		#endif  // HAVE_VIZ
+#endif  // HAVE_VIZ
 	}
 
 	if (config_["left"]) {
-		cv::imshow("RGB", rgb);
+		if (config_["crosshair"]) {
+			cv::line(rgb, cv::Point(0, rgb.rows/2), cv::Point(rgb.cols-1, rgb.rows/2), cv::Scalar(0,0,255), 1);
+            cv::line(rgb, cv::Point(rgb.cols/2, 0), cv::Point(rgb.cols/2, rgb.rows-1), cv::Scalar(0,0,255), 1);
+		}
+		cv::namedWindow("Left: " + name_, cv::WINDOW_KEEPRATIO);
+		cv::imshow("Left: " + name_, rgb);
+	}
+	if (config_["right"]) {
+		if (config_["crosshair"]) {
+			cv::line(rgbr, cv::Point(0, rgbr.rows/2), cv::Point(rgbr.cols-1, rgbr.rows/2), cv::Scalar(0,0,255), 1);
+            cv::line(rgbr, cv::Point(rgbr.cols/2, 0), cv::Point(rgbr.cols/2, rgbr.rows-1), cv::Scalar(0,0,255), 1);
+		}
+		cv::namedWindow("Right: " + name_, cv::WINDOW_KEEPRATIO);
+		cv::imshow("Right: " + name_, rgbr);
 	}
 
 	if (config_["depth"]) {
@@ -74,7 +159,7 @@ bool Display::render(const cv::Mat &rgb, const cv::Mat &depth) {
     	idepth.convertTo(idepth, CV_8U, 255.0f / 256.0f);  // TODO(nick)
 
     	applyColorMap(idepth, idepth, cv::COLORMAP_JET);
-		cv::imshow("Disparity", idepth);
+		cv::imshow("Disparity:" + name_, idepth);
 		//if(cv::waitKey(40) == 27) {
 	        // exit if ESC is pressed
 	    //    active_ = false;
@@ -84,14 +169,48 @@ bool Display::render(const cv::Mat &rgb, const cv::Mat &depth) {
 	return true;
 }
 
+#if defined HAVE_PCL
+bool Display::render(pcl::PointCloud<pcl::PointXYZRGB>::ConstPtr pc) {	
+	pcl::visualization::PointCloudColorHandlerRGBField<pcl::PointXYZRGB> rgb(pc);
+	if (!pclviz_->updatePointCloud<pcl::PointXYZRGB> (pc, rgb, "reconstruction")) {
+		pclviz_->addPointCloud<pcl::PointXYZRGB> (pc, rgb, "reconstruction");
+		pclviz_->setCameraPosition(-878.0, -71.0, -2315.0, -0.1, -0.99, 0.068, 0.0, -1.0, 0.0);
+		pclviz_->setPointCloudRenderingProperties (pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 2, "reconstruction");
+	}
+	return true;
+}
+#endif  // HAVE_PCL
+bool Display::render(const cv::Mat &img, style_t s) {
+	if (s == STYLE_NORMAL) {
+		cv::imshow("Image", img);
+	} else if (s = STYLE_DISPARITY) {
+		Mat idepth;
+
+		if ((bool)config_["flip_vert"]) {
+			cv::flip(img, idepth, 0);
+		} else {
+			idepth = img;
+		}
+
+    	idepth.convertTo(idepth, CV_8U, 255.0f / 256.0f);
+
+    	applyColorMap(idepth, idepth, cv::COLORMAP_JET);
+		cv::imshow("Disparity", idepth);
+	}
+
+	return true;
+}
+
 void Display::wait(int ms) {
-	if (config_["points"] && q_.rows != 0) {
-		#if defined HAVE_VIZ
+	if (config_["points"]) {
+		#if defined HAVE_PCL
+		pclviz_->spinOnce(20);
+		#elif defined HAVE_VIZ
 		window_->spinOnce(1, true);
 		#endif  // HAVE_VIZ
 	}
 	
-	if (config_["disparity"]) {
+	if (config_["disparity"] || config_["left"] || config_["right"]) {
 		if(cv::waitKey(ms) == 27) {
 	        // exit if ESC is pressed
 	        active_ = false;
@@ -100,7 +219,9 @@ void Display::wait(int ms) {
 }
 
 bool Display::active() const {
-	#if defined HAVE_VIZ
+	#if defined HAVE_PCL
+	return active_ && !pclviz_->wasStopped();
+	#elif defined HAVE_VIZ
 	return active_ && !window_->wasStopped();
 	#else
 	return active_;
diff --git a/vision/include/ftl/calibrate.hpp b/vision/include/ftl/calibrate.hpp
index 79fb310b348db16bbebde7f259e95db17c83b9f8..83a2bd9c139bee3a1920545ff0c9244bae43a2bc 100644
--- a/vision/include/ftl/calibrate.hpp
+++ b/vision/include/ftl/calibrate.hpp
@@ -29,7 +29,12 @@ class Calibrate {
 	// TODO(nick) replace or remove this class.
 	class Settings {
 		public:
-		Settings() : goodInput(false) {}
+		Settings() : calibrationPattern(CHESSBOARD), squareSize(50.0f),
+			nrFrames(30), aspectRatio(1.0f), delay(100), writePoints(false), writeExtrinsics(true),
+			writeGrid(false), calibZeroTangentDist(false), calibFixPrincipalPoint(true),
+			flipVertical(false), showUndistorsed(true), useFisheye(false), fixK1(false),
+			fixK2(false), fixK3(false), fixK4(false), fixK5(false), cameraID(0), atImageList(0),
+			inputType(INVALID), goodInput(false), flag(0) {}
 		enum Pattern { NOT_EXISTING, CHESSBOARD, CIRCLES_GRID, ASYMMETRIC_CIRCLES_GRID };
 		enum InputType { INVALID, CAMERA, VIDEO_FILE, IMAGE_LIST };
 
diff --git a/vision/include/ftl/local.hpp b/vision/include/ftl/local.hpp
index 80b527cc74c50122274f9568c1ce8b5b9ad2b653..9a4204e65bab87eb71af8768decb12d116116b20 100644
--- a/vision/include/ftl/local.hpp
+++ b/vision/include/ftl/local.hpp
@@ -12,7 +12,7 @@ namespace cv {
 namespace ftl {
 class LocalSource {
 	public:
-	LocalSource(nlohmann::json &config);
+	explicit LocalSource(nlohmann::json &config);
 	LocalSource(const std::string &vid, nlohmann::json &config);
 	
 	bool left(cv::Mat &m);
diff --git a/vision/lib/elas/triangle.cpp b/vision/lib/elas/triangle.cpp
index 8de1557bc5c7192530d4594c03d0ef9e90eef72c..14d4a2d03c06537a24547bb8c6c44526964ab207 100644
--- a/vision/lib/elas/triangle.cpp
+++ b/vision/lib/elas/triangle.cpp
@@ -1419,27 +1419,27 @@ void printtriangle(struct mesh *m, struct behavior *b, struct otri *t)
   struct osub printsh;
   vertex printvertex;
 
-  printf("triangle x%lx with orientation %d:\n", (unsigned long long) t->tri,
+  printf("triangle x%lx with orientation %d:\n", (unsigned long) t->tri,
          t->orient);
   decode(t->tri[0], printtri);
   if (printtri.tri == m->dummytri) {
     printf("    [0] = Outer space\n");
   } else {
-    printf("    [0] = x%lx  %d\n", (unsigned long long) printtri.tri,
+    printf("    [0] = x%lx  %d\n", (unsigned long) printtri.tri,
            printtri.orient);
   }
   decode(t->tri[1], printtri);
   if (printtri.tri == m->dummytri) {
     printf("    [1] = Outer space\n");
   } else {
-    printf("    [1] = x%lx  %d\n", (unsigned long long) printtri.tri,
+    printf("    [1] = x%lx  %d\n", (unsigned long) printtri.tri,
            printtri.orient);
   }
   decode(t->tri[2], printtri);
   if (printtri.tri == m->dummytri) {
     printf("    [2] = Outer space\n");
   } else {
-    printf("    [2] = x%lx  %d\n", (unsigned long long) printtri.tri,
+    printf("    [2] = x%lx  %d\n", (unsigned long) printtri.tri,
            printtri.orient);
   }
 
@@ -1448,37 +1448,37 @@ void printtriangle(struct mesh *m, struct behavior *b, struct otri *t)
     printf("    Origin[%d] = NULL\n", (t->orient + 1) % 3 + 3);
   else
     printf("    Origin[%d] = x%lx  (%.12g, %.12g)\n",
-           (t->orient + 1) % 3 + 3, (unsigned long long) printvertex,
+           (t->orient + 1) % 3 + 3, (unsigned long) printvertex,
            printvertex[0], printvertex[1]);
   dest(*t, printvertex);
   if (printvertex == (vertex) NULL)
     printf("    Dest  [%d] = NULL\n", (t->orient + 2) % 3 + 3);
   else
     printf("    Dest  [%d] = x%lx  (%.12g, %.12g)\n",
-           (t->orient + 2) % 3 + 3, (unsigned long long) printvertex,
+           (t->orient + 2) % 3 + 3, (unsigned long) printvertex,
            printvertex[0], printvertex[1]);
   apex(*t, printvertex);
   if (printvertex == (vertex) NULL)
     printf("    Apex  [%d] = NULL\n", t->orient + 3);
   else
     printf("    Apex  [%d] = x%lx  (%.12g, %.12g)\n",
-           t->orient + 3, (unsigned long long) printvertex,
+           t->orient + 3, (unsigned long) printvertex,
            printvertex[0], printvertex[1]);
 
   if (b->usesegments) {
     sdecode(t->tri[6], printsh);
     if (printsh.ss != m->dummysub) {
-      printf("    [6] = x%lx  %d\n", (unsigned long long) printsh.ss,
+      printf("    [6] = x%lx  %d\n", (unsigned long) printsh.ss,
              printsh.ssorient);
     }
     sdecode(t->tri[7], printsh);
     if (printsh.ss != m->dummysub) {
-      printf("    [7] = x%lx  %d\n", (unsigned long long) printsh.ss,
+      printf("    [7] = x%lx  %d\n", (unsigned long) printsh.ss,
              printsh.ssorient);
     }
     sdecode(t->tri[8], printsh);
     if (printsh.ss != m->dummysub) {
-      printf("    [8] = x%lx  %d\n", (unsigned long long) printsh.ss,
+      printf("    [8] = x%lx  %d\n", (unsigned long) printsh.ss,
              printsh.ssorient);
     }
   }
@@ -1506,19 +1506,19 @@ void printsubseg(struct mesh *m, struct behavior *b, struct osub *s)
   vertex printvertex;
 
   printf("subsegment x%lx with orientation %d and mark %d:\n",
-         (unsigned long long) s->ss, s->ssorient, mark(*s));
+         (unsigned long) s->ss, s->ssorient, mark(*s));
   sdecode(s->ss[0], printsh);
   if (printsh.ss == m->dummysub) {
     printf("    [0] = No subsegment\n");
   } else {
-    printf("    [0] = x%lx  %d\n", (unsigned long long) printsh.ss,
+    printf("    [0] = x%lx  %d\n", (unsigned long) printsh.ss,
            printsh.ssorient);
   }
   sdecode(s->ss[1], printsh);
   if (printsh.ss == m->dummysub) {
     printf("    [1] = No subsegment\n");
   } else {
-    printf("    [1] = x%lx  %d\n", (unsigned long long) printsh.ss,
+    printf("    [1] = x%lx  %d\n", (unsigned long) printsh.ss,
            printsh.ssorient);
   }
 
@@ -1527,28 +1527,28 @@ void printsubseg(struct mesh *m, struct behavior *b, struct osub *s)
     printf("    Origin[%d] = NULL\n", 2 + s->ssorient);
   else
     printf("    Origin[%d] = x%lx  (%.12g, %.12g)\n",
-           2 + s->ssorient, (unsigned long long) printvertex,
+           2 + s->ssorient, (unsigned long) printvertex,
            printvertex[0], printvertex[1]);
   sdest(*s, printvertex);
   if (printvertex == (vertex) NULL)
     printf("    Dest  [%d] = NULL\n", 3 - s->ssorient);
   else
     printf("    Dest  [%d] = x%lx  (%.12g, %.12g)\n",
-           3 - s->ssorient, (unsigned long long) printvertex,
+           3 - s->ssorient, (unsigned long) printvertex,
            printvertex[0], printvertex[1]);
 
   decode(s->ss[6], printtri);
   if (printtri.tri == m->dummytri) {
     printf("    [6] = Outer space\n");
   } else {
-    printf("    [6] = x%lx  %d\n", (unsigned long long) printtri.tri,
+    printf("    [6] = x%lx  %d\n", (unsigned long) printtri.tri,
            printtri.orient);
   }
   decode(s->ss[7], printtri);
   if (printtri.tri == m->dummytri) {
     printf("    [7] = Outer space\n");
   } else {
-    printf("    [7] = x%lx  %d\n", (unsigned long long) printtri.tri,
+    printf("    [7] = x%lx  %d\n", (unsigned long) printtri.tri,
            printtri.orient);
   }
 
@@ -1557,14 +1557,14 @@ void printsubseg(struct mesh *m, struct behavior *b, struct osub *s)
     printf("    Segment origin[%d] = NULL\n", 4 + s->ssorient);
   else
     printf("    Segment origin[%d] = x%lx  (%.12g, %.12g)\n",
-           4 + s->ssorient, (unsigned long long) printvertex,
+           4 + s->ssorient, (unsigned long) printvertex,
            printvertex[0], printvertex[1]);
   segdest(*s, printvertex);
   if (printvertex == (vertex) NULL)
     printf("    Segment dest  [%d] = NULL\n", 5 - s->ssorient);
   else
     printf("    Segment dest  [%d] = x%lx  (%.12g, %.12g)\n",
-           5 - s->ssorient, (unsigned long long) printvertex,
+           5 - s->ssorient, (unsigned long) printvertex,
            printvertex[0], printvertex[1]);
 }
 
diff --git a/vision/src/algorithms/elas.cpp b/vision/src/algorithms/elas.cpp
index ca37357442b2f7ecfcba7996601ae741cd463392..b99c309c96717739327a488efcdfd2e510c9bbe7 100644
--- a/vision/src/algorithms/elas.cpp
+++ b/vision/src/algorithms/elas.cpp
@@ -35,7 +35,7 @@ void ELAS::compute(const cv::Mat &l, const cv::Mat &r, cv::Mat &disp) {
 	disp = Mat(cv::Size(l.cols, l.rows), CV_32F);
 	Mat dispr(cv::Size(l.cols, l.rows), CV_32F);
 	
-	const int32_t dims[3] = {l.cols,l.rows,lbw.step};
+	const int32_t dims[3] = {l.cols,l.rows,static_cast<int32_t>(lbw.step)};
 	
 	if (disp.step/sizeof(float) != lbw.step) LOG(WARNING) << "Incorrect disparity step";
 
diff --git a/vision/src/calibrate.cpp b/vision/src/calibrate.cpp
index 7151a01bb2dfab9fa7b480612c3839f344e666a7..e5025a9d85a0c2470e504e11e7a8ffaa146914e7 100644
--- a/vision/src/calibrate.cpp
+++ b/vision/src/calibrate.cpp
@@ -491,7 +491,7 @@ static double computeReprojectionErrors(
 		vector<float>& perViewErrors, bool fisheye) {
     vector<Point2f> imagePoints2;
     size_t totalPoints = 0;
-    double totalErr = 0, err;
+    double totalErr = 0;
     perViewErrors.resize(objectPoints.size());
 
     for (size_t i = 0; i < objectPoints.size(); ++i) {
@@ -502,7 +502,7 @@ static double computeReprojectionErrors(
             projectPoints(objectPoints[i], rvecs[i], tvecs[i], cameraMatrix,
             		distCoeffs, imagePoints2);
         }
-        err = norm(imagePoints[i], imagePoints2, NORM_L2);
+        double err = norm(imagePoints[i], imagePoints2, NORM_L2);
 
         size_t n = objectPoints[i].size();
         perViewErrors[i] = static_cast<float>(std::sqrt(err*err/n));
@@ -576,10 +576,6 @@ static bool _runCalibration(const Calibrate::Settings& s, Size& imageSize,
             tvecs.push_back(_tvecs.row(i));
         }
     } else {
-        int iFixedPoint = -1;
-        if (release_object)
-            iFixedPoint = s.boardSize.width - 1;
-
         rms = calibrateCamera(objectPoints, imagePoints, imageSize,
                                 cameraMatrix, distCoeffs, rvecs, tvecs,
                                 s.flag | CALIB_USE_LU);
diff --git a/vision/src/local.cpp b/vision/src/local.cpp
index d8d5229e84ccb39b7639a914941185c423b36a27..f26bea1d31c571dbb9b3fc133add138e0e59df3e 100644
--- a/vision/src/local.cpp
+++ b/vision/src/local.cpp
@@ -58,6 +58,15 @@ LocalSource::LocalSource(nlohmann::json &config)
 		stereo_ = false;
 		LOG(WARNING) << "Not able to find second camera for stereo";
 	} else {
+		camera_a_->set(cv::CAP_PROP_FRAME_WIDTH,(int)config["width"]);
+		camera_a_->set(cv::CAP_PROP_FRAME_HEIGHT,(int)config["height"]);
+		camera_b_->set(cv::CAP_PROP_FRAME_WIDTH,(int)config["width"]);
+		camera_b_->set(cv::CAP_PROP_FRAME_HEIGHT,(int)config["height"]);
+
+		Mat frame;
+		camera_a_->grab();
+		camera_a_->retrieve(frame);
+		LOG(INFO) << "Video size : " << frame.cols << "x" << frame.rows;
 		stereo_ = true;
 	}
 
diff --git a/vision/src/main.cpp b/vision/src/main.cpp
index 0665b797cdb017c09dbafdfcf5f0dd9011f5ead4..49d3f43dbf0fa1bceda2b64b42d1b67d7f88bb83 100644
--- a/vision/src/main.cpp
+++ b/vision/src/main.cpp
@@ -108,18 +108,30 @@ static void run(const string &file) {
 		return buf;
 	});
 
+	Mat l, r, disp;
+	bool grabbed = false;
+	mutex datam;
+	condition_variable datacv;
+
+	// Wait for grab message to sync camera capture
+	net.bind("grab", [&calibrate,&l,&r,&datam,&datacv,&grabbed]() -> void {
+		unique_lock<mutex> datalk(datam);
+		if (grabbed) return;
+		calibrate.rectified(l, r);
+		grabbed = true;
+		datacv.notify_one();
+	});
+
     // Choose and configure disparity algorithm
     auto disparity = Disparity::create(config["disparity"]);
     if (!disparity) LOG(FATAL) << "Unknown disparity algorithm : " << config["disparity"];
 
-	Mat l, r, disp;
 	Mat pl, pdisp;
 	vector<unsigned char> rgb_buf;
 	vector<unsigned char> d_buf;
 	string uri = string("ftl://utu.fi/")+(string)config["stream"]["name"]+string("/rgb-d");
 
-	Display display(config["display"]);
-	display.setCalibration(Q_32F);
+	Display display(config["display"], "local");
 	
 	Streamer stream(net, config["stream"]);
 
@@ -130,23 +142,22 @@ static void run(const string &file) {
 		condition_variable cv;
 		int jobs = 0;
 
-		// Pipeline for disparity
-		pool.push([&](int id) {
-			auto start = std::chrono::high_resolution_clock::now();
-			// Read calibrated images.
+		// Fake grab if no peers to allow visualisation locally
+		if (net.numberOfPeers() == 0) {
+			grabbed = true;
 			calibrate.rectified(l, r);
+		}
 
-			// Feed into sync buffer and network forward
-			//sync->feed(ftl::LEFT, l, lsrc->getTimestamp());
-			//sync->feed(ftl::RIGHT, r, lsrc->getTimestamp());
-
-			// Read back from buffer
-			//sync->get(ftl::LEFT, l);
-			//sync->get(ftl::RIGHT, r);
+		// Pipeline for disparity
+		pool.push([&](int id) {
+			// Wait for image grab
+			unique_lock<mutex> datalk(datam);
+			datacv.wait(datalk, [&grabbed](){ return grabbed; });
+			grabbed = false;
 
-			LOG(INFO) << "PRE DISPARITY";
+			auto start = std::chrono::high_resolution_clock::now();
 		    disparity->compute(l, r, disp);
-			LOG(INFO) << "POST DISPARITY";
+			datalk.unlock();
 
 			unique_lock<mutex> lk(m);
 			jobs++;
@@ -158,50 +169,6 @@ static void run(const string &file) {
 			LOG(INFO) << "Disparity in " << elapsed.count() << "s";
 		});
 
-		// Pipeline for jpeg compression
-		/*pool.push([&](int id) {
-			auto start = std::chrono::high_resolution_clock::now();
-			if (pl.rows != 0) cv::imencode(".jpg", pl, rgb_buf);
-			unique_lock<mutex> lk(m);
-			jobs++;
-			lk.unlock();
-			cv.notify_one();
-
-			std::chrono::duration<double> elapsed =
-				std::chrono::high_resolution_clock::now() - start;
-			LOG(INFO) << "JPG in " << elapsed.count() << "s";
-		});*/
-
-		// Pipeline for zlib compression
-		/*pool.push([&](int id) {
-			auto start = std::chrono::high_resolution_clock::now();
-			if (pl.rows != 0) {
-				d_buf.resize(pdisp.step*pdisp.rows);
-				z_stream defstream;
-				defstream.zalloc = Z_NULL;
-				defstream.zfree = Z_NULL;
-				defstream.opaque = Z_NULL;
-				defstream.avail_in = pdisp.step*pdisp.rows;
-				defstream.next_in = (Bytef *)pdisp.data; // input char array
-				defstream.avail_out = (uInt)pdisp.step*pdisp.rows; // size of output
-				defstream.next_out = (Bytef *)d_buf.data(); // output char array
-				
-				deflateInit(&defstream, Z_BEST_COMPRESSION);
-				deflate(&defstream, Z_FINISH);
-				deflateEnd(&defstream);
-				
-				d_buf.resize(defstream.total_out);
-			}
-			unique_lock<mutex> lk(m);
-			jobs++;
-			lk.unlock();
-			cv.notify_one();
-
-			std::chrono::duration<double> elapsed =
-				std::chrono::high_resolution_clock::now() - start;
-			LOG(INFO) << "ZLIB in " << elapsed.count() << "s";
-		});*/
-
 		// Pipeline for stream compression
 		pool.push([&](int id) {
 			auto start = std::chrono::high_resolution_clock::now();
@@ -217,7 +184,7 @@ static void run(const string &file) {
 		});
 
 		// Send RGB+Depth images for local rendering
-		if (pl.rows > 0) display.render(pl, pdisp);
+		if (pl.rows > 0) display.render(pl, r, pdisp, Q_32F);
 		display.wait(1);
 
 		// Wait for both pipelines to complete