[libcamera-devel,v4,12/13] android: Introduce JPEG compression

Message ID 20200805151507.227503-13-kieran.bingham@ideasonboard.com
State Superseded
Headers show
Series
  • android: JPEG support
Related show

Commit Message

Kieran Bingham Aug. 5, 2020, 3:15 p.m. UTC
Provide a compressor interface and implement a JPEG compressor using
libjpeg.

Reviewed-by: Jacopo Mondi <jacopo@jmondi.org>
Signed-off-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
---

v2:

 - Convert to use the libcamera format information rather than duplicating it ourselves.
   - Not easy to get the horizontal subsampling
   - not easy to determine if we have an nvSwap...

v3:
 - Fix frame.error return value (negative inversion)
 - Fix comments
 - constify data table
 - set jpeg image lenght to zero after free
 - set override on class interface functions.
 - Remove ununsed fucntion prototype
 - Rename pixelFormatInfo to pixelFormatInfo_

v4:
 - Add todo regarding strides from buffers
 - Rename to compressor_libjpeg rather than compressor_jpeg
 - Define android dependencies in their own variable in meson
---
 src/android/jpeg/compressor.h           |  25 +++
 src/android/jpeg/compressor_libjpeg.cpp | 217 ++++++++++++++++++++++++
 src/android/jpeg/compressor_libjpeg.h   |  41 +++++
 src/android/meson.build                 |   5 +
 src/libcamera/meson.build               |   2 +
 5 files changed, 290 insertions(+)
 create mode 100644 src/android/jpeg/compressor.h
 create mode 100644 src/android/jpeg/compressor_libjpeg.cpp
 create mode 100644 src/android/jpeg/compressor_libjpeg.h

Patch

