From 97e8b3a2eb321884fe1e15fb584f41a38cc33d51 Mon Sep 17 00:00:00 2001 From: Laurent Pinchart Date: Sat, 23 Mar 2019 03:24:25 +0200 Subject: qcam: Add Qt-based GUI application qcam is a sample camera GUI application based on Qt. It demonstrates integration of the Qt event loop with libcamera. The application lets the user select a camera through the GUI, and then captures a single stream from the camera and displays it in a window. Only streams in YUYV formats are supported for now. Signed-off-by: Laurent Pinchart Acked-by: Kieran Bingham Tested-by: Kieran Bingham --- src/meson.build | 1 + src/qcam/format_converter.cpp | 104 ++++++++++++++++++ src/qcam/format_converter.h | 25 +++++ src/qcam/main.cpp | 75 +++++++++++++ src/qcam/main_window.cpp | 228 +++++++++++++++++++++++++++++++++++++++ src/qcam/main_window.h | 54 ++++++++++ src/qcam/meson.build | 19 ++++ src/qcam/qt_event_dispatcher.cpp | 145 +++++++++++++++++++++++++ src/qcam/qt_event_dispatcher.h | 62 +++++++++++ src/qcam/viewfinder.cpp | 46 ++++++++ src/qcam/viewfinder.h | 34 ++++++ 11 files changed, 793 insertions(+) create mode 100644 src/qcam/format_converter.cpp create mode 100644 src/qcam/format_converter.h create mode 100644 src/qcam/main.cpp create mode 100644 src/qcam/main_window.cpp create mode 100644 src/qcam/main_window.h create mode 100644 src/qcam/meson.build create mode 100644 src/qcam/qt_event_dispatcher.cpp create mode 100644 src/qcam/qt_event_dispatcher.h create mode 100644 src/qcam/viewfinder.cpp create mode 100644 src/qcam/viewfinder.h diff --git a/src/meson.build b/src/meson.build index a7f2e75d..4e41fd3e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,2 +1,3 @@ subdir('libcamera') subdir('cam') +subdir('qcam') diff --git a/src/qcam/format_converter.cpp b/src/qcam/format_converter.cpp new file mode 100644 index 00000000..6979a054 --- /dev/null +++ b/src/qcam/format_converter.cpp @@ -0,0 +1,104 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * format_convert.cpp - qcam - Convert buffer to RGB + */ + +#include + +#include + +#include "format_converter.h" + +#define RGBSHIFT 8 +#ifndef MAX +#define MAX(a,b) ((a)>(b)?(a):(b)) +#endif +#ifndef MIN +#define MIN(a,b) ((a)<(b)?(a):(b)) +#endif +#ifndef CLAMP +#define CLAMP(a,low,high) MAX((low),MIN((high),(a))) +#endif +#ifndef CLIP +#define CLIP(x) CLAMP(x,0,255) +#endif + +int FormatConverter::configure(unsigned int format, unsigned int width, + unsigned int height) +{ + switch (format) { + case V4L2_PIX_FMT_VYUY: + y_pos_ = 1; + cb_pos_ = 2; + break; + case V4L2_PIX_FMT_YVYU: + y_pos_ = 0; + cb_pos_ = 3; + break; + case V4L2_PIX_FMT_UYVY: + y_pos_ = 1; + cb_pos_ = 0; + break; + case V4L2_PIX_FMT_YUYV: + y_pos_ = 0; + cb_pos_ = 1; + break; + default: + return -EINVAL; + }; + + width_ = width; + height_ = height; + + return 0; +} + +static void yuv_to_rgb(int y, int u, int v, int *r, int *g, int *b) +{ + int c = y - 16; + int d = u - 128; + int e = v - 128; + *r = CLIP(( 298 * c + 409 * e + 128) >> RGBSHIFT); + *g = CLIP(( 298 * c - 100 * d - 208 * e + 128) >> RGBSHIFT); + *b = CLIP(( 298 * c + 516 * d + 128) >> RGBSHIFT); +} + +void FormatConverter::convert(const unsigned char *src, unsigned char *dst) +{ + unsigned int src_x, src_y, dst_x, dst_y; + unsigned int src_stride; + unsigned int dst_stride; + unsigned int cr_pos; + int r, g, b, y, cr, cb; + + cr_pos = (cb_pos_ + 2) % 4; + src_stride = width_ * 2; + dst_stride = width_ * 4; + + for (src_y = 0, dst_y = 0; dst_y < height_; src_y++, dst_y++) { + for (src_x = 0, dst_x = 0; dst_x < width_; ) { + cb = src[src_y * src_stride + src_x * 4 + cb_pos_]; + cr = src[src_y * src_stride + src_x * 4 + cr_pos]; + + y = src[src_y * src_stride + src_x * 4 + y_pos_]; + yuv_to_rgb(y, cb, cr, &r, &g, &b); + dst[dst_y * dst_stride + 4 * dst_x + 0] = b; + dst[dst_y * dst_stride + 4 * dst_x + 1] = g; + dst[dst_y * dst_stride + 4 * dst_x + 2] = r; + dst[dst_y * dst_stride + 4 * dst_x + 3] = 0xff; + dst_x++; + + y = src[src_y * src_stride + src_x * 4 + y_pos_ + 2]; + yuv_to_rgb(y, cb, cr, &r, &g, &b); + dst[dst_y * dst_stride + 4 * dst_x + 0] = b; + dst[dst_y * dst_stride + 4 * dst_x + 1] = g; + dst[dst_y * dst_stride + 4 * dst_x + 2] = r; + dst[dst_y * dst_stride + 4 * dst_x + 3] = 0xff; + dst_x++; + + src_x++; + } + } +} diff --git a/src/qcam/format_converter.h b/src/qcam/format_converter.h new file mode 100644 index 00000000..196064c7 --- /dev/null +++ b/src/qcam/format_converter.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * format_convert.h - qcam - Convert buffer to RGB + */ +#ifndef __QCAM_FORMAT_CONVERTER_H__ +#define __QCAM_FORMAT_CONVERTER_H__ + +class FormatConverter +{ +public: + int configure(unsigned int format, unsigned int width, + unsigned int height); + + void convert(const unsigned char *src, unsigned char *dst); + +private: + unsigned int width_; + unsigned int height_; + unsigned int y_pos_; + unsigned int cb_pos_; +}; + +#endif /* __QCAM_FORMAT_CONVERTER_H__ */ diff --git a/src/qcam/main.cpp b/src/qcam/main.cpp new file mode 100644 index 00000000..106b8a16 --- /dev/null +++ b/src/qcam/main.cpp @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * main.cpp - cam - The libcamera swiss army knife + */ + +#include +#include +#include + +#include + +#include + +#include "main_window.h" +#include "../cam/options.h" +#include "qt_event_dispatcher.h" + +void signalHandler(int signal) +{ + std::cout << "Exiting" << std::endl; + qApp->quit(); +} + +OptionsParser::Options parseOptions(int argc, char *argv[]) +{ + OptionsParser parser; + parser.addOption(OptCamera, OptionString, + "Specify which camera to operate on", "camera", + ArgumentRequired, "camera"); + parser.addOption(OptHelp, OptionNone, "Display this help message", + "help"); + + OptionsParser::Options options = parser.parse(argc, argv); + if (options.isSet(OptHelp)) + parser.usage(); + + return options; +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + int ret; + + OptionsParser::Options options = parseOptions(argc, argv); + if (!options.valid()) + return EXIT_FAILURE; + if (options.isSet(OptHelp)) + return 0; + + struct sigaction sa = {}; + sa.sa_handler = &signalHandler; + sigaction(SIGINT, &sa, nullptr); + + std::unique_ptr dispatcher(new QtEventDispatcher()); + CameraManager *cm = CameraManager::instance(); + cm->setEventDispatcher(std::move(dispatcher)); + + ret = cm->start(); + if (ret) { + std::cout << "Failed to start camera manager: " + << strerror(-ret) << std::endl; + return EXIT_FAILURE; + } + + MainWindow *mainWindow = new MainWindow(options); + mainWindow->show(); + ret = app.exec(); + delete mainWindow; + + cm->stop(); + return ret; +} diff --git a/src/qcam/main_window.cpp b/src/qcam/main_window.cpp new file mode 100644 index 00000000..a148aa4d --- /dev/null +++ b/src/qcam/main_window.cpp @@ -0,0 +1,228 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * main_window.cpp - qcam - Main application window + */ + +#include +#include +#include + +#include +#include +#include + +#include + +#include "main_window.h" +#include "viewfinder.h" + +using namespace libcamera; + +MainWindow::MainWindow(const OptionsParser::Options &options) + : options_(options), isCapturing_(false) +{ + int ret; + + viewfinder_ = new ViewFinder(this); + setCentralWidget(viewfinder_); + viewfinder_->setFixedSize(500, 500); + adjustSize(); + + ret = openCamera(); + if (!ret) + ret = startCapture(); + + if (ret < 0) + QTimer::singleShot(0, QCoreApplication::instance(), + &QCoreApplication::quit); +} + +MainWindow::~MainWindow() +{ + if (camera_) { + stopCapture(); + camera_->release(); + camera_.reset(); + } + + CameraManager::instance()->stop(); +} + +int MainWindow::openCamera() +{ + CameraManager *cm = CameraManager::instance(); + std::string cameraName; + + if (!options_.isSet(OptCamera)) { + QStringList cameras; + bool result; + + for (const std::shared_ptr &cam : cm->cameras()) + cameras.append(QString::fromStdString(cam->name())); + + QString name = QInputDialog::getItem(this, "Select Camera", + "Camera:", cameras, 0, + false, &result); + if (!result) + return -EINVAL; + + cameraName = name.toStdString(); + } else { + cameraName = static_cast(options_[OptCamera]); + } + + camera_ = cm->get(cameraName); + if (!camera_) { + std::cout << "Camera " << cameraName << " not found" + << std::endl; + return -ENODEV; + } + + if (camera_->acquire()) { + std::cout << "Failed to acquire camera" << std::endl; + camera_.reset(); + return -EBUSY; + } + + std::cout << "Using camera " << camera_->name() << std::endl; + + camera_->requestCompleted.connect(this, &MainWindow::requestComplete); + + return 0; +} + +int MainWindow::startCapture() +{ + int ret; + + Stream *stream = *camera_->streams().begin(); + std::set streams{ stream }; + config_ = camera_->streamConfiguration(streams); + ret = camera_->configureStreams(config_); + if (ret < 0) { + std::cout << "Failed to configure camera" << std::endl; + return ret; + } + + const StreamConfiguration &sconf = config_[stream]; + ret = viewfinder_->setFormat(sconf.pixelFormat, sconf.width, sconf.height); + if (ret < 0) { + std::cout << "Failed to set viewfinder format" << std::endl; + return ret; + } + + adjustSize(); + + ret = camera_->allocateBuffers(); + if (ret) { + std::cerr << "Failed to allocate buffers" + << std::endl; + return ret; + } + + BufferPool &pool = stream->bufferPool(); + std::vector requests; + + for (Buffer &buffer : pool.buffers()) { + Request *request = camera_->createRequest(); + if (!request) { + std::cerr << "Can't create request" << std::endl; + ret = -ENOMEM; + goto error; + } + + std::map map; + map[stream] = &buffer; + ret = request->setBuffers(map); + if (ret < 0) { + std::cerr << "Can't set buffers for request" << std::endl; + goto error; + } + + requests.push_back(request); + } + + ret = camera_->start(); + if (ret) { + std::cout << "Failed to start capture" << std::endl; + goto error; + } + + for (Request *request : requests) { + ret = camera_->queueRequest(request); + if (ret < 0) { + std::cerr << "Can't queue request" << std::endl; + goto error; + } + } + + isCapturing_ = true; + return 0; + +error: + for (Request *request : requests) + delete request; + + camera_->freeBuffers(); + return ret; +} + +void MainWindow::stopCapture() +{ + if (!isCapturing_) + return; + + int ret = camera_->stop(); + if (ret) + std::cout << "Failed to stop capture" << std::endl; + + camera_->freeBuffers(); + isCapturing_ = false; +} + +void MainWindow::requestComplete(Request *request, + const std::map &buffers) +{ + static uint64_t last = 0; + + if (request->status() == Request::RequestCancelled) + return; + + Buffer *buffer = buffers.begin()->second; + + double fps = buffer->timestamp() - last; + fps = last && fps ? 1000000000.0 / fps : 0.0; + last = buffer->timestamp(); + + std::cout << "seq: " << std::setw(6) << std::setfill('0') << buffer->sequence() + << " buf: " << buffer->index() + << " bytesused: " << buffer->bytesused() + << " timestamp: " << buffer->timestamp() + << " fps: " << std::fixed << std::setprecision(2) << fps + << std::endl; + + display(buffer); + + request = camera_->createRequest(); + if (!request) { + std::cerr << "Can't create request" << std::endl; + return; + } + + request->setBuffers(buffers); + camera_->queueRequest(request); +} + +int MainWindow::display(Buffer *buffer) +{ + if (buffer->planes().size() != 1) + return -EINVAL; + + Plane &plane = buffer->planes().front(); + unsigned char *raw = static_cast(plane.mem()); + viewfinder_->display(raw); + + return 0; +} diff --git a/src/qcam/main_window.h b/src/qcam/main_window.h new file mode 100644 index 00000000..5e27a8fd --- /dev/null +++ b/src/qcam/main_window.h @@ -0,0 +1,54 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * main_window.h - qcam - Main application window + */ +#ifndef __QCAM_MAIN_WINDOW_H__ +#define __QCAM_MAIN_WINDOW_H__ + +#include + +#include + +#include +#include + +#include "../cam/options.h" + +using namespace libcamera; + +class ViewFinder; + +enum { + OptCamera = 'c', + OptHelp = 'h', +}; + +class MainWindow : public QMainWindow +{ +public: + MainWindow(const OptionsParser::Options &options); + ~MainWindow(); + +private: + int openCamera(); + + int startCapture(); + int configureStreams(Camera *camera, std::set &streams); + void stopCapture(); + + void requestComplete(Request *request, + const std::map &buffers); + int display(Buffer *buffer); + + const OptionsParser::Options &options_; + + std::shared_ptr camera_; + bool isCapturing_; + std::map config_; + + ViewFinder *viewfinder_; +}; + +#endif /* __QCAM_MAIN_WINDOW__ */ diff --git a/src/qcam/meson.build b/src/qcam/meson.build new file mode 100644 index 00000000..8a71cda3 --- /dev/null +++ b/src/qcam/meson.build @@ -0,0 +1,19 @@ +qcam_sources = files([ + 'format_converter.cpp', + 'main.cpp', + 'main_window.cpp', + '../cam/options.cpp', + 'qt_event_dispatcher.cpp', + 'viewfinder.cpp', +]) + +import('qt5') +qt5_dep = dependency('qt5', modules: ['Core', 'Gui', 'Widgets'], required : false) + +if qt5_dep.found() + qcam = executable('qcam', qcam_sources, + link_with : libcamera, + include_directories : libcamera_includes, + dependencies : qt5_dep, + cpp_args : '-DQT_NO_KEYWORDS') +endif diff --git a/src/qcam/qt_event_dispatcher.cpp b/src/qcam/qt_event_dispatcher.cpp new file mode 100644 index 00000000..5ba451bf --- /dev/null +++ b/src/qcam/qt_event_dispatcher.cpp @@ -0,0 +1,145 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * qt_event_dispatcher.cpp - qcam - Qt-based event dispatcher + */ + +#include + +#include +#include +#include +#include + +#include +#include + +#include "qt_event_dispatcher.h" + +using namespace libcamera; + +QtEventDispatcher::QtEventDispatcher() +{ +} + +QtEventDispatcher::~QtEventDispatcher() +{ + for (auto &it : notifiers_) { + NotifierSet &set = it.second; + delete set.read.qnotifier; + delete set.write.qnotifier; + delete set.exception.qnotifier; + } +} + +void QtEventDispatcher::registerEventNotifier(EventNotifier *notifier) +{ + NotifierSet &set = notifiers_[notifier->fd()]; + QSocketNotifier::Type qtype; + void (QtEventDispatcher::*method)(int); + NotifierPair *pair; + + switch (notifier->type()) { + case EventNotifier::Read: + default: + qtype = QSocketNotifier::Read; + method = &QtEventDispatcher::readNotifierActivated; + pair = &set.read; + break; + + case EventNotifier::Write: + qtype = QSocketNotifier::Write; + method = &QtEventDispatcher::writeNotifierActivated; + pair = &set.write; + break; + + case EventNotifier::Exception: + qtype = QSocketNotifier::Exception; + method = &QtEventDispatcher::exceptionNotifierActivated; + pair = &set.exception; + break; + } + + QSocketNotifier *qnotifier = new QSocketNotifier(notifier->fd(), qtype); + connect(qnotifier, &QSocketNotifier::activated, this, method); + pair->notifier = notifier; + pair->qnotifier = qnotifier; +} + +void QtEventDispatcher::unregisterEventNotifier(EventNotifier *notifier) +{ + NotifierSet &set = notifiers_[notifier->fd()]; + NotifierPair *pair; + + switch (notifier->type()) { + case EventNotifier::Read: + default: + pair = &set.read; + break; + + case EventNotifier::Write: + pair = &set.write; + break; + + case EventNotifier::Exception: + pair = &set.exception; + break; + } + + delete pair->qnotifier; + pair->qnotifier = nullptr; + pair->notifier = nullptr; +} + +void QtEventDispatcher::readNotifierActivated(int socket) +{ + EventNotifier *notifier = notifiers_[socket].read.notifier; + notifier->activated.emit(notifier); +} + +void QtEventDispatcher::writeNotifierActivated(int socket) +{ + EventNotifier *notifier = notifiers_[socket].write.notifier; + notifier->activated.emit(notifier); +} + +void QtEventDispatcher::exceptionNotifierActivated(int socket) +{ + EventNotifier *notifier = notifiers_[socket].exception.notifier; + notifier->activated.emit(notifier); +} + +void QtEventDispatcher::registerTimer(Timer *timer) +{ + int timerId = startTimer(timer->interval()); + timers_[timerId] = timer; + timerIds_[timer] = timerId; +} + +void QtEventDispatcher::unregisterTimer(Timer *timer) +{ + auto it = timerIds_.find(timer); + timers_.erase(it->second); + killTimer(it->second); + timerIds_.erase(it); +} + +void QtEventDispatcher::timerEvent(QTimerEvent *event) +{ + auto it = timers_.find(event->timerId()); + timerIds_.erase(it->second); + killTimer(it->first); + timers_.erase(it); +} + +void QtEventDispatcher::processEvents() +{ + std::cout << "QtEventDispatcher::processEvents() should not be called" + << std::endl; +} + +void QtEventDispatcher::interrupt() +{ + QCoreApplication::eventDispatcher()->interrupt(); +} diff --git a/src/qcam/qt_event_dispatcher.h b/src/qcam/qt_event_dispatcher.h new file mode 100644 index 00000000..b0f123e5 --- /dev/null +++ b/src/qcam/qt_event_dispatcher.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * qt_event_dispatcher.h - qcam - Qt-based event dispatcher + */ +#ifndef __QCAM_QT_EVENT_DISPATCHER_H__ +#define __QCAM_QT_EVENT_DISPATCHER_H__ + +#include + +#include + +using namespace libcamera; + +class QSocketNotifier; + +class QtEventDispatcher final : public EventDispatcher, public QObject +{ +public: + QtEventDispatcher(); + ~QtEventDispatcher(); + + void registerEventNotifier(EventNotifier *notifier); + void unregisterEventNotifier(EventNotifier *notifier); + + void registerTimer(Timer *timer); + void unregisterTimer(Timer *timer); + + void processEvents(); + + void interrupt(); + +protected: + void timerEvent(QTimerEvent *event); + +private: + void readNotifierActivated(int socket); + void writeNotifierActivated(int socket); + void exceptionNotifierActivated(int socket); + + struct NotifierPair { + NotifierPair() + : notifier(nullptr), qnotifier(nullptr) + { + } + EventNotifier *notifier; + QSocketNotifier *qnotifier; + }; + + struct NotifierSet { + NotifierPair read; + NotifierPair write; + NotifierPair exception; + }; + + std::map notifiers_; + std::map timers_; + std::map timerIds_; +}; + +#endif /* __QCAM_QT_EVENT_DISPATCHER_H__ */ diff --git a/src/qcam/viewfinder.cpp b/src/qcam/viewfinder.cpp new file mode 100644 index 00000000..5841dc03 --- /dev/null +++ b/src/qcam/viewfinder.cpp @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * viewfinder.cpp - qcam - Viewfinder + */ + +#include +#include + +#include "format_converter.h" +#include "viewfinder.h" + +ViewFinder::ViewFinder(QWidget *parent) + : QLabel(parent), format_(0), width_(0), height_(0), image_(nullptr) +{ +} + +void ViewFinder::display(const unsigned char *raw) +{ + converter_.convert(raw, image_->bits()); + + QPixmap pixmap = QPixmap::fromImage(*image_); + setPixmap(pixmap); +} + +int ViewFinder::setFormat(unsigned int format, unsigned int width, + unsigned int height) +{ + int ret; + + ret = converter_.configure(format, width, height); + if (ret < 0) + return ret; + + format_ = format; + width_ = width; + height_ = height; + + setFixedSize(width, height); + + delete image_; + image_ = new QImage(width, height, QImage::Format_RGB32); + + return 0; +} diff --git a/src/qcam/viewfinder.h b/src/qcam/viewfinder.h new file mode 100644 index 00000000..df490169 --- /dev/null +++ b/src/qcam/viewfinder.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2019, Google Inc. + * + * viewfinder.h - qcam - Viewfinder + */ +#ifndef __QCAM_VIEWFINDER_H__ +#define __QCAM_VIEWFINDER_H__ + +#include + +#include "format_converter.h" + +class QImage; + +class ViewFinder : public QLabel +{ +public: + ViewFinder(QWidget *parent); + + int setFormat(unsigned int format, unsigned int width, + unsigned int height); + void display(const unsigned char *rgb); + +private: + unsigned int format_; + unsigned int width_; + unsigned int height_; + + FormatConverter converter_; + QImage *image_; +}; + +#endif /* __QCAM_VIEWFINDER__ */ -- cgit v1.2.1