diff --git a/applications/gui2/src/modules/calibration/extrinsic.cpp b/applications/gui2/src/modules/calibration/extrinsic.cpp
index 9ed307139e212e633a5f847505db45508c4a5df1..bf7c892f3abfee2b82bdfd91b46f33e1f009f5ee 100644
--- a/applications/gui2/src/modules/calibration/extrinsic.cpp
+++ b/applications/gui2/src/modules/calibration/extrinsic.cpp
@@ -419,6 +419,9 @@ const std::vector<cv::Point2d>& ExtrinsicCalibration::previousPoints(int camera)
 	return state_.points_prev[camera];
 }
 
+
+
+
 int ExtrinsicCalibration::getFrameCount(int camera) {
 	return state_.calib.points().getCount(unsigned(camera));
 }
diff --git a/applications/gui2/src/views/calibration/extrinsicview.cpp b/applications/gui2/src/views/calibration/extrinsicview.cpp
index d2f9f64dd2069ac36b59f50bc1fe6a40e61b5221..c4071ccf1f76af4cec1d9000d2486c3665fd530e 100644
--- a/applications/gui2/src/views/calibration/extrinsicview.cpp
+++ b/applications/gui2/src/views/calibration/extrinsicview.cpp
@@ -378,8 +378,8 @@ void ExtrinsicCalibrationView::CalibrationWindow::build() {
 		auto* b = new nanogui::Button(use_extrinsics, std::to_string(n));
 		b->setFlags(nanogui::Button::Flags::ToggleButton);
 		b->setPushed(ctrl_->calib().useExtrinsic(n));
+		b->setEnabled(ctrl_->calib().calibration(n).extrinsic.valid());
 		b->setChangeCallback([this, n](bool v){
-			LOG(INFO) << "CHANGED";
 			ctrl_->calib().setUseExtrinsic(n, v);
 		});
 	}
