Skip to content

ARW: Support new LJPEG compression on ILCE-7M4 #386

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions src/librawspeed/decoders/ArwDecoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
#include "adt/Point.h" // for iPoint2D
#include "common/Common.h" // for roundDown
#include "decoders/RawDecoderException.h" // for ThrowException
#include "common/RawspeedException.h" // for RawspeedException
#include "decoders/RawDecoderException.h" // for ThrowRDE
#include "decompressors/LJpegDecompressor.h" // for LJpegDecompressor
#include "decompressors/SonyArw1Decompressor.h" // for SonyArw1Decompre...
#include "decompressors/SonyArw2Decompressor.h" // for SonyArw2Decompre...
#include "decompressors/UncompressedDecompressor.h" // for UncompressedDeco...
Expand Down Expand Up @@ -142,6 +145,11 @@ RawImage ArwDecoder::decodeRawInternal() {
return mRaw;
}

if (7 == compression) {
DecodeLJpeg(raw);
return mRaw;
}

if (32767 != compression)
ThrowRDE("Unsupported compression %i", compression);

Expand Down Expand Up @@ -183,8 +191,8 @@ RawImage ArwDecoder::decodeRawInternal() {
}
}

if (width == 0 || height == 0 || height % 2 != 0 || width > 9600 ||
height > 6376)
if (width == 0 || height == 0 || height % 2 != 0 || width > 9728 ||
height > 6656)
ThrowRDE("Unexpected image dimensions found: (%u; %u)", width, height);

