仅计算 "core" QImage 数据(不包括元数据)的 QCryptographicHash

Compute QCryptographicHash of only the "core" QImage data (excluding metadata)

我有一堆 "JPG" 文件,仅 EXIF 数据不同。

我想做一个快速检查(使用 Qt 框架),尝试计算 "core" 图像数据的哈希值(并且 而不是 文件本身,这将包括元数据)。 到目前为止一切顺利。

这是我加载图像和计算哈希值的方式:

QImage img(R"(D:\Picture.jpg)");
auto data = QByteArray::fromRawData(reinterpret_cast<const char *>(img.constBits()), int(img.sizeInBytes()));

QCryptographicHash hash(QCryptographicHash::Sha256);
hash.addData(data);
qDebug() << hash.result().toHex();

我想将相同的概念扩展到 "JPG" 以外的文件,所以我在不改变分辨率的情况下以不同的无损格式(BMP、PNG、TIF)保存了原始 JPG 文件。

我遇到了一个问题。 BMP、PNG、TIF 图像的 Hash 给出了相同的结果,但与 JPG 中相同图像的结果不同。

如果我从 LOSSLES 格式创建 JPG 文件,我可以理解结果。
但反过来呢???

谁能帮我弄清楚我哪里错了?


给出如下代码我看到了:

// JPG w/o EXIF data
QImage img1(R"(D:\Picture.jpg)");
auto data1 = QByteArray::fromRawData(reinterpret_cast<const char *>(img1.constBits()), int(img1.sizeInBytes()));

// JPG w/  EXIF data
QImage img2(R"(D:\Picture_EXIF.jpg)");
auto data2 = QByteArray::fromRawData(reinterpret_cast<const char *>(img2.constBits()), int(img2.sizeInBytes()));

// BMP
QImage img3(R"(D:\Picture.bmp)");
auto data3 = QByteArray::fromRawData(reinterpret_cast<const char *>(img3.constBits()), int(img3.sizeInBytes()));

// PNG w/o transparency
QImage img4(R"(D:\Picture.png)");
auto data4 = QByteArray::fromRawData(reinterpret_cast<const char *>(img4.constBits()), int(img4.sizeInBytes()));

// TIF (lossles)
QImage img5(R"(D:\Picture.tif)");
auto data5 = QByteArray::fromRawData(reinterpret_cast<const char *>(img5.constBits()), int(img5.sizeInBytes()));

qDebug() << img1.sizeInBytes(); // 23918592
qDebug() << img2.sizeInBytes(); // 23918592
qDebug() << img3.sizeInBytes(); // 23918592
qDebug() << img4.sizeInBytes(); // 23918592
qDebug() << img5.sizeInBytes(); // 23918592

qDebug() << (data1 == data2); // True
qDebug() << (data1 == data3); // False
qDebug() << (data3 == data4); // True
qDebug() << (data3 == data5); // True

qDebug() << img1.format(); // 4 = QImage::Format_RGB32
qDebug() << img2.format(); // 4 = QImage::Format_RGB32
qDebug() << img3.format(); // 4 = QImage::Format_RGB32
qDebug() << img4.format(); // 5 = QImage::Format_ARGB32
qDebug() << img5.format(); // 6 = QImage::Format_ARGB32_Premultiplied

QCryptographicHash hash(QCryptographicHash::Sha256);
hash.reset(); hash.addData(data1); qDebug() << hash.result().toHex(); // c37143639914056add1f90be4bfe780e14500d24f1d3484a087fc1943508157f
hash.reset(); hash.addData(data2); qDebug() << hash.result().toHex(); // c37143639914056add1f90be4bfe780e14500d24f1d3484a087fc1943508157f
hash.reset(); hash.addData(data3); qDebug() << hash.result().toHex(); // 0149c60b883df67ba002d791a1362dbd02ccab09241864341483a16ec0af635d
hash.reset(); hash.addData(data4); qDebug() << hash.result().toHex(); // 0149c60b883df67ba002d791a1362dbd02ccab09241864341483a16ec0af635d
hash.reset(); hash.addData(data5); qDebug() << hash.result().toHex(); // 0149c60b883df67ba002d791a1362dbd02ccab09241864341483a16ec0af635d


