diff --git a/applications/gui2/src/modules/camera.cpp b/applications/gui2/src/modules/camera.cpp
index 670695023f9cbcb04cacd6326cf21e115e6d7300..36e0e37793c1a66dc356e121166820af3ad7706c 100644
--- a/applications/gui2/src/modules/camera.cpp
+++ b/applications/gui2/src/modules/camera.cpp
@@ -5,6 +5,10 @@
 #include <ftl/rgbd/capabilities.hpp>
 #include <chrono>
 
+#include <opencv2/imgproc.hpp>
+#include <opencv2/imgcodecs.hpp>
+#include <opencv2/cudaarithm.hpp>
+
 #include <loguru.hpp>
 
 using ftl::gui2::Camera;
@@ -341,6 +345,19 @@ void Camera::startStreaming(const std::unordered_set<ftl::codecs::Channel> &chan
 	io->feed()->startStreaming(filter);
 }
 
+void Camera::snapshot(const std::string &filename) {
+	auto ptr = std::atomic_load(&latest_);
+	if (ptr) {
+		auto &frame = ptr->frames[frame_idx];
+		if (frame.hasChannel(channel)) {
+			const auto &snap = frame.get<cv::Mat>(channel);
+			cv::Mat output;
+			cv::cvtColor(snap, output, cv::COLOR_BGRA2BGR);
+			cv::imwrite(filename, output);
+		}
+	}
+}
+
 ftl::cuda::TextureObject<uchar4>& Camera::getFrame() {
 	if (std::atomic_load(&current_fs_)) {
 		auto& frame = current_fs_->frames[frame_idx].cast<ftl::rgbd::Frame>();
diff --git a/applications/gui2/src/modules/camera.hpp b/applications/gui2/src/modules/camera.hpp
index 16bf9f1b44751382d911ecac0aba66f8524122dd..7405c8c5091dda95b43fdfd44dd6d594b4ff8193 100644
--- a/applications/gui2/src/modules/camera.hpp
+++ b/applications/gui2/src/modules/camera.hpp
@@ -61,6 +61,8 @@ public:
 	void startRecording(const std::string &filename, const std::unordered_set<ftl::codecs::Channel> &channels);
 	void startStreaming(const std::unordered_set<ftl::codecs::Channel> &channels);
 
+	void snapshot(const std::string &filename);
+
 private:
 	int frame_idx = -1;
 	ftl::data::FrameID frame_id_;
diff --git a/applications/gui2/src/views/camera.cpp b/applications/gui2/src/views/camera.cpp
index 04a14afde483d5e0801884bda4f1a34773fa7aa6..565381ebc11f6512d6f12c8b0e9a1e92db4eec5c 100644
--- a/applications/gui2/src/views/camera.cpp
+++ b/applications/gui2/src/views/camera.cpp
@@ -377,6 +377,29 @@ CameraView::CameraView(ftl::gui2::Screen* parent, ftl::gui2::Camera* ctrl) :
 		imview_->setCursor(nanogui::Cursor::Crosshair);
 		mod->setCursor(nanogui::Cursor::Crosshair);
 	}
+
+	auto theme = dynamic_cast<ftl::gui2::Screen*>(screen())->getTheme("toolbutton");
+	//this->setTheme(theme);
+
+	context_menu_ = new nanogui::Window(parent, "");
+	context_menu_->setVisible(false);
+	context_menu_->setLayout(new nanogui::BoxLayout(nanogui::Orientation::Vertical));
+	context_menu_->setTheme(theme);
+
+	auto *button = new nanogui::Button(context_menu_, "Capture Image");
+	button->setCallback([this]() {
+		char timestamp[18];
+		std::time_t t=std::time(NULL);
+		std::strftime(timestamp, sizeof(timestamp), "%F-%H%M%S", std::localtime(&t));
+		context_menu_->setVisible(false);
+		ctrl_->snapshot(std::string(timestamp)+std::string(".png"));
+	});
+
+	button = new nanogui::Button(context_menu_, "Settings");
+	button->setCallback([this, button]() {
+		context_menu_->setVisible(false);
+		ctrl_->screen->getModule<ftl::gui2::ConfigCtrl>()->show(ctrl_->getID());
+	});
 }
 
 CameraView::~CameraView() {
@@ -385,6 +408,11 @@ CameraView::~CameraView() {
 		// should be fixed in nanogui
 		panel_->dispose();
 	}
+
+	if (context_menu_->parent()->getRefCount() > 0) {
+		context_menu_->setVisible(false);
+		context_menu_->dispose();
+	}
 }
 
 void CameraView::refresh() {
@@ -434,7 +462,16 @@ bool CameraView::mouseButtonEvent(const Eigen::Vector2i &p, int button, bool dow
 		if (pos.x() >= 0.0f && pos.y() >= 0.0f) {
 			ctrl_->touch(0, ftl::codecs::TouchType::MOUSE_LEFT, pos.x(), pos.y(), 0.0f, (down) ? 255 : 0);
 		}
+		context_menu_->setVisible(false);
 		return true;
+	} else if (button == 1) {
+		if (!down) {
+			context_menu_->setPosition(p - mPos);
+			context_menu_->setVisible(true);
+			return true;
+		}
+	} else {
+		context_menu_->setVisible(false);
 	}
 	return false;
 }
