diff --git a/applications/gui2/src/modules/calibration/extrinsic.cpp b/applications/gui2/src/modules/calibration/extrinsic.cpp
index b0ced5bb10af804b3c3a1f999c4fa344bf2a69d6..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});
@@ -75,11 +77,17 @@ void ExtrinsicCalibration::start(unsigned int fsid, std::vector<FrameID> sources
 		auto sz = cv::Size((int) frame.getLeftCamera().width, (int) frame.getLeftCamera().height);
 		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(
-			CalibrationData::Intrinsic(getCalibration(cl).intrinsic, sz),
-			CalibrationData::Intrinsic(getCalibration(cr).intrinsic, sz));
+		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
@@ -298,6 +306,8 @@ void ExtrinsicCalibration::stereoRectify(int cl, int cr,
 
 	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());
diff --git a/applications/gui2/src/views/calibration/extrinsicview.cpp b/applications/gui2/src/views/calibration/extrinsicview.cpp
index c637ab249563d615984f2ab60f8c43862df5f65a..863d26ef97cd98c3d2c756c4392bd22cc3f53dd5 100644
--- a/applications/gui2/src/views/calibration/extrinsicview.cpp
+++ b/applications/gui2/src/views/calibration/extrinsicview.cpp
@@ -481,7 +481,7 @@ void ExtrinsicCalibrationView::ResultsWindow::draw(NVGcontext* ctx) {
 
 ////////////////////////////////////////////////////////////////////////////////
 
-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");
@@ -497,63 +497,88 @@ static void  drawText(NVGcontext* ctx, nanogui::Vector2f &pos, const std::string
 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:
-	bool draw_line_ = false;
-	float row_image_ = 0;
+	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) {
-		float row = round(imageCoordinateAt(p.cast<float>()).y());
-		if (row == row_image_ && draw_line_) {
-			draw_line_ = false;
-		}
-		else {
-			draw_line_ = true;
-			row_image_ = row;
-		}
+		// 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);
-	if (!draw_line_) { return; }
-
-	// assumes vertical alignment (horizontal not tested)
+	// 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();
-	nanogui::Vector2f l = left()->positionForCoordinate({0.0f, row_image_}) + left()->position().cast<float>();
-	nanogui::Vector2f r = right()->positionForCoordinate({0.0f, row_image_}) + right()->position().cast<float>();
 	float swidth = std::max(1.0f, left()->scale());
+	int c = 0; // color
 
-	for (auto& p : {l, r}) {
-		nvgScissor(ctx, x, y, w, h);
-		nvgBeginPath(ctx);
-		nvgMoveTo(ctx, x, p.y() - swidth*0.5f); // "half a pixel" shift
-		nvgLineTo(ctx, x + w, p.y() - swidth*0.5f);
-		nvgStrokeColor(ctx, nvgRGBA(255, 32, 32, (swidth == 1.0f) ? 255 : 96));
-		nvgStrokeWidth(ctx, swidth);
-		nvgStroke(ctx);
+	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);
 
-		if (swidth*0.5f > 2.0f) {
+		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, nvgRGBA(0, 0, 0, 196));
-			nvgStrokeWidth(ctx, 1.0f);
+			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;
 		}
-		nvgResetScissor(ctx);
-		y += h;
+		c++;
 	}
 }
 
@@ -672,6 +697,8 @@ void ExtrinsicCalibrationView::draw(NVGcontext* ctx) {
 				drawText(ctx, paths[p], std::to_string(p), 14.0f);
 			}
 		}*/
+
+		// 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);
 
@@ -680,6 +707,20 @@ void ExtrinsicCalibrationView::draw(NVGcontext* ctx) {
 
 		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() {