最终结论
在随后的测试中,我意识到问题既不是 Qt 也不是我(最后)代码的实现(感谢@Scheff)。 BMP、PNG 和 TIF 实际上与原始 JPG 文件不同

BMP、PNG 和 TIF 文件是通过在 Windows Paint 中打开原始 JPG 文件并将其保存为这些无损格式而创建的。因此 Windows Paint 在读取(或)保存步骤中以某种方式失败。 商业软件 Duplicate Cleaner 也失败了,因为它报告 JPG 文件与 BMP、PNG、TIF 版本 100% 相同。

前言:

我考虑

data1.append(reinterpret_cast<const char *>(img1.constBits()));

用不存储 C 字符串的数据填充 QByteArray 的错误方法。

QByteArray::append(const char*) 很适合在 QByteArray 中复制 C 字符串。它复制数据直到找到 0 字节(0 终止符)。 0 字节可能位于图像原始数据中的任何位置,也可能无处可寻。在第一种情况下,复制的数据太少,在后一种情况下,考虑超出范围的数据。两者都是无意的。

顺便说一句。甚至不需要复制图像数据(可能很大)。

我做了一个样本直接比较两个QImage的原始数据,包括预先检查大小和格式是否匹配。

我的样本testQImageRawCmp.cc

#include <QtWidgets>

bool equalImgData(const QImage &qImg1, const QImage &qImg2, int eps = 0)
{
  // test properties
#define TEST_PROP(PROP) \
  do if (qImg1.PROP != qImg2.PROP) { \
    qDebug() << "qImg1."#PROP" != qImg2."#PROP; \
    return false; \
  } while(false)
  TEST_PROP(width());
  TEST_PROP(height());
  TEST_PROP(format());
#undef TEST_PROP
  // test raw data
  const uchar *const data1 = qImg1.bits();
  const uchar *const data2 = qImg2.bits();
  const int bytesPerLine1 = qImg1.bytesPerLine();
  const int bytesPerLine2 = qImg2.bytesPerLine();
  const int nBits = qImg1.depth() * qImg1.width();
  const int nBytes = (nBits + 7) / 8;
  assert(nBytes <= bytesPerLine1);
  assert(nBytes <= bytesPerLine2);
  for (int y = 0; y < qImg1.height(); ++y) {
    const uchar *row1 = data1 + y * bytesPerLine1;
    const uchar *row2 = data2 + y * bytesPerLine2;
    for (int x = 0; x < nBytes; ++x) {
      if (abs(row2[x] - row1[x]) > eps) {
        qDebug() << "Row" << y << "byte" << x << "difference:" << (row2[x] - row1[x]);
        return false;
      }
    }
  }
  return true;
}

int main(int argc, char **argv)
{
  qDebug() << "Qt Version:" << QT_VERSION_STR;
  // load sample data
  QImage img1("Picture.jpg");
  QImage img2("Picture.bmp");
#if 0 // U.B.
  // Juttas test:
  QByteArray data1; data1.append(reinterpret_cast<const char *>(img1.constBits()));
  QByteArray data2; data2.append(reinterpret_cast<const char *>(img2.constBits()));
#endif // 0
  // My test:
  if (!equalImgData(img1, img2, 3)) {
    qDebug() << "Images not equal!";
  } else {
    qDebug() << "Images equal.";
  }
}

我用OP提供的示例数据测试了这个程序:

并得到以下输出:

Qt Version: 5.13.0
Row 0 byte 60 difference: 1
Images not equal!

我必须承认,这段代码的第一个版本只是报告了不等式。

然后,我尝试制作自己的反样本并在 GIMP 中将 Picture.bmp 转换为 Picture.bmp.jpg(使用 100% 质量设置,我认为这是无损的)。这导致 Row 0 byte 0 的差异。糟糕!

然后,我开始好奇并修改代码以查看图像有多少不同。

一个像素的红、绿、蓝值相差1并不大。我怀疑这对于普通人来说甚至是可见的。

因此,我修改了代码(进入公开版本)以容忍某种差异。

eps 为 3:

if (equalImgData(img1, img2, 3)) {

图像被认为是平等的。