[libcamera-devel,RFC,3/3] android: jpeg: Add a basic NV12 image thumbnailer
diff mbox series

Message ID 20201008141038.83425-4-email@uajain.com
State Superseded
Delegated to: Umang Jain
Headers show
Series
  • Introduce PostProcessor Interface for CameraStream
Related show

Commit Message

Umang Jain Oct. 8, 2020, 2:10 p.m. UTC
Add a basic image thumbnailer for NV12 frames being captured.
It shall generate a thumbnail image to be embedded as a part of
EXIF metadata of the frame. The output of the thumbnail will still
be NV12.

Signed-off-by: Umang Jain <email@uajain.com>
---
 src/android/jpeg/post_processor_jpeg.cpp |  11 ++
 src/android/jpeg/thumbnailer.cpp         | 134 +++++++++++++++++++++++
 src/android/jpeg/thumbnailer.h           |  34 ++++++
 src/android/meson.build                  |   1 +
 4 files changed, 180 insertions(+)
 create mode 100644 src/android/jpeg/thumbnailer.cpp
 create mode 100644 src/android/jpeg/thumbnailer.h

Comments

Kieran Bingham Oct. 9, 2020, 1:33 p.m. UTC | #1
Hi Umang,

On 08/10/2020 15:10, Umang Jain wrote:
> Add a basic image thumbnailer for NV12 frames being captured.
> It shall generate a thumbnail image to be embedded as a part of
> EXIF metadata of the frame. The output of the thumbnail will still
> be NV12.
> 
> Signed-off-by: Umang Jain <email@uajain.com>
> ---
>  src/android/jpeg/post_processor_jpeg.cpp |  11 ++
>  src/android/jpeg/thumbnailer.cpp         | 134 +++++++++++++++++++++++
>  src/android/jpeg/thumbnailer.h           |  34 ++++++
>  src/android/meson.build                  |   1 +
>  4 files changed, 180 insertions(+)
>  create mode 100644 src/android/jpeg/thumbnailer.cpp
>  create mode 100644 src/android/jpeg/thumbnailer.h
> 
> diff --git a/src/android/jpeg/post_processor_jpeg.cpp b/src/android/jpeg/post_processor_jpeg.cpp
> index eeb4e95..9076d04 100644
> --- a/src/android/jpeg/post_processor_jpeg.cpp
> +++ b/src/android/jpeg/post_processor_jpeg.cpp
> @@ -8,6 +8,7 @@
>  #include "post_processor_jpeg.h"
>  
>  #include "exif.h"
> +#include "thumbnailer.h"
>  
>  #include "../camera_device.h"
>  
> @@ -286,6 +287,16 @@ int PostProcessorJpeg::encode(const FrameBuffer *source,
>  	LOG(JPEG, Debug) << "JPEG Encode Starting:" << compress_.image_width
>  			 << "x" << compress_.image_height;
>  
> +	Thumbnailer th;
> +	libcamera::Span<uint8_t> thumbnail;
> +	th.configure(Size (compress_.image_width, compress_.image_height),
> +		     pixelFormatInfo_->format);


You could move the Thumbnailer into the PostProcessorJpeg class as a
private, and call th.configure during PostProcessorJpeg::configure().

Potentially even allocate the destination thumbnail buffer there too?

> +	th.scaleBuffer(source, thumbnail);
> +	/*
> +	 * \todo: Compress the thumbnail again encode() and set it in the
> +	 * respective EXIF field.
> +	 */
> +
>  	if (nv_)
>  		compressNV(&frame);
>  	else
> diff --git a/src/android/jpeg/thumbnailer.cpp b/src/android/jpeg/thumbnailer.cpp
> new file mode 100644
> index 0000000..d01b4af
> --- /dev/null
> +++ b/src/android/jpeg/thumbnailer.cpp
> @@ -0,0 +1,134 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */

LGPL-2.1...

> +/*
> + * Copyright (C) 2020, Google Inc.
> + *
> + * thumbnailer.cpp - Basic image thumbnailer from NV12
> + */
> +
> +#include "thumbnailer.h"
> +
> +#include <libcamera/formats.h>
> +
> +#include "libcamera/internal/file.h"
> +#include "libcamera/internal/log.h"
> +
> +using namespace libcamera;
> +
> +LOG_DEFINE_CATEGORY(Thumbnailer)
> +
> +Thumbnailer::Thumbnailer()
> +	: validConfiguration_(false)
> +{
> +}
> +
> +void Thumbnailer::configure(const Size &sourceSize, PixelFormat pixelFormat)
> +{
> +	sourceSize_ = sourceSize;
> +	pixelFormat_ = pixelFormat;
> +
> +	if (pixelFormat_ != formats::NV12) {
> +		LOG (Thumbnailer, Error) << "Failed to configure: Pixel Format "
> +				    << pixelFormat_.toString() << " unsupported.";
> +		return;
> +	}
> +
> +	validConfiguration_ = true;
> +}
> +
> +static std::string datetime()
> +{
> +	time_t rawtime;
> +	struct tm *timeinfo;
> +	char buffer[80];
> +	static unsigned int milliseconds = 0;
> +
> +	time(&rawtime);
> +	timeinfo = localtime(&rawtime);
> +
> +	strftime(buffer, 80, "%d-%m-%Y.%H-%M-%S.", timeinfo);
> +
> +	/* milliseconds is just a fast hack to ensure unique filenames */
> +	return std::string(buffer) + std::to_string(milliseconds++);
> +}
> +
> +/*
> + * The Exif specification recommends the width of the thumbnail to be a
> + * mutiple of 16 (section 4.8.1). Hence, compute the corresponding height

s/mutiple/multiple/

> + * keeping the aspect ratio same as of the source.
> + */
> +Size Thumbnailer::computeThumbnailSize()
> +{
> +	unsigned int targetHeight;
> +	unsigned int targetWidth = 160;
> +
> +	targetHeight = targetWidth * sourceSize_.height / sourceSize_.width;
> +
> +	if (targetHeight & 1)
> +		targetHeight++;
> +
> +	return Size(targetWidth, targetHeight);
> +}
> +
> +int Thumbnailer::scaleBuffer(const FrameBuffer *source, Span<uint8_t> &dest)
> +{
> +	MappedFrameBuffer frame(source, PROT_READ);
> +	if (!frame.isValid()) {
> +		LOG(Thumbnailer, Error) << "Failed to map FrameBuffer : "
> +				        << strerror(frame.error());
> +		return frame.error();
> +	}
> +
> +	if (!validConfiguration_) {
> +		LOG(Thumbnailer, Error) << "config is unconfigured or invalid.";
> +		return -1;
> +	}
> +
> +	targetSize_ = computeThumbnailSize();
> +
> +	const unsigned int sw = sourceSize_.width;
> +	const unsigned int sh = sourceSize_.height;
> +	const unsigned int tw = targetSize_.width;
> +	const unsigned int th = targetSize_.height;
> +
> +	/* Image scaling block implementing nearest-neighbour algorithm. */
> +	unsigned char *src = static_cast<unsigned char *>(frame.maps()[0].data());
> +	unsigned char *src_c = src + sh * sw;
> +	unsigned int cb_pos = 0;
> +	unsigned int cr_pos = 1;
> +	unsigned char *src_cb, *src_cr;
> +
> +	size_t dstSize = (th * tw) + ((th/2) * tw);

How about move this to a helper function:

int Thumbnailer::outputSize()
{
	if (!validConfiguration_)
		return -1
	
	return targetSize_.height * targetSize_.width * 3 / 2
}

> +	unsigned char *destination = static_cast<unsigned char *>(malloc(dstSize));

And allocate this in the caller, passing in the buffer as a const Span
reference or such.

> +	unsigned char *dst = destination;
> +	unsigned char *dst_c = destination + th * tw;
> +
> +	for (unsigned int y = 0; y < th; y+=2) {
> +		unsigned int sourceY = (sh*y + th/2) / th;
> +
> +		src_cb = src_c + (sourceY/2) * sw + cb_pos;
> +		src_cr = src_c + (sourceY/2) * sw + cr_pos;
> +
> +		for (unsigned int x = 0; x < tw; x+=2) {
> +			unsigned int sourceX = (sw*x + tw/2) / tw;
> +
> +			dst[y     * tw + x]     = src[sw * sourceY     + sourceX];
> +			dst[(y+1) * tw + x]     = src[sw * (sourceY+1) + sourceX];
> +			dst[y     * tw + (x+1)] = src[sw * sourceY     + (sourceX+1)];
> +			dst[(y+1) * tw + (x+1)] = src[sw * (sourceY+1) + (sourceX+1)];
> +
> +			dst_c[(y/2) * tw + x + cb_pos] = src_cb[(sourceX/2) * 2];
> +			dst_c[(y/2) * tw + x + cr_pos] = src_cr[(sourceX/2) * 2];
> +		}
> +	}
> +
> +	/* Helper code: Write the output pixels to a file so we can inspect */
> +	File file("/tmp/" + datetime() + ".raw");
> +	int32_t ret = file.open(File::WriteOnly);
> +	ret = file.write({ destination, dstSize });
> +	LOG(Thumbnailer, Info) << "Wrote " << ret << " bytes: " << targetSize_.width << "x" << targetSize_.height;
> +

And of course that will be removed for a non-rfc...

> +	/* Write scaled pixels to dest */
> +	dest = { destination, dstSize };

And this wouldnt' be needed if the dest is passed in.

> +
> +	return 0;
> +}
> diff --git a/src/android/jpeg/thumbnailer.h b/src/android/jpeg/thumbnailer.h
> new file mode 100644
> index 0000000..af3a194
> --- /dev/null
> +++ b/src/android/jpeg/thumbnailer.h
> @@ -0,0 +1,34 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Copyright (C) 2020, Google Inc.
> + *
> + * thumbnailer.h - Basic image thumbnailer from NV12
> + */
> +#ifndef __ANDROID_JPEG_THUMBNAILER_H__
> +#define __ANDROID_JPEG_THUMBNAILER_H__
> +
> +#include <libcamera/geometry.h>
> +
> +#include "libcamera/internal/buffer.h"
> +#include "libcamera/internal/formats.h"
> +
> +class Thumbnailer
> +{
> +public:
> +	Thumbnailer();
> +
> +	void configure(const libcamera::Size &sourceSize,
> +		       libcamera::PixelFormat pixelFormat);
> +	int scaleBuffer(const libcamera::FrameBuffer *source, libcamera::Span<uint8_t> &dest);
> +
> +private:
> +	libcamera::Size computeThumbnailSize();
> +
> +	libcamera::PixelFormat pixelFormat_;
> +	libcamera::Size sourceSize_;
> +	libcamera::Size targetSize_;
> +
> +	bool validConfiguration_;
> +};
> +
> +#endif /* __ANDROID_JPEG_THUMBNAILER_H__ */
> diff --git a/src/android/meson.build b/src/android/meson.build
> index 02b3b47..854005e 100644
> --- a/src/android/meson.build
> +++ b/src/android/meson.build
> @@ -23,6 +23,7 @@ android_hal_sources = files([
>      'camera_stream.cpp',
>      'jpeg/exif.cpp',
>      'jpeg/post_processor_jpeg.cpp',
> +    'jpeg/thumbnailer.cpp',
>  ])
>  
>  android_camera_metadata_sources = files([
>
Laurent Pinchart Oct. 9, 2020, 10:31 p.m. UTC | #2
Hello,

On Fri, Oct 09, 2020 at 02:33:54PM +0100, Kieran Bingham wrote:
> On 08/10/2020 15:10, Umang Jain wrote:
> > Add a basic image thumbnailer for NV12 frames being captured.
> > It shall generate a thumbnail image to be embedded as a part of
> > EXIF metadata of the frame. The output of the thumbnail will still
> > be NV12.
> > 
> > Signed-off-by: Umang Jain <email@uajain.com>
> > ---
> >  src/android/jpeg/post_processor_jpeg.cpp |  11 ++
> >  src/android/jpeg/thumbnailer.cpp         | 134 +++++++++++++++++++++++
> >  src/android/jpeg/thumbnailer.h           |  34 ++++++
> >  src/android/meson.build                  |   1 +
> >  4 files changed, 180 insertions(+)
> >  create mode 100644 src/android/jpeg/thumbnailer.cpp
> >  create mode 100644 src/android/jpeg/thumbnailer.h
> > 
> > diff --git a/src/android/jpeg/post_processor_jpeg.cpp b/src/android/jpeg/post_processor_jpeg.cpp
> > index eeb4e95..9076d04 100644
> > --- a/src/android/jpeg/post_processor_jpeg.cpp
> > +++ b/src/android/jpeg/post_processor_jpeg.cpp
> > @@ -8,6 +8,7 @@
> >  #include "post_processor_jpeg.h"
> >  
> >  #include "exif.h"
> > +#include "thumbnailer.h"
> >  
> >  #include "../camera_device.h"
> >  
> > @@ -286,6 +287,16 @@ int PostProcessorJpeg::encode(const FrameBuffer *source,
> >  	LOG(JPEG, Debug) << "JPEG Encode Starting:" << compress_.image_width
> >  			 << "x" << compress_.image_height;
> >  
> > +	Thumbnailer th;
> > +	libcamera::Span<uint8_t> thumbnail;
> > +	th.configure(Size (compress_.image_width, compress_.image_height),
> > +		     pixelFormatInfo_->format);
> 
> You could move the Thumbnailer into the PostProcessorJpeg class as a
> private, and call th.configure during PostProcessorJpeg::configure().
> 
> Potentially even allocate the destination thumbnail buffer there too?

Given that there's not much to configure (it's just about storing the
size internally), how about passing the target size to the
Thumbnailer::scaleBuffer() function ? configure() is useful for
components that are configured once (potentially with expensive
operations at configure time) and run multiple times, but here I don't
think that's needed.

> > +	th.scaleBuffer(source, thumbnail);
> > +	/*
> > +	 * \todo: Compress the thumbnail again encode() and set it in the
> > +	 * respective EXIF field.
> > +	 */
> > +
> >  	if (nv_)
> >  		compressNV(&frame);
> >  	else
> > diff --git a/src/android/jpeg/thumbnailer.cpp b/src/android/jpeg/thumbnailer.cpp
> > new file mode 100644
> > index 0000000..d01b4af
> > --- /dev/null
> > +++ b/src/android/jpeg/thumbnailer.cpp
> > @@ -0,0 +1,134 @@
> > +/* SPDX-License-Identifier: GPL-2.0-or-later */
> 
> LGPL-2.1...
> 
> > +/*
> > + * Copyright (C) 2020, Google Inc.
> > + *
> > + * thumbnailer.cpp - Basic image thumbnailer from NV12
> > + */
> > +
> > +#include "thumbnailer.h"
> > +
> > +#include <libcamera/formats.h>
> > +
> > +#include "libcamera/internal/file.h"
> > +#include "libcamera/internal/log.h"
> > +
> > +using namespace libcamera;
> > +
> > +LOG_DEFINE_CATEGORY(Thumbnailer)
> > +
> > +Thumbnailer::Thumbnailer()
> > +	: validConfiguration_(false)
> > +{
> > +}
> > +
> > +void Thumbnailer::configure(const Size &sourceSize, PixelFormat pixelFormat)
> > +{
> > +	sourceSize_ = sourceSize;
> > +	pixelFormat_ = pixelFormat;
> > +
> > +	if (pixelFormat_ != formats::NV12) {
> > +		LOG (Thumbnailer, Error) << "Failed to configure: Pixel Format "
> > +				    << pixelFormat_.toString() << " unsupported.";
> > +		return;
> > +	}
> > +
> > +	validConfiguration_ = true;
> > +}
> > +
> > +static std::string datetime()
> > +{
> > +	time_t rawtime;
> > +	struct tm *timeinfo;
> > +	char buffer[80];
> > +	static unsigned int milliseconds = 0;
> > +
> > +	time(&rawtime);
> > +	timeinfo = localtime(&rawtime);
> > +
> > +	strftime(buffer, 80, "%d-%m-%Y.%H-%M-%S.", timeinfo);
> > +
> > +	/* milliseconds is just a fast hack to ensure unique filenames */
> > +	return std::string(buffer) + std::to_string(milliseconds++);
> > +}
> > +
> > +/*
> > + * The Exif specification recommends the width of the thumbnail to be a
> > + * mutiple of 16 (section 4.8.1). Hence, compute the corresponding height
> 
> s/mutiple/multiple/
> 
> > + * keeping the aspect ratio same as of the source.
> > + */
> > +Size Thumbnailer::computeThumbnailSize()
> > +{
> > +	unsigned int targetHeight;
> > +	unsigned int targetWidth = 160;
> > +
> > +	targetHeight = targetWidth * sourceSize_.height / sourceSize_.width;
> > +
> > +	if (targetHeight & 1)
> > +		targetHeight++;
> > +
> > +	return Size(targetWidth, targetHeight);
> > +}
> > +
> > +int Thumbnailer::scaleBuffer(const FrameBuffer *source, Span<uint8_t> &dest)
> > +{
> > +	MappedFrameBuffer frame(source, PROT_READ);
> > +	if (!frame.isValid()) {
> > +		LOG(Thumbnailer, Error) << "Failed to map FrameBuffer : "
> > +				        << strerror(frame.error());
> > +		return frame.error();
> > +	}
> > +
> > +	if (!validConfiguration_) {
> > +		LOG(Thumbnailer, Error) << "config is unconfigured or invalid.";
> > +		return -1;
> > +	}
> > +
> > +	targetSize_ = computeThumbnailSize();
> > +
> > +	const unsigned int sw = sourceSize_.width;
> > +	const unsigned int sh = sourceSize_.height;
> > +	const unsigned int tw = targetSize_.width;
> > +	const unsigned int th = targetSize_.height;
> > +
> > +	/* Image scaling block implementing nearest-neighbour algorithm. */
> > +	unsigned char *src = static_cast<unsigned char *>(frame.maps()[0].data());
> > +	unsigned char *src_c = src + sh * sw;
> > +	unsigned int cb_pos = 0;
> > +	unsigned int cr_pos = 1;
> > +	unsigned char *src_cb, *src_cr;
> > +
> > +	size_t dstSize = (th * tw) + ((th/2) * tw);
> 
> How about move this to a helper function:
> 
> int Thumbnailer::outputSize()
> {
> 	if (!validConfiguration_)
> 		return -1
> 	
> 	return targetSize_.height * targetSize_.width * 3 / 2
> }

It's only done once though, not sure it's worth it.

> > +	unsigned char *destination = static_cast<unsigned char *>(malloc(dstSize));
> 
> And allocate this in the caller, passing in the buffer as a const Span
> reference or such.

Wouldn't that make it more complex for the caller, that would need to
retrieve the size, allocate the buffer and then call scaleBuffer() ? How
about returning a std::vector from this function instead ? That would
make buffer ownership clear, simplify the API for the caller, and with
copy elision there will be no copy.

Now that I've written this, I've read
https://en.cppreference.com/w/cpp/language/copy_elision, and it seems
copy elision is only mandatory for the compiler to implement when the
operand to the return statement is a prvalue. In practice I expect the
copy to always be eluded (I've tested it with recent g++ and clang++
with -O0 and -Os), but if we want a strong guarantee, we would need to
pass the vector as an argument.

> > +	unsigned char *dst = destination;
> > +	unsigned char *dst_c = destination + th * tw;
> > +
> > +	for (unsigned int y = 0; y < th; y+=2) {
> > +		unsigned int sourceY = (sh*y + th/2) / th;
> > +
> > +		src_cb = src_c + (sourceY/2) * sw + cb_pos;
> > +		src_cr = src_c + (sourceY/2) * sw + cr_pos;
> > +
> > +		for (unsigned int x = 0; x < tw; x+=2) {
> > +			unsigned int sourceX = (sw*x + tw/2) / tw;
> > +
> > +			dst[y     * tw + x]     = src[sw * sourceY     + sourceX];
> > +			dst[(y+1) * tw + x]     = src[sw * (sourceY+1) + sourceX];
> > +			dst[y     * tw + (x+1)] = src[sw * sourceY     + (sourceX+1)];
> > +			dst[(y+1) * tw + (x+1)] = src[sw * (sourceY+1) + (sourceX+1)];
> > +
> > +			dst_c[(y/2) * tw + x + cb_pos] = src_cb[(sourceX/2) * 2];
> > +			dst_c[(y/2) * tw + x + cr_pos] = src_cr[(sourceX/2) * 2];
> > +		}
> > +	}
> > +
> > +	/* Helper code: Write the output pixels to a file so we can inspect */
> > +	File file("/tmp/" + datetime() + ".raw");
> > +	int32_t ret = file.open(File::WriteOnly);
> > +	ret = file.write({ destination, dstSize });
> > +	LOG(Thumbnailer, Info) << "Wrote " << ret << " bytes: " << targetSize_.width << "x" << targetSize_.height;
> > +
> 
> And of course that will be removed for a non-rfc...
> 
> > +	/* Write scaled pixels to dest */
> > +	dest = { destination, dstSize };
> 
> And this wouldnt' be needed if the dest is passed in.
> 
> > +
> > +	return 0;
> > +}
> > diff --git a/src/android/jpeg/thumbnailer.h b/src/android/jpeg/thumbnailer.h
> > new file mode 100644
> > index 0000000..af3a194
> > --- /dev/null
> > +++ b/src/android/jpeg/thumbnailer.h
> > @@ -0,0 +1,34 @@
> > +/* SPDX-License-Identifier: GPL-2.0-or-later */
> > +/*
> > + * Copyright (C) 2020, Google Inc.
> > + *
> > + * thumbnailer.h - Basic image thumbnailer from NV12
> > + */
> > +#ifndef __ANDROID_JPEG_THUMBNAILER_H__
> > +#define __ANDROID_JPEG_THUMBNAILER_H__
> > +
> > +#include <libcamera/geometry.h>
> > +
> > +#include "libcamera/internal/buffer.h"
> > +#include "libcamera/internal/formats.h"
> > +
> > +class Thumbnailer
> > +{
> > +public:
> > +	Thumbnailer();
> > +
> > +	void configure(const libcamera::Size &sourceSize,
> > +		       libcamera::PixelFormat pixelFormat);
> > +	int scaleBuffer(const libcamera::FrameBuffer *source, libcamera::Span<uint8_t> &dest);
> > +
> > +private:
> > +	libcamera::Size computeThumbnailSize();
> > +
> > +	libcamera::PixelFormat pixelFormat_;
> > +	libcamera::Size sourceSize_;
> > +	libcamera::Size targetSize_;
> > +
> > +	bool validConfiguration_;
> > +};
> > +
> > +#endif /* __ANDROID_JPEG_THUMBNAILER_H__ */
> > diff --git a/src/android/meson.build b/src/android/meson.build
> > index 02b3b47..854005e 100644
> > --- a/src/android/meson.build
> > +++ b/src/android/meson.build
> > @@ -23,6 +23,7 @@ android_hal_sources = files([
> >      'camera_stream.cpp',
> >      'jpeg/exif.cpp',
> >      'jpeg/post_processor_jpeg.cpp',
> > +    'jpeg/thumbnailer.cpp',
> >  ])
> >  
> >  android_camera_metadata_sources = files([

Patch
diff mbox series

diff --git a/src/android/jpeg/post_processor_jpeg.cpp b/src/android/jpeg/post_processor_jpeg.cpp
index eeb4e95..9076d04 100644
--- a/src/android/jpeg/post_processor_jpeg.cpp
+++ b/src/android/jpeg/post_processor_jpeg.cpp
@@ -8,6 +8,7 @@ 
 #include "post_processor_jpeg.h"
 
 #include "exif.h"
+#include "thumbnailer.h"
 
 #include "../camera_device.h"
 
@@ -286,6 +287,16 @@  int PostProcessorJpeg::encode(const FrameBuffer *source,
 	LOG(JPEG, Debug) << "JPEG Encode Starting:" << compress_.image_width
 			 << "x" << compress_.image_height;
 
+	Thumbnailer th;
+	libcamera::Span<uint8_t> thumbnail;
+	th.configure(Size (compress_.image_width, compress_.image_height),
+		     pixelFormatInfo_->format);
+	th.scaleBuffer(source, thumbnail);
+	/*
+	 * \todo: Compress the thumbnail again encode() and set it in the
+	 * respective EXIF field.
+	 */
+
 	if (nv_)
 		compressNV(&frame);
 	else
diff --git a/src/android/jpeg/thumbnailer.cpp b/src/android/jpeg/thumbnailer.cpp
new file mode 100644
index 0000000..d01b4af
--- /dev/null
+++ b/src/android/jpeg/thumbnailer.cpp
@@ -0,0 +1,134 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * thumbnailer.cpp - Basic image thumbnailer from NV12
+ */
+
+#include "thumbnailer.h"
+
+#include <libcamera/formats.h>
+
+#include "libcamera/internal/file.h"
+#include "libcamera/internal/log.h"
+
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(Thumbnailer)
+
+Thumbnailer::Thumbnailer()
+	: validConfiguration_(false)
+{
+}
+
+void Thumbnailer::configure(const Size &sourceSize, PixelFormat pixelFormat)
+{
+	sourceSize_ = sourceSize;
+	pixelFormat_ = pixelFormat;
+
+	if (pixelFormat_ != formats::NV12) {
+		LOG (Thumbnailer, Error) << "Failed to configure: Pixel Format "
+				    << pixelFormat_.toString() << " unsupported.";
+		return;
+	}
+
+	validConfiguration_ = true;
+}
+
+static std::string datetime()
+{
+	time_t rawtime;
+	struct tm *timeinfo;
+	char buffer[80];
+	static unsigned int milliseconds = 0;
+
+	time(&rawtime);
+	timeinfo = localtime(&rawtime);
+
+	strftime(buffer, 80, "%d-%m-%Y.%H-%M-%S.", timeinfo);
+
+	/* milliseconds is just a fast hack to ensure unique filenames */
+	return std::string(buffer) + std::to_string(milliseconds++);
+}
+
+/*
+ * The Exif specification recommends the width of the thumbnail to be a
+ * mutiple of 16 (section 4.8.1). Hence, compute the corresponding height
+ * keeping the aspect ratio same as of the source.
+ */
+Size Thumbnailer::computeThumbnailSize()
+{
+	unsigned int targetHeight;
+	unsigned int targetWidth = 160;
+
+	targetHeight = targetWidth * sourceSize_.height / sourceSize_.width;
+
+	if (targetHeight & 1)
+		targetHeight++;
+
+	return Size(targetWidth, targetHeight);
+}
+
+int Thumbnailer::scaleBuffer(const FrameBuffer *source, Span<uint8_t> &dest)
+{
+	MappedFrameBuffer frame(source, PROT_READ);
+	if (!frame.isValid()) {
+		LOG(Thumbnailer, Error) << "Failed to map FrameBuffer : "
+				        << strerror(frame.error());
+		return frame.error();
+	}
+
+	if (!validConfiguration_) {
+		LOG(Thumbnailer, Error) << "config is unconfigured or invalid.";
+		return -1;
+	}
+
+	targetSize_ = computeThumbnailSize();
+
+	const unsigned int sw = sourceSize_.width;
+	const unsigned int sh = sourceSize_.height;
+	const unsigned int tw = targetSize_.width;
+	const unsigned int th = targetSize_.height;
+
+	/* Image scaling block implementing nearest-neighbour algorithm. */
+	unsigned char *src = static_cast<unsigned char *>(frame.maps()[0].data());
+	unsigned char *src_c = src + sh * sw;
+	unsigned int cb_pos = 0;
+	unsigned int cr_pos = 1;
+	unsigned char *src_cb, *src_cr;
+
+	size_t dstSize = (th * tw) + ((th/2) * tw);
+	unsigned char *destination = static_cast<unsigned char *>(malloc(dstSize));
+	unsigned char *dst = destination;
+	unsigned char *dst_c = destination + th * tw;
+
+	for (unsigned int y = 0; y < th; y+=2) {
+		unsigned int sourceY = (sh*y + th/2) / th;
+
+		src_cb = src_c + (sourceY/2) * sw + cb_pos;
+		src_cr = src_c + (sourceY/2) * sw + cr_pos;
+
+		for (unsigned int x = 0; x < tw; x+=2) {
+			unsigned int sourceX = (sw*x + tw/2) / tw;
+
+			dst[y     * tw + x]     = src[sw * sourceY     + sourceX];
+			dst[(y+1) * tw + x]     = src[sw * (sourceY+1) + sourceX];
+			dst[y     * tw + (x+1)] = src[sw * sourceY     + (sourceX+1)];
+			dst[(y+1) * tw + (x+1)] = src[sw * (sourceY+1) + (sourceX+1)];
+
+			dst_c[(y/2) * tw + x + cb_pos] = src_cb[(sourceX/2) * 2];
+			dst_c[(y/2) * tw + x + cr_pos] = src_cr[(sourceX/2) * 2];
+		}
+	}
+
+	/* Helper code: Write the output pixels to a file so we can inspect */
+	File file("/tmp/" + datetime() + ".raw");
+	int32_t ret = file.open(File::WriteOnly);
+	ret = file.write({ destination, dstSize });
+	LOG(Thumbnailer, Info) << "Wrote " << ret << " bytes: " << targetSize_.width << "x" << targetSize_.height;
+
+	/* Write scaled pixels to dest */
+	dest = { destination, dstSize };
+
+	return 0;
+}
diff --git a/src/android/jpeg/thumbnailer.h b/src/android/jpeg/thumbnailer.h
new file mode 100644
index 0000000..af3a194
--- /dev/null
+++ b/src/android/jpeg/thumbnailer.h
@@ -0,0 +1,34 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * thumbnailer.h - Basic image thumbnailer from NV12
+ */
+#ifndef __ANDROID_JPEG_THUMBNAILER_H__
+#define __ANDROID_JPEG_THUMBNAILER_H__
+
+#include <libcamera/geometry.h>
+
+#include "libcamera/internal/buffer.h"
+#include "libcamera/internal/formats.h"
+
+class Thumbnailer
+{
+public:
+	Thumbnailer();
+
+	void configure(const libcamera::Size &sourceSize,
+		       libcamera::PixelFormat pixelFormat);
+	int scaleBuffer(const libcamera::FrameBuffer *source, libcamera::Span<uint8_t> &dest);
+
+private:
+	libcamera::Size computeThumbnailSize();
+
+	libcamera::PixelFormat pixelFormat_;
+	libcamera::Size sourceSize_;
+	libcamera::Size targetSize_;
+
+	bool validConfiguration_;
+};
+
+#endif /* __ANDROID_JPEG_THUMBNAILER_H__ */
diff --git a/src/android/meson.build b/src/android/meson.build
index 02b3b47..854005e 100644
--- a/src/android/meson.build
+++ b/src/android/meson.build
@@ -23,6 +23,7 @@  android_hal_sources = files([
     'camera_stream.cpp',
     'jpeg/exif.cpp',
     'jpeg/post_processor_jpeg.cpp',
+    'jpeg/thumbnailer.cpp',
 ])
 
 android_camera_metadata_sources = files([