[libcamera-devel,RFC/PATCH,5/5] qcam: Add RAW capture support

Message ID 20200430003604.2423018-6-niklas.soderlund@ragnatech.se
State Superseded
Headers show
Series
  • qcam: Add RAW capture support
Related show

Commit Message

Niklas Söderlund April 30, 2020, 12:36 a.m. UTC
Add a toolbar button that captures RAW data to disk. The button is only
enabled if the camera is configured to provide a raw stream to the
application.

Only when the capture action is triggered will a request with a raw
buffer be queued to the camera.

Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
---
 src/qcam/assets/feathericons/feathericons.qrc |  1 +
 src/qcam/main_window.cpp                      | 94 ++++++++++++++++++-
 src/qcam/main_window.h                        |  4 +
 3 files changed, 98 insertions(+), 1 deletion(-)

Comments

Laurent Pinchart April 30, 2020, 7:44 p.m. UTC | #1
Hi Niklas,

Thank you for the patch.

On Thu, Apr 30, 2020 at 02:36:04AM +0200, Niklas Söderlund wrote:
> Add a toolbar button that captures RAW data to disk. The button is only
> enabled if the camera is configured to provide a raw stream to the
> application.
> 
> Only when the capture action is triggered will a request with a raw
> buffer be queued to the camera.
> 
> Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
> ---
>  src/qcam/assets/feathericons/feathericons.qrc |  1 +
>  src/qcam/main_window.cpp                      | 94 ++++++++++++++++++-
>  src/qcam/main_window.h                        |  4 +
>  3 files changed, 98 insertions(+), 1 deletion(-)
> 
> diff --git a/src/qcam/assets/feathericons/feathericons.qrc b/src/qcam/assets/feathericons/feathericons.qrc
> index c4eb7a0be6884373..fc8213928ece70ea 100644
> --- a/src/qcam/assets/feathericons/feathericons.qrc
> +++ b/src/qcam/assets/feathericons/feathericons.qrc
> @@ -1,5 +1,6 @@
>  <!DOCTYPE RCC><RCC version="1.0">
>  <qresource>
> +<file>./aperture.svg</file>
>  <file>./camera-off.svg</file>
>  <file>./play-circle.svg</file>
>  <file>./save.svg</file>
> diff --git a/src/qcam/main_window.cpp b/src/qcam/main_window.cpp
> index f57aaf4a27e5f4ca..535fa53d6705a1a9 100644
> --- a/src/qcam/main_window.cpp
> +++ b/src/qcam/main_window.cpp
> @@ -7,10 +7,13 @@
>  
>  #include "main_window.h"
>  
> +#include <fcntl.h>
>  #include <iomanip>
>  #include <sstream>
>  #include <string>
>  #include <sys/mman.h>
> +#include <sys/stat.h>
> +#include <unistd.h>
>  
>  #include <QComboBox>
>  #include <QCoreApplication>
> @@ -49,7 +52,8 @@ public:
>  };
>  
>  MainWindow::MainWindow(CameraManager *cm, const OptionsParser::Options &options)
> -	: options_(options), cm_(cm), allocator_(nullptr), isCapturing_(false)
> +	: options_(options), cm_(cm), allocator_(nullptr), isCapturing_(false),
> +	  captureRaw_(false)
>  {
>  	int ret;
>  
> @@ -149,6 +153,14 @@ int MainWindow::createToolbars()
>  				     "Save As...");
>  	connect(action, &QAction::triggered, this, &MainWindow::saveViewfinder);
>  
> +	/* Save Raw action. */
> +	action = toolbar_->addAction(QIcon::fromTheme("document-save-as",

This will end up using the same icon as for the regulare image capture
on platforms whose theme offer document-save-as. Could we pick a
different one ?

> +						      QIcon(":aperture.svg")),
> +				     "Save Raw");
> +	action->setEnabled(false);
> +	connect(action, &QAction::triggered, this, &MainWindow::saveRaw);
> +	saveRaw_ = action;
> +
>  	return 0;
>  }
>  
> @@ -369,6 +381,9 @@ int MainWindow::startCapture()
>  
>  	adjustSize();
>  
> +	/* Configure the raw capture button. */
> +	saveRaw_->setEnabled(config_->size() == 2);