bool arw1 = uint64_t(counts->getU32()) * 8 != width * height * bitPerPixel;
Expand Down Expand Up @@ -242,7 +250,7 @@ void ArwDecoder::DecodeUncompressed(const TiffIFD* raw) const {

mRaw->dim = iPoint2D(width, height);

if (width == 0 || height == 0 || width > 9600 || height > 6376)
if (width == 0 || height == 0 || width > 9728 || height > 6656)
ThrowRDE("Unexpected image dimensions found: (%u; %u)", width, height);

if (c2 == 0)
Expand All @@ -261,6 +269,78 @@ void ArwDecoder::DecodeUncompressed(const TiffIFD* raw) const {
u.decodeRawUnpacked<16, Endianness::little>(width, height);
}

void ArwDecoder::DecodeLJpeg(const TiffIFD* raw) const {
uint32_t width = raw->getEntry(TiffTag::IMAGEWIDTH)->getU32();
uint32_t height = raw->getEntry(TiffTag::IMAGELENGTH)->getU32();
uint32_t bitPerPixel = raw->getEntry(TiffTag::BITSPERSAMPLE)->getU32();

switch (bitPerPixel) {
case 8:
case 12:
case 14:
break;
default:
ThrowRDE("Unexpected bits per pixel: %u", bitPerPixel);
}

if (width == 0 || height == 0 || height % 2 != 0 || width > 9728 ||
height > 6656)
ThrowRDE("Unexpected image dimensions found: (%u; %u)", width, height);

mRaw->dim = iPoint2D(width, height);


const uint32_t tilew = raw->getEntry(TiffTag::TILEWIDTH)->getU32();
const uint32_t tileh = raw->getEntry(TiffTag::TILELENGTH)->getU32();

if (!(tilew > 0 && tileh > 0))
ThrowRDE("Invalid tile size: (%u, %u)", tilew, tileh);

assert(tilew > 0);
const uint32_t tilesX = roundUpDivision(mRaw->dim.x, tilew);
if (!tilesX)
ThrowRDE("Zero tiles horizontally");

assert(tileh > 0);
const uint32_t tilesY = roundUpDivision(mRaw->dim.y, tileh);
if (!tilesY)
ThrowRDE("Zero tiles vertically");

const TiffEntry* offsets = raw->getEntry(TiffTag::TILEOFFSETS);
const TiffEntry* counts = raw->getEntry(TiffTag::TILEBYTECOUNTS);
if (offsets->count != counts->count) {
ThrowRDE("Tile count mismatch: offsets:%u count:%u", offsets->count,
counts->count);
}

// tilesX * tilesY may overflow, but division is fine, so let's do that.
if ((offsets->count / tilesX != tilesY || (offsets->count % tilesX != 0)) ||
(offsets->count / tilesY != tilesX || (offsets->count % tilesY != 0))) {
ThrowRDE("Tile X/Y count mismatch: total:%u X:%u, Y:%u", offsets->count,
tilesX, tilesY);
}

mRaw->createData();
for (uint32_t tile = 0; tile < offsets->count; tile++) {
const uint32_t tileX = tile % tilesX;
const uint32_t tileY = tile / tilesX;
const uint32_t offset = offsets->getU32(tile);
const uint32_t length = counts->getU32(tile);
LJpegDecompressor decompressor(
ByteStream(
DataBuffer(mFile.getSubView(offset, length), Endianness::little)),
mRaw);
decompressor.decode(tileX * tilew, tileY * tileh, tilew, tileh, false,
true);
}

const TiffEntry* origin_entry = raw->getEntry(TiffTag::DEFAULTCROPORIGIN);
const TiffEntry* size_entry = raw->getEntry(TiffTag::DEFAULTCROPSIZE);
iRectangle2D crop(origin_entry->getU32(0), origin_entry->getU32(1),
size_entry->getU32(0), size_entry->getU32(1));
mRaw->subFrame(crop);
Comment on lines +337 to +341
Copy link
Collaborator

@kmilos kmilos Oct 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we use the file crop in any other modes (uncompressed and lossy), but rather leave it to the settings from XML? The way it is suggested now, there will be a difference between the modes, in both final image size, and the location of top left pixel...

There is a small problem to handle there, as width/height for lossless is wrongly specified by Sony as integer number of tiles, and it didn't have to be... One easy way to handle this was to have always an absolute positive crop in the XML for all modes, but it was rejected...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remove it then the original under history has black bars on the bottom right, although it is cropped out later in the default module history.

Copy link
Collaborator

@kmilos kmilos Feb 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I didn't mean remove the crop completely. But we'll have to find a way to provide the same crop as uncompressed/lossy mode (defined in data/cameras.xml), and that works for APC-C/S35 files as well.

Copy link
Collaborator

@kmilos kmilos Mar 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@artemist I think this tag has the information of the maximum available raw image size that you actually want, e.g. for the 7M4:

Exif.SubImage1.0x7038                          2  7032 4688

@LebedevRI It looks like cameras.xml crop for uncompressed/lossy could be too conservative and we could have it match this by changing to "-8" (will check for other recent Sonys as well; alternatively we could start recognizing ARW 4.0 files and go directly to this tag in all modes edit: tag not available in other modes)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remove it then the original under history has black bars on the bottom right, although it is cropped out later in the default module history.

This is the correct behaviour.

<...>

Since we don't use the camera-specified crop,
i don't really see why we care what the EXIF says,
we are just going to re-use the existing cameras.xml-specified crop in that mode too,
unless we need a different one, like some Fuji's need.

Copy link
Collaborator

@kmilos kmilos Mar 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't really see why we care what the EXIF says,

Because of this new lossless mode and tile size rounding up nonsense. This is actually the one thing that correlates it to the traditional modes and cameras.xml (unless you want to bloat cameras.xml with more entries, which I don't think is a good idea).

I'll provide an example if it's not clear.

Copy link
Collaborator

@kmilos kmilos Mar 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LebedevRI Ok, let's take the 7M4 here - in the traditional uncompressed and lossy modes it has

Exif.SubImage1.ImageWidth                     7040
Exif.SubImage1.ImageLength                    4688

and in (tweaked) cameras.xml we put <Crop x="0" y="0" width="-8" height="0"/>, so the resulting raw image size is 7032 4688.

In the new lossless mode it has this nonsensical round up to integer multiple of 512x512 tiles:

Exif.SubImage1.ImageWidth                     7168
Exif.SubImage1.ImageLength                    5120

that produces huge black padding on the right and at the bottom, and then this new tag

Exif.SubImage1.0x7038                          7032 4688

See the pattern here?

So options for the lossless mode implementation to give us the same output size as uncompressed are 1) bloat cameras.xml with more entries just so we can have different crop numbers (and not just one; we'll need extra for APS-C outputs as remainder/padding after rounding up is different of course) or 2) just use the bloody 0x7038 tag as is because it is exactly what it means - maximum usable raw area, not "camera-specified crop".

I have verified all the ILCE-1/7M4/7SM3/7RM5 samples on RPU (dump using LibRaw's unprocessed_raw which also works for lossless) and everything matches up (w/ minor tweaks), and it also lines up perfectly for the APS-C mode of 7RM5 and cameras.xml crop for the corresponding uncompressed.

i don't really see why we care what the EXIF says.

And a final thing - this is not "crop according to EXIF", as we have just established. That would in fact be further cropping according to

Exif.SubImage1.DefaultCropOrigin              12 8
Exif.SubImage1.DefaultCropSize                7008 4672

which is what @artemist is doing now (and matching OOC JPEG size). I agree, this is not what we generally want or care about.

}

void ArwDecoder::DecodeARW2(const ByteStream& input, uint32_t w, uint32_t h,
uint32_t bpp) {

Expand Down
1 change: 1 addition & 0 deletions src/librawspeed/decoders/ArwDecoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class ArwDecoder final : public AbstractTiffDecoder
void DecodeARW2(const ByteStream& input, uint32_t w, uint32_t h,
uint32_t bpp);
void DecodeUncompressed(const TiffIFD* raw) const;
void DecodeLJpeg(const TiffIFD* raw) const;
static void SonyDecrypt(const uint32_t* ibuf, uint32_t* obuf, uint32_t len,
uint32_t key);
void GetWB() const;
Expand Down
40 changes: 40 additions & 0 deletions src/librawspeed/decompressors/AbstractLJpegDecompressor.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,46 @@ class AbstractLJpegDecompressor : public AbstractDecompressor {
bool fixDng16Bug = false; // DNG v1.0.x compatibility
bool fullDecodeHT = true; // FullDecode Huffman

/**
* Whether or not to interleave rows;
* Until this was added we assumed that pixels were in order,
* from left to right then top to bottom. However, the JFIF specification
* does not require this. An interleave has a specified width (H, horizontal)
* and height (V, vertical). Within each rectangle of the specified size pixels
* are arranged from left to right then top to bottom. These rectanges are then
* arranged to fill the image.
*
* If this bool is false then V=1 and H=N_COMP, if this is true then V=2 and H=N_COMP/2.
*
* With V=1 then any H value will be in the order you expect (here is a 4x4 image)
* 0 1 2 3
* 4 5 6 7
* 8 9 10 11
* 12 13 14 15
*
* However, this isn't true on all cameras.
* Some Sony cameras have V=2 and H=2
* 0 1 4 5
* 2 3 6 7
* 8 9 12 13
* 10 11 14 15
*
* This looks weird but makes some sense because all pixels will be predicted based on
* the same color (Red, Green 1, Green 2, or Blue). With interleaveHeight=1 this property
* won't hold on the first pixel of each row.
*
* Some Blackmagic cameras layout with V=2 and H=1 which looks like
* 0 2 4 6
* 1 3 5 7
* 8 10 12 14
* 9 11 13 15
* This is about as efficient as V=1,H=2 so it's unclear why they did this but
* I don't get to decide.
*
* Also see section A.2.3 of ITU standard T.81 (Section 1 of the JPEG JFIF standard)
*/
bool interleaveRows = false;

void decode();
void parseSOF(ByteStream data, SOFInfo* i);
void parseSOS(ByteStream data);
Expand Down
45 changes: 31 additions & 14 deletions src/librawspeed/decompressors/LJpegDecompressor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

#include "decompressors/LJpegDecompressor.h"
#include "adt/CroppedArray2DRef.h" // for CroppedArray2DRef
#include "adt/Point.h" // for iPoint2D
#include "common/Common.h" // for to_array, roundUpDivision
#include "common/RawImage.h" // for RawImage, RawImageData
#include "decoders/RawDecoderException.h" // for ThrowException, ThrowRDE
Expand Down Expand Up @@ -59,7 +58,7 @@ LJpegDecompressor::LJpegDecompressor(const ByteStream& bs, const RawImage& img)

void LJpegDecompressor::decode(uint32_t offsetX, uint32_t offsetY,
uint32_t width, uint32_t height,
bool fixDng16Bug_) {
bool fixDng16Bug_, bool interleaveRows_) {
if (offsetX >= static_cast<unsigned>(mRaw->dim.x))
ThrowRDE("X offset outside of image");
if (offsetY >= static_cast<unsigned>(mRaw->dim.y))
Expand All @@ -84,12 +83,12 @@ void LJpegDecompressor::decode(uint32_t offsetX, uint32_t offsetY,
h = height;

fixDng16Bug = fixDng16Bug_;
interleaveRows = interleaveRows_;

AbstractLJpegDecompressor::decode();
}

void LJpegDecompressor::decodeScan()
{
void LJpegDecompressor::decodeScan() {
assert(frame.cps > 0);

if (predictorMode != 1)
Expand All @@ -104,14 +103,16 @@ void LJpegDecompressor::decodeScan()
ThrowRDE("Got less pixels than the components per sample");

// How many output pixels are we expected to produce, as per DNG tiling?
const auto tileRequiredWidth = mRaw->getCpp() * w;
const auto tileRequiredWidth = mRaw->getCpp() * w * (interleaveRows ? 2 : 1);
// How many of these rows do we need?
const auto numRows = h / (interleaveRows ? 2 : 1);

// How many full pixel blocks do we need to consume for that?
if (const auto blocksToConsume =
roundUpDivision(tileRequiredWidth, frame.cps);
frame.w < blocksToConsume || frame.h < h) {
frame.w < blocksToConsume || frame.h < numRows) {
ThrowRDE("LJpeg frame (%u, %u) is smaller than expected (%u, %u)",
frame.cps * frame.w, frame.h, tileRequiredWidth, h);
frame.cps * frame.w, frame.h, tileRequiredWidth, numRows);
}

// How many full pixel blocks will we produce?
Expand Down Expand Up @@ -164,10 +165,14 @@ template <int N_COMP, bool WeirdWidth> void LJpegDecompressor::decodeN() {
assert(N_COMP > 0);
assert(N_COMP >= mRaw->getCpp());
assert((N_COMP / mRaw->getCpp()) > 0);
assert(N_COMP & 1 == 0 | !interleaveRows);

assert(mRaw->dim.x >= N_COMP);
assert((mRaw->getCpp() * (mRaw->dim.x - offX)) >= N_COMP);

auto interleaveHeight = (interleaveRows ? 2 : 1);
auto interleaveWidth = N_COMP / interleaveHeight;

const CroppedArray2DRef img(mRaw->getU16DataAsUncroppedArray2DRef(),
mRaw->getCpp() * offX, offY, mRaw->getCpp() * w,
h);
Expand All @@ -188,22 +193,24 @@ template <int N_COMP, bool WeirdWidth> void LJpegDecompressor::decodeN() {
assert(offY + h <= static_cast<unsigned>(mRaw->dim.y));
assert(offX + w <= static_cast<unsigned>(mRaw->dim.x));

const auto numRows = h / interleaveHeight;

// For y, we can simply stop decoding when we reached the border.
for (unsigned row = 0; row < h; ++row) {
for (unsigned row = 0; row < numRows; ++row) {
unsigned col = 0;

copy_n(predNext, N_COMP, pred.data());
// the predictor for the next line is the start of this line
predNext = &img(row, col);

// FIXME: predictor may have value outside of the uint16_t.
// https://github.com/darktable-org/rawspeed/issues/175

// For x, we first process all full pixel blocks within the image buffer ...
for (; col < N_COMP * fullBlocks; col += N_COMP) {
for (int i = 0; i != N_COMP; ++i) {
pred[i] = uint16_t(pred[i] + ht[i]->decodeDifference(bitStream));
img(row, col + i) = pred[i];
if (interleaveRows) {
img((row * interleaveHeight) + (i / interleaveHeight), (col / interleaveWidth) + (i % interleaveWidth)) = pred[i];
} else {
img(row, col + i) = pred[i];
}
}
}

Expand Down Expand Up @@ -231,7 +238,17 @@ template <int N_COMP, bool WeirdWidth> void LJpegDecompressor::decodeN() {

// ... and discard the rest.
for (; col < N_COMP * frame.w; col += N_COMP) {
for (int i = 0; i != N_COMP; ++i) ht[i]->decodeDifference(bitStream);
for (int i = 0; i != N_COMP; ++i)
ht[i]->decodeDifference(bitStream);
}

// The first sample of the next row is calculated based on the first sample of this row,
// so copy it for the next iteration
if (interleaveRows) {
copy_n(&img(row * interleaveHeight, 0), interleaveWidth, pred.data());
copy_n(&img(row * interleaveHeight + 1, 0), interleaveWidth, pred.data() + interleaveWidth);
} else {
copy_n(&img(row, 0), N_COMP, pred.data());
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/librawspeed/decompressors/LJpegDecompressor.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class LJpegDecompressor final : public AbstractLJpegDecompressor
LJpegDecompressor(const ByteStream& bs, const RawImage& img);

void decode(uint32_t offsetX, uint32_t offsetY, uint32_t width,
uint32_t height, bool fixDng16Bug_);
uint32_t height, bool fixDng16Bug_, bool interleaveRows_ = false);
};

} // namespace rawspeed