@@ -413,6 +413,7 @@ void ExtrinsicCalibrationView::CalibrationWindow::build() {
 	for (int n = 0; n < ctrl_->cameraCount(); n++) {
 		auto* b = new nanogui::Button(fix_extrinsics, std::to_string(n));
 		b->setFlags(nanogui::Button::Flags::ToggleButton);
+		b->setEnabled(ctrl_->calib().useExtrinsic(n));
 		b->setPushed(ctrl_->calib().options().fix_camera_extrinsic.count(n));
 		b->setChangeCallback([this, n](bool v){
 			if (v) {
diff --git a/components/calibration/include/ftl/calibration/extrinsic.hpp b/components/calibration/include/ftl/calibration/extrinsic.hpp
index 185a45c174842b61454541822ce39e042babb4db..ef2e48595e258622e6dfadddd8ac6f83a539d48a 100644
--- a/components/calibration/include/ftl/calibration/extrinsic.hpp
+++ b/components/calibration/include/ftl/calibration/extrinsic.hpp
@@ -206,17 +206,29 @@ public:
 	MSGPACK_DEFINE(points_, mask_, pairs_, calib_, is_calibrated_);
 
 protected:
+	/** Calculate initial pose and triangulate points **/
+	void calculatePairPose(unsigned int c1, unsigned int c2);
+
+	/** Only triangulate points using existing calibration */
+	void triangulate(unsigned int c1, unsigned int c2);
+
 	/** Initial pairwise calibration and triangulation. */
 	void calculatePairPoses();
 
-	/** Calculate initial poses from pairs */
+	/** Calculate initial poses from pairs for non calibrated cameras */
 	void calculateInitialPoses();
 
 	/** Bundle adjustment on initial poses and triangulations. */
 	double optimize();
 
+	/** Select optimal camera for chains. Optimal camera has most visibility and
+	 * is already calibrated (if any initial calibrations included).
+	 */
+	int selectOptimalCamera();
+
 private:
 	void updateStatus_(std::string);
+
 	std::vector<CalibrationData::Calibration> calib_;
 	std::vector<CalibrationData::Calibration> calib_optimized_;
 	ftl::calibration::BundleAdjustment::Options options_;
@@ -224,7 +236,6 @@ 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)
diff --git a/components/calibration/include/ftl/calibration/structures.hpp b/components/calibration/include/ftl/calibration/structures.hpp
index 9766143742977cb171d9e60fddd7c4db607d7731..6b1f5b46ebdfcb902abcd68796c804d8efdda852 100644
--- a/components/calibration/include/ftl/calibration/structures.hpp
+++ b/components/calibration/include/ftl/calibration/structures.hpp
@@ -115,6 +115,10 @@ struct CalibrationData {
 	struct Calibration {
 		Intrinsic intrinsic;
 		Extrinsic extrinsic;
+
+		/** 4x4 projection matrix */
+		cv::Mat matrix();
+
 		MSGPACK_DEFINE(intrinsic, extrinsic);
 	};
 
diff --git a/components/calibration/src/extrinsic.cpp b/components/calibration/src/extrinsic.cpp
index ac8c037a578f14b1afaeacfe31a5da2aaa295d42..2857527b324c3bd34ffbff9d039093c67e7b37d3 100644
--- a/components/calibration/src/extrinsic.cpp
+++ b/components/calibration/src/extrinsic.cpp
@@ -290,6 +290,24 @@ int recoverPose(const cv::Mat &E, const std::vector<cv::Point2d> &_points1,
 							distanceThresh, cv::noArray(), triangulatedPoints);
 }
 
+static double scalePoints(const std::vector<cv::Point3d> &object_points, const cv::Mat& points_in, std::vector<cv::Point3d> &points_out) {
+
+	points_out.clear();
+	points_out.reserve(points_in.cols);
+	// convert from homogenous coordinates
+	for (int col = 0; col < points_in.cols; col++) {
+		CHECK_NE(points_in.at<double>(3, col), 0);
+		cv::Point3d p = cv::Point3d(points_in.at<double>(0, col),
+							points_in.at<double>(1, col),
+							points_in.at<double>(2, col))
+							/ points_in.at<double>(3, col);
+		points_out.push_back(p);
+	}
+
+	double s = ftl::calibration::optimizeScale(object_points, points_out);
+	return s;
+}
+
 double calibratePair(const cv::Mat &K1, const cv::Mat &D1,
 		const cv::Mat &K2, const cv::Mat &D2,
 		const std::vector<cv::Point2d> &points1,
@@ -304,20 +322,7 @@ double calibratePair(const cv::Mat &K1, const cv::Mat &D1,
 	// distanceThresh unit?
 	recoverPose(E, points1, points2, K1, K2, R, t, 1000.0, points3dh);
 
-	points_out.clear();
-	points_out.reserve(points3dh.cols);
-
-	// convert from homogenous coordinates
-	for (int col = 0; col < points3dh.cols; col++) {
-		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))
-							/ points3dh.at<double>(3, col);
-		points_out.push_back(p);
-	}
-
-	double s = ftl::calibration::optimizeScale(object_points, points_out);
+	double s = scalePoints(object_points, points3dh, points_out);
 	t = t * s;
 
 	auto params1 = Camera(K1, D1, cv::Mat::eye(3, 3, CV_64FC1), cv::Mat::zeros(3, 1, CV_64FC1), {0, 0});
@@ -403,12 +408,87 @@ void ExtrinsicCalibration::updateStatus_(std::string str) {
 	std::atomic_store(&status_, std::make_shared<std::string>(str));
 }
 
-void ExtrinsicCalibration::calculatePairPoses() {
+void ExtrinsicCalibration::calculatePairPose(unsigned int c1, unsigned int c2) {
 
 	const auto& visibility =  points_.visibility();
+	// calculate paramters and update triangulation
+
+	cv::Mat K1 = calib_[c1].intrinsic.matrix();
+	cv::Mat distCoeffs1 = calib_[c1].intrinsic.distCoeffs.Mat();
+	cv::Mat K2 = calib_[c2].intrinsic.matrix();
+	cv::Mat distCoeffs2 = calib_[c2].intrinsic.distCoeffs.Mat();
+	auto pts = points().getPoints({c1, c2}, 0);
+	auto object = points().getObject(0);
+	cv::Mat R, t;
+	std::vector<cv::Point3d> points3d;
+	auto rmse = calibratePair(K1, distCoeffs1, K2, distCoeffs2,
+		pts[0], pts[1], object, R, t, points3d, true);
+
+	// debug info
+	LOG(INFO) << "RMSE (cameras " << c1 << " & " << c2 << "): " << rmse;
+
+	points().setTriangulatedPoints(c1, c2, points3d);
+
+	pairs_[{c1, c2}] = {R, t, rmse};
+
+	cv::Mat R_i, t_i;
+	R.copyTo(R_i);
+	t.copyTo(t_i);
+	transform::inverse(R_i, t_i);
+	pairs_[{c2, c1}] = {R_i, t_i, rmse};
+}
+
+void ExtrinsicCalibration::triangulate(unsigned int c1, unsigned int c2) {
+
+	cv::Mat T, R, t, R_i, t_i;
+
+	T = calib_[c1].extrinsic.matrix() *
+		transform::inverse(calib_[c2].extrinsic.matrix());
+	transform::getRotationAndTranslation(T, R_i, t_i);
+
+	T = calib_[c2].extrinsic.matrix() *
+			transform::inverse(calib_[c1].extrinsic.matrix());
+	transform::getRotationAndTranslation(T, R, t);
+
+	pairs_[{c1, c1}] = {R, t, NAN};
+	pairs_[{c2, c1}] = {R_i, t_i, NAN};
+
+	auto pts = points().getPoints({c1, c2}, 0);
+	std::vector<cv::Point3d> pointsw;
+
+	const auto& calib1 = calib_[c1];
+	const auto& calib2 = calib_[c2];
+
+	CHECK_EQ(pts[0].size(), pts[1].size());
+
+	cv::Mat pts1(1, pts[0].size(), CV_64FC2, pts[0].data());
+	cv::Mat pts2(1, pts[1].size(), CV_64FC2, pts[1].data());
+	cv::Mat out(1, pts[0].size(), CV_64FC2);
+
+	// Undistort points first. Note: undistortPoints() returns points in
+	// normalized coordinates, therefore projection matrices for
+	// cv::triangulatePoints() only include extrinsic parameters.
+	std::vector<cv::Point2d> pts1u, pts2u;
+	cv::undistortPoints(pts1, pts1u, calib1.intrinsic.matrix(), calib1.intrinsic.distCoeffs.Mat());
+	cv::undistortPoints(pts2, pts2u, calib2.intrinsic.matrix(), calib2.intrinsic.distCoeffs.Mat());
+
+	cv::Mat P1 = cv::Mat::eye(3, 4, CV_64FC1);
+	cv::Mat P2 = T(cv::Rect(0, 0, 4, 3));
+
+	// documentation claims cv::triangulatePoints() requires floats; however
+	// seems to only work with doubles (documentation outdated?).
+	// According to https://stackoverflow.com/a/16299909 cv::triangulatePoints()
+	// implements least squares method described in H&Z p312
+	cv::triangulatePoints(P1, P2, pts1u, pts2u, out);
+	// scalePoints() converts to non-homogenous coordinates and estimates scale
+	scalePoints(points().getObject(0), out, pointsw);
+	points().setTriangulatedPoints(c1, c2, pointsw);
+}
+
+void ExtrinsicCalibration::calculatePairPoses() {
+
 	// Calibrate all pairs. TODO: might be expensive if number of cameras is high
-	// if not performed for all pairs, remaining non-triangulated poits have to
-	// be separately triangulated later.
+	// if not performed for all pairs.
 
 	int i = 1;
 	int i_max = (camerasCount() * camerasCount()) / 2 + 1;
@@ -417,7 +497,9 @@ void ExtrinsicCalibration::calculatePairPoses() {
 	for (unsigned int c2 = c1; c2 < camerasCount(); c2++) {
 
 		updateStatus_(	"Calculating pose for pair " +
-						std::to_string(i++) + " of " + std::to_string(i_max));
+						std::to_string(i++) + " of " + std::to_string(i_max) +
+						" and triangulating points");
+
 
 		if (c1 == c2) {
 			pairs_[{c1, c2}] = { cv::Mat::eye(cv::Size(3, 3), CV_64FC1),
@@ -427,43 +509,44 @@ void ExtrinsicCalibration::calculatePairPoses() {
 			continue;
 		}
 
-		if (mask_.count({c1, c2}) > 0 ) { continue; }
-		if (pairs_.find({c1, c2}) != pairs_.end()) { continue; }
+		if (pairs_.find({c1, c2}) != pairs_.end()) {
+			LOG(WARNING) << "pair already processed (this shold not happen)";
+			continue;
+		}
 
 		// require minimum number of visible points
-		if (visibility.count(c1, c2) < min_points_) {
+		if (points().visibility().count(c1, c2) < min_points_) {
 			LOG(WARNING) << "skipped pair (" << c1 << ", " << c2 << "), not enough points";
-			continue;
+			return;
 		}
 
-		// calculate paramters and update triangulation
-
-		cv::Mat K1 = calib_[c1].intrinsic.matrix();
-		cv::Mat distCoeffs1 = calib_[c1].intrinsic.distCoeffs.Mat();
-		cv::Mat K2 = calib_[c2].intrinsic.matrix();
-		cv::Mat distCoeffs2 = calib_[c2].intrinsic.distCoeffs.Mat();
-		auto pts = points().getPoints({c1, c2}, 0);
-		auto object = points().getObject(0);
-		cv::Mat R, t;
-		std::vector<cv::Point3d> points3d;
-		auto rmse = calibratePair(K1, distCoeffs1, K2, distCoeffs2,
-			pts[0], pts[1], object, R, t, points3d, true);
-
-		// debug info
-		LOG(INFO) << "RMSE (cameras " << c1 << " & " << c2 << "): " << rmse;
-
-		points().setTriangulatedPoints(c1, c2, points3d);
-
-		pairs_[{c1, c2}] = {R, t, rmse};
-
-		cv::Mat R_i, t_i;
-		R.copyTo(R_i);
-		t.copyTo(t_i);
-		transform::inverse(R_i, t_i);
-		pairs_[{c2, c1}] = {R_i, t_i, rmse};
+		if (is_calibrated_[c1] && is_calibrated_[c2]) {
+			triangulate(c1, c2);
+		}
+		else {
+			if (mask_.count({c1, c2}) > 0 ) { continue; }
+			calculatePairPose(c1, c2);
+		}
 	}}
 }
 
+int ExtrinsicCalibration::selectOptimalCamera() {
+	// Pick optimal camera: most views of calibration pattern. If existing
+	// calibration is used, reference camera must already be calibrated.
+	int c = 0;
+	int calibrated_c_ref_max = 0;
+	for (unsigned int i = 0; i < is_calibrated_[i]; i++) {
+		if (is_calibrated_[i] && points().getCount(i) > calibrated_c_ref_max) {
+			calibrated_c_ref_max = points().getCount(i);
+			c = i;
+		}
+	}
+	if (!calibrated_c_ref_max == 0) {
+		c = points().visibility().argmax();
+	}
+	return c;
+}
+
 void ExtrinsicCalibration::calculateInitialPoses() {
 	updateStatus_("Initial poses from chained transformations");
 
@@ -480,20 +563,19 @@ void ExtrinsicCalibration::calculateInitialPoses() {
 		if (pairs_.count({c1, c2}) == 0) {
 			visibility.mask(c1, c2);
 		}
-
-		// mask bad pairs (high rmse)
-		/*if (std::get<2>(pairs_.at({c1, c2})) > 16.0) {
-			visibility.mask(c1, c2);
-		}*/
 	}}
 
-	// pick optimal camera: most views of calibration pattern
-	c_ref_ = visibility.argmax();
+	// select optimal camera to calculate chains to
+	auto c_ref = selectOptimalCamera();
 
-	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 (is_calibrated_[c]) {
+			// already calibrated. skip chain
+			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));
@@ -517,7 +599,6 @@ void ExtrinsicCalibration::calculateInitialPoses() {
 		while(path.size() > 1);
 
 		// note: direction of chain in the loop (ref to target transformation)
-
 		calib_[c].extrinsic =
 			CalibrationData::Extrinsic(R_chain, t_chain).inverse();
 	}
@@ -629,8 +710,6 @@ double ExtrinsicCalibration::optimize() {
 			if (m == 0) {
 				n_points_missing++;
 				break;
-				// not triangulated (see earlier steps)
-				// TODO: triangulate here
 			}
 			if (n % 2 == 0 && n > 1) {
 				// mean of two points if number of points even
@@ -663,13 +742,10 @@ double ExtrinsicCalibration::optimize() {
 	if (float(n_points_missing)/float(n_points - n_points_bad) > threhsold_warning_) {
 		// low number of points; most points only visible in pairs?
 		LOG(WARNING) << "Large number of points skipped. Are there enough "
-						"visible points between stereo camera pairs? (TODO: "
-						"implement necessary triangulation after pair "
-						"calibration)";
+						"visible points between stereo camera pairs?";
 	}
 
 	updateStatus_("Bundle adjustment");
-	options_.fix_camera_extrinsic = {int(c_ref_)};
 	options_.verbose = true;
 	options_.max_iter = 250; // should converge much earlier
 
diff --git a/components/calibration/src/optimize.cpp b/components/calibration/src/optimize.cpp
index a7ff076238bfdadaf835bc1db5c644f5f0b566d3..40005872504cdcb01c0a46907b84b1bb636c3b5d 100644
--- a/components/calibration/src/optimize.cpp
+++ b/components/calibration/src/optimize.cpp
@@ -359,7 +359,7 @@ double ftl::calibration::optimizeScale(const vector<Point3d> &object_points, vec
 
 	vector<double> d;
 	ceres::Problem problem;
-	auto loss_function = new ceres::HuberLoss(1.0);
+	ceres::LossFunction* loss_function = nullptr;
 
 	// use all pairwise distances
 	for (size_t i = 0; i < object_points.size(); i++) {
diff --git a/components/calibration/src/structures.cpp b/components/calibration/src/structures.cpp
index 36d3ff4fedab5c28d790d2f3e9750aab28783685..e215043850e916d3b7f344c17b7ad07bae1b92b5 100644
--- a/components/calibration/src/structures.cpp
+++ b/components/calibration/src/structures.cpp
@@ -87,7 +87,7 @@ cv::Mat CalibrationData::Intrinsic::matrix(cv::Size size) const {
 }
 
 bool CalibrationData::Intrinsic::valid() const {
-	return (resolution == cv::Size{0, 0});
+	return (resolution != cv::Size{0, 0});
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -243,3 +243,18 @@ CalibrationData::Calibration& CalibrationData::get(ftl::codecs::Channel channel)
 bool CalibrationData::hasCalibration(ftl::codecs::Channel channel) const {
 	return data_.count(channel) != 0;
 }
+
+// ==== CalibrationData::Calibration ===========================================
+#include <loguru.hpp>
+cv::Mat CalibrationData::Calibration::matrix() {
+	if (!intrinsic.valid() || !extrinsic.valid()) {
+		throw ftl::exception("Invalid calibration");
+	}
+
+	cv::Mat P = extrinsic.matrix();
+	cv::Mat R = P(cv::Rect(0, 0, 3, 3));
+	R = intrinsic.matrix() * R;
+	LOG(INFO) << extrinsic.matrix();
+	LOG(INFO) << P;
+	return P;
+}
\ No newline at end of file