Shouldn't the normal save button be enabled and disabled when starting
and stopping capture too ?

> +
>  	/* Allocate and map buffers. */
>  	allocator_ = new FrameBufferAllocator(camera_);
>  	for (StreamConfiguration &config : *config_) {
> @@ -474,6 +489,7 @@ void MainWindow::stopCapture()
>  		return;
>  
>  	viewfinder_->stop();
> +	saveRaw_->setEnabled(false);
>  
>  	int ret = camera_->stop();
>  	if (ret)
> @@ -514,6 +530,11 @@ void MainWindow::saveViewfinder()
>  	saveViewfinder_ = true;
>  }
>  
> +void MainWindow::saveRaw()
> +{
> +	captureRaw_ = true;
> +}
> +
>  /* -----------------------------------------------------------------------------
>   * Request Completion Handling
>   */
> @@ -558,6 +579,9 @@ void MainWindow::processCapture()
>  		if (buffers.count(vfStream_))
>  			processViewfinder(buffers[vfStream_]);
>  
> +		if (buffers.count(rawStream_))
> +			processRaw(buffers[rawStream_]);
> +
>  		/*
>  		 * Return buffers so they can be reused. No processing involving
>  		 * a buffer can happen after they are returned to the free list.
> @@ -618,6 +642,61 @@ void MainWindow::processViewfinder(FrameBuffer *buffer)
>  	}
>  }
>  
> +void MainWindow::processRaw(FrameBuffer *raw)

s/raw/buffer/ ?

> +{
> +	/* TODO: Should write a DNG file instead of a .raw and .jpeg file.  */
> +
> +	unsigned int seq = raw->metadata().sequence;
> +	std::string filename;
> +	int fd, ret = 0;
> +	size_t pos;
> +
> +	/* Write .raw */
> +	filename = defaultPath_.toStdString() + "/raw-#.raw";
> +	pos = filename.find_first_of('#');
> +	if (pos != std::string::npos) {
> +		std::stringstream ss;
> +		ss << std::setw(6) << std::setfill('0') << seq;
> +		filename.replace(pos, 1, ss.str());
> +	}

Same comment as for the previous patch, use QString.

> +	qInfo() << "Saving" << filename.c_str();
> +
> +	fd = open(filename.c_str(), O_CREAT | O_WRONLY |
> +		  (pos == std::string::npos ? O_APPEND : O_TRUNC),
> +		  S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
> +	if (fd == -1) {
> +		qWarning() << "Failed to open raw output file";
> +		return;
> +	}

And use QFile :-)

> +
> +	const MappedBuffer &info = mappedBuffers_[raw];
> +	ret = ::write(fd, info.memory, info.size);
> +	if (ret < 0) {
> +		ret = -errno;
> +		qWarning() << "write error: " << strerror(-ret);
> +	} else if (ret != (int)info.size) {
> +		qWarning() << "write error: only " << ret
> +			   << " bytes written instead of " << info.size;
> +	}
> +
> +	::close(fd);
> +
> +	/* Write scaled thumbnail .jpeg */
> +	filename = defaultPath_.toStdString() + "/raw-#.jpeg";
> +	pos = filename.find_first_of('#');
> +	if (pos != std::string::npos) {
> +		std::stringstream ss;
> +		ss << std::setw(6) << std::setfill('0') << seq;
> +		filename.replace(pos, 1, ss.str());
> +	}
> +	qInfo() << "Saving" << filename.c_str();
> +
> +	QImage image = viewfinder_->getCurrentImage().scaledToHeight(640);

That's a big thumbnail. And a weird height value, did you mean
scaledToWidth(), or 480 instead of 640 ?