diff --git a/src/android/jpeg/compressor.h b/src/android/jpeg/compressor.h
new file mode 100644
index 000000000000..113693b8cdb6
--- /dev/null
+++ b/src/android/jpeg/compressor.h
@@ -0,0 +1,25 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * compressor.h - Image compression interface
+ */
+#ifndef __ANDROID_JPEG_COMPRESSOR_H__
+#define __ANDROID_JPEG_COMPRESSOR_H__
+
+#include <libcamera/buffer.h>
+#include <libcamera/span.h>
+#include <libcamera/stream.h>
+
+class Compressor
+{
+public:
+	using CompressedImage = libcamera::Span<uint8_t>;
+
+	virtual ~Compressor() {};
+
+	virtual int configure(const libcamera::StreamConfiguration &cfg) = 0;
+	virtual int compress(const libcamera::FrameBuffer *source, const CompressedImage &image) = 0;
+};
+
+#endif /* __ANDROID_JPEG_COMPRESSOR_H__ */
diff --git a/src/android/jpeg/compressor_libjpeg.cpp b/src/android/jpeg/compressor_libjpeg.cpp
new file mode 100644
index 000000000000..5c843ab1dd47
--- /dev/null
+++ b/src/android/jpeg/compressor_libjpeg.cpp
@@ -0,0 +1,217 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * compressor_libjpeg.cpp - JPEG compression using libjpeg native API
+ */
+
+#include "compressor_libjpeg.h"
+
+#include <fcntl.h>
+#include <iomanip>
+#include <iostream>
+#include <sstream>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#include <vector>
+
+#include <libcamera/camera.h>
+#include <libcamera/formats.h>
+#include <libcamera/pixel_format.h>
+
+#include "libcamera/internal/formats.h"
+#include "libcamera/internal/log.h"
+
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(JPEG)
+
+namespace {
+
+struct JPEGPixelFormatInfo {
+	J_COLOR_SPACE colorSpace;
+	const PixelFormatInfo &pixelFormatInfo;
+	bool nvSwap;
+};
+
+const std::map<PixelFormat, JPEGPixelFormatInfo> pixelInfo{
+	{ formats::R8, { JCS_GRAYSCALE, PixelFormatInfo::info(formats::R8), false } },
+
+	{ formats::RGB888, { JCS_EXT_BGR, PixelFormatInfo::info(formats::RGB888), false } },
+	{ formats::BGR888, { JCS_EXT_RGB, PixelFormatInfo::info(formats::BGR888), false } },
+
+	{ formats::NV12, { JCS_YCbCr, PixelFormatInfo::info(formats::NV12), false } },
+	{ formats::NV21, { JCS_YCbCr, PixelFormatInfo::info(formats::NV21), true } },
+	{ formats::NV16, { JCS_YCbCr, PixelFormatInfo::info(formats::NV16), false } },
+	{ formats::NV61, { JCS_YCbCr, PixelFormatInfo::info(formats::NV61), true } },
+	{ formats::NV24, { JCS_YCbCr, PixelFormatInfo::info(formats::NV24), false } },
+	{ formats::NV42, { JCS_YCbCr, PixelFormatInfo::info(formats::NV42), true } },
+};
+
+const struct JPEGPixelFormatInfo &findPixelInfo(const PixelFormat &format)
+{
+	static const struct JPEGPixelFormatInfo invalidPixelFormat {
+		JCS_UNKNOWN, PixelFormatInfo(), false
+	};
+
+	const auto iter = pixelInfo.find(format);
+	if (iter == pixelInfo.end()) {
+		LOG(JPEG, Error) << "Unsupported pixel format for JPEG compressor: "
+				 << format.toString();
+		return invalidPixelFormat;
+	}
+
+	return iter->second;
+}
+
+} /* namespace */
+
+CompressorLibJpeg::CompressorLibJpeg()
+	: quality_(95)
+{
+	/* \todo Expand error handling coverage with a custom handler. */
+	compress_.err = jpeg_std_error(&jerr_);
+
+	jpeg_create_compress(&compress_);
+}
+
+CompressorLibJpeg::~CompressorLibJpeg()
+{
+	jpeg_destroy_compress(&compress_);
+}
+
+int CompressorLibJpeg::configure(const StreamConfiguration &cfg)
+{
+	const struct JPEGPixelFormatInfo info = findPixelInfo(cfg.pixelFormat);
+	if (info.colorSpace == JCS_UNKNOWN)
+		return -ENOTSUP;
+
+	compress_.image_width = cfg.size.width;
+	compress_.image_height = cfg.size.height;
+	compress_.in_color_space = info.colorSpace;
+
+	compress_.input_components = info.colorSpace == JCS_GRAYSCALE ? 1 : 3;
+
+	jpeg_set_defaults(&compress_);
+	jpeg_set_quality(&compress_, quality_, TRUE);
+
+	pixelFormatInfo_ = &info.pixelFormatInfo;
+
+	nv_ = pixelFormatInfo_->numPlanes() == 2;
+	nvSwap_ = info.nvSwap;
+
+	return 0;
+}
+
+void CompressorLibJpeg::compressRGB(const libcamera::MappedBuffer *frame)
+{
+	unsigned char *src = static_cast<unsigned char *>(frame->maps()[0].data());
+	/* \todo Stride information should come from buffer configuration. */
+	unsigned int stride = pixelFormatInfo_->stride(compress_.image_width, 0);
+
+	JSAMPROW row_pointer[1];
+
+	while (compress_.next_scanline < compress_.image_height) {
+		row_pointer[0] = &src[compress_.next_scanline * stride];
+		jpeg_write_scanlines(&compress_, row_pointer, 1);
+	}
+}
+
+/*
+ * Compress the incoming buffer from a supported NV format.
+ * This naively unpacks the semi-planar NV12 to a YUV888 format for libjpeg.
+ */
+void CompressorLibJpeg::compressNV(const libcamera::MappedBuffer *frame)
+{
+	uint8_t tmprowbuf[compress_.image_width * 3];
+
+	/*
+	 * \todo Use the raw api, and only unpack the cb/cr samples to new line
+	 * buffers. If possible, see if we can set appropriate pixel strides
+	 * too to save even that copy.
+	 *
+	 * Possible hints at:
+	 * https://sourceforge.net/p/libjpeg/mailman/message/30815123/
+	 */
+	unsigned int y_stride = pixelFormatInfo_->stride(compress_.image_width, 0);
+	unsigned int c_stride = pixelFormatInfo_->stride(compress_.image_width, 1);
+
+	unsigned int horzSubSample = 2 * compress_.image_width / c_stride;
+	unsigned int vertSubSample = pixelFormatInfo_->planes[1].verticalSubSampling;
+
+	unsigned int c_inc = horzSubSample == 1 ? 2 : 0;
+	unsigned int cb_pos = nvSwap_ ? 1 : 0;
+	unsigned int cr_pos = nvSwap_ ? 0 : 1;
+
+	const unsigned char *src = static_cast<unsigned char *>(frame->maps()[0].data());
+	const unsigned char *src_c = src + y_stride * compress_.image_height;
+
+	JSAMPROW row_pointer[1];
+	row_pointer[0] = &tmprowbuf[0];
+
+	for (unsigned int y = 0; y < compress_.image_height; y++) {
+		unsigned char *dst = &tmprowbuf[0];
+
+		const unsigned char *src_y = src + y * compress_.image_width;
+		const unsigned char *src_cb = src_c + (y / vertSubSample) * c_stride + cb_pos;
+		const unsigned char *src_cr = src_c + (y / vertSubSample) * c_stride + cr_pos;
+
+		for (unsigned int x = 0; x < compress_.image_width; x += 2) {
+			dst[0] = *src_y;
+			dst[1] = *src_cb;
+			dst[2] = *src_cr;
+			src_y++;
+			src_cb += c_inc;
+			src_cr += c_inc;
+			dst += 3;
+
+			dst[0] = *src_y;
+			dst[1] = *src_cb;
+			dst[2] = *src_cr;
+			src_y++;
+			src_cb += 2;
+			src_cr += 2;
+			dst += 3;
+		}
+
+		jpeg_write_scanlines(&compress_, row_pointer, 1);
+	}
+}
+
+int CompressorLibJpeg::compress(const FrameBuffer *source, const CompressedImage &dest)
+{
+	MappedFrameBuffer frame(source, PROT_READ);
+	if (!frame.isValid()) {
+		LOG(JPEG, Error) << "Failed to map FrameBuffer : "
+				 << strerror(frame.error());
+		return frame.error();
+	}
+
+	unsigned char *destination = dest.data();
+	unsigned long size = dest.size();
+
+	/*
+	 * The jpeg_mem_dest will reallocate if the required size is not
+	 * sufficient. That means the output won't be written to the correct
+	 * buffers.
+	 *
+	 * \todo Implement our own custom memory destination to prevent
+	 * reallocation and prefer failure with correct reporting.
+	 */
+	jpeg_mem_dest(&compress_, &destination, &size);
+
+	jpeg_start_compress(&compress_, TRUE);
+
+	LOG(JPEG, Debug) << "JPEG Compress Starting:" << compress_.image_width
+			 << "x" << compress_.image_height;
+
+	if (nv_)
+		compressNV(&frame);
+	else
+		compressRGB(&frame);
+
+	jpeg_finish_compress(&compress_);
+
+	return size;
+}
diff --git a/src/android/jpeg/compressor_libjpeg.h b/src/android/jpeg/compressor_libjpeg.h
new file mode 100644
index 000000000000..ba4650832332
--- /dev/null
+++ b/src/android/jpeg/compressor_libjpeg.h
@@ -0,0 +1,41 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2020, Google Inc.
+ *
+ * compressor_libjpeg.h - JPEG compression using libjpeg
+ */
+#ifndef __ANDROID_JPEG_COMPRESSOR_LIBJPEG_H__
+#define __ANDROID_JPEG_COMPRESSOR_LIBJPEG_H__
+
+#include "compressor.h"
+
+#include "libcamera/internal/buffer.h"
+#include "libcamera/internal/formats.h"
+
+#include <jpeglib.h>
+
+class CompressorLibJpeg : public Compressor
+{
+public:
+	CompressorLibJpeg();
+	~CompressorLibJpeg();
+
+	int configure(const libcamera::StreamConfiguration &cfg) override;
+	int compress(const libcamera::FrameBuffer *source, const CompressedImage &jpeg) override;
+
+private:
+	void compressRGB(const libcamera::MappedBuffer *frame);
+	void compressNV(const libcamera::MappedBuffer *frame);
+
+	struct jpeg_compress_struct compress_;
+	struct jpeg_error_mgr jerr_;
+
+	unsigned int quality_;
+
+	const libcamera::PixelFormatInfo *pixelFormatInfo_;
+
+	bool nv_;
+	bool nvSwap_;
+};
+
+#endif /* __ANDROID_JPEG_COMPRESSOR_LIBJPEG_H__ */
diff --git a/src/android/meson.build b/src/android/meson.build
index 822cad621f01..7dd375e73eff 100644
--- a/src/android/meson.build
+++ b/src/android/meson.build
@@ -6,12 +6,17 @@  android_hal_sources = files([
     'camera_device.cpp',
     'camera_metadata.cpp',
     'camera_ops.cpp',
+    'jpeg/compressor_libjpeg.cpp',
 ])
 
 android_camera_metadata_sources = files([
     'metadata/camera_metadata.c',
 ])
 
+android_deps = [
+    dependency('libjpeg'),
+]
+
 android_camera_metadata = static_library('camera_metadata',
                                          android_camera_metadata_sources,
                                          include_directories : android_includes)
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index 3aad4386ffc2..8ad0a1b0740f 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -124,6 +124,8 @@  if get_option('android')
     libcamera_sources += android_hal_sources
     includes += android_includes
     libcamera_link_with += android_camera_metadata
+
+    libcamera_deps += android_deps
 endif
 
 # We add '/' to the build_rpath as a 'safe' path to act as a boolean flag.