diff --git a/applications/gui2/src/views/camera.hpp b/applications/gui2/src/views/camera.hpp
index 6965a2903d07ecd916acc69e8e2a00023493412e..efeb4221b177b0a4bc15066ce5b5ad8160a7a480 100644
--- a/applications/gui2/src/views/camera.hpp
+++ b/applications/gui2/src/views/camera.hpp
@@ -34,6 +34,7 @@ protected:
 	Camera* ctrl_;
 	MediaPanel* panel_;
 	FTLImageView* imview_;
+	nanogui::Window *context_menu_;
 
 public:
 	EIGEN_MAKE_ALIGNED_OPERATOR_NEW
diff --git a/applications/gui2/src/views/camera3d.cpp b/applications/gui2/src/views/camera3d.cpp
index 06e6e2400433ae81d1ad6a70e4ecc1c824868f17..9d1c0fea298ba1e51380cf2691455d5a83b6be52 100644
--- a/applications/gui2/src/views/camera3d.cpp
+++ b/applications/gui2/src/views/camera3d.cpp
@@ -65,8 +65,7 @@ bool CameraView3D::keyboardEvent(int key, int scancode, int action, int modifier
 }
 
 bool CameraView3D::mouseButtonEvent(const Eigen::Vector2i &p, int button, bool down, int modifiers) {
-	LOG(INFO) << "mouseButtonEvent: " << p;
-	return true;
+	return CameraView::mouseButtonEvent(p, button, down, modifiers);
 }
 
 bool CameraView3D::mouseMotionEvent(const Eigen::Vector2i &p, const Eigen::Vector2i &rel, int button, int modifiers) {
diff --git a/components/streams/src/feed.cpp b/components/streams/src/feed.cpp
index 0eeadf3d309dc3ee01cc47c1ea28704046a89074..8a2eb4a2840a95f726b0b6db77d2534da943cb90 100644
--- a/components/streams/src/feed.cpp
+++ b/components/streams/src/feed.cpp
@@ -65,13 +65,13 @@ std::list<ftl::data::FrameSetPtr> Feed::Filter::getLatestFrameSets() {
 	SHARED_LOCK(feed_->mtx_, lk);
 	if (sources_.empty()) {
 		for (auto &i : feed_->latest_) {
-			results.emplace_back(std::atomic_load(&(i.second)));
+			if (i.second) results.emplace_back(std::atomic_load(&(i.second)));
 		}
 	} else {
 		for (auto &s : sources_) {
 			auto i = feed_->latest_.find(s);
 			if (i != feed_->latest_.end()) {
-				results.emplace_back(std::atomic_load(&(i->second)));
+				if (i->second) results.emplace_back(std::atomic_load(&(i->second)));
 			}
 		}
 	}
@@ -181,10 +181,10 @@ Feed::Feed(nlohmann::json &config, ftl::net::Universe*net) :
 				pre_pipelines_[fs->frameset()]->apply(*fs, *fs, 0);
 			}
 
-			// FIXME: Adding new to latest_ will modify data structure in non-threadsafe way!
-			std::atomic_store(&latest_[fs->frameset()], fs);
-
 			SHARED_LOCK(mtx_, lk);
+
+			std::atomic_store(&latest_.at(fs->frameset()), fs);
+
 			for (auto* filter : filters_) {
 				// TODO: smarter update (update only when changed) instead of
 				// filter->channels_available_ = fs->channels();
@@ -603,6 +603,7 @@ std::string Feed::getName(const std::string &puri) {
 
 void Feed::add(uint32_t fsid, const std::string &uri, ftl::stream::Stream* stream) {
 	fsid_lookup_[uri] = fsid;
+	latest_[fsid] = nullptr;
 	streams_[fsid].push_back(stream);
 
 	_createPipeline(fsid);
@@ -677,6 +678,7 @@ uint32_t Feed::add(const std::string &path) {
 		// Make the source object
 		ftl::data::DiscreteSource *source;
 
+		latest_[fsid] = nullptr;
 		lk.unlock();
 
 		if (uri.getBaseURI() == "device:render" || uri.getBaseURI() == "device:openvr") {
@@ -768,7 +770,8 @@ uint32_t Feed::getID(const std::string &source) {
 
 const std::unordered_set<Channel> Feed::availableChannels(ftl::data::FrameID id) {
 	ftl::data::FrameSetPtr fs;
-	std::atomic_store(&fs, latest_[id.frameset()]);
+	// FIXME: Should this be locked?
+	std::atomic_store(&fs, latest_.at(id.frameset()));
 	if (fs && fs->hasFrame(id.source())) {
 		return (*fs.get())[id.source()].allChannels();
 	}
@@ -780,8 +783,10 @@ std::vector<ftl::data::FrameID> Feed::listFrames() {
 	SHARED_LOCK(mtx_, lk);
 	result.reserve(fsid_lookup_.size());
 	for (const auto [k, fs] : latest_) {
-		for (unsigned i = 0; i < fs->frames.size(); i++) {
-			result.push_back(ftl::data::FrameID(k, i));
+		if (fs) {
+			for (unsigned i = 0; i < fs->frames.size(); i++) {
+				result.push_back(ftl::data::FrameID(k, i));
+			}
 		}
 	}
 	return result;
@@ -817,8 +822,9 @@ std::vector<unsigned int> Feed::listFrameSets() {
 	std::vector<unsigned int> result;
 	result.reserve(fsid_lookup_.size());
 	for (const auto [k, fs] : latest_) {
-		std::ignore = fs;
-		result.push_back(k);
+		if (fs) {
+			result.push_back(k);
+		}
 	}
 	return result;
 }