> +	QImageWriter writer(QString::fromUtf8(filename.c_str()));
> +	writer.setQuality(95);
> +	writer.write(image);
> +}
> +
>  void MainWindow::queueRequest(FrameBuffer *buffer)
>  {
>  	Request *request = camera_->createRequest();
> @@ -628,5 +707,18 @@ void MainWindow::queueRequest(FrameBuffer *buffer)
>  
>  	request->addBuffer(vfStream_, buffer);
>  
> +	if (captureRaw_) {
> +		QMutexLocker locker(&mutex_);
> +
> +		if (freeBuffers_[rawStream_].isEmpty()) {
> +			qWarning() << "Raw stream buffer empty";
> +			return;

Shouldn't we still queue the request, without a raw buffer ?

> +		}
> +
> +		request->addBuffer(rawStream_,
> +				   freeBuffers_[rawStream_].dequeue());
> +		captureRaw_ = false;
> +	}
> +
>  	camera_->queueRequest(request);
>  }
> diff --git a/src/qcam/main_window.h b/src/qcam/main_window.h
> index 580bcac146fabe07..37a1d95351e144e0 100644
> --- a/src/qcam/main_window.h
> +++ b/src/qcam/main_window.h
> @@ -55,6 +55,7 @@ private Q_SLOTS:
>  	void toggleCapture(bool start);
>  
>  	void saveViewfinder();
> +	void saveRaw();
>  
>  	void queueRequest(FrameBuffer *buffer);
>  
> @@ -70,10 +71,12 @@ private:
>  	void requestComplete(Request *request);
>  	void processCapture();
>  	void processViewfinder(FrameBuffer *buffer);
> +	void processRaw(FrameBuffer *raw);

s/raw/buffer/ ?

>  
>  	/* UI elements */
>  	QToolBar *toolbar_;
>  	QAction *startStopAction_;
> +	QAction *saveRaw_;
>  	ViewFinder *viewfinder_;
>  
>  	QIcon iconPlay_;
> @@ -97,6 +100,7 @@ private:
>  	/* Capture state, buffers queue and statistics */
>  	bool isCapturing_;
>  	bool saveViewfinder_;
> +	bool captureRaw_;
>  	Stream *vfStream_;
>  	Stream *rawStream_;
>  	std::map<Stream *, QQueue<FrameBuffer *>> freeBuffers_;
Niklas Söderlund May 1, 2020, 12:29 p.m. UTC | #2
Hi Laurent,

Thanks for your feedback.

