diff --git a/applications/gui2/src/modules/calibration/calibration.cpp b/applications/gui2/src/modules/calibration/calibration.cpp
index f892ba61c1699ae70dcc1ae837ef4f231905a4b7..83627e6fd2ec0b5baccfeaeebf4d818276edf951 100644
--- a/applications/gui2/src/modules/calibration/calibration.cpp
+++ b/applications/gui2/src/modules/calibration/calibration.cpp
@@ -32,11 +32,13 @@ using ftl::data::FrameSetPtr;
 using ftl::gui2::OpenCVCalibrateFlags;
 using ftl::gui2::OpenCVCalibrateFlagsStereo;
 
-int OpenCVCalibrateFlags::defaultFlags() const { return (
-			cv::CALIB_RATIONAL_MODEL |
-			cv::CALIB_THIN_PRISM_MODEL |
-			cv::CALIB_FIX_ASPECT_RATIO
-);}
+int OpenCVCalibrateFlags::defaultFlags() const {
+	// For finding distortion coefficients fix focal length and principal point.
+	// Otherwise results might be unreliable.
+	return (cv::CALIB_FIX_FOCAL_LENGTH |
+			cv::CALIB_FIX_PRINCIPAL_POINT |
+			cv::CALIB_FIX_ASPECT_RATIO);
+}
 
 std::vector<int> OpenCVCalibrateFlags::list() const {
 	return {
diff --git a/applications/gui2/src/modules/calibration/calibration.hpp b/applications/gui2/src/modules/calibration/calibration.hpp
index 4986689b1bc52e2cda19f812944c15280901f55a..00b33b3ec5288c7f49469f8c51002e2a33efa372 100644
--- a/applications/gui2/src/modules/calibration/calibration.hpp
+++ b/applications/gui2/src/modules/calibration/calibration.hpp
@@ -261,6 +261,9 @@ public:
 
 	int cameraCount();
 
+	std::string cameraName(int camera);
+	std::vector<std::string> cameraNames();
+
 	ftl::calibration::ExtrinsicCalibration& calib();
 
 	/** hasFrame(int) must be true before calling getFrame() **/
@@ -340,10 +343,6 @@ protected:
 
 private:
 	// map frameid+channel to int. used by ExtrinsicCalibration
-	struct Camera {
-		const CameraID id;
-		ftl::calibration::CalibrationData::Calibration calib;
-	};
 
 	bool onFrameSet_(const ftl::data::FrameSetPtr& fs);
 
@@ -356,7 +355,7 @@ private:
 		bool capture = false;
 		int min_cameras = 2;
 		int flags = 0;
-		std::vector<Camera> cameras;
+		std::vector<CameraID> cameras;
 
 		std::unique_ptr<ftl::calibration::CalibrationObject> calib_object;
 		ftl::calibration::ExtrinsicCalibration calib;
diff --git a/applications/gui2/src/modules/calibration/extrinsic.cpp b/applications/gui2/src/modules/calibration/extrinsic.cpp
index 3228fd2c0fd9a1e17ee5a664503c76f683c582ee..c1e4c07e3764bcfe62f9eadf50952b81f0cc7c8d 100644
--- a/applications/gui2/src/modules/calibration/extrinsic.cpp
+++ b/applications/gui2/src/modules/calibration/extrinsic.cpp
@@ -56,6 +56,8 @@ void ExtrinsicCalibration::start(unsigned int fsid, std::vector<FrameID> sources
 	reset();
 
 	state_.cameras.reserve(sources.size()*2);
+	state_.maps1.resize(sources.size()*2);
+	state_.maps2.resize(sources.size()*2);
 
 	auto* filter = io->feed()->filter
 		(std::unordered_set<uint32_t>{fsid}, {Channel::Left, Channel::Right});
@@ -73,12 +75,19 @@ void ExtrinsicCalibration::start(unsigned int fsid, std::vector<FrameID> sources
 		auto cr = CameraID(id.frameset(), id.source(), Channel::Right);
 		const auto& frame = (*fs_current_)[id.source()].cast<ftl::rgbd::Frame>();
 		auto sz = cv::Size((int) frame.getLeftCamera().width, (int) frame.getLeftCamera().height);
-		state_.cameras.push_back({cl, {}});
-		state_.cameras.push_back({cr, {}});
-
-		state_.calib.addStereoCamera(
-			CalibrationData::Intrinsic(getCalibration(cl).intrinsic, sz),
-			CalibrationData::Intrinsic(getCalibration(cr).intrinsic, sz));
+		state_.cameras.push_back(cl);
+		state_.cameras.push_back(cr);
+		auto calibl = getCalibration(cl);
+		calibl.intrinsic = CalibrationData::Intrinsic(calibl.intrinsic, sz);
+		auto calibr = getCalibration(cr);
+		calibr.intrinsic = CalibrationData::Intrinsic(calibr.intrinsic, sz);
+
+		// Scale intrinsics
+		state_.calib.addStereoCamera(calibl.intrinsic, calibr.intrinsic);
+
+		// Update rectification
+		unsigned int idx = state_.cameras.size() - 2;
+		stereoRectify(idx, idx + 1, calibl, calibr);
 	}
 
 	// initialize last points structure; can't be resized while running (without
@@ -90,15 +99,20 @@ void ExtrinsicCalibration::start(unsigned int fsid, std::vector<FrameID> sources
 	}
 
 	auto* view = new ftl::gui2::ExtrinsicCalibrationView(screen, this);
-	view->onClose([this, filter](){
-		running_ = false;
+	view->onClose([this, filter]() {
 		filter->remove();
+		state_.capture = false;
+
+		if (future_.valid()) {
+			future_.wait();
+		}
+
 		if (fs_current_ == nullptr) { return; }
 
 		// change mode only once per frame (cameras contain same frame twice)
 		std::unordered_set<uint32_t> fids;
 		for (const auto camera : state_.cameras) {
-			fids.insert(camera.id.source());
+			fids.insert(camera.source());
 		}
 
 		for (const auto i : fids) {
@@ -109,8 +123,23 @@ void ExtrinsicCalibration::start(unsigned int fsid, std::vector<FrameID> sources
 	screen->setView(view);
 }
 
+std::string ExtrinsicCalibration::cameraName(int c) {
+	const auto& camera = state_.cameras[c];
+	return (*fs_current_)[camera.id].name() + " - " +
+		((camera.channel == Channel::Left) ? "Left" : "Right");
+}
+
+std::vector<std::string> ExtrinsicCalibration::cameraNames() {
+	std::vector<std::string> names;
+	names.reserve(cameraCount());
+	for (int i = 0; i < cameraCount(); i++) {
+		names.push_back(cameraName(i));
+	}
+	return names;
+}
+
 CalibrationData::Calibration ExtrinsicCalibration::calibration(int c) {
-	return state_.calib.calibration(c);
+	return state_.calib.calibrationOptimized(c);
 }
 
 bool ExtrinsicCalibration::onFrameSet_(const FrameSetPtr& fs) {
@@ -119,9 +148,8 @@ bool ExtrinsicCalibration::onFrameSet_(const FrameSetPtr& fs) {
 	screen->redraw();
 
 	bool all_good = true;
-	for (const auto& [id, channel] : state_.cameras) {
-		std::ignore = channel;
-		all_good &= checkFrame((*fs)[id.source()]);
+	for (const auto& c : state_.cameras) {
+		all_good &= checkFrame((*fs)[c.source()]);
 	}
 	//if (!all_good) { return true; }
 
@@ -136,14 +164,14 @@ bool ExtrinsicCalibration::onFrameSet_(const FrameSetPtr& fs) {
 		int count = 0;
 
 		for (unsigned int i = 0; i < state_.cameras.size(); i++) {
-			const auto& [id, calib] = state_.cameras[i];
-
+			const auto& id = state_.cameras[i];
+			const auto& calib = state_.calib.calibration(i).intrinsic;
 			if (!(*fs)[id.source()].hasChannel(id.channel)) { continue; }
 
 			points.clear();
 			const cv::cuda::GpuMat& im = (*fs)[id.source()].get<cv::cuda::GpuMat>(id.channel);
-			K = calib.intrinsic.matrix();
-			distCoeffs = calib.intrinsic.distCoeffs.Mat();
+			K = calib.matrix();
+			distCoeffs = calib.distCoeffs.Mat();
 
 			try {
 				int n = state_.calib_object->detect(im, points, K, distCoeffs);
@@ -171,13 +199,13 @@ bool ExtrinsicCalibration::onFrameSet_(const FrameSetPtr& fs) {
 }
 
 bool ExtrinsicCalibration::hasFrame(int camera) {
-	const auto id = state_.cameras[camera].id;
+	const auto id = state_.cameras[camera];
 	return	(std::atomic_load(&fs_current_).get() != nullptr) &&
 			((*fs_current_)[id.source()].hasChannel(id.channel));
 }
 
 const cv::cuda::GpuMat ExtrinsicCalibration::getFrame(int camera) {
-	const auto id = state_.cameras[camera].id;
+	const auto id = state_.cameras[camera];
 	return (*fs_current_)[id.source()].cast<ftl::rgbd::Frame>().get<cv::cuda::GpuMat>(id.channel);
 }
 
@@ -239,7 +267,7 @@ std::vector<ExtrinsicCalibration::CameraID> ExtrinsicCalibration::cameras() {
 	std::vector<ExtrinsicCalibration::CameraID> res;
 	res.reserve(cameraCount());
 	for (const auto& camera : state_.cameras) {
-		res.push_back(camera.id);
+		res.push_back(camera);
 	}
 	return res;
 }
@@ -254,13 +282,13 @@ void ExtrinsicCalibration::updateCalibration() {
 
 	for (unsigned int i = 0; i < state_.cameras.size(); i++) {
 		auto& c = state_.cameras[i];
-		auto frame_id = ftl::data::FrameID(c.id);
+		auto frame_id = ftl::data::FrameID(c);
 
 		if (update.count(frame_id) == 0) {
-			auto& frame = fs->frames[c.id];
+			auto& frame = fs->frames[c];
 			update[frame_id] = frame.get<CalibrationData>(Channel::CalibrationData);
 		}
-		update[frame_id].get(c.id.channel) = state_.calib.calibrationOptimized(i);
+		update[frame_id].get(c.channel) = state_.calib.calibrationOptimized(i);
 	}
 
 	for (auto& [fid, calib] : update) {
@@ -276,8 +304,10 @@ void ExtrinsicCalibration::updateCalibration(int c) {
 void ExtrinsicCalibration::stereoRectify(int cl, int cr,
 	const CalibrationData::Calibration& l, const CalibrationData::Calibration& r) {
 
-	CHECK(l.extrinsic.tvec != r.extrinsic.tvec);
-	CHECK(l.intrinsic.resolution == r.intrinsic.resolution);
+	CHECK_NE(l.extrinsic.tvec, r.extrinsic.tvec);
+	CHECK_EQ(l.intrinsic.resolution, r.intrinsic.resolution);
+	CHECK_LT(cr, state_.maps1.size());
+	CHECK_LT(cr, state_.maps2.size());
 
 	auto size = l.intrinsic.resolution;
 	cv::Mat T = r.extrinsic.matrix() * inverse(l.extrinsic.matrix());
@@ -290,6 +320,15 @@ void ExtrinsicCalibration::stereoRectify(int cl, int cr,
 		r.intrinsic.matrix(), r.intrinsic.distCoeffs.Mat(), size,
 		R, t, R1, R2, P1, P2, Q, cv::CALIB_ZERO_DISPARITY, 1.0);
 
+	// sanity check: rectification should give same rotation for both cameras
+	// cameras (with certain accuracy). R1 and R2 contain 3x3 rotation matrices
+	// from unrectified to rectified coordinates.
+	cv::Vec3d rvec1;
+	cv::Vec3d rvec2;
+	cv::Rodrigues(R1 * l.extrinsic.matrix()(cv::Rect(0, 0, 3, 3)), rvec1);
+	cv::Rodrigues(R2 * r.extrinsic.matrix()(cv::Rect(0, 0, 3, 3)), rvec2);
+	CHECK_LT(cv::norm(rvec1, rvec2), 0.01);
+
 	cv::initUndistortRectifyMap(l.intrinsic.matrix(), l.intrinsic.distCoeffs.Mat(),
 		R1, P1, size, CV_32FC1, map1, map2);
 	state_.maps1[cl].upload(map1);
@@ -331,6 +370,8 @@ void ExtrinsicCalibration::run() {
 			for (int c = 0; c < cameraCount(); c += 2) {
 				auto l = state_.calib.calibrationOptimized(c);
 				auto r = state_.calib.calibrationOptimized(c + 1);
+				LOG(INFO) << c << ": rvec " << l.extrinsic.rvec << "; tvec " << l.extrinsic.tvec;
+				LOG(INFO) << c  + 1 << ": rvec " << r.extrinsic.rvec << "; tvec " << r.extrinsic.tvec;
 				stereoRectify(c, c + 1, l, r);
 
 				LOG(INFO) << "baseline (" << c << ", " << c + 1 << "): "
diff --git a/applications/gui2/src/modules/calibration/intrinsic.cpp b/applications/gui2/src/modules/calibration/intrinsic.cpp
index ba754184b06488c9bd41224507573ab2d7fe59cd..9a3dc06bdef71397f48626edf3a02e05a1a3875d 100644
--- a/applications/gui2/src/modules/calibration/intrinsic.cpp
+++ b/applications/gui2/src/modules/calibration/intrinsic.cpp
@@ -246,7 +246,7 @@ void IntrinsicCalibration::run() {
 			cv::Size size = state_->calib.resolution;
 			if (state_->flags.has(cv::CALIB_USE_INTRINSIC_GUESS)) {
 				// OpenCV seems to use these anyways?
-				K = state_->calib.matrix();
+				K = state_->calib.matrix(size);
 				state_->calib.distCoeffs.Mat(12).copyTo(distCoeffs);
 			}
 			std::vector<cv::Mat> rvecs, tvecs;
@@ -341,8 +341,8 @@ void IntrinsicCalibration::setFocalLength(double value, cv::Size2d sensor_size)
 	setSensorSize(sensor_size);
 	double f = value*(state_->calib.resolution.width/sensor_size.width);
 
-	state_->calib.cx = f;
-	state_->calib.cy = f;
+	state_->calib.fx = f;
+	state_->calib.fy = f;
 }
 
 void IntrinsicCalibration::resetPrincipalPoint() {
diff --git a/applications/gui2/src/views/calibration/extrinsicview.cpp b/applications/gui2/src/views/calibration/extrinsicview.cpp
index 0c703474eac7ec293a526c013459522cba33fba1..b59f2ffa46b31802a5fa7b60c757439575324f29 100644
--- a/applications/gui2/src/views/calibration/extrinsicview.cpp
+++ b/applications/gui2/src/views/calibration/extrinsicview.cpp
@@ -1,5 +1,7 @@
 #include "extrinsicview.hpp"
 #include "visualization.hpp"
+#include "widgets.hpp"
+
 #include "../../screen.hpp"
 #include "../../widgets/window.hpp"
 
@@ -10,6 +12,7 @@
 #include <nanogui/checkbox.h>
 #include <nanogui/label.h>
 #include <nanogui/formhelper.h>
+#include <nanogui/tabwidget.h>
 
 using ftl::gui2::ExtrinsicCalibrationStart;
 using ftl::gui2::ExtrinsicCalibrationView;
@@ -76,7 +79,7 @@ ExtrinsicCalibrationStart::~ExtrinsicCalibrationStart() {
 
 void ExtrinsicCalibrationStart::draw(NVGcontext* ctx) {
 	window_->center();
-	bcontinue_->setEnabled(sources_ != 0);
+	bcontinue_->setEnabled((lssources_->childCount() != 0));
 	ftl::gui2::View::draw(ctx);
 }
 
@@ -212,6 +215,7 @@ ExtrinsicCalibrationView::ControlWindow::ControlWindow(nanogui::Widget* parent,
 	bapply_->setFlags(nanogui::Button::Flags::ToggleButton);
 	bapply_->setPushed(view_->rectify());
 	bapply_->setChangeCallback([button = bapply_, view = view_](bool v){
+		view->setMode(Mode::VIDEO); // stop capture
 		view->setRectify(v);
 	});
 
@@ -404,14 +408,19 @@ class ExtrinsicCalibrationView::ResultsWindow : public FixedWindow {
 public:
 	ResultsWindow(nanogui::Widget* parent, ExtrinsicCalibrationView* view);
 	virtual void draw(NVGcontext* ctx) override;
+	virtual void performLayout(NVGcontext* ctx);
+	//virtual nanogui::Vector2i preferredSize(NVGcontext* ctx) const override;
+
+	void update();
 
 private:
 	ExtrinsicCalibrationView* view_;
 	ExtrinsicCalibration* ctrl_;
 
-	nanogui::Button* bcalibrate_;
-	nanogui::Button* bpause_;
-	nanogui::Button* bresults_;
+	std::vector<ftl::calibration::CalibrationData::Calibration> calib_;
+	std::vector<std::string> names_;
+
+	nanogui::TabWidget* tabs_ = nullptr;
 
 public:
 	EIGEN_MAKE_ALIGNED_OPERATOR_NEW
@@ -420,25 +429,60 @@ public:
 ExtrinsicCalibrationView::ResultsWindow::ResultsWindow(nanogui::Widget* parent, ExtrinsicCalibrationView* view) :
 	FixedWindow(parent, "Results"), view_(view), ctrl_(view->ctrl_) {
 
+	setLayout(new nanogui::BoxLayout
+		(nanogui::Orientation::Vertical, nanogui::Alignment::Maximum));
+
 	(new nanogui::Button(buttonPanel(), "", ENTYPO_ICON_CROSS))->setCallback(
 		[view = view_]() {
 		view->setMode(Mode::VIDEO);
 	});
-	setSize({300, 300});
+
+	tabs_ = new nanogui::TabWidget(this);
+	tabs_->createTab("Extrinsic");
+}
+
+/*nanogui::Vector2i ExtrinsicCalibrationView::ResultsWindow::preferredSize(NVGcontext* ctx) const {
+	return {600, 400};
+}*/
+
+void ExtrinsicCalibrationView::ResultsWindow::ResultsWindow::performLayout(NVGcontext* ctx) {
+	setFixedSize({600, 400});
+	tabs_->setFixedWidth(width());
+	FixedWindow::performLayout(ctx);
+}
+
+void ExtrinsicCalibrationView::ResultsWindow::ResultsWindow::update() {
+	calib_.resize(ctrl_->cameraCount());
+	while (tabs_->tabCount() > 1) {
+		// bug in nanogui: incorrect assert in removeTab(int).
+		// workaround: use tabLabelAt()
+		tabs_->removeTab(tabs_->tabLabelAt(tabs_->tabCount() - 1));
+	}
+
+	for (int i = 0; i < ctrl_->cameraCount(); i++) {
+		calib_[i] = ctrl_->calibration(i);
+		// nanogui issue: too many tabs/long names header goes outside of widget
+		// use just idx for now
+		auto* tab = tabs_->createTab(std::to_string(i));
+		new nanogui::Label(tab, ctrl_->cameraName(i), "sans-bold", 18);
+		tab->setLayout(new nanogui::BoxLayout
+			(nanogui::Orientation::Vertical, nanogui::Alignment::Middle, 0, 8));
+
+		auto* display = new IntrinsicDetails(tab);
+		display->update(calib_[i].intrinsic);
+	}
 }
 
 void ExtrinsicCalibrationView::ResultsWindow::draw(NVGcontext* ctx) {
 	FixedWindow::draw(ctx);
-	std::vector<ftl::calibration::CalibrationData::Extrinsic> calib(ctrl_->cameraCount());
-	for (int i = 0; i < ctrl_->cameraCount(); i++) {
-		calib[i] = ctrl_->calibration(i).extrinsic;
+	if (tabs_->activeTab() == 0) { // create a widget and move there
+		drawFloorPlan(ctx, tabs_->tab(0), calib_);
 	}
-	drawFloorPlan(ctx, this, calib);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 
-static void drawText(NVGcontext* ctx, nanogui::Vector2f &pos, const std::string& text,
+static void  drawText(NVGcontext* ctx, const nanogui::Vector2f &pos, const std::string& text,
 		float size=12.0f, int align=NVG_ALIGN_MIDDLE|NVG_ALIGN_CENTER) {
 	nvgFontSize(ctx, size);
 	nvgFontFace(ctx, "sans-bold");
@@ -449,6 +493,98 @@ static void drawText(NVGcontext* ctx, nanogui::Vector2f &pos, const std::string&
 	nvgText(ctx, pos.x() + 1, pos.y() + 1, text.c_str(), nullptr);
 }
 
+////////////////////////////////////////////////////////////////////////////////
+
+class StereoCalibrationImageView : public ftl::gui2::StereoImageView {
+public:
+	using ftl::gui2::StereoImageView::StereoImageView;
+
+	virtual bool keyboardCharacterEvent(unsigned int codepoint) override;
+	virtual bool mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) override;
+	virtual void draw(NVGcontext* ctx) override;
+
+	void reset();
+
+private:
+	std::set<int> rows_;
+	std::map<int, nanogui::Color> colors_;
+
+	int n_colors_ = 16;
+	float alpha_threshold_ = 2.0f;
+
+public:
+	EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
+};
+
+void StereoCalibrationImageView::reset() {
+	rows_.clear();
+}
+
+bool StereoCalibrationImageView::keyboardCharacterEvent(unsigned int codepoint) {
+	if (codepoint == 'r') {
+		reset();
+		return true;
+	}
+	return StereoImageView::keyboardCharacterEvent(codepoint);
+}
+
+bool StereoCalibrationImageView::mouseButtonEvent(const nanogui::Vector2i &p, int button, bool down, int modifiers) {
+	nanogui::Widget::mouseButtonEvent(p, button, down, modifiers);
+	if (button == GLFW_MOUSE_BUTTON_1 && !down) {
+		// half a pixel offset to match "square pixel" visualization
+		nanogui::Vector2f offset{left()->scale()/2.0f, left()->scale()/2.0f};
+		float row = round(imageCoordinateAt(p.cast<float>() + offset).y());
+
+		if (rows_.count(row))	{ rows_.erase(row); }
+		else					{ rows_.insert(row); }
+	}
+	return true;
+}
+
+void StereoCalibrationImageView::draw(NVGcontext* ctx) {
+	StereoImageView::draw(ctx);
+	// assumes vertical alignment (horizontal not implemented)
+	CHECK(orientation() == nanogui::Orientation::Vertical);
+
+	int x = position().x();
+	int y = position().y();
+	int w = width();
+	int h = left()->height();
+	float swidth = std::max(1.0f, left()->scale());
+	int c = 0; // color
+
+	for (int row : rows_) {
+		int y_im = y;
+		nanogui::Vector2f l = left()->positionForCoordinate({0.0f, row}) + left()->position().cast<float>();
+		nanogui::Vector2f r = right()->positionForCoordinate({0.0f, row}) + right()->position().cast<float>();
+		auto color = nvgHSLA(float(c%n_colors_)/float(n_colors_), 0.9, 0.5, (swidth < alpha_threshold_) ? 255 : 96);
+
+		for (auto& p : {l, r}) {
+			nvgScissor(ctx, x, y_im, w, h);
+			nvgBeginPath(ctx);
+			nvgMoveTo(ctx, x, p.y() - swidth*0.5f);
+			nvgLineTo(ctx, x + w, p.y() - swidth*0.5f);
+			nvgStrokeColor(ctx, color);
+			nvgStrokeWidth(ctx, swidth);
+			nvgStroke(ctx);
+
+			if (swidth*0.5f > alpha_threshold_) {
+				nvgBeginPath(ctx);
+				nvgMoveTo(ctx, x, p.y() - swidth*0.5f);
+				nvgLineTo(ctx, x + w, p.y() - swidth*0.5f);
+				nvgStrokeColor(ctx, nvgRGBA(0, 0, 0, 196));
+				nvgStrokeWidth(ctx, 1.0f);
+				nvgStroke(ctx);
+			}
+			nvgResetScissor(ctx);
+			y_im += h;
+		}
+		c++;
+	}
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
 ExtrinsicCalibrationView::ExtrinsicCalibrationView(Screen* widget, ExtrinsicCalibration* ctrl) :
 		ftl::gui2::View(widget), ctrl_(ctrl), rows_(0) {
 
@@ -461,7 +597,7 @@ ExtrinsicCalibrationView::ExtrinsicCalibrationView(Screen* widget, ExtrinsicCali
 
 	// assumes all cameras stereo cameras, indexed in order
 	for (int i = 0; i < ctrl_->cameraCount(); i += 2) {
-		new StereoImageView(frames_, nanogui::Orientation::Vertical);
+		new StereoCalibrationImageView(frames_, nanogui::Orientation::Vertical);
 	}
 	paused_ = false;
 	wcontrol_ = new ControlWindow(screen(), this);
@@ -483,7 +619,7 @@ void ExtrinsicCalibrationView::performLayout(NVGcontext* ctx) {
 
 	nanogui::Vector2i fsize = { width()/(frames_->childCount()), height() };
 	for (int i = 0; i < frames_->childCount(); i++) {
-		auto* stereo = dynamic_cast<StereoImageView*>(frames_->childAt(i));
+		auto* stereo = dynamic_cast<StereoCalibrationImageView*>(frames_->childAt(i));
 		stereo->setFixedSize(fsize);
 		stereo->fit();
 	}
@@ -562,10 +698,30 @@ void ExtrinsicCalibrationView::draw(NVGcontext* ctx) {
 				drawText(ctx, paths[p], std::to_string(p), 14.0f);
 			}
 		}*/
-		nanogui::Vector2f cpos = wpos + nanogui::Vector2f{10.0f, 10.0f};
-		drawText(ctx, cpos, std::to_string(ctrl_->getFrameCount(i)), 20.0f, NVG_ALIGN_TOP|NVG_ALIGN_LEFT);
+
+		// TODO: move to stereocalibrateimageview
+		nanogui::Vector2f tpos = wpos + nanogui::Vector2f{10.0f, 10.0f};
+		drawText(ctx, tpos, std::to_string(ctrl_->getFrameCount(i)), 20.0f, NVG_ALIGN_TOP|NVG_ALIGN_LEFT);
+
+		tpos = wpos + nanogui::Vector2f{10.0f, wsize.y() - 30.0f};
+		drawText(ctx, tpos, ctrl_->cameraName(i), 20.0f, NVG_ALIGN_TOP|NVG_ALIGN_LEFT);
+
 		nvgResetScissor(ctx);
 	}
+
+	{
+		float h = 14.0f;
+		for (const auto& text : {"Left click: draw line",
+								 "Right click: pan",
+								 "Scroll: zoom",
+								 "C center",
+								 "F fit",
+								 "R clear lines"
+				}) {
+			drawText(ctx, {float(width()) - 60.0, h}, text, 14, NVGalign::NVG_ALIGN_BOTTOM | NVG_ALIGN_MIDDLE);
+			h += 20.0;
+		}
+	}
 }
 
 ExtrinsicCalibrationView::~ExtrinsicCalibrationView() {
@@ -602,7 +758,7 @@ void ExtrinsicCalibrationView::setMode(Mode mode) {
 			wcontrol_->setVisible(false);
 			wcalibration_->setVisible(false);
 			wresults_->setVisible(true);
-			//wresults_->update();
+			wresults_->update();
 			break;
 	}
 	screen()->performLayout();
diff --git a/applications/gui2/src/views/calibration/visualization.hpp b/applications/gui2/src/views/calibration/visualization.hpp
index de2fe29ab0410ebaf68fadceecd98c1749a94149..026c40dc20bd12197503909d8f41990261f96834 100644
--- a/applications/gui2/src/views/calibration/visualization.hpp
+++ b/applications/gui2/src/views/calibration/visualization.hpp
@@ -42,8 +42,45 @@ static void drawChessboardCorners(NVGcontext* ctx, ftl::gui2::ImageView* imview,
 	nvgResetScissor(ctx);
 }
 
+static void drawTriangle(NVGcontext* ctx, const ftl::calibration::CalibrationData::Extrinsic &calib,
+		const nanogui::Vector2f &pos, const nanogui::Vector2f offset, float scale, float sz=1.0f) {
+	const int idx_x = 0;
+	const int idx_y = 2;
+
+	cv::Mat T = calib.matrix();
+	cv::Vec4f p1(cv::Mat(T * cv::Vec4d{sz/2.0f, 0.0f, 0.0f, 1.0f}));
+	cv::Vec4f p2(cv::Mat(T * cv::Vec4d{-sz/2.0f, 0.0f, 0.0f, 1.0f}));
+	cv::Vec4f p3(cv::Mat(T * cv::Vec4d{0.0f, 0.0f, -sz*sqrtf(3.0f)/2.0f, 1.0f}));
+
+	p1[idx_x] -= offset.x();
+	p2[idx_x] -= offset.x();
+	p3[idx_x] -= offset.x();
+	p1[idx_y] -= offset.y();
+	p2[idx_y] -= offset.y();
+	p3[idx_y] -= offset.y();
+	p1 *= scale;
+	p2 *= scale;
+	p3 *= scale;
+
+	nvgBeginPath(ctx);
+
+	// NOTE: flip x
+	nvgMoveTo(ctx, pos.x() + p1[idx_x], pos.y() + p1[idx_y]);
+	nvgLineTo(ctx, pos.x() + p2[idx_x], pos.y() + p2[idx_y]);
+	nvgLineTo(ctx, pos.x() + p3[idx_x], pos.y() + p3[idx_y]);
+	nvgLineTo(ctx, pos.x() + p1[idx_x], pos.y() + p1[idx_y]);
+	if (calib.tvec == cv::Vec3d{0.0, 0.0, 0.0}) {
+		nvgStrokeColor(ctx, nvgRGBA(255, 64, 64, 255));
+	}
+	else {
+		nvgStrokeColor(ctx, nvgRGBA(255, 255, 255, 255));
+	}
+	nvgStrokeWidth(ctx, 1.0f);
+	nvgStroke(ctx);
+}
+
 static void drawFloorPlan(NVGcontext* ctx, nanogui::Widget* parent,
-		const std::vector<ftl::calibration::CalibrationData::Extrinsic>& calib,
+		const std::vector<ftl::calibration::CalibrationData::Calibration>& calib,
 		const std::vector<std::string>& names = {},
 		int origin=0) {
 
@@ -51,35 +88,31 @@ static void drawFloorPlan(NVGcontext* ctx, nanogui::Widget* parent,
 	float miny = INFINITY;
 	float maxx = -INFINITY;
 	float maxy = -INFINITY;
-
+	cv::Vec3f center = {0.0f, 0.0f};
 	std::vector<cv::Point2f> points(calib.size());
 	for (unsigned int i = 0; i < points.size(); i++) {
-		// xz, assume floor on y-plane
-		float x = calib[i].tvec[0];
-		float y = calib[i].tvec[2];
+		const auto& extrinsic = calib[i].extrinsic;
+		// xz, assume floor on y-plane y = 0
+		float x = extrinsic.tvec[0];
+		float y = extrinsic.tvec[2];
 		points[i] = {x, y};
 		minx = std::min(minx, x);
 		miny = std::min(miny, y);
 		maxx = std::max(maxx, x);
 		maxy = std::max(maxy, y);
+		center += extrinsic.tvec;
 	}
-
+	center /= float(points.size());
 	float w = parent->width();
-	float sx = w/(maxx - minx);
+	float dx = maxx - minx;
 	float h = parent->height();
-	float sy = h/(maxy - miny);
-	float s = min(sx, sy);
+	float dy = maxy - miny;
+	float s = min(w/dx, h/dy) * 0.8; // scale
 
 	nanogui::Vector2f apos = parent->absolutePosition().cast<float>() + nanogui::Vector2f{w/2.0f, h/2.0f};
+	nanogui::Vector2f off{center[0], center[2]};
 
 	for (unsigned int i = 0; i < points.size(); i++) {
-		float x = points[i].x*s + apos.x();
-		float y = points[i].y*s + apos.y();
-		// TODO: draw triangles (rotate by camera rotation)
-		nvgBeginPath(ctx);
-		nvgCircle(ctx, x, y, 2.5);
-		nvgStrokeColor(ctx, nvgRGBA(0, 0, 0, 255));
-		nvgStrokeWidth(ctx, 1.0f);
-		nvgStroke(ctx);
+		drawTriangle(ctx, calib[i].extrinsic, apos, off, s, 0.3);
 	}
 }
diff --git a/applications/gui2/src/views/calibration/widgets.cpp b/applications/gui2/src/views/calibration/widgets.cpp
index f907b2ce1a37ecb5f69504b6987f3d186ae361a7..80a4af5d5d4a43ff19b3bf206891af924de456aa 100644
--- a/applications/gui2/src/views/calibration/widgets.cpp
+++ b/applications/gui2/src/views/calibration/widgets.cpp
@@ -126,17 +126,23 @@ void IntrinsicDetails::update(const ftl::calibration::CalibrationData::Intrinsic
 		"(" + to_string(values.cx) + ", " +
 			  to_string(values.cy) + ")");
 
+	new nanogui::Widget(params_);
+	new nanogui::Label(params_,
+			"(" + to_string(100.0*(2.0*values.cx/double(imsize.width) - 1.0)) + "% , " +
+				to_string(100.0*(2.0*values.cy/double(imsize.height) - 1.0)) + "%)");
+	if (use_physical) new nanogui::Widget(params_);
+
 	new nanogui::Label(params_, "Field of View (x):");
 	new nanogui::Label(params_, to_string(fovx) + "°");
-	if (use_physical) new nanogui::Label(params_, "");
+	if (use_physical) new nanogui::Widget(params_);
 
 	new nanogui::Label(params_, "Field of View (y):");
 	new nanogui::Label(params_, to_string(fovy)+ "°");
-	if (use_physical) new nanogui::Label(params_, "");
+	if (use_physical) new nanogui::Widget(params_);
 
 	new nanogui::Label(params_, "Aspect ratio:");
 	new nanogui::Label(params_, to_string(ar));
-	if (use_physical) new nanogui::Label(params_, "");
+	if (use_physical) new nanogui::Widget(params_);
 
 	std::string pK;
 	std::string pP;
diff --git a/applications/gui2/src/views/thumbnails.cpp b/applications/gui2/src/views/thumbnails.cpp
index 7250e176341d7888d3256e2010cdec4aaab6a7a3..b04a6428da734a5d6bda323c6c9526d5971cc7e2 100644
--- a/applications/gui2/src/views/thumbnails.cpp
+++ b/applications/gui2/src/views/thumbnails.cpp
@@ -165,7 +165,7 @@ void Thumbnails::updateThumbnails() {
 				continue;
 			}
 
-			auto* tab = tabwidget_->createTab("Frameset " + std::to_string(fsid));
+			auto* tab = tabwidget_->createTab(framesets[fsid]->name());
 			tab->setLayout(new nanogui::BoxLayout
 				(nanogui::Orientation::Vertical, nanogui::Alignment::Middle, 40));
 			auto* panel = new nanogui::Widget(tab);
diff --git a/applications/gui2/src/widgets/imageview.cpp b/applications/gui2/src/widgets/imageview.cpp
index 3562577975d3d882b3599e6efbbc693811bb5c8f..2137b1ce9f8124c65234952dfaaa93605323bbb5 100644
--- a/applications/gui2/src/widgets/imageview.cpp
+++ b/applications/gui2/src/widgets/imageview.cpp
@@ -538,6 +538,24 @@ StereoImageView::StereoImageView(nanogui::Widget* parent, nanogui::Orientation o
 	right_->setFixedScale(true);
 }
 
+
+nanogui::Vector2f StereoImageView::imageCoordinateAt(const nanogui::Vector2f& p) const {
+
+	nanogui::Vector2f pos = position().cast<float>();
+	nanogui::Vector2f posr = pos + right_->position().cast<float>();
+
+	bool is_right =
+		((p.x() >= posr.x()) && (orientation_ == nanogui::Orientation::Horizontal)) ||
+		((p.y() >= posr.y()) && (orientation_ == nanogui::Orientation::Vertical));
+
+	if (is_right) {
+		return right_->imageCoordinateAt(p - right_->position().cast<float>());
+	}
+	else {
+		return left_->imageCoordinateAt(p);
+	}
+}
+
 bool StereoImageView::mouseMotionEvent(const nanogui::Vector2i &p, const nanogui::Vector2i &rel, int button, int modifiers) {
 	if ((button & (1 << GLFW_MOUSE_BUTTON_RIGHT)) != 0) {
 		nanogui::Vector2f posl = left_->imageCoordinateAt(p.cast<float>());
@@ -554,7 +572,7 @@ bool StereoImageView::mouseMotionEvent(const nanogui::Vector2i &p, const nanogui
 	}
 	return false;
 }
-#include <loguru.hpp>
+
 bool StereoImageView::scrollEvent(const nanogui::Vector2i& p, const nanogui::Vector2f& rel) {
 	// synchronized zoom
 
diff --git a/applications/gui2/src/widgets/imageview.hpp b/applications/gui2/src/widgets/imageview.hpp
index 740152cdf15183a2aed2440c53ea52edb1059fa3..0209636fafd0049e4fcc8304db2b0240f15c8f2d 100644
--- a/applications/gui2/src/widgets/imageview.hpp
+++ b/applications/gui2/src/widgets/imageview.hpp
@@ -217,7 +217,10 @@ public:
 	EIGEN_MAKE_ALIGNED_OPERATOR_NEW
 };
 
-/** Two ImageViews with synchronized zoom and pan. */
+/** Two ImageViews with synchronized zoom and pan. Widget split in two equal
+ * size sections (left and right). With vertical orientation right is the lower
+ * image.
+*/
 class StereoImageView : public nanogui::Widget {
 public:
 	StereoImageView(nanogui::Widget* parent, nanogui::Orientation orientation = nanogui::Orientation::Horizontal);
@@ -232,6 +235,11 @@ public:
 	FTLImageView* left() { return left_; }
 	FTLImageView* right() { return right_; }
 
+	/** get image coordinate at given widget coordinate */
+	nanogui::Vector2f imageCoordinateAt(const nanogui::Vector2f& position) const;
+
+	nanogui::Orientation orientation() { return orientation_; }
+
 	void fit();
 
 	void bindLeft(GLuint id) { left_->texture().free(); left_->bindImage(id); }
diff --git a/components/calibration/include/ftl/calibration/extrinsic.hpp b/components/calibration/include/ftl/calibration/extrinsic.hpp
index 5bded2bc3ddf423d2b8ac4ee7ae2e9a0c7395047..a9f7208e39ff158cb830e0e8900b5d263defe2b6 100644
--- a/components/calibration/include/ftl/calibration/extrinsic.hpp
+++ b/components/calibration/include/ftl/calibration/extrinsic.hpp
@@ -158,14 +158,18 @@ double calibratePair(const cv::Mat &K1, const cv::Mat &D1,
 
 class ExtrinsicCalibration {
 public:
-
-	/** add a single camera. Returns index of camera. */
+	/** add a single (uncalibrated) camera. Returns index of camera. */
 	unsigned int addCamera(const CalibrationData::Intrinsic &);
+
+	/** add a single calibrated camera (if valid calibration). Returns index of camera. */
+	unsigned int addCamera(const CalibrationData::Calibration &);
+
 	/** Add a stereo camera pair. Pairs always use other cameras to estimate
 	 * initial pose. Returns index of first camera. */
 	unsigned int addStereoCamera(const CalibrationData::Intrinsic &, const CalibrationData::Intrinsic &);
-	/** Add stereo camera pair with initial pose. Returns index of first camera. */
-	unsigned int addStereoCamera(const CalibrationData::Intrinsic &, const CalibrationData::Intrinsic &, cv::Vec3d rvec, cv::Vec3d tvec);
+
+	/** Add calibrated stereo camera (if contains valid calibration) */
+	unsigned int addStereoCamera(const CalibrationData::Calibration &, const CalibrationData::Calibration &);
 
 	const CalibrationData::Intrinsic& intrinsic(unsigned int c);
 	const CalibrationData::Extrinsic& extrinsic(unsigned int c);
@@ -201,7 +205,7 @@ public:
 	bool fromFile(const std::string& fname);
 	bool toFile(const std::string& fname); // should return new instance...
 
-	MSGPACK_DEFINE(points_, mask_, pairs_, calib_);
+	MSGPACK_DEFINE(points_, mask_, pairs_, calib_, is_calibrated_);
 
 protected:
 	/** Initial pairwise calibration and triangulation. */
@@ -222,6 +226,12 @@ private:
 	CalibrationPoints<double> points_;
 	std::set<std::pair<unsigned int, unsigned int>> mask_;
 	std::map<std::pair<unsigned int, unsigned int>, std::tuple<cv::Mat, cv::Mat, double>> pairs_;
+	unsigned int c_ref_;
+
+	// true if camera already has valid calibration; initial pose estimation is
+	// skipped (and points are directly triangulated)
+	std::vector<bool> is_calibrated_;
+
 	int min_points_ = 64; // minimum number of points required for pair calibration
 	// TODO: add map {c1,c2} for existing calibration which is used if available.
 	//
diff --git a/components/calibration/include/ftl/calibration/optimize.hpp b/components/calibration/include/ftl/calibration/optimize.hpp
index 17dd1b42c9a33199077f874acddad3ccbb996f87..2e694cd9d7d7d77197543ba8ad7d9e097bf49f53 100644
--- a/components/calibration/include/ftl/calibration/optimize.hpp
+++ b/components/calibration/include/ftl/calibration/optimize.hpp
@@ -59,11 +59,15 @@ struct Camera {
 	cv::Mat extrinsicMatrix() const;
 	cv::Mat extrinsicMatrixInverse() const;
 
+	void toQuaternion();
+	void toAngleAxis();
+
 	cv::Size size;
 
 	const static int n_parameters = 18;
 	const static int n_distortion_parameters = 8;
 
+	bool quaternion = false;
 	double data[n_parameters] = {0.0};
 
 	enum Parameter {
@@ -131,6 +135,9 @@ public:
 
 		bool use_nonmonotonic_steps = false;
 
+		// use quaternion rotation
+		bool use_quaternion = false;
+
 		// fix_camera_extrinsic and fix_camera_intrinsic overlap with some of
 		// the generic options. The more generic setting is always used, the
 		// specific extrinsic/intrinsic options are applied on top of those.
@@ -193,6 +200,11 @@ public:
 	 */
 	void run(const BundleAdjustment::Options& options);
 
+	/**  @brief Get optimized points
+	 *
+	*/
+	std::vector<cv::Point3d> getPoints();
+
 	/** @brief Perform bundle adjustment using default options
 	 */
 	void run();
@@ -205,6 +217,9 @@ public:
 	 */
 	double reprojectionError() const;
 
+	/**/
+	int removeObservations(double threshold);
+
 protected:
 	double* getCameraPtr(int i) { return cameras_.at(i)->data; }
 
@@ -220,6 +235,9 @@ protected:
 	void _buildProblem(ceres::Problem& problem, const BundleAdjustment::Options& options);
 	void _buildBundleAdjustmentProblem(ceres::Problem& problem, const BundleAdjustment::Options& options);
 
+	// remove?
+	void _buildLengthProblem(ceres::Problem& problem, const BundleAdjustment::Options& options);
+
 private:
 	// point to be optimized and corresponding observations
 	struct Point {
diff --git a/components/calibration/include/ftl/calibration/structures.hpp b/components/calibration/include/ftl/calibration/structures.hpp
index e0eeb2fcea82b491637b580680d0398813f90137..9766143742977cb171d9e60fddd7c4db607d7731 100644
--- a/components/calibration/include/ftl/calibration/structures.hpp
+++ b/components/calibration/include/ftl/calibration/structures.hpp
@@ -12,6 +12,8 @@ struct CalibrationData {
 	struct Intrinsic {
 		friend CalibrationData;
 
+		/** 12 distortion coefficients. OpenCV also provides tilted camera model
+		 * coefficients, but not used here. */
 		struct DistortionCoefficients {
 			friend CalibrationData;
 
@@ -25,10 +27,16 @@ struct CalibrationData {
 			 * 2,3			p1-p2 tangential distortion
 			 * 4,5,6,7		r3-r6 radial distortion
 			 * 8,9,10,11	s1-s4 thin prism distortion
+			 *
 			 */
 			double& operator[](unsigned i);
 			double operator[](unsigned i) const;
 
+			/** are radial distortion values are for rational model */
+			bool rationalModel() const;
+			/** is thin prism model is used (s1-s4 set) */
+			bool thinPrism() const;
+
 			/**
 			 * Return distortion parameters in cv::Mat. Shares same memory.
 			 */
@@ -48,6 +56,18 @@ struct CalibrationData {
 		/** New instance with scaled values for new resolution */
 		Intrinsic(const Intrinsic& other, cv::Size sz);
 
+		/* valid values (resolution is non-zero) */
+		bool valid() const;
+
+		/** horizontal field of view in degrees */
+		double fovx() const;
+		/** vertical field of view in degrees */
+		double fovy() const;
+		/** focal length in sensor size units */
+		double focal() const;
+		/** aspect ratio: fx/fy */
+		double aspectRatio() const;
+
 		/** Replace current values with new ones */
 		void set(const cv::Mat &K, cv::Size sz);
 		void set(const cv::Mat &K, const cv::Mat &D, cv::Size sz);
@@ -64,7 +84,7 @@ struct CalibrationData {
 		double cy;
 		DistortionCoefficients distCoeffs;
 
-		/** (optional) sensor size */
+		/** (optional) sensor size; Move elsehwere? */
 		cv::Size2d sensorSize;
 
 		MSGPACK_DEFINE(resolution, fx, fy, cx, cy, distCoeffs, sensorSize);
@@ -79,13 +99,16 @@ struct CalibrationData {
 
 		Extrinsic inverse() const;
 
+		/** valid calibration (values not NAN) */
+		bool valid() const;
+
 		/** get as a 4x4 matrix */
 		cv::Mat matrix() const;
 		/** get 3x3 rotation matrix */
 		cv::Mat rmat() const;
 
-		cv::Vec3d rvec;
-		cv::Vec3d tvec;
+		cv::Vec3d rvec = {NAN, NAN, NAN};
+		cv::Vec3d tvec = {NAN, NAN, NAN};
 		MSGPACK_DEFINE(rvec, tvec);
 	};
 
@@ -107,6 +130,7 @@ struct CalibrationData {
 	bool hasCalibration(ftl::codecs::Channel channel) const;
 
 private:
+	// TODO: identify cameras with unique ID string instead of channel.
 	std::map<ftl::codecs::Channel, Calibration> data_;
 
 public:
diff --git a/components/calibration/src/extrinsic.cpp b/components/calibration/src/extrinsic.cpp
index 4e59a54665a24384a1abc64d13746eb55116e98f..d32961040338efec217048c1017b84588b12d443 100644
--- a/components/calibration/src/extrinsic.cpp
+++ b/components/calibration/src/extrinsic.cpp
@@ -248,7 +248,7 @@ int recoverPose(const cv::Mat &E, const std::vector<cv::Point2d> &_points1,
 	cv::Mat points1(_points1.size(), 2, CV_64FC1);
 	cv::Mat points2(_points2.size(), 2, CV_64FC1);
 
-	CHECK(points1.size() == points2.size());
+	CHECK_EQ(points1.size(), points2.size());
 
 	for (size_t i = 0; i < _points1.size(); i++) {
 		auto p1 = points1.ptr<double>(i);
@@ -309,7 +309,7 @@ double calibratePair(const cv::Mat &K1, const cv::Mat &D1,
 
 	// convert from homogenous coordinates
 	for (int col = 0; col < points3dh.cols; col++) {
-		CHECK(points3dh.at<double>(3, col) != 0);
+		CHECK_NE(points3dh.at<double>(3, col), 0);
 		cv::Point3d p = cv::Point3d(points3dh.at<double>(0, col),
 							points3dh.at<double>(1, col),
 							points3dh.at<double>(2, col))
@@ -332,7 +332,7 @@ double calibratePair(const cv::Mat &K1, const cv::Mat &D1,
 	}
 
 	// needs to be implemented correctly: optimize each pose of the target
-	ba.addObject(object_points);
+	//ba.addObject(object_points);
 
 	double error = ba.reprojectionError();
 
@@ -345,8 +345,8 @@ double calibratePair(const cv::Mat &K1, const cv::Mat &D1,
 		ba.run(options);
 		error = ba.reprojectionError();
 	}
-	CHECK((cv::countNonZero(params1.rvec()) == 0) &&
-		  (cv::countNonZero(params1.tvec()) == 0));
+	CHECK_EQ(cv::countNonZero(params1.rvec()), 0);
+	CHECK_EQ(cv::countNonZero(params1.tvec()), 0);
 
 	return sqrt(error);
 }
@@ -356,13 +356,39 @@ double calibratePair(const cv::Mat &K1, const cv::Mat &D1,
 unsigned int ExtrinsicCalibration::addCamera(const CalibrationData::Intrinsic &c) {
 	unsigned int idx = calib_.size();
 	calib_.push_back({c, {}});
+	calib_optimized_.push_back(calib_.back());
+	is_calibrated_.push_back(false);
+	return idx;
+}
+
+unsigned int ExtrinsicCalibration::addCamera(const CalibrationData::Calibration &c) {
+	unsigned int idx = calib_.size();
+	calib_.push_back(c);
+	calib_optimized_.push_back(calib_.back());
+	is_calibrated_.push_back(true);
 	return idx;
 }
 
 unsigned int ExtrinsicCalibration::addStereoCamera(const CalibrationData::Intrinsic &c1, const CalibrationData::Intrinsic &c2) {
 	unsigned int idx = calib_.size();
 	calib_.push_back({c1, {}});
+	calib_optimized_.push_back(calib_.back());
 	calib_.push_back({c2, {}});
+	calib_optimized_.push_back(calib_.back());
+	is_calibrated_.push_back(false);
+	is_calibrated_.push_back(false);
+	mask_.insert({idx, idx + 1});
+	return idx;
+}
+
+unsigned int ExtrinsicCalibration::addStereoCamera(const CalibrationData::Calibration &c1, const CalibrationData::Calibration &c2) {
+	unsigned int idx = calib_.size();
+	calib_.push_back({c1.intrinsic, c1.extrinsic});
+	calib_optimized_.push_back(calib_.back());
+	calib_.push_back({c2.intrinsic, c2.extrinsic});
+	calib_optimized_.push_back(calib_.back());
+	is_calibrated_.push_back(c1.extrinsic.valid());
+	is_calibrated_.push_back(c2.extrinsic.valid());
 	mask_.insert({idx, idx + 1});
 	return idx;
 }
@@ -462,12 +488,12 @@ void ExtrinsicCalibration::calculateInitialPoses() {
 	}}
 
 	// pick optimal camera: most views of calibration pattern
-	unsigned int c_ref = visibility.argmax();
+	c_ref_ = visibility.argmax();
 
-	auto paths = visibility.shortestPath(c_ref);
+	auto paths = visibility.shortestPath(c_ref_);
 
 	for (unsigned int c = 0; c < camerasCount(); c++) {
-		if (c == c_ref) { continue; }
+		if (c == c_ref_) { continue; }
 
 		cv::Mat R_chain = cv::Mat::eye(cv::Size(3, 3), CV_64FC1);
 		cv::Mat t_chain = cv::Mat(cv::Size(1, 3), CV_64FC1, cv::Scalar(0.0));
@@ -490,7 +516,8 @@ void ExtrinsicCalibration::calculateInitialPoses() {
 		}
 		while(path.size() > 1);
 
-		// note: direction of chain in the loop, hence inverse()
+		// note: direction of chain in the loop (ref to target transformation)
+
 		calib_[c].extrinsic =
 			CalibrationData::Extrinsic(R_chain, t_chain).inverse();
 	}
@@ -545,6 +572,8 @@ double ExtrinsicCalibration::optimize() {
 		// BundleAdjustment stores pointers; do not resize cameras vector
 		ba.addCamera(c);
 	}
+	// TODO (?) is this good idea?; make optional
+	ba.addObject(points_.getObject(0));
 
 	// Transform triangulated points into same coordinates. Poinst which are
 	// triangulated multiple times: use median values. Note T[] contains
@@ -640,6 +669,7 @@ double ExtrinsicCalibration::optimize() {
 	}
 
 	updateStatus_("Bundle adjustment");
+	options_.fix_camera_extrinsic = {int(c_ref_)};
 	options_.verbose = true;
 	options_.max_iter = 250; // should converge much earlier
 
@@ -648,6 +678,10 @@ double ExtrinsicCalibration::optimize() {
 	LOG(INFO) << "fix principal point: " << (options_.fix_principal_point ? "yes" : "no");
 	LOG(INFO) << "fix distortion: " << (options_.fix_distortion ? "yes" : "no");
 
+	ba.run(options_);
+	LOG(INFO) << "removed points: " << ba.removeObservations(2.0);
+	ba.run(options_);
+	LOG(INFO) << "removed points: " << ba.removeObservations(1.0);
 	ba.run(options_);
 
 	calib_optimized_.resize(calib_.size());
@@ -656,13 +690,13 @@ double ExtrinsicCalibration::optimize() {
 	for (unsigned int i = 0; i < cameras.size(); i++) {
 		rmse_[i] = ba.reprojectionError(i);
 		auto intr = cameras[i].intrinsic();
-		auto extr = cameras[i].extrinsic();
 		calib_optimized_[i] = calib_[i];
 		calib_optimized_[i].intrinsic.set(intr.matrix(), intr.distCoeffs.Mat(), intr.resolution);
-		calib_optimized_[i].extrinsic.set(extr.rvec, extr.tvec);
+		calib_optimized_[i].extrinsic.set(cameras[i].rvec(), cameras[i].tvec());
 	}
 
 	rmse_total_ = ba.reprojectionError();
+
 	LOG(INFO) << "reprojection error (all cameras): " << rmse_total_;
 	return rmse_total_;
 }
diff --git a/components/calibration/src/object.cpp b/components/calibration/src/object.cpp
index 83f70a3b69a8d6160c418efe4b6810e082cd78ba..4236b864a1711fee2d180e3f37c703ba96d4def0 100644
--- a/components/calibration/src/object.cpp
+++ b/components/calibration/src/object.cpp
@@ -19,7 +19,7 @@ ArUCoObject::ArUCoObject(cv::aruco::PREDEFINED_DICTIONARY_NAME dictionary,
 	params_ = cv::aruco::DetectorParameters::create();
 	params_->cornerRefinementMinAccuracy = 0.01;
 	// cv::aruco::CORNER_REFINE_CONTOUR memory issues? intrinsic quality?
-	params_->cornerRefinementMethod = cv::aruco::CORNER_REFINE_APRILTAG;
+	params_->cornerRefinementMethod = cv::aruco::CORNER_REFINE_CONTOUR;
 }
 
 std::vector<cv::Point3d> ArUCoObject::object() {
@@ -42,7 +42,9 @@ int ArUCoObject::detect(cv::InputArray im, std::vector<cv::Point2d>& result, con
 	std::vector<std::vector<cv::Point2f>> corners;
 	std::vector<int> ids;
 	cv::Mat im_gray;
-
+	// OpenCV bug: detectMarkers consumes all available memory when any
+	// distortion parameters are passes
+	const cv::Mat d;
 	if (im.size() == cv::Size(0, 0)) {
 		return -1;
 	}
@@ -59,7 +61,7 @@ int ArUCoObject::detect(cv::InputArray im, std::vector<cv::Point2d>& result, con
 	}
 
 	cv::aruco::detectMarkers(im_gray, dict_, corners, ids, params_,
-								cv::noArray(), K, distCoeffs);
+								cv::noArray(), K, d);
 
 
 	if (ids.size() < 2) { return 0; }
diff --git a/components/calibration/src/optimize.cpp b/components/calibration/src/optimize.cpp
index 76db14c07b5a28e22e0db846e3fb4190b5e55eed..a7ff076238bfdadaf835bc1db5c644f5f0b566d3 100644
--- a/components/calibration/src/optimize.cpp
+++ b/components/calibration/src/optimize.cpp
@@ -154,7 +154,7 @@ Mat Camera::distortionCoefficients() const {
 
 Mat Camera::rvec() const {
 	cv::Mat rvec(cv::Size(3, 1), CV_64FC1);
-	CHECK(rvec.step1() == 3);
+	CHECK_EQ(rvec.step1(), 3);
 	ceres::QuaternionToAngleAxis(data + Parameter::ROTATION,
 		(double*)(rvec.data));
 	return rvec;
@@ -166,7 +166,7 @@ Mat Camera::tvec() const {
 
 Mat Camera::rmat() const {
 	cv::Mat R(cv::Size(3, 3), CV_64FC1);
-	CHECK(R.step1() == 3);
+	CHECK_EQ(R.step1(), 3);
 	ceres::QuaternionToRotation<double>(data + Parameter::ROTATION,
 		ceres::RowMajorAdapter3x3<double>((double*)(R.data)));
 
@@ -284,6 +284,29 @@ cv::Point2d ftl::calibration::projectPoint(const Camera& camera, const cv::Point
 }
 
 ////////////////////////////////////////////////////////////////////////////////
+// TODO: estimate pose and optimize it instead (?)
+
+struct LengthError {
+	explicit LengthError(const double d) : d(d) {}
+
+	template <typename T>
+	bool operator()(const T* const p1, const T* const p2, T* residual) const {
+		auto x = p1[0] - p2[0];
+		auto y = p1[1] - p2[1];
+		auto z = p1[2] - p2[2];
+		residual[0] = d - sqrt(x*x + y*y + z*z);
+
+		return true;
+	}
+
+	static ceres::CostFunction* Create(const double d) {
+		return (new ceres::AutoDiffCostFunction<LengthError, 1, 3, 3>(new LengthError(d)));
+	}
+
+	double d;
+};
+
+//
 
 struct ScaleError {
 	ScaleError(const double d, const Point3d& p) : d(d), p(p) {}
@@ -311,8 +334,8 @@ struct ScaleError {
 double ftl::calibration::optimizeScale(const vector<Point3d> &object_points, vector<Point3d> &points) {
 
 	// use exceptions instead
-	CHECK(points.size() % object_points.size() == 0);
-	CHECK(points.size() % 2 == 0);
+	CHECK_EQ(points.size() % object_points.size(), 0);
+	CHECK_EQ(points.size() % 2, 0);
 
 	// initial scale guess from first two object points
 
@@ -420,14 +443,9 @@ void BundleAdjustment::addPoints(const vector<vector<Point2d>>& observations, st
 	}
 }
 
-void BundleAdjustment::addObject(const vector<Point3d> &object_points) {
-	if (points_.size() % object_points.size() != 0) { throw ftl::exception("object does match point count"); }
-	objects_.push_back(BundleAdjustment::Object {0, (int) points_.size(), object_points});
-}
-
 void BundleAdjustment::_setCameraParametrization(ceres::Problem &problem, const BundleAdjustment::Options &options) {
 
-	vector<int> constant_camera_parameters;
+	std::set<int> constant_camera_parameters;
 
 	// apply options
 	for (size_t i = 0; i < cameras_.size(); i++) {
@@ -435,6 +453,9 @@ void BundleAdjustment::_setCameraParametrization(ceres::Problem &problem, const
 			cameras_[i]->data[Camera::Parameter::K4] = 0.0;
 			cameras_[i]->data[Camera::Parameter::K5] = 0.0;
 			cameras_[i]->data[Camera::Parameter::K6] = 0.0;
+			constant_camera_parameters.insert(Camera::Parameter::K4);
+			constant_camera_parameters.insert(Camera::Parameter::K5);
+			constant_camera_parameters.insert(Camera::Parameter::K6);
 		}
 		if (options.zero_distortion) {
 			cameras_[i]->data[Camera::Parameter::K1] = 0.0;
@@ -450,33 +471,33 @@ void BundleAdjustment::_setCameraParametrization(ceres::Problem &problem, const
 
 	// set extrinsic paramters constant for all cameras
 	if (!options.optimize_motion) {
-		constant_camera_parameters.push_back(Camera::Parameter::Q1);
-		constant_camera_parameters.push_back(Camera::Parameter::Q2);
-		constant_camera_parameters.push_back(Camera::Parameter::Q3);
-		constant_camera_parameters.push_back(Camera::Parameter::Q4);
-		constant_camera_parameters.push_back(Camera::Parameter::TX);
-		constant_camera_parameters.push_back(Camera::Parameter::TY);
-		constant_camera_parameters.push_back(Camera::Parameter::TZ);
+		constant_camera_parameters.insert(Camera::Parameter::Q1);
+		constant_camera_parameters.insert(Camera::Parameter::Q2);
+		constant_camera_parameters.insert(Camera::Parameter::Q3);
+		constant_camera_parameters.insert(Camera::Parameter::Q4);
+		constant_camera_parameters.insert(Camera::Parameter::TX);
+		constant_camera_parameters.insert(Camera::Parameter::TY);
+		constant_camera_parameters.insert(Camera::Parameter::TZ);
 	}
 
 	// set intrinsic parameters constant for all cameras
 	if (!options.optimize_intrinsic || options.fix_focal) {
-		constant_camera_parameters.push_back(Camera::Parameter::F);
+		constant_camera_parameters.insert(Camera::Parameter::F);
 	}
 	if (!options.optimize_intrinsic || options.fix_principal_point) {
-		constant_camera_parameters.push_back(Camera::Parameter::CX);
-		constant_camera_parameters.push_back(Camera::Parameter::CY);
+		constant_camera_parameters.insert(Camera::Parameter::CX);
+		constant_camera_parameters.insert(Camera::Parameter::CY);
 	}
 
 	if (!options.optimize_intrinsic || options.fix_distortion) {
-		constant_camera_parameters.push_back(Camera::Parameter::K1);
-		constant_camera_parameters.push_back(Camera::Parameter::K2);
-		constant_camera_parameters.push_back(Camera::Parameter::K3);
-		constant_camera_parameters.push_back(Camera::Parameter::K4);
-		constant_camera_parameters.push_back(Camera::Parameter::K5);
-		constant_camera_parameters.push_back(Camera::Parameter::K6);
-		constant_camera_parameters.push_back(Camera::Parameter::P1);
-		constant_camera_parameters.push_back(Camera::Parameter::P2);
+		constant_camera_parameters.insert(Camera::Parameter::K1);
+		constant_camera_parameters.insert(Camera::Parameter::K2);
+		constant_camera_parameters.insert(Camera::Parameter::K3);
+		constant_camera_parameters.insert(Camera::Parameter::K4);
+		constant_camera_parameters.insert(Camera::Parameter::K5);
+		constant_camera_parameters.insert(Camera::Parameter::K6);
+		constant_camera_parameters.insert(Camera::Parameter::P1);
+		constant_camera_parameters.insert(Camera::Parameter::P2);
 	}
 
 	if (!options.optimize_motion && !options.optimize_intrinsic) {
@@ -486,14 +507,14 @@ void BundleAdjustment::_setCameraParametrization(ceres::Problem &problem, const
 		}
 	}
 	else {
-		std::unordered_set<int> fix_extrinsic(
+		std::set<int> fix_extrinsic(
 			options.fix_camera_extrinsic.begin(), options.fix_camera_extrinsic.end());
 
-		std::unordered_set<int> fix_intrinsic(
+		std::set<int> fix_intrinsic(
 			options.fix_camera_extrinsic.begin(), options.fix_camera_extrinsic.end());
 
 		for (size_t i = 0; i < cameras_.size(); i++) {
-			std::unordered_set<int> constant_parameters(
+			std::set<int> constant_parameters(
 				constant_camera_parameters.begin(),
 				constant_camera_parameters.end());
 
@@ -591,6 +612,7 @@ void BundleAdjustment::_buildBundleAdjustmentProblem(ceres::Problem &problem, co
 void BundleAdjustment::_buildProblem(ceres::Problem &problem, const BundleAdjustment::Options &options) {
 
 	_buildBundleAdjustmentProblem(problem, options);
+	_buildLengthProblem(problem, options);
 }
 
 void BundleAdjustment::run(const BundleAdjustment::Options &bundle_adjustment_options) {
@@ -627,6 +649,40 @@ void BundleAdjustment::run() {
 	run(options);
 }
 
+int BundleAdjustment::removeObservations(double threshold) {
+	int removed = 0;
+	std::vector<double> error(cameras_.size(), 0.0);
+
+	for (auto& point : points_) {
+		double error_total = 0.0;
+		double n_points = 0.0;
+
+		for (unsigned int c = 0; c < cameras_.size(); c++) {
+			if (!point.visibility[c]) { continue; }
+			const auto& obs = point.observations[c];
+			const auto& proj = projectPoint(*(cameras_[c]), point.point);
+			double err = pow(proj.x - obs.x, 2) + pow(proj.y - obs.y, 2);
+			error[c] = err;
+			error_total += err;
+			n_points += 1;
+		}
+		error_total /= n_points;
+
+		if (n_points <= 1) { continue; } // TODO: remove observation completely
+
+		for (unsigned int c = 0; c < cameras_.size(); c++) {
+			if (!point.visibility[c]) { continue; }
+			if ((error[c] - error_total) > threshold) {
+				point.visibility[c] = false;
+				n_points -= 1;
+				removed++;
+				break;
+			}
+		}
+	}
+	return removed;
+}
+
 void BundleAdjustment::_reprojectionErrorSE(const int camera, double &error, double &npoints) const {
 	error = 0.0;
 	npoints = 0.0;
@@ -658,3 +714,53 @@ double BundleAdjustment::reprojectionError() const {
 	}
 	return sqrt(error / npoints);
 }
+
+////
+
+void BundleAdjustment::addObject(const std::vector<cv::Point3d> &object_points) {
+	if (points_.size() % object_points.size() != 0) { throw std::exception(); }
+	objects_.push_back(BundleAdjustment::Object {0, (int) points_.size(), object_points});
+}
+
+void BundleAdjustment::_buildLengthProblem(ceres::Problem &problem, const BundleAdjustment::Options &options) {
+
+	// same idea as in scale optimization
+
+	ceres::LossFunction *loss_function = nullptr;
+
+	// should use separate configuration option
+	/*
+	if (options.loss == Options::Loss::HUBER) {
+		loss_function = new ceres::HuberLoss(1.0);
+	}
+	else if (options.loss == Options::Loss::CAUCHY) {
+		loss_function = new ceres::CauchyLoss(1.0);
+	}
+	*/
+
+	for (auto &object : objects_) {
+		int npoints = object.object_points.size();
+		auto &object_points = object.object_points;
+
+		vector<double> d;
+		for (int i = 0; i < npoints; i++) {
+			for (int j = i + 1; j < npoints; j++) {
+				d.push_back(norm(object_points[i]-object_points[j]));
+			}
+		}
+
+		for (int p = object.idx_start; p < object.idx_end; p += npoints) {
+			size_t i_d = 0;
+			for (size_t i = 0; i < object_points.size(); i++) {
+				for (size_t j = i + 1; j < object_points.size(); j++) {
+					double* p1 = static_cast<double*>(&(points_[p+i].point.x));
+					double* p2 = static_cast<double*>(&(points_[p+j].point.x));
+
+					auto cost_function = LengthError::Create(d[i_d++]);
+
+					problem.AddResidualBlock(cost_function, loss_function, p1, p2);
+				}
+			}
+		}
+	}
+}
diff --git a/components/calibration/src/structures.cpp b/components/calibration/src/structures.cpp
index c464e80b9d59997edc364af0286fcf69d01097e3..36d3ff4fedab5c28d790d2f3e9750aab28783685 100644
--- a/components/calibration/src/structures.cpp
+++ b/components/calibration/src/structures.cpp
@@ -6,6 +6,8 @@
 #include <ftl/calibration/structures.hpp>
 #include <ftl/calibration/parameters.hpp>
 
+#include <cmath>
+
 using ftl::calibration::CalibrationData;
 
 CalibrationData::Intrinsic::DistortionCoefficients::DistortionCoefficients() :
@@ -84,8 +86,11 @@ cv::Mat CalibrationData::Intrinsic::matrix(cv::Size size) const {
 	return ftl::calibration::scaleCameraMatrix(matrix(), resolution, size);
 }
 
-////////////////////////////////////////////////////////////////////////////////
+bool CalibrationData::Intrinsic::valid() const {
+	return (resolution == cv::Size{0, 0});
+}
 
+////////////////////////////////////////////////////////////////////////////////
 
 void CalibrationData::Extrinsic::set(const cv::Mat& T) {
 	if (T.type() != CV_64FC1) {
@@ -165,6 +170,12 @@ cv::Mat CalibrationData::Extrinsic::rmat() const {
 	return R;
 }
 
+bool CalibrationData::Extrinsic::valid() const {
+	return !(
+		std::isnan(tvec[0]) || std::isnan(tvec[1]) || std::isnan(tvec[2]) ||
+		std::isnan(rvec[0]) || std::isnan(rvec[1]) || std::isnan(rvec[2]));
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 
 CalibrationData CalibrationData::readFile(const std::string &path) {
diff --git a/components/codecs/include/ftl/codecs/shapes.hpp b/components/codecs/include/ftl/codecs/shapes.hpp
index 3fffaee3e209000733bba5f94f4cf84f01cce87a..313381887bee18c8b70eec45127c4fd7c662352b 100644
--- a/components/codecs/include/ftl/codecs/shapes.hpp
+++ b/components/codecs/include/ftl/codecs/shapes.hpp
@@ -21,8 +21,6 @@ enum class Shape3DType {
 };
 
 struct Shape3D {
-	Shape3D() {};
-
 	int id;
 	Shape3DType type;
 	Eigen::Vector3f size;
diff --git a/components/rgbd-sources/src/sources/stereovideo/rectification.cpp b/components/rgbd-sources/src/sources/stereovideo/rectification.cpp
index f755d5de89c62fc5272fc125d43ab7d2452cecfb..e495ef263e18f7e349b51704582fb2511f7e7689 100644
--- a/components/rgbd-sources/src/sources/stereovideo/rectification.cpp
+++ b/components/rgbd-sources/src/sources/stereovideo/rectification.cpp
@@ -91,6 +91,7 @@ void StereoRectification::updateCalibration_() {
 }
 
 void StereoRectification::calculateParameters_() {
+	// cv::stereoRectify() throws an exception if parameters are invalid
 	if (!valid_) { return; }
 
 	cv::Mat K_l = calib_left_.intrinsic.matrix(image_resolution_);
@@ -98,7 +99,6 @@ void StereoRectification::calculateParameters_() {
 	cv::Mat dc_l = calib_left_.intrinsic.distCoeffs.Mat();
 	cv::Mat dc_r = calib_right_.intrinsic.distCoeffs.Mat();
 
-	// calculate rectification parameters
 	cv::stereoRectify(	K_l, dc_l, K_r, dc_r, image_resolution_,
 						R_, t_, R_l_, R_r_, P_l_, P_r_, Q_, 0, 0);
 
@@ -198,6 +198,8 @@ cv::Mat StereoRectification::cameraMatrix(Channel c) {
 		else if (c == Channel::Right) {
 			// Extrinsics are included in P_r_, can't do same as above
 			throw ftl::exception("Not implemented");
+			// not tested!
+			return cv::Mat(P_r_(cv::Rect(0, 0, 3, 3)) * R_r_.t());
 		}
 	}
 	else {
diff --git a/components/rgbd-sources/src/sources/stereovideo/rectification.hpp b/components/rgbd-sources/src/sources/stereovideo/rectification.hpp
index d326d0d167b6cae7f06bdccd1bad6be1b6bfca40..756747e04d29bc95d66496be111fcd660e3c7709 100644
--- a/components/rgbd-sources/src/sources/stereovideo/rectification.hpp
+++ b/components/rgbd-sources/src/sources/stereovideo/rectification.hpp
@@ -35,12 +35,14 @@ public:
 	*/
 	void setInterpolation(int interpolation);
 
-	/**
-	 * Calculate rectification parameters from given calibration.
+	/** Calculate rectification parameters from given calibration.
 	 */
 	void setCalibration(ftl::calibration::CalibrationData &calib);
 	bool calibrated();
 
+	/** Rectify image. Valid channels Left and Right. No-op if disabled with
+	 * setEnabled() or calibration parameters have not been set.
+	 */
 	void rectify(cv::InputArray im, cv::OutputArray out, ftl::codecs::Channel c);
 
 	/** Enable/disable rectification. TODO: move outside (to stereovideo)?
@@ -53,7 +55,7 @@ public:
 	 */
 	cv::Mat getPose(ftl::codecs::Channel c = ftl::codecs::Channel::Left);
 
-	/** Get intrinsic matrix. */
+	/** Get intrinsic matrix. Not implemented for right channel. */
 	cv::Mat cameraMatrix(ftl::codecs::Channel c = ftl::codecs::Channel::Left);
 	cv::Mat cameraMatrix(cv::Size size, ftl::codecs::Channel c = ftl::codecs::Channel::Left);
 
@@ -74,17 +76,17 @@ private:
 	cv::Size image_resolution_;
 
 	// rectification parameters
-	bool enabled_;
-	bool valid_;
+	bool enabled_; // rectification enabled
+	bool valid_; // instance contains valid parameters
 	int interpolation_;
 	double baseline_;
 	cv::Mat R_; // rotation left to right
 	cv::Mat t_; // translation left to right
-	cv::Mat Q_;
-	cv::Mat R_l_;
-	cv::Mat R_r_;
-	cv::Mat P_l_;
-	cv::Mat P_r_;
+	cv::Mat Q_; // disparity to depth matrix
+	cv::Mat R_l_; // rotation for left camera: unrectified to rectified
+	cv::Mat R_r_; // rotation for right camera: unrectified to rectified
+	cv::Mat P_l_; // rectified projection matrix for left camera
+	cv::Mat P_r_; // rectified projection matrix for right camera
 
 	// rectification maps for cv::remap(); should be CV_16SC2 if remap done on
 	// CPU and CV_32SC2 for GPU (generated by calculateParameters(), used by
diff --git a/components/rgbd-sources/src/sources/stereovideo/stereovideo.cpp b/components/rgbd-sources/src/sources/stereovideo/stereovideo.cpp
index b7f3d6ce3123335fcbe77d7cf720beb27f9db6dc..ecd902da1e67d28f92523352fafb80d25a2daa26 100644
--- a/components/rgbd-sources/src/sources/stereovideo/stereovideo.cpp
+++ b/components/rgbd-sources/src/sources/stereovideo/stereovideo.cpp
@@ -23,6 +23,7 @@
 #include <ftl/operators/mask.hpp>
 
 #include <ftl/rgbd/capabilities.hpp>
+#include <ftl/codecs/shapes.hpp>
 #include <ftl/calibration/structures.hpp>
 
 #include "stereovideo.hpp"
@@ -239,9 +240,9 @@ void StereoVideoSource::updateParameters(ftl::rgbd::Frame &frame) {
 
 		frame.setPose() = pose;
 
-		cv::Mat K;
+		cv::Mat K = rectification_->cameraMatrix(color_size_);
+		float fx = static_cast<float>(K.at<double>(0,0));
 
-		// same for left and right
 		float baseline = static_cast<float>(rectification_->baseline());
 		float doff = rectification_->doff(color_size_);
 
@@ -249,14 +250,8 @@ void StereoVideoSource::updateParameters(ftl::rgbd::Frame &frame) {
 		float min_depth = this->host_->getConfig().value<double>("min_depth", 0.45);
 		float max_depth = this->host_->getConfig().value<double>("max_depth", (lsrc_->isStereo()) ? 12.0 : 1.0);
 
-		// left
-
-		K = rectification_->cameraMatrix(color_size_);
-		float fx = static_cast<float>(K.at<double>(0,0));
-
 		if (d_resolution > 0.0) {
-			// Learning OpenCV p. 442
-			// TODO: remove, should not be used here
+			// Learning OpenCV p. 442; TODO: remove. should not be applied here
 			float max_depth_new = sqrt(d_resolution * fx * baseline);
 			max_depth = (max_depth_new > max_depth) ? max_depth : max_depth_new;
 		}
@@ -275,27 +270,20 @@ void StereoVideoSource::updateParameters(ftl::rgbd::Frame &frame) {
 			doff
 		};
 
+		CHECK(params.fx > 0) << "focal length must be positive";
+		CHECK(params.fy > 0) << "focal length must be positive";
+		CHECK(params.cx < 0) << "bad principal point coordinate (negative value)";
+		CHECK(-params.cx < params.width) << "principal point must be inside image";
+		CHECK(params.cy < 0) << "bad principal point coordinate (negative value)";
+		CHECK(-params.cy < params.height) << "principal point must be inside image";
+		CHECK(params.baseline >= 0.0) << "baseline must be positive";
+
 		host_->getConfig()["focal"] = params.fx;
 		host_->getConfig()["centre_x"] = params.cx;
 		host_->getConfig()["centre_y"] = params.cy;
 		host_->getConfig()["baseline"] = params.baseline;
 		host_->getConfig()["doffs"] = params.doffs;
 
-		// right
-		/* not used
-		K = rectification_->cameraMatrix(color_size_, Channel::Right);
-		frame.setRight() = {
-			static_cast<float>(K.at<double>(0,0)),	// Fx
-			static_cast<float>(K.at<double>(1,1)),	// Fy
-			static_cast<float>(-K.at<double>(0,2)),	// Cx
-			static_cast<float>(-K.at<double>(1,2)),	// Cy
-			(unsigned int) color_size_.width,
-			(unsigned int) color_size_.height,
-			min_depth,
-			max_depth,
-			baseline,
-			doff
-		};*/
 	} else {
 		Eigen::Matrix4d pose;
 		auto& params = frame.setLeft();
@@ -337,6 +325,20 @@ bool StereoVideoSource::retrieve(ftl::rgbd::Frame &frame) {
 
 	if (!cap_status_) return false;
 
+	if (host_->value("add_right_pose", false)) {
+		auto shapes = frame.create<std::list<ftl::codecs::Shape3D>>(Channel::Shapes3D);
+		Eigen::Matrix4d pose;
+		cv::cv2eigen(rectification_->getPose(Channel::Right), pose);
+		Eigen::Matrix4f posef = pose.cast<float>();
+		shapes.list.push_back(ftl::codecs::Shape3D{
+				1,
+				ftl::codecs::Shape3DType::CAMERA,
+				Eigen::Vector3f{0.2, 0.2, 0.2},
+				posef,
+				std::string("Right Camera")
+		});
+	}
+
 	if (do_update_params_) {
 		updateParameters(frame);
 		do_update_params_ = false;