Kaidan/src/QrCodeVideoFrame.cpp

309 lines
9.5 KiB
C++

/*
* Kaidan - A user-friendly XMPP client for every device!
*
* Copyright (C) 2016-2021 Kaidan developers and contributors
* (see the LICENSE file for a full list of copyright authors)
*
* Kaidan is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, as a special exception, the author of Kaidan gives
* permission to link the code of its release with the OpenSSL
* project's "OpenSSL" library (or with modified versions of it that
* use the same license as the "OpenSSL" library), and distribute the
* linked executables. You must obey the GNU General Public License in
* all respects for all of the code used other than "OpenSSL". If you
* modify this file, you may extend this exception to your version of
* the file, but you are not obligated to do so. If you do not wish to
* do so, delete this exception statement from your version.
*
* Kaidan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kaidan. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright 2017 QZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "QrCodeVideoFrame.h"
#include <QImage>
#include <cmath>
/**
* rectangle of the video frame which may contain a QR code
*/
struct CaptureRect
{
CaptureRect(const QRect& captureRect, int sourceWidth, int sourceHeight)
: isValid(captureRect.x() >= 0 && captureRect.y() >= 0 && captureRect.isValid()),
sourceWidth(sourceWidth),
sourceHeight(sourceHeight),
startX(isValid ? captureRect.x() : 0),
targetWidth(isValid ? captureRect.width() : sourceWidth),
endX(startX + targetWidth),
startY(isValid ? captureRect.y() : 0),
targetHeight(isValid ? captureRect.height() : sourceHeight),
endY(startY + targetHeight)
{}
bool isValid;
int sourceWidth;
int sourceHeight;
int startX;
int targetWidth;
int endX;
int startY;
int targetHeight;
int endY;
};
uchar gray(uchar r, uchar g, uchar b)
{
return (306 * (r & 0xFF) +
601 * (g & 0xFF) +
117 * (b & 0xFF) +
0x200) >> 10;
}
uchar yuvToGray(uchar Y, uchar U, uchar V)
{
const int C = int(Y) - 16;
const int D = int(U) - 128;
const int E = int(V) - 128;
return gray(
qBound<uchar>(0, uchar((298 * C + 409 * E + 128) >> 8), 255),
qBound<uchar>(0, uchar((298 * C - 100 * D - 208 * E + 128) >> 8), 255),
qBound<uchar>(0, uchar((298 * C + 516 * D + 128) >> 8), 255)
);
}
uchar yuvToGray2(uchar y, uchar u, uchar v)
{
double rD = y + 1.4075 * (v - 128);
double gD = y - 0.3455 * (u - 128) - (0.7169 * (v - 128));
double bD = y + 1.7790 * (u - 128);
return gray(
qBound<uchar>(0, uchar(floor(rD)), 255),
qBound<uchar>(0, uchar(floor(gD)), 255),
qBound<uchar>(0, uchar(floor(bD)), 255)
);
}
static QImage* rgbDataToGrayscale(
const uchar* data,
const CaptureRect &captureRect,
const int alpha,
const int red,
const int green,
const int blue,
const bool isPremultiplied = false
) {
const int stride = (alpha < 0) ? 3 : 4;
const int endX = captureRect.sourceWidth - captureRect.startX - captureRect.targetWidth;
const int skipX = (endX + captureRect.startX) * stride;
QImage *image_ptr = new QImage(captureRect.targetWidth, captureRect.targetHeight, QImage::Format_Grayscale8);
uchar* pixelInit = image_ptr->bits();
data += (captureRect.startY * captureRect.sourceWidth + captureRect.startX) * stride;
for (int y = 1; y <= captureRect.targetHeight; ++y) {
// Quick fix for iOS devices. Will be handled better in the future
#ifdef Q_OS_IOS
uchar* pixel = pixelInit + (y - 1) * captureRect.targetWidth;
#else
uchar* pixel = pixelInit + (captureRect.targetHeight - y) * captureRect.targetWidth;
#endif
for (int x = 0; x < captureRect.targetWidth; ++x) {
uchar r = data[red];
uchar g = data[green];
uchar b = data[blue];
if (isPremultiplied) {
uchar a = data[alpha];
r = uchar((uint(r) * 255) / a);
g = uchar((uint(g) * 255) / a);
b = uchar((uint(b) * 255) / a);
}
*pixel = gray(r, g, b);
++pixel;
data += stride;
}
data += skipX;
}
return image_ptr;
}
void QrCodeVideoFrame::setData(QVideoFrame &frame)
{
frame.map(QAbstractVideoBuffer::ReadOnly);
// Copy video frame bytes to this.data.
// This is made to try to get a better performance (less memory allocation, faster unmap)
// Any other task is performed in a QFuture task, as we want to leave the UI thread asap.
if (m_data.size() != frame.mappedBytes()) {
m_data.resize(frame.mappedBytes());
}
memcpy(m_data.data(), frame.bits(), frame.mappedBytes());
m_size = frame.size();
m_pixelFormat = frame.pixelFormat();
frame.unmap();
}
QImage *QrCodeVideoFrame::toGrayscaleImage()
{
const CaptureRect captureRect(QRect(), m_size.width(), m_size.height());
const auto* data = reinterpret_cast<const uchar *>(m_data.constData());
const auto *yuvPtr = reinterpret_cast<const uint32_t *>(data);
uchar *pixel;
int wh;
int w_2;
int wh_54;
QImage *image;
switch (m_pixelFormat) {
case QVideoFrame::Format_ARGB32:
image = rgbDataToGrayscale(data, captureRect, 0, 1, 2, 3);
break;
case QVideoFrame::Format_ARGB32_Premultiplied:
image = rgbDataToGrayscale(data, captureRect, 0, 1, 2, 3, true);
break;
case QVideoFrame::Format_RGB32:
image = rgbDataToGrayscale(data, captureRect, 0, 1, 2, 3);
break;
case QVideoFrame::Format_RGB24:
image = rgbDataToGrayscale(data, captureRect, -1, 0, 1, 2);
break;
// TODO: QVideoFrame::Format_RGB565
// TODO: QVideoFrame::Format_RGB555
// TODO: QVideoFrame::Format_ARGB8565_Premultiplied
case QVideoFrame::Format_BGRA32:
image = rgbDataToGrayscale(data, captureRect, 3, 2, 1, 0);
break;
case QVideoFrame::Format_BGRA32_Premultiplied:
image = rgbDataToGrayscale(data, captureRect, 3, 2, 1, 0, true);
break;
case QVideoFrame::Format_ABGR32:
image = rgbDataToGrayscale(data, captureRect, 0, 3, 2, 1);
break;
case QVideoFrame::Format_BGR32:
image = rgbDataToGrayscale(data, captureRect, 3, 2, 1, 0);
break;
case QVideoFrame::Format_BGR24:
image = rgbDataToGrayscale(data, captureRect, -1, 2, 1, 0);
break;
case QVideoFrame::Format_BGR565:
/// This is a forced "conversion", colors end up swapped.
image = new QImage(data, m_size.width(), m_size.height(), QImage::Format_RGB16);
break;
case QVideoFrame::Format_BGR555:
/// This is a forced "conversion", colors end up swapped.
image = new QImage(data, m_size.width(), m_size.height(), QImage::Format_RGB555);
break;
// TODO: QVideoFrame::Format_BGRA5658_Premultiplied
case QVideoFrame::Format_YUV420P:
case QVideoFrame::Format_NV12:
/// nv12 format, encountered on macOS
image = new QImage(captureRect.targetWidth, captureRect.targetHeight, QImage::Format_Grayscale8);
pixel = image->bits();
wh = m_size.width() * m_size.height();
w_2 = m_size.width() / 2;
wh_54 = wh * 5 / 4;
for (int y = captureRect.startY; y < captureRect.endY; y++) {
const int Y_offset = y * m_size.width();
const int y_2 = y / 2;
const int U_offset = y_2 * w_2 + wh;
const int V_offset = y_2 * w_2 + wh_54;
for (int x = captureRect.startX; x < captureRect.endX; x++) {
const int x_2 = x / 2;
const uchar Y = data[Y_offset + x];
const uchar U = data[U_offset + x_2];
const uchar V = data[V_offset + x_2];
*pixel = yuvToGray(Y, U, V);
++pixel;
}
}
break;
case QVideoFrame::Format_NV21:
/// nv21 format, default on android
/// image starts with a complete Y image, which we can use directly
image = new QImage(data, captureRect.targetWidth, captureRect.targetHeight, QImage::Format_Grayscale8);
break;
case QVideoFrame::Format_YUYV:
image = new QImage(captureRect.targetWidth, captureRect.targetHeight, QImage::Format_Grayscale8);
pixel = image->bits();
for (int y = captureRect.startY; y < captureRect.endY; y++){
const uint32_t *row = &yuvPtr[y*(m_size.width()/2)];
int end = captureRect.startX/2 + (captureRect.endX - captureRect.startX)/2;
for (int x = captureRect.startX/2; x < end; x++){
const uint8_t *pxl = reinterpret_cast<const uint8_t *>(&row[x]);
const uint8_t y0 = pxl[0];
const uint8_t u = pxl[1];
const uint8_t v = pxl[3];
const uint8_t y1 = pxl[2];
*pixel = yuvToGray2(y0, u, v);
++pixel;
*pixel = yuvToGray2(y1, u, v);
++pixel;
}
}
break;
// TODO: QVideoFrame::Format_IMC*
// TODO: QVideoFrame::Format_*YUV*
// TODO: QVideoFrame::Format_Y*
// TODO: QVideoFrame::Format_Jpeg (needed?)
default:
image = new QImage(
data,
m_size.width(),
m_size.height(),
QVideoFrame::imageFormatFromPixelFormat(m_pixelFormat)
);
break;
}
return image;
}
QByteArray QrCodeVideoFrame::data() const
{
return m_data;
}
QSize QrCodeVideoFrame::size() const
{
return m_size;
}
QVideoFrame::PixelFormat QrCodeVideoFrame::pixelFormat() const
{
return m_pixelFormat;
}