On 2020-04-30 22:44:17 +0300, Laurent Pinchart wrote:
> Hi Niklas,
> 
> Thank you for the patch.
> 
> On Thu, Apr 30, 2020 at 02:36:04AM +0200, Niklas Söderlund wrote:
> > Add a toolbar button that captures RAW data to disk. The button is only
> > enabled if the camera is configured to provide a raw stream to the
> > application.
> > 
> > Only when the capture action is triggered will a request with a raw
> > buffer be queued to the camera.
> > 
> > Signed-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>
> > ---
> >  src/qcam/assets/feathericons/feathericons.qrc |  1 +
> >  src/qcam/main_window.cpp                      | 94 ++++++++++++++++++-
> >  src/qcam/main_window.h                        |  4 +
> >  3 files changed, 98 insertions(+), 1 deletion(-)
> > 
> > diff --git a/src/qcam/assets/feathericons/feathericons.qrc b/src/qcam/assets/feathericons/feathericons.qrc
> > index c4eb7a0be6884373..fc8213928ece70ea 100644
> > --- a/src/qcam/assets/feathericons/feathericons.qrc
> > +++ b/src/qcam/assets/feathericons/feathericons.qrc
> > @@ -1,5 +1,6 @@
> >  <!DOCTYPE RCC><RCC version="1.0">
> >  <qresource>
> > +<file>./aperture.svg</file>
> >  <file>./camera-off.svg</file>
> >  <file>./play-circle.svg</file>
> >  <file>./save.svg</file>
> > diff --git a/src/qcam/main_window.cpp b/src/qcam/main_window.cpp
> > index f57aaf4a27e5f4ca..535fa53d6705a1a9 100644
> > --- a/src/qcam/main_window.cpp
> > +++ b/src/qcam/main_window.cpp
> > @@ -7,10 +7,13 @@
> >  
> >  #include "main_window.h"
> >  
> > +#include <fcntl.h>
> >  #include <iomanip>
> >  #include <sstream>
> >  #include <string>
> >  #include <sys/mman.h>
> > +#include <sys/stat.h>
> > +#include <unistd.h>
> >  
> >  #include <QComboBox>
> >  #include <QCoreApplication>
> > @@ -49,7 +52,8 @@ public:
> >  };
> >  
> >  MainWindow::MainWindow(CameraManager *cm, const OptionsParser::Options &options)
> > -	: options_(options), cm_(cm), allocator_(nullptr), isCapturing_(false)
> > +	: options_(options), cm_(cm), allocator_(nullptr), isCapturing_(false),
> > +	  captureRaw_(false)
> >  {
> >  	int ret;
> >  
> > @@ -149,6 +153,14 @@ int MainWindow::createToolbars()
> >  				     "Save As...");
> >  	connect(action, &QAction::triggered, this, &MainWindow::saveViewfinder);
> >  
> > +	/* Save Raw action. */
> > +	action = toolbar_->addAction(QIcon::fromTheme("document-save-as",
> 
> This will end up using the same icon as for the regulare image capture
> on platforms whose theme offer document-save-as. Could we pick a
> different one ?

Good point.

> 
> > +						      QIcon(":aperture.svg")),
> > +				     "Save Raw");
> > +	action->setEnabled(false);
> > +	connect(action, &QAction::triggered, this, &MainWindow::saveRaw);
> > +	saveRaw_ = action;
> > +
> >  	return 0;
> >  }
> >  
> > @@ -369,6 +381,9 @@ int MainWindow::startCapture()
> >  
> >  	adjustSize();
> >  
> > +	/* Configure the raw capture button. */
> > +	saveRaw_->setEnabled(config_->size() == 2);
> 
> Shouldn't the normal save button be enabled and disabled when starting
> and stopping capture too ?

Maybe but that is work for a different patch :-)

> 
> > +
> >  	/* Allocate and map buffers. */
> >  	allocator_ = new FrameBufferAllocator(camera_);
> >  	for (StreamConfiguration &config : *config_) {
> > @@ -474,6 +489,7 @@ void MainWindow::stopCapture()
> >  		return;
> >  
> >  	viewfinder_->stop();
> > +	saveRaw_->setEnabled(false);
> >  
> >  	int ret = camera_->stop();
> >  	if (ret)
> > @@ -514,6 +530,11 @@ void MainWindow::saveViewfinder()
> >  	saveViewfinder_ = true;
> >  }
> >  
> > +void MainWindow::saveRaw()
> > +{
> > +	captureRaw_ = true;
> > +}
> > +
> >  /* -----------------------------------------------------------------------------
> >   * Request Completion Handling
> >   */
> > @@ -558,6 +579,9 @@ void MainWindow::processCapture()
> >  		if (buffers.count(vfStream_))
> >  			processViewfinder(buffers[vfStream_]);
> >  
> > +		if (buffers.count(rawStream_))
> > +			processRaw(buffers[rawStream_]);
> > +
> >  		/*
> >  		 * Return buffers so they can be reused. No processing involving
> >  		 * a buffer can happen after they are returned to the free list.
> > @@ -618,6 +642,61 @@ void MainWindow::processViewfinder(FrameBuffer *buffer)
> >  	}
> >  }
> >  
> > +void MainWindow::processRaw(FrameBuffer *raw)
> 
> s/raw/buffer/ ?
> 
> > +{
> > +	/* TODO: Should write a DNG file instead of a .raw and .jpeg file.  */
> > +
> > +	unsigned int seq = raw->metadata().sequence;
> > +	std::string filename;
> > +	int fd, ret = 0;
> > +	size_t pos;
> > +
> > +	/* Write .raw */
> > +	filename = defaultPath_.toStdString() + "/raw-#.raw";
> > +	pos = filename.find_first_of('#');
> > +	if (pos != std::string::npos) {
> > +		std::stringstream ss;
> > +		ss << std::setw(6) << std::setfill('0') << seq;
> > +		filename.replace(pos, 1, ss.str());
> > +	}
> 
> Same comment as for the previous patch, use QString.
> 
> > +	qInfo() << "Saving" << filename.c_str();
> > +
> > +	fd = open(filename.c_str(), O_CREAT | O_WRONLY |
> > +		  (pos == std::string::npos ? O_APPEND : O_TRUNC),
> > +		  S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
> > +	if (fd == -1) {
> > +		qWarning() << "Failed to open raw output file";
> > +		return;
> > +	}
> 
> And use QFile :-)
> 
> > +
> > +	const MappedBuffer &info = mappedBuffers_[raw];
> > +	ret = ::write(fd, info.memory, info.size);
> > +	if (ret < 0) {
> > +		ret = -errno;
> > +		qWarning() << "write error: " << strerror(-ret);
> > +	} else if (ret != (int)info.size) {
> > +		qWarning() << "write error: only " << ret
> > +			   << " bytes written instead of " << info.size;
> > +	}
> > +
> > +	::close(fd);
> > +
> > +	/* Write scaled thumbnail .jpeg */
> > +	filename = defaultPath_.toStdString() + "/raw-#.jpeg";
> > +	pos = filename.find_first_of('#');
> > +	if (pos != std::string::npos) {
> > +		std::stringstream ss;
> > +		ss << std::setw(6) << std::setfill('0') << seq;
> > +		filename.replace(pos, 1, ss.str());
> > +	}
> > +	qInfo() << "Saving" << filename.c_str();
> > +
> > +	QImage image = viewfinder_->getCurrentImage().scaledToHeight(640);
> 
> That's a big thumbnail. And a weird height value, did you mean
> scaledToWidth(), or 480 instead of 640 ?
> 
> > +	QImageWriter writer(QString::fromUtf8(filename.c_str()));
> > +	writer.setQuality(95);
> > +	writer.write(image);
> > +}
> > +
> >  void MainWindow::queueRequest(FrameBuffer *buffer)
> >  {
> >  	Request *request = camera_->createRequest();
> > @@ -628,5 +707,18 @@ void MainWindow::queueRequest(FrameBuffer *buffer)
> >  
> >  	request->addBuffer(vfStream_, buffer);
> >  
> > +	if (captureRaw_) {
> > +		QMutexLocker locker(&mutex_);
> > +
> > +		if (freeBuffers_[rawStream_].isEmpty()) {
> > +			qWarning() << "Raw stream buffer empty";
> > +			return;
> 
> Shouldn't we still queue the request, without a raw buffer ?

Good point!

> 
> > +		}
> > +
> > +		request->addBuffer(rawStream_,
> > +				   freeBuffers_[rawStream_].dequeue());
> > +		captureRaw_ = false;
> > +	}
> > +
> >  	camera_->queueRequest(request);
> >  }
> > diff --git a/src/qcam/main_window.h b/src/qcam/main_window.h
> > index 580bcac146fabe07..37a1d95351e144e0 100644
> > --- a/src/qcam/main_window.h
> > +++ b/src/qcam/main_window.h
> > @@ -55,6 +55,7 @@ private Q_SLOTS:
> >  	void toggleCapture(bool start);
> >  
> >  	void saveViewfinder();
> > +	void saveRaw();
> >  
> >  	void queueRequest(FrameBuffer *buffer);
> >  
> > @@ -70,10 +71,12 @@ private:
> >  	void requestComplete(Request *request);
> >  	void processCapture();
> >  	void processViewfinder(FrameBuffer *buffer);
> > +	void processRaw(FrameBuffer *raw);
> 
> s/raw/buffer/ ?
> 
> >  
> >  	/* UI elements */
> >  	QToolBar *toolbar_;
> >  	QAction *startStopAction_;
> > +	QAction *saveRaw_;
> >  	ViewFinder *viewfinder_;
> >  
> >  	QIcon iconPlay_;
> > @@ -97,6 +100,7 @@ private:
> >  	/* Capture state, buffers queue and statistics */
> >  	bool isCapturing_;
> >  	bool saveViewfinder_;
> > +	bool captureRaw_;
> >  	Stream *vfStream_;
> >  	Stream *rawStream_;
> >  	std::map<Stream *, QQueue<FrameBuffer *>> freeBuffers_;
> 
> -- 
> Regards,
> 
> Laurent Pinchart

Patch

diff --git a/src/qcam/assets/feathericons/feathericons.qrc b/src/qcam/assets/feathericons/feathericons.qrc
index c4eb7a0be6884373..fc8213928ece70ea 100644
--- a/src/qcam/assets/feathericons/feathericons.qrc
+++ b/src/qcam/assets/feathericons/feathericons.qrc
@@ -1,5 +1,6 @@ 
 <!DOCTYPE RCC><RCC version="1.0">
 <qresource>
+<file>./aperture.svg</file>
 <file>./camera-off.svg</file>
 <file>./play-circle.svg</file>
 <file>./save.svg</file>
diff --git a/src/qcam/main_window.cpp b/src/qcam/main_window.cpp
index f57aaf4a27e5f4ca..535fa53d6705a1a9 100644
--- a/src/qcam/main_window.cpp
+++ b/src/qcam/main_window.cpp
@@ -7,10 +7,13 @@ 
 
 #include "main_window.h"
 
+#include <fcntl.h>
 #include <iomanip>
 #include <sstream>
 #include <string>
 #include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
 
 #include <QComboBox>
 #include <QCoreApplication>
@@ -49,7 +52,8 @@  public:
 };
 
 MainWindow::MainWindow(CameraManager *cm, const OptionsParser::Options &options)
-	: options_(options), cm_(cm), allocator_(nullptr), isCapturing_(false)
+	: options_(options), cm_(cm), allocator_(nullptr), isCapturing_(false),
+	  captureRaw_(false)
 {
 	int ret;
 
@@ -149,6 +153,14 @@  int MainWindow::createToolbars()
 				     "Save As...");
 	connect(action, &QAction::triggered, this, &MainWindow::saveViewfinder);
 
+	/* Save Raw action. */
+	action = toolbar_->addAction(QIcon::fromTheme("document-save-as",
+						      QIcon(":aperture.svg")),
+				     "Save Raw");
+	action->setEnabled(false);
+	connect(action, &QAction::triggered, this, &MainWindow::saveRaw);
+	saveRaw_ = action;
+
 	return 0;
 }
 
@@ -369,6 +381,9 @@  int MainWindow::startCapture()
 
 	adjustSize();
 
+	/* Configure the raw capture button. */
+	saveRaw_->setEnabled(config_->size() == 2);
+
 	/* Allocate and map buffers. */
 	allocator_ = new FrameBufferAllocator(camera_);
 	for (StreamConfiguration &config : *config_) {
@@ -474,6 +489,7 @@  void MainWindow::stopCapture()
 		return;
 
 	viewfinder_->stop();
+	saveRaw_->setEnabled(false);
 
 	int ret = camera_->stop();
 	if (ret)
@@ -514,6 +530,11 @@  void MainWindow::saveViewfinder()
 	saveViewfinder_ = true;
 }
 
+void MainWindow::saveRaw()
+{
+	captureRaw_ = true;
+}
+
 /* -----------------------------------------------------------------------------
  * Request Completion Handling
  */
@@ -558,6 +579,9 @@  void MainWindow::processCapture()
 		if (buffers.count(vfStream_))
 			processViewfinder(buffers[vfStream_]);
 
+		if (buffers.count(rawStream_))
+			processRaw(buffers[rawStream_]);
+
 		/*
 		 * Return buffers so they can be reused. No processing involving
 		 * a buffer can happen after they are returned to the free list.
@@ -618,6 +642,61 @@  void MainWindow::processViewfinder(FrameBuffer *buffer)
 	}
 }
 
+void MainWindow::processRaw(FrameBuffer *raw)
+{
+	/* TODO: Should write a DNG file instead of a .raw and .jpeg file.  */
+
+	unsigned int seq = raw->metadata().sequence;
+	std::string filename;
+	int fd, ret = 0;
+	size_t pos;
+
+	/* Write .raw */
+	filename = defaultPath_.toStdString() + "/raw-#.raw";
+	pos = filename.find_first_of('#');
+	if (pos != std::string::npos) {
+		std::stringstream ss;
+		ss << std::setw(6) << std::setfill('0') << seq;
+		filename.replace(pos, 1, ss.str());
+	}
+	qInfo() << "Saving" << filename.c_str();
+
+	fd = open(filename.c_str(), O_CREAT | O_WRONLY |
+		  (pos == std::string::npos ? O_APPEND : O_TRUNC),
+		  S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
+	if (fd == -1) {
+		qWarning() << "Failed to open raw output file";
+		return;
+	}
+
+	const MappedBuffer &info = mappedBuffers_[raw];
+	ret = ::write(fd, info.memory, info.size);
+	if (ret < 0) {
+		ret = -errno;
+		qWarning() << "write error: " << strerror(-ret);
+	} else if (ret != (int)info.size) {
+		qWarning() << "write error: only " << ret
+			   << " bytes written instead of " << info.size;
+	}
+
+	::close(fd);
+
+	/* Write scaled thumbnail .jpeg */
+	filename = defaultPath_.toStdString() + "/raw-#.jpeg";
+	pos = filename.find_first_of('#');
+	if (pos != std::string::npos) {
+		std::stringstream ss;
+		ss << std::setw(6) << std::setfill('0') << seq;
+		filename.replace(pos, 1, ss.str());
+	}
+	qInfo() << "Saving" << filename.c_str();
+
+	QImage image = viewfinder_->getCurrentImage().scaledToHeight(640);
+	QImageWriter writer(QString::fromUtf8(filename.c_str()));
+	writer.setQuality(95);
+	writer.write(image);
+}
+
 void MainWindow::queueRequest(FrameBuffer *buffer)
 {
 	Request *request = camera_->createRequest();
@@ -628,5 +707,18 @@  void MainWindow::queueRequest(FrameBuffer *buffer)
 
 	request->addBuffer(vfStream_, buffer);
 
+	if (captureRaw_) {
+		QMutexLocker locker(&mutex_);
+
+		if (freeBuffers_[rawStream_].isEmpty()) {
+			qWarning() << "Raw stream buffer empty";
+			return;
+		}
+
+		request->addBuffer(rawStream_,
+				   freeBuffers_[rawStream_].dequeue());
+		captureRaw_ = false;
+	}
+
 	camera_->queueRequest(request);
 }
diff --git a/src/qcam/main_window.h b/src/qcam/main_window.h
index 580bcac146fabe07..37a1d95351e144e0 100644
--- a/src/qcam/main_window.h
+++ b/src/qcam/main_window.h
@@ -55,6 +55,7 @@  private Q_SLOTS:
 	void toggleCapture(bool start);
 
 	void saveViewfinder();
+	void saveRaw();
 
 	void queueRequest(FrameBuffer *buffer);
 
@@ -70,10 +71,12 @@  private:
 	void requestComplete(Request *request);
 	void processCapture();
 	void processViewfinder(FrameBuffer *buffer);
+	void processRaw(FrameBuffer *raw);
 
 	/* UI elements */
 	QToolBar *toolbar_;
 	QAction *startStopAction_;
+	QAction *saveRaw_;
 	ViewFinder *viewfinder_;
 
 	QIcon iconPlay_;
@@ -97,6 +100,7 @@  private:
 	/* Capture state, buffers queue and statistics */
 	bool isCapturing_;
 	bool saveViewfinder_;
+	bool captureRaw_;
 	Stream *vfStream_;
 	Stream *rawStream_;
 	std::map<Stream *, QQueue<FrameBuffer *>> freeBuffers_;