From 4061b7209bd4a249ddf52d3702bf7801a5329c65 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Mon, 9 Mar 2026 22:09:03 +0100 Subject: [PATCH 01/29] minor ui fix --- TASKS.md | 15 +++++++++++++-- content/components/PresetPanel.qml | 15 +++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/TASKS.md b/TASKS.md index 133f7af..2c61250 100644 --- a/TASKS.md +++ b/TASKS.md @@ -498,15 +498,26 @@ - [x] Kept filter criteria cycling in active context-menu flow for faster iteration. - [x] Build + tests + offscreen runtime smoke validated after integration. -## Phase 36: Old session Not Found and log rotation +--- + +# Taking back control of the codebase + +## Phase 37: Old session Not Found and log rotation - [x] Pop up error when continue session folder is not found, then reset it and return to WelcomeView - [x] Auto log cleanup +## Phase 38 + +- [ ] JPEG and TIFF support. +- [ ] Panorama Stitching + - [ ] OpenCV integration + - [ ] Stitching + - [ ] TIFF output + ## Backlog / Future - [ ] **Perspective Correction** - [ ] Keystone/perspective transform controls. - [ ] **Lens Correction** - [ ] Integrate `lensfun` for automatic distortion/vignette removal. -- [ ] **Panorama Stitching** diff --git a/content/components/PresetPanel.qml b/content/components/PresetPanel.qml index 3c146aa..a01d145 100644 --- a/content/components/PresetPanel.qml +++ b/content/components/PresetPanel.qml @@ -115,7 +115,7 @@ Rectangle { Layout.fillWidth: true } - T.Button { + PhotonButton { visible: parent.parent.hovered implicitWidth: 32 implicitHeight: 32 @@ -125,16 +125,11 @@ Rectangle { } icon.source: "qrc:/Main/assets/icons/trash.svg" - icon.width: 32 - icon.height: 32 + icon.width: 16 + icon.height: 16 + icon.color: Theme.foreground + variantDestructive: true - background: Rectangle { - color: Theme.destructive - radius: 4 - } - - T.ToolTip.visible: hovered - T.ToolTip.text: "Delete Preset" } } From e7e249b77714264287015334d6988fd57845bf06 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Mon, 9 Mar 2026 22:49:21 +0100 Subject: [PATCH 02/29] adding tiny dng writer and opencv --- src/libraries/tiny_dng_writer.h | 2583 +++++++++++++++++++++++++++++++ 1 file changed, 2583 insertions(+) create mode 100644 src/libraries/tiny_dng_writer.h diff --git a/src/libraries/tiny_dng_writer.h b/src/libraries/tiny_dng_writer.h new file mode 100644 index 0000000..9e061b8 --- /dev/null +++ b/src/libraries/tiny_dng_writer.h @@ -0,0 +1,2583 @@ +// +// TinyDNGWriter, single header only DNG writer in C++11. +// + +/* +The MIT License (MIT) + +Copyright (c) 2016 - 2020 Syoyo Fujita. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#ifndef TINY_DNG_WRITER_H_ +#define TINY_DNG_WRITER_H_ + +#include +#include +#include + +namespace tinydngwriter { + +typedef enum { + TIFFTAG_SUB_FILETYPE = 254, + TIFFTAG_IMAGE_WIDTH = 256, + TIFFTAG_IMAGE_LENGTH = 257, + TIFFTAG_BITS_PER_SAMPLE = 258, + TIFFTAG_COMPRESSION = 259, + TIFFTAG_PHOTOMETRIC = 262, + TIFFTAG_IMAGEDESCRIPTION = 270, + TIFFTAG_STRIP_OFFSET = 273, + TIFFTAG_SAMPLES_PER_PIXEL = 277, + TIFFTAG_ROWS_PER_STRIP = 278, + TIFFTAG_STRIP_BYTE_COUNTS = 279, + TIFFTAG_PLANAR_CONFIG = 284, + TIFFTAG_ORIENTATION = 274, + + TIFFTAG_XRESOLUTION = 282, // rational + TIFFTAG_YRESOLUTION = 283, // rational + TIFFTAG_RESOLUTION_UNIT = 296, + + TIFFTAG_SOFTWARE = 305, + + TIFFTAG_SAMPLEFORMAT = 339, + + // DNG extension + TIFFTAG_CFA_REPEAT_PATTERN_DIM = 33421, + TIFFTAG_CFA_PATTERN = 33422, + + TIFFTAG_DNG_VERSION = 50706, + TIFFTAG_DNG_BACKWARD_VERSION = 50707, + TIFFTAG_UNIQUE_CAMERA_MODEL = 50708, + TIFFTAG_CHRROMA_BLUR_RADIUS = 50703, + TIFFTAG_BLACK_LEVEL_REPEAT_DIM = 50713, + TIFFTAG_BLACK_LEVEL = 50714, + TIFFTAG_WHITE_LEVEL = 50717, + TIFFTAG_COLOR_MATRIX1 = 50721, + TIFFTAG_COLOR_MATRIX2 = 50722, + TIFFTAG_CAMERA_CALIBRATION1 = 50723, + TIFFTAG_CAMERA_CALIBRATION2 = 50724, + TIFFTAG_ANALOG_BALANCE = 50727, + TIFFTAG_AS_SHOT_NEUTRAL = 50728, + TIFFTAG_AS_SHOT_WHITE_XY = 50729, + TIFFTAG_CALIBRATION_ILLUMINANT1 = 50778, + TIFFTAG_CALIBRATION_ILLUMINANT2 = 50779, + TIFFTAG_EXTRA_CAMERA_PROFILES = 50933, + TIFFTAG_PROFILE_NAME = 50936, + TIFFTAG_AS_SHOT_PROFILE_NAME = 50934, + TIFFTAG_DEFAULT_BLACK_RENDER = 51110, + TIFFTAG_ACTIVE_AREA = 50829, + TIFFTAG_FORWARD_MATRIX1 = 50964, + TIFFTAG_FORWARD_MATRIX2 = 50965 +} Tag; + +// SUBFILETYPE(bit field) +static const int FILETYPE_REDUCEDIMAGE = 1; +static const int FILETYPE_PAGE = 2; +static const int FILETYPE_MASK = 4; + +// PLANARCONFIG +static const int PLANARCONFIG_CONTIG = 1; +static const int PLANARCONFIG_SEPARATE = 2; + +// COMPRESSION +// TODO(syoyo) more compressin types. +static const int COMPRESSION_NONE = 1; +static const int COMPRESSION_NEW_JPEG = 7; + +// ORIENTATION +static const int ORIENTATION_TOPLEFT = 1; +static const int ORIENTATION_TOPRIGHT = 2; +static const int ORIENTATION_BOTRIGHT = 3; +static const int ORIENTATION_BOTLEFT = 4; +static const int ORIENTATION_LEFTTOP = 5; +static const int ORIENTATION_RIGHTTOP = 6; +static const int ORIENTATION_RIGHTBOT = 7; +static const int ORIENTATION_LEFTBOT = 8; + +// RESOLUTIONUNIT +static const int RESUNIT_NONE = 1; +static const int RESUNIT_INCH = 2; +static const int RESUNIT_CENTIMETER = 2; + +// PHOTOMETRIC +// TODO(syoyo): more photometric types. +static const int PHOTOMETRIC_WHITE_IS_ZERO = 0; // For bilevel and grayscale +static const int PHOTOMETRIC_BLACK_IS_ZERO = 1; // For bilevel and grayscale +static const int PHOTOMETRIC_RGB = 2; // Default +static const int PHOTOMETRIC_CFA = 32803; // DNG ext +static const int PHOTOMETRIC_LINEARRAW = 34892; // DNG ext + +// Sample format +static const int SAMPLEFORMAT_UINT = 1; // Default +static const int SAMPLEFORMAT_INT = 2; +static const int SAMPLEFORMAT_IEEEFP = 3; // floating point + +struct IFDTag { + unsigned short tag; + unsigned short type; + unsigned int count; + unsigned int offset_or_value; +}; +// 12 bytes. + +class DNGImage { + public: + DNGImage(); + ~DNGImage() {} + + /// + /// Optional: Explicitly specify endian. + /// Must be called before calling other Set methods. + /// + void SetBigEndian(bool big_endian); + + /// + /// Default = 0 + /// + bool SetSubfileType(bool reduced_image = false, bool page = false, + bool mask = false); + + bool SetImageWidth(unsigned int value); + bool SetImageLength(unsigned int value); + bool SetRowsPerStrip(unsigned int value); + bool SetSamplesPerPixel(unsigned short value); + // Set bits for each samples + bool SetBitsPerSample(const unsigned int num_samples, + const unsigned short *values); + bool SetPhotometric(unsigned short value); + bool SetPlanarConfig(unsigned short value); + bool SetOrientation(unsigned short value); + bool SetCompression(unsigned short value); + bool SetSampleFormat(const unsigned int num_samples, + const unsigned short *values); + bool SetXResolution(double value); + bool SetYResolution(double value); + bool SetResolutionUnit(const unsigned short value); + + /// + /// Set arbitrary string for image description. + /// Currently we limit to 1024*1024 chars at max. + /// + bool SetImageDescription(const std::string &ascii); + + /// + /// Set arbitrary string for unique camera model name (not localized!). + /// Currently we limit to 1024*1024 chars at max. + /// + bool SetUniqueCameraModel(const std::string &ascii); + + /// + /// Set software description(string). + /// Currently we limit to 4095 chars at max. + /// + bool SetSoftware(const std::string &ascii); + + bool SetActiveArea(const unsigned int values[4]); + + bool SetChromaBlurRadius(double value); + + /// Specify black level per sample. + bool SetBlackLevel(const unsigned int num_samples, const unsigned short *values); + + /// Specify black level per sample (as rational values). + bool SetBlackLevelRational(unsigned int num_samples, const double *values); + + /// Specify white level per sample. + bool SetWhiteLevelRational(unsigned int num_samples, const double *values); + + /// Specify analog white balance from camera for raw values. + bool SetAnalogBalance(const unsigned int plane_count, const double *matrix_values); + + /// Specify CFA repeating pattern dimensions. + bool SetCFARepeatPatternDim(const unsigned short width, const unsigned short height); + + /// Specify black level repeating pattern dimensions. + bool SetBlackLevelRepeatDim(const unsigned short width, const unsigned short height); + + bool SetCalibrationIlluminant1(const unsigned short value); + bool SetCalibrationIlluminant2(const unsigned short value); + + /// Specify DNG version. + bool SetDNGVersion(const unsigned char a, const unsigned char b, const unsigned char c, const unsigned char d); + + /// Specify transformation matrix (XYZ to reference camera native color space values, under the first calibration illuminant). + bool SetColorMatrix1(const unsigned int plane_count, const double *matrix_values); + + /// Specify transformation matrix (XYZ to reference camera native color space values, under the second calibration illuminant). + bool SetColorMatrix2(const unsigned int plane_count, const double *matrix_values); + + bool SetForwardMatrix1(const unsigned int plane_count, const double *matrix_values); + bool SetForwardMatrix2(const unsigned int plane_count, const double *matrix_values); + + bool SetCameraCalibration1(const unsigned int plane_count, const double *matrix_values); + bool SetCameraCalibration2(const unsigned int plane_count, const double *matrix_values); + + /// Specify CFA geometric pattern (left-to-right, top-to-bottom). + bool SetCFAPattern(const unsigned int num_components, const unsigned char *values); + + /// Specify the selected white balance at time of capture, encoded as the coordinates of a perfectly neutral color in linear reference space values. + bool SetAsShotNeutral(const unsigned int plane_count, const double *matrix_values); + + /// Specify the the selected white balance at time of capture, encoded as x-y chromaticity coordinates. + bool SetAsShotWhiteXY(const double x, const double y); + + /// Set image data with packing (take 16-bit values and pack them to input_bpp values). + bool SetImageDataPacked(const unsigned short *input_buffer, const int input_count, const unsigned int input_bpp, bool big_endian); + + /// Set image data. + bool SetImageData(const unsigned char *data, const size_t data_len); + + /// Set image data. + bool SetImageDataJpeg(const unsigned short *data, unsigned int width, unsigned int height, unsigned int bpp); + + /// Set custom field. + bool SetCustomFieldLong(const unsigned short tag, const int value); + bool SetCustomFieldULong(const unsigned short tag, const unsigned int value); + + size_t GetDataSize() const { return data_os_.str().length(); } + + size_t GetStripOffset() const { return data_strip_offset_; } + size_t GetStripBytes() const { return data_strip_bytes_; } + + /// Write aux IFD data and strip image data to stream. + bool WriteDataToStream(std::ostream *ofs) const; + + /// + /// Write IFD to stream. + /// + /// @param[in] data_base_offset : Byte offset to data + /// @param[in] strip_offset : Byte offset to image strip data + /// + /// TODO(syoyo): Support multiple strips + /// + bool WriteIFDToStream(const unsigned int data_base_offset, + const unsigned int strip_offset, std::ostream *ofs) const; + + std::string Error() const { return err_; } + + private: + std::ostringstream data_os_; + bool swap_endian_; + bool dng_big_endian_; + unsigned short num_fields_; + unsigned int samples_per_pixels_; + std::vector bits_per_samples_; + + // TODO(syoyo): Support multiple strips + size_t data_strip_offset_{0}; + size_t data_strip_bytes_{0}; + + mutable std::string err_; // Error message + + std::vector ifd_tags_; +}; + +class DNGWriter { + public: + // TODO(syoyo): Use same endian setting with DNGImage. + DNGWriter(bool big_endian); + ~DNGWriter() {} + + /// + /// Add DNGImage. + /// It just retains the pointer of the image, thus + /// application must not free resources until `WriteToFile` has been called. + /// + bool AddImage(const DNGImage *image) { + images_.push_back(image); + + return true; + } + + /// Write DNG to a file. + /// Return error string to `err` when Write() returns false. + /// Returns true upon success. + bool WriteToFile(const char *filename, std::string *err) const; + + private: + bool swap_endian_; + bool dng_big_endian_; // Endianness of DNG file. + + std::vector images_; +}; + +} // namespace tinydngwriter + +#endif // TINY_DNG_WRITER_H_ + +#ifdef TINY_DNG_WRITER_IMPLEMENTATION + +// +// TIFF format resources. +// +// http://c0de517e.blogspot.jp/2013/07/tiny-hdr-writer.html +// http://paulbourke.net/dataformats/tiff/ and +// http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Undef if you want to use builtin function for clz +#if 0 +#ifdef _MSC_VER +#include +#endif +#endif + + +namespace tinydngwriter { + +namespace detail { + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Weverything" +#endif + +// Begin liblj92, Lossless JPEG decode/encoder ------------------------------ +// +// With fixes: https://github.com/ilia3101/MLV-App/pull/151 + +/* +lj92.c +(c) Andrew Baldwin 2014 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +enum LJ92_ERRORS { + LJ92_ERROR_NONE = 0, + LJ92_ERROR_CORRUPT = -1, + LJ92_ERROR_NO_MEMORY = -2, + LJ92_ERROR_BAD_HANDLE = -3, + LJ92_ERROR_TOO_WIDE = -4 +}; + +/* + * Encode a grayscale image supplied as 16bit values within the given bitdepth + * Read from tile in the image + * Apply delinearization if given + * Return the encoded lossless JPEG stream + */ +int lj92_encode(uint16_t *image, int width, int height, int bitdepth, + int readLength, int skipLength, uint16_t *delinearize, + int delinearizeLength, uint8_t **encoded, int *encodedLength); + +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; + +//#define LJ92_DEBUG + +/* Encoder implementation */ + +#if 0 +uint32_t __inline clz32(uint32_t value) { + unsigned long leading_zero = 0; + + if (_BitScanReverse(&leading_zero, value)) { + return 31 - leading_zero; + } else { + // Same remarks as above + return 32; + } +} +#endif + +// Very simple count leading zero implementation. +static int clz32(unsigned int x) { + int n; + if (x == 0) return 32; + for (n = 0; ((x & 0x80000000) == 0); n++, x <<= 1) + ; + return n; +} + +typedef struct _lje { + uint16_t *image; + int width; + int height; + int bitdepth; + int readLength; + int skipLength; + uint16_t *delinearize; + int delinearizeLength; + uint8_t *encoded; + int encodedWritten; + int encodedLength; + int hist[17]; // SSSS frequency histogram + int bits[17]; + int huffval[17]; + u16 huffenc[17]; + u16 huffbits[17]; + int huffsym[17]; +} lje; + +int frequencyScan(lje *self) { + // Scan through the tile using the standard type 6 prediction + // Need to cache the previous 2 row in target coordinates because of tiling + uint16_t *pixel = self->image; + int pixcount = self->width * self->height; + int scan = self->readLength; + uint16_t *rowcache = (uint16_t *)calloc(1, self->width * 4); + uint16_t *rows[2]; + rows[0] = rowcache; + rows[1] = &rowcache[self->width]; + + int col = 0; + int row = 0; + int Px = 0; + int32_t diff = 0; + int maxval = (1 << self->bitdepth); + while (pixcount--) { + uint16_t p = *pixel; + if (self->delinearize) { + if (p >= self->delinearizeLength) { + free(rowcache); + return LJ92_ERROR_TOO_WIDE; + } + p = self->delinearize[p]; + } + if (p >= maxval) { + free(rowcache); + return LJ92_ERROR_TOO_WIDE; + } + rows[1][col] = p; + + if ((row == 0) && (col == 0)) + Px = 1 << (self->bitdepth - 1); + else if (row == 0) + Px = rows[1][col - 1]; + else if (col == 0) + Px = rows[0][col]; + else + Px = rows[0][col] + ((rows[1][col - 1] - rows[0][col - 1]) >> 1); + diff = rows[1][col] - Px; + int ssss = 32 - clz32(abs(diff)); + if (diff == 0) ssss = 0; + self->hist[ssss]++; + // printf("%d %d %d %d %d %d\n",col,row,p,Px,diff,ssss); + pixel++; + scan--; + col++; + if (scan == 0) { + pixel += self->skipLength; + scan = self->readLength; + } + if (col == self->width) { + uint16_t *tmprow = rows[1]; + rows[1] = rows[0]; + rows[0] = tmprow; + col = 0; + row++; + } + } +#ifdef DEBUG + int sort[17]; + for (int h = 0; h < 17; h++) { + sort[h] = h; + printf("%d:%d\n", h, self->hist[h]); + } +#endif + free(rowcache); + return LJ92_ERROR_NONE; +} + +void createEncodeTable(lje *self) { + float freq[18]; + int codesize[18]; + int others[18]; + + // Calculate frequencies + float totalpixels = self->width * self->height; + for (int i = 0; i < 17; i++) { + freq[i] = (float)(self->hist[i]) / totalpixels; +#ifdef DEBUG + printf("%d:%f\n", i, freq[i]); +#endif + codesize[i] = 0; + others[i] = -1; + } + codesize[17] = 0; + others[17] = -1; + freq[17] = 1.0f; + + float v1f, v2f; + int v1, v2; + + while (1) { + v1f = 3.0f; + v1 = -1; + for (int i = 0; i < 18; i++) { + if ((freq[i] <= v1f) && (freq[i] > 0.0f)) { + v1f = freq[i]; + v1 = i; + } + } +#ifdef DEBUG + printf("v1:%d,%f\n", v1, v1f); +#endif + v2f = 3.0f; + v2 = -1; + for (int i = 0; i < 18; i++) { + if (i == v1) continue; + if ((freq[i] < v2f) && (freq[i] > 0.0f)) { + v2f = freq[i]; + v2 = i; + } + } + if (v2 == -1) break; // Done + + freq[v1] += freq[v2]; + freq[v2] = 0.0f; + + while (1) { + codesize[v1]++; + if (others[v1] == -1) break; + v1 = others[v1]; + } + others[v1] = v2; + while (1) { + codesize[v2]++; + if (others[v2] == -1) break; + v2 = others[v2]; + } + } + int *bits = self->bits; + memset(bits, 0, sizeof(self->bits)); + for (int i = 0; i < 18; i++) { + if (codesize[i] != 0) { + bits[codesize[i]]++; + } + } +#ifdef DEBUG + for (int i = 0; i < 17; i++) { + printf("bits:%d,%d,%d\n", i, bits[i], codesize[i]); + } +#endif + int *huffval = self->huffval; + int i = 1; + int k = 0; + int j; + memset(huffval, 0, sizeof(self->huffval)); + while (i <= 32) { + j = 0; + while (j < 17) { + if (codesize[j] == i) { + huffval[k++] = j; + } + j++; + } + i++; + } +#ifdef DEBUG + for (i = 0; i < 17; i++) { + printf("i=%d,huffval[i]=%x\n", i, huffval[i]); + } +#endif + int maxbits = 16; + while (maxbits > 0) { + if (bits[maxbits]) break; + maxbits--; + } + u16 *huffenc = self->huffenc; + u16 *huffbits = self->huffbits; + int *huffsym = self->huffsym; + memset(huffenc, 0, sizeof(self->huffenc)); + memset(huffbits, 0, sizeof(self->huffbits)); + memset(self->huffsym, 0, sizeof(self->huffsym)); + i = 0; + int hv = 0; + int rv = 0; + int vl = 0; // i + // int hcode; + int bitsused = 1; + int sym = 0; + // printf("%04x:%x:%d:%x\n",i,huffvals[hv],bitsused,1<<(maxbits-bitsused)); + while (i < 1 << maxbits) { + if (bitsused > maxbits) { + break; // Done. Should never get here! + } + if (vl >= bits[bitsused]) { + bitsused++; + vl = 0; + continue; + } + if (rv == 1 << (maxbits - bitsused)) { + rv = 0; + vl++; + hv++; + // printf("%04x:%x:%d:%x\n",i,huffvals[hv],bitsused,1<<(maxbits-bitsused)); + continue; + } + huffbits[sym] = bitsused; + huffenc[sym++] = i >> (maxbits - bitsused); + // printf("%d %d %d\n",i,bitsused,hcode); + i += (1 << (maxbits - bitsused)); + rv = 1 << (maxbits - bitsused); + } + for (i = 0; i < 17; i++) { + if (huffbits[i] > 0) { + huffsym[huffval[i]] = i; + } +#ifdef DEBUG + printf("huffval[%d]=%d,huffenc[%d]=%x,bits=%d\n", i, huffval[i], i, + huffenc[i], huffbits[i]); +#endif + if (huffbits[i] > 0) { + huffsym[huffval[i]] = i; + } + } +#ifdef DEBUG + for (i = 0; i < 17; i++) { + printf("huffsym[%d]=%d\n", i, huffsym[i]); + } +#endif +} + +void writeHeader(lje *self) { + int w = self->encodedWritten; + uint8_t *e = self->encoded; + e[w++] = 0xff; + e[w++] = 0xd8; // SOI + e[w++] = 0xff; + e[w++] = 0xc3; // SOF3 + // Write SOF + e[w++] = 0x0; + e[w++] = 11; // Lf, frame header length + e[w++] = self->bitdepth; + e[w++] = self->height >> 8; + e[w++] = self->height & 0xFF; + e[w++] = self->width >> 8; + e[w++] = self->width & 0xFF; + e[w++] = 1; // Components + e[w++] = 0; // Component ID + e[w++] = 0x11; // Component X/Y + e[w++] = 0; // Unused (Quantisation) + e[w++] = 0xff; + e[w++] = 0xc4; // HUFF + // Write HUFF + int count = 0; + for (int i = 0; i < 17; i++) { + count += self->bits[i]; + } + e[w++] = 0x0; + e[w++] = 17 + 2 + count; // Lf, frame header length + e[w++] = 0; // Table ID + for (int i = 1; i < 17; i++) { + e[w++] = self->bits[i]; + } + for (int i = 0; i < count; i++) { + e[w++] = self->huffval[i]; + } + e[w++] = 0xff; + e[w++] = 0xda; // SCAN + // Write SCAN + e[w++] = 0x0; + e[w++] = 8; // Ls, scan header length + e[w++] = 1; // Components + e[w++] = 0; // + e[w++] = 0; // + e[w++] = 6; // Predictor + e[w++] = 0; // + e[w++] = 0; // + self->encodedWritten = w; +} + +void writePost(lje *self) { + int w = self->encodedWritten; + uint8_t *e = self->encoded; + e[w++] = 0xff; + e[w++] = 0xd9; // EOI + self->encodedWritten = w; +} + +void writeBody(lje *self) { + // Scan through the tile using the standard type 6 prediction + // Need to cache the previous 2 row in target coordinates because of tiling + uint16_t *pixel = self->image; + int pixcount = self->width * self->height; + int scan = self->readLength; + uint16_t *rowcache = (uint16_t *)calloc(1, self->width * 4); + uint16_t *rows[2]; + rows[0] = rowcache; + rows[1] = &rowcache[self->width]; + + int col = 0; + int row = 0; + int Px = 0; + int32_t diff = 0; + int bitcount = 0; + uint8_t *out = self->encoded; + int w = self->encodedWritten; + uint8_t next = 0; + uint8_t nextbits = 8; + while (pixcount--) { + uint16_t p = *pixel; + if (self->delinearize) p = self->delinearize[p]; + rows[1][col] = p; + + if ((row == 0) && (col == 0)) + Px = 1 << (self->bitdepth - 1); + else if (row == 0) + Px = rows[1][col - 1]; + else if (col == 0) + Px = rows[0][col]; + else + Px = rows[0][col] + ((rows[1][col - 1] - rows[0][col - 1]) >> 1); + diff = rows[1][col] - Px; + int ssss = 32 - clz32(abs(diff)); + if (diff == 0) ssss = 0; + // printf("%d %d %d %d %d\n",col,row,Px,diff,ssss); + + // Write the huffman code for the ssss value + int huffcode = self->huffsym[ssss]; + int huffenc = self->huffenc[huffcode]; + int huffbits = self->huffbits[huffcode]; + bitcount += huffbits + ssss; + + int vt = ssss > 0 ? (1 << (ssss - 1)) : 0; + // printf("%d %d %d %d\n",rows[1][col],Px,diff,Px+diff); +#ifdef DEBUG +#endif + if (diff < vt) diff += (1 << (ssss)) - 1; + + // Write the ssss + while (huffbits > 0) { + int usebits = huffbits > nextbits ? nextbits : huffbits; + // Add top usebits from huffval to next usebits of nextbits + int tophuff = huffenc >> (huffbits - usebits); + next |= (tophuff << (nextbits - usebits)); + nextbits -= usebits; + huffbits -= usebits; + huffenc &= (1 << huffbits) - 1; + if (nextbits == 0) { + out[w++] = next; + if (next == 0xff) out[w++] = 0x0; + next = 0; + nextbits = 8; + } + } + // Write the rest of the bits for the value + + while (ssss > 0) { + int usebits = ssss > nextbits ? nextbits : ssss; + // Add top usebits from huffval to next usebits of nextbits + int tophuff = diff >> (ssss - usebits); + next |= (tophuff << (nextbits - usebits)); + nextbits -= usebits; + ssss -= usebits; + diff &= (1 << ssss) - 1; + if (nextbits == 0) { + out[w++] = next; + if (next == 0xff) out[w++] = 0x0; + next = 0; + nextbits = 8; + } + } + + // printf("%d %d\n",diff,ssss); + pixel++; + scan--; + col++; + if (scan == 0) { + pixel += self->skipLength; + scan = self->readLength; + } + if (col == self->width) { + uint16_t *tmprow = rows[1]; + rows[1] = rows[0]; + rows[0] = tmprow; + col = 0; + row++; + } + } + // Flush the final bits + if (nextbits < 8) { + out[w++] = next; + if (next == 0xff) out[w++] = 0x0; + } +#ifdef DEBUG + int sort[17]; + for (int h = 0; h < 17; h++) { + sort[h] = h; + printf("%d:%d\n", h, self->hist[h]); + } + printf("Total bytes: %d\n", bitcount >> 3); +#endif + free(rowcache); + self->encodedWritten = w; +} + +/* Encoder + * Read tile from an image and encode in one shot + * Return the encoded data + */ +int lj92_encode(uint16_t *image, int width, int height, int bitdepth, + int readLength, int skipLength, uint16_t *delinearize, + int delinearizeLength, uint8_t **encoded, int *encodedLength) { + int ret = LJ92_ERROR_NONE; + + lje *self = (lje *)calloc(sizeof(lje), 1); + if (self == NULL) return LJ92_ERROR_NO_MEMORY; + self->image = image; + self->width = width; + self->height = height; + self->bitdepth = bitdepth; + self->readLength = readLength; + self->skipLength = skipLength; + self->delinearize = delinearize; + self->delinearizeLength = delinearizeLength; + self->encodedLength = width * height * 3 + 200; + self->encoded = (uint8_t*)malloc(self->encodedLength); + if (self->encoded == NULL) { + free(self); + return LJ92_ERROR_NO_MEMORY; + } + // Scan through data to gather frequencies of ssss prefixes + ret = frequencyScan(self); + if (ret != LJ92_ERROR_NONE) { + free(self->encoded); + free(self); + return ret; + } + // Create encoded table based on frequencies + createEncodeTable(self); + // Write JPEG head and scan header + writeHeader(self); + // Scan through and do the compression + writeBody(self); + // Finish + writePost(self); +#ifdef DEBUG + printf("written:%d\n", self->encodedWritten); +#endif + self->encoded = (uint8_t*)realloc(self->encoded, self->encodedWritten); + self->encodedLength = self->encodedWritten; + *encoded = self->encoded; + *encodedLength = self->encodedLength; + + free(self); + + return ret; +} + +// End liblj92 --------------------------------------------------------- + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +} // namespace detail + +#ifdef __clang__ +#pragma clang diagnostic push +#if __has_warning("-Wzero-as-null-pointer-constant") +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#endif +#endif + +// +// TinyDNGWriter stores IFD table in the end of file so that offset to +// image data can be easily computed. +// +// +----------------------+ +// | header | +// +----------------------+ +// | | +// | image & meta 0 | +// | | +// +----------------------+ +// | | +// | image & meta 1 | +// | | +// +----------------------+ +// ... +// +----------------------+ +// | | +// | image & meta N | +// | | +// +----------------------+ +// | | +// | IFD 0 | +// | | +// +----------------------+ +// | | +// | IFD 1 | +// | | +// +----------------------+ +// ... +// +----------------------+ +// | | +// | IFD 2 | +// | | +// +----------------------+ +// + +// From tiff.h +typedef enum { + TIFF_NOTYPE = 0, /* placeholder */ + TIFF_BYTE = 1, /* 8-bit unsigned integer */ + TIFF_ASCII = 2, /* 8-bit bytes w/ last byte null */ + TIFF_SHORT = 3, /* 16-bit unsigned integer */ + TIFF_LONG = 4, /* 32-bit unsigned integer */ + TIFF_RATIONAL = 5, /* 64-bit unsigned fraction */ + TIFF_SBYTE = 6, /* !8-bit signed integer */ + TIFF_UNDEFINED = 7, /* !8-bit untyped data */ + TIFF_SSHORT = 8, /* !16-bit signed integer */ + TIFF_SLONG = 9, /* !32-bit signed integer */ + TIFF_SRATIONAL = 10, /* !64-bit signed fraction */ + TIFF_FLOAT = 11, /* !32-bit IEEE floating point */ + TIFF_DOUBLE = 12, /* !64-bit IEEE floating point */ + TIFF_IFD = 13, /* %32-bit unsigned integer (offset) */ + TIFF_LONG8 = 16, /* BigTIFF 64-bit unsigned integer */ + TIFF_SLONG8 = 17, /* BigTIFF 64-bit signed integer */ + TIFF_IFD8 = 18 /* BigTIFF 64-bit unsigned integer (offset) */ +} DataType; + +const static int kHeaderSize = 8; // TIFF header size. + +// floating point to integer rational value conversion +// https://stackoverflow.com/questions/51142275/exact-value-of-a-floating-point-number-as-a-rational +// +// Return error flag +static int DoubleToRational(double x, double *numerator, double *denominator) { + if (!std::isfinite(x)) { + *numerator = *denominator = 0.0; + if (x > 0.0) *numerator = 1.0; + if (x < 0.0) *numerator = -1.0; + return 1; + } + + // TIFF Rational use two uint32's, so reduce the bits + int bdigits = FLT_MANT_DIG; + int expo; + *denominator = 1.0; + *numerator = std::frexp(x, &expo) * std::pow(2.0, bdigits); + expo -= bdigits; + if (expo > 0) { + *numerator *= std::pow(2.0, expo); + } else if (expo < 0) { + expo = -expo; + if (expo >= FLT_MAX_EXP - 1) { + *numerator /= std::pow(2.0, expo - (FLT_MAX_EXP - 1)); + *denominator *= std::pow(2.0, FLT_MAX_EXP - 1); + return fabs(*numerator) < 1.0; + } else { + *denominator *= std::pow(2.0, expo); + } + } + + while (((std::fabs(*numerator) > 0.0) && + (std::fabs(std::fmod(*numerator, 2)) < + std::numeric_limits::epsilon()) && + (std::fabs(std::fmod(*denominator, 2)) < + std::numeric_limits::epsilon())) || + (std::max(std::fabs(*numerator), std::fabs(*denominator)) > + std::numeric_limits::max())) { + *numerator /= 2.0; + *denominator /= 2.0; + } + return 0; +} + +static inline bool IsBigEndian() { + unsigned int i = 0x01020304; + char c[4]; + memcpy(c, &i, 4); + return (c[0] == 1); +} + +static void swap2(unsigned short *val) { + unsigned short tmp = *val; + unsigned char *dst = reinterpret_cast(val); + unsigned char *src = reinterpret_cast(&tmp); + + dst[0] = src[1]; + dst[1] = src[0]; +} + +static void swap4(unsigned int *val) { + unsigned int tmp = *val; + unsigned char *dst = reinterpret_cast(val); + unsigned char *src = reinterpret_cast(&tmp); + + dst[0] = src[3]; + dst[1] = src[2]; + dst[2] = src[1]; + dst[3] = src[0]; +} + +static void swap8(uint64_t *val) { + uint64_t tmp = *val; + unsigned char *dst = reinterpret_cast(val); + unsigned char *src = reinterpret_cast(&tmp); + + dst[0] = src[7]; + dst[1] = src[6]; + dst[2] = src[5]; + dst[3] = src[4]; + dst[4] = src[3]; + dst[5] = src[2]; + dst[6] = src[1]; + dst[7] = src[0]; +} + +static void Write1(const unsigned char c, std::ostringstream *out) { + unsigned char value = c; + out->write(reinterpret_cast(&value), 1); +} + +static void Write2(const unsigned short c, std::ostringstream *out, + const bool swap_endian) { + unsigned short value = c; + if (swap_endian) { + swap2(&value); + } + + out->write(reinterpret_cast(&value), 2); +} + +static void Write4(const unsigned int c, std::ostringstream *out, + const bool swap_endian) { + unsigned int value = c; + if (swap_endian) { + swap4(&value); + } + + out->write(reinterpret_cast(&value), 4); +} + +static bool WriteTIFFTag(const unsigned short tag, const unsigned short type, + const unsigned int count, const unsigned char *data, + std::vector *tags_out, + std::ostringstream *data_out) { + assert(sizeof(IFDTag) == + 12); // FIXME(syoyo): Use static_assert for C++11 compiler + + IFDTag ifd; + ifd.tag = tag; + ifd.type = type; + ifd.count = count; + + size_t typesize_table[] = {1, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4}; + + size_t len = count * (typesize_table[(type) < 14 ? (type) : 0]); + if (len > 4) { + assert(data_out); + if (!data_out) { + return false; + } + + // Store offset value. + + unsigned int offset = + static_cast(data_out->tellp()) + kHeaderSize; + ifd.offset_or_value = offset; + + data_out->write(reinterpret_cast(data), + static_cast(len)); + + } else { + ifd.offset_or_value = 0; + + // less than 4 bytes = store data itself. + if (len == 1) { + unsigned char value = *(data); + memcpy(&(ifd.offset_or_value), &value, sizeof(unsigned char)); + } else if (len == 2) { + unsigned short value = *(reinterpret_cast(data)); + memcpy(&(ifd.offset_or_value), &value, sizeof(unsigned short)); + } else if (len == 4) { + unsigned int value = *(reinterpret_cast(data)); + ifd.offset_or_value = value; + } else { + assert(0); + } + } + + tags_out->push_back(ifd); + + return true; +} + +static bool WriteTIFFVersionHeader(std::ostringstream *out, bool big_endian) { + // TODO(syoyo): Support BigTIFF? + + // 4d 4d = Big endian. 49 49 = Little endian. + if (big_endian) { + Write1(0x4d, out); + Write1(0x4d, out); + Write1(0x0, out); + Write1(0x2a, out); // Tiff version ID + } else { + Write1(0x49, out); + Write1(0x49, out); + Write1(0x2a, out); // Tiff version ID + Write1(0x0, out); + } + + return true; +} + +DNGImage::DNGImage() + : dng_big_endian_(true), + num_fields_(0), + samples_per_pixels_(0), + data_strip_offset_{0}, + data_strip_bytes_{0} { + swap_endian_ = (IsBigEndian() != dng_big_endian_); +} + +void DNGImage::SetBigEndian(bool big_endian) { + dng_big_endian_ = big_endian; + swap_endian_ = (IsBigEndian() != dng_big_endian_); +} + +bool DNGImage::SetSubfileType(bool reduced_image, bool page, bool mask) { + unsigned int count = 1; + + unsigned int bits = 0; + if (reduced_image) { + bits |= FILETYPE_REDUCEDIMAGE; + } + if (page) { + bits |= FILETYPE_PAGE; + } + if (mask) { + bits |= FILETYPE_MASK; + } + + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_SUB_FILETYPE), TIFF_LONG, count, + reinterpret_cast(&bits), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetImageWidth(const unsigned int width) { + unsigned int count = 1; + + unsigned int data = width; + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_IMAGE_WIDTH), TIFF_LONG, count, + reinterpret_cast(&data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetImageLength(const unsigned int length) { + unsigned int count = 1; + + const unsigned int data = length; + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_IMAGE_LENGTH), TIFF_LONG, count, + reinterpret_cast(&data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetRowsPerStrip(const unsigned int rows) { + if (rows == 0) { + return false; + } + + unsigned int count = 1; + + const unsigned int data = rows; + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_ROWS_PER_STRIP), TIFF_LONG, count, + reinterpret_cast(&data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetSamplesPerPixel(const unsigned short value) { + if (value > 4) { + { + std::stringstream ss; + ss << "Samples per pixel must be less than or equal to 4, but got " << value << ".\n"; + err_ += ss.str(); + } + return false; + } + + unsigned int count = 1; + + const unsigned short data = value; + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_SAMPLES_PER_PIXEL), TIFF_SHORT, count, + reinterpret_cast(&data), &ifd_tags_, &data_os_); + + if (!ret) { + err_ += "Failed to write `TIFFTAG_SAMPLES_PER_PIXEL` tag.\n"; + return false; + } + + samples_per_pixels_ = value; // Store SPP for later use. + + num_fields_++; + return true; +} + +bool DNGImage::SetBitsPerSample(const unsigned int num_samples, + const unsigned short *values) { + // `SetSamplesPerPixel()` must be called in advance and SPP shoud be equal to + // `num_samples`. + + if (samples_per_pixels_ == 0) { + err_ += "SetSamplesPerPixel() must be called before SetBitsPerSample().\n"; + return false; + } + + if ((num_samples == 0) || (num_samples > 4)) { + std::stringstream ss; + ss << "Invalid number of samples: " << num_samples << "\n"; + err_ += ss.str(); + return false; + } else if (num_samples != samples_per_pixels_) { + std::stringstream ss; + ss << "Samples per pixel mismatch. " << num_samples << " is given for SetBitsPerSample(), but SamplesPerPixel is set to " << samples_per_pixels_ << "\n"; + err_ += ss.str(); + return false; + } else { + // ok + } + + unsigned short bps = values[0]; + + std::vector vs(num_samples); + for (size_t i = 0; i < vs.size(); i++) { + // FIXME(syoyo): Currently bps must be same for all samples + if (bps != values[i]) { + err_ += "BitsPerSample must be same among samples at the moment.\n"; + return false; + } + + vs[i] = values[i]; + + // TODO(syoyo): Swap values when writing IFD tag, not here. + if (swap_endian_) { + swap2(&vs[i]); + } + } + + unsigned int count = num_samples; + + bool ret = WriteTIFFTag(static_cast(TIFFTAG_BITS_PER_SAMPLE), + TIFF_SHORT, count, + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + // Store BPS for later use. + bits_per_samples_.resize(num_samples); + for (size_t i = 0; i < num_samples; i++) { + bits_per_samples_[i] = values[i]; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetPhotometric(const unsigned short value) { + if ((value == PHOTOMETRIC_LINEARRAW) || + (value == PHOTOMETRIC_CFA) || + (value == PHOTOMETRIC_RGB) || + (value == PHOTOMETRIC_WHITE_IS_ZERO) || + (value == PHOTOMETRIC_BLACK_IS_ZERO)) { + // OK + } else { + return false; + } + + unsigned int count = 1; + + const unsigned short data = value; + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_PHOTOMETRIC), TIFF_SHORT, count, + reinterpret_cast(&data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetPlanarConfig(const unsigned short value) { + unsigned int count = 1; + + if ((value == PLANARCONFIG_CONTIG) || (value == PLANARCONFIG_SEPARATE)) { + // OK + } else { + return false; + } + + const unsigned short data = value; + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_PLANAR_CONFIG), TIFF_SHORT, count, + reinterpret_cast(&data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetCompression(const unsigned short value) { + unsigned int count = 1; + + const unsigned short data = value; + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_COMPRESSION), TIFF_SHORT, count, + reinterpret_cast(&data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetSampleFormat(const unsigned int num_samples, + const unsigned short *values) { + // `SetSamplesPerPixel()` must be called in advance + if ((num_samples > 0) && (num_samples == samples_per_pixels_)) { + // OK + } else { + err_ += "SetSamplesPerPixel() must be called before SetSampleFormat().\n"; + return false; + } + + unsigned short format = values[0]; + + std::vector vs(num_samples); + for (size_t i = 0; i < vs.size(); i++) { + // FIXME(syoyo): Currently format must be same for all samples + if (format != values[i]) { + err_ += "SampleFormat must be same among samples at the moment.\n"; + return false; + } + + if ((format == SAMPLEFORMAT_UINT) || (format == SAMPLEFORMAT_INT) || + (format == SAMPLEFORMAT_IEEEFP)) { + // OK + } else { + err_ += "Invalid format value specified for SetSampleFormat().\n"; + return false; + } + + vs[i] = values[i]; + + // TODO(syoyo): Swap values when writing IFD tag, not here. + if (swap_endian_) { + swap2(&vs[i]); + } + } + + unsigned int count = num_samples; + + bool ret = WriteTIFFTag(static_cast(TIFFTAG_SAMPLEFORMAT), + TIFF_SHORT, count, + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetOrientation(const unsigned short value) { + unsigned int count = 1; + + if ((value == ORIENTATION_TOPLEFT) || (value == ORIENTATION_TOPRIGHT) || + (value == ORIENTATION_BOTRIGHT) || (value == ORIENTATION_BOTLEFT) || + (value == ORIENTATION_LEFTTOP) || (value == ORIENTATION_RIGHTTOP) || + (value == ORIENTATION_RIGHTBOT) || (value == ORIENTATION_LEFTBOT)) { + // OK + } else { + return false; + } + + const unsigned int data = value; + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_ORIENTATION), TIFF_SHORT, count, + reinterpret_cast(&data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetBlackLevel(const unsigned int num_components, + const unsigned short *values) { + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_BLACK_LEVEL), TIFF_SHORT, num_components, + reinterpret_cast(values), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetBlackLevelRational(unsigned int num_samples, + const double *values) { + // `SetSamplesPerPixel()` must be called in advance and SPP shoud be equal to + // `num_samples`. + if ((num_samples > 0) && (num_samples == samples_per_pixels_)) { + // OK + } else { + return false; + } + + std::vector vs(num_samples * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + + unsigned int count = num_samples; + + bool ret = WriteTIFFTag(static_cast(TIFFTAG_BLACK_LEVEL), + TIFF_RATIONAL, count, + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetWhiteLevelRational(unsigned int num_samples, + const double *values) { + // `SetSamplesPerPixel()` must be called in advance and SPP shoud be equal to + // `num_samples`. + if ((num_samples > 0) && (num_samples == samples_per_pixels_)) { + // OK + } else { + return false; + } + + std::vector vs(num_samples * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + + unsigned int count = num_samples; + + bool ret = WriteTIFFTag(static_cast(TIFFTAG_WHITE_LEVEL), + TIFF_RATIONAL, count, + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetXResolution(const double value) { + double numerator, denominator; + if (DoubleToRational(value, &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + unsigned int data[2]; + data[0] = static_cast(numerator); + data[1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&data[0]); + swap4(&data[1]); + } + + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_XRESOLUTION), TIFF_RATIONAL, 1, + reinterpret_cast(data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetYResolution(const double value) { + double numerator, denominator; + if (DoubleToRational(value, &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + unsigned int data[2]; + data[0] = static_cast(numerator); + data[1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&data[0]); + swap4(&data[1]); + } + + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_YRESOLUTION), TIFF_RATIONAL, 1, + reinterpret_cast(data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetResolutionUnit(const unsigned short value) { + unsigned int count = 1; + + if ((value == RESUNIT_NONE) || (value == RESUNIT_INCH) || + (value == RESUNIT_CENTIMETER)) { + // OK + } else { + return false; + } + + const unsigned short data = value; + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_RESOLUTION_UNIT), TIFF_SHORT, count, + reinterpret_cast(&data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetImageDescription(const std::string &ascii) { + unsigned int count = + static_cast(ascii.length() + 1); // +1 for '\0' + + if (count < 2) { + // empty string + return false; + } + + if (count > (1024 * 1024)) { + // too large + return false; + } + + bool ret = WriteTIFFTag(static_cast(TIFFTAG_IMAGEDESCRIPTION), + TIFF_ASCII, count, + reinterpret_cast(ascii.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetUniqueCameraModel(const std::string &ascii) { + unsigned int count = + static_cast(ascii.length() + 1); // +1 for '\0' + + if (count < 2) { + // empty string + return false; + } + + if (count > (1024 * 1024)) { + // too large + return false; + } + + bool ret = WriteTIFFTag(static_cast(TIFFTAG_UNIQUE_CAMERA_MODEL), + TIFF_ASCII, count, + reinterpret_cast(ascii.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetSoftware(const std::string &ascii) { + unsigned int count = + static_cast(ascii.length() + 1); // +1 for '\0' + + if (count < 2) { + // empty string + return false; + } + + if (count > 4096) { + // too large + return false; + } + + bool ret = WriteTIFFTag(static_cast(TIFFTAG_SOFTWARE), + TIFF_ASCII, count, + reinterpret_cast(ascii.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + + +bool DNGImage::SetActiveArea(const unsigned int values[4]) { + unsigned int count = 4; + + const unsigned int *data = values; + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_ACTIVE_AREA), TIFF_LONG, count, + reinterpret_cast(data), &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetDNGVersion(const unsigned char a, + const unsigned char b, + const unsigned char c, + const unsigned char d) { + unsigned char data[4] = {a, b, c, d}; + + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_DNG_VERSION), TIFF_BYTE, 4, + reinterpret_cast(data), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetColorMatrix1(const unsigned int plane_count, + const double *matrix_values) { + std::vector vs(plane_count * 3 * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + bool ret = WriteTIFFTag(static_cast(TIFFTAG_COLOR_MATRIX1), + TIFF_SRATIONAL, uint32_t(vs.size() / 2), + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetColorMatrix2(const unsigned int plane_count, + const double *matrix_values) { + std::vector vs(plane_count * 3 * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + bool ret = WriteTIFFTag(static_cast(TIFFTAG_COLOR_MATRIX2), + TIFF_SRATIONAL, uint32_t(vs.size() / 2), + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetForwardMatrix1(const unsigned int plane_count, + const double *matrix_values) { + std::vector vs(plane_count * 3 * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + bool ret = WriteTIFFTag(static_cast(TIFFTAG_FORWARD_MATRIX1), + TIFF_SRATIONAL, uint32_t(vs.size() / 2), + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetForwardMatrix2(const unsigned int plane_count, + const double *matrix_values) { + std::vector vs(plane_count * 3 * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + bool ret = WriteTIFFTag(static_cast(TIFFTAG_FORWARD_MATRIX2), + TIFF_SRATIONAL, uint32_t(vs.size() / 2), + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetCameraCalibration1(const unsigned int plane_count, + const double *matrix_values) { + std::vector vs(plane_count * plane_count * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + bool ret = WriteTIFFTag(static_cast(TIFFTAG_CAMERA_CALIBRATION1), + TIFF_SRATIONAL, uint32_t(vs.size() / 2), + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetCameraCalibration2(const unsigned int plane_count, + const double *matrix_values) { + std::vector vs(plane_count * plane_count * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + bool ret = WriteTIFFTag(static_cast(TIFFTAG_CAMERA_CALIBRATION2), + TIFF_SRATIONAL, uint32_t(vs.size() / 2), + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetAnalogBalance(const unsigned int plane_count, + const double *matrix_values) { + std::vector vs(plane_count * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + bool ret = WriteTIFFTag(static_cast(TIFFTAG_ANALOG_BALANCE), + TIFF_RATIONAL, uint32_t(vs.size() / 2), + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetCFARepeatPatternDim(const unsigned short width, + const unsigned short height) { + unsigned short data[2] = {width, height}; + + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_CFA_REPEAT_PATTERN_DIM), TIFF_SHORT, 2, + reinterpret_cast(data), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetBlackLevelRepeatDim(const unsigned short width, + const unsigned short height) { + unsigned short data[2] = {width, height}; + + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_BLACK_LEVEL_REPEAT_DIM), TIFF_SHORT, 2, + reinterpret_cast(data), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetCalibrationIlluminant1(const unsigned short value) { + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_CALIBRATION_ILLUMINANT1), TIFF_SHORT, 1, + reinterpret_cast(&value), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetCalibrationIlluminant2(const unsigned short value) { + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_CALIBRATION_ILLUMINANT2), TIFF_SHORT, 1, + reinterpret_cast(&value), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetCFAPattern(const unsigned int num_components, + const unsigned char *values) { + if ((values == NULL) || (num_components < 1)) { + return false; + } + + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_CFA_PATTERN), TIFF_BYTE, num_components, + reinterpret_cast(values), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetAsShotNeutral(const unsigned int plane_count, + const double *matrix_values) { + std::vector vs(plane_count * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + bool ret = WriteTIFFTag(static_cast(TIFFTAG_AS_SHOT_NEUTRAL), + TIFF_RATIONAL, uint32_t(vs.size() / 2), + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetAsShotWhiteXY(const double x, const double y) { + const double values[2] = {x, y}; + std::vector vs(2 * 2); + for (size_t i = 0; i * 2 < vs.size(); i++) { + double numerator, denominator; + if (DoubleToRational(values[i], &numerator, &denominator) != 0) { + // Couldn't represent fp value as integer rational value. + return false; + } + + vs[2 * i + 0] = static_cast(numerator); + vs[2 * i + 1] = static_cast(denominator); + + // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. + if (swap_endian_) { + swap4(&vs[2 * i + 0]); + swap4(&vs[2 * i + 1]); + } + } + bool ret = WriteTIFFTag(static_cast(TIFFTAG_AS_SHOT_WHITE_XY), + TIFF_RATIONAL, uint32_t(vs.size() / 2), + reinterpret_cast(vs.data()), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetImageDataPacked(const unsigned short *input_buffer, const int input_count, const unsigned int input_bpp, bool big_endian) +{ + +#ifndef ROL32 +#define ROL32(v,a) ((v) << (a) | (v) >> (32-(a))) +#endif + +#ifndef ROL16 +#define ROL16(v,a) ((v) << (a) | (v) >> (16-(a))) +#endif + + if (input_count <= 0) { + return false; + } + + if (input_bpp > 16) + return false; + + unsigned int bits_free = 16 - input_bpp; + const unsigned short *unpacked_bits = input_buffer; + + std::vector output(static_cast(input_count)); + unsigned short *packed_bits = output.data(); + + packed_bits[0] = static_cast(unpacked_bits[0] << bits_free); + for (unsigned int pixel_index = 1; pixel_index < static_cast(input_count); pixel_index++) + { + unsigned int bits_offset = (pixel_index * bits_free) % 16; + unsigned int bits_to_rol = bits_free + bits_offset + (bits_offset > 0) * 16; + + unsigned int data = ROL32(static_cast(unpacked_bits[pixel_index]), bits_to_rol); + *(reinterpret_cast(packed_bits)) = (*(reinterpret_cast(packed_bits)) & 0x0000FFFF) | data; + + if(bits_offset > 0 && bits_offset <= input_bpp) + { + if(big_endian) + *(reinterpret_cast(packed_bits)) = static_cast(ROL16(*(reinterpret_cast(packed_bits)), 8)); + + ++packed_bits; + } + } + + return SetImageData(reinterpret_cast(output.data()), output.size() * sizeof(unsigned short)); + +#undef ROL32 +#undef ROL16 +} + +bool DNGImage::SetImageData(const unsigned char *data, const size_t data_len) { + if ((data == NULL) || (data_len < 1)) { + return false; + } + + data_strip_offset_ = size_t(data_os_.tellp()); + data_strip_bytes_ = data_len; + + data_os_.write(reinterpret_cast(data), + static_cast(data_len)); + + // NOTE: STRIP_OFFSET tag will be written at `WriteIFDToStream()`. + + { + unsigned int count = 1; + unsigned int bytes = static_cast(data_len); + + bool ret = WriteTIFFTag( + static_cast(TIFFTAG_STRIP_BYTE_COUNTS), TIFF_LONG, + count, reinterpret_cast(&bytes), &ifd_tags_, + NULL); + + if (!ret) { + return false; + } + + num_fields_++; + } + + return true; +} + +bool DNGImage::SetImageDataJpeg(const unsigned short *data, unsigned int width, + unsigned int height, unsigned int bpp) { + if ((data == NULL) || (height % 2 == 1) || (width % 2 == 1)) { + return false; + } + + uint8_t *compressed = NULL; + int output_buffer_size = 0; + + // Width x2 to move each second line + // ----------- + // Before: + // + // GRGRGR... + // BGBGBG... + // GRGRGR... + // BGBGBG... + // ----------- + // After: + // + // GRGRGR...BGBGBG... + // GRGRGR...BGBGBG... + // ----------- + int new_width = int(width * 2); + int new_height = int(height / 2); + + // Encode image + int ret = detail::lj92_encode(const_cast(data), new_width, new_height, int(bpp), + new_width * new_height, 0, NULL, 0, &compressed, + &output_buffer_size); + + if (ret != detail::LJ92_ERROR_NONE) + return false; + + bool sid_res = SetImageData(compressed, size_t(output_buffer_size)); + + if (compressed) + free(compressed); + + return sid_res; +} + +bool DNGImage::SetCustomFieldLong(const unsigned short tag, const int value) { + unsigned int count = 1; + + // TODO(syoyo): Check if `tag` value does not conflict with existing TIFF tag + // value. + + bool ret = WriteTIFFTag(tag, TIFF_SLONG, count, + reinterpret_cast(&value), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +bool DNGImage::SetCustomFieldULong(const unsigned short tag, + const unsigned int value) { + unsigned int count = 1; + + // TODO(syoyo): Check if `tag` value does not conflict with existing TIFF tag + // value. + + bool ret = WriteTIFFTag(tag, TIFF_LONG, count, + reinterpret_cast(&value), + &ifd_tags_, &data_os_); + + if (!ret) { + return false; + } + + num_fields_++; + return true; +} + +static bool IFDComparator(const IFDTag &a, const IFDTag &b) { + return (a.tag < b.tag); +} + +bool DNGImage::WriteDataToStream(std::ostream *ofs) const { + if ((data_os_.str().length() == 0)) { + err_ += "Empty IFD data and image data.\n"; + return false; + } + + if (bits_per_samples_.empty()) { + err_ += "BitsPerSample is not set\n"; + return false; + } + + for (size_t i = 0; i < bits_per_samples_.size(); i++) { + if (bits_per_samples_[i] == 0) { + err_ += std::to_string(i) + "'th BitsPerSample is zero"; + return false; + } + } + + if (samples_per_pixels_ == 0) { + err_ += "SamplesPerPixels is not set or zero."; + return false; + } + + std::vector data(data_os_.str().length()); + memcpy(data.data(), data_os_.str().data(), data.size()); + + if (data_strip_bytes_ == 0) { + // May ok?. + } else { + // FIXME(syoyo): Assume all channels use sample bps + uint32_t bps = bits_per_samples_[0]; + + // We may need to swap endian for pixel data. + if (swap_endian_) { + if (bps == 16) { + size_t n = data_strip_bytes_ / sizeof(uint16_t); + uint16_t *ptr = + reinterpret_cast(data.data() + data_strip_offset_); + + for (size_t i = 0; i < n; i++) { + swap2(&ptr[i]); + } + + } else if (bps == 32) { + size_t n = data_strip_bytes_ / sizeof(uint32_t); + uint32_t *ptr = + reinterpret_cast(data.data() + data_strip_offset_); + + for (size_t i = 0; i < n; i++) { + swap4(&ptr[i]); + } + + } else if (bps == 64) { + size_t n = data_strip_bytes_ / sizeof(uint64_t); + uint64_t *ptr = + reinterpret_cast(data.data() + data_strip_offset_); + + for (size_t i = 0; i < n; i++) { + swap8(&ptr[i]); + } + } + } + } + + ofs->write(reinterpret_cast(data.data()), + static_cast(data.size())); + + return true; +} + +bool DNGImage::WriteIFDToStream(const unsigned int data_base_offset, + const unsigned int strip_offset, + std::ostream *ofs) const { + if ((num_fields_ == 0) || (ifd_tags_.size() < 1)) { + err_ += "No TIFF Tags.\n"; + return false; + } + + // add STRIP_OFFSET tag and sort IFD tags. + std::vector tags = ifd_tags_; + { + // For STRIP_OFFSET we need the actual offset value to data(image), + // thus write STRIP_OFFSET here. + unsigned int offset = strip_offset + kHeaderSize; + IFDTag ifd; + ifd.tag = TIFFTAG_STRIP_OFFSET; + ifd.type = TIFF_LONG; + ifd.count = 1; + ifd.offset_or_value = offset; + tags.push_back(ifd); + } + + // TIFF expects IFD tags are sorted. + std::sort(tags.begin(), tags.end(), IFDComparator); + + std::ostringstream ifd_os; + + unsigned short num_fields = static_cast(tags.size()); + + Write2(num_fields, &ifd_os, swap_endian_); + + { + size_t typesize_table[] = {1, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4}; + + for (size_t i = 0; i < tags.size(); i++) { + const IFDTag &ifd = tags[i]; + Write2(ifd.tag, &ifd_os, swap_endian_); + Write2(ifd.type, &ifd_os, swap_endian_); + Write4(ifd.count, &ifd_os, swap_endian_); + + size_t len = + ifd.count * (typesize_table[(ifd.type) < 14 ? (ifd.type) : 0]); + if (len > 4) { + // Store offset value. + unsigned int ifd_offt = ifd.offset_or_value + data_base_offset; + Write4(ifd_offt, &ifd_os, swap_endian_); + } else { + // less than 4 bytes = store data itself. + + if (len == 1) { + const unsigned char value = + *(reinterpret_cast(&ifd.offset_or_value)); + Write1(value, &ifd_os); + unsigned char pad = 0; + Write1(pad, &ifd_os); + Write1(pad, &ifd_os); + Write1(pad, &ifd_os); + } else if (len == 2) { + const unsigned short value = + *(reinterpret_cast(&ifd.offset_or_value)); + Write2(value, &ifd_os, swap_endian_); + const unsigned short pad = 0; + Write2(pad, &ifd_os, swap_endian_); + } else if (len == 4) { + const unsigned int value = + *(reinterpret_cast(&ifd.offset_or_value)); + Write4(value, &ifd_os, swap_endian_); + } else { + assert(0); + } + } + } + + ofs->write(ifd_os.str().c_str(), + static_cast(ifd_os.str().length())); + } + + return true; +} + +// ------------------------------------------- + +DNGWriter::DNGWriter(bool big_endian) : dng_big_endian_(big_endian) { + swap_endian_ = (IsBigEndian() != dng_big_endian_); +} + +bool DNGWriter::WriteToFile(const char *filename, std::string *err) const { + std::ofstream ofs(filename, std::ostream::binary); + + if (!ofs) { + if (err) { + (*err) = "Failed to open file.\n"; + } + + return false; + } + + std::ostringstream header; + bool ret = WriteTIFFVersionHeader(&header, dng_big_endian_); + if (!ret) { + if (err) { + (*err) = "Failed to write TIFF version header.\n"; + } + return false; + } + + if (images_.size() == 0) { + if (err) { + (*err) = "No image added for writing.\n"; + } + + return false; + } + + // 1. Compute offset and data size(exclude TIFF header bytes) + size_t data_len = 0; + size_t strip_offset = 0; + std::vector data_offset_table; + std::vector strip_offset_table; + for (size_t i = 0; i < images_.size(); i++) { + strip_offset = data_len + images_[i]->GetStripOffset(); + data_offset_table.push_back(data_len); + strip_offset_table.push_back(strip_offset); + data_len += images_[i]->GetDataSize(); + } + + // 2. Write offset to ifd table. + const unsigned int ifd_offset = + kHeaderSize + static_cast(data_len); + Write4(ifd_offset, &header, swap_endian_); + + assert(header.str().length() == 8); + + // std::cout << "ifd_offset " << ifd_offset << std::endl; + // std::cout << "data_len " << data_os_.str().length() << std::endl; + // std::cout << "ifd_len " << ifd_os_.str().length() << std::endl; + // std::cout << "swap endian " << swap_endian_ << std::endl; + + // 3. Write header + ofs.write(header.str().c_str(), + static_cast(header.str().length())); + + // 4. Write image and meta data + // TODO(syoyo): Write IFD first, then image/meta data + for (size_t i = 0; i < images_.size(); i++) { + bool ok = images_[i]->WriteDataToStream(&ofs); + if (!ok) { + if (err) { + std::stringstream ss; + ss << "Failed to write data at image[" << i << "]. err = " << images_[i]->Error() << "\n"; + (*err) += ss.str(); + } + return false; + } + } + + // 5. Write IFD entries; + for (size_t i = 0; i < images_.size(); i++) { + bool ok = images_[i]->WriteIFDToStream( + static_cast(data_offset_table[i]), + static_cast(strip_offset_table[i]), &ofs); + if (!ok) { + if (err) { + std::stringstream ss; + ss << "Failed to write IFD at image[" << i << "]. err = " << images_[i]->Error() << "\n"; + (*err) += ss.str(); + } + return false; + } + + unsigned int next_ifd_offset = + static_cast(ofs.tellp()) + sizeof(unsigned int); + + if (i == (images_.size() - 1)) { + // Write zero as IFD offset(= end of data) + next_ifd_offset = 0; + } + + if (swap_endian_) { + swap4(&next_ifd_offset); + } + + ofs.write(reinterpret_cast(&next_ifd_offset), 4); + } + + return true; +} + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +} // namespace tinydngwriter + +#endif // TINY_DNG_WRITER_IMPLEMENTATION From b419518d2bffc947e4e2bdb53caaaf5da8c459b8 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Mon, 9 Mar 2026 22:52:15 +0100 Subject: [PATCH 03/29] adding tiny dng writer and opencv --- CMakeLists.txt | 56 +++++++++++++++++++++++++----------- README.md | 2 +- src/engine/Panorama.cpp | 0 src/engine/Panorama.h | 0 src/managers/FileScanner.cpp | 3 +- 5 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 src/engine/Panorama.cpp create mode 100644 src/engine/Panorama.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 35533e5..b2e6622 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,11 +21,13 @@ find_package(Vulkan REQUIRED) if(WIN32) # Use find_package for vcpkg compatibility on Windows find_package(LibRaw REQUIRED) + find_package(OpenCV REQUIRED) add_compile_definitions(WIN32_LEAN_AND_MEAN) add_compile_definitions(NOMINMAX) else() find_package(PkgConfig REQUIRED) pkg_check_modules(LIBRAW REQUIRED libraw) + pkg_check_modules(OPENCV REQUIRED opencv4) endif() qt_standard_project_setup(REQUIRES 6.8) @@ -45,6 +47,7 @@ qt_add_executable(Photon src/engine/VulkanComputeContext.cpp src/engine/VulkanComputeContext.h src/engine/patch_search.comp + src/libraries/tiny_dng_writer.h src/components/RawViewport.cpp src/components/RawViewport.h src/components/ToneLutProvider.h @@ -114,6 +117,8 @@ qt_add_qml_module(Photon src/engine/GpuChromaFilter.h src/engine/ImageDeveloper.cpp src/engine/ImageDeveloper.h + src/engine/Panorama.cpp + src/engine/Panorama.h src/engine/VulkanComputeContext.cpp src/engine/VulkanComputeContext.h src/managers/AppStateManager.cpp @@ -208,12 +213,14 @@ if(WIN32) target_link_libraries(Photon PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate PRIVATE ${LibRaw_LIBRARIES} + PRIVATE ${OpenCV_LIBRARIES} PRIVATE Vulkan::Vulkan ) else() target_link_libraries(Photon PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate PRIVATE ${LIBRAW_LIBRARIES} + PRIVATE ${OPENCV_LIBRARIES} PRIVATE Vulkan::Vulkan ) endif() @@ -268,23 +275,40 @@ install(DIRECTORY content/ DESTINATION ${CMAKE_INSTALL_BINDIR}/content) install(DIRECTORY assets/ DESTINATION ${CMAKE_INSTALL_BINDIR}/assets) # Install vcpkg DLLs on Windows -if(WIN32) +# if(WIN32) # Find LibRaw DLL based on build configuration - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - file(GLOB LIBRAW_DLL "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/bin/rawd.dll") - else() - file(GLOB LIBRAW_DLL "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin/raw.dll") - endif() - if(LIBRAW_DLL) - install(FILES ${LIBRAW_DLL} DESTINATION ${CMAKE_INSTALL_BINDIR}) - endif() - - # Find and install all vcpkg DLLs (but not raw.dll again) - file(GLOB VCPKG_DLLS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin/*.dll") - if(VCPKG_DLLS) - list(REMOVE_ITEM VCPKG_DLLS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin/raw.dll") - install(FILES ${VCPKG_DLLS} DESTINATION ${CMAKE_INSTALL_BINDIR}) - endif() + # if(CMAKE_BUILD_TYPE STREQUAL "Debug") + # file(GLOB LIBRAW_DLL "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/bin/rawd.dll") + # else() + # file(GLOB LIBRAW_DLL "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin/raw.dll") + # endif() + # if(LIBRAW_DLL) + # install(FILES ${LIBRAW_DLL} DESTINATION ${CMAKE_INSTALL_BINDIR}) + # endif() + # + # # Find and install all vcpkg DLLs (but not raw.dll again) + # file(GLOB VCPKG_DLLS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin/*.dll") + # if(VCPKG_DLLS) + # list(REMOVE_ITEM VCPKG_DLLS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin/raw.dll") + # install(FILES ${VCPKG_DLLS} DESTINATION ${CMAKE_INSTALL_BINDIR}) + # endif() + #endif() + +if(WIN32) + # 1. Determine the correct vcpkg bin folder based on build type + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(VCPKG_BIN_DIR "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/debug/bin") + else() + set(VCPKG_BIN_DIR "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin") + endif() + + # 2. Grab ALL DLLs from that folder (this handles LibRaw, OpenCV, and any others automatically) + file(GLOB VCPKG_DLLS "${VCPKG_BIN_DIR}/*.dll") + + # 3. Install them + if(VCPKG_DLLS) + install(FILES ${VCPKG_DLLS} DESTINATION ${CMAKE_INSTALL_BINDIR}) + endif() endif() if(WIN32) diff --git a/README.md b/README.md index b3dbb1e..1eea799 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ If you are looking for a modern quick photo editor, this might be for you. On th Not having much experience with both Qt6 and how images are processed, I used both Gemini and Copilot to kickstart the project, especially to implement what could have taken months and months of full time work, which I cannot afford right now. -The project is in an advanced state and most of functionalities listed below as completed work good enough for me, so I decided to step back from automatic programming and start to implement and refine what's missing manually, to both asses the code quality produced up until now (I would be a liar if I say that I diligently reviewed all the AI output...) and to actually keep my skills sharp in these funny times. +The project is in an advanced state and most of the functionalities listed below as completed work good enough for me, so I decided to step back from automatic programming/vibe coding/whatever and start to implement and refine what's missing manually, to both asses the code quality produced up until now (I would be a liar if I say that I diligently reviewed all the AI output...) and to actually keep my skills sharp in these funny times. Here's what works and what is still in the backlog: diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/engine/Panorama.h b/src/engine/Panorama.h new file mode 100644 index 0000000..e69de29 diff --git a/src/managers/FileScanner.cpp b/src/managers/FileScanner.cpp index a0c267b..6215ba5 100644 --- a/src/managers/FileScanner.cpp +++ b/src/managers/FileScanner.cpp @@ -36,6 +36,7 @@ QVariantList FileScanner::scanForRawFiles(const QString& folderPath) const { fileMap["name"] = fileInfo.fileName(); fileMap["size"] = fileInfo.size(); fileMap["modified"] = fileInfo.lastModified(); + fileMap["extension"] = fileInfo.suffix().toLower(); // Read rating from sidecar if it exists int rating = 0; @@ -65,4 +66,4 @@ QVariantList FileScanner::scanForRawFiles(const QString& folderPath) const { bool FileScanner::isRawFile(const QFileInfo& fileInfo) const { QString extension = fileInfo.suffix().toLower(); return m_supportedExtensions.contains(extension); -} \ No newline at end of file +} From ae70b2cf5c6abe80100ffd0772ac545d644e5123 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Mon, 9 Mar 2026 23:04:14 +0100 Subject: [PATCH 04/29] workflow --- .github/workflows/build.yml | 4 ++-- TASKS.md | 3 ++- src/engine/RawEngine.cpp | 4 +++- src/managers/ExportManager.cpp | 3 ++- src/managers/PreviewManager.cpp | 3 ++- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce68f3e..0c03432 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,7 +59,7 @@ jobs: libxcb-cursor0 libxcb-xinerama0 libxcb-xinput0 libxcb-icccm4 \ libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-shape0 \ libwayland-client0 libwayland-cursor0 libwayland-egl1 libxkbcommon-x11-0 \ - libwayland-dev wayland-protocols libwayland-server0 \ + libwayland-dev wayland-protocols libwayland-server0 libopencv-dev \ imagemagick wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage @@ -68,7 +68,7 @@ jobs: - name: Install Dependencies (Windows) if: runner.os == 'Windows' run: | - vcpkg install libraw:x64-windows vulkan:x64-windows + vcpkg install libraw:x64-windows vulkan:x64-windows opencv4:x64-windows - name: Configure CMake shell: bash diff --git a/TASKS.md b/TASKS.md index 2c61250..706d6f5 100644 --- a/TASKS.md +++ b/TASKS.md @@ -507,7 +507,7 @@ - [x] Pop up error when continue session folder is not found, then reset it and return to WelcomeView - [x] Auto log cleanup -## Phase 38 +## Phase 38: Panorama - [ ] JPEG and TIFF support. - [ ] Panorama Stitching @@ -521,3 +521,4 @@ - [ ] Keystone/perspective transform controls. - [ ] **Lens Correction** - [ ] Integrate `lensfun` for automatic distortion/vignette removal. +- [ ] Let the use decide whether to use auto brightness or not (and threshold) diff --git a/src/engine/RawEngine.cpp b/src/engine/RawEngine.cpp index f3a26bf..389d015 100644 --- a/src/engine/RawEngine.cpp +++ b/src/engine/RawEngine.cpp @@ -279,7 +279,9 @@ void RawEngine::releaseGpuResources() { void RawEngine::updateProcessingParams() { m_processor->imgdata.params.use_camera_wb = 1; m_processor->imgdata.params.output_bps = 16; - m_processor->imgdata.params.no_auto_bright = 1; + // NOTE: The user should be able to change/adjust this + m_processor->imgdata.params.no_auto_bright = 0; + m_processor->imgdata.params.auto_bright_thr = 0.01; m_processor->imgdata.params.half_size = m_halfSize ? 1 : 0; } diff --git a/src/managers/ExportManager.cpp b/src/managers/ExportManager.cpp index 894d83c..de05c20 100644 --- a/src/managers/ExportManager.cpp +++ b/src/managers/ExportManager.cpp @@ -84,7 +84,8 @@ void ExportManager::processExport(const QStringList& paths, LibRaw processor; processor.imgdata.params.output_bps = 16; processor.imgdata.params.use_camera_wb = 1; - processor.imgdata.params.no_auto_bright = 1; + processor.imgdata.params.no_auto_bright = 0; + processor.imgdata.params.auto_bright_thr = 0.01; if (processor.open_file(path.toLocal8Bit().data()) == LIBRAW_SUCCESS) { if (processor.unpack() == LIBRAW_SUCCESS) { diff --git a/src/managers/PreviewManager.cpp b/src/managers/PreviewManager.cpp index 8f9577d..07c312f 100644 --- a/src/managers/PreviewManager.cpp +++ b/src/managers/PreviewManager.cpp @@ -201,7 +201,8 @@ void PreviewManager::processItem(const QString& rawPath, bool skipGpu) { LibRaw processor; processor.imgdata.params.output_bps = 16; processor.imgdata.params.use_camera_wb = 1; - processor.imgdata.params.no_auto_bright = 1; + processor.imgdata.params.no_auto_bright = 0; + processor.imgdata.params.auto_bright_thr = 0.01; processor.imgdata.params.half_size = 1; // 1080p is enough, half_size is fast if (processor.open_file(rawPath.toLocal8Bit().data()) == LIBRAW_SUCCESS) { From 4052326f73e0377faed7ab3eb62a679207ec69b8 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Wed, 11 Mar 2026 16:14:21 +0100 Subject: [PATCH 05/29] wip: changed log parameter and started implementing stitche --- src/engine/GpuChromaFilter.cpp | 10 +-- src/engine/GpuSearcher.cpp | 24 +++---- src/engine/ImageDeveloper.cpp | 16 ++--- src/engine/Panorama.cpp | 106 ++++++++++++++++++++++++++++ src/engine/Panorama.h | 78 ++++++++++++++++++++ src/engine/RawEngine.cpp | 30 ++++---- src/engine/VulkanComputeContext.cpp | 2 +- src/managers/AppStateManager.cpp | 22 ++++-- src/managers/FileScanner.cpp | 8 ++- src/managers/LogManager.cpp | 34 +++++---- src/managers/LogManager.h | 23 +++--- src/managers/PreviewManager.cpp | 10 +-- 12 files changed, 286 insertions(+), 77 deletions(-) diff --git a/src/engine/GpuChromaFilter.cpp b/src/engine/GpuChromaFilter.cpp index 8a2308c..3f1b333 100644 --- a/src/engine/GpuChromaFilter.cpp +++ b/src/engine/GpuChromaFilter.cpp @@ -432,12 +432,12 @@ bool GpuChromaFilter::run(const float* guide, const float* input, QString("[ GpuChromaFilter ] - Starting GPU guided filter %1x%2") .arg(width) .arg(height), - "INFO"); + INFO); GpuResources res{}; if (!createResources(res, width, height)) { LogManager::instance()->log( - "[ GpuChromaFilter ] - Failed to create GPU resources", "ERROR"); + "[ GpuChromaFilter ] - Failed to create GPU resources", ERROR); destroyResources(res); return false; } @@ -447,7 +447,7 @@ bool GpuChromaFilter::run(const float* guide, const float* input, if (!uploadBuffer(res, BUF_GUIDE, guide, bufSize) || !uploadBuffer(res, BUF_INPUT, input, bufSize)) { LogManager::instance()->log( - "[ GpuChromaFilter ] - Failed to upload data to GPU", "ERROR"); + "[ GpuChromaFilter ] - Failed to upload data to GPU", ERROR); destroyResources(res); return false; } @@ -485,7 +485,7 @@ bool GpuChromaFilter::run(const float* guide, const float* input, // After 3 passes + swaps, result is in the buffer pointed to by inputBuf if (!readbackBuffer(res, inputBuf, output, bufSize)) { LogManager::instance()->log( - "[ GpuChromaFilter ] - Failed to read back GPU results", "ERROR"); + "[ GpuChromaFilter ] - Failed to read back GPU results", ERROR); destroyResources(res); return false; } @@ -493,7 +493,7 @@ bool GpuChromaFilter::run(const float* guide, const float* input, destroyResources(res); LogManager::instance()->log( - "[ GpuChromaFilter ] - GPU guided filter complete", "INFO"); + "[ GpuChromaFilter ] - GPU guided filter complete", INFO); return true; } diff --git a/src/engine/GpuSearcher.cpp b/src/engine/GpuSearcher.cpp index 2085da1..4046b22 100644 --- a/src/engine/GpuSearcher.cpp +++ b/src/engine/GpuSearcher.cpp @@ -29,7 +29,7 @@ std::vector GpuSearcher::runSearch( const auto& f = ctx->functions(); VkDevice device = ctx->device(); - LogManager::instance()->log(QString("[ GpuSearcher ] - Starting raw Vulkan search %1x%2").arg(width).arg(height), "INFO"); + LogManager::instance()->log(QString("[ GpuSearcher ] - Starting raw Vulkan search %1x%2").arg(width).arg(height), INFO); // 1. Create Resources VkBuffer lumaBuffer = VK_NULL_HANDLE, resultBuffer = VK_NULL_HANDLE; @@ -46,14 +46,14 @@ std::vector GpuSearcher::runSearch( resultBuffer, resultMemory); if (lumaBuffer == VK_NULL_HANDLE || resultBuffer == VK_NULL_HANDLE) { - LogManager::instance()->log("Failed to create Vulkan buffers for search", "ERROR"); + LogManager::instance()->log("Failed to create Vulkan buffers for search", ERROR); return {}; } // Upload Luma void* dataPtr = nullptr; if (f.MapMemory(device, lumaMemory, 0, lumaSize, 0, &dataPtr) != VK_SUCCESS) { - LogManager::instance()->log("Failed to map luma memory", "ERROR"); + LogManager::instance()->log("Failed to map luma memory", ERROR); return {}; } memcpy(dataPtr, luma, lumaSize); @@ -78,7 +78,7 @@ std::vector GpuSearcher::runSearch( VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; if (f.CreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) { - LogManager::instance()->log("Failed to create descriptor set layout", "ERROR"); + LogManager::instance()->log("Failed to create descriptor set layout", ERROR); return {}; } @@ -94,7 +94,7 @@ std::vector GpuSearcher::runSearch( VkDescriptorPool descriptorPool = VK_NULL_HANDLE; if (f.CreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { - LogManager::instance()->log("Failed to create descriptor pool", "ERROR"); + LogManager::instance()->log("Failed to create descriptor pool", ERROR); return {}; } @@ -106,7 +106,7 @@ std::vector GpuSearcher::runSearch( VkDescriptorSet descriptorSet = VK_NULL_HANDLE; if (f.AllocateDescriptorSets(device, &allocInfo, &descriptorSet) != VK_SUCCESS) { - LogManager::instance()->log("Failed to allocate descriptor set", "ERROR"); + LogManager::instance()->log("Failed to allocate descriptor set", ERROR); return {}; } @@ -145,13 +145,13 @@ std::vector GpuSearcher::runSearch( VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; if (f.CreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) { - LogManager::instance()->log("Failed to create pipeline layout", "ERROR"); + LogManager::instance()->log("Failed to create pipeline layout", ERROR); return {}; } QFile shaderFile(":/Main/shaders/patch_search.comp.qsb"); if (!shaderFile.open(QIODevice::ReadOnly)) { - LogManager::instance()->log("Failed to open shader resource", "ERROR"); + LogManager::instance()->log("Failed to open shader resource", ERROR); return {}; } @@ -166,7 +166,7 @@ std::vector GpuSearcher::runSearch( } if (spirvCode.isEmpty()) { - LogManager::instance()->log("Failed to extract SPIR-V from shader", "ERROR"); + LogManager::instance()->log("Failed to extract SPIR-V from shader", ERROR); return {}; } @@ -181,7 +181,7 @@ std::vector GpuSearcher::runSearch( VkShaderModule computeShaderModule = VK_NULL_HANDLE; if (f.CreateShaderModule(device, &shaderModuleCreateInfo, nullptr, &computeShaderModule) != VK_SUCCESS) { - LogManager::instance()->log("Failed to create shader module", "ERROR"); + LogManager::instance()->log("Failed to create shader module", ERROR); return {}; } @@ -195,7 +195,7 @@ std::vector GpuSearcher::runSearch( VkPipeline pipeline = VK_NULL_HANDLE; if (f.CreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline) != VK_SUCCESS) { - LogManager::instance()->log("Failed to create compute pipeline", "ERROR"); + LogManager::instance()->log("Failed to create compute pipeline", ERROR); return {}; } @@ -221,7 +221,7 @@ std::vector GpuSearcher::runSearch( } f.UnmapMemory(device, resultMemory); } else { - LogManager::instance()->log("Failed to map result memory for readback", "ERROR"); + LogManager::instance()->log("Failed to map result memory for readback", ERROR); } // 6. Cleanup diff --git a/src/engine/ImageDeveloper.cpp b/src/engine/ImageDeveloper.cpp index a15f436..ec3285c 100644 --- a/src/engine/ImageDeveloper.cpp +++ b/src/engine/ImageDeveloper.cpp @@ -261,10 +261,10 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, .arg(width) .arg(height) .arg((quintptr)QThread::currentThread()), - "DEBUG"); + DEBUG); if (!src || width <= 0 || height <= 0) { - LogManager::instance()->log("[ ImageDeveloper ] - develop ABORT: invalid params", "ERROR"); + LogManager::instance()->log("[ ImageDeveloper ] - develop ABORT: invalid params", ERROR); return QImage(); } @@ -302,7 +302,7 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, .arg(high, 0, 'f', 2) .arg(shad, 0, 'f', 2) .arg(denoiseAmount, 0, 'f', 1), - "DEBUG"); + DEBUG); // HSL Params float hsl_h[8], hsl_s[8], hsl_l[8]; @@ -616,7 +616,7 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, QString("[ ImageDeveloper ] - Denoising: amount=%1 (rhi=%2)") .arg(denoiseAmount, 0, 'f', 1) .arg((quintptr)rhi), - "INFO"); + INFO); std::vector gpuMatches; if (rhi) { int w = output.width(); @@ -638,9 +638,9 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, gpuMatches = searcher.runSearch(luma.data(), w, h, 19); if (gpuMatches.empty()) { - LogManager::instance()->log("[ ImageDeveloper ] - GPU search produced no matches (possibly due to frame conflict or shader error). Falling back to CPU matching.", "WARNING"); + LogManager::instance()->log("[ ImageDeveloper ] - GPU search produced no matches (possibly due to frame conflict or shader error). Falling back to CPU matching.", WARNING); } else { - LogManager::instance()->log(QString("[ ImageDeveloper ] - GPU search successful: %1 matches").arg(gpuMatches.size()), "DEBUG"); + LogManager::instance()->log(QString("[ ImageDeveloper ] - GPU search successful: %1 matches").arg(gpuMatches.size()), DEBUG); } } photon::DenoiseParams dparams; @@ -740,11 +740,11 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, .arg(cropBottom) .arg(output.width()) .arg(output.height()), - "DEBUG"); + DEBUG); LogManager::instance()->log( QString("[ ImageDeveloper ] - export END: %1x%2").arg(output.width()).arg(output.height()), - "DEBUG"); + DEBUG); return output; } diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp index e69de29..e059df7 100644 --- a/src/engine/Panorama.cpp +++ b/src/engine/Panorama.cpp @@ -0,0 +1,106 @@ +#include "Panorama.h" +using namespace photon; + + +void Panorama::stitchAsync(const QStringList& inputFiles) { + QThread* thread = QThread::create([this, inputFiles]() { + + }); +} + +void Panorama::stitchPhotos(const std::vector inputFiles) { + if (inputFiles.size() < 2) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Cannot stitch a single image"), + ERROR); + } + + std::vector images; + for (const auto& filename : inputFiles) { + cv::Mat img = raw_to_linear(filename); + if (img.empty()) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Failed to process image %1").arg(filename), + ERROR); + continue; + } + images.push_back(img); + } + + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Merging %1 images").arg(images.size()), + DEBUG); + + cv::Mat panoramaBGR; + cv::Ptr stitcher = cv::Stitcher::create(cv::Stitcher::PANORAMA); + + cv::Stitcher::Status status = stitcher->stitch(images, panoramaBGR); + + if (status != cv::Stitcher::OK) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Stitcher failed!"), + DEBUG); + // emit result + } + + cv::Mat panoramaRGB; + + cv::cvtColor(panoramaBGR, panoramaRGB, cv::COLOR_BGR2RGB); + + tinydngwriter::DNGWriter dngwriter(false); + tinydngwriter::DNGImage dngimage; + + dngimage.SetSubfileType(false, false, false); + dngimage.SetImageWidth(panoramaRGB.cols); + dngimage.SetImageLength(panoramaRGB.rows); + dngimage.SetRowsPerStrip(panoramaRGB.rows); + dngimage.SetSamplesPerPixel(3); // 3 channels rgb + uint16_t bps[3] = {16, 16, 16}; + + dngimage.SetBitsPerSample(3, bps); + dngimage.SetPlanarConfig(tinydngwriter::PLANARCONFIG_CONTIG); + dngimage.SetCompression(tinydngwriter::COMPRESSION_NONE); + dngimage.SetPhotometric(tinydngwriter::PHOTOMETRIC_RGB); + // + // Standard DNG tags for color calibration. + // Since we outputted to sRGB from LibRaw, an identity matrix is technically + // sufficient, but D65 illuminant provides standard baseline compatibility. + uint16_t illuminant = 21; // D65 + dngimage.SetCalibrationIlluminant1(illuminant); + double colorMatrix[9] = { + 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f + }; + dngimage.SetColorMatrix1(3, colorMatrix); + dngimage.SetImageData(reinterpret_cast(panoramaRGB.data), panoramaRGB.total() * panoramaRGB.elemSize()); + dngwriter.AddImage(&dngimage); + QString filename = + QDir::toNativeSeparators( + QString( + "%1/%2.pano.dng" + ).arg( + QFileInfo(inputFiles[0]).absolutePath(), + QFileInfo(inputFiles[0]).baseName() + ) + ); + + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Saving panorama to %1").arg(filename), + ERROR); + + std::string errMsg; + if ( + !dngwriter.WriteToFile( + filename.toStdString().c_str(), + &errMsg + ) + ) { + + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Couldn't write dng file %1: %2").arg(filename, errMsg), + ERROR); + // emit failure + } + +} diff --git a/src/engine/Panorama.h b/src/engine/Panorama.h index e69de29..70e9303 100644 --- a/src/engine/Panorama.h +++ b/src/engine/Panorama.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "../managers/LogManager.h" +#define TINY_DNG_WRITER_IMPLEMENTATION +#include "../libraries/tiny_dng_writer.h" +namespace photon { + +class Panorama : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +private: + static cv::Mat raw_to_linear(QString file) { + LibRaw processor; + processor.imgdata.params.output_bps = 16; + processor.imgdata.params.gamm[0] = 1.0; + processor.imgdata.params.gamm[1] = 1.0; + processor.imgdata.params.no_auto_bright = 1; + processor.imgdata.params.use_auto_wb = 1; + processor.imgdata.params.output_color = 1; + if (processor.open_file(file.toStdString().c_str()) != LIBRAW_SUCCESS) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Cannot open file %1").arg(file), + ERROR); + return cv::Mat(); + } + + if (processor.unpack() != LIBRAW_SUCCESS) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Cannot unpack data of file %1").arg(file), + ERROR); + return cv::Mat(); + } + + if (processor.dcraw_process() != LIBRAW_SUCCESS) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Cannot dcraw file %1").arg(file), + ERROR); + return cv::Mat(); + } + + libraw_processed_image_t *image = processor.dcraw_make_mem_image(); + + if (!image) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Cannot create procesed image from %1").arg(file), + ERROR); + return cv::Mat(); + } + + cv::Mat rawRGB(image->height, image->width, CV_16UC3, image->data); + cv::Mat matBGR; + cv::cvtColor(rawRGB, matBGR, cv::COLOR_RGB2BGR); + LibRaw::dcraw_clear_mem(image); + + return matBGR; + } + + void stitchPhotos(const std::vector inputFiles); +public: + Q_INVOKABLE void stitchAsync(const QStringList& inputFiles); + explicit Panorama(QObject* parent = nullptr); + ~Panorama() override; + +signals: + void stitchCompleted(bool success, const QString& message); +}; + +} diff --git a/src/engine/RawEngine.cpp b/src/engine/RawEngine.cpp index 389d015..d2b317d 100644 --- a/src/engine/RawEngine.cpp +++ b/src/engine/RawEngine.cpp @@ -222,10 +222,10 @@ RawEngine::RawEngine(QObject* parent) LogManager::instance()->log( QString("[ RawEngine ] - previewWatcher callback START (thread: %1)") .arg((quintptr)QThread::currentThread()), - "DEBUG"); + DEBUG); if (m_previewWatcher.isCanceled()) { LogManager::instance()->log("[ RawEngine ] - previewWatcher: canceled", - "DEBUG"); + DEBUG); return; } QImage result = m_previewWatcher.result(); @@ -234,11 +234,11 @@ RawEngine::RawEngine(QObject* parent) .arg(result.isNull()) .arg(result.width()) .arg(result.height()), - "DEBUG"); + DEBUG); m_previewImage = result; emit previewImageChanged(); LogManager::instance()->log("[ RawEngine ] - previewWatcher callback END", - "DEBUG"); + DEBUG); }); // Listen for background previews @@ -301,14 +301,14 @@ void RawEngine::setHalfSize(bool half) { void RawEngine::setSource(const QString& source) { LogManager::instance()->log( - QString("[ RawEngine ] - setSource START: %1").arg(source), "INFO"); + QString("[ RawEngine ] - setSource START: %1").arg(source), INFO); // 1. Abort any ongoing denoise tasks m_abortDenoise = true; if (m_source == source) { LogManager::instance()->log( - "[ RawEngine ] - setSource: same source, skipping", "DEBUG"); + "[ RawEngine ] - setSource: same source, skipping", DEBUG); return; } @@ -332,7 +332,7 @@ void RawEngine::setSource(const QString& source) { // Try to get existing preview immediately LogManager::instance()->log("[ RawEngine ] - setSource: getting preview path", - "DEBUG"); + DEBUG); if (photon::PreviewManager::instance()) { m_previewPath = photon::PreviewManager::instance()->getPreviewPath(m_source); @@ -342,7 +342,7 @@ void RawEngine::setSource(const QString& source) { LogManager::instance()->log( QString("[ RawEngine ] - setSource: starting preview image load: %1") .arg(m_previewPath), - "DEBUG"); + DEBUG); m_previewWatcher.setFuture( QtConcurrent::run([path = m_previewPath]() { return QImage(path); })); } @@ -374,10 +374,10 @@ void RawEngine::setSource(const QString& source) { // Start async loading LogManager::instance()->log("[ RawEngine ] - setSource: starting async load", - "DEBUG"); + DEBUG); loadRawFileAsync(source); - LogManager::instance()->log("[ RawEngine ] - setSource END", "INFO"); + LogManager::instance()->log("[ RawEngine ] - setSource END", INFO); } void RawEngine::setViewportSize(const QSize& size) { @@ -2265,7 +2265,7 @@ void RawEngine::loadEdits() { QString("[ RawEngine ] - Loading edits: denoiseEnabled=%1, denoiseAmount=%2") .arg(lastState["denoiseEnabled"].toBool()) .arg(lastState["denoiseAmount"].toDouble()), - "DEBUG"); + DEBUG); applyJsonToState(this, lastState); emit editStackChanged(); @@ -2597,7 +2597,7 @@ QImage RawEngine::applyGeometryTransforms(const QImage& input, int orientSteps, .arg(cropBottom) .arg(output.width()) .arg(output.height()), - "DEBUG"); + DEBUG); return output; } @@ -2607,7 +2607,7 @@ void RawEngine::reloadWithGeometry() { LogManager::instance()->log( "[ RawEngine.cpp ] - reloadWithGeometry: re-decoding with geometry bake", - "DEBUG"); + DEBUG); m_inCropMode = false; m_isLoading = true; @@ -2696,7 +2696,7 @@ void RawEngine::enterCropMode() { LogManager::instance()->log( "[ RawEngine.cpp ] - enterCropMode: showing original for crop editing", - "DEBUG"); + DEBUG); m_inCropMode = true; @@ -2726,7 +2726,7 @@ void RawEngine::enterCropMode() { void RawEngine::exitCropMode() { LogManager::instance()->log( - "[ RawEngine.cpp ] - exitCropMode: re-baking geometry", "DEBUG"); + "[ RawEngine.cpp ] - exitCropMode: re-baking geometry", DEBUG); m_inCropMode = false; diff --git a/src/engine/VulkanComputeContext.cpp b/src/engine/VulkanComputeContext.cpp index d100f9d..6e92612 100644 --- a/src/engine/VulkanComputeContext.cpp +++ b/src/engine/VulkanComputeContext.cpp @@ -95,7 +95,7 @@ bool VulkanComputeContext::init(QRhi* rhi) { return false; } - LogManager::instance()->log("[ VulkanComputeContext ] - Initialized plain Vulkan compute context", "INFO"); + LogManager::instance()->log("[ VulkanComputeContext ] - Initialized plain Vulkan compute context", INFO); return true; } diff --git a/src/managers/AppStateManager.cpp b/src/managers/AppStateManager.cpp index ae390a8..3e8475c 100644 --- a/src/managers/AppStateManager.cpp +++ b/src/managers/AppStateManager.cpp @@ -115,8 +115,8 @@ void AppStateManager::loadSettings() { m_accentColor = m_settings.value(KEY_ACCENT_COLOR, "#3b82f6").toString(); m_previewDenoiseFull = m_settings.value(KEY_PREVIEW_DENOISE_FULL, false).toBool(); - QString level = m_settings.value("diagnostics/logLevel", "INFO").toString(); - LogManager::instance()->setMinLogLevel(level); + QString level = m_settings.value("diagnostics/logLevel", INFO).toString(); + LogManager::instance()->setLogLevel(level); emit lastOpenedFolderChanged(); emit hasLastSessionChanged(); @@ -241,7 +241,7 @@ void AppStateManager::setCurrentFolder(const QString& folder) { void AppStateManager::setCurrentImage(const QString& image) { LogManager::instance()->log( QString("[ AppStateManager ] - setCurrentImage START: %1").arg(image), - "DEBUG"); + DEBUG); if (m_currentImage != image) { m_currentImage = image; @@ -257,7 +257,7 @@ void AppStateManager::setCurrentImage(const QString& image) { } LogManager::instance()->log("[ AppStateManager ] - setCurrentImage END", - "DEBUG"); + DEBUG); } void AppStateManager::toggleSelection(const QString& path) { @@ -585,11 +585,21 @@ void AppStateManager::setLogLocation(const QString& location) { } QString AppStateManager::logLevel() const { - return LogManager::instance()->minLogLevel(); + switch (LogManager::instance()->logLevel()) { + case DEBUG: + return "DEBUG"; + case INFO: + return "INFO"; + case WARNING: + return "WARNING"; + case ERROR: + return "ERROR"; + } + return ""; } void AppStateManager::setLogLevel(const QString& level) { - LogManager::instance()->setMinLogLevel(level); + LogManager::instance()->setLogLevel(level); emit logLevelChanged(); m_settings.setValue("diagnostics/logLevel", level); m_settings.sync(); diff --git a/src/managers/FileScanner.cpp b/src/managers/FileScanner.cpp index 6215ba5..851317a 100644 --- a/src/managers/FileScanner.cpp +++ b/src/managers/FileScanner.cpp @@ -1,13 +1,15 @@ #include "FileScanner.h" #include -#include +#include #include #include #include #include #include +#include "LogManager.h" + FileScanner::FileScanner(QObject* parent) : QObject(parent) { // Initialize supported RAW file extensions m_supportedExtensions << "arw" << "cr2" << "cr3" << "nef" << "dng" @@ -20,7 +22,9 @@ QVariantList FileScanner::scanForRawFiles(const QString& folderPath) const { QDir dir(folderPath); if (!dir.exists()) { - qWarning() << "Folder does not exist:" << folderPath; + photon::LogManager::instance()->log( + QString("Folder %1 does not exists").arg(folderPath) + ); return rawFiles; } diff --git a/src/managers/LogManager.cpp b/src/managers/LogManager.cpp index aa3be33..c5b938f 100644 --- a/src/managers/LogManager.cpp +++ b/src/managers/LogManager.cpp @@ -45,19 +45,23 @@ void LogManager::setLogLocation(const QString& location) { } } -void LogManager::setMinLogLevel(const QString& level) { - if (m_minLogLevel != level) { - m_minLogLevel = level; - emit minLogLevelChanged(); - } + +void LogManager::setLogLevel(const QString& level) { + if (level == "DEBUG") + setLogLevel(DEBUG); + else if (level == "INFO") + setLogLevel(INFO); + else if (level == "WARNING") + setLogLevel(WARNING); + else if (level == "ERROR") + setLogLevel(ERROR); } -int LogManager::levelToInt(const QString& level) const { - if (level == "DEBUG") return 0; - if (level == "INFO") return 1; - if (level == "WARNING") return 2; - if (level == "ERROR") return 3; - return 1; +void LogManager::setLogLevel(int level) { + if (m_logLevel != level) { + m_logLevel = level; + emit logLevelChanged(); + } } void LogManager::openLogFile() { @@ -69,19 +73,19 @@ void LogManager::openLogFile() { QIODevice::Text)) { qWarning() << "Failed to open log file at" << m_logLocation; } else { - log("Logging started at " + m_logLocation, "INFO"); + log("Logging started at " + m_logLocation, INFO); } } -void LogManager::log(const QString& message, const QString& level) { +void LogManager::log(const QString& message, int level) { QMutexLocker locker(&m_logMutex); - if (levelToInt(level) < levelToInt(m_minLogLevel)) return; + if (level < m_logLevel) return; if (!m_logFile.isOpen()) return; QTextStream out(&m_logFile); QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz"); - out << QString("[%1] [%2] %3\n").arg(timestamp, level, message); + out << QString("[%1] [%2] %3\n").arg(timestamp, QString::number(level), message); out.flush(); } diff --git a/src/managers/LogManager.h b/src/managers/LogManager.h index f093f77..76e41a2 100644 --- a/src/managers/LogManager.h +++ b/src/managers/LogManager.h @@ -11,12 +11,19 @@ namespace photon { +enum LogLevel { + DEBUG, + INFO, + WARNING, + ERROR +}; + class LogManager : public QObject { Q_OBJECT Q_PROPERTY(QString logLocation READ logLocation WRITE setLogLocation NOTIFY logLocationChanged) - Q_PROPERTY(QString minLogLevel READ minLogLevel WRITE setMinLogLevel NOTIFY - minLogLevelChanged) + Q_PROPERTY(QString logLevel READ logLevel WRITE setLogLevel NOTIFY + logLevelChanged) public: explicit LogManager(QObject* parent = nullptr); @@ -27,24 +34,24 @@ class LogManager : public QObject { QString logLocation() const { return m_logLocation; } void setLogLocation(const QString& location); - QString minLogLevel() const { return m_minLogLevel; } - void setMinLogLevel(const QString& level); + int logLevel() const { return m_logLevel; } + void setLogLevel(int level); + void setLogLevel(const QString& level); - Q_INVOKABLE void log(const QString& message, const QString& level = "INFO"); + Q_INVOKABLE void log(const QString& message, int level = INFO); Q_INVOKABLE void clearLog(); signals: void logLocationChanged(); - void minLogLevelChanged(); + void logLevelChanged(); private: static LogManager* s_instance; QString m_logLocation; - QString m_minLogLevel = "INFO"; + int m_logLevel = INFO; QFile m_logFile; QMutex m_logMutex; - int levelToInt(const QString& level) const; void openLogFile(); }; diff --git a/src/managers/PreviewManager.cpp b/src/managers/PreviewManager.cpp index 07c312f..16cb779 100644 --- a/src/managers/PreviewManager.cpp +++ b/src/managers/PreviewManager.cpp @@ -167,12 +167,12 @@ void PreviewManager::cancelAll() { } void PreviewManager::processItem(const QString& rawPath, bool skipGpu) { - LogManager::instance()->log(QString("[ PreviewManager ] - processItem START: %1").arg(rawPath), "DEBUG"); + LogManager::instance()->log(QString("[ PreviewManager ] - processItem START: %1").arg(rawPath), DEBUG); { QMutexLocker locker(&m_mutex); if (m_abort) { - LogManager::instance()->log(QString("[ PreviewManager ] - processItem ABORTED: %1").arg(rawPath), "DEBUG"); + LogManager::instance()->log(QString("[ PreviewManager ] - processItem ABORTED: %1").arg(rawPath), DEBUG); return; } } @@ -185,7 +185,7 @@ void PreviewManager::processItem(const QString& rawPath, bool skipGpu) { fileInfo.fileName() + ".json"); QJsonObject lastState; if (QFile::exists(editsPath)) { - LogManager::instance()->log(QString("[ PreviewManager ] - Loading sidecar: %1").arg(editsPath), "DEBUG"); + LogManager::instance()->log(QString("[ PreviewManager ] - Loading sidecar: %1").arg(editsPath), DEBUG); QFile file(editsPath); if (file.open(QIODevice::ReadOnly)) { QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); @@ -197,7 +197,7 @@ void PreviewManager::processItem(const QString& rawPath, bool skipGpu) { } // 2. Load RAW via LibRaw (Fast mode) - LogManager::instance()->log(QString("[ PreviewManager ] - Opening RAW file: %1").arg(rawPath), "DEBUG"); + LogManager::instance()->log(QString("[ PreviewManager ] - Opening RAW file: %1").arg(rawPath), DEBUG); LibRaw processor; processor.imgdata.params.output_bps = 16; processor.imgdata.params.use_camera_wb = 1; @@ -251,7 +251,7 @@ void PreviewManager::processItem(const QString& rawPath, bool skipGpu) { } } } - LogManager::instance()->log(QString("[ PreviewManager ] - processItem END: %1").arg(rawPath), "DEBUG"); + LogManager::instance()->log(QString("[ PreviewManager ] - processItem END: %1").arg(rawPath), DEBUG); } } // namespace photon From c6790932fe20d80db2d9910882b155448e62f94f Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Wed, 11 Mar 2026 19:58:14 +0100 Subject: [PATCH 06/29] wip: wired up, stitching fails --- CMakeLists.txt | 7 +++++ assets/icons/hdr.svg | 1 + assets/icons/panorama.svg | 1 + content/components/PhotoContextMenu.qml | 27 ++++++++++++++++++- content/views/App.qml | 8 +++++- content/views/LibraryView.qml | 1 + src/components/RawViewport.cpp | 12 ++++----- src/engine/Panorama.cpp | 27 +++++++++++++++---- src/engine/Panorama.h | 10 +++---- src/libraries/tiny_dng_writer_impl.cpp | 2 ++ src/main.cpp | 3 +++ src/managers/AppStateManager.cpp | 12 +-------- src/managers/LogManager.cpp | 11 ++------ src/managers/LogManager.h | 36 ++++++++++++++++++++++++- 14 files changed, 118 insertions(+), 40 deletions(-) create mode 100644 assets/icons/hdr.svg create mode 100644 assets/icons/panorama.svg create mode 100644 src/libraries/tiny_dng_writer_impl.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b2e6622..d7c93de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,8 @@ qt_standard_project_setup(REQUIRES 6.8) qt_add_executable(Photon src/main.cpp + src/libraries/tiny_dng_writer_impl.cpp + src/libraries/tiny_dng_writer.h src/engine/RawEngine.cpp src/engine/RawEngine.h src/engine/Denoiser.cpp @@ -47,7 +49,10 @@ qt_add_executable(Photon src/engine/VulkanComputeContext.cpp src/engine/VulkanComputeContext.h src/engine/patch_search.comp + src/engine/Panorama.cpp + src/engine/Panorama.h src/libraries/tiny_dng_writer.h + src/libraries/tiny_dng_writer_impl.cpp src/components/RawViewport.cpp src/components/RawViewport.h src/components/ToneLutProvider.h @@ -188,6 +193,8 @@ qt_add_qml_module(Photon assets/icons/tube.svg assets/icons/trash.svg assets/icons/download.svg + assets/icons/panorama.svg + assets/icons/hdr.svg assets/icons/folder.svg assets/icons/home.svg assets/icons/eye.svg diff --git a/assets/icons/hdr.svg b/assets/icons/hdr.svg new file mode 100644 index 0000000..e3e7ab0 --- /dev/null +++ b/assets/icons/hdr.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/panorama.svg b/assets/icons/panorama.svg new file mode 100644 index 0000000..c0db581 --- /dev/null +++ b/assets/icons/panorama.svg @@ -0,0 +1 @@ + diff --git a/content/components/PhotoContextMenu.qml b/content/components/PhotoContextMenu.qml index 1ea17d5..bbee616 100644 --- a/content/components/PhotoContextMenu.qml +++ b/content/components/PhotoContextMenu.qml @@ -23,6 +23,7 @@ T.Menu { signal rotateLeftRequested() signal flipHorizontalRequested() signal flipVerticalRequested() + signal createPanoramaRequested() function openAt(x, y) { root.x = x @@ -120,9 +121,33 @@ T.Menu { } T.MenuSeparator { - visible: root.showFilterSection + visible: root.showFilterSection } + T.Menu { + id: mergeMenu + title: "Merge Photos" + enabled: root.selectionCount > 1 + T.MenuItem { + text: "Panorama" + icon.source: "qrc:/Main/assets/icons/panorama.svg" + onTriggered: { + root.createPanoramaRequested() + root.close() + } + } + T.MenuItem { + text: "HDR" + icon.source: "qrc:/Main/assets/icons/hdr.svg" + enabled: false + onTriggered: { + } + } + } + + + T.MenuSeparator {} + T.MenuItem { text: "Rotate right" icon.source: "qrc:/Main/assets/icons/rotate-cw-menu.svg" diff --git a/content/views/App.qml b/content/views/App.qml index 873d206..4ef29ae 100644 --- a/content/views/App.qml +++ b/content/views/App.qml @@ -40,6 +40,12 @@ Window { rawViewport.showSharpenMask = false; } } + Connections { + target: Panorama + function onStitchCompleted(result) { + toaster.show(result.message, result.success ? "info" : "error") + } + } // List model to hold the RAW files ListModel { @@ -61,7 +67,6 @@ Window { selectionCount: AppState.selectionCount canCopy: AppState.selectionCount == 1 canPaste: Object.keys(window.copiedSettings).length > 0 - showFilterSection: true filterOperator: window.ratingOperator filterRating: window.ratingFilter operatorLabels: window.ratingOperatorLabels @@ -78,6 +83,7 @@ Window { onRotateLeftRequested: window.rotateSelectionLeft() onFlipHorizontalRequested: window.flipSelectionHorizontal() onFlipVerticalRequested: window.flipSelectionVertical() + onCreatePanoramaRequested: Panorama.stitchAsync(AppState.selectedImages) } // Function to refresh the file list diff --git a/content/views/LibraryView.qml b/content/views/LibraryView.qml index eacd345..5ea36b9 100644 --- a/content/views/LibraryView.qml +++ b/content/views/LibraryView.qml @@ -48,6 +48,7 @@ Control { onRotateLeftRequested: AppState.rotateSelectedLeft("") onFlipHorizontalRequested: AppState.flipSelectedHorizontal("") onFlipVerticalRequested: AppState.flipSelectedVertical("") + onCreatePanoramaRequested: Panorama.stitchAsync(AppState.selectedImages) } // Function to refresh the file list diff --git a/src/components/RawViewport.cpp b/src/components/RawViewport.cpp index 10458a8..0d81319 100644 --- a/src/components/RawViewport.cpp +++ b/src/components/RawViewport.cpp @@ -398,10 +398,10 @@ RawViewport::RawViewport(QQuickItem* parent) : QQuickItem(parent) { } void RawViewport::setSource(const QString& source) { - LogManager::instance()->log(QString("[ RawViewport ] - setSource START: %1").arg(source), "DEBUG"); + LogManager::instance()->log(QString("[ RawViewport ] - setSource START: %1").arg(source), DEBUG); if (m_engine.source() == source) { - LogManager::instance()->log("[ RawViewport ] - setSource: same source, skipping", "DEBUG"); + LogManager::instance()->log("[ RawViewport ] - setSource: same source, skipping", DEBUG); return; } @@ -417,14 +417,14 @@ void RawViewport::setSource(const QString& source) { // This ensures the old image is removed before the new one loads update(); - LogManager::instance()->log("[ RawViewport ] - setSource: calling m_engine.setSource", "DEBUG"); + LogManager::instance()->log("[ RawViewport ] - setSource: calling m_engine.setSource", DEBUG); m_engine.setSource(source); - LogManager::instance()->log("[ RawViewport ] - setSource: emitting sourceChanged", "DEBUG"); + LogManager::instance()->log("[ RawViewport ] - setSource: emitting sourceChanged", DEBUG); emit sourceChanged(); update(); - LogManager::instance()->log("[ RawViewport ] - setSource END", "DEBUG"); + LogManager::instance()->log("[ RawViewport ] - setSource END", DEBUG); } void RawViewport::setExposure(float ev) { @@ -1139,7 +1139,7 @@ QSGNode* RawViewport::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { .arg(m_zoom, 0, 'f', 3) .arg(m_panOffset.x(), 0, 'f', 2) .arg(m_panOffset.y(), 0, 'f', 2), - "DEBUG"); + DEBUG); } // --- THREAD-SAFE SIGNAL EMISSION --- diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp index e059df7..ae66ced 100644 --- a/src/engine/Panorama.cpp +++ b/src/engine/Panorama.cpp @@ -4,11 +4,20 @@ using namespace photon; void Panorama::stitchAsync(const QStringList& inputFiles) { QThread* thread = QThread::create([this, inputFiles]() { + QString message; + QVariantMap result = stitchPhotos(inputFiles); + QMetaObject::invokeMethod(this, [this, result]() { + emit stitchCompleted(result); + }); }); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + thread->start(); + LogManager::instance()->log(QString("Started panorama stitching thread"), INFO); } -void Panorama::stitchPhotos(const std::vector inputFiles) { +QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles) { + QVariantMap result; if (inputFiles.size() < 2) { LogManager::instance()->log( QString("[ Panorama.cpp ] - Cannot stitch a single image"), @@ -39,8 +48,10 @@ void Panorama::stitchPhotos(const std::vector inputFiles) { if (status != cv::Stitcher::OK) { LogManager::instance()->log( QString("[ Panorama.cpp ] - Stitcher failed!"), - DEBUG); - // emit result + ERROR); + result["message"] = QString("Stitching failed!"); + result["success"] = false; + return result; } cv::Mat panoramaRGB; @@ -87,7 +98,7 @@ void Panorama::stitchPhotos(const std::vector inputFiles) { LogManager::instance()->log( QString("[ Panorama.cpp ] - Saving panorama to %1").arg(filename), - ERROR); + INFO); std::string errMsg; if ( @@ -100,7 +111,13 @@ void Panorama::stitchPhotos(const std::vector inputFiles) { LogManager::instance()->log( QString("[ Panorama.cpp ] - Couldn't write dng file %1: %2").arg(filename, errMsg), ERROR); - // emit failure + result["message"] = QString("Couldn't write dng file %1: %2").arg(filename, errMsg); + result["success"] = false; + return result; } + result["message"] = QString("Panorama create successfully and saved to %1!").arg(filename, errMsg); + result["success"] = true; + return result; + } diff --git a/src/engine/Panorama.h b/src/engine/Panorama.h index 70e9303..c8e1abd 100644 --- a/src/engine/Panorama.h +++ b/src/engine/Panorama.h @@ -9,8 +9,8 @@ #include #include #include "../managers/LogManager.h" -#define TINY_DNG_WRITER_IMPLEMENTATION #include "../libraries/tiny_dng_writer.h" + namespace photon { class Panorama : public QObject { @@ -65,14 +65,12 @@ class Panorama : public QObject { return matBGR; } - void stitchPhotos(const std::vector inputFiles); + QVariantMap stitchPhotos(const QStringList& inputFiles); public: + explicit Panorama(QObject* parent = nullptr) : QObject(parent) {} Q_INVOKABLE void stitchAsync(const QStringList& inputFiles); - explicit Panorama(QObject* parent = nullptr); - ~Panorama() override; - signals: - void stitchCompleted(bool success, const QString& message); + void stitchCompleted(QVariantMap result); }; } diff --git a/src/libraries/tiny_dng_writer_impl.cpp b/src/libraries/tiny_dng_writer_impl.cpp new file mode 100644 index 0000000..48f221e --- /dev/null +++ b/src/libraries/tiny_dng_writer_impl.cpp @@ -0,0 +1,2 @@ +#define TINY_DNG_WRITER_IMPLEMENTATION +#include "tiny_dng_writer.h" diff --git a/src/main.cpp b/src/main.cpp index bd1f215..099ecba 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,7 @@ #include "managers/PreviewManager.h" #include "managers/ThumbnailImageProvider.h" #include "managers/ThumbnailProvider.h" +#include "engine/Panorama.h" #include "components/ToneLutProvider.h" using namespace photon; @@ -170,10 +171,12 @@ int main(int argc, char* argv[]) { qmlRegisterSingletonInstance("Main", 1, 0, "Logger", logManager); auto *keyTracker = new KeyTracker(&app); + auto *panorama = new photon::Panorama(&app); qmlRegisterSingletonInstance("Main", 1, 0, "KeyTracker", keyTracker); qmlRegisterSingletonInstance("Main", 1, 0, "PresetManager", presetManager); qmlRegisterSingletonInstance("Main", 1, 0, "PreviewManager", previewManager); qmlRegisterSingletonInstance("Main", 1, 0, "ExportManager", exportManager); + qmlRegisterSingletonInstance("Main", 1, 0, "Panorama", panorama); qmlRegisterType("Main", 1, 0, "RawViewport"); engine.rootContext()->setContextProperty("thumbnailProvider", thumbProvider); qmlRegisterType("Main", 1, 0, "FileScanner"); diff --git a/src/managers/AppStateManager.cpp b/src/managers/AppStateManager.cpp index 3e8475c..37ccf5e 100644 --- a/src/managers/AppStateManager.cpp +++ b/src/managers/AppStateManager.cpp @@ -585,17 +585,7 @@ void AppStateManager::setLogLocation(const QString& location) { } QString AppStateManager::logLevel() const { - switch (LogManager::instance()->logLevel()) { - case DEBUG: - return "DEBUG"; - case INFO: - return "INFO"; - case WARNING: - return "WARNING"; - case ERROR: - return "ERROR"; - } - return ""; + return LogManager::instance()->logLevel(); } void AppStateManager::setLogLevel(const QString& level) { diff --git a/src/managers/LogManager.cpp b/src/managers/LogManager.cpp index c5b938f..7902c1b 100644 --- a/src/managers/LogManager.cpp +++ b/src/managers/LogManager.cpp @@ -47,14 +47,7 @@ void LogManager::setLogLocation(const QString& location) { void LogManager::setLogLevel(const QString& level) { - if (level == "DEBUG") - setLogLevel(DEBUG); - else if (level == "INFO") - setLogLevel(INFO); - else if (level == "WARNING") - setLogLevel(WARNING); - else if (level == "ERROR") - setLogLevel(ERROR); + setLogLevel(strLevelToEnum(level)); } void LogManager::setLogLevel(int level) { @@ -85,7 +78,7 @@ void LogManager::log(const QString& message, int level) { QTextStream out(&m_logFile); QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz"); - out << QString("[%1] [%2] %3\n").arg(timestamp, QString::number(level), message); + out << QString("[ %1 ] [ %2 ] %3\n").arg(timestamp, enumLevelToStr(level), message); out.flush(); } diff --git a/src/managers/LogManager.h b/src/managers/LogManager.h index 76e41a2..d57f72f 100644 --- a/src/managers/LogManager.h +++ b/src/managers/LogManager.h @@ -34,7 +34,41 @@ class LogManager : public QObject { QString logLocation() const { return m_logLocation; } void setLogLocation(const QString& location); - int logLevel() const { return m_logLevel; } + + LogLevel strLevelToEnum(QString level) const { + if (level == "DEBUG") + return DEBUG; + else if (level == "INFO") + return INFO; + else if (level == "WARNING") + return WARNING; + else if (level == "ERROR") + return ERROR; + + return DEBUG; + } + + QString enumLevelToStr(int level) const { + switch(level) { + case DEBUG: + return "DEBUG"; + + case INFO: + return "INFO"; + + case WARNING: + return "WARNING"; + + case ERROR: + return "ERROR"; + } + return ""; + } + + QString logLevel() const { + return LogManager::enumLevelToStr(m_logLevel); + } + void setLogLevel(int level); void setLogLevel(const QString& level); From e4d85ccf3164e0f77c58e8610ebdf131d4b75905 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Wed, 11 Mar 2026 22:12:38 +0100 Subject: [PATCH 07/29] wip: wired up, stitching fails --- src/engine/Panorama.cpp | 52 ++++++++++++++++++++++++++++++++++++----- src/engine/Panorama.h | 4 ++++ src/main.cpp | 5 ++++ 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp index ae66ced..9c4ed23 100644 --- a/src/engine/Panorama.cpp +++ b/src/engine/Panorama.cpp @@ -16,15 +16,21 @@ void Panorama::stitchAsync(const QStringList& inputFiles) { LogManager::instance()->log(QString("Started panorama stitching thread"), INFO); } + + QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles) { QVariantMap result; if (inputFiles.size() < 2) { LogManager::instance()->log( QString("[ Panorama.cpp ] - Cannot stitch a single image"), ERROR); + + result["message"] = QString("Select more photos!"); + result["success"] = false; + return result; } - std::vector images; + std::vector images, estImgs; for (const auto& filename : inputFiles) { cv::Mat img = raw_to_linear(filename); if (img.empty()) { @@ -34,29 +40,63 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles) { continue; } images.push_back(img); + cv::Mat img32F, img8; + img.convertTo(img32F, CV_32F, 1.0 / 65535.0); + // Apply a 2.2 gamma curve to brighten shadows/midtones so OpenCV can "see" the features + cv::pow(img32F, 1.0 / 2.2, img32F); + img32F.convertTo(img8, CV_8U, 255.0); + estImgs.push_back(img8); } LogManager::instance()->log( QString("[ Panorama.cpp ] - Merging %1 images").arg(images.size()), DEBUG); - cv::Mat panoramaBGR; cv::Ptr stitcher = cv::Stitcher::create(cv::Stitcher::PANORAMA); + stitcher->setExposureCompensator(cv::makePtr()); + cv::Ptr blender = cv::detail::Blender::createDefault(cv::detail::Blender::MULTI_BAND); + stitcher->setBlender(blender); + cv::Stitcher::Status status = stitcher->estimateTransform(estImgs); + if (status != cv::Stitcher::OK) { + std::string errorReason; + switch (status) { + case cv::Stitcher::ERR_NEED_MORE_IMGS: + errorReason = "not enough matching features found. The images may lack contrast, or there is too little overlap between them."; + break; + case cv::Stitcher::ERR_HOMOGRAPHY_EST_FAIL: + errorReason = "failed to align the images. The overlap might be too small or contain moving subjects."; + break; + case cv::Stitcher::ERR_CAMERA_PARAMS_ADJUST_FAIL: + errorReason = "failed to optimize camera parameters. The images might have extreme lens distortion or inconsistent exposure."; + break; + default: + errorReason = "Unknown error occurred (Code: " + std::to_string(int(status)) + ")."; + break; + } + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Stitching failed: %1").arg(errorReason), + ERROR); + result["message"] = QString("Stitching failed: %1").arg(errorReason); + result["success"] = false; + return result; + } - cv::Stitcher::Status status = stitcher->stitch(images, panoramaBGR); + cv::Mat panoramaBGR; + status = stitcher->composePanorama(images, panoramaBGR); if (status != cv::Stitcher::OK) { LogManager::instance()->log( - QString("[ Panorama.cpp ] - Stitcher failed!"), + QString("[ Panorama.cpp ] - Stitching failed during composition!"), ERROR); result["message"] = QString("Stitching failed!"); result["success"] = false; return result; - } + } cv::Mat panoramaRGB; cv::cvtColor(panoramaBGR, panoramaRGB, cv::COLOR_BGR2RGB); + panoramaRGB.convertTo(panoramaRGB, cv::COLOR_16U); tinydngwriter::DNGWriter dngwriter(false); tinydngwriter::DNGImage dngimage; @@ -116,7 +156,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles) { return result; } - result["message"] = QString("Panorama create successfully and saved to %1!").arg(filename, errMsg); + result["message"] = QString("Panorama create successfully and saved to %1!").arg(filename); result["success"] = true; return result; diff --git a/src/engine/Panorama.h b/src/engine/Panorama.h index c8e1abd..0049861 100644 --- a/src/engine/Panorama.h +++ b/src/engine/Panorama.h @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "../managers/LogManager.h" #include "../libraries/tiny_dng_writer.h" @@ -65,7 +67,9 @@ class Panorama : public QObject { return matBGR; } + void estimateTransform(cv::Stitcher& stitcher); QVariantMap stitchPhotos(const QStringList& inputFiles); + public: explicit Panorama(QObject* parent = nullptr) : QObject(parent) {} Q_INVOKABLE void stitchAsync(const QStringList& inputFiles); diff --git a/src/main.cpp b/src/main.cpp index 099ecba..645fded 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include #endif +#include #include #include @@ -43,6 +44,7 @@ typedef void (VKAPI_PTR *PFN_vkDestroyInstance_t)(VkInstance, const VkAllocationCallbacks*); int main(int argc, char* argv[]) { + // Enable RHI info and Vulkan logging // qputenv("QSG_INFO", "1"); // qputenv("QSG_RHI_DEBUG", "1"); @@ -141,6 +143,9 @@ int main(int argc, char* argv[]) { QGuiApplication app(argc, argv); + // Needed by OpenCL during panorama stitching!! + std::setlocale(LC_NUMERIC, "C"); + QVulkanInstance vulkanInstance; vulkanInstance.setLayers({}); if (!vulkanInstance.create()) { From 8a04095696ce6d9e7c8e85f9a02cb47e90a89c87 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Mon, 16 Mar 2026 18:44:51 +0100 Subject: [PATCH 08/29] wip: the panorama works, but the dng is not compatible with libraw --- .github/workflows/build.yml | 4 +- CMakeLists.txt | 10 +- panorama_report.md | 782 +++++++ src/engine/Panorama.cpp | 564 +++++- src/engine/Panorama.h | 64 +- src/libraries/tiny_dng_writer.h | 2583 ------------------------ src/libraries/tiny_dng_writer_impl.cpp | 2 - src/managers/LogManager.cpp | 3 + src/managers/LogManager.h | 8 +- 9 files changed, 1270 insertions(+), 2750 deletions(-) create mode 100644 panorama_report.md delete mode 100644 src/libraries/tiny_dng_writer.h delete mode 100644 src/libraries/tiny_dng_writer_impl.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c03432..6812417 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,7 @@ jobs: libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-shape0 \ libwayland-client0 libwayland-cursor0 libwayland-egl1 libxkbcommon-x11-0 \ libwayland-dev wayland-protocols libwayland-server0 libopencv-dev \ - imagemagick + imagemagick libtiff-dev wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage @@ -68,7 +68,7 @@ jobs: - name: Install Dependencies (Windows) if: runner.os == 'Windows' run: | - vcpkg install libraw:x64-windows vulkan:x64-windows opencv4:x64-windows + vcpkg install libraw:x64-windows vulkan:x64-windows opencv4:x64-windows tiff:x64-windows - name: Configure CMake shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index d7c93de..72ebf60 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,20 +22,20 @@ if(WIN32) # Use find_package for vcpkg compatibility on Windows find_package(LibRaw REQUIRED) find_package(OpenCV REQUIRED) + find_package(TIFF REQUIRED) add_compile_definitions(WIN32_LEAN_AND_MEAN) add_compile_definitions(NOMINMAX) else() find_package(PkgConfig REQUIRED) pkg_check_modules(LIBRAW REQUIRED libraw) pkg_check_modules(OPENCV REQUIRED opencv4) + pkg_check_modules(TIFF REQUIRED libtiff-4) endif() -qt_standard_project_setup(REQUIRES 6.8) +qt_standard_project_setup(REQUIRES 6.10) qt_add_executable(Photon src/main.cpp - src/libraries/tiny_dng_writer_impl.cpp - src/libraries/tiny_dng_writer.h src/engine/RawEngine.cpp src/engine/RawEngine.h src/engine/Denoiser.cpp @@ -51,8 +51,6 @@ qt_add_executable(Photon src/engine/patch_search.comp src/engine/Panorama.cpp src/engine/Panorama.h - src/libraries/tiny_dng_writer.h - src/libraries/tiny_dng_writer_impl.cpp src/components/RawViewport.cpp src/components/RawViewport.h src/components/ToneLutProvider.h @@ -221,6 +219,7 @@ if(WIN32) PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate PRIVATE ${LibRaw_LIBRARIES} PRIVATE ${OpenCV_LIBRARIES} + PRIVATE ${TIFF_LIBRARIES} PRIVATE Vulkan::Vulkan ) else() @@ -228,6 +227,7 @@ else() PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate PRIVATE ${LIBRAW_LIBRARIES} PRIVATE ${OPENCV_LIBRARIES} + PRIVATE ${TIFF_LIBRARIES} PRIVATE Vulkan::Vulkan ) endif() diff --git a/panorama_report.md b/panorama_report.md new file mode 100644 index 0000000..ff445e5 --- /dev/null +++ b/panorama_report.md @@ -0,0 +1,782 @@ +# Panorama Stitching Technical Report + +## Executive Summary + +This report analyzes the panorama stitching implementation found in RapidRAW, a Rust-based RAW image processing application. The system implements a sophisticated feature-based stitching pipeline that combines classical computer vision algorithms with modern optimizations including parallel processing and adaptive seam finding. + +--- + +## Phase 1: Image Loading and Preprocessing + +### High-Level Overview + +The first phase converts RAW images into a processable format while preparing them for computationally expensive feature detection operations through intelligent downscaling. + +### Algorithmic Details + +**RAW Processing Pipeline:** + +```rust +let mut dynamic_image = load_base_image_from_bytes(&file_bytes, filename, false, 2.5, None)?; +if is_raw_file(filename) { + apply_cpu_default_raw_processing(&mut dynamic_image); +} +let image_f32 = dynamic_image.to_rgb32f(); +``` + +1. **RAW Decoding**: Uses internal RAW loader with exposure boost (2.5x) +2. **CPU Demosaicing**: Applies default RAW processing pipeline (likely using DCraw or similar) +3. **Format Conversion**: Converts to RGB32F (32-bit floating point per channel) for high dynamic range preservation + +**Adaptive Downscaling Strategy:** + +```rust +const MAX_PROCESSING_DIMENSION: u32 = 1600; + +pub fn calculate_downscale_dimensions(width: u32, height: u32) -> (u32, u32, f64) { + let long_side = width.max(height); + if long_side <= MAX_PROCESSING_DIMENSION { + return (width, height, 1.0); + } + let scale_factor = long_side as f64 / MAX_PROCESSING_DIMENSION as f64; + let new_width = (width as f64 / scale_factor).round() as u32; + let new_height = (height as f64 / scale_factor).round() as u32; + (new_width, new_height, scale_factor) +} +``` + +This ensures feature detection operates on manageable 1600px images regardless of input resolution, dramatically improving performance while maintaining sufficient detail for matching. + +**Low-Detail Mask Generation:** +A crucial preprocessing step identifies low-detail regions (sky, walls) using integral images for variance computation: + +```rust +pub fn generate_low_detail_mask(gray_full: &GrayImage) -> GrayImage { + let (sat, sat_sq) = build_integral_images(gray_full); + // Uses 32x32 windows, threshold = 60.0 variance +} +``` + +The algorithm: + +1. Computes Summed Area Table (SAT) for mean calculation: `SAT(x,y) = Σ_{i≤x, j≤y} I(i,j)` +2. Computes Squared SAT for variance: `Var = E[X²] - (E[X])²` +3. Marks pixels with variance < 60 as low-detail (mask value = 255) + +This mask is later used during blending to expand feathering widths in low-detail regions, preventing visible seams in smooth areas. + +--- + +## Phase 2: Feature Detection (FAST + BRIEF) + +### High-Level Overview + +The system employs a FAST corner detector combined with BRIEF descriptors—a computationally efficient alternative to SIFT/SURF that enables real-time performance without GPU acceleration. + +### Algorithmic Details + +**Step 2.1: Gaussian Preprocessing** + +```rust +let blurred_img_u8 = imageproc::filter::gaussian_blur_f32(img, 1.5); +let corners = corners_fast9(&blurred_img_u8, FAST_THRESHOLD); +``` + +**FAST9 Corner Detection:** + +- **Threshold**: 15 (intensity difference threshold) +- **Mechanism**: Checks 16 pixels on Bresenham circle around candidate pixel +- **Corner condition**: 9+ consecutive pixels with intensity > p + threshold OR < p - threshold +- **Complexity**: O(N) where N = number of pixels + +**Step 2.2: Non-Maximal Suppression** + +```rust +fn non_maximal_suppression(corners: &[Corner], radius: f32) -> Vec { + let radius_sq = radius * radius; // 15.0 pixels + // Sorts by corner score, suppresses neighbors within radius +} +``` + +Prevents feature clustering by keeping only the strongest corner in each 15-pixel radius neighborhood. + +**Step 2.3: BRIEF Descriptor Computation** + +```rust +pub const BRIEF_DESCRIPTOR_SIZE: usize = 256; +pub type Descriptor = [u8; BRIEF_DESCRIPTOR_SIZE / 8]; // 32 bytes +``` + +The BRIEF (Binary Robust Independent Elementary Features) algorithm: + +1. **Patch Extraction**: 32×32 window around each keypoint (smoothed with σ=2.0 Gaussian) +2. **Binary Tests**: Compares 256 pre-generated random pixel pairs within the patch +3. **Bit Packing**: Each test result becomes one bit in a 32-byte descriptor + +```rust +fn compute_brief_descriptor(img: &ImageBuffer, Vec>, + kp: &KeyPoint, patch_size: u32, + pairs: &[(Point2, Point2)]) -> Option { + for (i, pair) in pairs.iter().enumerate() { + let p1_x = (kp.x as i32 + pair.0.x) as u32; + let p1_y = (kp.y as i32 + pair.0.y) as u32; + let p2_x = (kp.x as i32 + pair.1.x) as u32; + let p2_y = (kp.y as i32 + pair.1.y) as u32; + + if img[p1_x, p1_y] < img[p2_x, p2_y] { + descriptor[byte_index] |= 1 << bit_index; + } + } +} +``` + +**Random Pair Generation (Deterministic):** + +```rust +pub fn generate_brief_pairs() -> Vec<(Point2, Point2)> { + let mut rng = StdRng::seed_from_u64(12345); // Fixed seed for reproducibility + let half_patch = BRIEF_PATCH_SIZE as i32 / 2; // 16 + // Generates 256 pairs from Uniform(-16, 16) distribution +} +``` + +**Advantages of FAST+BRIEF:** + +- **Speed**: Binary comparisons using XOR + popcount (CPU SIMD optimized) +- **Memory**: 32 bytes vs 128 bytes for SIFT +- **Rotation Invariance**: Limited (ORB improves this with orientation) +- **Scale Invariance**: Achieved through image pyramid (not implemented here) + +--- + +## Phase 3: Feature Matching with Ratio Test + +### High-Level Overview + +Features are matched between image pairs using Hamming distance, filtered by Lowe's ratio test to reject ambiguous matches. + +### Algorithmic Details + +**Hamming Distance Calculation:** + +```rust +fn hamming_distance(d1: &Descriptor, d2: &Descriptor) -> u32 { + d1.iter() + .zip(d2.iter()) + .map(|(b1, b2)| (b1 ^ b2).count_ones()) + .sum() +} +``` + +Computes population count (number of differing bits) between two 256-bit descriptors using CPU popcount instructions. + +**Brute-Force Matching with Ratio Test:** + +```rust +const MATCH_RATIO_THRESHOLD: f32 = 0.8; + +pub fn match_features(features1: &[Feature], features2: &[Feature]) -> Vec { + features1.par_iter().enumerate().filter_map(|(i, f1)| { + let mut best_dist = u32::MAX; + let mut second_best_dist = u32::MAX; + let mut best_idx = 0; + + for (j, f2) in features2.iter().enumerate() { + let dist = hamming_distance(&f1.descriptor, &f2.descriptor); + if dist < best_dist { + second_best_dist = best_dist; + best_dist = dist; + best_idx = j; + } else if dist < second_best_dist { + second_best_dist = dist; + } + } + + // Lowe's ratio test: best must be significantly better than second-best + if second_best_dist > 0 && + (best_dist as f32 / second_best_dist as f32) < MATCH_RATIO_THRESHOLD { + Some(Match { index1: i, index2: best_idx }) + } else { + None + } + }).collect() +} +``` + +**Lowe's Ratio Test (2004):** + +- Rejects matches where the best match is not significantly better than the second-best +- Threshold of 0.8 means best distance must be < 80% of second-best +- Effectively eliminates matches to repetitive patterns and ambiguous features + +**Computational Complexity:** O(N×M) for N features in image 1, M in image 2 + +- Parallelized using Rayon for multi-threading +- Could benefit from k-d tree or FLANN for large feature sets + +--- + +## Phase 4: Geometric Verification with RANSAC + +### High-Level Overview + +Matches are geometrically validated using RANSAC to estimate a homography transformation, filtering out outliers caused by moving objects, parallax, or mismatches. + +### Algorithmic Details + +**Random Sample Consensus (RANSAC) Loop:** + +```rust +const RANSAC_ITERATIONS: usize = 2500; +const RANSAC_INLIER_THRESHOLD: f64 = 5.0; // pixels +const MIN_INLIERS_FOR_CONNECTION: usize = 15; +``` + +**Algorithm Steps:** + +1. **Random Sampling**: Select 4 random match pairs (minimum for homography) +2. **Collinearity Check**: Reject degenerate configurations + + ```rust + fn are_points_collinear(p1: Point2, p2: Point2, p3: Point2) -> bool { + let area = p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y); + area.abs() < 1e-6 + } + ``` + +3. **Homography Estimation**: Solve 8-DOF transformation using DLT (Direct Linear Transform) + +**Direct Linear Transform (DLT):** + +```rust +pub fn compute_homography(points: &[(Point2, Point2)]) -> Option> { + // For N point pairs, builds 2N × 9 matrix A + // Each point pair contributes 2 rows: + // [-x, -y, -1, 0, 0, 0, x*x', y*x', x'] + // [0, 0, 0, -x, -y, -1, x*y', y*y', y'] + + let a = nalgebra::DMatrix::from_rows(&a_rows); + let svd = SVD::new(a, true, true); + let v_t = svd.v_t.expect("SVD failed to compute V_t"); + let h_vec = v_t.row(v_t.nrows() - 1).transpose(); + Some(Matrix3::from_iterator(h_vec.iter().cloned()).transpose()) +} +``` + +**Homography Matrix H (3×3):** + +``` +[x'] [h11 h12 h13] [x] +[y'] = [h21 h22 h23] [y] +[1 ] [h31 h32 h33] [1] +``` + +Maps points from image 1 to image 2 coordinates using projective transformation (preserves lines but not necessarily parallelism). + +1. **Inlier Counting**: Transform all points and count matches within threshold + + ```rust + let p2_transformed = Point2::new( + p2_h_transformed.x / p2_h_transformed.z, + p2_transformed.y / p2_h_transformed.z, + ); + let dist_sq = (p2.x - p2_transformed.x).powi(2) + (p2.y - p2_transformed.y).powi(2); + if dist_sq < ransac_inlier_threshold_sq { /* inlier */ } + ``` + +2. **Refinement**: Recompute homography using all inliers (more stable than single 4-point sample) + +**Scale Compensation:** +Since feature detection runs on downscaled images, the homography is scaled back to full resolution: + +```rust +let s1 = image_data[i].scale_factor; +let s2 = image_data[j].scale_factor; +let scale_mat_i_inv = Matrix3::new(1.0/s1, 0.0, 0.0, 0.0, 1.0/s1, 0.0, 0.0, 0.0, 1.0); +let scale_mat_j = Matrix3::new(s2, 0.0, 0.0, 0.0, s2, 0.0, 0.0, 0.0, 1.0); +let h_full = scale_mat_j * h_refined * scale_mat_i_inv; +``` + +--- + +## Phase 5: Graph-Based Image Ordering + +### High-Level Overview + +Images are organized into a Minimum Spanning Tree (MST) to determine optimal stitching order and compute global coordinate transformations. + +### Algorithmic Details + +**Disjoint Set Union (Union-Find) Structure:** + +```rust +struct DSU { + parent: Vec, +} + +impl DSU { + fn find(&mut self, i: usize) -> usize { + if self.parent[i] == i { i } + else { + self.parent[i] = self.find(self.parent[i]); // Path compression + self.parent[i] + } + } + + fn union(&mut self, i: usize, j: usize) { + let root_i = self.find(i); + let root_j = self.find(j); + if root_i != root_j { self.parent[root_i] = root_j; } + } +} +``` + +**Kruskal's MST Algorithm:** + +```rust +let mut edges = Vec::new(); +for (&(i, j), m) in matches { + edges.push((m.inliers, i, j)); +} +edges.sort_by_key(|&(inliers, _, _)| std::cmp::Reverse(inliers)); + +let mut dsu = DSU::new(n); +for &(_, i, j) in &edges { + if dsu.find(i) != dsu.find(j) { + dsu.union(i, j); + mst_adj.entry(i).or_default().push(j); + mst_adj.entry(j).or_default().push(i); + num_edges += 1; + if num_edges == n - 1 { break; } + } +} +``` + +Prioritizes connections with more inliers (more confident matches). + +**Breadth-First Traversal for Global Homographies:** + +```rust +let mut q = VecDeque::new(); +q.push_back((start_node, Matrix3::identity())); + +while let Some((u, h_u_global)) = q.pop_front() { + ordered_indices.push(u); + global_homographies.insert(u, h_u_global); + + for &v in neighbors { + if !visited.contains(&v) { + let h_vu = if let Some(m) = matches.get(&(v, u)) { + m.homography + } else { + matches.get(&(u, v)).unwrap().homography.try_inverse().unwrap() + }; + let h_v_global = h_u_global * h_vu; + q.push_back((v, h_v_global)); + } + } +} +``` + +Chains homographies through the MST to compute each image's transformation into the global panorama coordinate system. + +--- + +## Phase 6: Progressive Seam-Based Stitching + +### High-Level Overview + +The final blending phase uses dynamic programming to find optimal seams between overlapping images, with adaptive feathering based on local image content. + +### Algorithmic Details + +**Canvas Size Computation:** + +```rust +for &img_info in images { + let h = global_homographies[&img_info.id]; + let corners = [ + Point3::new(0.0, 0.0, 1.0), + Point3::new(w as f64, 0.0, 1.0), + Point3::new(w as f64, h_img as f64, 1.0), + Point3::new(0.0, h_img as f64, 1.0), + ]; + for p in corners.iter() { + let tp = h * p; + let tx = tp.x / tp.z; + let ty = tp.y / tp.z; + // Update bounds + } +} +``` + +Transforms all image corners to determine output canvas dimensions. + +**Dynamic Programming Seam Finding:** + +**Vertical Seam (for horizontally overlapping images):** + +```rust +fn find_pairwise_seam_dp_vertical(...) -> Vec { + // Build cost matrix: energy = color difference between images at overlap + for y_out in 0..out_height as usize { + for x_out in 0..out_width as usize { + if overlap_exists { + cost_matrix[y_out][x_out] = sqrt(ΔR² + ΔG² + ΔB²); + } + } + } + + // Dynamic programming: accumulate minimum cost paths + for y in (first_overlap_row + 1)..=last_overlap_row { + for x in 0..out_width as usize { + let up_left = if x > 0 { cost_matrix[y - 1][x - 1] } else { INF }; + let up = cost_matrix[y - 1][x]; + let up_right = if x < width-1 { cost_matrix[y - 1][x + 1] } else { INF }; + + cost_matrix[y][x] += up.min(up_left).min(up_right); + path_matrix[y][x] = direction_of_min_cost; + } + } + + // Backtrack from minimum cost endpoint + let mut seam = vec![0i32; out_height as usize]; + // ...trace path through path_matrix +} +``` + +**Energy Function:** Euclidean distance in RGB space between corresponding pixels from the two images at each overlap location. + +**Adaptive Blending with Feathering:** + +```rust +const FEATHER_WIDTH: f64 = 100.0; + +if dist_to_seam.abs() < dynamic_feather_width / 2.0 { + // Cosine-smoothed alpha blending + let alpha = if new_image_is_dominant_side { + (dist_to_seam + dynamic_feather_width / 2.0) / dynamic_feather_width + } else { + (-dist_to_seam + dynamic_feather_width / 2.0) / dynamic_feather_width + }; + + let weight_add = (1.0 - (alpha.clamp(0.0, 1.0) * PI).cos()) / 2.0; + let weight_pano = 1.0 - weight_add; + + // Blend colors + final_color = pano_color * weight_pano + add_color * weight_add; +} +``` + +**Content-Adaptive Feathering:** + +```rust +let is_low_detail = low_detail_mask_add.get_pixel(sx_u, sy_u)[0] > 0; +let dynamic_feather_width = if is_low_detail { FEATHER_WIDTH * 5.0 } else { FEATHER_WIDTH }; +``` + +Low-detail regions (sky, walls) receive 5x wider feathering (500px) to prevent visible seams in smooth gradients. + +**Bilinear Interpolation:** + +```rust +fn get_interpolated_pixel(img: &Rgb32FImage, x: f64, y: f64) -> Rgb { + let x_floor = x.floor() as u32; + let y_floor = y.floor() as u32; + let dx = x - x_floor as f64; + let dy = y - y_floor as f64; + + // Sample 4 neighbors and interpolate + let top = p00 * (1.0 - dx) + p10 * dx; + let bottom = p01 * (1.0 - dx) + p11 * dx; + top * (1.0 - dy) + bottom * dy +} +``` + +Prevents aliasing when sampling from transformed coordinates. + +--- + +## C++ Implementation Recommendations + +### Recommended Libraries + +**1. Matrix and Linear Algebra: Eigen3** + +```cpp +#include + +using Matrix3d = Eigen::Matrix; +using Vector3d = Eigen::Matrix; + +// Homography computation +Matrix3d computeHomography(const std::vector>& points) { + Eigen::MatrixXd A(points.size() * 2, 9); + // Fill A matrix with DLT equations + Eigen::JacobiSVD svd(A, Eigen::ComputeFullV); + VectorXd h = svd.matrixV().col(8); + return Eigen::Map(h.data()).transpose(); +} +``` + +**2. Image Processing: OpenCV (cv::Mat)** + +```cpp +#include + +// Gaussian blur +cv::Mat blurred; +cv::GaussianBlur(input, blurred, cv::Size(0, 0), 1.5); + +// Warp perspective +cv::Mat warped; +cv::warpPerspective(src, warped, homography, cv::Size(width, height)); +``` + +**3. Parallelism: Intel TBB or C++17 Parallel Algorithms** + +```cpp +#include +#include + +// Parallel for_each +std::for_each(std::execution::par_unseq, + features.begin(), features.end(), + [&](const Feature& f) { + // Process feature + }); +``` + +**4. Random Number Generation: STL ** + +```cpp +#include + +std::mt19937_64 rng(12345); // Fixed seed like Rust version +std::uniform_int_distribution dist(-16, 16); +``` + +### Implementation Structure + +```cpp +// Core data structures +struct KeyPoint { + uint32_t x, y; +}; + +using Descriptor = std::array; // 256 bits + +struct Feature { + KeyPoint kp; + Descriptor desc; +}; + +struct Match { + size_t idx1, idx2; + float distance; +}; + +// Feature detector class +class FASTBriefDetector { +public: + std::vector detect(const cv::Mat& grayImage); + +private: + std::vector> briefPairs_; + static constexpr int FAST_THRESHOLD = 15; + static constexpr float NMS_RADIUS = 15.0f; + static constexpr int PATCH_SIZE = 32; + static constexpr int DESC_SIZE = 256; + + void generateBriefPairs(); + std::vector fastDetect(const cv::Mat& img); + void nonMaximalSuppression(std::vector& keypoints); + Descriptor computeBrief(const cv::Mat& img, const KeyPoint& kp); +}; + +// RANSAC homography estimator +class RansacHomography { +public: + struct Result { + Eigen::Matrix3d H; + std::vector inliers; + }; + + std::optional estimate(const std::vector& matches, + const std::vector& kp1, + const std::vector& kp2); + +private: + static constexpr int ITERATIONS = 2500; + static constexpr double INLIER_THRESHOLD = 5.0; + static constexpr int MIN_INLIERS = 15; +}; + +// Stitcher with seam finding +class PanoramaStitcher { +public: + cv::Mat stitch(const std::vector& images, + const std::vector& homographies); + +private: + struct SeamInfo { + enum Orientation { VERTICAL, HORIZONTAL }; + Orientation orient; + std::vector coords; + bool newImageDominant; + }; + + std::optional findAdaptiveSeam(...); + std::vector findVerticalSeamDP(...); + std::vector findHorizontalSeamDP(...); + void blendImages(cv::Mat& panorama, const cv::Mat& newImage, + const SeamInfo& seam); +}; +``` + +--- + +## OpenCV Building Blocks Evaluation + +### Available OpenCV Functions and Their Equivalents + +| Rust Implementation | OpenCV Equivalent | Function | Notes | +| ------------------------- | ------------------------------------------- | ----------- | -------------------------------------- | +| `gaussian_blur_f32` | `cv::GaussianBlur` | ✓ Available | Native implementation, supports CV_32F | +| `corners_fast9` | `cv::FastFeatureDetector` | ✓ Available | Use `TYPE_9_16` for 9/16 pixel test | +| `non_maximal_suppression` | Built into FAST | ⚠️ Partial | OpenCV's FAST includes NMS | +| BRIEF descriptor | `cv::xfeatures2d::BriefDescriptorExtractor` | ✓ Available | In contrib module | +| Hamming distance | `cv::NORM_HAMMING` | ✓ Available | Optimized POPCNT instruction | +| `match_features` | `cv::BFMatcher` | ✓ Available | Supports ratio test natively | +| `compute_homography` | `cv::findHomography` | ✓ Available | Built-in RANSAC support | +| `find_homography_ransac` | `cv::findHomography` with `RANSAC` | ✓ Available | Single function call | +| `warpPerspective` | `cv::warpPerspective` | ✓ Available | Supports various interpolation modes | +| `get_interpolated_pixel` | `cv::remap` or `cv::warpPerspective` | ✓ Available | Prefer warp for whole image | +| Seam finding (DP) | Not directly available | ✗ Custom | OpenCV has graph cut but not DP seam | +| Multi-band blending | `cv::detail::MultiBandBlender` | ✓ Available | Advanced blending in stitching module | +| Exposure compensation | `cv::detail::ExposureCompensator` | ✓ Available | For exposure differences | + +### Recommended OpenCV Pipeline + +```cpp +#include +#include +#include + +class OpenCVPanoramaStitcher { +public: + cv::Mat stitch(const std::vector& images) { + // Method 1: Use OpenCV's built-in Stitcher (simpler but less control) + cv::Ptr stitcher = cv::Stitcher::create(); + cv::Mat result; + stitcher->stitch(images, result); + return result; + + // Method 2: Custom pipeline with OpenCV primitives + // (More control over individual steps) + } + + cv::Mat customStitch(const std::vector& images) { + // 1. Detect FAST + BRIEF + cv::Ptr fast = + cv::FastFeatureDetector::create(15, true); // threshold, nonmaxSuppression + cv::Ptr brief = + cv::xfeatures2d::BriefDescriptorExtractor::create(32); // bytes + + std::vector keypoints; + cv::Mat descriptors; + fast->detect(image, keypoints); + brief->compute(image, keypoints, descriptors); + + // 2. Match with BF + Hamming + Ratio test + cv::BFMatcher matcher(cv::NORM_HAMMING); + std::vector> knnMatches; + matcher.knnMatch(desc1, desc2, knnMatches, 2); + + std::vector goodMatches; + for (auto& m : knnMatches) { + if (m[0].distance < 0.8 * m[1].distance) { + goodMatches.push_back(m[0]); + } + } + + // 3. Find homography with RANSAC + std::vector srcPoints, dstPoints; + // ... extract points from matches + cv::Mat H = cv::findHomography(srcPoints, dstPoints, cv::RANSAC, 5.0); + + // 4. Warp and blend + cv::Mat warped; + cv::warpPerspective(src, warped, H, cv::Size(width, height)); + + // 5. Custom seam finding (DP-based) remains manual + // OpenCV's graph cut seam finder: cv::detail::GraphCutSeamFinder + } +}; +``` + +### OpenCV Stitcher Module Comparison + +The `cv::Stitcher` class implements a complete pipeline: + +- **Feature detection**: SURF (default), ORB, AKAZE +- **Matching**: FLANN or Brute-force +- **Homography estimation**: RANSAC +- **Bundle adjustment**: Optimizes all homographies jointly +- **Wave correction**: Reduces wavy artifacts +- **Exposure compensation**: Multi-band blending +- **Seam finding**: Graph cut or Voronoi +- **Blending**: Multi-band or feathering + +**Advantages of OpenCV's implementation:** + +- Optimized assembly/SIMD kernels +- GPU acceleration (CUDA) available +- Well-tested, handles edge cases +- Automatic camera parameter estimation + +**Advantages of Custom Implementation:** + +- Full control over each step +- Custom FAST9 + BRIEF for speed +- Content-adaptive feathering +- Better handling of specific use cases +- Easier to tune for specific hardware + +### Performance Considerations + +OpenCV implementations are generally faster due to: + +1. **Intel IPP integration**: Hand-optimized image processing primitives +2. **OpenCL/CUDA support**: GPU acceleration for warp/blend operations +3. **SIMD optimization**: SSE/AVX instructions for pixel operations +4. **Memory pooling**: Reduced allocation overhead + +However, the custom Rust implementation offers: + +1. **Parallelism**: Rayon provides excellent data parallelism +2. **Zero-cost abstractions**: Rust's optimization produces efficient code +3. **Memory safety**: No undefined behavior or memory leaks +4. **Determinism**: Fixed random seeds for reproducibility + +--- + +## Summary + +The RapidRAW panorama stitcher implements a sophisticated multi-phase pipeline that combines: + +1. **FAST9 corner detection** for efficient feature localization +2. **BRIEF binary descriptors** for fast matching with Hamming distance +3. **RANSAC geometric verification** for robust homography estimation +4. **MST-based image ordering** for optimal global alignment +5. **Dynamic programming seam finding** for minimal energy seams +6. **Content-adaptive feathering** for smooth blending + +The C++ port should leverage: + +- **Eigen3** for matrix operations +- **OpenCV** for image processing primitives +- **Custom implementations** for BRIEF (or use contrib module) and DP seam finding +- **Intel TBB** for parallelism + +OpenCV provides excellent building blocks for most operations, particularly `cv::findHomography` with RANSAC and the Stitcher module for quick prototyping. However, the content-adaptive seam finding and blending strategy in RapidRAW provides superior quality for challenging panoramas and is worth preserving in a C++ port. diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp index 9c4ed23..dad2df3 100644 --- a/src/engine/Panorama.cpp +++ b/src/engine/Panorama.cpp @@ -1,163 +1,511 @@ #include "Panorama.h" +#include "../managers/LogManager.h" + using namespace photon; -void Panorama::stitchAsync(const QStringList& inputFiles) { - QThread* thread = QThread::create([this, inputFiles]() { - QString message; - QVariantMap result = stitchPhotos(inputFiles); +static ColorInfo extractColorInfo(LibRaw *processor) { + ColorInfo info; + + // cam_xyz is [4][3]: camera-RGB → XYZ D50 + // Only first 3 rows (R,G,B) are needed; row 3 is an unused 4th channel + double cam2xyz[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + cam2xyz[i][j] = processor->imgdata.color.cam_xyz[i][j]; + + + cv::Mat C(3, 3, CV_64F, cam2xyz); + // Not sure if the inverse is neede here + // cv::Mat Cinv = C.inv(); // now XYZ D50 → camera: this is ColorMatrix1 + + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + info.matrix[i * 3 + j] = C.at(i, j); + + // WB is baked in (use_auto_wb=1), so tell DNG "no further WB needed" + info.asShotNeutral[0] = 1.0f; + info.asShotNeutral[1] = 1.0f; + info.asShotNeutral[2] = 1.0f; + + + return info; +} - QMetaObject::invokeMethod(this, [this, result]() { - emit stitchCompleted(result); - }); +void Panorama::stitchAsync(const QStringList& inputFiles, + bool compensateExposure) { + QThread* thread = QThread::create([this, inputFiles, compensateExposure]() { + QVariantMap result = stitchPhotos(inputFiles, compensateExposure); + QMetaObject::invokeMethod( + this, [this, result]() { emit stitchCompleted(result); }); }); connect(thread, &QThread::finished, thread, &QObject::deleteLater); thread->start(); - LogManager::instance()->log(QString("Started panorama stitching thread"), INFO); + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Started panorama stitching thread"), INFO); } +cv::Mat Panorama::raw_to_linear(const QString& file, std::unique_ptr& colorInfo) { + LibRaw processor; + processor.imgdata.params.output_bps = 16; + processor.imgdata.params.no_auto_bright = 1; + processor.imgdata.params.use_camera_wb = 1; + processor.imgdata.params.output_color = 0; + processor.imgdata.params.use_camera_matrix = 1; + if (processor.open_file(file.toStdString().c_str()) != LIBRAW_SUCCESS) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Cannot open file %1").arg(file), ERROR); + return cv::Mat(); + } + + if (processor.unpack() != LIBRAW_SUCCESS) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Cannot unpack data of file %1").arg(file), + ERROR); + return cv::Mat(); + } + if (!colorInfo.get()) { + colorInfo = std::make_unique (ColorInfo (extractColorInfo(&processor))); + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Initialized ColorInfo on %1").arg(file), + DEBUG); + } + + if (processor.dcraw_process() != LIBRAW_SUCCESS) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Cannot dcraw file %1").arg(file), ERROR); + return cv::Mat(); + } + + libraw_processed_image_t* image = processor.dcraw_make_mem_image(); + + if (!image) { + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Cannot create processed image from %1") + .arg(file), + ERROR); + return cv::Mat(); + } + + cv::Mat rawRGB(image->height, image->width, CV_16UC3, image->data); + cv::Mat matBGR; + cv::cvtColor(rawRGB, matBGR, cv::COLOR_RGB2BGR); + LibRaw::dcraw_clear_mem(image); + return matBGR; +} -QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles) { +QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, + bool compensateExposure, size_t featuresThreshold) { + cv::ocl::setUseOpenCL(true); QVariantMap result; + if (inputFiles.size() < 2) { LogManager::instance()->log( - QString("[ Panorama.cpp ] - Cannot stitch a single image"), - ERROR); - + "[ Panorama.cpp ] - Cannot stitch a single image", ERROR); result["message"] = QString("Select more photos!"); result["success"] = false; return result; } - std::vector images, estImgs; + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Loading %1 images").arg(inputFiles.size()), + INFO); + + std::unique_ptr colorInfo; + + // These are the 16 bit images loaded from the camera + std::vector images16; for (const auto& filename : inputFiles) { - cv::Mat img = raw_to_linear(filename); + cv::Mat img = Panorama::raw_to_linear(filename, colorInfo); if (img.empty()) { LogManager::instance()->log( - QString("[ Panorama.cpp ] - Failed to process image %1").arg(filename), - ERROR); + QString("[ Panorama.cpp ] - Failed to process image %1") + .arg(filename), + ERROR); continue; } - images.push_back(img); + images16.push_back(img.clone()); + } + + if (images16.size() < 2) { + LogManager::instance()->log( + "[ Panorama.cpp ] - Not enough valid images to stitch", ERROR); + result["message"] = QString("Not enough valid images to stitch"); + result["success"] = false; + return result; + } + + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Processing %1 images").arg(images16.size()), + DEBUG); + + // Create 8-bit gamma-corrected images for feature detection + // Feature detectors require 8-bit input + std::vector images8bit; + std::vector sizes; + for (const auto& img16 : images16) { cv::Mat img32F, img8; - img.convertTo(img32F, CV_32F, 1.0 / 65535.0); - // Apply a 2.2 gamma curve to brighten shadows/midtones so OpenCV can "see" the features + img16.convertTo(img32F, CV_32FC3, 1.0 / 65535.0); cv::pow(img32F, 1.0 / 2.2, img32F); - img32F.convertTo(img8, CV_8U, 255.0); - estImgs.push_back(img8); + img32F.convertTo(img8, CV_8UC3, 255.0); + images8bit.push_back(img8); + sizes.push_back(img8.size()); } - LogManager::instance()->log( - QString("[ Panorama.cpp ] - Merging %1 images").arg(images.size()), - DEBUG); - - cv::Ptr stitcher = cv::Stitcher::create(cv::Stitcher::PANORAMA); - stitcher->setExposureCompensator(cv::makePtr()); - cv::Ptr blender = cv::detail::Blender::createDefault(cv::detail::Blender::MULTI_BAND); - stitcher->setBlender(blender); - cv::Stitcher::Status status = stitcher->estimateTransform(estImgs); - if (status != cv::Stitcher::OK) { - std::string errorReason; - switch (status) { - case cv::Stitcher::ERR_NEED_MORE_IMGS: - errorReason = "not enough matching features found. The images may lack contrast, or there is too little overlap between them."; - break; - case cv::Stitcher::ERR_HOMOGRAPHY_EST_FAIL: - errorReason = "failed to align the images. The overlap might be too small or contain moving subjects."; - break; - case cv::Stitcher::ERR_CAMERA_PARAMS_ADJUST_FAIL: - errorReason = "failed to optimize camera parameters. The images might have extreme lens distortion or inconsistent exposure."; - break; - default: - errorReason = "Unknown error occurred (Code: " + std::to_string(int(status)) + ")."; - break; - } + // PHASE 1: Feature Detection and Matching + LogManager::instance()->log("[ Panorama.cpp ] - Phase 1: Feature detection", + DEBUG); + + cv::Ptr finder = cv::SIFT::create(); + std::vector features(images8bit.size()); + + for (size_t i = 0; i < images8bit.size(); i++) { + cv::detail::computeImageFeatures(finder, images8bit[i], features[i]); + features[i].img_idx = static_cast(i); + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Image %1: %2 features detected") + .arg(i) + .arg(static_cast(features[i].keypoints.size())), + DEBUG); + } + + + // Match features between images + cv::Ptr matcher = + cv::makePtr(false, 0.3f); + std::vector pairwise_matches; + + (*matcher)(features, pairwise_matches); + matcher->collectGarbage(); + + // Check if we have enough matches + int num_matches = 0; + for (const auto& match : pairwise_matches) { + if (match.confidence > 0.0) num_matches++; + } + + if (num_matches < static_cast(images8bit.size()) - 1) { LogManager::instance()->log( - QString("[ Panorama.cpp ] - Stitching failed: %1").arg(errorReason), - ERROR); - result["message"] = QString("Stitching failed: %1").arg(errorReason); + "[ Panorama.cpp ] - Not enough matching features found", ERROR); + result["message"] = + QString("Stitching failed: not enough matching features found."); result["success"] = false; return result; } - cv::Mat panoramaBGR; - status = stitcher->composePanorama(images, panoramaBGR); + // PHASE 2: Camera Parameter Estimation + LogManager::instance()->log("[ Panorama.cpp ] - Phase 2: Camera estimation", + DEBUG); + + cv::Ptr estimator = + cv::makePtr(); + std::vector cameras; - if (status != cv::Stitcher::OK) { + if (!(*estimator)(features, pairwise_matches, cameras)) { LogManager::instance()->log( - QString("[ Panorama.cpp ] - Stitching failed during composition!"), - ERROR); - result["message"] = QString("Stitching failed!"); + "[ Panorama.cpp ] - Homography estimation failed", ERROR); + result["message"] = + QString("Stitching failed: failed to align the images."); result["success"] = false; return result; + } + + // Convert rotation matrices to CV_32F format required by bundle adjuster + for (size_t i = 0; i < cameras.size(); ++i) { + cameras[i].R.convertTo(cameras[i].R, CV_32F); + } + + // Refine camera parameters with bundle adjustment + cv::Ptr adjuster = + cv::makePtr(); + + adjuster->setConfThresh(1.0); + if (!(*adjuster)(features, pairwise_matches, cameras)) { + LogManager::instance()->log("[ Panorama.cpp ] - Bundle adjustment failed", + ERROR); + result["message"] = + QString("Stitching failed: failed to optimize camera parameters."); + result["success"] = false; + return result; + } + // PHASE 3: Warping Images (16-bit) + LogManager::instance()->log("[ Panorama.cpp ] - Phase 3: Warping images", + DEBUG); + + // Find median focal length + std::vector focals; + for (size_t i = 0; i < cameras.size(); ++i) { + focals.push_back(cameras[i].focal); + } + std::sort(focals.begin(), focals.end()); + float median_focal = static_cast(focals[focals.size() / 2]); + + // Create cylindrical warper with scale based on focal length + float warped_image_scale = median_focal; + cv::Ptr warper_creator = + cv::makePtr(); + cv::Ptr warper = + warper_creator->create(static_cast(warped_image_scale)); + + LogManager::instance()->log("[ Panorama.cpp ] - Created warper", DEBUG); + // Warp images and create masks + std::vector images_warped16; + std::vector masks_warped; + std::vector images_warped16_umat; + std::vector masks_warped_umat; + std::vector corners; + std::vector sizes_warped; + + for (size_t i = 0; i < images16.size(); i++) { + cv::Mat K; + cameras[i].K().convertTo(K, CV_32F); + cv::Rect roi = warper->warpRoi(sizes[i], K, cameras[i].R); + corners.push_back(roi.tl()); + sizes_warped.push_back(roi.size()); + + // Warp the 16-bit image + cv::Mat warped; + warper->warp(images16[i], K, cameras[i].R, cv::INTER_LINEAR, + cv::BORDER_REFLECT, warped); + images_warped16.push_back(warped); + images_warped16_umat.push_back(warped.getUMat(cv::ACCESS_READ)); + + // Create and warp mask + cv::Mat mask = cv::Mat::ones(sizes[i], CV_8U) * 255; + cv::Mat warped_mask; + warper->warp(mask, K, cameras[i].R, cv::INTER_NEAREST, cv::BORDER_CONSTANT, + warped_mask); + masks_warped.push_back(warped_mask); + masks_warped_umat.push_back(warped_mask.getUMat(cv::ACCESS_READ)); + } + + // PHASE 4: Exposure Compensation (optional) + if (compensateExposure) { + LogManager::instance()->log( + "[ Panorama.cpp ] - Phase 4: Exposure compensation", DEBUG); + + cv::Ptr compensator = + cv::makePtr(); + compensator->feed(corners, images_warped16_umat, masks_warped_umat); + for (size_t i = 0; i < images_warped16.size(); ++i) { + compensator->apply(static_cast(i), corners[i], images_warped16[i], + masks_warped[i]); + } + } else { + LogManager::instance()->log( + "[ Panorama.cpp ] - Phase 4: Skipping exposure compensation", DEBUG); + } + + // PHASE 5: Seam Finding (Graph-Cut) + // GraphCutSeamFinder expects 8-bit UMat images and binary masks + LogManager::instance()->log("[ Panorama.cpp ] - Phase 5: Seam finding", + DEBUG); + + // Ensure masks are binary (0 or 255) + std::vector masks_binary; + for (auto& mask : masks_warped) { + cv::Mat mask_bin; + cv::threshold(mask, mask_bin, 127, 255, cv::THRESH_BINARY); + masks_binary.push_back(mask_bin.getUMat(cv::ACCESS_READ)); + } + + std::vector images_warped8_umat; + for (const auto& img16 : images_warped16) { + cv::Mat img8; + img16.convertTo(img8, CV_8UC3, 255.0 / 65535.0); + images_warped8_umat.push_back(img8.getUMat(cv::ACCESS_READ)); + } + + // Use a simpler seam finder that's more robust + + cv::Ptr seam_finder = + cv::makePtr(); + seam_finder->find(images_warped8_umat, corners, masks_binary); + /* + cv::Ptr seam_finder = + cv::makePtr( + cv::detail::GraphCutSeamFinder::COST_COLOR); + seam_finder->find(images_warped8_umat, corners, masks_binary); +*/ + // PHASE 6: Multi-band Blending (16-bit) + LogManager::instance()->log("[ Panorama.cpp ] - Phase 6: Multi-band blending", + DEBUG); + + // Calculate final panorama size + cv::Rect dst_roi = cv::detail::resultRoi(corners, sizes_warped); + + // Create multi-band blender with high number of bands for quality + // Blend width is typically based on image size - use 1/8 of the smaller + // dimension + int blend_width = std::min(dst_roi.width, dst_roi.height) / 8; + int num_bands = static_cast( + std::ceil(std::log(static_cast(blend_width)) / std::log(2.0))); + num_bands = std::min(num_bands, 8); // Cap at 8 for performance + + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Using %1 bands for blending").arg(num_bands), + DEBUG); + + // Create blender - use default CV_32F weight type + cv::Ptr blender = + cv::makePtr(false, num_bands); + blender->prepare(corners, sizes_warped); + + + float scale_factor = 8.0; +// 1. Feed images to blender using Scaled 16-bit Signed + for (size_t i = 0; i < images_warped16.size(); i++) { + cv::Mat img16S; + // We multiply by 1/scale_factor to allow the blend to sum up the highlights without clipping + images_warped16[i].convertTo(img16S, CV_16SC3, 1/scale_factor); + blender->feed(img16S, masks_warped[i], corners[i]); + } + + // 2. Blend + cv::Mat result_16s, result_mask; + blender->blend(result_16s, result_mask); + + // 3. Convert back to 16-bit Unsigned + cv::Mat result16; + // Now we multiply again bythe scale_factor to restore the original data. + // The more you scale the more you lose data. + result_16s.convertTo(result16, CV_16UC3, scale_factor); + + // Blend + /* + cv::Mat result_mask; + cv::Mat result8; + blender->blend(result8, result_mask); + */ + // Convert 8-bit gamma result back to 16-bit linear + /* + cv::Mat result32F, result16; + result8.convertTo(result32F, CV_32FC3, 1.0 / 255.0); + cv::pow(result32F, 2.2, result32F); + result32F.convertTo(result16, CV_16UC3, 65535.0); + */ + cv::Mat resultRGB; + cv::cvtColor(result16, resultRGB, cv::COLOR_BGR2RGB); + + // Ensure continuous memory layout for TIFF writing + if (!resultRGB.isContinuous()) resultRGB = resultRGB.clone(); + + if (resultRGB.empty()) { + LogManager::instance()->log("[ Panorama.cpp ] - Blending failed", ERROR); + result["message"] = QString("Stitching failed during blending!"); + result["success"] = false; + return result; } - cv::Mat panoramaRGB; - - cv::cvtColor(panoramaBGR, panoramaRGB, cv::COLOR_BGR2RGB); - panoramaRGB.convertTo(panoramaRGB, cv::COLOR_16U); - - tinydngwriter::DNGWriter dngwriter(false); - tinydngwriter::DNGImage dngimage; - - dngimage.SetSubfileType(false, false, false); - dngimage.SetImageWidth(panoramaRGB.cols); - dngimage.SetImageLength(panoramaRGB.rows); - dngimage.SetRowsPerStrip(panoramaRGB.rows); - dngimage.SetSamplesPerPixel(3); // 3 channels rgb - uint16_t bps[3] = {16, 16, 16}; - - dngimage.SetBitsPerSample(3, bps); - dngimage.SetPlanarConfig(tinydngwriter::PLANARCONFIG_CONTIG); - dngimage.SetCompression(tinydngwriter::COMPRESSION_NONE); - dngimage.SetPhotometric(tinydngwriter::PHOTOMETRIC_RGB); - // - // Standard DNG tags for color calibration. - // Since we outputted to sRGB from LibRaw, an identity matrix is technically - // sufficient, but D65 illuminant provides standard baseline compatibility. - uint16_t illuminant = 21; // D65 - dngimage.SetCalibrationIlluminant1(illuminant); - double colorMatrix[9] = { - 1.0f, 0.0f, 0.0f, - 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 1.0f - }; - dngimage.SetColorMatrix1(3, colorMatrix); - dngimage.SetImageData(reinterpret_cast(panoramaRGB.data), panoramaRGB.total() * panoramaRGB.elemSize()); - dngwriter.AddImage(&dngimage); + + LogManager::instance()->log( + QString("[ Panorama.cpp ] - Blended result: %1x%2") + .arg(result16.cols) + .arg(result16.rows), + DEBUG); + + // Ensure continuous memory layout for TIFF writing + // if (!result16.isContinuous()) result16 = result16.clone(); + + // PHASE 7: Save to DNG + LogManager::instance()->log("[ Panorama.cpp ] - Phase 7: Saving to DNG", + DEBUG); + QString filename = - QDir::toNativeSeparators( - QString( - "%1/%2.pano.dng" - ).arg( - QFileInfo(inputFiles[0]).absolutePath(), - QFileInfo(inputFiles[0]).baseName() - ) - ); + QDir::toNativeSeparators(QString("%1/%2.pano.dng") + .arg(QFileInfo(inputFiles[0]).absolutePath(), + QFileInfo(inputFiles[0]).baseName())); LogManager::instance()->log( - QString("[ Panorama.cpp ] - Saving panorama to %1").arg(filename), - INFO); + QString("[ Panorama.cpp ] - Saving panorama to %1").arg(filename), INFO); + /* + libdng_init(); + libdng_info dng = {0}; + libdng_new(&dng); + if (!libdng_set_mode_from_name(&dng, "SRGGB16")) { + fprintf(stderr, "Invalid pixel format supplied\n"); + } + + for (size_t i = 0; i < 9; i++) + dng.color_matrix_1[i] = colorInfo.get()->matrix[i]; + + + for (size_t i = 0; i < 3; i++) + dng.analogbalance[i] = colorInfo->asShotNeutral[i]; + + libdng_set_make_model(&dng, "Photon", "Panorama"); - std::string errMsg; + if ( - !dngwriter.WriteToFile( + !libdng_write( + &dng, filename.toStdString().c_str(), - &errMsg + result16.cols, result16.rows, + reinterpret_cast(result16.data), + result16.total() * result16.elemSize() ) ) { - - LogManager::instance()->log( - QString("[ Panorama.cpp ] - Couldn't write dng file %1: %2").arg(filename, errMsg), - ERROR); - result["message"] = QString("Couldn't write dng file %1: %2").arg(filename, errMsg); result["success"] = false; + result["message"] = "Error creating DNG file."; + + libdng_free(&dng); return result; } - result["message"] = QString("Panorama create successfully and saved to %1!").arg(filename); - result["success"] = true; + libdng_free(&dng); + */ + + TIFF* out = TIFFOpen(filename.toStdString().c_str(), "w"); + if (!out) { + result["success"] = false; + result["message"] = "Could not open file for writing."; return result; + } + + TIFFSetField(out, TIFFTAG_IMAGEWIDTH, resultRGB.cols); + TIFFSetField(out, TIFFTAG_IMAGELENGTH, resultRGB.rows); + TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 3); + TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 16); + TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); + TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + + static const uint8_t dng_ver[] = {1, 4, 0, 0}; + TIFFSetField(out, TIFFTAG_DNGVERSION, dng_ver); + TIFFSetField(out, TIFFTAG_SUBFILETYPE, 0); + TIFFSetField(out, TIFFTAG_UNIQUECAMERAMODEL, "Photon Panorama"); + TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(out, 0)); + + uint32_t whiteLevel[3] = {65535, 65535, 65535}; + TIFFSetField(out, TIFFTAG_WHITELEVEL, 3, whiteLevel); + + TIFFSetField(out, TIFFTAG_COLORMATRIX1, 9, colorInfo.get()->matrix); + TIFFSetField(out, TIFFTAG_ASSHOTNEUTRAL, 3, colorInfo.get()->asShotNeutral); + TIFFSetField(out, TIFFTAG_CALIBRATIONILLUMINANT1, 23); + + // Write the 16-bit data + for (int row = 0; row < resultRGB.rows; row++) { + uint16_t* rowPtr = resultRGB.ptr(row); + if (TIFFWriteScanline(out, rowPtr, row, 0) < 0) { + TIFFClose(out); + result["success"] = false; + result["message"] = "Error writing scanline to DNG."; + return result; + } + } + + TIFFClose(out); + + + LogManager::instance()->log( + "[ Panorama.cpp ] - Panorama stitching completed successfully", INFO); + result["success"] = true; + result["message"] = "Success! DNG saved to " + filename; + result["filename"] = filename; + result["width"] = resultRGB.cols; + result["height"] = resultRGB.rows; + return result; } diff --git a/src/engine/Panorama.h b/src/engine/Panorama.h index 0049861..6f44cf4 100644 --- a/src/engine/Panorama.h +++ b/src/engine/Panorama.h @@ -10,69 +10,35 @@ #include #include #include -#include "../managers/LogManager.h" -#include "../libraries/tiny_dng_writer.h" +#include +#include +/* +extern "C" { + #include "libdng/libdng.h" +}*/ namespace photon { +struct ColorInfo { + float matrix[9]; // XYZ→camera (for ColorMatrix1) + float asShotNeutral[3]; // normalized WB + +}; + class Panorama : public QObject { Q_OBJECT QML_ELEMENT QML_SINGLETON private: - static cv::Mat raw_to_linear(QString file) { - LibRaw processor; - processor.imgdata.params.output_bps = 16; - processor.imgdata.params.gamm[0] = 1.0; - processor.imgdata.params.gamm[1] = 1.0; - processor.imgdata.params.no_auto_bright = 1; - processor.imgdata.params.use_auto_wb = 1; - processor.imgdata.params.output_color = 1; - if (processor.open_file(file.toStdString().c_str()) != LIBRAW_SUCCESS) { - LogManager::instance()->log( - QString("[ Panorama.cpp ] - Cannot open file %1").arg(file), - ERROR); - return cv::Mat(); - } - - if (processor.unpack() != LIBRAW_SUCCESS) { - LogManager::instance()->log( - QString("[ Panorama.cpp ] - Cannot unpack data of file %1").arg(file), - ERROR); - return cv::Mat(); - } - - if (processor.dcraw_process() != LIBRAW_SUCCESS) { - LogManager::instance()->log( - QString("[ Panorama.cpp ] - Cannot dcraw file %1").arg(file), - ERROR); - return cv::Mat(); - } - - libraw_processed_image_t *image = processor.dcraw_make_mem_image(); - - if (!image) { - LogManager::instance()->log( - QString("[ Panorama.cpp ] - Cannot create procesed image from %1").arg(file), - ERROR); - return cv::Mat(); - } - - cv::Mat rawRGB(image->height, image->width, CV_16UC3, image->data); - cv::Mat matBGR; - cv::cvtColor(rawRGB, matBGR, cv::COLOR_RGB2BGR); - LibRaw::dcraw_clear_mem(image); - - return matBGR; - } + static cv::Mat raw_to_linear(const QString& file, std::unique_ptr& colorInfo); void estimateTransform(cv::Stitcher& stitcher); - QVariantMap stitchPhotos(const QStringList& inputFiles); + QVariantMap stitchPhotos(const QStringList& inputFiles, bool compensateExposure, size_t featuresThreshold = 2000); public: explicit Panorama(QObject* parent = nullptr) : QObject(parent) {} - Q_INVOKABLE void stitchAsync(const QStringList& inputFiles); + Q_INVOKABLE void stitchAsync(const QStringList& inputFiles, bool compensateExposure = false); signals: void stitchCompleted(QVariantMap result); }; diff --git a/src/libraries/tiny_dng_writer.h b/src/libraries/tiny_dng_writer.h deleted file mode 100644 index 9e061b8..0000000 --- a/src/libraries/tiny_dng_writer.h +++ /dev/null @@ -1,2583 +0,0 @@ -// -// TinyDNGWriter, single header only DNG writer in C++11. -// - -/* -The MIT License (MIT) - -Copyright (c) 2016 - 2020 Syoyo Fujita. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -#ifndef TINY_DNG_WRITER_H_ -#define TINY_DNG_WRITER_H_ - -#include -#include -#include - -namespace tinydngwriter { - -typedef enum { - TIFFTAG_SUB_FILETYPE = 254, - TIFFTAG_IMAGE_WIDTH = 256, - TIFFTAG_IMAGE_LENGTH = 257, - TIFFTAG_BITS_PER_SAMPLE = 258, - TIFFTAG_COMPRESSION = 259, - TIFFTAG_PHOTOMETRIC = 262, - TIFFTAG_IMAGEDESCRIPTION = 270, - TIFFTAG_STRIP_OFFSET = 273, - TIFFTAG_SAMPLES_PER_PIXEL = 277, - TIFFTAG_ROWS_PER_STRIP = 278, - TIFFTAG_STRIP_BYTE_COUNTS = 279, - TIFFTAG_PLANAR_CONFIG = 284, - TIFFTAG_ORIENTATION = 274, - - TIFFTAG_XRESOLUTION = 282, // rational - TIFFTAG_YRESOLUTION = 283, // rational - TIFFTAG_RESOLUTION_UNIT = 296, - - TIFFTAG_SOFTWARE = 305, - - TIFFTAG_SAMPLEFORMAT = 339, - - // DNG extension - TIFFTAG_CFA_REPEAT_PATTERN_DIM = 33421, - TIFFTAG_CFA_PATTERN = 33422, - - TIFFTAG_DNG_VERSION = 50706, - TIFFTAG_DNG_BACKWARD_VERSION = 50707, - TIFFTAG_UNIQUE_CAMERA_MODEL = 50708, - TIFFTAG_CHRROMA_BLUR_RADIUS = 50703, - TIFFTAG_BLACK_LEVEL_REPEAT_DIM = 50713, - TIFFTAG_BLACK_LEVEL = 50714, - TIFFTAG_WHITE_LEVEL = 50717, - TIFFTAG_COLOR_MATRIX1 = 50721, - TIFFTAG_COLOR_MATRIX2 = 50722, - TIFFTAG_CAMERA_CALIBRATION1 = 50723, - TIFFTAG_CAMERA_CALIBRATION2 = 50724, - TIFFTAG_ANALOG_BALANCE = 50727, - TIFFTAG_AS_SHOT_NEUTRAL = 50728, - TIFFTAG_AS_SHOT_WHITE_XY = 50729, - TIFFTAG_CALIBRATION_ILLUMINANT1 = 50778, - TIFFTAG_CALIBRATION_ILLUMINANT2 = 50779, - TIFFTAG_EXTRA_CAMERA_PROFILES = 50933, - TIFFTAG_PROFILE_NAME = 50936, - TIFFTAG_AS_SHOT_PROFILE_NAME = 50934, - TIFFTAG_DEFAULT_BLACK_RENDER = 51110, - TIFFTAG_ACTIVE_AREA = 50829, - TIFFTAG_FORWARD_MATRIX1 = 50964, - TIFFTAG_FORWARD_MATRIX2 = 50965 -} Tag; - -// SUBFILETYPE(bit field) -static const int FILETYPE_REDUCEDIMAGE = 1; -static const int FILETYPE_PAGE = 2; -static const int FILETYPE_MASK = 4; - -// PLANARCONFIG -static const int PLANARCONFIG_CONTIG = 1; -static const int PLANARCONFIG_SEPARATE = 2; - -// COMPRESSION -// TODO(syoyo) more compressin types. -static const int COMPRESSION_NONE = 1; -static const int COMPRESSION_NEW_JPEG = 7; - -// ORIENTATION -static const int ORIENTATION_TOPLEFT = 1; -static const int ORIENTATION_TOPRIGHT = 2; -static const int ORIENTATION_BOTRIGHT = 3; -static const int ORIENTATION_BOTLEFT = 4; -static const int ORIENTATION_LEFTTOP = 5; -static const int ORIENTATION_RIGHTTOP = 6; -static const int ORIENTATION_RIGHTBOT = 7; -static const int ORIENTATION_LEFTBOT = 8; - -// RESOLUTIONUNIT -static const int RESUNIT_NONE = 1; -static const int RESUNIT_INCH = 2; -static const int RESUNIT_CENTIMETER = 2; - -// PHOTOMETRIC -// TODO(syoyo): more photometric types. -static const int PHOTOMETRIC_WHITE_IS_ZERO = 0; // For bilevel and grayscale -static const int PHOTOMETRIC_BLACK_IS_ZERO = 1; // For bilevel and grayscale -static const int PHOTOMETRIC_RGB = 2; // Default -static const int PHOTOMETRIC_CFA = 32803; // DNG ext -static const int PHOTOMETRIC_LINEARRAW = 34892; // DNG ext - -// Sample format -static const int SAMPLEFORMAT_UINT = 1; // Default -static const int SAMPLEFORMAT_INT = 2; -static const int SAMPLEFORMAT_IEEEFP = 3; // floating point - -struct IFDTag { - unsigned short tag; - unsigned short type; - unsigned int count; - unsigned int offset_or_value; -}; -// 12 bytes. - -class DNGImage { - public: - DNGImage(); - ~DNGImage() {} - - /// - /// Optional: Explicitly specify endian. - /// Must be called before calling other Set methods. - /// - void SetBigEndian(bool big_endian); - - /// - /// Default = 0 - /// - bool SetSubfileType(bool reduced_image = false, bool page = false, - bool mask = false); - - bool SetImageWidth(unsigned int value); - bool SetImageLength(unsigned int value); - bool SetRowsPerStrip(unsigned int value); - bool SetSamplesPerPixel(unsigned short value); - // Set bits for each samples - bool SetBitsPerSample(const unsigned int num_samples, - const unsigned short *values); - bool SetPhotometric(unsigned short value); - bool SetPlanarConfig(unsigned short value); - bool SetOrientation(unsigned short value); - bool SetCompression(unsigned short value); - bool SetSampleFormat(const unsigned int num_samples, - const unsigned short *values); - bool SetXResolution(double value); - bool SetYResolution(double value); - bool SetResolutionUnit(const unsigned short value); - - /// - /// Set arbitrary string for image description. - /// Currently we limit to 1024*1024 chars at max. - /// - bool SetImageDescription(const std::string &ascii); - - /// - /// Set arbitrary string for unique camera model name (not localized!). - /// Currently we limit to 1024*1024 chars at max. - /// - bool SetUniqueCameraModel(const std::string &ascii); - - /// - /// Set software description(string). - /// Currently we limit to 4095 chars at max. - /// - bool SetSoftware(const std::string &ascii); - - bool SetActiveArea(const unsigned int values[4]); - - bool SetChromaBlurRadius(double value); - - /// Specify black level per sample. - bool SetBlackLevel(const unsigned int num_samples, const unsigned short *values); - - /// Specify black level per sample (as rational values). - bool SetBlackLevelRational(unsigned int num_samples, const double *values); - - /// Specify white level per sample. - bool SetWhiteLevelRational(unsigned int num_samples, const double *values); - - /// Specify analog white balance from camera for raw values. - bool SetAnalogBalance(const unsigned int plane_count, const double *matrix_values); - - /// Specify CFA repeating pattern dimensions. - bool SetCFARepeatPatternDim(const unsigned short width, const unsigned short height); - - /// Specify black level repeating pattern dimensions. - bool SetBlackLevelRepeatDim(const unsigned short width, const unsigned short height); - - bool SetCalibrationIlluminant1(const unsigned short value); - bool SetCalibrationIlluminant2(const unsigned short value); - - /// Specify DNG version. - bool SetDNGVersion(const unsigned char a, const unsigned char b, const unsigned char c, const unsigned char d); - - /// Specify transformation matrix (XYZ to reference camera native color space values, under the first calibration illuminant). - bool SetColorMatrix1(const unsigned int plane_count, const double *matrix_values); - - /// Specify transformation matrix (XYZ to reference camera native color space values, under the second calibration illuminant). - bool SetColorMatrix2(const unsigned int plane_count, const double *matrix_values); - - bool SetForwardMatrix1(const unsigned int plane_count, const double *matrix_values); - bool SetForwardMatrix2(const unsigned int plane_count, const double *matrix_values); - - bool SetCameraCalibration1(const unsigned int plane_count, const double *matrix_values); - bool SetCameraCalibration2(const unsigned int plane_count, const double *matrix_values); - - /// Specify CFA geometric pattern (left-to-right, top-to-bottom). - bool SetCFAPattern(const unsigned int num_components, const unsigned char *values); - - /// Specify the selected white balance at time of capture, encoded as the coordinates of a perfectly neutral color in linear reference space values. - bool SetAsShotNeutral(const unsigned int plane_count, const double *matrix_values); - - /// Specify the the selected white balance at time of capture, encoded as x-y chromaticity coordinates. - bool SetAsShotWhiteXY(const double x, const double y); - - /// Set image data with packing (take 16-bit values and pack them to input_bpp values). - bool SetImageDataPacked(const unsigned short *input_buffer, const int input_count, const unsigned int input_bpp, bool big_endian); - - /// Set image data. - bool SetImageData(const unsigned char *data, const size_t data_len); - - /// Set image data. - bool SetImageDataJpeg(const unsigned short *data, unsigned int width, unsigned int height, unsigned int bpp); - - /// Set custom field. - bool SetCustomFieldLong(const unsigned short tag, const int value); - bool SetCustomFieldULong(const unsigned short tag, const unsigned int value); - - size_t GetDataSize() const { return data_os_.str().length(); } - - size_t GetStripOffset() const { return data_strip_offset_; } - size_t GetStripBytes() const { return data_strip_bytes_; } - - /// Write aux IFD data and strip image data to stream. - bool WriteDataToStream(std::ostream *ofs) const; - - /// - /// Write IFD to stream. - /// - /// @param[in] data_base_offset : Byte offset to data - /// @param[in] strip_offset : Byte offset to image strip data - /// - /// TODO(syoyo): Support multiple strips - /// - bool WriteIFDToStream(const unsigned int data_base_offset, - const unsigned int strip_offset, std::ostream *ofs) const; - - std::string Error() const { return err_; } - - private: - std::ostringstream data_os_; - bool swap_endian_; - bool dng_big_endian_; - unsigned short num_fields_; - unsigned int samples_per_pixels_; - std::vector bits_per_samples_; - - // TODO(syoyo): Support multiple strips - size_t data_strip_offset_{0}; - size_t data_strip_bytes_{0}; - - mutable std::string err_; // Error message - - std::vector ifd_tags_; -}; - -class DNGWriter { - public: - // TODO(syoyo): Use same endian setting with DNGImage. - DNGWriter(bool big_endian); - ~DNGWriter() {} - - /// - /// Add DNGImage. - /// It just retains the pointer of the image, thus - /// application must not free resources until `WriteToFile` has been called. - /// - bool AddImage(const DNGImage *image) { - images_.push_back(image); - - return true; - } - - /// Write DNG to a file. - /// Return error string to `err` when Write() returns false. - /// Returns true upon success. - bool WriteToFile(const char *filename, std::string *err) const; - - private: - bool swap_endian_; - bool dng_big_endian_; // Endianness of DNG file. - - std::vector images_; -}; - -} // namespace tinydngwriter - -#endif // TINY_DNG_WRITER_H_ - -#ifdef TINY_DNG_WRITER_IMPLEMENTATION - -// -// TIFF format resources. -// -// http://c0de517e.blogspot.jp/2013/07/tiny-hdr-writer.html -// http://paulbourke.net/dataformats/tiff/ and -// http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf -// - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Undef if you want to use builtin function for clz -#if 0 -#ifdef _MSC_VER -#include -#endif -#endif - - -namespace tinydngwriter { - -namespace detail { - -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Weverything" -#endif - -// Begin liblj92, Lossless JPEG decode/encoder ------------------------------ -// -// With fixes: https://github.com/ilia3101/MLV-App/pull/151 - -/* -lj92.c -(c) Andrew Baldwin 2014 - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -enum LJ92_ERRORS { - LJ92_ERROR_NONE = 0, - LJ92_ERROR_CORRUPT = -1, - LJ92_ERROR_NO_MEMORY = -2, - LJ92_ERROR_BAD_HANDLE = -3, - LJ92_ERROR_TOO_WIDE = -4 -}; - -/* - * Encode a grayscale image supplied as 16bit values within the given bitdepth - * Read from tile in the image - * Apply delinearization if given - * Return the encoded lossless JPEG stream - */ -int lj92_encode(uint16_t *image, int width, int height, int bitdepth, - int readLength, int skipLength, uint16_t *delinearize, - int delinearizeLength, uint8_t **encoded, int *encodedLength); - -typedef uint8_t u8; -typedef uint16_t u16; -typedef uint32_t u32; - -//#define LJ92_DEBUG - -/* Encoder implementation */ - -#if 0 -uint32_t __inline clz32(uint32_t value) { - unsigned long leading_zero = 0; - - if (_BitScanReverse(&leading_zero, value)) { - return 31 - leading_zero; - } else { - // Same remarks as above - return 32; - } -} -#endif - -// Very simple count leading zero implementation. -static int clz32(unsigned int x) { - int n; - if (x == 0) return 32; - for (n = 0; ((x & 0x80000000) == 0); n++, x <<= 1) - ; - return n; -} - -typedef struct _lje { - uint16_t *image; - int width; - int height; - int bitdepth; - int readLength; - int skipLength; - uint16_t *delinearize; - int delinearizeLength; - uint8_t *encoded; - int encodedWritten; - int encodedLength; - int hist[17]; // SSSS frequency histogram - int bits[17]; - int huffval[17]; - u16 huffenc[17]; - u16 huffbits[17]; - int huffsym[17]; -} lje; - -int frequencyScan(lje *self) { - // Scan through the tile using the standard type 6 prediction - // Need to cache the previous 2 row in target coordinates because of tiling - uint16_t *pixel = self->image; - int pixcount = self->width * self->height; - int scan = self->readLength; - uint16_t *rowcache = (uint16_t *)calloc(1, self->width * 4); - uint16_t *rows[2]; - rows[0] = rowcache; - rows[1] = &rowcache[self->width]; - - int col = 0; - int row = 0; - int Px = 0; - int32_t diff = 0; - int maxval = (1 << self->bitdepth); - while (pixcount--) { - uint16_t p = *pixel; - if (self->delinearize) { - if (p >= self->delinearizeLength) { - free(rowcache); - return LJ92_ERROR_TOO_WIDE; - } - p = self->delinearize[p]; - } - if (p >= maxval) { - free(rowcache); - return LJ92_ERROR_TOO_WIDE; - } - rows[1][col] = p; - - if ((row == 0) && (col == 0)) - Px = 1 << (self->bitdepth - 1); - else if (row == 0) - Px = rows[1][col - 1]; - else if (col == 0) - Px = rows[0][col]; - else - Px = rows[0][col] + ((rows[1][col - 1] - rows[0][col - 1]) >> 1); - diff = rows[1][col] - Px; - int ssss = 32 - clz32(abs(diff)); - if (diff == 0) ssss = 0; - self->hist[ssss]++; - // printf("%d %d %d %d %d %d\n",col,row,p,Px,diff,ssss); - pixel++; - scan--; - col++; - if (scan == 0) { - pixel += self->skipLength; - scan = self->readLength; - } - if (col == self->width) { - uint16_t *tmprow = rows[1]; - rows[1] = rows[0]; - rows[0] = tmprow; - col = 0; - row++; - } - } -#ifdef DEBUG - int sort[17]; - for (int h = 0; h < 17; h++) { - sort[h] = h; - printf("%d:%d\n", h, self->hist[h]); - } -#endif - free(rowcache); - return LJ92_ERROR_NONE; -} - -void createEncodeTable(lje *self) { - float freq[18]; - int codesize[18]; - int others[18]; - - // Calculate frequencies - float totalpixels = self->width * self->height; - for (int i = 0; i < 17; i++) { - freq[i] = (float)(self->hist[i]) / totalpixels; -#ifdef DEBUG - printf("%d:%f\n", i, freq[i]); -#endif - codesize[i] = 0; - others[i] = -1; - } - codesize[17] = 0; - others[17] = -1; - freq[17] = 1.0f; - - float v1f, v2f; - int v1, v2; - - while (1) { - v1f = 3.0f; - v1 = -1; - for (int i = 0; i < 18; i++) { - if ((freq[i] <= v1f) && (freq[i] > 0.0f)) { - v1f = freq[i]; - v1 = i; - } - } -#ifdef DEBUG - printf("v1:%d,%f\n", v1, v1f); -#endif - v2f = 3.0f; - v2 = -1; - for (int i = 0; i < 18; i++) { - if (i == v1) continue; - if ((freq[i] < v2f) && (freq[i] > 0.0f)) { - v2f = freq[i]; - v2 = i; - } - } - if (v2 == -1) break; // Done - - freq[v1] += freq[v2]; - freq[v2] = 0.0f; - - while (1) { - codesize[v1]++; - if (others[v1] == -1) break; - v1 = others[v1]; - } - others[v1] = v2; - while (1) { - codesize[v2]++; - if (others[v2] == -1) break; - v2 = others[v2]; - } - } - int *bits = self->bits; - memset(bits, 0, sizeof(self->bits)); - for (int i = 0; i < 18; i++) { - if (codesize[i] != 0) { - bits[codesize[i]]++; - } - } -#ifdef DEBUG - for (int i = 0; i < 17; i++) { - printf("bits:%d,%d,%d\n", i, bits[i], codesize[i]); - } -#endif - int *huffval = self->huffval; - int i = 1; - int k = 0; - int j; - memset(huffval, 0, sizeof(self->huffval)); - while (i <= 32) { - j = 0; - while (j < 17) { - if (codesize[j] == i) { - huffval[k++] = j; - } - j++; - } - i++; - } -#ifdef DEBUG - for (i = 0; i < 17; i++) { - printf("i=%d,huffval[i]=%x\n", i, huffval[i]); - } -#endif - int maxbits = 16; - while (maxbits > 0) { - if (bits[maxbits]) break; - maxbits--; - } - u16 *huffenc = self->huffenc; - u16 *huffbits = self->huffbits; - int *huffsym = self->huffsym; - memset(huffenc, 0, sizeof(self->huffenc)); - memset(huffbits, 0, sizeof(self->huffbits)); - memset(self->huffsym, 0, sizeof(self->huffsym)); - i = 0; - int hv = 0; - int rv = 0; - int vl = 0; // i - // int hcode; - int bitsused = 1; - int sym = 0; - // printf("%04x:%x:%d:%x\n",i,huffvals[hv],bitsused,1<<(maxbits-bitsused)); - while (i < 1 << maxbits) { - if (bitsused > maxbits) { - break; // Done. Should never get here! - } - if (vl >= bits[bitsused]) { - bitsused++; - vl = 0; - continue; - } - if (rv == 1 << (maxbits - bitsused)) { - rv = 0; - vl++; - hv++; - // printf("%04x:%x:%d:%x\n",i,huffvals[hv],bitsused,1<<(maxbits-bitsused)); - continue; - } - huffbits[sym] = bitsused; - huffenc[sym++] = i >> (maxbits - bitsused); - // printf("%d %d %d\n",i,bitsused,hcode); - i += (1 << (maxbits - bitsused)); - rv = 1 << (maxbits - bitsused); - } - for (i = 0; i < 17; i++) { - if (huffbits[i] > 0) { - huffsym[huffval[i]] = i; - } -#ifdef DEBUG - printf("huffval[%d]=%d,huffenc[%d]=%x,bits=%d\n", i, huffval[i], i, - huffenc[i], huffbits[i]); -#endif - if (huffbits[i] > 0) { - huffsym[huffval[i]] = i; - } - } -#ifdef DEBUG - for (i = 0; i < 17; i++) { - printf("huffsym[%d]=%d\n", i, huffsym[i]); - } -#endif -} - -void writeHeader(lje *self) { - int w = self->encodedWritten; - uint8_t *e = self->encoded; - e[w++] = 0xff; - e[w++] = 0xd8; // SOI - e[w++] = 0xff; - e[w++] = 0xc3; // SOF3 - // Write SOF - e[w++] = 0x0; - e[w++] = 11; // Lf, frame header length - e[w++] = self->bitdepth; - e[w++] = self->height >> 8; - e[w++] = self->height & 0xFF; - e[w++] = self->width >> 8; - e[w++] = self->width & 0xFF; - e[w++] = 1; // Components - e[w++] = 0; // Component ID - e[w++] = 0x11; // Component X/Y - e[w++] = 0; // Unused (Quantisation) - e[w++] = 0xff; - e[w++] = 0xc4; // HUFF - // Write HUFF - int count = 0; - for (int i = 0; i < 17; i++) { - count += self->bits[i]; - } - e[w++] = 0x0; - e[w++] = 17 + 2 + count; // Lf, frame header length - e[w++] = 0; // Table ID - for (int i = 1; i < 17; i++) { - e[w++] = self->bits[i]; - } - for (int i = 0; i < count; i++) { - e[w++] = self->huffval[i]; - } - e[w++] = 0xff; - e[w++] = 0xda; // SCAN - // Write SCAN - e[w++] = 0x0; - e[w++] = 8; // Ls, scan header length - e[w++] = 1; // Components - e[w++] = 0; // - e[w++] = 0; // - e[w++] = 6; // Predictor - e[w++] = 0; // - e[w++] = 0; // - self->encodedWritten = w; -} - -void writePost(lje *self) { - int w = self->encodedWritten; - uint8_t *e = self->encoded; - e[w++] = 0xff; - e[w++] = 0xd9; // EOI - self->encodedWritten = w; -} - -void writeBody(lje *self) { - // Scan through the tile using the standard type 6 prediction - // Need to cache the previous 2 row in target coordinates because of tiling - uint16_t *pixel = self->image; - int pixcount = self->width * self->height; - int scan = self->readLength; - uint16_t *rowcache = (uint16_t *)calloc(1, self->width * 4); - uint16_t *rows[2]; - rows[0] = rowcache; - rows[1] = &rowcache[self->width]; - - int col = 0; - int row = 0; - int Px = 0; - int32_t diff = 0; - int bitcount = 0; - uint8_t *out = self->encoded; - int w = self->encodedWritten; - uint8_t next = 0; - uint8_t nextbits = 8; - while (pixcount--) { - uint16_t p = *pixel; - if (self->delinearize) p = self->delinearize[p]; - rows[1][col] = p; - - if ((row == 0) && (col == 0)) - Px = 1 << (self->bitdepth - 1); - else if (row == 0) - Px = rows[1][col - 1]; - else if (col == 0) - Px = rows[0][col]; - else - Px = rows[0][col] + ((rows[1][col - 1] - rows[0][col - 1]) >> 1); - diff = rows[1][col] - Px; - int ssss = 32 - clz32(abs(diff)); - if (diff == 0) ssss = 0; - // printf("%d %d %d %d %d\n",col,row,Px,diff,ssss); - - // Write the huffman code for the ssss value - int huffcode = self->huffsym[ssss]; - int huffenc = self->huffenc[huffcode]; - int huffbits = self->huffbits[huffcode]; - bitcount += huffbits + ssss; - - int vt = ssss > 0 ? (1 << (ssss - 1)) : 0; - // printf("%d %d %d %d\n",rows[1][col],Px,diff,Px+diff); -#ifdef DEBUG -#endif - if (diff < vt) diff += (1 << (ssss)) - 1; - - // Write the ssss - while (huffbits > 0) { - int usebits = huffbits > nextbits ? nextbits : huffbits; - // Add top usebits from huffval to next usebits of nextbits - int tophuff = huffenc >> (huffbits - usebits); - next |= (tophuff << (nextbits - usebits)); - nextbits -= usebits; - huffbits -= usebits; - huffenc &= (1 << huffbits) - 1; - if (nextbits == 0) { - out[w++] = next; - if (next == 0xff) out[w++] = 0x0; - next = 0; - nextbits = 8; - } - } - // Write the rest of the bits for the value - - while (ssss > 0) { - int usebits = ssss > nextbits ? nextbits : ssss; - // Add top usebits from huffval to next usebits of nextbits - int tophuff = diff >> (ssss - usebits); - next |= (tophuff << (nextbits - usebits)); - nextbits -= usebits; - ssss -= usebits; - diff &= (1 << ssss) - 1; - if (nextbits == 0) { - out[w++] = next; - if (next == 0xff) out[w++] = 0x0; - next = 0; - nextbits = 8; - } - } - - // printf("%d %d\n",diff,ssss); - pixel++; - scan--; - col++; - if (scan == 0) { - pixel += self->skipLength; - scan = self->readLength; - } - if (col == self->width) { - uint16_t *tmprow = rows[1]; - rows[1] = rows[0]; - rows[0] = tmprow; - col = 0; - row++; - } - } - // Flush the final bits - if (nextbits < 8) { - out[w++] = next; - if (next == 0xff) out[w++] = 0x0; - } -#ifdef DEBUG - int sort[17]; - for (int h = 0; h < 17; h++) { - sort[h] = h; - printf("%d:%d\n", h, self->hist[h]); - } - printf("Total bytes: %d\n", bitcount >> 3); -#endif - free(rowcache); - self->encodedWritten = w; -} - -/* Encoder - * Read tile from an image and encode in one shot - * Return the encoded data - */ -int lj92_encode(uint16_t *image, int width, int height, int bitdepth, - int readLength, int skipLength, uint16_t *delinearize, - int delinearizeLength, uint8_t **encoded, int *encodedLength) { - int ret = LJ92_ERROR_NONE; - - lje *self = (lje *)calloc(sizeof(lje), 1); - if (self == NULL) return LJ92_ERROR_NO_MEMORY; - self->image = image; - self->width = width; - self->height = height; - self->bitdepth = bitdepth; - self->readLength = readLength; - self->skipLength = skipLength; - self->delinearize = delinearize; - self->delinearizeLength = delinearizeLength; - self->encodedLength = width * height * 3 + 200; - self->encoded = (uint8_t*)malloc(self->encodedLength); - if (self->encoded == NULL) { - free(self); - return LJ92_ERROR_NO_MEMORY; - } - // Scan through data to gather frequencies of ssss prefixes - ret = frequencyScan(self); - if (ret != LJ92_ERROR_NONE) { - free(self->encoded); - free(self); - return ret; - } - // Create encoded table based on frequencies - createEncodeTable(self); - // Write JPEG head and scan header - writeHeader(self); - // Scan through and do the compression - writeBody(self); - // Finish - writePost(self); -#ifdef DEBUG - printf("written:%d\n", self->encodedWritten); -#endif - self->encoded = (uint8_t*)realloc(self->encoded, self->encodedWritten); - self->encodedLength = self->encodedWritten; - *encoded = self->encoded; - *encodedLength = self->encodedLength; - - free(self); - - return ret; -} - -// End liblj92 --------------------------------------------------------- - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - -} // namespace detail - -#ifdef __clang__ -#pragma clang diagnostic push -#if __has_warning("-Wzero-as-null-pointer-constant") -#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" -#endif -#endif - -// -// TinyDNGWriter stores IFD table in the end of file so that offset to -// image data can be easily computed. -// -// +----------------------+ -// | header | -// +----------------------+ -// | | -// | image & meta 0 | -// | | -// +----------------------+ -// | | -// | image & meta 1 | -// | | -// +----------------------+ -// ... -// +----------------------+ -// | | -// | image & meta N | -// | | -// +----------------------+ -// | | -// | IFD 0 | -// | | -// +----------------------+ -// | | -// | IFD 1 | -// | | -// +----------------------+ -// ... -// +----------------------+ -// | | -// | IFD 2 | -// | | -// +----------------------+ -// - -// From tiff.h -typedef enum { - TIFF_NOTYPE = 0, /* placeholder */ - TIFF_BYTE = 1, /* 8-bit unsigned integer */ - TIFF_ASCII = 2, /* 8-bit bytes w/ last byte null */ - TIFF_SHORT = 3, /* 16-bit unsigned integer */ - TIFF_LONG = 4, /* 32-bit unsigned integer */ - TIFF_RATIONAL = 5, /* 64-bit unsigned fraction */ - TIFF_SBYTE = 6, /* !8-bit signed integer */ - TIFF_UNDEFINED = 7, /* !8-bit untyped data */ - TIFF_SSHORT = 8, /* !16-bit signed integer */ - TIFF_SLONG = 9, /* !32-bit signed integer */ - TIFF_SRATIONAL = 10, /* !64-bit signed fraction */ - TIFF_FLOAT = 11, /* !32-bit IEEE floating point */ - TIFF_DOUBLE = 12, /* !64-bit IEEE floating point */ - TIFF_IFD = 13, /* %32-bit unsigned integer (offset) */ - TIFF_LONG8 = 16, /* BigTIFF 64-bit unsigned integer */ - TIFF_SLONG8 = 17, /* BigTIFF 64-bit signed integer */ - TIFF_IFD8 = 18 /* BigTIFF 64-bit unsigned integer (offset) */ -} DataType; - -const static int kHeaderSize = 8; // TIFF header size. - -// floating point to integer rational value conversion -// https://stackoverflow.com/questions/51142275/exact-value-of-a-floating-point-number-as-a-rational -// -// Return error flag -static int DoubleToRational(double x, double *numerator, double *denominator) { - if (!std::isfinite(x)) { - *numerator = *denominator = 0.0; - if (x > 0.0) *numerator = 1.0; - if (x < 0.0) *numerator = -1.0; - return 1; - } - - // TIFF Rational use two uint32's, so reduce the bits - int bdigits = FLT_MANT_DIG; - int expo; - *denominator = 1.0; - *numerator = std::frexp(x, &expo) * std::pow(2.0, bdigits); - expo -= bdigits; - if (expo > 0) { - *numerator *= std::pow(2.0, expo); - } else if (expo < 0) { - expo = -expo; - if (expo >= FLT_MAX_EXP - 1) { - *numerator /= std::pow(2.0, expo - (FLT_MAX_EXP - 1)); - *denominator *= std::pow(2.0, FLT_MAX_EXP - 1); - return fabs(*numerator) < 1.0; - } else { - *denominator *= std::pow(2.0, expo); - } - } - - while (((std::fabs(*numerator) > 0.0) && - (std::fabs(std::fmod(*numerator, 2)) < - std::numeric_limits::epsilon()) && - (std::fabs(std::fmod(*denominator, 2)) < - std::numeric_limits::epsilon())) || - (std::max(std::fabs(*numerator), std::fabs(*denominator)) > - std::numeric_limits::max())) { - *numerator /= 2.0; - *denominator /= 2.0; - } - return 0; -} - -static inline bool IsBigEndian() { - unsigned int i = 0x01020304; - char c[4]; - memcpy(c, &i, 4); - return (c[0] == 1); -} - -static void swap2(unsigned short *val) { - unsigned short tmp = *val; - unsigned char *dst = reinterpret_cast(val); - unsigned char *src = reinterpret_cast(&tmp); - - dst[0] = src[1]; - dst[1] = src[0]; -} - -static void swap4(unsigned int *val) { - unsigned int tmp = *val; - unsigned char *dst = reinterpret_cast(val); - unsigned char *src = reinterpret_cast(&tmp); - - dst[0] = src[3]; - dst[1] = src[2]; - dst[2] = src[1]; - dst[3] = src[0]; -} - -static void swap8(uint64_t *val) { - uint64_t tmp = *val; - unsigned char *dst = reinterpret_cast(val); - unsigned char *src = reinterpret_cast(&tmp); - - dst[0] = src[7]; - dst[1] = src[6]; - dst[2] = src[5]; - dst[3] = src[4]; - dst[4] = src[3]; - dst[5] = src[2]; - dst[6] = src[1]; - dst[7] = src[0]; -} - -static void Write1(const unsigned char c, std::ostringstream *out) { - unsigned char value = c; - out->write(reinterpret_cast(&value), 1); -} - -static void Write2(const unsigned short c, std::ostringstream *out, - const bool swap_endian) { - unsigned short value = c; - if (swap_endian) { - swap2(&value); - } - - out->write(reinterpret_cast(&value), 2); -} - -static void Write4(const unsigned int c, std::ostringstream *out, - const bool swap_endian) { - unsigned int value = c; - if (swap_endian) { - swap4(&value); - } - - out->write(reinterpret_cast(&value), 4); -} - -static bool WriteTIFFTag(const unsigned short tag, const unsigned short type, - const unsigned int count, const unsigned char *data, - std::vector *tags_out, - std::ostringstream *data_out) { - assert(sizeof(IFDTag) == - 12); // FIXME(syoyo): Use static_assert for C++11 compiler - - IFDTag ifd; - ifd.tag = tag; - ifd.type = type; - ifd.count = count; - - size_t typesize_table[] = {1, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4}; - - size_t len = count * (typesize_table[(type) < 14 ? (type) : 0]); - if (len > 4) { - assert(data_out); - if (!data_out) { - return false; - } - - // Store offset value. - - unsigned int offset = - static_cast(data_out->tellp()) + kHeaderSize; - ifd.offset_or_value = offset; - - data_out->write(reinterpret_cast(data), - static_cast(len)); - - } else { - ifd.offset_or_value = 0; - - // less than 4 bytes = store data itself. - if (len == 1) { - unsigned char value = *(data); - memcpy(&(ifd.offset_or_value), &value, sizeof(unsigned char)); - } else if (len == 2) { - unsigned short value = *(reinterpret_cast(data)); - memcpy(&(ifd.offset_or_value), &value, sizeof(unsigned short)); - } else if (len == 4) { - unsigned int value = *(reinterpret_cast(data)); - ifd.offset_or_value = value; - } else { - assert(0); - } - } - - tags_out->push_back(ifd); - - return true; -} - -static bool WriteTIFFVersionHeader(std::ostringstream *out, bool big_endian) { - // TODO(syoyo): Support BigTIFF? - - // 4d 4d = Big endian. 49 49 = Little endian. - if (big_endian) { - Write1(0x4d, out); - Write1(0x4d, out); - Write1(0x0, out); - Write1(0x2a, out); // Tiff version ID - } else { - Write1(0x49, out); - Write1(0x49, out); - Write1(0x2a, out); // Tiff version ID - Write1(0x0, out); - } - - return true; -} - -DNGImage::DNGImage() - : dng_big_endian_(true), - num_fields_(0), - samples_per_pixels_(0), - data_strip_offset_{0}, - data_strip_bytes_{0} { - swap_endian_ = (IsBigEndian() != dng_big_endian_); -} - -void DNGImage::SetBigEndian(bool big_endian) { - dng_big_endian_ = big_endian; - swap_endian_ = (IsBigEndian() != dng_big_endian_); -} - -bool DNGImage::SetSubfileType(bool reduced_image, bool page, bool mask) { - unsigned int count = 1; - - unsigned int bits = 0; - if (reduced_image) { - bits |= FILETYPE_REDUCEDIMAGE; - } - if (page) { - bits |= FILETYPE_PAGE; - } - if (mask) { - bits |= FILETYPE_MASK; - } - - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_SUB_FILETYPE), TIFF_LONG, count, - reinterpret_cast(&bits), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetImageWidth(const unsigned int width) { - unsigned int count = 1; - - unsigned int data = width; - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_IMAGE_WIDTH), TIFF_LONG, count, - reinterpret_cast(&data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetImageLength(const unsigned int length) { - unsigned int count = 1; - - const unsigned int data = length; - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_IMAGE_LENGTH), TIFF_LONG, count, - reinterpret_cast(&data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetRowsPerStrip(const unsigned int rows) { - if (rows == 0) { - return false; - } - - unsigned int count = 1; - - const unsigned int data = rows; - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_ROWS_PER_STRIP), TIFF_LONG, count, - reinterpret_cast(&data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetSamplesPerPixel(const unsigned short value) { - if (value > 4) { - { - std::stringstream ss; - ss << "Samples per pixel must be less than or equal to 4, but got " << value << ".\n"; - err_ += ss.str(); - } - return false; - } - - unsigned int count = 1; - - const unsigned short data = value; - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_SAMPLES_PER_PIXEL), TIFF_SHORT, count, - reinterpret_cast(&data), &ifd_tags_, &data_os_); - - if (!ret) { - err_ += "Failed to write `TIFFTAG_SAMPLES_PER_PIXEL` tag.\n"; - return false; - } - - samples_per_pixels_ = value; // Store SPP for later use. - - num_fields_++; - return true; -} - -bool DNGImage::SetBitsPerSample(const unsigned int num_samples, - const unsigned short *values) { - // `SetSamplesPerPixel()` must be called in advance and SPP shoud be equal to - // `num_samples`. - - if (samples_per_pixels_ == 0) { - err_ += "SetSamplesPerPixel() must be called before SetBitsPerSample().\n"; - return false; - } - - if ((num_samples == 0) || (num_samples > 4)) { - std::stringstream ss; - ss << "Invalid number of samples: " << num_samples << "\n"; - err_ += ss.str(); - return false; - } else if (num_samples != samples_per_pixels_) { - std::stringstream ss; - ss << "Samples per pixel mismatch. " << num_samples << " is given for SetBitsPerSample(), but SamplesPerPixel is set to " << samples_per_pixels_ << "\n"; - err_ += ss.str(); - return false; - } else { - // ok - } - - unsigned short bps = values[0]; - - std::vector vs(num_samples); - for (size_t i = 0; i < vs.size(); i++) { - // FIXME(syoyo): Currently bps must be same for all samples - if (bps != values[i]) { - err_ += "BitsPerSample must be same among samples at the moment.\n"; - return false; - } - - vs[i] = values[i]; - - // TODO(syoyo): Swap values when writing IFD tag, not here. - if (swap_endian_) { - swap2(&vs[i]); - } - } - - unsigned int count = num_samples; - - bool ret = WriteTIFFTag(static_cast(TIFFTAG_BITS_PER_SAMPLE), - TIFF_SHORT, count, - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - // Store BPS for later use. - bits_per_samples_.resize(num_samples); - for (size_t i = 0; i < num_samples; i++) { - bits_per_samples_[i] = values[i]; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetPhotometric(const unsigned short value) { - if ((value == PHOTOMETRIC_LINEARRAW) || - (value == PHOTOMETRIC_CFA) || - (value == PHOTOMETRIC_RGB) || - (value == PHOTOMETRIC_WHITE_IS_ZERO) || - (value == PHOTOMETRIC_BLACK_IS_ZERO)) { - // OK - } else { - return false; - } - - unsigned int count = 1; - - const unsigned short data = value; - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_PHOTOMETRIC), TIFF_SHORT, count, - reinterpret_cast(&data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetPlanarConfig(const unsigned short value) { - unsigned int count = 1; - - if ((value == PLANARCONFIG_CONTIG) || (value == PLANARCONFIG_SEPARATE)) { - // OK - } else { - return false; - } - - const unsigned short data = value; - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_PLANAR_CONFIG), TIFF_SHORT, count, - reinterpret_cast(&data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetCompression(const unsigned short value) { - unsigned int count = 1; - - const unsigned short data = value; - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_COMPRESSION), TIFF_SHORT, count, - reinterpret_cast(&data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetSampleFormat(const unsigned int num_samples, - const unsigned short *values) { - // `SetSamplesPerPixel()` must be called in advance - if ((num_samples > 0) && (num_samples == samples_per_pixels_)) { - // OK - } else { - err_ += "SetSamplesPerPixel() must be called before SetSampleFormat().\n"; - return false; - } - - unsigned short format = values[0]; - - std::vector vs(num_samples); - for (size_t i = 0; i < vs.size(); i++) { - // FIXME(syoyo): Currently format must be same for all samples - if (format != values[i]) { - err_ += "SampleFormat must be same among samples at the moment.\n"; - return false; - } - - if ((format == SAMPLEFORMAT_UINT) || (format == SAMPLEFORMAT_INT) || - (format == SAMPLEFORMAT_IEEEFP)) { - // OK - } else { - err_ += "Invalid format value specified for SetSampleFormat().\n"; - return false; - } - - vs[i] = values[i]; - - // TODO(syoyo): Swap values when writing IFD tag, not here. - if (swap_endian_) { - swap2(&vs[i]); - } - } - - unsigned int count = num_samples; - - bool ret = WriteTIFFTag(static_cast(TIFFTAG_SAMPLEFORMAT), - TIFF_SHORT, count, - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetOrientation(const unsigned short value) { - unsigned int count = 1; - - if ((value == ORIENTATION_TOPLEFT) || (value == ORIENTATION_TOPRIGHT) || - (value == ORIENTATION_BOTRIGHT) || (value == ORIENTATION_BOTLEFT) || - (value == ORIENTATION_LEFTTOP) || (value == ORIENTATION_RIGHTTOP) || - (value == ORIENTATION_RIGHTBOT) || (value == ORIENTATION_LEFTBOT)) { - // OK - } else { - return false; - } - - const unsigned int data = value; - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_ORIENTATION), TIFF_SHORT, count, - reinterpret_cast(&data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetBlackLevel(const unsigned int num_components, - const unsigned short *values) { - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_BLACK_LEVEL), TIFF_SHORT, num_components, - reinterpret_cast(values), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetBlackLevelRational(unsigned int num_samples, - const double *values) { - // `SetSamplesPerPixel()` must be called in advance and SPP shoud be equal to - // `num_samples`. - if ((num_samples > 0) && (num_samples == samples_per_pixels_)) { - // OK - } else { - return false; - } - - std::vector vs(num_samples * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - - unsigned int count = num_samples; - - bool ret = WriteTIFFTag(static_cast(TIFFTAG_BLACK_LEVEL), - TIFF_RATIONAL, count, - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetWhiteLevelRational(unsigned int num_samples, - const double *values) { - // `SetSamplesPerPixel()` must be called in advance and SPP shoud be equal to - // `num_samples`. - if ((num_samples > 0) && (num_samples == samples_per_pixels_)) { - // OK - } else { - return false; - } - - std::vector vs(num_samples * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - - unsigned int count = num_samples; - - bool ret = WriteTIFFTag(static_cast(TIFFTAG_WHITE_LEVEL), - TIFF_RATIONAL, count, - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetXResolution(const double value) { - double numerator, denominator; - if (DoubleToRational(value, &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - unsigned int data[2]; - data[0] = static_cast(numerator); - data[1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&data[0]); - swap4(&data[1]); - } - - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_XRESOLUTION), TIFF_RATIONAL, 1, - reinterpret_cast(data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetYResolution(const double value) { - double numerator, denominator; - if (DoubleToRational(value, &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - unsigned int data[2]; - data[0] = static_cast(numerator); - data[1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&data[0]); - swap4(&data[1]); - } - - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_YRESOLUTION), TIFF_RATIONAL, 1, - reinterpret_cast(data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetResolutionUnit(const unsigned short value) { - unsigned int count = 1; - - if ((value == RESUNIT_NONE) || (value == RESUNIT_INCH) || - (value == RESUNIT_CENTIMETER)) { - // OK - } else { - return false; - } - - const unsigned short data = value; - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_RESOLUTION_UNIT), TIFF_SHORT, count, - reinterpret_cast(&data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetImageDescription(const std::string &ascii) { - unsigned int count = - static_cast(ascii.length() + 1); // +1 for '\0' - - if (count < 2) { - // empty string - return false; - } - - if (count > (1024 * 1024)) { - // too large - return false; - } - - bool ret = WriteTIFFTag(static_cast(TIFFTAG_IMAGEDESCRIPTION), - TIFF_ASCII, count, - reinterpret_cast(ascii.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetUniqueCameraModel(const std::string &ascii) { - unsigned int count = - static_cast(ascii.length() + 1); // +1 for '\0' - - if (count < 2) { - // empty string - return false; - } - - if (count > (1024 * 1024)) { - // too large - return false; - } - - bool ret = WriteTIFFTag(static_cast(TIFFTAG_UNIQUE_CAMERA_MODEL), - TIFF_ASCII, count, - reinterpret_cast(ascii.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetSoftware(const std::string &ascii) { - unsigned int count = - static_cast(ascii.length() + 1); // +1 for '\0' - - if (count < 2) { - // empty string - return false; - } - - if (count > 4096) { - // too large - return false; - } - - bool ret = WriteTIFFTag(static_cast(TIFFTAG_SOFTWARE), - TIFF_ASCII, count, - reinterpret_cast(ascii.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - - -bool DNGImage::SetActiveArea(const unsigned int values[4]) { - unsigned int count = 4; - - const unsigned int *data = values; - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_ACTIVE_AREA), TIFF_LONG, count, - reinterpret_cast(data), &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetDNGVersion(const unsigned char a, - const unsigned char b, - const unsigned char c, - const unsigned char d) { - unsigned char data[4] = {a, b, c, d}; - - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_DNG_VERSION), TIFF_BYTE, 4, - reinterpret_cast(data), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetColorMatrix1(const unsigned int plane_count, - const double *matrix_values) { - std::vector vs(plane_count * 3 * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - bool ret = WriteTIFFTag(static_cast(TIFFTAG_COLOR_MATRIX1), - TIFF_SRATIONAL, uint32_t(vs.size() / 2), - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetColorMatrix2(const unsigned int plane_count, - const double *matrix_values) { - std::vector vs(plane_count * 3 * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - bool ret = WriteTIFFTag(static_cast(TIFFTAG_COLOR_MATRIX2), - TIFF_SRATIONAL, uint32_t(vs.size() / 2), - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetForwardMatrix1(const unsigned int plane_count, - const double *matrix_values) { - std::vector vs(plane_count * 3 * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - bool ret = WriteTIFFTag(static_cast(TIFFTAG_FORWARD_MATRIX1), - TIFF_SRATIONAL, uint32_t(vs.size() / 2), - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetForwardMatrix2(const unsigned int plane_count, - const double *matrix_values) { - std::vector vs(plane_count * 3 * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - bool ret = WriteTIFFTag(static_cast(TIFFTAG_FORWARD_MATRIX2), - TIFF_SRATIONAL, uint32_t(vs.size() / 2), - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetCameraCalibration1(const unsigned int plane_count, - const double *matrix_values) { - std::vector vs(plane_count * plane_count * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - bool ret = WriteTIFFTag(static_cast(TIFFTAG_CAMERA_CALIBRATION1), - TIFF_SRATIONAL, uint32_t(vs.size() / 2), - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetCameraCalibration2(const unsigned int plane_count, - const double *matrix_values) { - std::vector vs(plane_count * plane_count * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - bool ret = WriteTIFFTag(static_cast(TIFFTAG_CAMERA_CALIBRATION2), - TIFF_SRATIONAL, uint32_t(vs.size() / 2), - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetAnalogBalance(const unsigned int plane_count, - const double *matrix_values) { - std::vector vs(plane_count * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - bool ret = WriteTIFFTag(static_cast(TIFFTAG_ANALOG_BALANCE), - TIFF_RATIONAL, uint32_t(vs.size() / 2), - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetCFARepeatPatternDim(const unsigned short width, - const unsigned short height) { - unsigned short data[2] = {width, height}; - - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_CFA_REPEAT_PATTERN_DIM), TIFF_SHORT, 2, - reinterpret_cast(data), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetBlackLevelRepeatDim(const unsigned short width, - const unsigned short height) { - unsigned short data[2] = {width, height}; - - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_BLACK_LEVEL_REPEAT_DIM), TIFF_SHORT, 2, - reinterpret_cast(data), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetCalibrationIlluminant1(const unsigned short value) { - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_CALIBRATION_ILLUMINANT1), TIFF_SHORT, 1, - reinterpret_cast(&value), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetCalibrationIlluminant2(const unsigned short value) { - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_CALIBRATION_ILLUMINANT2), TIFF_SHORT, 1, - reinterpret_cast(&value), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetCFAPattern(const unsigned int num_components, - const unsigned char *values) { - if ((values == NULL) || (num_components < 1)) { - return false; - } - - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_CFA_PATTERN), TIFF_BYTE, num_components, - reinterpret_cast(values), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetAsShotNeutral(const unsigned int plane_count, - const double *matrix_values) { - std::vector vs(plane_count * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(matrix_values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - bool ret = WriteTIFFTag(static_cast(TIFFTAG_AS_SHOT_NEUTRAL), - TIFF_RATIONAL, uint32_t(vs.size() / 2), - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetAsShotWhiteXY(const double x, const double y) { - const double values[2] = {x, y}; - std::vector vs(2 * 2); - for (size_t i = 0; i * 2 < vs.size(); i++) { - double numerator, denominator; - if (DoubleToRational(values[i], &numerator, &denominator) != 0) { - // Couldn't represent fp value as integer rational value. - return false; - } - - vs[2 * i + 0] = static_cast(numerator); - vs[2 * i + 1] = static_cast(denominator); - - // TODO(syoyo): Swap rational value(8 bytes) when writing IFD tag, not here. - if (swap_endian_) { - swap4(&vs[2 * i + 0]); - swap4(&vs[2 * i + 1]); - } - } - bool ret = WriteTIFFTag(static_cast(TIFFTAG_AS_SHOT_WHITE_XY), - TIFF_RATIONAL, uint32_t(vs.size() / 2), - reinterpret_cast(vs.data()), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetImageDataPacked(const unsigned short *input_buffer, const int input_count, const unsigned int input_bpp, bool big_endian) -{ - -#ifndef ROL32 -#define ROL32(v,a) ((v) << (a) | (v) >> (32-(a))) -#endif - -#ifndef ROL16 -#define ROL16(v,a) ((v) << (a) | (v) >> (16-(a))) -#endif - - if (input_count <= 0) { - return false; - } - - if (input_bpp > 16) - return false; - - unsigned int bits_free = 16 - input_bpp; - const unsigned short *unpacked_bits = input_buffer; - - std::vector output(static_cast(input_count)); - unsigned short *packed_bits = output.data(); - - packed_bits[0] = static_cast(unpacked_bits[0] << bits_free); - for (unsigned int pixel_index = 1; pixel_index < static_cast(input_count); pixel_index++) - { - unsigned int bits_offset = (pixel_index * bits_free) % 16; - unsigned int bits_to_rol = bits_free + bits_offset + (bits_offset > 0) * 16; - - unsigned int data = ROL32(static_cast(unpacked_bits[pixel_index]), bits_to_rol); - *(reinterpret_cast(packed_bits)) = (*(reinterpret_cast(packed_bits)) & 0x0000FFFF) | data; - - if(bits_offset > 0 && bits_offset <= input_bpp) - { - if(big_endian) - *(reinterpret_cast(packed_bits)) = static_cast(ROL16(*(reinterpret_cast(packed_bits)), 8)); - - ++packed_bits; - } - } - - return SetImageData(reinterpret_cast(output.data()), output.size() * sizeof(unsigned short)); - -#undef ROL32 -#undef ROL16 -} - -bool DNGImage::SetImageData(const unsigned char *data, const size_t data_len) { - if ((data == NULL) || (data_len < 1)) { - return false; - } - - data_strip_offset_ = size_t(data_os_.tellp()); - data_strip_bytes_ = data_len; - - data_os_.write(reinterpret_cast(data), - static_cast(data_len)); - - // NOTE: STRIP_OFFSET tag will be written at `WriteIFDToStream()`. - - { - unsigned int count = 1; - unsigned int bytes = static_cast(data_len); - - bool ret = WriteTIFFTag( - static_cast(TIFFTAG_STRIP_BYTE_COUNTS), TIFF_LONG, - count, reinterpret_cast(&bytes), &ifd_tags_, - NULL); - - if (!ret) { - return false; - } - - num_fields_++; - } - - return true; -} - -bool DNGImage::SetImageDataJpeg(const unsigned short *data, unsigned int width, - unsigned int height, unsigned int bpp) { - if ((data == NULL) || (height % 2 == 1) || (width % 2 == 1)) { - return false; - } - - uint8_t *compressed = NULL; - int output_buffer_size = 0; - - // Width x2 to move each second line - // ----------- - // Before: - // - // GRGRGR... - // BGBGBG... - // GRGRGR... - // BGBGBG... - // ----------- - // After: - // - // GRGRGR...BGBGBG... - // GRGRGR...BGBGBG... - // ----------- - int new_width = int(width * 2); - int new_height = int(height / 2); - - // Encode image - int ret = detail::lj92_encode(const_cast(data), new_width, new_height, int(bpp), - new_width * new_height, 0, NULL, 0, &compressed, - &output_buffer_size); - - if (ret != detail::LJ92_ERROR_NONE) - return false; - - bool sid_res = SetImageData(compressed, size_t(output_buffer_size)); - - if (compressed) - free(compressed); - - return sid_res; -} - -bool DNGImage::SetCustomFieldLong(const unsigned short tag, const int value) { - unsigned int count = 1; - - // TODO(syoyo): Check if `tag` value does not conflict with existing TIFF tag - // value. - - bool ret = WriteTIFFTag(tag, TIFF_SLONG, count, - reinterpret_cast(&value), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -bool DNGImage::SetCustomFieldULong(const unsigned short tag, - const unsigned int value) { - unsigned int count = 1; - - // TODO(syoyo): Check if `tag` value does not conflict with existing TIFF tag - // value. - - bool ret = WriteTIFFTag(tag, TIFF_LONG, count, - reinterpret_cast(&value), - &ifd_tags_, &data_os_); - - if (!ret) { - return false; - } - - num_fields_++; - return true; -} - -static bool IFDComparator(const IFDTag &a, const IFDTag &b) { - return (a.tag < b.tag); -} - -bool DNGImage::WriteDataToStream(std::ostream *ofs) const { - if ((data_os_.str().length() == 0)) { - err_ += "Empty IFD data and image data.\n"; - return false; - } - - if (bits_per_samples_.empty()) { - err_ += "BitsPerSample is not set\n"; - return false; - } - - for (size_t i = 0; i < bits_per_samples_.size(); i++) { - if (bits_per_samples_[i] == 0) { - err_ += std::to_string(i) + "'th BitsPerSample is zero"; - return false; - } - } - - if (samples_per_pixels_ == 0) { - err_ += "SamplesPerPixels is not set or zero."; - return false; - } - - std::vector data(data_os_.str().length()); - memcpy(data.data(), data_os_.str().data(), data.size()); - - if (data_strip_bytes_ == 0) { - // May ok?. - } else { - // FIXME(syoyo): Assume all channels use sample bps - uint32_t bps = bits_per_samples_[0]; - - // We may need to swap endian for pixel data. - if (swap_endian_) { - if (bps == 16) { - size_t n = data_strip_bytes_ / sizeof(uint16_t); - uint16_t *ptr = - reinterpret_cast(data.data() + data_strip_offset_); - - for (size_t i = 0; i < n; i++) { - swap2(&ptr[i]); - } - - } else if (bps == 32) { - size_t n = data_strip_bytes_ / sizeof(uint32_t); - uint32_t *ptr = - reinterpret_cast(data.data() + data_strip_offset_); - - for (size_t i = 0; i < n; i++) { - swap4(&ptr[i]); - } - - } else if (bps == 64) { - size_t n = data_strip_bytes_ / sizeof(uint64_t); - uint64_t *ptr = - reinterpret_cast(data.data() + data_strip_offset_); - - for (size_t i = 0; i < n; i++) { - swap8(&ptr[i]); - } - } - } - } - - ofs->write(reinterpret_cast(data.data()), - static_cast(data.size())); - - return true; -} - -bool DNGImage::WriteIFDToStream(const unsigned int data_base_offset, - const unsigned int strip_offset, - std::ostream *ofs) const { - if ((num_fields_ == 0) || (ifd_tags_.size() < 1)) { - err_ += "No TIFF Tags.\n"; - return false; - } - - // add STRIP_OFFSET tag and sort IFD tags. - std::vector tags = ifd_tags_; - { - // For STRIP_OFFSET we need the actual offset value to data(image), - // thus write STRIP_OFFSET here. - unsigned int offset = strip_offset + kHeaderSize; - IFDTag ifd; - ifd.tag = TIFFTAG_STRIP_OFFSET; - ifd.type = TIFF_LONG; - ifd.count = 1; - ifd.offset_or_value = offset; - tags.push_back(ifd); - } - - // TIFF expects IFD tags are sorted. - std::sort(tags.begin(), tags.end(), IFDComparator); - - std::ostringstream ifd_os; - - unsigned short num_fields = static_cast(tags.size()); - - Write2(num_fields, &ifd_os, swap_endian_); - - { - size_t typesize_table[] = {1, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4}; - - for (size_t i = 0; i < tags.size(); i++) { - const IFDTag &ifd = tags[i]; - Write2(ifd.tag, &ifd_os, swap_endian_); - Write2(ifd.type, &ifd_os, swap_endian_); - Write4(ifd.count, &ifd_os, swap_endian_); - - size_t len = - ifd.count * (typesize_table[(ifd.type) < 14 ? (ifd.type) : 0]); - if (len > 4) { - // Store offset value. - unsigned int ifd_offt = ifd.offset_or_value + data_base_offset; - Write4(ifd_offt, &ifd_os, swap_endian_); - } else { - // less than 4 bytes = store data itself. - - if (len == 1) { - const unsigned char value = - *(reinterpret_cast(&ifd.offset_or_value)); - Write1(value, &ifd_os); - unsigned char pad = 0; - Write1(pad, &ifd_os); - Write1(pad, &ifd_os); - Write1(pad, &ifd_os); - } else if (len == 2) { - const unsigned short value = - *(reinterpret_cast(&ifd.offset_or_value)); - Write2(value, &ifd_os, swap_endian_); - const unsigned short pad = 0; - Write2(pad, &ifd_os, swap_endian_); - } else if (len == 4) { - const unsigned int value = - *(reinterpret_cast(&ifd.offset_or_value)); - Write4(value, &ifd_os, swap_endian_); - } else { - assert(0); - } - } - } - - ofs->write(ifd_os.str().c_str(), - static_cast(ifd_os.str().length())); - } - - return true; -} - -// ------------------------------------------- - -DNGWriter::DNGWriter(bool big_endian) : dng_big_endian_(big_endian) { - swap_endian_ = (IsBigEndian() != dng_big_endian_); -} - -bool DNGWriter::WriteToFile(const char *filename, std::string *err) const { - std::ofstream ofs(filename, std::ostream::binary); - - if (!ofs) { - if (err) { - (*err) = "Failed to open file.\n"; - } - - return false; - } - - std::ostringstream header; - bool ret = WriteTIFFVersionHeader(&header, dng_big_endian_); - if (!ret) { - if (err) { - (*err) = "Failed to write TIFF version header.\n"; - } - return false; - } - - if (images_.size() == 0) { - if (err) { - (*err) = "No image added for writing.\n"; - } - - return false; - } - - // 1. Compute offset and data size(exclude TIFF header bytes) - size_t data_len = 0; - size_t strip_offset = 0; - std::vector data_offset_table; - std::vector strip_offset_table; - for (size_t i = 0; i < images_.size(); i++) { - strip_offset = data_len + images_[i]->GetStripOffset(); - data_offset_table.push_back(data_len); - strip_offset_table.push_back(strip_offset); - data_len += images_[i]->GetDataSize(); - } - - // 2. Write offset to ifd table. - const unsigned int ifd_offset = - kHeaderSize + static_cast(data_len); - Write4(ifd_offset, &header, swap_endian_); - - assert(header.str().length() == 8); - - // std::cout << "ifd_offset " << ifd_offset << std::endl; - // std::cout << "data_len " << data_os_.str().length() << std::endl; - // std::cout << "ifd_len " << ifd_os_.str().length() << std::endl; - // std::cout << "swap endian " << swap_endian_ << std::endl; - - // 3. Write header - ofs.write(header.str().c_str(), - static_cast(header.str().length())); - - // 4. Write image and meta data - // TODO(syoyo): Write IFD first, then image/meta data - for (size_t i = 0; i < images_.size(); i++) { - bool ok = images_[i]->WriteDataToStream(&ofs); - if (!ok) { - if (err) { - std::stringstream ss; - ss << "Failed to write data at image[" << i << "]. err = " << images_[i]->Error() << "\n"; - (*err) += ss.str(); - } - return false; - } - } - - // 5. Write IFD entries; - for (size_t i = 0; i < images_.size(); i++) { - bool ok = images_[i]->WriteIFDToStream( - static_cast(data_offset_table[i]), - static_cast(strip_offset_table[i]), &ofs); - if (!ok) { - if (err) { - std::stringstream ss; - ss << "Failed to write IFD at image[" << i << "]. err = " << images_[i]->Error() << "\n"; - (*err) += ss.str(); - } - return false; - } - - unsigned int next_ifd_offset = - static_cast(ofs.tellp()) + sizeof(unsigned int); - - if (i == (images_.size() - 1)) { - // Write zero as IFD offset(= end of data) - next_ifd_offset = 0; - } - - if (swap_endian_) { - swap4(&next_ifd_offset); - } - - ofs.write(reinterpret_cast(&next_ifd_offset), 4); - } - - return true; -} - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - -} // namespace tinydngwriter - -#endif // TINY_DNG_WRITER_IMPLEMENTATION diff --git a/src/libraries/tiny_dng_writer_impl.cpp b/src/libraries/tiny_dng_writer_impl.cpp deleted file mode 100644 index 48f221e..0000000 --- a/src/libraries/tiny_dng_writer_impl.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#define TINY_DNG_WRITER_IMPLEMENTATION -#include "tiny_dng_writer.h" diff --git a/src/managers/LogManager.cpp b/src/managers/LogManager.cpp index 7902c1b..4de27f3 100644 --- a/src/managers/LogManager.cpp +++ b/src/managers/LogManager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace photon { @@ -80,6 +81,8 @@ void LogManager::log(const QString& message, int level) { QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz"); out << QString("[ %1 ] [ %2 ] %3\n").arg(timestamp, enumLevelToStr(level), message); out.flush(); + if (level == FATAL) + QGuiApplication::quit(); } void LogManager::clearLog() { diff --git a/src/managers/LogManager.h b/src/managers/LogManager.h index d57f72f..9bfb9a6 100644 --- a/src/managers/LogManager.h +++ b/src/managers/LogManager.h @@ -15,7 +15,8 @@ enum LogLevel { DEBUG, INFO, WARNING, - ERROR + ERROR, + FATAL }; class LogManager : public QObject { @@ -44,6 +45,8 @@ class LogManager : public QObject { return WARNING; else if (level == "ERROR") return ERROR; + else if (level == "FATAL") + return FATAL; return DEBUG; } @@ -61,6 +64,9 @@ class LogManager : public QObject { case ERROR: return "ERROR"; + + case FATAL: + return "FATAL"; } return ""; } From b15da4a8a339db45de1932400ae94346fb1c2203 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Mon, 16 Mar 2026 18:45:16 +0100 Subject: [PATCH 09/29] wip: the panorama works, but the dng is not compatible with libraw --- panorama_report.md | 782 --------------------------------------------- 1 file changed, 782 deletions(-) delete mode 100644 panorama_report.md diff --git a/panorama_report.md b/panorama_report.md deleted file mode 100644 index ff445e5..0000000 --- a/panorama_report.md +++ /dev/null @@ -1,782 +0,0 @@ -# Panorama Stitching Technical Report - -## Executive Summary - -This report analyzes the panorama stitching implementation found in RapidRAW, a Rust-based RAW image processing application. The system implements a sophisticated feature-based stitching pipeline that combines classical computer vision algorithms with modern optimizations including parallel processing and adaptive seam finding. - ---- - -## Phase 1: Image Loading and Preprocessing - -### High-Level Overview - -The first phase converts RAW images into a processable format while preparing them for computationally expensive feature detection operations through intelligent downscaling. - -### Algorithmic Details - -**RAW Processing Pipeline:** - -```rust -let mut dynamic_image = load_base_image_from_bytes(&file_bytes, filename, false, 2.5, None)?; -if is_raw_file(filename) { - apply_cpu_default_raw_processing(&mut dynamic_image); -} -let image_f32 = dynamic_image.to_rgb32f(); -``` - -1. **RAW Decoding**: Uses internal RAW loader with exposure boost (2.5x) -2. **CPU Demosaicing**: Applies default RAW processing pipeline (likely using DCraw or similar) -3. **Format Conversion**: Converts to RGB32F (32-bit floating point per channel) for high dynamic range preservation - -**Adaptive Downscaling Strategy:** - -```rust -const MAX_PROCESSING_DIMENSION: u32 = 1600; - -pub fn calculate_downscale_dimensions(width: u32, height: u32) -> (u32, u32, f64) { - let long_side = width.max(height); - if long_side <= MAX_PROCESSING_DIMENSION { - return (width, height, 1.0); - } - let scale_factor = long_side as f64 / MAX_PROCESSING_DIMENSION as f64; - let new_width = (width as f64 / scale_factor).round() as u32; - let new_height = (height as f64 / scale_factor).round() as u32; - (new_width, new_height, scale_factor) -} -``` - -This ensures feature detection operates on manageable 1600px images regardless of input resolution, dramatically improving performance while maintaining sufficient detail for matching. - -**Low-Detail Mask Generation:** -A crucial preprocessing step identifies low-detail regions (sky, walls) using integral images for variance computation: - -```rust -pub fn generate_low_detail_mask(gray_full: &GrayImage) -> GrayImage { - let (sat, sat_sq) = build_integral_images(gray_full); - // Uses 32x32 windows, threshold = 60.0 variance -} -``` - -The algorithm: - -1. Computes Summed Area Table (SAT) for mean calculation: `SAT(x,y) = Σ_{i≤x, j≤y} I(i,j)` -2. Computes Squared SAT for variance: `Var = E[X²] - (E[X])²` -3. Marks pixels with variance < 60 as low-detail (mask value = 255) - -This mask is later used during blending to expand feathering widths in low-detail regions, preventing visible seams in smooth areas. - ---- - -## Phase 2: Feature Detection (FAST + BRIEF) - -### High-Level Overview - -The system employs a FAST corner detector combined with BRIEF descriptors—a computationally efficient alternative to SIFT/SURF that enables real-time performance without GPU acceleration. - -### Algorithmic Details - -**Step 2.1: Gaussian Preprocessing** - -```rust -let blurred_img_u8 = imageproc::filter::gaussian_blur_f32(img, 1.5); -let corners = corners_fast9(&blurred_img_u8, FAST_THRESHOLD); -``` - -**FAST9 Corner Detection:** - -- **Threshold**: 15 (intensity difference threshold) -- **Mechanism**: Checks 16 pixels on Bresenham circle around candidate pixel -- **Corner condition**: 9+ consecutive pixels with intensity > p + threshold OR < p - threshold -- **Complexity**: O(N) where N = number of pixels - -**Step 2.2: Non-Maximal Suppression** - -```rust -fn non_maximal_suppression(corners: &[Corner], radius: f32) -> Vec { - let radius_sq = radius * radius; // 15.0 pixels - // Sorts by corner score, suppresses neighbors within radius -} -``` - -Prevents feature clustering by keeping only the strongest corner in each 15-pixel radius neighborhood. - -**Step 2.3: BRIEF Descriptor Computation** - -```rust -pub const BRIEF_DESCRIPTOR_SIZE: usize = 256; -pub type Descriptor = [u8; BRIEF_DESCRIPTOR_SIZE / 8]; // 32 bytes -``` - -The BRIEF (Binary Robust Independent Elementary Features) algorithm: - -1. **Patch Extraction**: 32×32 window around each keypoint (smoothed with σ=2.0 Gaussian) -2. **Binary Tests**: Compares 256 pre-generated random pixel pairs within the patch -3. **Bit Packing**: Each test result becomes one bit in a 32-byte descriptor - -```rust -fn compute_brief_descriptor(img: &ImageBuffer, Vec>, - kp: &KeyPoint, patch_size: u32, - pairs: &[(Point2, Point2)]) -> Option { - for (i, pair) in pairs.iter().enumerate() { - let p1_x = (kp.x as i32 + pair.0.x) as u32; - let p1_y = (kp.y as i32 + pair.0.y) as u32; - let p2_x = (kp.x as i32 + pair.1.x) as u32; - let p2_y = (kp.y as i32 + pair.1.y) as u32; - - if img[p1_x, p1_y] < img[p2_x, p2_y] { - descriptor[byte_index] |= 1 << bit_index; - } - } -} -``` - -**Random Pair Generation (Deterministic):** - -```rust -pub fn generate_brief_pairs() -> Vec<(Point2, Point2)> { - let mut rng = StdRng::seed_from_u64(12345); // Fixed seed for reproducibility - let half_patch = BRIEF_PATCH_SIZE as i32 / 2; // 16 - // Generates 256 pairs from Uniform(-16, 16) distribution -} -``` - -**Advantages of FAST+BRIEF:** - -- **Speed**: Binary comparisons using XOR + popcount (CPU SIMD optimized) -- **Memory**: 32 bytes vs 128 bytes for SIFT -- **Rotation Invariance**: Limited (ORB improves this with orientation) -- **Scale Invariance**: Achieved through image pyramid (not implemented here) - ---- - -## Phase 3: Feature Matching with Ratio Test - -### High-Level Overview - -Features are matched between image pairs using Hamming distance, filtered by Lowe's ratio test to reject ambiguous matches. - -### Algorithmic Details - -**Hamming Distance Calculation:** - -```rust -fn hamming_distance(d1: &Descriptor, d2: &Descriptor) -> u32 { - d1.iter() - .zip(d2.iter()) - .map(|(b1, b2)| (b1 ^ b2).count_ones()) - .sum() -} -``` - -Computes population count (number of differing bits) between two 256-bit descriptors using CPU popcount instructions. - -**Brute-Force Matching with Ratio Test:** - -```rust -const MATCH_RATIO_THRESHOLD: f32 = 0.8; - -pub fn match_features(features1: &[Feature], features2: &[Feature]) -> Vec { - features1.par_iter().enumerate().filter_map(|(i, f1)| { - let mut best_dist = u32::MAX; - let mut second_best_dist = u32::MAX; - let mut best_idx = 0; - - for (j, f2) in features2.iter().enumerate() { - let dist = hamming_distance(&f1.descriptor, &f2.descriptor); - if dist < best_dist { - second_best_dist = best_dist; - best_dist = dist; - best_idx = j; - } else if dist < second_best_dist { - second_best_dist = dist; - } - } - - // Lowe's ratio test: best must be significantly better than second-best - if second_best_dist > 0 && - (best_dist as f32 / second_best_dist as f32) < MATCH_RATIO_THRESHOLD { - Some(Match { index1: i, index2: best_idx }) - } else { - None - } - }).collect() -} -``` - -**Lowe's Ratio Test (2004):** - -- Rejects matches where the best match is not significantly better than the second-best -- Threshold of 0.8 means best distance must be < 80% of second-best -- Effectively eliminates matches to repetitive patterns and ambiguous features - -**Computational Complexity:** O(N×M) for N features in image 1, M in image 2 - -- Parallelized using Rayon for multi-threading -- Could benefit from k-d tree or FLANN for large feature sets - ---- - -## Phase 4: Geometric Verification with RANSAC - -### High-Level Overview - -Matches are geometrically validated using RANSAC to estimate a homography transformation, filtering out outliers caused by moving objects, parallax, or mismatches. - -### Algorithmic Details - -**Random Sample Consensus (RANSAC) Loop:** - -```rust -const RANSAC_ITERATIONS: usize = 2500; -const RANSAC_INLIER_THRESHOLD: f64 = 5.0; // pixels -const MIN_INLIERS_FOR_CONNECTION: usize = 15; -``` - -**Algorithm Steps:** - -1. **Random Sampling**: Select 4 random match pairs (minimum for homography) -2. **Collinearity Check**: Reject degenerate configurations - - ```rust - fn are_points_collinear(p1: Point2, p2: Point2, p3: Point2) -> bool { - let area = p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y); - area.abs() < 1e-6 - } - ``` - -3. **Homography Estimation**: Solve 8-DOF transformation using DLT (Direct Linear Transform) - -**Direct Linear Transform (DLT):** - -```rust -pub fn compute_homography(points: &[(Point2, Point2)]) -> Option> { - // For N point pairs, builds 2N × 9 matrix A - // Each point pair contributes 2 rows: - // [-x, -y, -1, 0, 0, 0, x*x', y*x', x'] - // [0, 0, 0, -x, -y, -1, x*y', y*y', y'] - - let a = nalgebra::DMatrix::from_rows(&a_rows); - let svd = SVD::new(a, true, true); - let v_t = svd.v_t.expect("SVD failed to compute V_t"); - let h_vec = v_t.row(v_t.nrows() - 1).transpose(); - Some(Matrix3::from_iterator(h_vec.iter().cloned()).transpose()) -} -``` - -**Homography Matrix H (3×3):** - -``` -[x'] [h11 h12 h13] [x] -[y'] = [h21 h22 h23] [y] -[1 ] [h31 h32 h33] [1] -``` - -Maps points from image 1 to image 2 coordinates using projective transformation (preserves lines but not necessarily parallelism). - -1. **Inlier Counting**: Transform all points and count matches within threshold - - ```rust - let p2_transformed = Point2::new( - p2_h_transformed.x / p2_h_transformed.z, - p2_transformed.y / p2_h_transformed.z, - ); - let dist_sq = (p2.x - p2_transformed.x).powi(2) + (p2.y - p2_transformed.y).powi(2); - if dist_sq < ransac_inlier_threshold_sq { /* inlier */ } - ``` - -2. **Refinement**: Recompute homography using all inliers (more stable than single 4-point sample) - -**Scale Compensation:** -Since feature detection runs on downscaled images, the homography is scaled back to full resolution: - -```rust -let s1 = image_data[i].scale_factor; -let s2 = image_data[j].scale_factor; -let scale_mat_i_inv = Matrix3::new(1.0/s1, 0.0, 0.0, 0.0, 1.0/s1, 0.0, 0.0, 0.0, 1.0); -let scale_mat_j = Matrix3::new(s2, 0.0, 0.0, 0.0, s2, 0.0, 0.0, 0.0, 1.0); -let h_full = scale_mat_j * h_refined * scale_mat_i_inv; -``` - ---- - -## Phase 5: Graph-Based Image Ordering - -### High-Level Overview - -Images are organized into a Minimum Spanning Tree (MST) to determine optimal stitching order and compute global coordinate transformations. - -### Algorithmic Details - -**Disjoint Set Union (Union-Find) Structure:** - -```rust -struct DSU { - parent: Vec, -} - -impl DSU { - fn find(&mut self, i: usize) -> usize { - if self.parent[i] == i { i } - else { - self.parent[i] = self.find(self.parent[i]); // Path compression - self.parent[i] - } - } - - fn union(&mut self, i: usize, j: usize) { - let root_i = self.find(i); - let root_j = self.find(j); - if root_i != root_j { self.parent[root_i] = root_j; } - } -} -``` - -**Kruskal's MST Algorithm:** - -```rust -let mut edges = Vec::new(); -for (&(i, j), m) in matches { - edges.push((m.inliers, i, j)); -} -edges.sort_by_key(|&(inliers, _, _)| std::cmp::Reverse(inliers)); - -let mut dsu = DSU::new(n); -for &(_, i, j) in &edges { - if dsu.find(i) != dsu.find(j) { - dsu.union(i, j); - mst_adj.entry(i).or_default().push(j); - mst_adj.entry(j).or_default().push(i); - num_edges += 1; - if num_edges == n - 1 { break; } - } -} -``` - -Prioritizes connections with more inliers (more confident matches). - -**Breadth-First Traversal for Global Homographies:** - -```rust -let mut q = VecDeque::new(); -q.push_back((start_node, Matrix3::identity())); - -while let Some((u, h_u_global)) = q.pop_front() { - ordered_indices.push(u); - global_homographies.insert(u, h_u_global); - - for &v in neighbors { - if !visited.contains(&v) { - let h_vu = if let Some(m) = matches.get(&(v, u)) { - m.homography - } else { - matches.get(&(u, v)).unwrap().homography.try_inverse().unwrap() - }; - let h_v_global = h_u_global * h_vu; - q.push_back((v, h_v_global)); - } - } -} -``` - -Chains homographies through the MST to compute each image's transformation into the global panorama coordinate system. - ---- - -## Phase 6: Progressive Seam-Based Stitching - -### High-Level Overview - -The final blending phase uses dynamic programming to find optimal seams between overlapping images, with adaptive feathering based on local image content. - -### Algorithmic Details - -**Canvas Size Computation:** - -```rust -for &img_info in images { - let h = global_homographies[&img_info.id]; - let corners = [ - Point3::new(0.0, 0.0, 1.0), - Point3::new(w as f64, 0.0, 1.0), - Point3::new(w as f64, h_img as f64, 1.0), - Point3::new(0.0, h_img as f64, 1.0), - ]; - for p in corners.iter() { - let tp = h * p; - let tx = tp.x / tp.z; - let ty = tp.y / tp.z; - // Update bounds - } -} -``` - -Transforms all image corners to determine output canvas dimensions. - -**Dynamic Programming Seam Finding:** - -**Vertical Seam (for horizontally overlapping images):** - -```rust -fn find_pairwise_seam_dp_vertical(...) -> Vec { - // Build cost matrix: energy = color difference between images at overlap - for y_out in 0..out_height as usize { - for x_out in 0..out_width as usize { - if overlap_exists { - cost_matrix[y_out][x_out] = sqrt(ΔR² + ΔG² + ΔB²); - } - } - } - - // Dynamic programming: accumulate minimum cost paths - for y in (first_overlap_row + 1)..=last_overlap_row { - for x in 0..out_width as usize { - let up_left = if x > 0 { cost_matrix[y - 1][x - 1] } else { INF }; - let up = cost_matrix[y - 1][x]; - let up_right = if x < width-1 { cost_matrix[y - 1][x + 1] } else { INF }; - - cost_matrix[y][x] += up.min(up_left).min(up_right); - path_matrix[y][x] = direction_of_min_cost; - } - } - - // Backtrack from minimum cost endpoint - let mut seam = vec![0i32; out_height as usize]; - // ...trace path through path_matrix -} -``` - -**Energy Function:** Euclidean distance in RGB space between corresponding pixels from the two images at each overlap location. - -**Adaptive Blending with Feathering:** - -```rust -const FEATHER_WIDTH: f64 = 100.0; - -if dist_to_seam.abs() < dynamic_feather_width / 2.0 { - // Cosine-smoothed alpha blending - let alpha = if new_image_is_dominant_side { - (dist_to_seam + dynamic_feather_width / 2.0) / dynamic_feather_width - } else { - (-dist_to_seam + dynamic_feather_width / 2.0) / dynamic_feather_width - }; - - let weight_add = (1.0 - (alpha.clamp(0.0, 1.0) * PI).cos()) / 2.0; - let weight_pano = 1.0 - weight_add; - - // Blend colors - final_color = pano_color * weight_pano + add_color * weight_add; -} -``` - -**Content-Adaptive Feathering:** - -```rust -let is_low_detail = low_detail_mask_add.get_pixel(sx_u, sy_u)[0] > 0; -let dynamic_feather_width = if is_low_detail { FEATHER_WIDTH * 5.0 } else { FEATHER_WIDTH }; -``` - -Low-detail regions (sky, walls) receive 5x wider feathering (500px) to prevent visible seams in smooth gradients. - -**Bilinear Interpolation:** - -```rust -fn get_interpolated_pixel(img: &Rgb32FImage, x: f64, y: f64) -> Rgb { - let x_floor = x.floor() as u32; - let y_floor = y.floor() as u32; - let dx = x - x_floor as f64; - let dy = y - y_floor as f64; - - // Sample 4 neighbors and interpolate - let top = p00 * (1.0 - dx) + p10 * dx; - let bottom = p01 * (1.0 - dx) + p11 * dx; - top * (1.0 - dy) + bottom * dy -} -``` - -Prevents aliasing when sampling from transformed coordinates. - ---- - -## C++ Implementation Recommendations - -### Recommended Libraries - -**1. Matrix and Linear Algebra: Eigen3** - -```cpp -#include - -using Matrix3d = Eigen::Matrix; -using Vector3d = Eigen::Matrix; - -// Homography computation -Matrix3d computeHomography(const std::vector>& points) { - Eigen::MatrixXd A(points.size() * 2, 9); - // Fill A matrix with DLT equations - Eigen::JacobiSVD svd(A, Eigen::ComputeFullV); - VectorXd h = svd.matrixV().col(8); - return Eigen::Map(h.data()).transpose(); -} -``` - -**2. Image Processing: OpenCV (cv::Mat)** - -```cpp -#include - -// Gaussian blur -cv::Mat blurred; -cv::GaussianBlur(input, blurred, cv::Size(0, 0), 1.5); - -// Warp perspective -cv::Mat warped; -cv::warpPerspective(src, warped, homography, cv::Size(width, height)); -``` - -**3. Parallelism: Intel TBB or C++17 Parallel Algorithms** - -```cpp -#include -#include - -// Parallel for_each -std::for_each(std::execution::par_unseq, - features.begin(), features.end(), - [&](const Feature& f) { - // Process feature - }); -``` - -**4. Random Number Generation: STL ** - -```cpp -#include - -std::mt19937_64 rng(12345); // Fixed seed like Rust version -std::uniform_int_distribution dist(-16, 16); -``` - -### Implementation Structure - -```cpp -// Core data structures -struct KeyPoint { - uint32_t x, y; -}; - -using Descriptor = std::array; // 256 bits - -struct Feature { - KeyPoint kp; - Descriptor desc; -}; - -struct Match { - size_t idx1, idx2; - float distance; -}; - -// Feature detector class -class FASTBriefDetector { -public: - std::vector detect(const cv::Mat& grayImage); - -private: - std::vector> briefPairs_; - static constexpr int FAST_THRESHOLD = 15; - static constexpr float NMS_RADIUS = 15.0f; - static constexpr int PATCH_SIZE = 32; - static constexpr int DESC_SIZE = 256; - - void generateBriefPairs(); - std::vector fastDetect(const cv::Mat& img); - void nonMaximalSuppression(std::vector& keypoints); - Descriptor computeBrief(const cv::Mat& img, const KeyPoint& kp); -}; - -// RANSAC homography estimator -class RansacHomography { -public: - struct Result { - Eigen::Matrix3d H; - std::vector inliers; - }; - - std::optional estimate(const std::vector& matches, - const std::vector& kp1, - const std::vector& kp2); - -private: - static constexpr int ITERATIONS = 2500; - static constexpr double INLIER_THRESHOLD = 5.0; - static constexpr int MIN_INLIERS = 15; -}; - -// Stitcher with seam finding -class PanoramaStitcher { -public: - cv::Mat stitch(const std::vector& images, - const std::vector& homographies); - -private: - struct SeamInfo { - enum Orientation { VERTICAL, HORIZONTAL }; - Orientation orient; - std::vector coords; - bool newImageDominant; - }; - - std::optional findAdaptiveSeam(...); - std::vector findVerticalSeamDP(...); - std::vector findHorizontalSeamDP(...); - void blendImages(cv::Mat& panorama, const cv::Mat& newImage, - const SeamInfo& seam); -}; -``` - ---- - -## OpenCV Building Blocks Evaluation - -### Available OpenCV Functions and Their Equivalents - -| Rust Implementation | OpenCV Equivalent | Function | Notes | -| ------------------------- | ------------------------------------------- | ----------- | -------------------------------------- | -| `gaussian_blur_f32` | `cv::GaussianBlur` | ✓ Available | Native implementation, supports CV_32F | -| `corners_fast9` | `cv::FastFeatureDetector` | ✓ Available | Use `TYPE_9_16` for 9/16 pixel test | -| `non_maximal_suppression` | Built into FAST | ⚠️ Partial | OpenCV's FAST includes NMS | -| BRIEF descriptor | `cv::xfeatures2d::BriefDescriptorExtractor` | ✓ Available | In contrib module | -| Hamming distance | `cv::NORM_HAMMING` | ✓ Available | Optimized POPCNT instruction | -| `match_features` | `cv::BFMatcher` | ✓ Available | Supports ratio test natively | -| `compute_homography` | `cv::findHomography` | ✓ Available | Built-in RANSAC support | -| `find_homography_ransac` | `cv::findHomography` with `RANSAC` | ✓ Available | Single function call | -| `warpPerspective` | `cv::warpPerspective` | ✓ Available | Supports various interpolation modes | -| `get_interpolated_pixel` | `cv::remap` or `cv::warpPerspective` | ✓ Available | Prefer warp for whole image | -| Seam finding (DP) | Not directly available | ✗ Custom | OpenCV has graph cut but not DP seam | -| Multi-band blending | `cv::detail::MultiBandBlender` | ✓ Available | Advanced blending in stitching module | -| Exposure compensation | `cv::detail::ExposureCompensator` | ✓ Available | For exposure differences | - -### Recommended OpenCV Pipeline - -```cpp -#include -#include -#include - -class OpenCVPanoramaStitcher { -public: - cv::Mat stitch(const std::vector& images) { - // Method 1: Use OpenCV's built-in Stitcher (simpler but less control) - cv::Ptr stitcher = cv::Stitcher::create(); - cv::Mat result; - stitcher->stitch(images, result); - return result; - - // Method 2: Custom pipeline with OpenCV primitives - // (More control over individual steps) - } - - cv::Mat customStitch(const std::vector& images) { - // 1. Detect FAST + BRIEF - cv::Ptr fast = - cv::FastFeatureDetector::create(15, true); // threshold, nonmaxSuppression - cv::Ptr brief = - cv::xfeatures2d::BriefDescriptorExtractor::create(32); // bytes - - std::vector keypoints; - cv::Mat descriptors; - fast->detect(image, keypoints); - brief->compute(image, keypoints, descriptors); - - // 2. Match with BF + Hamming + Ratio test - cv::BFMatcher matcher(cv::NORM_HAMMING); - std::vector> knnMatches; - matcher.knnMatch(desc1, desc2, knnMatches, 2); - - std::vector goodMatches; - for (auto& m : knnMatches) { - if (m[0].distance < 0.8 * m[1].distance) { - goodMatches.push_back(m[0]); - } - } - - // 3. Find homography with RANSAC - std::vector srcPoints, dstPoints; - // ... extract points from matches - cv::Mat H = cv::findHomography(srcPoints, dstPoints, cv::RANSAC, 5.0); - - // 4. Warp and blend - cv::Mat warped; - cv::warpPerspective(src, warped, H, cv::Size(width, height)); - - // 5. Custom seam finding (DP-based) remains manual - // OpenCV's graph cut seam finder: cv::detail::GraphCutSeamFinder - } -}; -``` - -### OpenCV Stitcher Module Comparison - -The `cv::Stitcher` class implements a complete pipeline: - -- **Feature detection**: SURF (default), ORB, AKAZE -- **Matching**: FLANN or Brute-force -- **Homography estimation**: RANSAC -- **Bundle adjustment**: Optimizes all homographies jointly -- **Wave correction**: Reduces wavy artifacts -- **Exposure compensation**: Multi-band blending -- **Seam finding**: Graph cut or Voronoi -- **Blending**: Multi-band or feathering - -**Advantages of OpenCV's implementation:** - -- Optimized assembly/SIMD kernels -- GPU acceleration (CUDA) available -- Well-tested, handles edge cases -- Automatic camera parameter estimation - -**Advantages of Custom Implementation:** - -- Full control over each step -- Custom FAST9 + BRIEF for speed -- Content-adaptive feathering -- Better handling of specific use cases -- Easier to tune for specific hardware - -### Performance Considerations - -OpenCV implementations are generally faster due to: - -1. **Intel IPP integration**: Hand-optimized image processing primitives -2. **OpenCL/CUDA support**: GPU acceleration for warp/blend operations -3. **SIMD optimization**: SSE/AVX instructions for pixel operations -4. **Memory pooling**: Reduced allocation overhead - -However, the custom Rust implementation offers: - -1. **Parallelism**: Rayon provides excellent data parallelism -2. **Zero-cost abstractions**: Rust's optimization produces efficient code -3. **Memory safety**: No undefined behavior or memory leaks -4. **Determinism**: Fixed random seeds for reproducibility - ---- - -## Summary - -The RapidRAW panorama stitcher implements a sophisticated multi-phase pipeline that combines: - -1. **FAST9 corner detection** for efficient feature localization -2. **BRIEF binary descriptors** for fast matching with Hamming distance -3. **RANSAC geometric verification** for robust homography estimation -4. **MST-based image ordering** for optimal global alignment -5. **Dynamic programming seam finding** for minimal energy seams -6. **Content-adaptive feathering** for smooth blending - -The C++ port should leverage: - -- **Eigen3** for matrix operations -- **OpenCV** for image processing primitives -- **Custom implementations** for BRIEF (or use contrib module) and DP seam finding -- **Intel TBB** for parallelism - -OpenCV provides excellent building blocks for most operations, particularly `cv::findHomography` with RANSAC and the Stitcher module for quick prototyping. However, the content-adaptive seam finding and blending strategy in RapidRAW provides superior quality for challenging panoramas and is worth preserving in a C++ port. From 8938370b0680070eea9c45e864296c94ccc14337 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Mon, 16 Mar 2026 18:50:43 +0100 Subject: [PATCH 10/29] wip: the panorama works, but the dng is not compatible with libraw --- src/engine/Panorama.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp index dad2df3..be246e5 100644 --- a/src/engine/Panorama.cpp +++ b/src/engine/Panorama.cpp @@ -469,13 +469,16 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 16); TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); - TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(out, TIFFTAG_PHOTOMETRIC, 34892); TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); static const uint8_t dng_ver[] = {1, 4, 0, 0}; TIFFSetField(out, TIFFTAG_DNGVERSION, dng_ver); + TIFFSetField(out, TIFFTAG_DNGBACKWARDVERSION, dng_ver); TIFFSetField(out, TIFFTAG_SUBFILETYPE, 0); - TIFFSetField(out, TIFFTAG_UNIQUECAMERAMODEL, "Photon Panorama"); + TIFFSetField(out, TIFFTAG_MAKE, "Photon"); + TIFFSetField(out, TIFFTAG_MODEL, "Panorama Engine"); + TIFFSetField(out, TIFFTAG_UNIQUECAMERAMODEL, "Photon Panorama Engine"); TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(out, 0)); uint32_t whiteLevel[3] = {65535, 65535, 65535}; From af95a253dafb3fee2910933726da9d5e17700cc7 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Mon, 16 Mar 2026 19:32:17 +0100 Subject: [PATCH 11/29] wip: almost ok, missing dng thumbnail --- src/engine/Panorama.cpp | 71 ++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp index be246e5..e01d52b 100644 --- a/src/engine/Panorama.cpp +++ b/src/engine/Panorama.cpp @@ -354,13 +354,13 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, blender->prepare(corners, sizes_warped); - float scale_factor = 8.0; + float scale_factor = 2.0; // 1. Feed images to blender using Scaled 16-bit Signed for (size_t i = 0; i < images_warped16.size(); i++) { cv::Mat img16S; // We multiply by 1/scale_factor to allow the blend to sum up the highlights without clipping images_warped16[i].convertTo(img16S, CV_16SC3, 1/scale_factor); - blender->feed(img16S, masks_warped[i], corners[i]); + blender->feed(img16S, masks_binary[i], corners[i]); } // 2. Blend @@ -373,19 +373,6 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, // The more you scale the more you lose data. result_16s.convertTo(result16, CV_16UC3, scale_factor); - // Blend - /* - cv::Mat result_mask; - cv::Mat result8; - blender->blend(result8, result_mask); - */ - // Convert 8-bit gamma result back to 16-bit linear - /* - cv::Mat result32F, result16; - result8.convertTo(result32F, CV_32FC3, 1.0 / 255.0); - cv::pow(result32F, 2.2, result32F); - result32F.convertTo(result16, CV_16UC3, 65535.0); - */ cv::Mat resultRGB; cv::cvtColor(result16, resultRGB, cv::COLOR_BGR2RGB); @@ -498,6 +485,60 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, return result; } } + // ========================================== + // 1. FINISH WRITING MAIN RAW DIRECTORY + // ========================================== + + // This tells LibTIFF to save the current tags and image data, + // and open a fresh page for the thumbnail. + TIFFWriteDirectory(out); + + // ========================================== + // 2. GENERATE THE THUMBNAIL (OpenCV) + // ========================================== + + // Calculate thumbnail size (e.g., 256 pixels on the longest edge) + int max_dim = 256; + double scale = (double)max_dim / std::max(result16.cols, result16.rows); + + cv::Mat thumbnail16; + cv::resize(result16, thumbnail16, cv::Size(), scale, scale, cv::INTER_AREA); + + // Convert to float (0.0 to 1.0 range) for gamma math + cv::Mat thumbFloat; + thumbnail16.convertTo(thumbFloat, CV_32FC3, 1.0 / 65535.0); + + // Apply an approximate sRGB Gamma curve (1.0 / 2.2) so it isn't completely dark + cv::pow(thumbFloat, 1.0 / 2.2, thumbFloat); + + // Scale back up to 8-bit (0-255) + cv::Mat thumbnail8; + thumbFloat.convertTo(thumbnail8, CV_8UC3, 255.0); + + // ========================================== + // 3. WRITE THE THUMBNAIL DIRECTORY (IFD 1) + // ========================================== + + // The magic flag (1) that tells viewers "This is a thumbnail, not a real image" + TIFFSetField(out, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE); + + TIFFSetField(out, TIFFTAG_IMAGEWIDTH, thumbnail8.cols); + TIFFSetField(out, TIFFTAG_IMAGELENGTH, thumbnail8.rows); + TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 8); + TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 3); + + // Standard RGB for thumbnails (not LinearRaw like the main payload!) + TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(out, TIFFTAG_COMPRESSION, COMPRESSION_NONE); + TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); + + // Write the 8-bit thumbnail scanlines + for (int row = 0; row < thumbnail8.rows; row++) { + uint8_t* rowPtr = thumbnail8.ptr(row); + TIFFWriteScanline(out, rowPtr, row, 0); + } + TIFFClose(out); From c35f44dcffa1954819a44219be4030dd197bc99a Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Tue, 17 Mar 2026 11:48:15 +0100 Subject: [PATCH 12/29] fixed: dng thumbnail --- src/engine/Panorama.cpp | 202 +++++++++++++++------------------------- 1 file changed, 77 insertions(+), 125 deletions(-) diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp index e01d52b..48b110e 100644 --- a/src/engine/Panorama.cpp +++ b/src/engine/Panorama.cpp @@ -406,141 +406,93 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, LogManager::instance()->log( QString("[ Panorama.cpp ] - Saving panorama to %1").arg(filename), INFO); - /* - libdng_init(); - libdng_info dng = {0}; - libdng_new(&dng); - if (!libdng_set_mode_from_name(&dng, "SRGGB16")) { - fprintf(stderr, "Invalid pixel format supplied\n"); - } - - for (size_t i = 0; i < 9; i++) - dng.color_matrix_1[i] = colorInfo.get()->matrix[i]; - - - for (size_t i = 0; i < 3; i++) - dng.analogbalance[i] = colorInfo->asShotNeutral[i]; - - libdng_set_make_model(&dng, "Photon", "Panorama"); - if ( - !libdng_write( - &dng, - filename.toStdString().c_str(), - result16.cols, result16.rows, - reinterpret_cast(result16.data), - result16.total() * result16.elemSize() - ) - ) { - result["success"] = false; - result["message"] = "Error creating DNG file."; - - libdng_free(&dng); - return result; - } - - libdng_free(&dng); - */ - - TIFF* out = TIFFOpen(filename.toStdString().c_str(), "w"); - if (!out) { + // ========================================== +// 1. GENERATE THUMBNAIL FIRST (before opening TIFF) +// ========================================== +int max_dim = 256; +double scale = (double)max_dim / std::max(result16.cols, result16.rows); +cv::Mat thumbnail16; +cv::resize(result16, thumbnail16, cv::Size(), scale, scale, cv::INTER_AREA); + +cv::Mat thumbFloat; +thumbnail16.convertTo(thumbFloat, CV_32FC3, 1.0 / 65535.0); +cv::pow(thumbFloat, 1.0 / 2.2, thumbFloat); + +cv::Mat thumbnail8; +thumbFloat.convertTo(thumbnail8, CV_8UC3, 255.0); + +// ========================================== +// 2. OPEN FILE AND WRITE IFD 0 = THUMBNAIL +// ========================================== +TIFF* out = TIFFOpen(filename.toStdString().c_str(), "w"); +if (!out) { result["success"] = false; result["message"] = "Could not open file for writing."; return result; - } +} - TIFFSetField(out, TIFFTAG_IMAGEWIDTH, resultRGB.cols); - TIFFSetField(out, TIFFTAG_IMAGELENGTH, resultRGB.rows); - TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 3); - TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 16); - TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); - TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); - TIFFSetField(out, TIFFTAG_PHOTOMETRIC, 34892); - TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); - - static const uint8_t dng_ver[] = {1, 4, 0, 0}; - TIFFSetField(out, TIFFTAG_DNGVERSION, dng_ver); - TIFFSetField(out, TIFFTAG_DNGBACKWARDVERSION, dng_ver); - TIFFSetField(out, TIFFTAG_SUBFILETYPE, 0); - TIFFSetField(out, TIFFTAG_MAKE, "Photon"); - TIFFSetField(out, TIFFTAG_MODEL, "Panorama Engine"); - TIFFSetField(out, TIFFTAG_UNIQUECAMERAMODEL, "Photon Panorama Engine"); - TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(out, 0)); - - uint32_t whiteLevel[3] = {65535, 65535, 65535}; - TIFFSetField(out, TIFFTAG_WHITELEVEL, 3, whiteLevel); - - TIFFSetField(out, TIFFTAG_COLORMATRIX1, 9, colorInfo.get()->matrix); - TIFFSetField(out, TIFFTAG_ASSHOTNEUTRAL, 3, colorInfo.get()->asShotNeutral); - TIFFSetField(out, TIFFTAG_CALIBRATIONILLUMINANT1, 23); - - // Write the 16-bit data - for (int row = 0; row < resultRGB.rows; row++) { +// --- Shared DNG metadata on IFD 0 --- +TIFFSetField(out, TIFFTAG_MAKE, "Photon"); +TIFFSetField(out, TIFFTAG_MODEL, "Panorama Engine"); +TIFFSetField(out, TIFFTAG_UNIQUECAMERAMODEL, "Photon Panorama Engine"); + +static const uint8_t dng_ver[] = {1, 4, 0, 0}; +TIFFSetField(out, TIFFTAG_DNGVERSION, dng_ver); +TIFFSetField(out, TIFFTAG_DNGBACKWARDVERSION, dng_ver); + +// --- Thumbnail image fields --- +TIFFSetField(out, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE); // 0x1 +TIFFSetField(out, TIFFTAG_IMAGEWIDTH, thumbnail8.cols); +TIFFSetField(out, TIFFTAG_IMAGELENGTH, thumbnail8.rows); +TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 8); +TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 3); +TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); +TIFFSetField(out, TIFFTAG_COMPRESSION, COMPRESSION_JPEG); // JPEG is preferred by most viewers +TIFFSetField(out, TIFFTAG_JPEGQUALITY, 90); +TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); +TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); +TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, thumbnail8.rows); // single strip for thumbnail + +for (int row = 0; row < thumbnail8.rows; row++) { + uint8_t* rowPtr = thumbnail8.ptr(row); + TIFFWriteScanline(out, rowPtr, row, 0); +} + +// ========================================== +// 3. WRITE IFD 1 = MAIN RAW IMAGE +// ========================================== +TIFFWriteDirectory(out); // seals IFD 0, advances to IFD 1 + +TIFFSetField(out, TIFFTAG_SUBFILETYPE, 0); // full-resolution image +TIFFSetField(out, TIFFTAG_IMAGEWIDTH, resultRGB.cols); +TIFFSetField(out, TIFFTAG_IMAGELENGTH, resultRGB.rows); +TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 3); +TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 16); +TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); +TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); +TIFFSetField(out, TIFFTAG_PHOTOMETRIC, 34892); // LINEARRAW +TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); +TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(out, 0)); + +uint32_t whiteLevel[3] = {65535, 65535, 65535}; +TIFFSetField(out, TIFFTAG_WHITELEVEL, 3, whiteLevel); +TIFFSetField(out, TIFFTAG_COLORMATRIX1, 9, colorInfo.get()->matrix); +TIFFSetField(out, TIFFTAG_ASSHOTNEUTRAL, 3, colorInfo.get()->asShotNeutral); +TIFFSetField(out, TIFFTAG_CALIBRATIONILLUMINANT1, 23); + +for (int row = 0; row < resultRGB.rows; row++) { uint16_t* rowPtr = resultRGB.ptr(row); if (TIFFWriteScanline(out, rowPtr, row, 0) < 0) { - TIFFClose(out); - result["success"] = false; - result["message"] = "Error writing scanline to DNG."; - return result; + TIFFClose(out); + result["success"] = false; + result["message"] = "Error writing scanline to DNG."; + return result; } - } - // ========================================== - // 1. FINISH WRITING MAIN RAW DIRECTORY - // ========================================== - - // This tells LibTIFF to save the current tags and image data, - // and open a fresh page for the thumbnail. - TIFFWriteDirectory(out); - - // ========================================== - // 2. GENERATE THE THUMBNAIL (OpenCV) - // ========================================== - - // Calculate thumbnail size (e.g., 256 pixels on the longest edge) - int max_dim = 256; - double scale = (double)max_dim / std::max(result16.cols, result16.rows); - - cv::Mat thumbnail16; - cv::resize(result16, thumbnail16, cv::Size(), scale, scale, cv::INTER_AREA); - - // Convert to float (0.0 to 1.0 range) for gamma math - cv::Mat thumbFloat; - thumbnail16.convertTo(thumbFloat, CV_32FC3, 1.0 / 65535.0); - - // Apply an approximate sRGB Gamma curve (1.0 / 2.2) so it isn't completely dark - cv::pow(thumbFloat, 1.0 / 2.2, thumbFloat); - - // Scale back up to 8-bit (0-255) - cv::Mat thumbnail8; - thumbFloat.convertTo(thumbnail8, CV_8UC3, 255.0); - - // ========================================== - // 3. WRITE THE THUMBNAIL DIRECTORY (IFD 1) - // ========================================== - - // The magic flag (1) that tells viewers "This is a thumbnail, not a real image" - TIFFSetField(out, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE); - - TIFFSetField(out, TIFFTAG_IMAGEWIDTH, thumbnail8.cols); - TIFFSetField(out, TIFFTAG_IMAGELENGTH, thumbnail8.rows); - TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 8); - TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 3); - - // Standard RGB for thumbnails (not LinearRaw like the main payload!) - TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); - TIFFSetField(out, TIFFTAG_COMPRESSION, COMPRESSION_NONE); - TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); - TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); - - // Write the 8-bit thumbnail scanlines - for (int row = 0; row < thumbnail8.rows; row++) { - uint8_t* rowPtr = thumbnail8.ptr(row); - TIFFWriteScanline(out, rowPtr, row, 0); - } - +} - TIFFClose(out); +TIFFClose(out); LogManager::instance()->log( From f35a78b96ce9e538500927e56c3a4754da362820 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Tue, 17 Mar 2026 11:58:52 +0100 Subject: [PATCH 13/29] almost ok, need to fix tones targeting in frag and ImageDeveloper and CI builds --- src/engine/Panorama.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp index 48b110e..a2568a0 100644 --- a/src/engine/Panorama.cpp +++ b/src/engine/Panorama.cpp @@ -342,7 +342,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, int blend_width = std::min(dst_roi.width, dst_roi.height) / 8; int num_bands = static_cast( std::ceil(std::log(static_cast(blend_width)) / std::log(2.0))); - num_bands = std::min(num_bands, 8); // Cap at 8 for performance + // num_bands = std::min(num_bands, 8); // Cap at 8 for performance LogManager::instance()->log( QString("[ Panorama.cpp ] - Using %1 bands for blending").arg(num_bands), @@ -392,8 +392,6 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, .arg(result16.rows), DEBUG); - // Ensure continuous memory layout for TIFF writing - // if (!result16.isContinuous()) result16 = result16.clone(); // PHASE 7: Save to DNG LogManager::instance()->log("[ Panorama.cpp ] - Phase 7: Saving to DNG", From 539c07a2d8cade71567db4d10f16b98823b2f126 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Tue, 17 Mar 2026 13:55:13 +0100 Subject: [PATCH 14/29] CI --- .github/workflows/build.yml | 10 +++++++--- CMakeLists.txt | 1 + src/engine/Panorama.h | 4 ---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6812417..f90251b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,9 +40,13 @@ jobs: if: runner.os == 'Windows' uses: actions/cache@v4 with: - path: C:\vcpkg\installed - key: vcpkg-${{ runner.os }}-libraw-vulkan - restore-keys: vcpkg-${{ runner.os }}- + path: | + C:\vcpkg\installed + C:\vcpkg\downloads + # Hashes the workflow file itself. If you change the install step below, the cache resets automatically. + key: vcpkg-${{ runner.os }}-${{ hashFiles('.github/workflows/build.yml') }} + restore-keys: | + vcpkg-${{ runner.os }}- - name: Cache CMake build uses: actions/cache@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 72ebf60..8858012 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -248,6 +248,7 @@ target_include_directories(Photon PRIVATE ${CMAKE_CURRENT_BINARY_DIR} $<$>:/usr/include/qt6/QtGui/6.10.1> $<$>:/usr/include/qt6/QtGui/6.10.1/QtGui> + $<$>:${OPENCV_INCLUDE_DIRS}> ) # Ensure QML types are registered before main diff --git a/src/engine/Panorama.h b/src/engine/Panorama.h index 6f44cf4..4e8214f 100644 --- a/src/engine/Panorama.h +++ b/src/engine/Panorama.h @@ -12,10 +12,6 @@ #include #include #include -/* -extern "C" { - #include "libdng/libdng.h" -}*/ namespace photon { From 7b7c2f32127c68971451df93775ae6a99d75f61d Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Tue, 17 Mar 2026 14:08:47 +0100 Subject: [PATCH 15/29] CI --- .github/workflows/build.yml | 3 ++- CMakeLists.txt | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f90251b..e443721 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -114,7 +114,8 @@ jobs: # Tell the Qt plugin where to find QML files to scan for dependencies export QML_SOURCES_PATHS="${{ github.workspace }}/content" # Explicitly request extra platform plugins for better compatibility - export EXTRA_QT_PLUGINS="platforms,wayland-graphics-integration-client,wayland-shell-integration,imageformats" + export EXTRA_QT_MODULES="svg" + export EXTRA_QT_PLUGINS="platforms,wayland-graphics-integration-client,wayland-shell-integration,imageformats,iconengines" # Run linuxdeploy ./linuxdeploy-x86_64.AppImage --appdir AppDir -e build_dir/Photon -d Photon.desktop -i assets/icons/photon.png --plugin qt --output appimage diff --git a/CMakeLists.txt b/CMakeLists.txt index 8858012..e114872 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ configure_file( "${CMAKE_CURRENT_BINARY_DIR}/Version.h" ) -find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2 ShaderTools Gui Concurrent GuiPrivate) +find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2 ShaderTools Gui Concurrent GuiPrivate Svg) find_package(Vulkan REQUIRED) if(WIN32) @@ -216,7 +216,7 @@ set_target_properties(Photon PROPERTIES if(WIN32) target_link_libraries(Photon - PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate + PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate Qt6::Svg PRIVATE ${LibRaw_LIBRARIES} PRIVATE ${OpenCV_LIBRARIES} PRIVATE ${TIFF_LIBRARIES} @@ -224,7 +224,7 @@ if(WIN32) ) else() target_link_libraries(Photon - PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate + PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate Qt6::Svg PRIVATE ${LIBRAW_LIBRARIES} PRIVATE ${OPENCV_LIBRARIES} PRIVATE ${TIFF_LIBRARIES} From 5c64d9b9322a14137c56ae06f13a1d91dac8ba4d Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Tue, 17 Mar 2026 15:18:30 +0100 Subject: [PATCH 16/29] CI --- .github/workflows/build.yml | 3 +-- CMakeLists.txt | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e443721..62ca6e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -114,8 +114,7 @@ jobs: # Tell the Qt plugin where to find QML files to scan for dependencies export QML_SOURCES_PATHS="${{ github.workspace }}/content" # Explicitly request extra platform plugins for better compatibility - export EXTRA_QT_MODULES="svg" - export EXTRA_QT_PLUGINS="platforms,wayland-graphics-integration-client,wayland-shell-integration,imageformats,iconengines" + export EXTRA_QT_PLUGINS="platforms,wayland-graphics-integration-client,wayland-shell-integration,imageformats,styles,controls,quickcontrols2" # Run linuxdeploy ./linuxdeploy-x86_64.AppImage --appdir AppDir -e build_dir/Photon -d Photon.desktop -i assets/icons/photon.png --plugin qt --output appimage diff --git a/CMakeLists.txt b/CMakeLists.txt index e114872..e6bde7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ configure_file( "${CMAKE_CURRENT_BINARY_DIR}/Version.h" ) -find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2 ShaderTools Gui Concurrent GuiPrivate Svg) +find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2 ShaderTools Gui Concurrent GuiPrivate) find_package(Vulkan REQUIRED) if(WIN32) @@ -216,7 +216,7 @@ set_target_properties(Photon PROPERTIES if(WIN32) target_link_libraries(Photon - PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate Qt6::Svg + PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate PRIVATE ${LibRaw_LIBRARIES} PRIVATE ${OpenCV_LIBRARIES} PRIVATE ${TIFF_LIBRARIES} @@ -224,7 +224,7 @@ if(WIN32) ) else() target_link_libraries(Photon - PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate Qt6::Svg + PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::ShaderTools Qt6::Concurrent Qt6::GuiPrivate PRIVATE ${LIBRAW_LIBRARIES} PRIVATE ${OPENCV_LIBRARIES} PRIVATE ${TIFF_LIBRARIES} From dc2bd21af13be0979d48c450f37d26bea56c5a7f Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Tue, 17 Mar 2026 16:29:47 +0100 Subject: [PATCH 17/29] local builds --- .gitignore | 1 + Dockerfile.linux | 76 ++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile.windows | 58 +++++++++++++++++++++++++++++++++++ build_appimage.sh | 15 +++++++++ build_internal.sh | 19 ++++++++++++ build_windows.sh | 23 ++++++++++++++ 6 files changed, 192 insertions(+) create mode 100644 Dockerfile.linux create mode 100644 Dockerfile.windows create mode 100755 build_appimage.sh create mode 100644 build_internal.sh create mode 100755 build_windows.sh diff --git a/.gitignore b/.gitignore index a77b961..c45de3a 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,7 @@ CMakeLists.txt.user* .rcc/ .uic/ /build*/ +/dist/ testphoton tmp Testing diff --git a/Dockerfile.linux b/Dockerfile.linux new file mode 100644 index 0000000..68f78f0 --- /dev/null +++ b/Dockerfile.linux @@ -0,0 +1,76 @@ +# Dockerfile for Linux AppImage build (Ubuntu 22.04) +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# Install build tools and dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + git \ + wget \ + curl \ + python3 \ + python3-pip \ + libraw-dev \ + libvulkan-dev \ + libfuse2 \ + libgl1-mesa-dev \ + libxcb-cursor0 \ + libxcb-xinerama0 \ + libxcb-xinput0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-render-util0 \ + libxcb-shape0 \ + libwayland-client0 \ + libwayland-cursor0 \ + libwayland-egl1 \ + libxkbcommon-x11-0 \ + libxkbcommon-dev \ + libxkbcommon-x11-dev \ + libxcb-xkb-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libx11-xcb-dev \ + libwayland-dev \ + wayland-protocols \ + libwayland-server0 \ + libopencv-dev \ + imagemagick \ + libtiff-dev \ + file \ + && rm -rf /var/lib/apt/lists/* + +# Install aqtinstall for Qt installation +RUN pip3 install aqtinstall + +# Install Qt 6.10.1 (matches build.yml) +RUN aqt install-qt linux desktop 6.10.1 linux_gcc_64 -m qtshadertools --outputdir /opt/qt + +# Set Qt environment variables +ENV PATH="/opt/qt/6.10.1/gcc_64/bin:${PATH}" +ENV QT_DIR="/opt/qt/6.10.1/gcc_64" + +# Download and extract linuxdeploy tools +WORKDIR /tools +RUN wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage && \ + wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage && \ + chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage && \ + ./linuxdeploy-x86_64.AppImage --appimage-extract && \ + mv squashfs-root linuxdeploy-extracted && \ + ./linuxdeploy-plugin-qt-x86_64.AppImage --appimage-extract && \ + mv squashfs-root linuxdeploy-plugin-qt-extracted && \ + rm linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage + +# Symlink binaries to /usr/local/bin for easy access +RUN ln -s /tools/linuxdeploy-extracted/usr/bin/linuxdeploy /usr/local/bin/linuxdeploy && \ + ln -s /tools/linuxdeploy-plugin-qt-extracted/usr/bin/linuxdeploy-plugin-qt /usr/local/bin/linuxdeploy-plugin-qt + +# Copy source code and build script +WORKDIR /app +COPY . . +RUN chmod +x build_internal.sh + +CMD ["/app/build_internal.sh"] diff --git a/Dockerfile.windows b/Dockerfile.windows new file mode 100644 index 0000000..ef89428 --- /dev/null +++ b/Dockerfile.windows @@ -0,0 +1,58 @@ +# Dockerfile for Windows build using MSVC 2022 +# This MUST be run on a Windows host with Windows Containers enabled +FROM mcr.microsoft.com/windows/servercore:ltsc2022 + +# Set shell to powershell for easier setup +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +# Install Visual Studio 2022 Build Tools +RUN Invoke-WebRequest -Uri https://aka.ms/vs/17/release/vs_buildtools.exe -OutFile vs_buildtools.exe; \ + Start-Process -FilePath vs_buildtools.exe -ArgumentList '--quiet', '--norestart', '--nocache', \ + '--add', 'Microsoft.VisualStudio.Workload.VCTools', \ + '--add', 'Microsoft.VisualStudio.Component.VC.ATLMFC', \ + '--add', 'Microsoft.VisualStudio.Component.Windows11SDK.22000' -Wait; \ + Remove-Item -Force vs_buildtools.exe + +# Install Chocolatey to manage other tools +RUN Set-ExecutionPolicy Bypass -Scope Process -Force; \ + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; \ + iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) + +# Install CMake, Git, and Python +RUN choco install -y cmake git python3 + +# Install aqtinstall for Qt +RUN pip install aqtinstall + +# Install Qt 6.10.1 (matches build.yml) +RUN aqt install-qt windows desktop 6.10.1 win64_msvc2022_64 -m qtshadertools --outputdir C:\Qt + +# Install and bootstrap vcpkg +RUN git clone https://github.com/microsoft/vcpkg.git C:\vcpkg; \ + C:\vcpkg\bootstrap-vcpkg.bat + +# Set up work directory +WORKDIR C:\app +COPY . . + +# Build and Package script +# This script mirrors the build.yml steps +RUN @' \ +$env:PATH = \"C:\Qt\6.10.1\msvc2022_64\bin;C:\Program Files\CMake\bin;C:\Program Files\Git\cmd;C:\vcpkg;\" + $env:PATH; \ +# Install dependencies via vcpkg \ +vcpkg install libraw:x64-windows vulkan:x64-windows opencv4:x64-windows tiff:x64-windows; \ +# Configure and Build \ +mkdir build_win; cd build_win; \ +cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake; \ +cmake --build . --config Release -j $env:NUMBER_OF_PROCESSORS; \ +# Package \ +mkdir install; \ +cmake --install . --config Release --prefix install; \ +cd install\bin; \ +& \"C:\Qt\6.10.1\msvc2022_64\bin\windeployqt.exe\" --release --qmldir ..\..\..\content --no-compiler-runtime Photon.exe; \ +# Final Copy to dist (will be mapped via volume) \ +mkdir C:\app\dist\windows\Photon; \ +xcopy /E /I /Y C:\app\build_win\install\* C:\app\dist\windows\Photon\; \ +'@ | Out-File -FilePath C:\app\build_internal.ps1 -Encoding ascii + +CMD ["powershell", "-File", "C:\\app\\build_internal.ps1"] diff --git a/build_appimage.sh b/build_appimage.sh new file mode 100755 index 0000000..41fad4c --- /dev/null +++ b/build_appimage.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Script to build Photon AppImage using Docker + +# Ensure dist/linux directory exists +mkdir -p dist/linux + +# Build the Docker image (rebuilds everytime to collect changes) +echo "[ build_appimage.sh ] - Building Docker image..." +docker build -t photon-linux-builder -f Dockerfile.linux . + +# Run the container and mount the dist directory to get the output +echo "[ build_appimage.sh ] - Running container to build AppImage..." +docker run --rm -v "$(pwd)/dist:/app/dist" photon-linux-builder + +echo "[ build_appimage.sh ] - AppImage should be available at dist/linux/Photon-Linux.AppImage" diff --git a/build_internal.sh b/build_internal.sh new file mode 100644 index 0000000..402ab6f --- /dev/null +++ b/build_internal.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Internal build script for Photon AppImage + +mkdir -p build_dir && cd build_dir +cmake .. -DCMAKE_BUILD_TYPE=Release +cmake --build . -j$(nproc) +cd .. + +export QMAKE=$(which qmake) +export VERSION=$(git describe --tags --always || echo "latest") +export QML_SOURCES_PATHS="/app/content" +export EXTRA_QT_PLUGINS="platforms,wayland-graphics-integration-client,wayland-shell-integration,imageformats,styles,controls,quickcontrols2" + +# Run linuxdeploy +linuxdeploy --appdir AppDir -e build_dir/Photon -d Photon.desktop -i assets/icons/photon.png --plugin qt --output appimage + +# Move and rename output +mkdir -p /app/dist/linux +mv Photon-*.AppImage /app/dist/linux/Photon-Linux.AppImage diff --git a/build_windows.sh b/build_windows.sh new file mode 100755 index 0000000..270d7a3 --- /dev/null +++ b/build_windows.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Script to build Photon for Windows using Docker +# This MUST be run on a Windows machine (e.g., via Git Bash) with Windows Containers enabled + +# Check if we are likely on a Linux machine +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + echo "[ build_windows.sh ] - WARNING: You are on Linux. Windows containers require a Windows host." + echo "This script will likely fail unless your Docker is configured for remote Windows build nodes." +fi + +# Ensure dist/windows directory exists +mkdir -p dist/windows + +# Build the Docker image (rebuilds everytime to collect changes) +echo "[ build_windows.sh ] - Building Docker image (this may take 30+ minutes the first time)..." +docker build -t photon-windows-builder -f Dockerfile.windows . + +# Run the container and mount the dist directory to get the output +# Using PWD formatted for Windows if on Git Bash +echo "[ build_windows.sh ] - Running container to build Windows binaries..." +docker run --rm -v "$(pwd)/dist:C:/app/dist" photon-windows-builder + +echo "[ build_windows.sh ] - Build complete. Outputs in dist/windows/Photon" From f95b684c159f885559946ecf518fa2df37afa33e Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Wed, 18 Mar 2026 00:07:33 +0100 Subject: [PATCH 18/29] styling. need to fix contextmenu and make it more consistent --- build_internal.sh | 16 ++ content/components/PhotoContextMenu.qml | 197 ++++++++++++++++-------- content/views/App.qml | 21 ++- content/views/LibraryView.qml | 16 -- 4 files changed, 163 insertions(+), 87 deletions(-) diff --git a/build_internal.sh b/build_internal.sh index 402ab6f..4679d42 100644 --- a/build_internal.sh +++ b/build_internal.sh @@ -9,8 +9,24 @@ cd .. export QMAKE=$(which qmake) export VERSION=$(git describe --tags --always || echo "latest") export QML_SOURCES_PATHS="/app/content" +export EXTRA_QT_MODULES="quick,quickcontrols2,qml" export EXTRA_QT_PLUGINS="platforms,wayland-graphics-integration-client,wayland-shell-integration,imageformats,styles,controls,quickcontrols2" +mkdir -p AppDir/usr/share/fonts +cp -r /usr/share/fonts/truetype AppDir/usr/share/fonts/ 2>/dev/null || true +cp -r /usr/share/fonts/opentype AppDir/usr/share/fonts/ 2>/dev/null || true +export QT_QPA_FONTDIR="$APPDIR/usr/share/fonts" +cat >AppDir/AppRun <<'EOF' +#!/bin/bash +APPDIR="$(dirname "$(readlink -f "$0")")" + +export QT_QPA_FONTDIR="$APPDIR/usr/share/fonts" +export FONTCONFIG_PATH="$APPDIR/usr/share/fonts" +export QT_QUICK_CONTROLS_STYLE="${QT_QUICK_CONTROLS_STYLE:-Basic}" +export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-wayland;xcb}" +exec "$APPDIR/usr/bin/Photon" "$@" +EOF +chmod +x AppDir/AppRun # Run linuxdeploy linuxdeploy --appdir AppDir -e build_dir/Photon -d Photon.desktop -i assets/icons/photon.png --plugin qt --output appimage diff --git a/content/components/PhotoContextMenu.qml b/content/components/PhotoContextMenu.qml index bbee616..8a64150 100644 --- a/content/components/PhotoContextMenu.qml +++ b/content/components/PhotoContextMenu.qml @@ -1,9 +1,15 @@ import QtQuick -import QtQuick.Controls as T +import QtQuick.Controls.Basic as T import Main T.Menu { id: root + implicitWidth: 200 + topPadding: 4 + bottomPadding: 4 + closePolicy: T.Popup.CloseOnEscape | T.Popup.CloseOnPressOutside + + // ─── Properties ────────────────────────────────────────────────────────── property int selectionCount: 0 property bool canCopy: true @@ -14,6 +20,8 @@ T.Menu { property var operatorLabels: ["=", ">", "≥", "<", "≤"] property bool keepFilterMenuOpen: false + // ─── Signals ───────────────────────────────────────────────────────────── + signal copyRequested() signal pasteRequested() signal ratingRequested(int rating) @@ -25,6 +33,8 @@ T.Menu { signal flipVerticalRequested() signal createPanoramaRequested() + // ─── Helpers ───────────────────────────────────────────────────────────── + function openAt(x, y) { root.x = x root.y = y @@ -39,55 +49,140 @@ T.Menu { } onAboutToHide: { - if (!keepFilterMenuOpen) - return + if (!keepFilterMenuOpen) return keepFilterMenuOpen = false Qt.callLater(function() { - root.openAt(root.x, root.y) + root.x = root.x + root.y = root.y + root.open() filterMenu.open() }) } - background: Rectangle { + // ─── Internal components ───────────────────────────────────────────────── + + component MenuBg: Rectangle { color: Theme.secondary border.color: Theme.border border.width: 1 radius: Theme.radius } - T.MenuItem { + component StyledMenuItem: T.MenuItem { + id: item + implicitWidth: 200 + implicitHeight: 36 + leftPadding: 12 + rightPadding: 12 + topPadding: 0 + bottomPadding: 0 + spacing: 8 + + background: Rectangle { + color: item.highlighted ? Theme.accent : "transparent" + radius: Theme.radius + anchors.fill: parent + anchors.margins: 2 + } + + contentItem: Row { + spacing: item.spacing + anchors.verticalCenter: parent.verticalCenter + + Image { + source: item.icon.source + width: 16 + height: 16 + anchors.verticalCenter: parent.verticalCenter + visible: item.icon.source != "" + fillMode: Image.PreserveAspectFit + // Respect icon.color tinting if set + layer.enabled: item.icon.color !== Qt.rgba(0,0,0,0) && item.icon.color !== "#000000" + layer.effect: null + } + + Text { + text: item.text + font: Theme.fontSmall + color: item.enabled ? Theme.foreground : Theme.mutedFg + verticalAlignment: Text.AlignVCenter + height: item.implicitHeight + leftPadding: (item.icon.source == "" ) ? 24 : 0 + } + } + + // Submenu arrow indicator + indicator: Item { + width: 12 + height: item.implicitHeight + visible: item.subMenu !== null + anchors.right: parent.right + anchors.rightMargin: 8 + Text { + anchors.centerIn: parent + color: item.enabled ? Theme.foreground : Theme.mutedFg + } + } + } + + component StyledMenu: T.Menu { + implicitWidth: 150 + topPadding: 4 + bottomPadding: 4 + closePolicy: T.Popup.CloseOnEscape | T.Popup.CloseOnPressOutside + + background: MenuBg {} + + delegate: StyledMenuItem {} + } + + component StyledSeparator: T.MenuSeparator { + topPadding: 4 + bottomPadding: 4 + contentItem: Rectangle { + implicitWidth: root.implicitWidth + implicitHeight: 1 + color: Theme.mutedFg + } + } + + // ─── Root menu background ──────────────────────────────────────────────── + + background: MenuBg {} + delegate: StyledMenuItem {} + + // ─── Items ─────────────────────────────────────────────────────────────── + + StyledMenuItem { text: "Copy settings" icon.source: "qrc:/Main/assets/icons/copy-menu.svg" enabled: root.canCopy - onTriggered: { - root.copyRequested() - root.close() - } + onTriggered: { root.copyRequested(); root.close() } } - T.MenuItem { - text: canPaste ? root.selectionCount > 1 - ? "Paste settings to " + root.selectionCount + " photos" - : "Paste settings" : "Settings buffer empty" + StyledMenuItem { + text: root.canPaste + ? root.selectionCount > 1 + ? "Paste settings to " + root.selectionCount + " photos" + : "Paste settings" + : "Settings buffer empty" icon.source: "qrc:/Main/assets/icons/clipboard-paste-menu.svg" enabled: root.canPaste - onTriggered: { - root.pasteRequested() - root.close() - } + onTriggered: { root.pasteRequested(); root.close() } } - T.MenuSeparator {} + StyledSeparator {} - T.Menu { + StyledMenu { title: "Rating" - T.MenuItem { + + StyledMenuItem { text: "No rating" onTriggered: root.ratingRequested(0) } Repeater { model: 5 - delegate: T.MenuItem { + delegate: StyledMenuItem { required property int index text: (index + 1) + " " + root.stars(index + 1) onTriggered: root.ratingRequested(index + 1) @@ -95,24 +190,25 @@ T.Menu { } } - T.Menu { + StyledMenu { id: filterMenu title: "Filter" enabled: root.showFilterSection - T.MenuItem { + + StyledMenuItem { text: "Criteria: " + root.operatorLabels[root.filterOperator] onTriggered: { root.filterOperatorCycleRequested() root.keepFilterMenuOpen = true } } - T.MenuItem { + StyledMenuItem { text: "All" onTriggered: root.filterRatingRequested(0) } Repeater { model: 5 - delegate: T.MenuItem { + delegate: StyledMenuItem { required property int index text: (index + 1) + " " + root.stars(index + 1) onTriggered: root.filterRatingRequested(index + 1) @@ -120,68 +216,49 @@ T.Menu { } } - T.MenuSeparator { - visible: root.showFilterSection - } + StyledSeparator { visible: root.showFilterSection } - T.Menu { + StyledMenu { id: mergeMenu title: "Merge Photos" enabled: root.selectionCount > 1 - T.MenuItem { + + StyledMenuItem { text: "Panorama" icon.source: "qrc:/Main/assets/icons/panorama.svg" - onTriggered: { - root.createPanoramaRequested() - root.close() - } + onTriggered: { root.createPanoramaRequested(); root.close() } } - T.MenuItem { + StyledMenuItem { text: "HDR" icon.source: "qrc:/Main/assets/icons/hdr.svg" enabled: false - onTriggered: { - } } } + StyledSeparator {} - T.MenuSeparator {} - - T.MenuItem { + StyledMenuItem { text: "Rotate right" icon.source: "qrc:/Main/assets/icons/rotate-cw-menu.svg" - onTriggered: { - root.rotateRightRequested() - root.close() - } + onTriggered: { root.rotateRightRequested(); root.close() } } - T.MenuItem { + StyledMenuItem { text: "Rotate left" icon.source: "qrc:/Main/assets/icons/rotate-ccw.svg" icon.color: Theme.foreground - onTriggered: { - root.rotateLeftRequested() - root.close() - } + onTriggered: { root.rotateLeftRequested(); root.close() } } - T.MenuItem { + StyledMenuItem { text: "Flip horizontally" icon.source: "qrc:/Main/assets/icons/flip-horizontal-menu.svg" - onTriggered: { - root.flipHorizontalRequested() - root.close() - } + onTriggered: { root.flipHorizontalRequested(); root.close() } } - T.MenuItem { + StyledMenuItem { text: "Flip vertically" icon.source: "qrc:/Main/assets/icons/flip-vertical-menu.svg" - onTriggered: { - root.flipVerticalRequested() - root.close() - } + onTriggered: { root.flipVerticalRequested(); root.close() } } } diff --git a/content/views/App.qml b/content/views/App.qml index 4ef29ae..8e0317f 100644 --- a/content/views/App.qml +++ b/content/views/App.qml @@ -1,6 +1,5 @@ import QtQuick import QtQuick.Layouts -import QtQuick.Controls import QtQuick.Controls.Basic as T import QtQuick.Dialogs import Main 1.0 @@ -819,8 +818,8 @@ Window { flat: true enabled: rawViewport.canUndo opacity: enabled ? 1.0 : 0.3 - ToolTip.visible: hovered - ToolTip.text: "Undo" + T.ToolTip.visible: hovered + T.ToolTip.text: "Undo" display: AbstractButton.IconOnly padding: 0 background: null @@ -838,8 +837,8 @@ Window { flat: true enabled: rawViewport.canRedo opacity: enabled ? 1.0 : 0.3 - ToolTip.visible: hovered - ToolTip.text: "Redo" + T.ToolTip.visible: hovered + T.ToolTip.text: "Redo" display: AbstractButton.IconOnly padding: 0 background: null @@ -857,8 +856,8 @@ Window { flat: true enabled: !rawViewport.isDefault opacity: enabled ? 1.0 : 0.3 - ToolTip.visible: hovered - ToolTip.text: "Restore to Original" + T.ToolTip.visible: hovered + T.ToolTip.text: "Restore to Original" display: AbstractButton.IconOnly padding: 0 background: null @@ -874,8 +873,8 @@ Window { implicitHeight: 24 onClicked: window.showOriginal = !window.showOriginal flat: true - ToolTip.visible: hovered - ToolTip.text: "Before/After (B or \\)" + T.ToolTip.visible: hovered + T.ToolTip.text: "Before/After (B or \\)" display: AbstractButton.IconOnly padding: 0 background: null @@ -1069,7 +1068,7 @@ Window { orientation: ListView.Horizontal spacing: 10 model: rawFilesModel - ScrollBar.horizontal: PhotonScrollBar { orientation: Qt.Horizontal } + T.ScrollBar.horizontal: PhotonScrollBar { orientation: Qt.Horizontal } // Handle mouse wheel for horizontal scrolling MouseArea { @@ -1171,7 +1170,7 @@ Window { } window.contextMenuSourcePath = model.path var p = mapToItem(null, mouse.x, mouse.y) - developContextMenu.openAt(p.x, p.y) + developContextMenu.popup() } } } diff --git a/content/views/LibraryView.qml b/content/views/LibraryView.qml index 5ea36b9..0e1de2c 100644 --- a/content/views/LibraryView.qml +++ b/content/views/LibraryView.qml @@ -323,22 +323,6 @@ Control { } } } - - Item { width: 8 } - - // Home button - Button { - icon.source: "qrc:/Main/assets/icons/home.svg" - icon.color: Theme.foreground - icon.width: 20; icon.height: 20 - flat: true - onClicked: AppState.setCurrentView(AppState.ViewState.Welcome) - background: Rectangle { - color: parent.hovered ? Theme.highlight : "transparent" - radius: Theme.radius - } - implicitWidth: 36; implicitHeight: 36 - } } // --- Central Grid --- From d5fd24f753608d6464f507a5bafde4df1fc2ef51 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Tue, 31 Mar 2026 18:11:16 +0200 Subject: [PATCH 19/29] fix: darker tones behave better --- .gitignore | 1 + TASKS.md | 11 +- content/views/App.qml | 1 + content/views/DevelopView.qml | 2 +- src/components/RawViewport.cpp | 4 + src/components/RawViewport.frag | 97 ++++++++---- src/components/RawViewport.h | 4 + src/engine/ImageDeveloper.cpp | 59 +++---- src/engine/RawEngine.cpp | 129 +++++++++++---- src/engine/RawEngine.h | 11 +- tones_report.md | 267 -------------------------------- 11 files changed, 224 insertions(+), 362 deletions(-) delete mode 100644 tones_report.md diff --git a/.gitignore b/.gitignore index c45de3a..f4d668f 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ CMakeLists.txt.user* .uic/ /build*/ /dist/ +/research/ testphoton tmp Testing diff --git a/TASKS.md b/TASKS.md index 706d6f5..290d947 100644 --- a/TASKS.md +++ b/TASKS.md @@ -509,11 +509,12 @@ ## Phase 38: Panorama -- [ ] JPEG and TIFF support. -- [ ] Panorama Stitching - - [ ] OpenCV integration - - [ ] Stitching - - [ ] TIFF output +- [x] Panorama Stitching + - [x] OpenCV integration + - [x] Stitching +- [ ] Dng export + - [x] Implement a DNG-like export + - [ ] Move the implementation to ExportManager ## Backlog / Future diff --git a/content/views/App.qml b/content/views/App.qml index 8e0317f..7c28b3b 100644 --- a/content/views/App.qml +++ b/content/views/App.qml @@ -489,6 +489,7 @@ Window { property real highlights: rawViewport.highlights property real shadows: rawViewport.shadows property real whites: rawViewport.whites + property real sceneWhite: rawViewport.sceneWhite property real blacks: rawViewport.blacks property real adaptation: rawViewport.adaptation property real vibrance: rawViewport.vibrance diff --git a/content/views/DevelopView.qml b/content/views/DevelopView.qml index 515eb17..4f03f52 100644 --- a/content/views/DevelopView.qml +++ b/content/views/DevelopView.qml @@ -199,7 +199,7 @@ Control { ControlGroup { title: "Shadows"; value: root.viewport ? root.viewport.shadows : 0.0; from: -100; to: 100; defaultValue: 0.0; onMoved: (v) => { if(root.viewport) root.viewport.shadows = v }; onReleased: if(root.viewport) root.viewport.commitEdit() } ControlGroup { title: "Whites"; value: root.viewport ? root.viewport.whites : 0.0; from: -100; to: 100; defaultValue: 0.0; onMoved: (v) => { if(root.viewport) root.viewport.whites = v }; onReleased: if(root.viewport) root.viewport.commitEdit() } ControlGroup { title: "Blacks"; value: root.viewport ? root.viewport.blacks : 0.0; from: -100; to: 100; defaultValue: 0.0; onMoved: (v) => { if(root.viewport) root.viewport.blacks = v }; onReleased: if(root.viewport) root.viewport.commitEdit() } - ControlGroup { title: "Adaptation"; value: root.viewport ? root.viewport.adaptation : 9.0; from: 0; to: 100; defaultValue: 9.0; onMoved: (v) => { if(root.viewport) root.viewport.adaptation = v }; onReleased: if(root.viewport) root.viewport.commitEdit() } + //ControlGroup { title: "Adaptation"; value: root.viewport ? root.viewport.adaptation : 9.0; from: 0; to: 100; defaultValue: 9.0; onMoved: (v) => { if(root.viewport) root.viewport.adaptation = v }; onReleased: if(root.viewport) root.viewport.commitEdit() } Rectangle { Layout.fillWidth: true; height: 1; color: "#1A1A1C"; Layout.topMargin: 4; Layout.bottomMargin: 4 } diff --git a/src/components/RawViewport.cpp b/src/components/RawViewport.cpp index 0d81319..cb2292d 100644 --- a/src/components/RawViewport.cpp +++ b/src/components/RawViewport.cpp @@ -61,6 +61,10 @@ RawViewport::RawViewport(QQuickItem* parent) : QQuickItem(parent) { emit whitesChanged(); update(); }); + connect(&m_engine, &RawEngine::sceneWhiteChanged, this, [this]() { + emit sceneWhiteChanged(); + update(); + }); connect(&m_engine, &RawEngine::blacksChanged, this, [this]() { emit blacksChanged(); update(); diff --git a/src/components/RawViewport.frag b/src/components/RawViewport.frag index 064fcca..5ede166 100644 --- a/src/components/RawViewport.frag +++ b/src/components/RawViewport.frag @@ -14,6 +14,7 @@ layout(std140, binding = 0) uniform buf { float highlights; float shadows; float whites; + float sceneWhite; float blacks; float adaptation; float vibrance; @@ -351,28 +352,53 @@ float get_hsl_influence(float hue, float center, float width) { } float compute_target_luma(float luma, float stops) { + if (stops == 0.0) return luma; float target = luma * pow(2.0, stops); + + // Symmetrical soft-clipping for HSL to prevent "blowing out" + // while maintaining a more linear response than the specialized shoulder function. + if (target > 1.0) { + float over = target - 1.0; + target = 1.0 + over / (1.0 + over * 1.25); + } + return max(target, 0.0); +} + +float compute_toe_target(float luma, float stops) { + if (stops == 0.0) return luma; + + // Multiplicative base + float target = luma * pow(2.0, stops); + if (stops > 0.0) { - // Compress brightening to avoid harsh clipping artifacts. - float over = max(target - 1.0, 0.0); - if (over > 0.0) { - float shoulder = 1.2 + 3.0 * clamp(stops, 0.0, 1.0); - target = 1.0 + over / (1.0 + over * shoulder); - } + // Soft Gamma Lift (Prevents Posterization) + // A power curve is much smoother than a linear lift for deep darks. + float liftGamma = 1.0 / (1.0 + stops * 0.5); + float liftTarget = pow(max(luma, 1e-6), liftGamma); + + // Only apply the gamma lift to the bottom 15% of the range + float toeMask = 1.0 - smoothstep(0.0, 0.15, luma); + target = mix(target, liftTarget, toeMask * 0.4); } + return max(target, 0.0); } + vec3 apply_luma_target(vec3 color, float lumaIn, float targetLuma) { - targetLuma = max(targetLuma, 0.0); - float safeLuma = max(lumaIn, 1e-4); - float lumaDelta = targetLuma - lumaIn; + targetLuma = max(targetLuma, 0.0); + float safeLuma = max(lumaIn, 1e-4); float lumaRatio = targetLuma / safeLuma; - // Additive in deep shadows, multiplicative in mids/highlights. - float blend = smoothstep(0.02, 0.34, lumaIn); - vec3 additive = color + vec3(lumaDelta); - vec3 multiplicative = color * lumaRatio; - return mix(additive, multiplicative, blend); + + // --- Noise Floor Protection --- + // Cap the lift ratio in deep blacks to prevent noise/posterization. + // 1.0x cap at pure black, scaling up to 10.0x at 0.08 luma. + float maxRatio = 1.0 + 9.0 * smoothstep(0.0, 0.08, lumaIn); + float safeRatio = clamp(lumaRatio, 0.0, maxRatio); + + // --- Pure Multiplicative Adjustment --- + // Scaling R, G, and B equally preserves Hue and Saturation + return color * safeRatio; } float sample_tone_lut_channel(float value, int channel) { @@ -630,44 +656,51 @@ void main() color = apply_white_balance(color, ubuf.temperature / 100.0, ubuf.tint / 100.0); // 2. Exposure - color *= pow(2.0, ubuf.exposure); - - color = davinci_tonemap(color, ubuf.adaptation); + float exposure = pow(2.0, ubuf.exposure); + + color *= exposure; + float luma = get_luma(max(color, 0.0)); + if (luma > ubuf.sceneWhite && ubuf.exposure > 0.0) { + float over = luma - ubuf.sceneWhite; + // The higher the shoulder, the less the highlights get compressed + float knee = ubuf.sceneWhite * 0.7; // shoulder width + float compress = over / (1.0 + over / knee); // Reinhard-style on the excess + float targetL = ubuf.sceneWhite + compress; + color = apply_luma_target(color, luma, targetL); + } + float sceneWhiteNorm = max(ubuf.sceneWhite * exposure, 1e-4); + float lumaNorm = clamp(luma/sceneWhiteNorm, 0.0, 2.0); + //color = davinci_tonemap(color, ubuf.adaptation); // 3. Contrast color = max(vec3(0.0), color); color = pow(color, vec3(ubuf.contrast)); - // 4. Whites & Blacks (smoother masks, bounded response) - float luma = get_luma(max(color, 0.0)); + // 4. Whites & Blacks (specialized targeting) if (ubuf.whites != 0.0) { - float w = clamp(ubuf.whites / 100.0, -1.0, 1.0); - float whiteMask = smoothstep(0.42, 1.20, luma); - float targetLuma = compute_target_luma(luma, w * 0.85 * whiteMask); + float whiteMask = smoothstep(0.7, 1.25, lumaNorm); + float targetLuma = compute_target_luma(luma, (ubuf.whites / 100.0) * whiteMask); color = apply_luma_target(color, luma, targetLuma); luma = get_luma(max(color, 0.0)); } if (ubuf.blacks != 0.0) { - float bAdj = clamp(ubuf.blacks / 100.0, -1.0, 1.0); - float blackMask = 1.0 - smoothstep(0.0, 0.48, luma); - float targetLuma = compute_target_luma(luma, bAdj * 0.90 * blackMask); + float blackMask = 1.0 - smoothstep(0.0, 0.15, lumaNorm); + float targetLuma = compute_toe_target(luma, (ubuf.blacks / 100.0) * blackMask); color = apply_luma_target(color, luma, targetLuma); luma = get_luma(max(color, 0.0)); } - // 5. Highlights & Shadows (broader crossover, gentler extremes) + // 5. Highlights & Shadows (specialized targeting) if (ubuf.shadows != 0.0) { - float s = clamp(ubuf.shadows / 100.0, -1.0, 1.0); - float shadowMask = 1.0 - smoothstep(0.05, 0.62, luma); - float targetLuma = compute_target_luma(luma, s * 0.95 * shadowMask); + float shadowMask = 1.0 - smoothstep(0.05, 0.65, lumaNorm); + float targetLuma = compute_toe_target(luma, (ubuf.shadows / 100.0) * shadowMask); color = apply_luma_target(color, luma, targetLuma); luma = get_luma(max(color, 0.0)); } if (ubuf.highlights != 0.0) { - float h = clamp(ubuf.highlights / 100.0, -1.0, 1.0); - float highlightMask = smoothstep(0.22, 1.25, luma); - float targetLuma = compute_target_luma(luma, h * 0.90 * highlightMask); + float highlightMask = smoothstep(0.35, 1.1, lumaNorm); + float targetLuma = compute_target_luma(luma, (ubuf.highlights / 100.0) * highlightMask); color = apply_luma_target(color, luma, targetLuma); } diff --git a/src/components/RawViewport.h b/src/components/RawViewport.h index df84c79..7192635 100644 --- a/src/components/RawViewport.h +++ b/src/components/RawViewport.h @@ -21,6 +21,7 @@ class RawViewport : public QQuickItem { highlightsChanged) Q_PROPERTY(float shadows READ shadows WRITE setShadows NOTIFY shadowsChanged) Q_PROPERTY(float whites READ whites WRITE setWhites NOTIFY whitesChanged) + Q_PROPERTY(float sceneWhite READ sceneWhite NOTIFY sceneWhiteChanged) Q_PROPERTY(float blacks READ blacks WRITE setBlacks NOTIFY blacksChanged) Q_PROPERTY(float adaptation READ adaptation WRITE setAdaptation NOTIFY adaptationChanged) Q_PROPERTY( @@ -232,6 +233,8 @@ class RawViewport : public QQuickItem { float whites() const { return m_engine.whites(); } void setWhites(float val); + float sceneWhite() const { return m_engine.sceneWhite(); } + float blacks() const { return m_engine.blacks(); } void setBlacks(float val); @@ -488,6 +491,7 @@ class RawViewport : public QQuickItem { void highlightsChanged(); void shadowsChanged(); void whitesChanged(); + void sceneWhiteChanged(); void blacksChanged(); void adaptationChanged(); void vibranceChanged(); diff --git a/src/engine/ImageDeveloper.cpp b/src/engine/ImageDeveloper.cpp index ec3285c..904eb72 100644 --- a/src/engine/ImageDeveloper.cpp +++ b/src/engine/ImageDeveloper.cpp @@ -143,14 +143,23 @@ static float smoothstep_local(float edge0, float edge1, float x) { static float mix_local(float a, float b, float t) { return a + t * (b - a); } static float compute_target_luma_cpp(float luma, float stops) { + if (stops == 0.0f) return luma; + float target = luma * std::pow(2.0f, stops); + if (target > 1.0f) { + float over = target - 1.0f; + target = 1.0f + over / (1.0f + over * 1.25f); + } + return std::max(target, 0.0f); +} + +static float compute_toe_target_cpp(float luma, float stops) { + if (stops == 0.0f) return luma; float target = luma * std::pow(2.0f, stops); if (stops > 0.0f) { - // Compress brightening to avoid harsh clipping artifacts. - float over = std::max(target - 1.0f, 0.0f); - if (over > 0.0f) { - float shoulder = 1.2f + 3.0f * std::clamp(stops, 0.0f, 1.0f); - target = 1.0f + over / (1.0f + over * shoulder); - } + float liftGamma = 1.0f / (1.0f + stops * 0.5f); + float liftTarget = std::pow(std::max(luma, 1e-6f), liftGamma); + float toeMask = 1.0f - smoothstep_local(0.0f, 0.15f, luma); + target = mix_local(target, liftTarget, toeMask * 0.4f); } return std::max(target, 0.0f); } @@ -159,16 +168,12 @@ static void apply_luma_target_cpp(float& r, float& g, float& b, float lumaIn, float targetLuma) { targetLuma = std::max(targetLuma, 0.0f); float safeLuma = std::max(lumaIn, 1e-4f); - float lumaDelta = targetLuma - lumaIn; float lumaRatio = targetLuma / safeLuma; - // Additive in deep shadows, multiplicative in mids/highlights. - float blend = smoothstep_local(0.02f, 0.34f, lumaIn); - float addR = r + lumaDelta; - float addG = g + lumaDelta; - float addB = b + lumaDelta; - r = mix_local(addR, r * lumaRatio, blend); - g = mix_local(addG, g * lumaRatio, blend); - b = mix_local(addB, b * lumaRatio, blend); + float maxRatio = 1.0f + 9.0f * smoothstep_local(0.0f, 0.08f, lumaIn); + float safeRatio = std::clamp(lumaRatio, 0.0f, maxRatio); + r *= safeRatio; + g *= safeRatio; + b *= safeRatio; } static std::vector evalMonotonicSplineLut(const QVariantList& pts, @@ -428,42 +433,38 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, g = std::pow(g, con); b = std::pow(b, con); - // 3. Whites & Blacks (smoother masks, bounded response) + // 3. Whites & Blacks (specialized targeting) float luma = get_luma_cpp(std::max(0.0f, r), std::max(0.0f, g), std::max(0.0f, b)); if (whites != 0.0f) { - float w = std::clamp(whites / 100.0f, -1.0f, 1.0f); - float whiteMask = smoothstep(0.42f, 1.20f, luma); - float targetLuma = compute_target_luma_cpp(luma, w * 0.85f * whiteMask); + float whiteMask = smoothstep(0.7f, 1.25f, luma); + float targetLuma = compute_target_luma_cpp(luma, (whites / 100.0f) * whiteMask); apply_luma_target_cpp(r, g, b, luma, targetLuma); luma = get_luma_cpp(std::max(0.0f, r), std::max(0.0f, g), std::max(0.0f, b)); } if (blacks != 0.0f) { - float bAdj = std::clamp(blacks / 100.0f, -1.0f, 1.0f); - float blackMask = 1.0f - smoothstep(0.0f, 0.48f, luma); + float blackMask = 1.0f - smoothstep(0.0f, 0.15f, luma); float targetLuma = - compute_target_luma_cpp(luma, bAdj * 0.90f * blackMask); + compute_toe_target_cpp(luma, (blacks / 100.0f) * blackMask); apply_luma_target_cpp(r, g, b, luma, targetLuma); luma = get_luma_cpp(std::max(0.0f, r), std::max(0.0f, g), std::max(0.0f, b)); } - // 4. Highlights & Shadows (broader crossover, gentler extremes) + // 4. Highlights & Shadows (specialized targeting) if (shad != 0.0f) { - float s = std::clamp(shad / 100.0f, -1.0f, 1.0f); - float shadowMask = 1.0f - smoothstep(0.05f, 0.62f, luma); + float shadowMask = 1.0f - smoothstep(0.05f, 0.65f, luma); float targetLuma = - compute_target_luma_cpp(luma, s * 0.95f * shadowMask); + compute_toe_target_cpp(luma, (shad / 100.0f) * shadowMask); apply_luma_target_cpp(r, g, b, luma, targetLuma); luma = get_luma_cpp(std::max(0.0f, r), std::max(0.0f, g), std::max(0.0f, b)); } if (high != 0.0f) { - float h = std::clamp(high / 100.0f, -1.0f, 1.0f); - float highlightMask = smoothstep(0.22f, 1.25f, luma); + float highlightMask = smoothstep(0.35f, 1.1f, luma); float targetLuma = - compute_target_luma_cpp(luma, h * 0.90f * highlightMask); + compute_target_luma_cpp(luma, (high / 100.0f) * highlightMask); apply_luma_target_cpp(r, g, b, luma, targetLuma); } diff --git a/src/engine/RawEngine.cpp b/src/engine/RawEngine.cpp index d2b317d..c3fdda2 100644 --- a/src/engine/RawEngine.cpp +++ b/src/engine/RawEngine.cpp @@ -107,13 +107,23 @@ static void apply_region_tint_cpp(float& r, float& g, float& b, float hue, } static float compute_target_luma_hist(float luma, float stops) { + if (stops == 0.0f) return luma; + float target = luma * std::pow(2.0f, stops); + if (target > 1.0f) { + float over = target - 1.0f; + target = 1.0f + over / (1.0f + over * 1.25f); + } + return std::max(target, 0.0f); +} + +static float compute_toe_target_hist(float luma, float stops) { + if (stops == 0.0f) return luma; float target = luma * std::pow(2.0f, stops); if (stops > 0.0f) { - float over = std::max(target - 1.0f, 0.0f); - if (over > 0.0f) { - float shoulder = 1.2f + 3.0f * std::clamp(stops, 0.0f, 1.0f); - target = 1.0f + over / (1.0f + over * shoulder); - } + float liftGamma = 1.0f / (1.0f + stops * 0.5f); + float liftTarget = std::pow(std::max(luma, 1e-6f), liftGamma); + float toeMask = 1.0f - smoothstep(0.0f, 0.15f, luma); + target = lerp(target, liftTarget, toeMask * 0.4f); } return std::max(target, 0.0f); } @@ -122,15 +132,12 @@ static void apply_luma_target_hist(float& r, float& g, float& b, float lumaIn, float targetLuma) { targetLuma = std::max(targetLuma, 0.0f); float safeLuma = std::max(lumaIn, 1e-4f); - float lumaDelta = targetLuma - lumaIn; float lumaRatio = targetLuma / safeLuma; - float blend = smoothstep(0.02f, 0.34f, lumaIn); - float addR = r + lumaDelta; - float addG = g + lumaDelta; - float addB = b + lumaDelta; - r = lerp(addR, r * lumaRatio, blend); - g = lerp(addG, g * lumaRatio, blend); - b = lerp(addB, b * lumaRatio, blend); + float maxRatio = 1.0f + 9.0f * smoothstep(0.0f, 0.08f, lumaIn); + float safeRatio = std::clamp(lumaRatio, 0.0f, maxRatio); + r *= safeRatio; + g *= safeRatio; + b *= safeRatio; } RawEngine::RawEngine(QObject* parent) @@ -433,6 +440,12 @@ void RawEngine::setWhites(float val) { emit isDefaultChanged(); } +void RawEngine::setSceneWhite(float val) { + if (qFuzzyCompare(m_sceneWhite, val)) return; + m_sceneWhite = val; + emit sceneWhiteChanged(); +} + void RawEngine::setBlacks(float val) { if (qFuzzyCompare(m_blacks, val)) return; m_blacks = val; @@ -1463,41 +1476,37 @@ void RawEngine::requestHistogramUpdate() { g = std::pow(std::max(0.0f, g), con); b = std::pow(std::max(0.0f, b), con); - // 3. Whites & Blacks (smoother masks, bounded response) + // 3. Whites & Blacks (specialized targeting) float l_tone = 0.2126f * std::max(0.0f, r) + 0.7152f * std::max(0.0f, g) + 0.0722f * std::max(0.0f, b); if (whites != 0.0f) { - float w = std::clamp(whites / 100.0f, -1.0f, 1.0f); - float whiteMask = smoothstep(0.42f, 1.20f, l_tone); - float target = compute_target_luma_hist(l_tone, w * 0.85f * whiteMask); + float whiteMask = smoothstep(0.7f, 1.25f, l_tone); + float target = compute_target_luma_hist(l_tone, (whites / 100.0f) * whiteMask); apply_luma_target_hist(r, g, b, l_tone, target); l_tone = 0.2126f * std::max(0.0f, r) + 0.7152f * std::max(0.0f, g) + 0.0722f * std::max(0.0f, b); } if (blacks != 0.0f) { - float bAdj = std::clamp(blacks / 100.0f, -1.0f, 1.0f); - float blackMask = 1.0f - smoothstep(0.0f, 0.48f, l_tone); + float blackMask = 1.0f - smoothstep(0.0f, 0.15f, l_tone); float target = - compute_target_luma_hist(l_tone, bAdj * 0.90f * blackMask); + compute_toe_target_hist(l_tone, (blacks / 100.0f) * blackMask); apply_luma_target_hist(r, g, b, l_tone, target); l_tone = 0.2126f * std::max(0.0f, r) + 0.7152f * std::max(0.0f, g) + 0.0722f * std::max(0.0f, b); } - // 4. Highlights & Shadows (broader crossover, gentler extremes) + // 4. Highlights & Shadows (specialized targeting) if (shad != 0.0f) { - float s = std::clamp(shad / 100.0f, -1.0f, 1.0f); - float shadowMask = 1.0f - smoothstep(0.05f, 0.62f, l_tone); - float target = compute_target_luma_hist(l_tone, s * 0.95f * shadowMask); + float shadowMask = 1.0f - smoothstep(0.05f, 0.65f, l_tone); + float target = compute_toe_target_hist(l_tone, (shad / 100.0f) * shadowMask); apply_luma_target_hist(r, g, b, l_tone, target); l_tone = 0.2126f * std::max(0.0f, r) + 0.7152f * std::max(0.0f, g) + 0.0722f * std::max(0.0f, b); } if (high != 0.0f) { - float h = std::clamp(high / 100.0f, -1.0f, 1.0f); - float highlightMask = smoothstep(0.22f, 1.25f, l_tone); + float highlightMask = smoothstep(0.35f, 1.1f, l_tone); float target = - compute_target_luma_hist(l_tone, h * 0.90f * highlightMask); + compute_target_luma_hist(l_tone, (high / 100.0f) * highlightMask); apply_luma_target_hist(r, g, b, l_tone, target); } @@ -1630,6 +1639,67 @@ void RawEngine::clearProcessedImage() { } } +static constexpr float LUMA_R = 0.2126f; +static constexpr float LUMA_G = 0.7152f; +static constexpr float LUMA_B = 0.0722f; +static float srgb_to_linear(float c) { + return (c <= 0.04045f) ? (c / 12.92f) : std::pow((c + 0.055f) / 1.055f, 2.4f); +} + +float RawEngine::computeSceneWhite(const libraw_processed_image_t* img, float percentile) { + constexpr int BINS = 2048; + constexpr float BIN_SCALE = BINS - 1; + + uint32_t hist[BINS] = {}; + + size_t pixelCount = img->width * img->height; + size_t step = 4; // subsample every 4th pixel + int channels = img->colors; // 3 for RGB + + for (size_t i = 0; i < pixelCount; i += step) { + float r, g, b; + + if (img->bits == 16) { + const uint16_t* px = reinterpret_cast(img->data) + + i * channels; + r = px[0] / 65535.0f; + g = px[1] / 65535.0f; + b = px[2] / 65535.0f; + } else { + const uint8_t* px = img->data + i * channels; + r = px[0] / 255.0f; + g = px[1] / 255.0f; + b = px[2] / 255.0f; + } + + // sRGB -> linear, matches your shader's srgb_to_linear() + auto decode = [](float x) -> float { + return (x <= 0.04045f) + ? x / 12.92f + : std::pow((x + 0.055f) / 1.055f, 2.4f); + }; + + float luma = 0.2126f * decode(r) + + 0.7152f * decode(g) + + 0.0722f * decode(b); + + int bin = static_cast(std::clamp(luma, 0.0f, 1.0f) * BIN_SCALE); + hist[bin]++; + } + + size_t sampledPixels = (pixelCount + step - 1) / step; + size_t threshold = static_cast(sampledPixels * percentile); + size_t cumulative = 0; + + for (int bin = 0; bin < BINS; ++bin) { + cumulative += hist[bin]; + if (cumulative >= threshold) + return (bin + 0.5f) / BIN_SCALE; + } + + return 1.0f; +} + void RawEngine::loadRawFileAsync(const QString& path) { m_isLoading = true; emit isLoadingChanged(); @@ -1639,6 +1709,11 @@ void RawEngine::loadRawFileAsync(const QString& path) { QMutexLocker locker(&m_processorMutex); if (loadId != m_currentLoadId) return LoadResult{false, loadId}; bool ok = loadRawFileSync(path, loadId); + m_processor->dcraw_process(); + libraw_processed_image_t* img = m_processor->dcraw_make_mem_image(); + setSceneWhite(computeSceneWhite(img)); + LibRaw::dcraw_clear_mem(img); + img = nullptr; return LoadResult{ok, loadId}; }); m_loadWatcher.setFuture(future); diff --git a/src/engine/RawEngine.h b/src/engine/RawEngine.h index ce19d2d..acc4e50 100644 --- a/src/engine/RawEngine.h +++ b/src/engine/RawEngine.h @@ -30,6 +30,7 @@ class RawEngine : public QObject { highlightsChanged) Q_PROPERTY(float shadows READ shadows WRITE setShadows NOTIFY shadowsChanged) Q_PROPERTY(float whites READ whites WRITE setWhites NOTIFY whitesChanged) + Q_PROPERTY(float sceneWhite READ sceneWhite WRITE setSceneWhite NOTIFY sceneWhiteChanged) Q_PROPERTY(float blacks READ blacks WRITE setBlacks NOTIFY blacksChanged) Q_PROPERTY(float adaptation READ adaptation WRITE setAdaptation NOTIFY adaptationChanged) Q_PROPERTY( @@ -247,6 +248,9 @@ class RawEngine : public QObject { float whites() const { return m_whites; } void setWhites(float val); + float sceneWhite() const { return m_sceneWhite; } + void setSceneWhite(float val); + float blacks() const { return m_blacks; } void setBlacks(float val); @@ -508,6 +512,7 @@ class RawEngine : public QObject { void highlightsChanged(); void shadowsChanged(); void whitesChanged(); + void sceneWhiteChanged(); void blacksChanged(); void adaptationChanged(); void vibranceChanged(); @@ -615,6 +620,9 @@ class RawEngine : public QObject { }; void clearProcessedImage(); + + void recomputeSceneWhite(); + float computeSceneWhite(const libraw_processed_image_t* img, float percentile = 0.97f); void updateProcessingParams(); void rebuildToneLut(); static std::vector evalMonotonicSpline(const QVariantList& pts, @@ -633,6 +641,7 @@ class RawEngine : public QObject { float m_highlights = 0.0f; float m_shadows = 0.0f; float m_whites = 0.0f; + float m_sceneWhite = 0.0f; float m_blacks = 0.0f; float m_adaptation = 0.0f; float m_vibrance = 0.0f; @@ -751,7 +760,7 @@ class RawEngine : public QObject { int m_denoisedWidth = 0; int m_denoisedHeight = 0; QRectF m_denoisedRoi{0, 0, 1, 1}; - mutable QMutex m_processorMutex; + mutable QRecursiveMutex m_processorMutex; std::atomic m_abortDenoise{false}; std::atomic m_currentLoadId{0}; bool m_hasDenoisedResult = false; diff --git a/tones_report.md b/tones_report.md deleted file mode 100644 index b704e2b..0000000 --- a/tones_report.md +++ /dev/null @@ -1,267 +0,0 @@ -# Tone/HSL Research Report: Photon vs darktable (and RawTherapee availability) - -## Scope - -I inspected the following code in `tmp/` and Photon: - -- `tmp/darktable/src/iop/toneequal.c` -- `tmp/darktable/src/iop/shadhi.c` -- `tmp/darktable/src/iop/colorzones.c` -- `tmp/darktable/src/iop/filmicrgb.c` -- `src/components/RawViewport.frag` -- `src/engine/ImageDeveloper.cpp` - -### RawTherapee note - -I searched `tmp/` for a RawTherapee source tree (`rawtherapee`, `RawTherapee`, `therapee`) and did not find one in this workspace, so the comparison below is darktable vs Photon with explicit Photon-focused recommendations. - ---- - -## 1) How Photon currently handles tone transitions and HSL - -## Tone controls (Whites/Blacks/Shadows/Highlights) - -Photon currently uses direct, mostly per-pixel formulas in linear RGB (shader preview and C++ export): - -- **Whites**: global division by a scalar (`white_level`) - - `RawViewport.frag:605-608` - - `ImageDeveloper.cpp:388-393` -- **Blacks**: shadow-only mask + multiplicative boost - - `RawViewport.frag:609-613` - - `ImageDeveloper.cpp:394-403` -- **Shadows**: mask `1 - smoothstep(0.0, 0.25, luma)` + multiplicative gain - - `RawViewport.frag:619-623` - - `ImageDeveloper.cpp:408-415` -- **Highlights**: mask from `smoothstep(0.3, 0.95, tanh(luma * 1.5))` + custom luma transform - - `RawViewport.frag:626-654` - - `ImageDeveloper.cpp:416-437` - -Observed characteristics from this design: - -1. Transition thresholds are fixed and relatively tight, so tonal crossover can feel abrupt on some images. -2. Strong positive/negative highlight/shadow moves can push values aggressively and increase visible noise/artifacts. -3. Final clipping happens late (`RawViewport.frag:749`, `ImageDeveloper.cpp:551-553`), so overshoot can collapse into hard white/black regions. - -## HSL controls - -Photon HSL is HSV-based with 8 fixed hue bands and Gaussian influence: - -- Band influence function: `exp(-1.5 * falloff^2)` (`RawViewport.frag:346-350`) -- Fixed centers/widths (`RawViewport.frag:664-665`, `ImageDeveloper.cpp:345-350`) -- Accumulated hue/sat/luma deltas (`RawViewport.frag:670-675`, `ImageDeveloper.cpp:444-453`) -- Luminance change is a direct RGB multiplier: `color *= (1.0 + lum_adj)` (`RawViewport.frag:680`, `ImageDeveloper.cpp:457-459`) - -Observed characteristics: - -1. Selection can feel too narrow or too “banded” depending on hue neighborhood. -2. Luminance is not adjusted in a perceptual lightness space; large values can create clipping “blisters” (white/black spot artifacts). -3. No specific low-chroma protection path is applied before hue/lightness remap (gray/near-gray colors can be unstable). - ---- - -## 2) What darktable does differently (relevant to your issue) - -## A) Tone Equalizer: EV-domain, smooth interpolation, edge-aware masking - -From `toneequal.c`: - -- Works as an **exposure-octave equalizer** in scene-linear domain (`toneequal.c:21-58`). -- Uses **Gaussian radial-basis interpolation** over EV channels for smooth transitions (`toneequal.c:45-53`, `760-768`, `1224-1242`). -- Builds a luminance mask and optionally runs **guided filter / EIGF** to preserve local contrast while smoothing masks (`toneequal.c:61-69`, `865-930`). -- Has controls for **blending diameter, feathering, quantization, contrast/exposure boost** (`toneequal.c:180-187`, `3388-3396`). -- Applies bounded correction factors (LUT correction clamped to `[0.25, 4.0]`) to reduce instability (`toneequal.c:795-800`, `1239-1242`). - -Why this helps: - -- Tonal transitions are intentionally smooth and continuous in EV space. -- Local details are preserved better because mask smoothing is edge-aware rather than purely global. - -## B) Shadows/Highlights module: base-layer separation + compression controls - -From `shadhi.c`: - -- Builds a softened base layer with **Gaussian or bilateral filter** (`shadhi.c:370-398`). -- Uses dedicated **compress** and chroma-correction controls (`shadhi.c:359-364`). -- Applies transformations in controlled chunks for shadows/highlights overlays (`shadhi.c:424-487`). - -Why this helps: - -- Strong highlight/shadow moves are constrained through compression and base/detail separation, reducing harsh transitions and color damage. - -## C) Color Zones (HSL-like): curve/LUT in Lab/LCh with smoother targeting options - -From `colorzones.c`: - -- Operates in **Lab/LCh-like space** and allows selection by lightness/chroma/hue (`colorzones.c:453-467`, `499-513`, `542-555`). -- Uses curve-generated LUTs with interpolation options (Catmull, monotonic Hermite, etc.) (`colorzones.c:78`, `2732-2738`, `2869-2898`). -- “Smooth mode” blends hue/lightness influence toward neutral for low-chroma pixels (`colorzones.c:555-563`). - -Why this helps: - -- Color targeting transitions are smoother and more controllable. -- Low-saturation regions are protected from hue/lightness artifacts. - -## D) Filmic RGB: toe/shoulder shaping + desaturation/reconstruction near clipping - -From `filmicrgb.c`: - -- Parametric toe/latitude/shoulder spline for controlled dynamic-range compression (`filmicrgb.c:946-1009`). -- Dedicated desaturation shaping near extremes (`filmicrgb.c:1011-1035`). -- Highlight mask/reconstruction with soft weighting and optional inpainted noise in clipping regions (`filmicrgb.c:1048-1089`). - -Why this helps: - -- Reduces hard clipping and preserves natural highlight roll-off under extreme edits. - ---- - -## 3) Direct comparison summary - -| Area | Photon (current) | darktable approach | Practical impact | -|---|---|---|---| -| Tone targeting | Fixed masks + direct multipliers | EV-channel equalization + smooth interpolation | Photon can feel harsher at crossover points | -| Local detail preservation | No dedicated edge-aware luminance mask in tone sliders | Guided/EIGF/bilateral mask smoothing | Better detail retention and fewer halos/noise bursts in darktable | -| Extreme edits handling | Late clamp, limited protection | Bounded correction + filmic/reconstruction strategies | Photon more prone to blown/blocked artifact spots | -| HSL selection smoothness | 8 fixed Gaussian hue bands in HSV | Curve/LUT with selectable interpolation, smooth/strong modes | darktable offers smoother color transitions | -| HSL luminance behavior | RGB multiply by `(1 + lum_adj)` | Lightness/chroma/hue remap in LCh-like model | Photon more likely to produce white/black blisters at extremes | -| Low-chroma safety | No explicit low-chroma blend protection | Explicit blend-to-neutral in smooth mode | darktable avoids gray-area hue/lightness artifacts better | - ---- - -## 4) Suggestions for Photon (proposed implementation direction) - -## Priority 1 — Tone transition quality and artifact resistance - -1. **Move tone targeting to EV-domain interpolation** - Implement a tone-equalizer-like mapping (multi-band EV controls + Gaussian/RBF interpolation) instead of hard fixed tonal masks. - -2. **Add edge-aware luminance-mask smoothing path** - Add guided-filter/EIGF-style smoothing on the luminance mask for large highlight/shadow moves (with feathering + quantization controls). - -3. **Add bounded correction and soft roll-off** - Bound correction factors and add a dedicated soft shoulder/toe rolloff before final output clamp to reduce blister artifacts. - -## Priority 2 — HSL smoothness and luminance safety - -4. **Migrate HSL processing from HSV RGB-multiply to perceptual space (LCh/OKLCh-style)** - Keep hue/chroma/lightness edits in a perceptual model; avoid direct RGB luminance scaling for large adjustments. - -5. **Introduce low-chroma protection blend** - Fade hue/lightness adjustments toward neutral when chroma is low (similar to `colorzones` smooth mode). - -6. **Add interpolation mode for color targeting** - Keep current behavior as “strong”, add a “smooth” mode with monotonic or centripetal interpolation to prevent cusps/oscillation. - -## Priority 3 — Robustness and parity - -7. **Unify shader and `ImageDeveloper` tone/HSL math exactly** - Keep one formula set to avoid preview/export divergence when edge cases are hit. - -8. **Add regression tests for extreme controls** - Add automated tests for: - - highlight/shadow extremes, - - HSL luminance ±100 on saturated and near-gray samples, - - continuity checks (no step discontinuities across tonal boundaries). - ---- - -If you want, next step I can convert this into an implementation checklist with exact code touchpoints (`RawViewport.frag` + `ImageDeveloper.cpp` + UI controls) so we can start iterating safely. - ---- - -## 5) Addendum — Tone curve banding (Photon vs darktable) - -### Photon current behavior (banding-relevant) - -- Tone curve LUT is rebuilt at **256 samples/channel** and quantized to **8-bit RGBA**: - - `RawEngine.cpp:1314-1317`, `1335-1342` - - `ToneLutProvider.h:24-31` -- Shader tone-curve sampling uses the 256×4 LUT rows (`toneLUT`) in the processing pass: - - `RawViewport.frag:721-738` - - `App.qml:348-353` -- Photon dithering is currently a single pseudo-random per-pixel add at amplitude `1/255`: - - `RawViewport.frag:538-540`, `769-770` - - `ImageDeveloper.cpp:127-131`, `584-588` -- In `ImageDeveloper`, dithering is applied **before** denoise (`579-589` then `596-637`), so part of anti-banding noise can be removed again by denoising. - -### darktable references - -- darktable tone curve uses float processing with a **0x10000 LUT** (65536 entries), not 256: - - `tmp/darktable/src/iop/tonecurve.c:136`, `755` -- darktable has a dedicated **dither/posterize** module for output quantization control: - - module intent: reduce output banding/posterization (`dither.c:114-115`) - - auto bit-depth-aware mode (`dither.c:344-383`) - - methods include Floyd-Steinberg error diffusion and random TPDF (`dither.c:393-400`, `574-607`) - -### Why Photon shows more banding after strong tone-curve edits - -1. LUT precision is effectively 8-bit/256-sample in the GPU path, so steep/curvy segments can staircase. -2. Dither strategy is fixed-amplitude and not export bit-depth aware. -3. In CPU preview/export path, dither can be attenuated by subsequent denoise. - -### Suggested direction (engine-side) - -1. Raise tone-LUT precision (e.g., 4096+ samples or 65536 table, plus higher-precision LUT texture/storage). -2. Keep curve application in high precision until final output quantization. -3. Move/export dithering to the **final step** only (after denoise and all tone/color operations), with bit-depth aware amplitude. -4. Prefer TPDF/blue-noise dithering for raster output; optional FS diffusion for 8-bit export paths. - ---- - -## 6) Addendum — Denoise softness / detail loss (Photon vs darktable) - -### Photon current behavior (detail-relevant) - -- BM3D strength maps linearly from slider to sigma (`sigma = intensity * 80`): - - `Denoiser.h:19-22` -- Pipeline is BM3D on luminance + chroma BM3D + multi-scale guided filter on chroma: - - `Denoiser.cpp:127-176`, `1059-1081` -- Full denoised buffers are returned by the engine when available: - - `RawEngine.cpp:1804-1823` -- Shader pass still applies real-time denoise from slider value unconditionally: - - `RawViewport.frag:567-568` -- CPU developer path applies denoise after linear->sRGB conversion and after dithering: - - `ImageDeveloper.cpp:579-589`, `596-637` - -### darktable references - -- `raw denoise` is explicitly early, scene-linear/raw pipeline: - - `tmp/darktable/src/iop/rawdenoise.c:139-143` -- raw denoise uses variance-stabilizing transform + wavelet denoise: - - `rawdenoise.c:219-233`, `449-450` -- profiled denoise has camera/ISO-driven model + controls for preserving detail: - - modes (NLMeans/wavelets): `denoiseprofile.c:68-72` - - parameters (`shadows`, `central pixel weight`, `overshooting`): `99-114` - - adaptive preconditioning with shadows/WB and scaling: `1682-1705` - - noise-profile-driven auto inference (`radius/scattering/shadows/bias`): `2650-2668` -- darktable NLMeans implementation includes scattering pattern to avoid grid artifacts and central-pixel weighting: - - `nlmeans_core.c:84-90`, `135-140`, `432-435` - -### Why Photon can look over-soft - -1. Strength mapping is global and not noise-profile adaptive (can oversmooth clean files). -2. Denoise placement in `ImageDeveloper` is late (after gamma/8-bit conversion path), which is suboptimal for detail retention. -3. Engine can provide denoised buffers while shader still applies denoise logic, increasing perceived softness. - -### Suggested direction (engine-side, no UI changes required) - -1. Ensure denoise is applied once in viewport path (skip shader denoise when `m_hasDenoisedResult` is active). -2. Move CPU denoise earlier in `ImageDeveloper` (before sRGB quantization/dither). -3. Replace fixed sigma scaling with profile/adaptive scaling (ISO/noise model + luma-aware strength). -4. Keep denoise/detail separation explicit (edge/detail protection mask or blend-back strategy for high frequencies). - ---- - -## 7) Practical implementation touchpoints for discussion - -- Tone-curve precision/banding: - - `src/engine/RawEngine.cpp` (`rebuildToneLut`) - - `src/components/ToneLutProvider.h` - - `content/views/App.qml` (tone LUT source path) - - `src/components/RawViewport.frag` (tone-LUT sample + final dither) - - `src/engine/ImageDeveloper.cpp` (CPU LUT + final dithering stage) -- Denoise softness: - - `src/engine/Denoiser.h/.cpp` (strength mapping and BM3D/chroma strategy) - - `src/engine/RawEngine.cpp` (`startAsyncDenoise`, `getProcessedData`) - - `src/components/RawViewport.frag` (real-time denoise pass placement) - - `src/engine/ImageDeveloper.cpp` (denoise ordering in export/preview path) From da30d972cfbe0f6e703ee978c57dfe1a1abaf410 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Wed, 1 Apr 2026 10:11:27 +0200 Subject: [PATCH 20/29] fix: log level name confict in windows builds --- src/components/RawViewport.cpp | 12 +++++----- src/engine/GpuSearcher.cpp | 34 ++++++++++++++-------------- src/engine/ImageDeveloper.cpp | 16 +++++++------- src/engine/RawEngine.cpp | 30 ++++++++++++------------- src/managers/AppStateManager.cpp | 6 ++--- src/managers/LogManager.cpp | 4 ++-- src/managers/LogManager.h | 38 ++++++++++++++++---------------- src/managers/PreviewManager.cpp | 10 ++++----- 8 files changed, 75 insertions(+), 75 deletions(-) diff --git a/src/components/RawViewport.cpp b/src/components/RawViewport.cpp index cb2292d..14df740 100644 --- a/src/components/RawViewport.cpp +++ b/src/components/RawViewport.cpp @@ -402,10 +402,10 @@ RawViewport::RawViewport(QQuickItem* parent) : QQuickItem(parent) { } void RawViewport::setSource(const QString& source) { - LogManager::instance()->log(QString("[ RawViewport ] - setSource START: %1").arg(source), DEBUG); + LogManager::instance()->log(QString("[ RawViewport ] - setSource START: %1").arg(source), PHOTON_DEBUG); if (m_engine.source() == source) { - LogManager::instance()->log("[ RawViewport ] - setSource: same source, skipping", DEBUG); + LogManager::instance()->log("[ RawViewport ] - setSource: same source, skipping", PHOTON_DEBUG); return; } @@ -421,14 +421,14 @@ void RawViewport::setSource(const QString& source) { // This ensures the old image is removed before the new one loads update(); - LogManager::instance()->log("[ RawViewport ] - setSource: calling m_engine.setSource", DEBUG); + LogManager::instance()->log("[ RawViewport ] - setSource: calling m_engine.setSource", PHOTON_DEBUG); m_engine.setSource(source); - LogManager::instance()->log("[ RawViewport ] - setSource: emitting sourceChanged", DEBUG); + LogManager::instance()->log("[ RawViewport ] - setSource: emitting sourceChanged", PHOTON_DEBUG); emit sourceChanged(); update(); - LogManager::instance()->log("[ RawViewport ] - setSource END", DEBUG); + LogManager::instance()->log("[ RawViewport ] - setSource END", PHOTON_DEBUG); } void RawViewport::setExposure(float ev) { @@ -1143,7 +1143,7 @@ QSGNode* RawViewport::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { .arg(m_zoom, 0, 'f', 3) .arg(m_panOffset.x(), 0, 'f', 2) .arg(m_panOffset.y(), 0, 'f', 2), - DEBUG); + PHOTON_DEBUG); } // --- THREAD-SAFE SIGNAL EMISSION --- diff --git a/src/engine/GpuSearcher.cpp b/src/engine/GpuSearcher.cpp index 4046b22..c0aa10c 100644 --- a/src/engine/GpuSearcher.cpp +++ b/src/engine/GpuSearcher.cpp @@ -19,7 +19,7 @@ GpuSearcher::~GpuSearcher() { std::vector GpuSearcher::runSearch( const float* luma, int width, int height, int searchWindow) { - + auto* ctx = VulkanComputeContext::instance(); if (ctx->device() == VK_NULL_HANDLE) return {}; @@ -29,7 +29,7 @@ std::vector GpuSearcher::runSearch( const auto& f = ctx->functions(); VkDevice device = ctx->device(); - LogManager::instance()->log(QString("[ GpuSearcher ] - Starting raw Vulkan search %1x%2").arg(width).arg(height), INFO); + LogManager::instance()->log(QString("[ GpuSearcher ] - Starting raw Vulkan search %1x%2").arg(width).arg(height), PHOTON_INFO); // 1. Create Resources VkBuffer lumaBuffer = VK_NULL_HANDLE, resultBuffer = VK_NULL_HANDLE; @@ -40,20 +40,20 @@ std::vector GpuSearcher::runSearch( ctx->createBuffer(lumaSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, lumaBuffer, lumaMemory); - + ctx->createBuffer(resultSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, resultBuffer, resultMemory); if (lumaBuffer == VK_NULL_HANDLE || resultBuffer == VK_NULL_HANDLE) { - LogManager::instance()->log("Failed to create Vulkan buffers for search", ERROR); + LogManager::instance()->log("Failed to create Vulkan buffers for search", PHOTON_ERROR); return {}; } // Upload Luma void* dataPtr = nullptr; if (f.MapMemory(device, lumaMemory, 0, lumaSize, 0, &dataPtr) != VK_SUCCESS) { - LogManager::instance()->log("Failed to map luma memory", ERROR); + LogManager::instance()->log("Failed to map luma memory", PHOTON_ERROR); return {}; } memcpy(dataPtr, luma, lumaSize); @@ -78,7 +78,7 @@ std::vector GpuSearcher::runSearch( VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE; if (f.CreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) { - LogManager::instance()->log("Failed to create descriptor set layout", ERROR); + LogManager::instance()->log("Failed to create descriptor set layout", PHOTON_ERROR); return {}; } @@ -94,7 +94,7 @@ std::vector GpuSearcher::runSearch( VkDescriptorPool descriptorPool = VK_NULL_HANDLE; if (f.CreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { - LogManager::instance()->log("Failed to create descriptor pool", ERROR); + LogManager::instance()->log("Failed to create descriptor pool", PHOTON_ERROR); return {}; } @@ -106,7 +106,7 @@ std::vector GpuSearcher::runSearch( VkDescriptorSet descriptorSet = VK_NULL_HANDLE; if (f.AllocateDescriptorSets(device, &allocInfo, &descriptorSet) != VK_SUCCESS) { - LogManager::instance()->log("Failed to allocate descriptor set", ERROR); + LogManager::instance()->log("Failed to allocate descriptor set", PHOTON_ERROR); return {}; } @@ -145,16 +145,16 @@ std::vector GpuSearcher::runSearch( VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; if (f.CreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) { - LogManager::instance()->log("Failed to create pipeline layout", ERROR); + LogManager::instance()->log("Failed to create pipeline layout", PHOTON_ERROR); return {}; } QFile shaderFile(":/Main/shaders/patch_search.comp.qsb"); if (!shaderFile.open(QIODevice::ReadOnly)) { - LogManager::instance()->log("Failed to open shader resource", ERROR); + LogManager::instance()->log("Failed to open shader resource", PHOTON_ERROR); return {}; } - + QShader shader = QShader::fromSerialized(shaderFile.readAll()); QByteArray spirvCode; auto shaders = shader.availableShaders(); @@ -166,14 +166,14 @@ std::vector GpuSearcher::runSearch( } if (spirvCode.isEmpty()) { - LogManager::instance()->log("Failed to extract SPIR-V from shader", ERROR); + LogManager::instance()->log("Failed to extract SPIR-V from shader", PHOTON_ERROR); return {}; } // Ensure alignment by copying to vector std::vector code(spirvCode.size() / 4); memcpy(code.data(), spirvCode.constData(), spirvCode.size()); - + VkShaderModuleCreateInfo shaderModuleCreateInfo{}; shaderModuleCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; shaderModuleCreateInfo.codeSize = code.size() * 4; @@ -181,7 +181,7 @@ std::vector GpuSearcher::runSearch( VkShaderModule computeShaderModule = VK_NULL_HANDLE; if (f.CreateShaderModule(device, &shaderModuleCreateInfo, nullptr, &computeShaderModule) != VK_SUCCESS) { - LogManager::instance()->log("Failed to create shader module", ERROR); + LogManager::instance()->log("Failed to create shader module", PHOTON_ERROR); return {}; } @@ -195,7 +195,7 @@ std::vector GpuSearcher::runSearch( VkPipeline pipeline = VK_NULL_HANDLE; if (f.CreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline) != VK_SUCCESS) { - LogManager::instance()->log("Failed to create compute pipeline", ERROR); + LogManager::instance()->log("Failed to create compute pipeline", PHOTON_ERROR); return {}; } @@ -204,7 +204,7 @@ std::vector GpuSearcher::runSearch( if (cb != VK_NULL_HANDLE) { f.CmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); f.CmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); - + int pcs[3] = {width, height, searchWindow}; f.CmdPushConstants(cb, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, 12, pcs); @@ -221,7 +221,7 @@ std::vector GpuSearcher::runSearch( } f.UnmapMemory(device, resultMemory); } else { - LogManager::instance()->log("Failed to map result memory for readback", ERROR); + LogManager::instance()->log("Failed to map result memory for readback", PHOTON_ERROR); } // 6. Cleanup diff --git a/src/engine/ImageDeveloper.cpp b/src/engine/ImageDeveloper.cpp index 904eb72..ac275ac 100644 --- a/src/engine/ImageDeveloper.cpp +++ b/src/engine/ImageDeveloper.cpp @@ -266,10 +266,10 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, .arg(width) .arg(height) .arg((quintptr)QThread::currentThread()), - DEBUG); + PHOTON_DEBUG); if (!src || width <= 0 || height <= 0) { - LogManager::instance()->log("[ ImageDeveloper ] - develop ABORT: invalid params", ERROR); + LogManager::instance()->log("[ ImageDeveloper ] - develop ABORT: invalid params", PHOTON_ERROR); return QImage(); } @@ -307,7 +307,7 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, .arg(high, 0, 'f', 2) .arg(shad, 0, 'f', 2) .arg(denoiseAmount, 0, 'f', 1), - DEBUG); + PHOTON_DEBUG); // HSL Params float hsl_h[8], hsl_s[8], hsl_l[8]; @@ -617,7 +617,7 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, QString("[ ImageDeveloper ] - Denoising: amount=%1 (rhi=%2)") .arg(denoiseAmount, 0, 'f', 1) .arg((quintptr)rhi), - INFO); + PHOTON_INFO); std::vector gpuMatches; if (rhi) { int w = output.width(); @@ -639,9 +639,9 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, gpuMatches = searcher.runSearch(luma.data(), w, h, 19); if (gpuMatches.empty()) { - LogManager::instance()->log("[ ImageDeveloper ] - GPU search produced no matches (possibly due to frame conflict or shader error). Falling back to CPU matching.", WARNING); + LogManager::instance()->log("[ ImageDeveloper ] - GPU search produced no matches (possibly due to frame conflict or shader error). Falling back to CPU matching.", PHOTON_WARNING); } else { - LogManager::instance()->log(QString("[ ImageDeveloper ] - GPU search successful: %1 matches").arg(gpuMatches.size()), DEBUG); + LogManager::instance()->log(QString("[ ImageDeveloper ] - GPU search successful: %1 matches").arg(gpuMatches.size()), PHOTON_DEBUG); } } photon::DenoiseParams dparams; @@ -741,11 +741,11 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, .arg(cropBottom) .arg(output.width()) .arg(output.height()), - DEBUG); + PHOTON_DEBUG); LogManager::instance()->log( QString("[ ImageDeveloper ] - export END: %1x%2").arg(output.width()).arg(output.height()), - DEBUG); + PHOTON_DEBUG); return output; } diff --git a/src/engine/RawEngine.cpp b/src/engine/RawEngine.cpp index c3fdda2..95954af 100644 --- a/src/engine/RawEngine.cpp +++ b/src/engine/RawEngine.cpp @@ -229,10 +229,10 @@ RawEngine::RawEngine(QObject* parent) LogManager::instance()->log( QString("[ RawEngine ] - previewWatcher callback START (thread: %1)") .arg((quintptr)QThread::currentThread()), - DEBUG); + PHOTON_DEBUG); if (m_previewWatcher.isCanceled()) { LogManager::instance()->log("[ RawEngine ] - previewWatcher: canceled", - DEBUG); + PHOTON_DEBUG); return; } QImage result = m_previewWatcher.result(); @@ -241,11 +241,11 @@ RawEngine::RawEngine(QObject* parent) .arg(result.isNull()) .arg(result.width()) .arg(result.height()), - DEBUG); + PHOTON_DEBUG); m_previewImage = result; emit previewImageChanged(); LogManager::instance()->log("[ RawEngine ] - previewWatcher callback END", - DEBUG); + PHOTON_DEBUG); }); // Listen for background previews @@ -308,14 +308,14 @@ void RawEngine::setHalfSize(bool half) { void RawEngine::setSource(const QString& source) { LogManager::instance()->log( - QString("[ RawEngine ] - setSource START: %1").arg(source), INFO); + QString("[ RawEngine ] - setSource START: %1").arg(source), PHOTON_INFO); // 1. Abort any ongoing denoise tasks m_abortDenoise = true; if (m_source == source) { LogManager::instance()->log( - "[ RawEngine ] - setSource: same source, skipping", DEBUG); + "[ RawEngine ] - setSource: same source, skipping", PHOTON_DEBUG); return; } @@ -339,7 +339,7 @@ void RawEngine::setSource(const QString& source) { // Try to get existing preview immediately LogManager::instance()->log("[ RawEngine ] - setSource: getting preview path", - DEBUG); + PHOTON_DEBUG); if (photon::PreviewManager::instance()) { m_previewPath = photon::PreviewManager::instance()->getPreviewPath(m_source); @@ -349,7 +349,7 @@ void RawEngine::setSource(const QString& source) { LogManager::instance()->log( QString("[ RawEngine ] - setSource: starting preview image load: %1") .arg(m_previewPath), - DEBUG); + PHOTON_DEBUG); m_previewWatcher.setFuture( QtConcurrent::run([path = m_previewPath]() { return QImage(path); })); } @@ -381,10 +381,10 @@ void RawEngine::setSource(const QString& source) { // Start async loading LogManager::instance()->log("[ RawEngine ] - setSource: starting async load", - DEBUG); + PHOTON_DEBUG); loadRawFileAsync(source); - LogManager::instance()->log("[ RawEngine ] - setSource END", INFO); + LogManager::instance()->log("[ RawEngine ] - setSource END", PHOTON_INFO); } void RawEngine::setViewportSize(const QSize& size) { @@ -2340,7 +2340,7 @@ void RawEngine::loadEdits() { QString("[ RawEngine ] - Loading edits: denoiseEnabled=%1, denoiseAmount=%2") .arg(lastState["denoiseEnabled"].toBool()) .arg(lastState["denoiseAmount"].toDouble()), - DEBUG); + PHOTON_DEBUG); applyJsonToState(this, lastState); emit editStackChanged(); @@ -2672,7 +2672,7 @@ QImage RawEngine::applyGeometryTransforms(const QImage& input, int orientSteps, .arg(cropBottom) .arg(output.width()) .arg(output.height()), - DEBUG); + PHOTON_DEBUG); return output; } @@ -2682,7 +2682,7 @@ void RawEngine::reloadWithGeometry() { LogManager::instance()->log( "[ RawEngine.cpp ] - reloadWithGeometry: re-decoding with geometry bake", - DEBUG); + PHOTON_DEBUG); m_inCropMode = false; m_isLoading = true; @@ -2771,7 +2771,7 @@ void RawEngine::enterCropMode() { LogManager::instance()->log( "[ RawEngine.cpp ] - enterCropMode: showing original for crop editing", - DEBUG); + PHOTON_DEBUG); m_inCropMode = true; @@ -2801,7 +2801,7 @@ void RawEngine::enterCropMode() { void RawEngine::exitCropMode() { LogManager::instance()->log( - "[ RawEngine.cpp ] - exitCropMode: re-baking geometry", DEBUG); + "[ RawEngine.cpp ] - exitCropMode: re-baking geometry", PHOTON_DEBUG); m_inCropMode = false; diff --git a/src/managers/AppStateManager.cpp b/src/managers/AppStateManager.cpp index 37ccf5e..e7235c7 100644 --- a/src/managers/AppStateManager.cpp +++ b/src/managers/AppStateManager.cpp @@ -115,7 +115,7 @@ void AppStateManager::loadSettings() { m_accentColor = m_settings.value(KEY_ACCENT_COLOR, "#3b82f6").toString(); m_previewDenoiseFull = m_settings.value(KEY_PREVIEW_DENOISE_FULL, false).toBool(); - QString level = m_settings.value("diagnostics/logLevel", INFO).toString(); + QString level = m_settings.value("diagnostics/logLevel", PHOTON_INFO).toString(); LogManager::instance()->setLogLevel(level); emit lastOpenedFolderChanged(); @@ -241,7 +241,7 @@ void AppStateManager::setCurrentFolder(const QString& folder) { void AppStateManager::setCurrentImage(const QString& image) { LogManager::instance()->log( QString("[ AppStateManager ] - setCurrentImage START: %1").arg(image), - DEBUG); + PHOTON_DEBUG); if (m_currentImage != image) { m_currentImage = image; @@ -257,7 +257,7 @@ void AppStateManager::setCurrentImage(const QString& image) { } LogManager::instance()->log("[ AppStateManager ] - setCurrentImage END", - DEBUG); + PHOTON_DEBUG); } void AppStateManager::toggleSelection(const QString& path) { diff --git a/src/managers/LogManager.cpp b/src/managers/LogManager.cpp index 4de27f3..42f0ae7 100644 --- a/src/managers/LogManager.cpp +++ b/src/managers/LogManager.cpp @@ -67,7 +67,7 @@ void LogManager::openLogFile() { QIODevice::Text)) { qWarning() << "Failed to open log file at" << m_logLocation; } else { - log("Logging started at " + m_logLocation, INFO); + log("Logging started at " + m_logLocation, PHOTON_INFO); } } @@ -81,7 +81,7 @@ void LogManager::log(const QString& message, int level) { QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss.zzz"); out << QString("[ %1 ] [ %2 ] %3\n").arg(timestamp, enumLevelToStr(level), message); out.flush(); - if (level == FATAL) + if (level == PHOTON_FATAL) QGuiApplication::quit(); } diff --git a/src/managers/LogManager.h b/src/managers/LogManager.h index 9bfb9a6..14736b6 100644 --- a/src/managers/LogManager.h +++ b/src/managers/LogManager.h @@ -12,11 +12,11 @@ namespace photon { enum LogLevel { - DEBUG, - INFO, - WARNING, - ERROR, - FATAL + PHOTON_DEBUG, + PHOTON_INFO, + PHOTON_WARNING, + PHOTON_ERROR, + PHOTON_FATAL }; class LogManager : public QObject { @@ -38,47 +38,47 @@ class LogManager : public QObject { LogLevel strLevelToEnum(QString level) const { if (level == "DEBUG") - return DEBUG; + return PHOTON_DEBUG; else if (level == "INFO") - return INFO; + return PHOTON_INFO; else if (level == "WARNING") - return WARNING; + return PHOTON_WARNING; else if (level == "ERROR") - return ERROR; + return PHOTON_ERROR; else if (level == "FATAL") - return FATAL; + return PHOTON_FATAL; - return DEBUG; + return PHOTON_DEBUG; } QString enumLevelToStr(int level) const { switch(level) { - case DEBUG: + case PHOTON_DEBUG: return "DEBUG"; - case INFO: + case PHOTON_INFO: return "INFO"; - case WARNING: + case PHOTON_WARNING: return "WARNING"; - case ERROR: + case PHOTON_ERROR: return "ERROR"; - case FATAL: + case PHOTON_FATAL: return "FATAL"; } return ""; } - QString logLevel() const { + QString logLevel() const { return LogManager::enumLevelToStr(m_logLevel); } void setLogLevel(int level); void setLogLevel(const QString& level); - Q_INVOKABLE void log(const QString& message, int level = INFO); + Q_INVOKABLE void log(const QString& message, int level = PHOTON_INFO); Q_INVOKABLE void clearLog(); signals: @@ -88,7 +88,7 @@ class LogManager : public QObject { private: static LogManager* s_instance; QString m_logLocation; - int m_logLevel = INFO; + int m_logLevel = PHOTON_INFO; QFile m_logFile; QMutex m_logMutex; diff --git a/src/managers/PreviewManager.cpp b/src/managers/PreviewManager.cpp index 16cb779..bdc40ee 100644 --- a/src/managers/PreviewManager.cpp +++ b/src/managers/PreviewManager.cpp @@ -167,12 +167,12 @@ void PreviewManager::cancelAll() { } void PreviewManager::processItem(const QString& rawPath, bool skipGpu) { - LogManager::instance()->log(QString("[ PreviewManager ] - processItem START: %1").arg(rawPath), DEBUG); + LogManager::instance()->log(QString("[ PreviewManager ] - processItem START: %1").arg(rawPath), PHOTON_DEBUG); { QMutexLocker locker(&m_mutex); if (m_abort) { - LogManager::instance()->log(QString("[ PreviewManager ] - processItem ABORTED: %1").arg(rawPath), DEBUG); + LogManager::instance()->log(QString("[ PreviewManager ] - processItem ABORTED: %1").arg(rawPath), PHOTON_DEBUG); return; } } @@ -185,7 +185,7 @@ void PreviewManager::processItem(const QString& rawPath, bool skipGpu) { fileInfo.fileName() + ".json"); QJsonObject lastState; if (QFile::exists(editsPath)) { - LogManager::instance()->log(QString("[ PreviewManager ] - Loading sidecar: %1").arg(editsPath), DEBUG); + LogManager::instance()->log(QString("[ PreviewManager ] - Loading sidecar: %1").arg(editsPath), PHOTON_DEBUG); QFile file(editsPath); if (file.open(QIODevice::ReadOnly)) { QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); @@ -197,7 +197,7 @@ void PreviewManager::processItem(const QString& rawPath, bool skipGpu) { } // 2. Load RAW via LibRaw (Fast mode) - LogManager::instance()->log(QString("[ PreviewManager ] - Opening RAW file: %1").arg(rawPath), DEBUG); + LogManager::instance()->log(QString("[ PreviewManager ] - Opening RAW file: %1").arg(rawPath), PHOTON_DEBUG); LibRaw processor; processor.imgdata.params.output_bps = 16; processor.imgdata.params.use_camera_wb = 1; @@ -251,7 +251,7 @@ void PreviewManager::processItem(const QString& rawPath, bool skipGpu) { } } } - LogManager::instance()->log(QString("[ PreviewManager ] - processItem END: %1").arg(rawPath), DEBUG); + LogManager::instance()->log(QString("[ PreviewManager ] - processItem END: %1").arg(rawPath), PHOTON_DEBUG); } } // namespace photon From f6ee45034cf9218580c1a5df742fd91b9144ac62 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Wed, 1 Apr 2026 10:31:52 +0200 Subject: [PATCH 21/29] fix: typo --- src/engine/GpuChromaFilter.cpp | 10 ++--- src/engine/Panorama.cpp | 58 ++++++++++++++--------------- src/engine/VulkanComputeContext.cpp | 2 +- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/engine/GpuChromaFilter.cpp b/src/engine/GpuChromaFilter.cpp index 3f1b333..6801534 100644 --- a/src/engine/GpuChromaFilter.cpp +++ b/src/engine/GpuChromaFilter.cpp @@ -432,12 +432,12 @@ bool GpuChromaFilter::run(const float* guide, const float* input, QString("[ GpuChromaFilter ] - Starting GPU guided filter %1x%2") .arg(width) .arg(height), - INFO); + PHOTON_INFO); GpuResources res{}; if (!createResources(res, width, height)) { LogManager::instance()->log( - "[ GpuChromaFilter ] - Failed to create GPU resources", ERROR); + "[ GpuChromaFilter ] - Failed to create GPU resources", PHOTON_ERROR); destroyResources(res); return false; } @@ -447,7 +447,7 @@ bool GpuChromaFilter::run(const float* guide, const float* input, if (!uploadBuffer(res, BUF_GUIDE, guide, bufSize) || !uploadBuffer(res, BUF_INPUT, input, bufSize)) { LogManager::instance()->log( - "[ GpuChromaFilter ] - Failed to upload data to GPU", ERROR); + "[ GpuChromaFilter ] - Failed to upload data to GPU", PHOTON_ERROR); destroyResources(res); return false; } @@ -485,7 +485,7 @@ bool GpuChromaFilter::run(const float* guide, const float* input, // After 3 passes + swaps, result is in the buffer pointed to by inputBuf if (!readbackBuffer(res, inputBuf, output, bufSize)) { LogManager::instance()->log( - "[ GpuChromaFilter ] - Failed to read back GPU results", ERROR); + "[ GpuChromaFilter ] - Failed to read back GPU results", PHOTON_ERROR); destroyResources(res); return false; } @@ -493,7 +493,7 @@ bool GpuChromaFilter::run(const float* guide, const float* input, destroyResources(res); LogManager::instance()->log( - "[ GpuChromaFilter ] - GPU guided filter complete", INFO); + "[ GpuChromaFilter ] - GPU guided filter complete", PHOTON_INFO); return true; } diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp index a2568a0..69d2fe5 100644 --- a/src/engine/Panorama.cpp +++ b/src/engine/Panorama.cpp @@ -42,7 +42,7 @@ void Panorama::stitchAsync(const QStringList& inputFiles, connect(thread, &QThread::finished, thread, &QObject::deleteLater); thread->start(); LogManager::instance()->log( - QString("[ Panorama.cpp ] - Started panorama stitching thread"), INFO); + QString("[ Panorama.cpp ] - Started panorama stitching thread"), PHOTON_INFO); } cv::Mat Panorama::raw_to_linear(const QString& file, std::unique_ptr& colorInfo) { @@ -54,26 +54,26 @@ cv::Mat Panorama::raw_to_linear(const QString& file, std::unique_ptr& processor.imgdata.params.use_camera_matrix = 1; if (processor.open_file(file.toStdString().c_str()) != LIBRAW_SUCCESS) { LogManager::instance()->log( - QString("[ Panorama.cpp ] - Cannot open file %1").arg(file), ERROR); + QString("[ Panorama.cpp ] - Cannot open file %1").arg(file), PHOTON_ERROR); return cv::Mat(); } if (processor.unpack() != LIBRAW_SUCCESS) { LogManager::instance()->log( QString("[ Panorama.cpp ] - Cannot unpack data of file %1").arg(file), - ERROR); + PHOTON_ERROR); return cv::Mat(); } if (!colorInfo.get()) { colorInfo = std::make_unique (ColorInfo (extractColorInfo(&processor))); LogManager::instance()->log( QString("[ Panorama.cpp ] - Initialized ColorInfo on %1").arg(file), - DEBUG); + PHOTON_DEBUG); } if (processor.dcraw_process() != LIBRAW_SUCCESS) { LogManager::instance()->log( - QString("[ Panorama.cpp ] - Cannot dcraw file %1").arg(file), ERROR); + QString("[ Panorama.cpp ] - Cannot dcraw file %1").arg(file), PHOTON_ERROR); return cv::Mat(); } @@ -83,7 +83,7 @@ cv::Mat Panorama::raw_to_linear(const QString& file, std::unique_ptr& LogManager::instance()->log( QString("[ Panorama.cpp ] - Cannot create processed image from %1") .arg(file), - ERROR); + PHOTON_ERROR); return cv::Mat(); } @@ -102,7 +102,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, if (inputFiles.size() < 2) { LogManager::instance()->log( - "[ Panorama.cpp ] - Cannot stitch a single image", ERROR); + "[ Panorama.cpp ] - Cannot stitch a single image", PHOTON_ERROR); result["message"] = QString("Select more photos!"); result["success"] = false; return result; @@ -110,7 +110,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, LogManager::instance()->log( QString("[ Panorama.cpp ] - Loading %1 images").arg(inputFiles.size()), - INFO); + PHOTON_INFO); std::unique_ptr colorInfo; @@ -122,7 +122,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, LogManager::instance()->log( QString("[ Panorama.cpp ] - Failed to process image %1") .arg(filename), - ERROR); + PHOTON_ERROR); continue; } images16.push_back(img.clone()); @@ -130,7 +130,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, if (images16.size() < 2) { LogManager::instance()->log( - "[ Panorama.cpp ] - Not enough valid images to stitch", ERROR); + "[ Panorama.cpp ] - Not enough valid images to stitch", PHOTON_ERROR); result["message"] = QString("Not enough valid images to stitch"); result["success"] = false; return result; @@ -138,7 +138,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, LogManager::instance()->log( QString("[ Panorama.cpp ] - Processing %1 images").arg(images16.size()), - DEBUG); + PHOTON_DEBUG); // Create 8-bit gamma-corrected images for feature detection // Feature detectors require 8-bit input @@ -155,7 +155,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, // PHASE 1: Feature Detection and Matching LogManager::instance()->log("[ Panorama.cpp ] - Phase 1: Feature detection", - DEBUG); + PHOTON_DEBUG); cv::Ptr finder = cv::SIFT::create(); std::vector features(images8bit.size()); @@ -167,7 +167,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, QString("[ Panorama.cpp ] - Image %1: %2 features detected") .arg(i) .arg(static_cast(features[i].keypoints.size())), - DEBUG); + PHOTON_DEBUG); } @@ -187,7 +187,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, if (num_matches < static_cast(images8bit.size()) - 1) { LogManager::instance()->log( - "[ Panorama.cpp ] - Not enough matching features found", ERROR); + "[ Panorama.cpp ] - Not enough matching features found", PHOTON_ERROR); result["message"] = QString("Stitching failed: not enough matching features found."); result["success"] = false; @@ -196,7 +196,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, // PHASE 2: Camera Parameter Estimation LogManager::instance()->log("[ Panorama.cpp ] - Phase 2: Camera estimation", - DEBUG); + PHOTON_DEBUG); cv::Ptr estimator = cv::makePtr(); @@ -204,7 +204,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, if (!(*estimator)(features, pairwise_matches, cameras)) { LogManager::instance()->log( - "[ Panorama.cpp ] - Homography estimation failed", ERROR); + "[ Panorama.cpp ] - Homography estimation failed", PHOTON_ERROR); result["message"] = QString("Stitching failed: failed to align the images."); result["success"] = false; @@ -223,7 +223,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, adjuster->setConfThresh(1.0); if (!(*adjuster)(features, pairwise_matches, cameras)) { LogManager::instance()->log("[ Panorama.cpp ] - Bundle adjustment failed", - ERROR); + PHOTON_ERROR); result["message"] = QString("Stitching failed: failed to optimize camera parameters."); result["success"] = false; @@ -232,7 +232,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, // PHASE 3: Warping Images (16-bit) LogManager::instance()->log("[ Panorama.cpp ] - Phase 3: Warping images", - DEBUG); + PHOTON_DEBUG); // Find median focal length std::vector focals; @@ -249,7 +249,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, cv::Ptr warper = warper_creator->create(static_cast(warped_image_scale)); - LogManager::instance()->log("[ Panorama.cpp ] - Created warper", DEBUG); + LogManager::instance()->log("[ Panorama.cpp ] - Created warper", PHOTON_DEBUG); // Warp images and create masks std::vector images_warped16; std::vector masks_warped; @@ -284,7 +284,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, // PHASE 4: Exposure Compensation (optional) if (compensateExposure) { LogManager::instance()->log( - "[ Panorama.cpp ] - Phase 4: Exposure compensation", DEBUG); + "[ Panorama.cpp ] - Phase 4: Exposure compensation", PHOTON_DEBUG); cv::Ptr compensator = cv::makePtr(); @@ -295,13 +295,13 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, } } else { LogManager::instance()->log( - "[ Panorama.cpp ] - Phase 4: Skipping exposure compensation", DEBUG); + "[ Panorama.cpp ] - Phase 4: Skipping exposure compensation", PHOTON_DEBUG); } // PHASE 5: Seam Finding (Graph-Cut) // GraphCutSeamFinder expects 8-bit UMat images and binary masks LogManager::instance()->log("[ Panorama.cpp ] - Phase 5: Seam finding", - DEBUG); + PHOTON_DEBUG); // Ensure masks are binary (0 or 255) std::vector masks_binary; @@ -331,7 +331,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, */ // PHASE 6: Multi-band Blending (16-bit) LogManager::instance()->log("[ Panorama.cpp ] - Phase 6: Multi-band blending", - DEBUG); + PHOTON_DEBUG); // Calculate final panorama size cv::Rect dst_roi = cv::detail::resultRoi(corners, sizes_warped); @@ -346,7 +346,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, LogManager::instance()->log( QString("[ Panorama.cpp ] - Using %1 bands for blending").arg(num_bands), - DEBUG); + PHOTON_DEBUG); // Create blender - use default CV_32F weight type cv::Ptr blender = @@ -380,7 +380,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, if (!resultRGB.isContinuous()) resultRGB = resultRGB.clone(); if (resultRGB.empty()) { - LogManager::instance()->log("[ Panorama.cpp ] - Blending failed", ERROR); + LogManager::instance()->log("[ Panorama.cpp ] - Blending failed", PHOTON_ERROR); result["message"] = QString("Stitching failed during blending!"); result["success"] = false; return result; @@ -390,12 +390,12 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, QString("[ Panorama.cpp ] - Blended result: %1x%2") .arg(result16.cols) .arg(result16.rows), - DEBUG); + PHOTON_DEBUG); // PHASE 7: Save to DNG LogManager::instance()->log("[ Panorama.cpp ] - Phase 7: Saving to DNG", - DEBUG); + PHOTON_DEBUG); QString filename = QDir::toNativeSeparators(QString("%1/%2.pano.dng") @@ -403,7 +403,7 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, QFileInfo(inputFiles[0]).baseName())); LogManager::instance()->log( - QString("[ Panorama.cpp ] - Saving panorama to %1").arg(filename), INFO); + QString("[ Panorama.cpp ] - Saving panorama to %1").arg(filename), PHOTON_INFO); // ========================================== @@ -494,7 +494,7 @@ TIFFClose(out); LogManager::instance()->log( - "[ Panorama.cpp ] - Panorama stitching completed successfully", INFO); + "[ Panorama.cpp ] - Panorama stitching completed successfully", PHOTON_INFO); result["success"] = true; result["message"] = "Success! DNG saved to " + filename; diff --git a/src/engine/VulkanComputeContext.cpp b/src/engine/VulkanComputeContext.cpp index 6e92612..8b5802b 100644 --- a/src/engine/VulkanComputeContext.cpp +++ b/src/engine/VulkanComputeContext.cpp @@ -95,7 +95,7 @@ bool VulkanComputeContext::init(QRhi* rhi) { return false; } - LogManager::instance()->log("[ VulkanComputeContext ] - Initialized plain Vulkan compute context", INFO); + LogManager::instance()->log("[ VulkanComputeContext ] - Initialized plain Vulkan compute context", PHOTON_INFO); return true; } From 0af23609fb477a783fa0269a6e88e54e62c2852c Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Wed, 1 Apr 2026 23:33:29 +0200 Subject: [PATCH 22/29] fix: new tone editing pipeline --- TASKS.md | 6 +- src/components/RawViewport.frag | 293 +++++++++++--- src/engine/ImageDeveloper.cpp | 557 +++++++++++++++++++++------ src/engine/RawEngine.cpp | 659 +++++++++++++++++++++++--------- 4 files changed, 1179 insertions(+), 336 deletions(-) diff --git a/TASKS.md b/TASKS.md index 290d947..88b9509 100644 --- a/TASKS.md +++ b/TASKS.md @@ -507,7 +507,7 @@ - [x] Pop up error when continue session folder is not found, then reset it and return to WelcomeView - [x] Auto log cleanup -## Phase 38: Panorama +## Phase 38: Panorama and other improvements - [x] Panorama Stitching - [x] OpenCV integration @@ -515,6 +515,10 @@ - [ ] Dng export - [x] Implement a DNG-like export - [ ] Move the implementation to ExportManager +- [ ] New tone processing pipeline + - [x] Implement new pipeline + - [x] Port it to ImageDeveloper + - [ ] Tune tone targeting ## Backlog / Future diff --git a/src/components/RawViewport.frag b/src/components/RawViewport.frag index 5ede166..92765fc 100644 --- a/src/components/RawViewport.frag +++ b/src/components/RawViewport.frag @@ -248,6 +248,74 @@ vec3 apply_white_balance(vec3 color, float temp, float tnt) { return color * temp_mult * tint_mult; } +vec3 sample_source_linear(vec2 uv) { + return srgb_to_linear(texture(source, uv).rgb); +} + +vec3 compute_fine_blur(vec2 uv, vec2 texelSize) { + const float r1 = 1.5; + const float r2 = 3.0; + + vec3 b = sample_source_linear(uv) * 0.18; + + b += sample_source_linear(uv + vec2( r1, 0.0) * texelSize) * 0.095; + b += sample_source_linear(uv + vec2(-r1, 0.0) * texelSize) * 0.095; + b += sample_source_linear(uv + vec2(0.0, r1) * texelSize) * 0.095; + b += sample_source_linear(uv + vec2(0.0, -r1) * texelSize) * 0.095; + b += sample_source_linear(uv + vec2( r1, r1) * texelSize) * 0.055; + b += sample_source_linear(uv + vec2(-r1, r1) * texelSize) * 0.055; + b += sample_source_linear(uv + vec2( r1, -r1) * texelSize) * 0.055; + b += sample_source_linear(uv + vec2(-r1, -r1) * texelSize) * 0.055; + + b += sample_source_linear(uv + vec2( r2, 0.0) * texelSize) * 0.04; + b += sample_source_linear(uv + vec2(-r2, 0.0) * texelSize) * 0.04; + b += sample_source_linear(uv + vec2(0.0, r2) * texelSize) * 0.04; + b += sample_source_linear(uv + vec2(0.0, -r2) * texelSize) * 0.04; + b += sample_source_linear(uv + vec2( r2, r2) * texelSize) * 0.015; + b += sample_source_linear(uv + vec2(-r2, r2) * texelSize) * 0.015; + b += sample_source_linear(uv + vec2( r2, -r2) * texelSize) * 0.015; + b += sample_source_linear(uv + vec2(-r2, -r2) * texelSize) * 0.015; + + return b; +} + +vec3 compute_coarse_blur(vec2 uv, vec2 texelSize) { + const float r1 = 4.5; + const float r2 = 7.0; + const float r3 = 9.5; + + vec3 b = sample_source_linear(uv) * 0.20; + + b += sample_source_linear(uv + vec2( r1, 0.0) * texelSize) * 0.055; + b += sample_source_linear(uv + vec2(-r1, 0.0) * texelSize) * 0.055; + b += sample_source_linear(uv + vec2(0.0, r1) * texelSize) * 0.055; + b += sample_source_linear(uv + vec2(0.0, -r1) * texelSize) * 0.055; + b += sample_source_linear(uv + vec2( r1, r1) * texelSize) * 0.038; + b += sample_source_linear(uv + vec2(-r1, r1) * texelSize) * 0.038; + b += sample_source_linear(uv + vec2( r1, -r1) * texelSize) * 0.038; + b += sample_source_linear(uv + vec2(-r1, -r1) * texelSize) * 0.038; + + b += sample_source_linear(uv + vec2( r2, 0.0) * texelSize) * 0.04; + b += sample_source_linear(uv + vec2(-r2, 0.0) * texelSize) * 0.04; + b += sample_source_linear(uv + vec2(0.0, r2) * texelSize) * 0.04; + b += sample_source_linear(uv + vec2(0.0, -r2) * texelSize) * 0.04; + b += sample_source_linear(uv + vec2( r2, r2) * texelSize) * 0.03; + b += sample_source_linear(uv + vec2(-r2, r2) * texelSize) * 0.03; + b += sample_source_linear(uv + vec2( r2, -r2) * texelSize) * 0.03; + b += sample_source_linear(uv + vec2(-r2, -r2) * texelSize) * 0.03; + + b += sample_source_linear(uv + vec2( r3, 0.0) * texelSize) * 0.022; + b += sample_source_linear(uv + vec2(-r3, 0.0) * texelSize) * 0.022; + b += sample_source_linear(uv + vec2(0.0, r3) * texelSize) * 0.022; + b += sample_source_linear(uv + vec2(0.0, -r3) * texelSize) * 0.022; + b += sample_source_linear(uv + vec2( r3, r3) * texelSize) * 0.015; + b += sample_source_linear(uv + vec2(-r3, r3) * texelSize) * 0.015; + b += sample_source_linear(uv + vec2( r3, -r3) * texelSize) * 0.015; + b += sample_source_linear(uv + vec2(-r3, -r3) * texelSize) * 0.015; + + return b; +} + // --- Local Contrast, Clarity, Dehaze & Centre (Ported from RapidRAW) --- vec3 apply_local_contrast(vec3 color_linear, vec3 blurred_linear, float amount, int mode) { @@ -401,6 +469,164 @@ vec3 apply_luma_target(vec3 color, float lumaIn, float targetLuma) { return color * safeRatio; } +const float PV_FLARE_LINEAR = 0.000244140625; // 2^-12 +const float PV_FLARE_LOG = -12.0; +const float PV_EPS = 0.00000190734; + +const mat3 RGB_TO_PROPHOTO = mat3( + 0.529285, 0.098394, 0.016823, + 0.330046, 0.873493, 0.117671, + 0.140669, 0.028113, 0.865506 +); + +const vec3 PROPHOTO_LUMA_WEIGHTS = vec3(0.25, 0.5, 0.25); + +vec3 eval_undo_render_curve(vec3 col) { + vec2 fMinMax; + vec2 nMinMax; + const float eps = 0.00001; + + fMinMax.x = min(min(col.r, col.g), col.b); + fMinMax.y = max(max(col.r, col.g), col.b); + + vec2 t = pow(fMinMax, vec2(3.14453125)); + nMinMax = pow(fMinMax, vec2(0.8125)) * 0.3828125 * (1.0 - t) + + (1.0 - pow(1.0 - fMinMax, vec2(0.69140625))) * t; + + fMinMax.y = (nMinMax.y - nMinMax.x) / (fMinMax.y - fMinMax.x + eps); + return (col - fMinMax.x) * fMinMax.y + nMinMax.x; +} + +float pv_working_luma_linear(vec3 c) { + vec3 prophoto = RGB_TO_PROPHOTO * clamp(c, 0.0001, 0.999); + vec3 unmapped = clamp(eval_undo_render_curve(prophoto), 0.0, 1.0); + return max(dot(unmapped, PROPHOTO_LUMA_WEIGHTS), PV_EPS); +} + +float pv_encode_log_luma(float linearLuma) { + return log2(max(linearLuma + PV_FLARE_LINEAR, PV_EPS)); +} + +float pv_decode_log_luma(float logLuma) { + return max(exp2(logLuma) - PV_FLARE_LINEAR, PV_EPS); +} + +vec2 endpoint_pin_mask(vec2 x) { + x = clamp(x, 0.0, 1.0); + vec2 invX = 1.0 - x; + vec2 inv2 = invX * invX; + vec2 inv4 = inv2 * inv2; + vec2 inv8 = inv4 * inv4; + vec2 inv16 = inv8 * inv8; + vec2 base = 1.0 - inv8; + vec2 strong = 1.0 - inv16; + return mix(base, strong, smoothstep(vec2(0.35), vec2(1.0), x)); +} + +float pv_log_luma(vec3 c) { + return pv_encode_log_luma(pv_working_luma_linear(c)); +} + +float pv_tent_weight(float value, float center, float halfWidth) { + return max(1.0 - abs(value - center) / max(halfWidth, PV_EPS), 0.0); +} + +vec3 apply_pv2012_tone_ranges( + vec3 color, + vec3 blurredFine, + vec3 blurredCoarse, + float highlightsAmt, + float shadowsAmt, + float whitesAmt, + float blacksAmt, + float clarityAmt, + float sceneWhiteNorm +) { + float srcGrayLinear = pv_working_luma_linear(color); + float srcGrayLog = pv_encode_log_luma(srcGrayLinear); + float blurFineLog = pv_log_luma(blurredFine); + float blurCoarseLog = pv_log_luma(blurredCoarse); + float toneMid = pv_encode_log_luma(max(sceneWhiteNorm * 0.18, PV_EPS)); + + // Approximation of tonal windows in log-space (black -> shadow -> mid -> highlight -> white). + // Moving the center (toneMid - center) changes the tonal range of action, + // while moving the width (second param), changes the overlap with other tones. + float wBlacks = pv_tent_weight(srcGrayLog, toneMid - 3.8, 1.8); + float wShadows = pv_tent_weight(srcGrayLog, toneMid - 1.9, 1.9); + float wHighlights = pv_tent_weight(srcGrayLog, toneMid + 1.0, 1.9); + float wWhites = pv_tent_weight(srcGrayLog, toneMid + 3.1, 2.2); + + // Stronger single-pass 2-scale local mask proxy (fine + coarse residuals). + float maskFine = clamp(srcGrayLog - blurFineLog, -2.0, 2.0); + float maskCoarse = clamp(blurFineLog - blurCoarseLog, -2.0, 2.0); + float mask = clamp(maskFine * 0.70 + maskCoarse * 0.45, -2.5, 2.5); + + float partSwitch = step(srcGrayLog, toneMid); + float compressedLow = toneMid + (srcGrayLog - toneMid) * 0.78; + float compressedHigh = toneMid + (srcGrayLog - toneMid) * 0.58; + float baseCompressed = mix(compressedHigh, compressedLow, partSwitch); + + float localContrastSignal = srcGrayLog + mask - baseCompressed; + localContrastSignal *= max(clarityAmt, 0.0); + localContrastSignal *= clamp(1.0 + 0.35 * (-highlightsAmt + shadowsAmt), 1.0, 2.0); + vec2 localContrastSignal2 = vec2(max(localContrastSignal, 0.0), min(localContrastSignal, 0.0)); + + vec2 lumWeight = vec2( + clamp(wHighlights + 0.6 * wWhites, 0.0, 1.0), + clamp(wShadows + 0.6 * wBlacks, 0.0, 1.0) + ); + vec2 endpointStrength = clamp( + vec2(abs(highlightsAmt) + 0.35 * abs(whitesAmt), abs(shadowsAmt) + 0.35 * abs(blacksAmt)), + 0.0, + 1.0 + ); + vec2 claritySHPinMask = mix(endpoint_pin_mask(lumWeight), vec2(1.0), endpointStrength * endpointStrength); + + vec2 hsPinMask; + hsPinMask.y = mix(0.5 + 0.5 * max(1.0 - sign(shadowsAmt), 0.0), 1.0, claritySHPinMask.x); + hsPinMask.x = mix(1.0, 0.5, (1.0 - claritySHPinMask.y) * max(-sign(highlightsAmt), 0.0)); + hsPinMask.x = mix(1.0, hsPinMask.x, clamp(abs(highlightsAmt), 0.0, 1.0)); + + float maxAbsHS = max(max(abs(highlightsAmt), abs(shadowsAmt)), PV_EPS); + float baseOffset = 0.85 * (highlightsAmt + shadowsAmt) / maxAbsHS; + vec2 offsetHS = vec2(wHighlights, wShadows) * vec2(abs(highlightsAmt), abs(shadowsAmt)) * baseOffset; + vec2 deltaHS = vec2(-highlightsAmt, shadowsAmt); + deltaHS = clamp(deltaHS, -1.0, 1.0); + deltaHS *= vec2(min(mask, 0.0), max(mask, 0.0)); + deltaHS += offsetHS; + + float deltaStops = dot(deltaHS, hsPinMask); + deltaStops += whitesAmt * wWhites * hsPinMask.x; + deltaStops += blacksAmt * wBlacks * hsPinMask.y; + deltaStops += dot(localContrastSignal2, claritySHPinMask); + + float deltaSign = sign(deltaStops); + float flareSwitch = 1.0 - max(deltaSign, 0.0); + float zeroSwitch = 1.0 - abs(deltaSign); + float flare = flareSwitch * PV_FLARE_LOG; + float startpoint = flare - (deltaStops + deltaStops); + float t1 = step(startpoint, srcGrayLog); + float t2 = step(srcGrayLog, startpoint); + float t = clamp((srcGrayLog - startpoint) / (flare - startpoint + zeroSwitch), 0.0, 1.0); + t *= t * (1.0 - mix(t2, t1, flareSwitch)); + deltaStops = mix(deltaStops, 0.0, t); + + // Analogous to ToneMapLimitShadowGain path (max +4 stops lift). + deltaStops = min(deltaStops, 4.0); + + float targetLog = srcGrayLog + deltaStops; + float targetLuma = pv_decode_log_luma(targetLog); + + if (targetLuma > sceneWhiteNorm && deltaStops > 0.0) { + float over = targetLuma - sceneWhiteNorm; + float knee = max(sceneWhiteNorm * 0.7, PV_EPS); + float compress = over / (1.0 + over / knee); + targetLuma = sceneWhiteNorm + compress; + } + + return apply_luma_target(color, srcGrayLinear, targetLuma); +} + float sample_tone_lut_channel(float value, int channel) { int idx = int(clamp(floor(clamp(value, 0.0, 1.0) * 65535.0 + 0.5), 0.0, 65535.0)); int x = idx & 255; @@ -605,24 +831,11 @@ void main() color = apply_gpu_denoise(color, qt_TexCoord0, source, ubuf.denoiseAmount); // --- Approximated Blur for Local Contrast (Clarity, Structure, Sharpness) --- - // Dual-radius multi-tap blur for effective unsharp mask on denoised images. + // Stronger two-scale blur with more taps to better emulate a single-pass local pyramid mask. vec2 texelSize = 1.0 / ubuf.sourceSize; - // Inner ring (1.5 texels) — fine detail - vec3 blurred = color * 0.12; - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(1.5, 1.5) * texelSize).rgb) * 0.07; - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(-1.5, -1.5) * texelSize).rgb) * 0.07; - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(1.5, -1.5) * texelSize).rgb) * 0.07; - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(-1.5, 1.5) * texelSize).rgb) * 0.07; - // Outer axis ring (6.0 texels) — captures mid-frequency on denoised images - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(6.0, 0.0) * texelSize).rgb) * 0.08; - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(-6.0, 0.0) * texelSize).rgb) * 0.08; - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(0.0, 6.0) * texelSize).rgb) * 0.08; - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(0.0, -6.0) * texelSize).rgb) * 0.08; - // Outer diagonal ring (4.5 texels) - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(4.5, 4.5) * texelSize).rgb) * 0.07; - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(-4.5, -4.5) * texelSize).rgb) * 0.07; - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(4.5, -4.5) * texelSize).rgb) * 0.07; - blurred += srgb_to_linear(texture(source, qt_TexCoord0 + vec2(-4.5, 4.5) * texelSize).rgb) * 0.07; + vec3 blurredFine = compute_fine_blur(qt_TexCoord0, texelSize); + vec3 blurredCoarse = compute_coarse_blur(qt_TexCoord0, texelSize); + vec3 blurred = mix(blurredFine, blurredCoarse, 0.35); // Compute edge mask with feathering, then gate by focus detection float edgeMask = compute_edge_mask(source, qt_TexCoord0, texelSize, ubuf.sharpenMask, ubuf.maskFeather); @@ -640,9 +853,6 @@ void main() // Blend sharpened vs original using combined mask color = mix(preSharp, color, finalMask); - // Apply Clarity (Mode 1) - color = apply_local_contrast(color, blurred, ubuf.clarity / 100.0, 1); - // Apply Structure (Mode 1, but with different scaling/interpretation if needed) color = apply_local_contrast(color, blurred, ubuf.structure / 100.0, 1); @@ -669,40 +879,24 @@ void main() color = apply_luma_target(color, luma, targetL); } float sceneWhiteNorm = max(ubuf.sceneWhite * exposure, 1e-4); - float lumaNorm = clamp(luma/sceneWhiteNorm, 0.0, 2.0); + vec3 blurredFineTone = apply_white_balance(blurredFine, ubuf.temperature / 100.0, ubuf.tint / 100.0) * exposure; + vec3 blurredCoarseTone = apply_white_balance(blurredCoarse, ubuf.temperature / 100.0, ubuf.tint / 100.0) * exposure; + color = apply_pv2012_tone_ranges( + color, + blurredFineTone, + blurredCoarseTone, + ubuf.highlights / 100.0, + ubuf.shadows / 100.0, + ubuf.whites / 100.0, + ubuf.blacks / 100.0, + ubuf.clarity / 100.0, + sceneWhiteNorm + ); //color = davinci_tonemap(color, ubuf.adaptation); // 3. Contrast color = max(vec3(0.0), color); color = pow(color, vec3(ubuf.contrast)); - - // 4. Whites & Blacks (specialized targeting) - if (ubuf.whites != 0.0) { - float whiteMask = smoothstep(0.7, 1.25, lumaNorm); - float targetLuma = compute_target_luma(luma, (ubuf.whites / 100.0) * whiteMask); - color = apply_luma_target(color, luma, targetLuma); - luma = get_luma(max(color, 0.0)); - } - if (ubuf.blacks != 0.0) { - float blackMask = 1.0 - smoothstep(0.0, 0.15, lumaNorm); - float targetLuma = compute_toe_target(luma, (ubuf.blacks / 100.0) * blackMask); - color = apply_luma_target(color, luma, targetLuma); - luma = get_luma(max(color, 0.0)); - } - - // 5. Highlights & Shadows (specialized targeting) - if (ubuf.shadows != 0.0) { - float shadowMask = 1.0 - smoothstep(0.05, 0.65, lumaNorm); - float targetLuma = compute_toe_target(luma, (ubuf.shadows / 100.0) * shadowMask); - color = apply_luma_target(color, luma, targetLuma); - luma = get_luma(max(color, 0.0)); - } - - if (ubuf.highlights != 0.0) { - float highlightMask = smoothstep(0.35, 1.1, lumaNorm); - float targetLuma = compute_target_luma(luma, (ubuf.highlights / 100.0) * highlightMask); - color = apply_luma_target(color, luma, targetLuma); - } // --- HSL PANEL --- vec3 hsv = rgb_to_hsv(color); @@ -821,3 +1015,4 @@ void main() fragColor = vec4(clamp(final_rgb, 0.0, 1.0), tex.a) * ubuf.qt_Opacity; } + diff --git a/src/engine/ImageDeveloper.cpp b/src/engine/ImageDeveloper.cpp index ac275ac..01d2bae 100644 --- a/src/engine/ImageDeveloper.cpp +++ b/src/engine/ImageDeveloper.cpp @@ -6,9 +6,9 @@ #include #include #include -#include #include #include +#include #include #include "../managers/LogManager.h" @@ -66,8 +66,9 @@ static void davinci_tonemap(float& r, float& g, float& b, float adaptation) { const float input_white = 16.0f; const float output_white = 1.0f; - float bv = (input_white - (adaptation / 100.0f) * (input_white / output_white)) - / ((input_white / output_white) - 1.0f); + float bv = + (input_white - (adaptation / 100.0f) * (input_white / output_white)) / + ((input_white / output_white) - 1.0f); float a = output_white / (input_white / (input_white + bv)); r = std::min(r, input_white); @@ -176,8 +177,316 @@ static void apply_luma_target_cpp(float& r, float& g, float& b, float lumaIn, b *= safeRatio; } +struct Vec3fCpp { + float r; + float g; + float b; +}; + +static float step_local(float edge, float x) { return x < edge ? 0.0f : 1.0f; } + +static float sign_local(float x) { + if (x > 0.0f) return 1.0f; + if (x < 0.0f) return -1.0f; + return 0.0f; +} + +static Vec3fCpp clamp_vec3_cpp(const Vec3fCpp& c, float lo, float hi) { + return {std::clamp(c.r, lo, hi), std::clamp(c.g, lo, hi), + std::clamp(c.b, lo, hi)}; +} + +static Vec3fCpp sample_source_linear_bilinear_cpp(const ushort* src, int width, + int height, float u, + float v) { + u = std::clamp(u, 0.0f, 1.0f); + v = std::clamp(v, 0.0f, 1.0f); + + const float xf = u * float(width) - 0.5f; + const float yf = v * float(height) - 0.5f; + const int x0 = int(std::floor(xf)); + const int y0 = int(std::floor(yf)); + const int x1 = x0 + 1; + const int y1 = y0 + 1; + const float tx = xf - float(x0); + const float ty = yf - float(y0); + + auto sample_texel = [src, width, height](int x, int y) -> Vec3fCpp { + x = std::clamp(x, 0, width - 1); + y = std::clamp(y, 0, height - 1); + const int idx = (y * width + x) * 3; + return {srgb_to_linear_f(src[idx] / 65535.0f), + srgb_to_linear_f(src[idx + 1] / 65535.0f), + srgb_to_linear_f(src[idx + 2] / 65535.0f)}; + }; + + const Vec3fCpp c00 = sample_texel(x0, y0); + const Vec3fCpp c10 = sample_texel(x1, y0); + const Vec3fCpp c01 = sample_texel(x0, y1); + const Vec3fCpp c11 = sample_texel(x1, y1); + + Vec3fCpp out{}; + out.r = + mix_local(mix_local(c00.r, c10.r, tx), mix_local(c01.r, c11.r, tx), ty); + out.g = + mix_local(mix_local(c00.g, c10.g, tx), mix_local(c01.g, c11.g, tx), ty); + out.b = + mix_local(mix_local(c00.b, c10.b, tx), mix_local(c01.b, c11.b, tx), ty); + return out; +} + +static Vec3fCpp compute_fine_blur_cpp(const ushort* src, int width, int height, + float u, float v) { + Vec3fCpp blur{0.0f, 0.0f, 0.0f}; + auto tap = [&](float dx, float dy, float w) { + Vec3fCpp s = sample_source_linear_bilinear_cpp( + src, width, height, u + dx / float(width), v + dy / float(height)); + blur.r += s.r * w; + blur.g += s.g * w; + blur.b += s.b * w; + }; + + constexpr float r1 = 1.5f; + constexpr float r2 = 3.0f; + + tap(0.0f, 0.0f, 0.18f); + tap(r1, 0.0f, 0.095f); + tap(-r1, 0.0f, 0.095f); + tap(0.0f, r1, 0.095f); + tap(0.0f, -r1, 0.095f); + tap(r1, r1, 0.055f); + tap(-r1, r1, 0.055f); + tap(r1, -r1, 0.055f); + tap(-r1, -r1, 0.055f); + + tap(r2, 0.0f, 0.04f); + tap(-r2, 0.0f, 0.04f); + tap(0.0f, r2, 0.04f); + tap(0.0f, -r2, 0.04f); + tap(r2, r2, 0.015f); + tap(-r2, r2, 0.015f); + tap(r2, -r2, 0.015f); + tap(-r2, -r2, 0.015f); + + return blur; +} + +static Vec3fCpp compute_coarse_blur_cpp(const ushort* src, int width, + int height, float u, float v) { + Vec3fCpp blur{0.0f, 0.0f, 0.0f}; + auto tap = [&](float dx, float dy, float w) { + Vec3fCpp s = sample_source_linear_bilinear_cpp( + src, width, height, u + dx / float(width), v + dy / float(height)); + blur.r += s.r * w; + blur.g += s.g * w; + blur.b += s.b * w; + }; + + constexpr float r1 = 4.5f; + constexpr float r2 = 7.0f; + constexpr float r3 = 9.5f; + + tap(0.0f, 0.0f, 0.20f); + tap(r1, 0.0f, 0.055f); + tap(-r1, 0.0f, 0.055f); + tap(0.0f, r1, 0.055f); + tap(0.0f, -r1, 0.055f); + tap(r1, r1, 0.038f); + tap(-r1, r1, 0.038f); + tap(r1, -r1, 0.038f); + tap(-r1, -r1, 0.038f); + + tap(r2, 0.0f, 0.04f); + tap(-r2, 0.0f, 0.04f); + tap(0.0f, r2, 0.04f); + tap(0.0f, -r2, 0.04f); + tap(r2, r2, 0.03f); + tap(-r2, r2, 0.03f); + tap(r2, -r2, 0.03f); + tap(-r2, -r2, 0.03f); + + tap(r3, 0.0f, 0.022f); + tap(-r3, 0.0f, 0.022f); + tap(0.0f, r3, 0.022f); + tap(0.0f, -r3, 0.022f); + tap(r3, r3, 0.015f); + tap(-r3, r3, 0.015f); + tap(r3, -r3, 0.015f); + tap(-r3, -r3, 0.015f); + + return blur; +} + +constexpr float PV_FLARE_LINEAR_CPP = 0.000244140625f; // 2^-12 +constexpr float PV_FLARE_LOG_CPP = -12.0f; +constexpr float PV_EPS_CPP = 0.00000190734f; + +static Vec3fCpp eval_undo_render_curve_cpp(const Vec3fCpp& col) { + constexpr float eps = 0.00001f; + const float fMin = std::min({col.r, col.g, col.b}); + const float fMax = std::max({col.r, col.g, col.b}); + + const float tMin = std::pow(fMin, 3.14453125f); + const float tMax = std::pow(fMax, 3.14453125f); + const float nMin = std::pow(fMin, 0.8125f) * 0.3828125f * (1.0f - tMin) + + (1.0f - std::pow(1.0f - fMin, 0.69140625f)) * tMin; + const float nMax = std::pow(fMax, 0.8125f) * 0.3828125f * (1.0f - tMax) + + (1.0f - std::pow(1.0f - fMax, 0.69140625f)) * tMax; + + const float scale = (nMax - nMin) / (fMax - fMin + eps); + return {(col.r - fMin) * scale + nMin, (col.g - fMin) * scale + nMin, + (col.b - fMin) * scale + nMin}; +} + +static float pv_working_luma_linear_cpp(const Vec3fCpp& c) { + Vec3fCpp clamped = clamp_vec3_cpp(c, 0.0001f, 0.999f); + Vec3fCpp prophoto{ + 0.529285f * clamped.r + 0.330046f * clamped.g + 0.140669f * clamped.b, + 0.098394f * clamped.r + 0.873493f * clamped.g + 0.028113f * clamped.b, + 0.016823f * clamped.r + 0.117671f * clamped.g + 0.865506f * clamped.b}; + Vec3fCpp unmapped = + clamp_vec3_cpp(eval_undo_render_curve_cpp(prophoto), 0.0f, 1.0f); + return std::max(unmapped.r * 0.25f + unmapped.g * 0.5f + unmapped.b * 0.25f, + PV_EPS_CPP); +} + +static float pv_encode_log_luma_cpp(float linearLuma) { + return std::log2(std::max(linearLuma + PV_FLARE_LINEAR_CPP, PV_EPS_CPP)); +} + +static float pv_decode_log_luma_cpp(float logLuma) { + return std::max(std::exp2(logLuma) - PV_FLARE_LINEAR_CPP, PV_EPS_CPP); +} + +static float endpoint_pin_mask_component_cpp(float x) { + x = std::clamp(x, 0.0f, 1.0f); + const float inv = 1.0f - x; + const float inv2 = inv * inv; + const float inv4 = inv2 * inv2; + const float inv8 = inv4 * inv4; + const float inv16 = inv8 * inv8; + const float base = 1.0f - inv8; + const float strong = 1.0f - inv16; + return mix_local(base, strong, smoothstep_local(0.35f, 1.0f, x)); +} + +static float pv_log_luma_cpp(const Vec3fCpp& c) { + return pv_encode_log_luma_cpp(pv_working_luma_linear_cpp(c)); +} + +static float pv_tent_weight_cpp(float value, float center, float halfWidth) { + return std::max( + 1.0f - std::abs(value - center) / std::max(halfWidth, PV_EPS_CPP), 0.0f); +} + +static Vec3fCpp apply_pv2012_tone_ranges_cpp( + const Vec3fCpp& color, const Vec3fCpp& blurredFine, + const Vec3fCpp& blurredCoarse, float highlightsAmt, float shadowsAmt, + float whitesAmt, float blacksAmt, float clarityAmt, float sceneWhiteNorm) { + const float srcGrayLinear = pv_working_luma_linear_cpp(color); + const float srcGrayLog = pv_encode_log_luma_cpp(srcGrayLinear); + const float blurFineLog = pv_log_luma_cpp(blurredFine); + const float blurCoarseLog = pv_log_luma_cpp(blurredCoarse); + const float toneMid = + pv_encode_log_luma_cpp(std::max(sceneWhiteNorm * 0.18f, PV_EPS_CPP)); + + const float wBlacks = pv_tent_weight_cpp(srcGrayLog, toneMid - 3.8f, 1.8f); + const float wShadows = pv_tent_weight_cpp(srcGrayLog, toneMid - 1.9f, 1.9f); + const float wHighlights = + pv_tent_weight_cpp(srcGrayLog, toneMid + 1.0f, 1.9f); + const float wWhites = pv_tent_weight_cpp(srcGrayLog, toneMid + 3.1f, 2.2f); + + const float maskFine = std::clamp(srcGrayLog - blurFineLog, -2.0f, 2.0f); + const float maskCoarse = std::clamp(blurFineLog - blurCoarseLog, -2.0f, 2.0f); + const float mask = + std::clamp(maskFine * 0.70f + maskCoarse * 0.45f, -2.5f, 2.5f); + + const float partSwitch = step_local(srcGrayLog, toneMid); + const float compressedLow = toneMid + (srcGrayLog - toneMid) * 0.78f; + const float compressedHigh = toneMid + (srcGrayLog - toneMid) * 0.58f; + const float baseCompressed = + mix_local(compressedHigh, compressedLow, partSwitch); + + float localContrastSignal = srcGrayLog + mask - baseCompressed; + localContrastSignal *= std::max(clarityAmt, 0.0f); + localContrastSignal *= + std::clamp(1.0f + 0.35f * (-highlightsAmt + shadowsAmt), 1.0f, 2.0f); + const float localSignalHigh = std::max(localContrastSignal, 0.0f); + const float localSignalLow = std::min(localContrastSignal, 0.0f); + + const float lumWeightHigh = + std::clamp(wHighlights + 0.6f * wWhites, 0.0f, 1.0f); + const float lumWeightLow = std::clamp(wShadows + 0.6f * wBlacks, 0.0f, 1.0f); + const float endpointHigh = std::clamp( + std::abs(highlightsAmt) + 0.35f * std::abs(whitesAmt), 0.0f, 1.0f); + const float endpointLow = std::clamp( + std::abs(shadowsAmt) + 0.35f * std::abs(blacksAmt), 0.0f, 1.0f); + const float clarityPinHigh = + mix_local(endpoint_pin_mask_component_cpp(lumWeightHigh), 1.0f, + endpointHigh * endpointHigh); + const float clarityPinLow = + mix_local(endpoint_pin_mask_component_cpp(lumWeightLow), 1.0f, + endpointLow * endpointLow); + + float hsPinY = + mix_local(0.5f + 0.5f * std::max(1.0f - sign_local(shadowsAmt), 0.0f), + 1.0f, clarityPinHigh); + float hsPinX = mix_local( + 1.0f, 0.5f, + (1.0f - clarityPinLow) * std::max(-sign_local(highlightsAmt), 0.0f)); + hsPinX = + mix_local(1.0f, hsPinX, std::clamp(std::abs(highlightsAmt), 0.0f, 1.0f)); + + const float maxAbsHS = std::max( + std::max(std::abs(highlightsAmt), std::abs(shadowsAmt)), PV_EPS_CPP); + const float baseOffset = 0.85f * (highlightsAmt + shadowsAmt) / maxAbsHS; + const float offsetHSHigh = wHighlights * std::abs(highlightsAmt) * baseOffset; + const float offsetHSLow = wShadows * std::abs(shadowsAmt) * baseOffset; + + float deltaHSHigh = std::clamp(-highlightsAmt, -1.0f, 1.0f); + float deltaHSLow = std::clamp(shadowsAmt, -1.0f, 1.0f); + deltaHSHigh *= std::min(mask, 0.0f); + deltaHSLow *= std::max(mask, 0.0f); + deltaHSHigh += offsetHSHigh; + deltaHSLow += offsetHSLow; + + float deltaStops = deltaHSHigh * hsPinX + deltaHSLow * hsPinY; + deltaStops += whitesAmt * wWhites * hsPinX; + deltaStops += blacksAmt * wBlacks * hsPinY; + deltaStops += + localSignalHigh * clarityPinHigh + localSignalLow * clarityPinLow; + + const float deltaSign = sign_local(deltaStops); + const float flareSwitch = 1.0f - std::max(deltaSign, 0.0f); + const float zeroSwitch = 1.0f - std::abs(deltaSign); + const float flare = flareSwitch * PV_FLARE_LOG_CPP; + const float startpoint = flare - (deltaStops + deltaStops); + const float t1 = step_local(startpoint, srcGrayLog); + const float t2 = step_local(srcGrayLog, startpoint); + float t = + std::clamp((srcGrayLog - startpoint) / (flare - startpoint + zeroSwitch), + 0.0f, 1.0f); + t *= t * (1.0f - mix_local(t2, t1, flareSwitch)); + deltaStops = mix_local(deltaStops, 0.0f, t); + + deltaStops = std::min(deltaStops, 4.0f); + const float targetLog = srcGrayLog + deltaStops; + float targetLuma = pv_decode_log_luma_cpp(targetLog); + + if (targetLuma > sceneWhiteNorm && deltaStops > 0.0f) { + const float over = targetLuma - sceneWhiteNorm; + const float knee = std::max(sceneWhiteNorm * 0.7f, PV_EPS_CPP); + const float compress = over / (1.0f + over / knee); + targetLuma = sceneWhiteNorm + compress; + } + + Vec3fCpp out = color; + apply_luma_target_cpp(out.r, out.g, out.b, srcGrayLinear, targetLuma); + return out; +} + static std::vector evalMonotonicSplineLut(const QVariantList& pts, - int lutSize) { + int lutSize) { std::vector lut(lutSize); int n = pts.size(); if (n < 2) { @@ -223,8 +532,7 @@ static std::vector evalMonotonicSplineLut(const QVariantList& pts, std::vector m(n, 0.0); m[0] = delta[0]; m[n - 1] = delta[n - 2]; - for (int i = 1; i < n - 1; i++) - m[i] = (delta[i - 1] + delta[i]) * 0.5; + for (int i = 1; i < n - 1; i++) m[i] = (delta[i - 1] + delta[i]) * 0.5; for (int i = 0; i < n - 1; i++) { if (std::abs(delta[i]) < 1e-12) { m[i] = 0.0; @@ -243,16 +551,21 @@ static std::vector evalMonotonicSplineLut(const QVariantList& pts, int seg = 0; for (int i = 0; i < lutSize; i++) { double t_val = double(i) / (lutSize - 1); - if (t_val <= xs[0]) { lut[i] = float(ys[0]); continue; } - if (t_val >= xs[n - 1]) { lut[i] = float(ys[n - 1]); continue; } + if (t_val <= xs[0]) { + lut[i] = float(ys[0]); + continue; + } + if (t_val >= xs[n - 1]) { + lut[i] = float(ys[n - 1]); + continue; + } while (seg < n - 2 && t_val > xs[seg + 1]) seg++; double dx = xs[seg + 1] - xs[seg]; double t = (t_val - xs[seg]) / dx; double t2 = t * t, t3 = t2 * t; double val = (2 * t3 - 3 * t2 + 1) * ys[seg] + (t3 - 2 * t2 + t) * dx * m[seg] + - (-2 * t3 + 3 * t2) * ys[seg + 1] + - (t3 - t2) * dx * m[seg + 1]; + (-2 * t3 + 3 * t2) * ys[seg + 1] + (t3 - t2) * dx * m[seg + 1]; lut[i] = float(std::clamp(val, 0.0, 1.0)); } return lut; @@ -269,7 +582,8 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, PHOTON_DEBUG); if (!src || width <= 0 || height <= 0) { - LogManager::instance()->log("[ ImageDeveloper ] - develop ABORT: invalid params", PHOTON_ERROR); + LogManager::instance()->log( + "[ ImageDeveloper ] - develop ABORT: invalid params", PHOTON_ERROR); return QImage(); } @@ -279,8 +593,9 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, float high = obj["highlights"].toDouble(); float shad = obj["shadows"].toDouble(); float whites = obj["whites"].toDouble(); + float sceneWhite = obj["sceneWhite"].toDouble(1.0); float blacks = obj["blacks"].toDouble(); - float adaptation = obj["adaptation"].toDouble(); + float clarity = obj["clarity"].toDouble(); float temp = obj["temperature"].toDouble() / 100.0f; float tint = obj["tint"].toDouble() / 100.0f; float sat_global = obj["saturation"].toDouble(); @@ -290,24 +605,28 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, bool denoiseEnabled = obj["denoiseEnabled"].toBool(); bool denoiseSecondPass = obj["denoiseSecondPass"].toBool(); int denoiseSearchWindow = obj.contains("denoiseSearchWindow") - ? obj["denoiseSearchWindow"].toInt() : 19; - int denoiseGroupSize = obj.contains("denoiseGroupSize") - ? obj["denoiseGroupSize"].toInt() : 16; + ? obj["denoiseSearchWindow"].toInt() + : 19; + int denoiseGroupSize = + obj.contains("denoiseGroupSize") ? obj["denoiseGroupSize"].toInt() : 16; int denoiseChromaRadius = obj.contains("denoiseChromaRadius") - ? obj["denoiseChromaRadius"].toInt() : 4; + ? obj["denoiseChromaRadius"].toInt() + : 4; float denoiseChromaAmount = obj.contains("denoiseChromaAmount") - ? obj["denoiseChromaAmount"].toDouble() : 50.0f; + ? obj["denoiseChromaAmount"].toDouble() + : 50.0f; float denoiseChromaBm3d = obj.contains("denoiseChromaBm3d") - ? obj["denoiseChromaBm3d"].toDouble() : 50.0f; - - LogManager::instance()->log( - QString("[ ImageDeveloper ] - Params: exp=%1 con=%2 high=%3 shad=%4 denoise=%5") - .arg(exp, 0, 'f', 2) - .arg(con, 0, 'f', 2) - .arg(high, 0, 'f', 2) - .arg(shad, 0, 'f', 2) - .arg(denoiseAmount, 0, 'f', 1), - PHOTON_DEBUG); + ? obj["denoiseChromaBm3d"].toDouble() + : 50.0f; + + LogManager::instance()->log(QString("[ ImageDeveloper ] - Params: exp=%1 " + "con=%2 high=%3 shad=%4 denoise=%5") + .arg(exp, 0, 'f', 2) + .arg(con, 0, 'f', 2) + .arg(high, 0, 'f', 2) + .arg(shad, 0, 'f', 2) + .arg(denoiseAmount, 0, 'f', 1), + PHOTON_DEBUG); // HSL Params float hsl_h[8], hsl_s[8], hsl_l[8]; @@ -354,40 +673,50 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, QVariantList defaultPts; QVariantMap p0, p1; - p0["x"] = 0.0; p0["y"] = 0.0; - p1["x"] = 1.0; p1["y"] = 1.0; + p0["x"] = 0.0; + p0["y"] = 0.0; + p1["x"] = 1.0; + p1["y"] = 1.0; defaultPts << p0 << p1; - QVariantList tcLuma = obj.contains("toneCurveLuma") - ? jsonArrayToVariantList(obj["toneCurveLuma"].toArray()) : defaultPts; - QVariantList tcRed = obj.contains("toneCurveRed") - ? jsonArrayToVariantList(obj["toneCurveRed"].toArray()) : defaultPts; - QVariantList tcGreen = obj.contains("toneCurveGreen") - ? jsonArrayToVariantList(obj["toneCurveGreen"].toArray()) : defaultPts; - QVariantList tcBlue = obj.contains("toneCurveBlue") - ? jsonArrayToVariantList(obj["toneCurveBlue"].toArray()) : defaultPts; + QVariantList tcLuma = + obj.contains("toneCurveLuma") + ? jsonArrayToVariantList(obj["toneCurveLuma"].toArray()) + : defaultPts; + QVariantList tcRed = + obj.contains("toneCurveRed") + ? jsonArrayToVariantList(obj["toneCurveRed"].toArray()) + : defaultPts; + QVariantList tcGreen = + obj.contains("toneCurveGreen") + ? jsonArrayToVariantList(obj["toneCurveGreen"].toArray()) + : defaultPts; + QVariantList tcBlue = + obj.contains("toneCurveBlue") + ? jsonArrayToVariantList(obj["toneCurveBlue"].toArray()) + : defaultPts; constexpr int kToneLutEntries = 65536; constexpr float kToneLutMaxIndex = float(kToneLutEntries - 1); std::vector lutLuma = evalMonotonicSplineLut(tcLuma, kToneLutEntries); std::vector lutRed = evalMonotonicSplineLut(tcRed, kToneLutEntries); - std::vector lutGreen = evalMonotonicSplineLut(tcGreen, kToneLutEntries); + std::vector lutGreen = + evalMonotonicSplineLut(tcGreen, kToneLutEntries); std::vector lutBlue = evalMonotonicSplineLut(tcBlue, kToneLutEntries); // Check if tone curve is identity (skip application if so), using 16-bit // quantization to match shader LUT precision. bool toneCurveActive = false; for (int i = 0; i < kToneLutEntries && !toneCurveActive; i++) { - uint16_t qL = uint16_t(std::clamp(lutLuma[i] * kToneLutMaxIndex + 0.5f, 0.0f, - kToneLutMaxIndex)); + uint16_t qL = uint16_t(std::clamp(lutLuma[i] * kToneLutMaxIndex + 0.5f, + 0.0f, kToneLutMaxIndex)); uint16_t qR = uint16_t(std::clamp(lutRed[i] * kToneLutMaxIndex + 0.5f, 0.0f, kToneLutMaxIndex)); - uint16_t qG = uint16_t(std::clamp(lutGreen[i] * kToneLutMaxIndex + 0.5f, 0.0f, - kToneLutMaxIndex)); - uint16_t qB = uint16_t(std::clamp(lutBlue[i] * kToneLutMaxIndex + 0.5f, 0.0f, - kToneLutMaxIndex)); - if (qL != i || qR != i || qG != i || qB != i) - toneCurveActive = true; + uint16_t qG = uint16_t(std::clamp(lutGreen[i] * kToneLutMaxIndex + 0.5f, + 0.0f, kToneLutMaxIndex)); + uint16_t qB = uint16_t(std::clamp(lutBlue[i] * kToneLutMaxIndex + 0.5f, + 0.0f, kToneLutMaxIndex)); + if (qL != i || qR != i || qG != i || qB != i) toneCurveActive = true; } // HSL constants @@ -417,58 +746,47 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, g = srgb_to_linear_f(g); b = srgb_to_linear_f(b); + const float u = (float(x) + 0.5f) / float(width); + const float v = (float(y) + 0.5f) / float(height); + Vec3fCpp blurredFine = compute_fine_blur_cpp(src, width, height, u, v); + Vec3fCpp blurredCoarse = + compute_coarse_blur_cpp(src, width, height, u, v); + // 1. WB & Exposure r *= r_wb * exp_mult; g *= g_wb * exp_mult; b *= b_wb * exp_mult; + Vec3fCpp color{r, g, b}; - // DaVinci tonemapping to smoothen the highlights - davinci_tonemap(r, g, b, adaptation); - - // 2. Contrast - r = std::max(0.0f, r); - g = std::max(0.0f, g); - b = std::max(0.0f, b); - r = std::pow(r, con); - g = std::pow(g, con); - b = std::pow(b, con); - - // 3. Whites & Blacks (specialized targeting) float luma = - get_luma_cpp(std::max(0.0f, r), std::max(0.0f, g), std::max(0.0f, b)); - if (whites != 0.0f) { - float whiteMask = smoothstep(0.7f, 1.25f, luma); - float targetLuma = compute_target_luma_cpp(luma, (whites / 100.0f) * whiteMask); - apply_luma_target_cpp(r, g, b, luma, targetLuma); - luma = get_luma_cpp(std::max(0.0f, r), std::max(0.0f, g), - std::max(0.0f, b)); - } - if (blacks != 0.0f) { - float blackMask = 1.0f - smoothstep(0.0f, 0.15f, luma); - float targetLuma = - compute_toe_target_cpp(luma, (blacks / 100.0f) * blackMask); - apply_luma_target_cpp(r, g, b, luma, targetLuma); - luma = get_luma_cpp(std::max(0.0f, r), std::max(0.0f, g), - std::max(0.0f, b)); + get_luma_cpp(std::max(0.0f, color.r), std::max(0.0f, color.g), + std::max(0.0f, color.b)); + if (luma > sceneWhite && exp > 0.0f) { + float over = luma - sceneWhite; + float knee = sceneWhite * 0.7f; + float compress = over / (1.0f + over / knee); + float targetL = sceneWhite + compress; + apply_luma_target_cpp(color.r, color.g, color.b, luma, targetL); } - // 4. Highlights & Shadows (specialized targeting) - if (shad != 0.0f) { - float shadowMask = 1.0f - smoothstep(0.05f, 0.65f, luma); - float targetLuma = - compute_toe_target_cpp(luma, (shad / 100.0f) * shadowMask); - apply_luma_target_cpp(r, g, b, luma, targetLuma); - luma = get_luma_cpp(std::max(0.0f, r), std::max(0.0f, g), - std::max(0.0f, b)); - } - if (high != 0.0f) { - float highlightMask = smoothstep(0.35f, 1.1f, luma); - float targetLuma = - compute_target_luma_cpp(luma, (high / 100.0f) * highlightMask); - apply_luma_target_cpp(r, g, b, luma, targetLuma); - } + Vec3fCpp blurredFineTone{blurredFine.r * r_wb * exp_mult, + blurredFine.g * g_wb * exp_mult, + blurredFine.b * b_wb * exp_mult}; + Vec3fCpp blurredCoarseTone{blurredCoarse.r * r_wb * exp_mult, + blurredCoarse.g * g_wb * exp_mult, + blurredCoarse.b * b_wb * exp_mult}; + const float sceneWhiteNorm = std::max(sceneWhite * exp_mult, 1e-4f); + color = apply_pv2012_tone_ranges_cpp( + color, blurredFineTone, blurredCoarseTone, high / 100.0f, + shad / 100.0f, whites / 100.0f, blacks / 100.0f, clarity / 100.0f, + sceneWhiteNorm); - // 5. HSL + // 2. Contrast + r = std::pow(std::max(0.0f, color.r), con); + g = std::pow(std::max(0.0f, color.g), con); + b = std::pow(std::max(0.0f, color.b), con); + + // 3. HSL HSV hsv_struct = rgb_to_hsv(r, g, b); float hue = hsv_struct.h; float h_norm = hue / 360.0f; @@ -565,7 +883,8 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, float lumaDelta = lumaOut - lumaIn; if (lumaDelta > 0.0f) { // Soften black-point lift sensitivity near absolute black. - float blackLiftAtten = mix(0.60f, 1.0f, smoothstep(0.0f, 0.20f, lumaIn)); + float blackLiftAtten = + mix(0.60f, 1.0f, smoothstep(0.0f, 0.20f, lumaIn)); lumaDelta *= blackLiftAtten; } float lumaRatio = (lumaIn > 0.001f) ? lumaOut / lumaIn : 1.0f; @@ -581,8 +900,10 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, cr = lutRed[idxR]; cg = lutGreen[idxG]; cb = lutBlue[idxB]; - float arCr = cr + lumaDelta, arCg = cg + lumaDelta, arCb = cb + lumaDelta; - float mrCr = cr * lumaRatio, mrCg = cg * lumaRatio, mrCb = cb * lumaRatio; + float arCr = cr + lumaDelta, arCg = cg + lumaDelta, + arCb = cb + lumaDelta; + float mrCr = cr * lumaRatio, mrCg = cg * lumaRatio, + mrCb = cb * lumaRatio; cr = mix(arCr, mrCr, blendShadow); cg = mix(arCg, mrCg, blendShadow); cb = mix(arCb, mrCb, blendShadow); @@ -639,9 +960,15 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, gpuMatches = searcher.runSearch(luma.data(), w, h, 19); if (gpuMatches.empty()) { - LogManager::instance()->log("[ ImageDeveloper ] - GPU search produced no matches (possibly due to frame conflict or shader error). Falling back to CPU matching.", PHOTON_WARNING); + LogManager::instance()->log( + "[ ImageDeveloper ] - GPU search produced no matches (possibly due " + "to frame conflict or shader error). Falling back to CPU matching.", + PHOTON_WARNING); } else { - LogManager::instance()->log(QString("[ ImageDeveloper ] - GPU search successful: %1 matches").arg(gpuMatches.size()), PHOTON_DEBUG); + LogManager::instance()->log( + QString("[ ImageDeveloper ] - GPU search successful: %1 matches") + .arg(gpuMatches.size()), + PHOTON_DEBUG); } } photon::DenoiseParams dparams; @@ -651,8 +978,8 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, dparams.chromaDenoise = denoiseChromaAmount; dparams.chromaBm3d = denoiseChromaBm3d; - output = Denoiser::denoise(output, denoiseAmount, nullptr, denoiseSecondPass, - 4, gpuMatches, dparams); + output = Denoiser::denoise(output, denoiseAmount, nullptr, + denoiseSecondPass, 4, gpuMatches, dparams); // Convert back to RGB888 if Denoiser changed format to RGBX64 if (output.format() != QImage::Format_RGB888) { @@ -662,8 +989,8 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, // === Crop & Geometry transforms === // 1. Orientation steps (90° rotations) - int orientSteps = obj.contains("orientationSteps") - ? obj["orientationSteps"].toInt() : 0; + int orientSteps = + obj.contains("orientationSteps") ? obj["orientationSteps"].toInt() : 0; orientSteps = ((orientSteps % 4) + 4) % 4; if (orientSteps > 0) { QTransform rot; @@ -672,21 +999,24 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, } // 2. Flip - bool flipH = obj.contains("flipHorizontal") - ? obj["flipHorizontal"].toBool() : false; - bool flipV = obj.contains("flipVertical") - ? obj["flipVertical"].toBool() : false; + bool flipH = + obj.contains("flipHorizontal") ? obj["flipHorizontal"].toBool() : false; + bool flipV = + obj.contains("flipVertical") ? obj["flipVertical"].toBool() : false; if (flipH && flipV) { - output = output.transformed(QTransform().scale(-1, -1), Qt::SmoothTransformation); + output = output.transformed(QTransform().scale(-1, -1), + Qt::SmoothTransformation); } else if (flipH) { - output = output.transformed(QTransform().scale(-1, 1), Qt::SmoothTransformation); + output = + output.transformed(QTransform().scale(-1, 1), Qt::SmoothTransformation); } else if (flipV) { - output = output.transformed(QTransform().scale(1, -1), Qt::SmoothTransformation); + output = + output.transformed(QTransform().scale(1, -1), Qt::SmoothTransformation); } // 3. Straighten (fine rotation) - double straighten = obj.contains("straightenAngle") - ? obj["straightenAngle"].toDouble() : 0.0; + double straighten = + obj.contains("straightenAngle") ? obj["straightenAngle"].toDouble() : 0.0; if (std::abs(straighten) > 0.01) { QTransform rot; rot.rotate(straighten); @@ -725,14 +1055,14 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, int pw = cropRight - cropLeft; int ph = cropBottom - cropTop; - if (pw > 0 && ph > 0) - output = output.copy(cropLeft, cropTop, pw, ph); + if (pw > 0 && ph > 0) output = output.copy(cropLeft, cropTop, pw, ph); } } } LogManager::instance()->log( - QString("[ ImageDeveloper ] - cropDebug preCrop=%1x%2 cropPx=[%3,%4 -> %5,%6] out=%7x%8") + QString("[ ImageDeveloper ] - cropDebug preCrop=%1x%2 cropPx=[%3,%4 -> " + "%5,%6] out=%7x%8") .arg(preCropW) .arg(preCropH) .arg(cropLeft) @@ -743,9 +1073,10 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, .arg(output.height()), PHOTON_DEBUG); - LogManager::instance()->log( - QString("[ ImageDeveloper ] - export END: %1x%2").arg(output.width()).arg(output.height()), - PHOTON_DEBUG); + LogManager::instance()->log(QString("[ ImageDeveloper ] - export END: %1x%2") + .arg(output.width()) + .arg(output.height()), + PHOTON_DEBUG); return output; } diff --git a/src/engine/RawEngine.cpp b/src/engine/RawEngine.cpp index 95954af..ad712ef 100644 --- a/src/engine/RawEngine.cpp +++ b/src/engine/RawEngine.cpp @@ -17,12 +17,12 @@ #include #include +#include "../components/ToneLutProvider.h" #include "../managers/AppStateManager.h" #include "../managers/LogManager.h" #include "../managers/PreviewManager.h" #include "Denoiser.h" #include "GpuSearcher.h" -#include "../components/ToneLutProvider.h" using namespace photon; @@ -140,6 +140,301 @@ static void apply_luma_target_hist(float& r, float& g, float& b, float lumaIn, b *= safeRatio; } +struct Vec3fHist { + float r; + float g; + float b; +}; + +static float step_hist(float edge, float x) { return x < edge ? 0.0f : 1.0f; } + +static float sign_hist(float x) { + if (x > 0.0f) return 1.0f; + if (x < 0.0f) return -1.0f; + return 0.0f; +} + +static Vec3fHist clamp_vec3_hist(const Vec3fHist& c, float lo, float hi) { + return {std::clamp(c.r, lo, hi), std::clamp(c.g, lo, hi), + std::clamp(c.b, lo, hi)}; +} + +static float bilinear_channel_hist(const ushort* src, int width, int height, + float u, float v, int ch) { + u = std::clamp(u, 0.0f, 1.0f); + v = std::clamp(v, 0.0f, 1.0f); + const float xf = u * float(width) - 0.5f; + const float yf = v * float(height) - 0.5f; + const int x0 = int(std::floor(xf)); + const int y0 = int(std::floor(yf)); + const int x1 = x0 + 1; + const int y1 = y0 + 1; + const float tx = xf - float(x0); + const float ty = yf - float(y0); + auto sample = [src, width, height, ch](int x, int y) { + x = std::clamp(x, 0, width - 1); + y = std::clamp(y, 0, height - 1); + return src[(y * width + x) * 3 + ch] / 65535.0f; + }; + const float c00 = sample(x0, y0); + const float c10 = sample(x1, y0); + const float c01 = sample(x0, y1); + const float c11 = sample(x1, y1); + return lerp(lerp(c00, c10, tx), lerp(c01, c11, tx), ty); +} + +static Vec3fHist sample_source_linear_bilinear_hist(const ushort* src, + int width, int height, + float u, float v) { + return {bilinear_channel_hist(src, width, height, u, v, 0), + bilinear_channel_hist(src, width, height, u, v, 1), + bilinear_channel_hist(src, width, height, u, v, 2)}; +} + +static Vec3fHist compute_fine_blur_hist(const ushort* src, int width, + int height, float u, float v) { + Vec3fHist blur{0.0f, 0.0f, 0.0f}; + auto tap = [&](float dx, float dy, float w) { + Vec3fHist s = sample_source_linear_bilinear_hist( + src, width, height, u + dx / float(width), v + dy / float(height)); + blur.r += s.r * w; + blur.g += s.g * w; + blur.b += s.b * w; + }; + + constexpr float r1 = 1.5f; + constexpr float r2 = 3.0f; + + tap(0.0f, 0.0f, 0.18f); + tap(r1, 0.0f, 0.095f); + tap(-r1, 0.0f, 0.095f); + tap(0.0f, r1, 0.095f); + tap(0.0f, -r1, 0.095f); + tap(r1, r1, 0.055f); + tap(-r1, r1, 0.055f); + tap(r1, -r1, 0.055f); + tap(-r1, -r1, 0.055f); + + tap(r2, 0.0f, 0.04f); + tap(-r2, 0.0f, 0.04f); + tap(0.0f, r2, 0.04f); + tap(0.0f, -r2, 0.04f); + tap(r2, r2, 0.015f); + tap(-r2, r2, 0.015f); + tap(r2, -r2, 0.015f); + tap(-r2, -r2, 0.015f); + return blur; +} + +static Vec3fHist compute_coarse_blur_hist(const ushort* src, int width, + int height, float u, float v) { + Vec3fHist blur{0.0f, 0.0f, 0.0f}; + auto tap = [&](float dx, float dy, float w) { + Vec3fHist s = sample_source_linear_bilinear_hist( + src, width, height, u + dx / float(width), v + dy / float(height)); + blur.r += s.r * w; + blur.g += s.g * w; + blur.b += s.b * w; + }; + + constexpr float r1 = 4.5f; + constexpr float r2 = 7.0f; + constexpr float r3 = 9.5f; + + tap(0.0f, 0.0f, 0.20f); + tap(r1, 0.0f, 0.055f); + tap(-r1, 0.0f, 0.055f); + tap(0.0f, r1, 0.055f); + tap(0.0f, -r1, 0.055f); + tap(r1, r1, 0.038f); + tap(-r1, r1, 0.038f); + tap(r1, -r1, 0.038f); + tap(-r1, -r1, 0.038f); + + tap(r2, 0.0f, 0.04f); + tap(-r2, 0.0f, 0.04f); + tap(0.0f, r2, 0.04f); + tap(0.0f, -r2, 0.04f); + tap(r2, r2, 0.03f); + tap(-r2, r2, 0.03f); + tap(r2, -r2, 0.03f); + tap(-r2, -r2, 0.03f); + + tap(r3, 0.0f, 0.022f); + tap(-r3, 0.0f, 0.022f); + tap(0.0f, r3, 0.022f); + tap(0.0f, -r3, 0.022f); + tap(r3, r3, 0.015f); + tap(-r3, r3, 0.015f); + tap(r3, -r3, 0.015f); + tap(-r3, -r3, 0.015f); + return blur; +} + +constexpr float PV_FLARE_LINEAR_HIST = 0.000244140625f; // 2^-12 +constexpr float PV_FLARE_LOG_HIST = -12.0f; +constexpr float PV_EPS_HIST = 0.00000190734f; + +static Vec3fHist eval_undo_render_curve_hist(const Vec3fHist& col) { + constexpr float eps = 0.00001f; + const float fMin = std::min({col.r, col.g, col.b}); + const float fMax = std::max({col.r, col.g, col.b}); + const float tMin = std::pow(fMin, 3.14453125f); + const float tMax = std::pow(fMax, 3.14453125f); + const float nMin = std::pow(fMin, 0.8125f) * 0.3828125f * (1.0f - tMin) + + (1.0f - std::pow(1.0f - fMin, 0.69140625f)) * tMin; + const float nMax = std::pow(fMax, 0.8125f) * 0.3828125f * (1.0f - tMax) + + (1.0f - std::pow(1.0f - fMax, 0.69140625f)) * tMax; + const float scale = (nMax - nMin) / (fMax - fMin + eps); + return {(col.r - fMin) * scale + nMin, (col.g - fMin) * scale + nMin, + (col.b - fMin) * scale + nMin}; +} + +static float pv_working_luma_linear_hist(const Vec3fHist& c) { + Vec3fHist clamped = clamp_vec3_hist(c, 0.0001f, 0.999f); + Vec3fHist prophoto{ + 0.529285f * clamped.r + 0.330046f * clamped.g + 0.140669f * clamped.b, + 0.098394f * clamped.r + 0.873493f * clamped.g + 0.028113f * clamped.b, + 0.016823f * clamped.r + 0.117671f * clamped.g + 0.865506f * clamped.b}; + Vec3fHist unmapped = + clamp_vec3_hist(eval_undo_render_curve_hist(prophoto), 0.0f, 1.0f); + return std::max(unmapped.r * 0.25f + unmapped.g * 0.5f + unmapped.b * 0.25f, + PV_EPS_HIST); +} + +static float pv_encode_log_luma_hist(float linearLuma) { + return std::log2(std::max(linearLuma + PV_FLARE_LINEAR_HIST, PV_EPS_HIST)); +} + +static float pv_decode_log_luma_hist(float logLuma) { + return std::max(std::exp2(logLuma) - PV_FLARE_LINEAR_HIST, PV_EPS_HIST); +} + +static float endpoint_pin_mask_component_hist(float x) { + x = std::clamp(x, 0.0f, 1.0f); + const float inv = 1.0f - x; + const float inv2 = inv * inv; + const float inv4 = inv2 * inv2; + const float inv8 = inv4 * inv4; + const float inv16 = inv8 * inv8; + const float base = 1.0f - inv8; + const float strong = 1.0f - inv16; + return lerp(base, strong, smoothstep(0.35f, 1.0f, x)); +} + +static float pv_log_luma_hist(const Vec3fHist& c) { + return pv_encode_log_luma_hist(pv_working_luma_linear_hist(c)); +} + +static float pv_tent_weight_hist(float value, float center, float halfWidth) { + return std::max( + 1.0f - std::abs(value - center) / std::max(halfWidth, PV_EPS_HIST), 0.0f); +} + +static Vec3fHist apply_pv2012_tone_ranges_hist( + const Vec3fHist& color, const Vec3fHist& blurredFine, + const Vec3fHist& blurredCoarse, float highlightsAmt, float shadowsAmt, + float whitesAmt, float blacksAmt, float clarityAmt, float sceneWhiteNorm) { + const float srcGrayLinear = pv_working_luma_linear_hist(color); + const float srcGrayLog = pv_encode_log_luma_hist(srcGrayLinear); + const float blurFineLog = pv_log_luma_hist(blurredFine); + const float blurCoarseLog = pv_log_luma_hist(blurredCoarse); + const float toneMid = + pv_encode_log_luma_hist(std::max(sceneWhiteNorm * 0.18f, PV_EPS_HIST)); + + const float wBlacks = pv_tent_weight_hist(srcGrayLog, toneMid - 3.8f, 1.8f); + const float wShadows = pv_tent_weight_hist(srcGrayLog, toneMid - 1.9f, 1.9f); + const float wHighlights = + pv_tent_weight_hist(srcGrayLog, toneMid + 1.0f, 1.9f); + const float wWhites = pv_tent_weight_hist(srcGrayLog, toneMid + 3.1f, 2.2f); + + const float maskFine = std::clamp(srcGrayLog - blurFineLog, -2.0f, 2.0f); + const float maskCoarse = std::clamp(blurFineLog - blurCoarseLog, -2.0f, 2.0f); + const float mask = + std::clamp(maskFine * 0.70f + maskCoarse * 0.45f, -2.5f, 2.5f); + + const float partSwitch = step_hist(srcGrayLog, toneMid); + const float compressedLow = toneMid + (srcGrayLog - toneMid) * 0.78f; + const float compressedHigh = toneMid + (srcGrayLog - toneMid) * 0.58f; + const float baseCompressed = lerp(compressedHigh, compressedLow, partSwitch); + + float localContrastSignal = srcGrayLog + mask - baseCompressed; + localContrastSignal *= std::max(clarityAmt, 0.0f); + localContrastSignal *= + std::clamp(1.0f + 0.35f * (-highlightsAmt + shadowsAmt), 1.0f, 2.0f); + const float localSignalHigh = std::max(localContrastSignal, 0.0f); + const float localSignalLow = std::min(localContrastSignal, 0.0f); + + const float lumWeightHigh = + std::clamp(wHighlights + 0.6f * wWhites, 0.0f, 1.0f); + const float lumWeightLow = std::clamp(wShadows + 0.6f * wBlacks, 0.0f, 1.0f); + const float endpointHigh = std::clamp( + std::abs(highlightsAmt) + 0.35f * std::abs(whitesAmt), 0.0f, 1.0f); + const float endpointLow = std::clamp( + std::abs(shadowsAmt) + 0.35f * std::abs(blacksAmt), 0.0f, 1.0f); + const float clarityPinHigh = + lerp(endpoint_pin_mask_component_hist(lumWeightHigh), 1.0f, + endpointHigh * endpointHigh); + const float clarityPinLow = + lerp(endpoint_pin_mask_component_hist(lumWeightLow), 1.0f, + endpointLow * endpointLow); + + float hsPinY = + lerp(0.5f + 0.5f * std::max(1.0f - sign_hist(shadowsAmt), 0.0f), 1.0f, + clarityPinHigh); + float hsPinX = + lerp(1.0f, 0.5f, + (1.0f - clarityPinLow) * std::max(-sign_hist(highlightsAmt), 0.0f)); + hsPinX = lerp(1.0f, hsPinX, std::clamp(std::abs(highlightsAmt), 0.0f, 1.0f)); + + const float maxAbsHS = std::max( + std::max(std::abs(highlightsAmt), std::abs(shadowsAmt)), PV_EPS_HIST); + const float baseOffset = 0.85f * (highlightsAmt + shadowsAmt) / maxAbsHS; + const float offsetHSHigh = wHighlights * std::abs(highlightsAmt) * baseOffset; + const float offsetHSLow = wShadows * std::abs(shadowsAmt) * baseOffset; + + float deltaHSHigh = std::clamp(-highlightsAmt, -1.0f, 1.0f); + float deltaHSLow = std::clamp(shadowsAmt, -1.0f, 1.0f); + deltaHSHigh *= std::min(mask, 0.0f); + deltaHSLow *= std::max(mask, 0.0f); + deltaHSHigh += offsetHSHigh; + deltaHSLow += offsetHSLow; + + float deltaStops = deltaHSHigh * hsPinX + deltaHSLow * hsPinY; + deltaStops += whitesAmt * wWhites * hsPinX; + deltaStops += blacksAmt * wBlacks * hsPinY; + deltaStops += + localSignalHigh * clarityPinHigh + localSignalLow * clarityPinLow; + + const float deltaSign = sign_hist(deltaStops); + const float flareSwitch = 1.0f - std::max(deltaSign, 0.0f); + const float zeroSwitch = 1.0f - std::abs(deltaSign); + const float flare = flareSwitch * PV_FLARE_LOG_HIST; + const float startpoint = flare - (deltaStops + deltaStops); + const float t1 = step_hist(startpoint, srcGrayLog); + const float t2 = step_hist(srcGrayLog, startpoint); + float t = + std::clamp((srcGrayLog - startpoint) / (flare - startpoint + zeroSwitch), + 0.0f, 1.0f); + t *= t * (1.0f - lerp(t2, t1, flareSwitch)); + deltaStops = lerp(deltaStops, 0.0f, t); + + deltaStops = std::min(deltaStops, 4.0f); + const float targetLog = srcGrayLog + deltaStops; + float targetLuma = pv_decode_log_luma_hist(targetLog); + + if (targetLuma > sceneWhiteNorm && deltaStops > 0.0f) { + const float over = targetLuma - sceneWhiteNorm; + const float knee = std::max(sceneWhiteNorm * 0.7f, PV_EPS_HIST); + const float compress = over / (1.0f + over / knee); + targetLuma = sceneWhiteNorm + compress; + } + + Vec3fHist out = color; + apply_luma_target_hist(out.r, out.g, out.b, srcGrayLinear, targetLuma); + return out; +} + RawEngine::RawEngine(QObject* parent) : QObject(parent), m_processor(std::make_unique()) { m_denoiseEnabled = true; @@ -563,9 +858,12 @@ void RawEngine::setDenoiseSearchWindow(int val) { void RawEngine::setDenoiseGroupSize(int val) { // Snap to nearest power of 2 (4, 8, 16) - if (val <= 6) val = 4; - else if (val <= 12) val = 8; - else val = 16; + if (val <= 6) + val = 4; + else if (val <= 12) + val = 8; + else + val = 16; if (m_denoiseGroupSize == val) return; m_denoiseGroupSize = val; m_hasDenoisedResult = false; @@ -863,15 +1161,13 @@ void RawEngine::startAsyncDenoise(bool final, float zoom, const QRectF& roi) { dparams.chromaDenoise = m_denoiseChromaAmount; dparams.chromaBm3d = m_denoiseChromaBm3d; - QFuture future = QtConcurrent::run([img, amount, abortPtr, - useSecondPass, stride, gpuMatches, - dparams]() { - return photon::Denoiser::denoise(img, amount, abortPtr, useSecondPass, - stride, gpuMatches, dparams); - }); + QFuture future = QtConcurrent::run( + [img, amount, abortPtr, useSecondPass, stride, gpuMatches, dparams]() { + return photon::Denoiser::denoise(img, amount, abortPtr, useSecondPass, + stride, gpuMatches, dparams); + }); m_denoiseWatcher.setFuture(future); - LogManager::instance()->log( - QString("[ RawEngine.cpp ] - Started denoise")); + LogManager::instance()->log(QString("[ RawEngine.cpp ] - Started denoise")); } void RawEngine::clearDenoisedResult() { @@ -1218,7 +1514,7 @@ void RawEngine::setFlipVertical(bool flip) { } std::vector RawEngine::evalMonotonicSpline(const QVariantList& pts, - int lutSize) { + int lutSize) { std::vector lut(lutSize); int n = pts.size(); if (n < 2) { @@ -1331,7 +1627,8 @@ void RawEngine::rebuildToneLut() { constexpr int kToneLutSide = 256; constexpr int kToneLutRowsPerChannel = kToneLutEntries / kToneLutSide; // 256 constexpr int kToneLutChannels = 4; - constexpr int kToneLutHeight = kToneLutRowsPerChannel * kToneLutChannels; // 1024 + constexpr int kToneLutHeight = + kToneLutRowsPerChannel * kToneLutChannels; // 1024 auto lutL = evalMonotonicSpline(m_toneCurveLuma, kToneLutEntries); auto lutR = evalMonotonicSpline(m_toneCurveRed, kToneLutEntries); @@ -1351,13 +1648,13 @@ void RawEngine::rebuildToneLut() { uint16_t(std::clamp(lutG[i] * 65535.0f + 0.5f, 0.0f, 65535.0f)); uint16_t vB = uint16_t(std::clamp(lutB[i] * 65535.0f + 0.5f, 0.0f, 65535.0f)); - if (vL != i || vR != i || vG != i || vB != i) - active = true; + if (vL != i || vR != i || vG != i || vB != i) active = true; } // 256×1024 RGBA texture: 4 channel planes (Luma, R, G, B), each a 256×256 // tile encoding 65536 LUT entries packed as 16-bit in RG (high, low). - m_toneLutImage = QImage(kToneLutSide, kToneLutHeight, QImage::Format_RGBA8888); + m_toneLutImage = + QImage(kToneLutSide, kToneLutHeight, QImage::Format_RGBA8888); m_toneLutImage.fill(Qt::black); const std::vector* luts[4] = {&lutL, &lutR, &lutG, &lutB}; for (int channel = 0; channel < 4; channel++) { @@ -1400,7 +1697,9 @@ void RawEngine::requestHistogramUpdate() { float high = m_highlights; float shad = m_shadows; float whites = m_whites; + float sceneWhite = m_sceneWhite; float blacks = m_blacks; + float clarity = m_clarity; float temp = m_temperature / 100.0f; float tint = m_tint / 100.0f; @@ -1432,18 +1731,21 @@ void RawEngine::requestHistogramUpdate() { // Capture image data pointer and dimensions const ushort* src = reinterpret_cast(m_processedImage->data); - int totalPixels = m_processedImage->width * m_processedImage->height; + int imageWidth = m_processedImage->width; + int imageHeight = m_processedImage->height; + int totalPixels = imageWidth * imageHeight; if (!src || totalPixels <= 0) return; m_histogramUpdatePending = true; m_histogramNeedsUpdate = false; - m_histogramFuture = QtConcurrent::run([this, src, totalPixels, exp, con, high, - shad, whites, blacks, temp, tint, - hsl_h, hsl_s, hsl_l, cgSH, cgSS, cgSL, - cgMH, cgMS, cgML, cgHH, cgHS, cgHL, - cgBal, cgBlen]() { + m_histogramFuture = QtConcurrent::run([this, src, imageWidth, imageHeight, + totalPixels, exp, con, high, shad, + whites, sceneWhite, blacks, clarity, + temp, tint, hsl_h, hsl_s, hsl_l, cgSH, + cgSS, cgSL, cgMH, cgMS, cgML, cgHH, + cgHS, cgHL, cgBal, cgBlen]() { std::vector r_bins(256, 0); std::vector g_bins(256, 0); std::vector b_bins(256, 0); @@ -1466,50 +1768,53 @@ void RawEngine::requestHistogramUpdate() { float g = src[i * 3 + 1] / 65535.0f; float b = src[i * 3 + 2] / 65535.0f; + const int x = i % imageWidth; + const int y = i / imageWidth; + const float u = (float(x) + 0.5f) / float(imageWidth); + const float v = (float(y) + 0.5f) / float(imageHeight); + Vec3fHist blurredFine = + compute_fine_blur_hist(src, imageWidth, imageHeight, u, v); + Vec3fHist blurredCoarse = + compute_coarse_blur_hist(src, imageWidth, imageHeight, u, v); + // 1. WB & Exposure r *= r_wb * exp_mult; g *= g_wb * exp_mult; b *= b_wb * exp_mult; + Vec3fHist color{r, g, b}; + + float l_tone = 0.2126f * std::max(0.0f, color.r) + + 0.7152f * std::max(0.0f, color.g) + + 0.0722f * std::max(0.0f, color.b); + if (l_tone > sceneWhite && exp > 0.0f) { + float over = l_tone - sceneWhite; + float knee = sceneWhite * 0.7f; + float compress = over / (1.0f + over / knee); + float targetL = sceneWhite + compress; + apply_luma_target_hist(color.r, color.g, color.b, l_tone, targetL); + } + + Vec3fHist blurredFineTone{blurredFine.r * r_wb * exp_mult, + blurredFine.g * g_wb * exp_mult, + blurredFine.b * b_wb * exp_mult}; + Vec3fHist blurredCoarseTone{blurredCoarse.r * r_wb * exp_mult, + blurredCoarse.g * g_wb * exp_mult, + blurredCoarse.b * b_wb * exp_mult}; + const float sceneWhiteNorm = std::max(sceneWhite * exp_mult, 1e-4f); + color = apply_pv2012_tone_ranges_hist( + color, blurredFineTone, blurredCoarseTone, high / 100.0f, + shad / 100.0f, whites / 100.0f, blacks / 100.0f, clarity / 100.0f, + sceneWhiteNorm); + r = color.r; + g = color.g; + b = color.b; + // 2. Contrast r = std::pow(std::max(0.0f, r), con); g = std::pow(std::max(0.0f, g), con); b = std::pow(std::max(0.0f, b), con); - // 3. Whites & Blacks (specialized targeting) - float l_tone = 0.2126f * std::max(0.0f, r) + 0.7152f * std::max(0.0f, g) + - 0.0722f * std::max(0.0f, b); - if (whites != 0.0f) { - float whiteMask = smoothstep(0.7f, 1.25f, l_tone); - float target = compute_target_luma_hist(l_tone, (whites / 100.0f) * whiteMask); - apply_luma_target_hist(r, g, b, l_tone, target); - l_tone = 0.2126f * std::max(0.0f, r) + 0.7152f * std::max(0.0f, g) + - 0.0722f * std::max(0.0f, b); - } - if (blacks != 0.0f) { - float blackMask = 1.0f - smoothstep(0.0f, 0.15f, l_tone); - float target = - compute_toe_target_hist(l_tone, (blacks / 100.0f) * blackMask); - apply_luma_target_hist(r, g, b, l_tone, target); - l_tone = 0.2126f * std::max(0.0f, r) + 0.7152f * std::max(0.0f, g) + - 0.0722f * std::max(0.0f, b); - } - - // 4. Highlights & Shadows (specialized targeting) - if (shad != 0.0f) { - float shadowMask = 1.0f - smoothstep(0.05f, 0.65f, l_tone); - float target = compute_toe_target_hist(l_tone, (shad / 100.0f) * shadowMask); - apply_luma_target_hist(r, g, b, l_tone, target); - l_tone = 0.2126f * std::max(0.0f, r) + 0.7152f * std::max(0.0f, g) + - 0.0722f * std::max(0.0f, b); - } - if (high != 0.0f) { - float highlightMask = smoothstep(0.35f, 1.1f, l_tone); - float target = - compute_target_luma_hist(l_tone, (high / 100.0f) * highlightMask); - apply_luma_target_hist(r, g, b, l_tone, target); - } - // 5. HSL PANEL HSV hsv = rgb_to_hsv_cpp(r, g, b); float hue_shift = 0.0f; @@ -1590,7 +1895,8 @@ void RawEngine::requestHistogramUpdate() { std::sort(allBins.begin(), allBins.end()); size_t p99_idx = std::min(allBins.size() - 1, static_cast(allBins.size() * 0.99)); - uint32_t max_val = allBins.empty() ? 1 : std::max(allBins[p99_idx], uint32_t(1)); + uint32_t max_val = + allBins.empty() ? 1 : std::max(allBins[p99_idx], uint32_t(1)); QMetaObject::invokeMethod( this, @@ -1628,7 +1934,8 @@ void RawEngine::requestHistogramUpdate() { } void RawEngine::clearProcessedImage() { - // Wait for any in-flight histogram task that references m_processedImage->data + // Wait for any in-flight histogram task that references + // m_processedImage->data if (m_histogramUpdatePending) { m_histogramFuture.waitForFinished(); m_histogramUpdatePending = false; @@ -1646,55 +1953,53 @@ static float srgb_to_linear(float c) { return (c <= 0.04045f) ? (c / 12.92f) : std::pow((c + 0.055f) / 1.055f, 2.4f); } -float RawEngine::computeSceneWhite(const libraw_processed_image_t* img, float percentile) { - constexpr int BINS = 2048; +float RawEngine::computeSceneWhite(const libraw_processed_image_t* img, + float percentile) { + constexpr int BINS = 2048; constexpr float BIN_SCALE = BINS - 1; uint32_t hist[BINS] = {}; size_t pixelCount = img->width * img->height; - size_t step = 4; // subsample every 4th pixel - int channels = img->colors; // 3 for RGB + size_t step = 4; // subsample every 4th pixel + int channels = img->colors; // 3 for RGB for (size_t i = 0; i < pixelCount; i += step) { - float r, g, b; - - if (img->bits == 16) { - const uint16_t* px = reinterpret_cast(img->data) - + i * channels; - r = px[0] / 65535.0f; - g = px[1] / 65535.0f; - b = px[2] / 65535.0f; - } else { - const uint8_t* px = img->data + i * channels; - r = px[0] / 255.0f; - g = px[1] / 255.0f; - b = px[2] / 255.0f; - } + float r, g, b; + + if (img->bits == 16) { + const uint16_t* px = + reinterpret_cast(img->data) + i * channels; + r = px[0] / 65535.0f; + g = px[1] / 65535.0f; + b = px[2] / 65535.0f; + } else { + const uint8_t* px = img->data + i * channels; + r = px[0] / 255.0f; + g = px[1] / 255.0f; + b = px[2] / 255.0f; + } - // sRGB -> linear, matches your shader's srgb_to_linear() - auto decode = [](float x) -> float { - return (x <= 0.04045f) - ? x / 12.92f - : std::pow((x + 0.055f) / 1.055f, 2.4f); - }; + // sRGB -> linear, matches your shader's srgb_to_linear() + auto decode = [](float x) -> float { + return (x <= 0.04045f) ? x / 12.92f + : std::pow((x + 0.055f) / 1.055f, 2.4f); + }; - float luma = 0.2126f * decode(r) - + 0.7152f * decode(g) - + 0.0722f * decode(b); + float luma = + 0.2126f * decode(r) + 0.7152f * decode(g) + 0.0722f * decode(b); - int bin = static_cast(std::clamp(luma, 0.0f, 1.0f) * BIN_SCALE); - hist[bin]++; + int bin = static_cast(std::clamp(luma, 0.0f, 1.0f) * BIN_SCALE); + hist[bin]++; } size_t sampledPixels = (pixelCount + step - 1) / step; - size_t threshold = static_cast(sampledPixels * percentile); - size_t cumulative = 0; + size_t threshold = static_cast(sampledPixels * percentile); + size_t cumulative = 0; for (int bin = 0; bin < BINS; ++bin) { - cumulative += hist[bin]; - if (cumulative >= threshold) - return (bin + 0.5f) / BIN_SCALE; + cumulative += hist[bin]; + if (cumulative >= threshold) return (bin + 0.5f) / BIN_SCALE; } return 1.0f; @@ -2044,7 +2349,8 @@ static void applyJsonToState(RawEngine* e, const QJsonObject& obj) { if (obj.contains("shadows")) e->setShadows(obj["shadows"].toDouble()); if (obj.contains("whites")) e->setWhites(obj["whites"].toDouble()); if (obj.contains("blacks")) e->setBlacks(obj["blacks"].toDouble()); - if (obj.contains("adaptation")) e->setAdaptation(obj["adaptation"].toDouble()); + if (obj.contains("adaptation")) + e->setAdaptation(obj["adaptation"].toDouble()); if (obj.contains("vibrance")) e->setVibrance(obj["vibrance"].toDouble()); if (obj.contains("saturation")) e->setSaturation(obj["saturation"].toDouble()); @@ -2085,9 +2391,12 @@ static void applyJsonToState(RawEngine* e, const QJsonObject& obj) { if (obj.contains("structure")) e->setStructure(obj["structure"].toDouble()); if (obj.contains("centre")) e->setCentre(obj["centre"].toDouble()); if (obj.contains("sharpness")) e->setSharpness(obj["sharpness"].toDouble()); - if (obj.contains("sharpenMask")) e->setSharpenMask(obj["sharpenMask"].toDouble()); - if (obj.contains("maskFeather")) e->setMaskFeather(obj["maskFeather"].toDouble()); - if (obj.contains("focusDetect")) e->setFocusDetect(obj["focusDetect"].toDouble()); + if (obj.contains("sharpenMask")) + e->setSharpenMask(obj["sharpenMask"].toDouble()); + if (obj.contains("maskFeather")) + e->setMaskFeather(obj["maskFeather"].toDouble()); + if (obj.contains("focusDetect")) + e->setFocusDetect(obj["focusDetect"].toDouble()); if (obj.contains("hslRedHue")) e->setHslRedHue(obj["hslRedHue"].toDouble()); if (obj.contains("hslRedSaturation")) @@ -2301,8 +2610,9 @@ void RawEngine::loadEdits() { if (m_source.isEmpty()) return; QFileInfo fileInfo(m_source); - QString editsPath = QDir::toNativeSeparators(fileInfo.absolutePath() + "/.PhotonData/edits/" + - fileInfo.fileName() + ".json"); + QString editsPath = + QDir::toNativeSeparators(fileInfo.absolutePath() + "/.PhotonData/edits/" + + fileInfo.fileName() + ".json"); m_editStack.clear(); @@ -2337,7 +2647,8 @@ void RawEngine::loadEdits() { // Apply last state - this will trigger signals and update UI QJsonObject lastState = arr.last().toObject(); LogManager::instance()->log( - QString("[ RawEngine ] - Loading edits: denoiseEnabled=%1, denoiseAmount=%2") + QString( + "[ RawEngine ] - Loading edits: denoiseEnabled=%1, denoiseAmount=%2") .arg(lastState["denoiseEnabled"].toBool()) .arg(lastState["denoiseAmount"].toDouble()), PHOTON_DEBUG); @@ -2377,9 +2688,11 @@ void RawEngine::commitEdit() { // Save full stack to file QFileInfo fileInfo(m_source); - QString editsDir = QDir::toNativeSeparators(fileInfo.absolutePath() + "/.PhotonData/edits"); + QString editsDir = + QDir::toNativeSeparators(fileInfo.absolutePath() + "/.PhotonData/edits"); QDir().mkpath(editsDir); - QString editsPath = QDir::toNativeSeparators(editsDir + "/" + fileInfo.fileName() + ".json"); + QString editsPath = + QDir::toNativeSeparators(editsDir + "/" + fileInfo.fileName() + ".json"); QJsonArray arr; for (const auto& v : m_editStack) { @@ -2551,7 +2864,8 @@ bool RawEngine::isDefault() const { if (!qFuzzyIsNull(m_cgBalance)) return false; if (!qFuzzyCompare(m_cgBlending, 50.0f)) return false; - // Tone curve check (non-default = more than 2 points or non-identity endpoints) + // Tone curve check (non-default = more than 2 points or non-identity + // endpoints) auto isIdentityCurve = [](const QVariantList& pts) { if (pts.size() != 2) return false; auto p0 = pts[0].toMap(); @@ -2607,11 +2921,11 @@ QImage RawEngine::applyGeometryTransforms(const QImage& input, int orientSteps, output = output.transformed(QTransform().scale(-1, -1), Qt::SmoothTransformation); } else if (flipH) { - output = output.transformed(QTransform().scale(-1, 1), - Qt::SmoothTransformation); + output = + output.transformed(QTransform().scale(-1, 1), Qt::SmoothTransformation); } else if (flipV) { - output = output.transformed(QTransform().scale(1, -1), - Qt::SmoothTransformation); + output = + output.transformed(QTransform().scale(1, -1), Qt::SmoothTransformation); } // 3. Straighten (fine rotation) @@ -2653,7 +2967,9 @@ QImage RawEngine::applyGeometryTransforms(const QImage& input, int orientSteps, } LogManager::instance()->log( - QString("[ RawEngine.cpp ] - cropDebug applyGeometry in=%1x%2 orient=%3 flipH=%4 flipV=%5 straighten=%6 cropN=(%7,%8,%9,%10) preCrop=%11x%12 cropPx=[%13,%14 -> %15,%16] out=%17x%18") + QString("[ RawEngine.cpp ] - cropDebug applyGeometry in=%1x%2 orient=%3 " + "flipH=%4 flipV=%5 straighten=%6 cropN=(%7,%8,%9,%10) " + "preCrop=%11x%12 cropPx=[%13,%14 -> %15,%16] out=%17x%18") .arg(inputW) .arg(inputH) .arg(orientSteps) @@ -2697,71 +3013,68 @@ void RawEngine::reloadWithGeometry() { QRectF crop = m_cropRect; bool hasGeom = hasNonDefaultGeometry(); - QFuture future = QtConcurrent::run( - [this, path, loadId, orientSteps, flipH, flipV, straighten, crop, - hasGeom]() { - QMutexLocker locker(&m_processorMutex); - if (loadId != m_currentLoadId) - return LoadResult{false, loadId}; - - // Re-decode from RAW file - bool ok = loadRawFileSync(path, loadId); - if (!ok || loadId != m_currentLoadId) - return LoadResult{false, loadId}; - - if (!hasGeom) { - m_geometryBuffer.clear(); - m_geometryWidth = 0; - m_geometryHeight = 0; - return LoadResult{true, loadId}; - } - - // Get processed image from LibRaw - if (!m_processedImage) { - int ret = m_processor->dcraw_process(); - if (ret != LIBRAW_SUCCESS) return LoadResult{false, loadId}; - m_processedImage = m_processor->dcraw_make_mem_image(&ret); - if (!m_processedImage) return LoadResult{false, loadId}; - } - - int w = m_processedImage->width; - int h = m_processedImage->height; - int colors = m_processedImage->colors; - - // Convert LibRaw buffer to QImage - QImage srcImg; - if (colors == 3) { - srcImg = QImage(w, h, QImage::Format_RGBX64); - const ushort* src = - reinterpret_cast(m_processedImage->data); - QRgba64* dst = reinterpret_cast(srcImg.bits()); - for (int i = 0; i < w * h; ++i) { - dst[i] = QRgba64::fromRgba64(src[i * 3], src[i * 3 + 1], - src[i * 3 + 2], 65535); - } - } else { - srcImg = - QImage(reinterpret_cast(m_processedImage->data), w, - h, QImage::Format_RGBA64) - .copy(); - } - - // Apply geometry transforms - QImage transformed = applyGeometryTransforms(srcImg, orientSteps, flipH, - flipV, straighten, crop); - - // Convert back to RGBA64 buffer for getProcessedData - transformed = transformed.convertToFormat(QImage::Format_RGBA64); - int tw = transformed.width(); - int th = transformed.height(); - size_t bufSize = static_cast(tw) * th * 8; - m_geometryBuffer.resize(bufSize); - memcpy(m_geometryBuffer.data(), transformed.constBits(), bufSize); - m_geometryWidth = tw; - m_geometryHeight = th; - - return LoadResult{true, loadId}; - }); + QFuture future = QtConcurrent::run([this, path, loadId, + orientSteps, flipH, flipV, + straighten, crop, hasGeom]() { + QMutexLocker locker(&m_processorMutex); + if (loadId != m_currentLoadId) return LoadResult{false, loadId}; + + // Re-decode from RAW file + bool ok = loadRawFileSync(path, loadId); + if (!ok || loadId != m_currentLoadId) return LoadResult{false, loadId}; + + if (!hasGeom) { + m_geometryBuffer.clear(); + m_geometryWidth = 0; + m_geometryHeight = 0; + return LoadResult{true, loadId}; + } + + // Get processed image from LibRaw + if (!m_processedImage) { + int ret = m_processor->dcraw_process(); + if (ret != LIBRAW_SUCCESS) return LoadResult{false, loadId}; + m_processedImage = m_processor->dcraw_make_mem_image(&ret); + if (!m_processedImage) return LoadResult{false, loadId}; + } + + int w = m_processedImage->width; + int h = m_processedImage->height; + int colors = m_processedImage->colors; + + // Convert LibRaw buffer to QImage + QImage srcImg; + if (colors == 3) { + srcImg = QImage(w, h, QImage::Format_RGBX64); + const ushort* src = + reinterpret_cast(m_processedImage->data); + QRgba64* dst = reinterpret_cast(srcImg.bits()); + for (int i = 0; i < w * h; ++i) { + dst[i] = QRgba64::fromRgba64(src[i * 3], src[i * 3 + 1], src[i * 3 + 2], + 65535); + } + } else { + srcImg = QImage(reinterpret_cast(m_processedImage->data), w, + h, QImage::Format_RGBA64) + .copy(); + } + + // Apply geometry transforms + QImage transformed = applyGeometryTransforms(srcImg, orientSteps, flipH, + flipV, straighten, crop); + + // Convert back to RGBA64 buffer for getProcessedData + transformed = transformed.convertToFormat(QImage::Format_RGBA64); + int tw = transformed.width(); + int th = transformed.height(); + size_t bufSize = static_cast(tw) * th * 8; + m_geometryBuffer.resize(bufSize); + memcpy(m_geometryBuffer.data(), transformed.constBits(), bufSize); + m_geometryWidth = tw; + m_geometryHeight = th; + + return LoadResult{true, loadId}; + }); m_geometryLoadWatcher.setFuture(future); } From 29ac48ce1cb2b5410fb9f252b66f6509662495e4 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Wed, 1 Apr 2026 23:34:04 +0200 Subject: [PATCH 23/29] Release v0.2.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e6bde7d..ded8926 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) -project(Photon VERSION 0.1.1 LANGUAGES CXX) +project(Photon VERSION 0.2.0 LANGUAGES CXX) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) From b2e980144add914387d0f2f0a7f57aedb94dbc5a Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Fri, 3 Apr 2026 23:19:32 +0200 Subject: [PATCH 24/29] cleanup --- src/engine/Panorama.cpp | 205 ++++++++++++++++------------------------ 1 file changed, 83 insertions(+), 122 deletions(-) diff --git a/src/engine/Panorama.cpp b/src/engine/Panorama.cpp index 69d2fe5..1061189 100644 --- a/src/engine/Panorama.cpp +++ b/src/engine/Panorama.cpp @@ -16,7 +16,8 @@ static ColorInfo extractColorInfo(LibRaw *processor) { cv::Mat C(3, 3, CV_64F, cam2xyz); - // Not sure if the inverse is neede here + // NOTE: Not sure if the inverse is neede here. The DNG dump seems to work + // without issues, so keeping it here just in case. // cv::Mat Cinv = C.inv(); // now XYZ D50 → camera: this is ColorMatrix1 for (int i = 0; i < 3; i++) @@ -114,7 +115,6 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, std::unique_ptr colorInfo; - // These are the 16 bit images loaded from the camera std::vector images16; for (const auto& filename : inputFiles) { cv::Mat img = Panorama::raw_to_linear(filename, colorInfo); @@ -153,7 +153,6 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, sizes.push_back(img8.size()); } - // PHASE 1: Feature Detection and Matching LogManager::instance()->log("[ Panorama.cpp ] - Phase 1: Feature detection", PHOTON_DEBUG); @@ -171,7 +170,6 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, } - // Match features between images cv::Ptr matcher = cv::makePtr(false, 0.3f); std::vector pairwise_matches; @@ -179,7 +177,6 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, (*matcher)(features, pairwise_matches); matcher->collectGarbage(); - // Check if we have enough matches int num_matches = 0; for (const auto& match : pairwise_matches) { if (match.confidence > 0.0) num_matches++; @@ -194,7 +191,6 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, return result; } - // PHASE 2: Camera Parameter Estimation LogManager::instance()->log("[ Panorama.cpp ] - Phase 2: Camera estimation", PHOTON_DEBUG); @@ -211,12 +207,10 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, return result; } - // Convert rotation matrices to CV_32F format required by bundle adjuster - for (size_t i = 0; i < cameras.size(); ++i) { + for (size_t i = 0; i < cameras.size(); ++i) cameras[i].R.convertTo(cameras[i].R, CV_32F); - } + - // Refine camera parameters with bundle adjustment cv::Ptr adjuster = cv::makePtr(); @@ -230,11 +224,9 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, return result; } - // PHASE 3: Warping Images (16-bit) LogManager::instance()->log("[ Panorama.cpp ] - Phase 3: Warping images", PHOTON_DEBUG); - // Find median focal length std::vector focals; for (size_t i = 0; i < cameras.size(); ++i) { focals.push_back(cameras[i].focal); @@ -242,7 +234,6 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, std::sort(focals.begin(), focals.end()); float median_focal = static_cast(focals[focals.size() / 2]); - // Create cylindrical warper with scale based on focal length float warped_image_scale = median_focal; cv::Ptr warper_creator = cv::makePtr(); @@ -250,7 +241,6 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, warper_creator->create(static_cast(warped_image_scale)); LogManager::instance()->log("[ Panorama.cpp ] - Created warper", PHOTON_DEBUG); - // Warp images and create masks std::vector images_warped16; std::vector masks_warped; std::vector images_warped16_umat; @@ -265,14 +255,12 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, corners.push_back(roi.tl()); sizes_warped.push_back(roi.size()); - // Warp the 16-bit image cv::Mat warped; warper->warp(images16[i], K, cameras[i].R, cv::INTER_LINEAR, cv::BORDER_REFLECT, warped); images_warped16.push_back(warped); images_warped16_umat.push_back(warped.getUMat(cv::ACCESS_READ)); - // Create and warp mask cv::Mat mask = cv::Mat::ones(sizes[i], CV_8U) * 255; cv::Mat warped_mask; warper->warp(mask, K, cameras[i].R, cv::INTER_NEAREST, cv::BORDER_CONSTANT, @@ -281,7 +269,8 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, masks_warped_umat.push_back(warped_mask.getUMat(cv::ACCESS_READ)); } - // PHASE 4: Exposure Compensation (optional) + // As of now the user cannot choose to compensate for exposure. + // Will add that if I need it in my workflow. if (compensateExposure) { LogManager::instance()->log( "[ Panorama.cpp ] - Phase 4: Exposure compensation", PHOTON_DEBUG); @@ -298,12 +287,9 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, "[ Panorama.cpp ] - Phase 4: Skipping exposure compensation", PHOTON_DEBUG); } - // PHASE 5: Seam Finding (Graph-Cut) - // GraphCutSeamFinder expects 8-bit UMat images and binary masks LogManager::instance()->log("[ Panorama.cpp ] - Phase 5: Seam finding", PHOTON_DEBUG); - // Ensure masks are binary (0 or 255) std::vector masks_binary; for (auto& mask : masks_warped) { cv::Mat mask_bin; @@ -318,22 +304,14 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, images_warped8_umat.push_back(img8.getUMat(cv::ACCESS_READ)); } - // Use a simpler seam finder that's more robust cv::Ptr seam_finder = cv::makePtr(); seam_finder->find(images_warped8_umat, corners, masks_binary); - /* - cv::Ptr seam_finder = - cv::makePtr( - cv::detail::GraphCutSeamFinder::COST_COLOR); - seam_finder->find(images_warped8_umat, corners, masks_binary); -*/ - // PHASE 6: Multi-band Blending (16-bit) + LogManager::instance()->log("[ Panorama.cpp ] - Phase 6: Multi-band blending", PHOTON_DEBUG); - // Calculate final panorama size cv::Rect dst_roi = cv::detail::resultRoi(corners, sizes_warped); // Create multi-band blender with high number of bands for quality @@ -342,41 +320,36 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, int blend_width = std::min(dst_roi.width, dst_roi.height) / 8; int num_bands = static_cast( std::ceil(std::log(static_cast(blend_width)) / std::log(2.0))); + + // NOTE: + // It makes little difference, so I'm not going to cap it. // num_bands = std::min(num_bands, 8); // Cap at 8 for performance LogManager::instance()->log( QString("[ Panorama.cpp ] - Using %1 bands for blending").arg(num_bands), PHOTON_DEBUG); - // Create blender - use default CV_32F weight type cv::Ptr blender = cv::makePtr(false, num_bands); blender->prepare(corners, sizes_warped); float scale_factor = 2.0; -// 1. Feed images to blender using Scaled 16-bit Signed for (size_t i = 0; i < images_warped16.size(); i++) { cv::Mat img16S; - // We multiply by 1/scale_factor to allow the blend to sum up the highlights without clipping images_warped16[i].convertTo(img16S, CV_16SC3, 1/scale_factor); blender->feed(img16S, masks_binary[i], corners[i]); } - // 2. Blend cv::Mat result_16s, result_mask; blender->blend(result_16s, result_mask); - // 3. Convert back to 16-bit Unsigned cv::Mat result16; - // Now we multiply again bythe scale_factor to restore the original data. - // The more you scale the more you lose data. result_16s.convertTo(result16, CV_16UC3, scale_factor); cv::Mat resultRGB; cv::cvtColor(result16, resultRGB, cv::COLOR_BGR2RGB); - // Ensure continuous memory layout for TIFF writing if (!resultRGB.isContinuous()) resultRGB = resultRGB.clone(); if (resultRGB.empty()) { @@ -393,7 +366,6 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, PHOTON_DEBUG); - // PHASE 7: Save to DNG LogManager::instance()->log("[ Panorama.cpp ] - Phase 7: Saving to DNG", PHOTON_DEBUG); @@ -404,94 +376,83 @@ QVariantMap Panorama::stitchPhotos(const QStringList& inputFiles, LogManager::instance()->log( QString("[ Panorama.cpp ] - Saving panorama to %1").arg(filename), PHOTON_INFO); - - // ========================================== -// 1. GENERATE THUMBNAIL FIRST (before opening TIFF) -// ========================================== -int max_dim = 256; -double scale = (double)max_dim / std::max(result16.cols, result16.rows); -cv::Mat thumbnail16; -cv::resize(result16, thumbnail16, cv::Size(), scale, scale, cv::INTER_AREA); - -cv::Mat thumbFloat; -thumbnail16.convertTo(thumbFloat, CV_32FC3, 1.0 / 65535.0); -cv::pow(thumbFloat, 1.0 / 2.2, thumbFloat); - -cv::Mat thumbnail8; -thumbFloat.convertTo(thumbnail8, CV_8UC3, 255.0); - -// ========================================== -// 2. OPEN FILE AND WRITE IFD 0 = THUMBNAIL -// ========================================== -TIFF* out = TIFFOpen(filename.toStdString().c_str(), "w"); -if (!out) { - result["success"] = false; - result["message"] = "Could not open file for writing."; - return result; -} + int max_dim = 256; + double scale = (double)max_dim / std::max(result16.cols, result16.rows); + cv::Mat thumbnail16; + cv::resize(result16, thumbnail16, cv::Size(), scale, scale, cv::INTER_AREA); -// --- Shared DNG metadata on IFD 0 --- -TIFFSetField(out, TIFFTAG_MAKE, "Photon"); -TIFFSetField(out, TIFFTAG_MODEL, "Panorama Engine"); -TIFFSetField(out, TIFFTAG_UNIQUECAMERAMODEL, "Photon Panorama Engine"); - -static const uint8_t dng_ver[] = {1, 4, 0, 0}; -TIFFSetField(out, TIFFTAG_DNGVERSION, dng_ver); -TIFFSetField(out, TIFFTAG_DNGBACKWARDVERSION, dng_ver); - -// --- Thumbnail image fields --- -TIFFSetField(out, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE); // 0x1 -TIFFSetField(out, TIFFTAG_IMAGEWIDTH, thumbnail8.cols); -TIFFSetField(out, TIFFTAG_IMAGELENGTH, thumbnail8.rows); -TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 8); -TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 3); -TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); -TIFFSetField(out, TIFFTAG_COMPRESSION, COMPRESSION_JPEG); // JPEG is preferred by most viewers -TIFFSetField(out, TIFFTAG_JPEGQUALITY, 90); -TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); -TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); -TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, thumbnail8.rows); // single strip for thumbnail - -for (int row = 0; row < thumbnail8.rows; row++) { - uint8_t* rowPtr = thumbnail8.ptr(row); - TIFFWriteScanline(out, rowPtr, row, 0); -} + cv::Mat thumbFloat; + thumbnail16.convertTo(thumbFloat, CV_32FC3, 1.0 / 65535.0); + cv::pow(thumbFloat, 1.0 / 2.2, thumbFloat); -// ========================================== -// 3. WRITE IFD 1 = MAIN RAW IMAGE -// ========================================== -TIFFWriteDirectory(out); // seals IFD 0, advances to IFD 1 - -TIFFSetField(out, TIFFTAG_SUBFILETYPE, 0); // full-resolution image -TIFFSetField(out, TIFFTAG_IMAGEWIDTH, resultRGB.cols); -TIFFSetField(out, TIFFTAG_IMAGELENGTH, resultRGB.rows); -TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 3); -TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 16); -TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); -TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); -TIFFSetField(out, TIFFTAG_PHOTOMETRIC, 34892); // LINEARRAW -TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); -TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(out, 0)); - -uint32_t whiteLevel[3] = {65535, 65535, 65535}; -TIFFSetField(out, TIFFTAG_WHITELEVEL, 3, whiteLevel); -TIFFSetField(out, TIFFTAG_COLORMATRIX1, 9, colorInfo.get()->matrix); -TIFFSetField(out, TIFFTAG_ASSHOTNEUTRAL, 3, colorInfo.get()->asShotNeutral); -TIFFSetField(out, TIFFTAG_CALIBRATIONILLUMINANT1, 23); - -for (int row = 0; row < resultRGB.rows; row++) { - uint16_t* rowPtr = resultRGB.ptr(row); - if (TIFFWriteScanline(out, rowPtr, row, 0) < 0) { - TIFFClose(out); - result["success"] = false; - result["message"] = "Error writing scanline to DNG."; - return result; - } -} + cv::Mat thumbnail8; + thumbFloat.convertTo(thumbnail8, CV_8UC3, 255.0); -TIFFClose(out); - + TIFF* out = TIFFOpen(filename.toStdString().c_str(), "w"); + if (!out) { + result["success"] = false; + result["message"] = "Could not open file for writing."; + return result; + } + + // --- Shared DNG metadata on IFD 0 --- + TIFFSetField(out, TIFFTAG_MAKE, "Photon"); + TIFFSetField(out, TIFFTAG_MODEL, "Panorama Engine"); + TIFFSetField(out, TIFFTAG_UNIQUECAMERAMODEL, "Photon Panorama Engine"); + + static const uint8_t dng_ver[] = {1, 4, 0, 0}; + TIFFSetField(out, TIFFTAG_DNGVERSION, dng_ver); + TIFFSetField(out, TIFFTAG_DNGBACKWARDVERSION, dng_ver); + + // --- Thumbnail image fields --- + TIFFSetField(out, TIFFTAG_SUBFILETYPE, FILETYPE_REDUCEDIMAGE); // 0x1 + TIFFSetField(out, TIFFTAG_IMAGEWIDTH, thumbnail8.cols); + TIFFSetField(out, TIFFTAG_IMAGELENGTH, thumbnail8.rows); + TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 8); + TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 3); + TIFFSetField(out, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); + TIFFSetField(out, TIFFTAG_COMPRESSION, COMPRESSION_JPEG); // JPEG is preferred by most viewers + TIFFSetField(out, TIFFTAG_JPEGQUALITY, 90); + TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); + TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, thumbnail8.rows); // single strip for thumbnail + + for (int row = 0; row < thumbnail8.rows; row++) { + uint8_t* rowPtr = thumbnail8.ptr(row); + TIFFWriteScanline(out, rowPtr, row, 0); + } + + TIFFWriteDirectory(out); // seals IFD 0, advances to IFD 1 + + TIFFSetField(out, TIFFTAG_SUBFILETYPE, 0); // full-resolution image + TIFFSetField(out, TIFFTAG_IMAGEWIDTH, resultRGB.cols); + TIFFSetField(out, TIFFTAG_IMAGELENGTH, resultRGB.rows); + TIFFSetField(out, TIFFTAG_SAMPLESPERPIXEL, 3); + TIFFSetField(out, TIFFTAG_BITSPERSAMPLE, 16); + TIFFSetField(out, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); + TIFFSetField(out, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); + TIFFSetField(out, TIFFTAG_PHOTOMETRIC, 34892); // LINEARRAW + TIFFSetField(out, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_UINT); + TIFFSetField(out, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(out, 0)); + + uint32_t whiteLevel[3] = {65535, 65535, 65535}; + TIFFSetField(out, TIFFTAG_WHITELEVEL, 3, whiteLevel); + TIFFSetField(out, TIFFTAG_COLORMATRIX1, 9, colorInfo.get()->matrix); + TIFFSetField(out, TIFFTAG_ASSHOTNEUTRAL, 3, colorInfo.get()->asShotNeutral); + TIFFSetField(out, TIFFTAG_CALIBRATIONILLUMINANT1, 23); + + for (int row = 0; row < resultRGB.rows; row++) { + uint16_t* rowPtr = resultRGB.ptr(row); + if (TIFFWriteScanline(out, rowPtr, row, 0) < 0) { + TIFFClose(out); + result["success"] = false; + result["message"] = "Error writing scanline to DNG."; + return result; + } + } + + TIFFClose(out); LogManager::instance()->log( "[ Panorama.cpp ] - Panorama stitching completed successfully", PHOTON_INFO); From 8c3be10c1c21b209423b6927833a0f5c89ea6b19 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Sat, 4 Apr 2026 00:08:59 +0200 Subject: [PATCH 25/29] Added SIMD support for export and histogram update --- src/engine/ImageDeveloper.cpp | 102 +++++++++++++++++++++------- src/engine/RawEngine.cpp | 123 ++++++++++++++++++++++++++++------ 2 files changed, 180 insertions(+), 45 deletions(-) diff --git a/src/engine/ImageDeveloper.cpp b/src/engine/ImageDeveloper.cpp index 01d2bae..dccd05e 100644 --- a/src/engine/ImageDeveloper.cpp +++ b/src/engine/ImageDeveloper.cpp @@ -1,5 +1,7 @@ #include "ImageDeveloper.h" +#include +#include #include #include #include @@ -15,6 +17,10 @@ #include "Denoiser.h" #include "GpuSearcher.h" +#if defined(__SSE2__) || defined(_M_X64) || defined(_M_IX86_FP) +#include +#endif + namespace photon { // ... (rest of colorspace math) @@ -183,6 +189,17 @@ struct Vec3fCpp { float b; }; +static const std::array& srgb16_to_linear_lut_cpp() { + static const std::array lut = [] { + std::array v{}; + for (size_t i = 0; i < v.size(); ++i) { + v[i] = srgb_to_linear_f(static_cast(i) / 65535.0f); + } + return v; + }(); + return lut; +} + static float step_local(float edge, float x) { return x < edge ? 0.0f : 1.0f; } static float sign_local(float x) { @@ -197,8 +214,8 @@ static Vec3fCpp clamp_vec3_cpp(const Vec3fCpp& c, float lo, float hi) { } static Vec3fCpp sample_source_linear_bilinear_cpp(const ushort* src, int width, - int height, float u, - float v) { + int height, float u, float v, + const float* srgb16ToLinear) { u = std::clamp(u, 0.0f, 1.0f); v = std::clamp(v, 0.0f, 1.0f); @@ -211,36 +228,71 @@ static Vec3fCpp sample_source_linear_bilinear_cpp(const ushort* src, int width, const float tx = xf - float(x0); const float ty = yf - float(y0); - auto sample_texel = [src, width, height](int x, int y) -> Vec3fCpp { + auto sample_texel = [src, width, height, + srgb16ToLinear](int x, int y) -> Vec3fCpp { x = std::clamp(x, 0, width - 1); y = std::clamp(y, 0, height - 1); const int idx = (y * width + x) * 3; - return {srgb_to_linear_f(src[idx] / 65535.0f), - srgb_to_linear_f(src[idx + 1] / 65535.0f), - srgb_to_linear_f(src[idx + 2] / 65535.0f)}; + return {srgb16ToLinear[src[idx]], srgb16ToLinear[src[idx + 1]], + srgb16ToLinear[src[idx + 2]]}; }; +#if defined(__SSE2__) || defined(_M_X64) || defined(_M_IX86_FP) + auto sample_texel_sse = [src, width, height, + srgb16ToLinear](int x, int y) -> __m128 { + x = std::clamp(x, 0, width - 1); + y = std::clamp(y, 0, height - 1); + const int idx = (y * width + x) * 3; + return _mm_set_ps(0.0f, srgb16ToLinear[src[idx + 2]], + srgb16ToLinear[src[idx + 1]], srgb16ToLinear[src[idx]]); + }; + + const __m128 c00 = sample_texel_sse(x0, y0); + const __m128 c10 = sample_texel_sse(x1, y0); + const __m128 c01 = sample_texel_sse(x0, y1); + const __m128 c11 = sample_texel_sse(x1, y1); + + const float oneMinusTx = 1.0f - tx; + const float oneMinusTy = 1.0f - ty; + const float w00 = oneMinusTx * oneMinusTy; + const float w10 = tx * oneMinusTy; + const float w01 = oneMinusTx * ty; + const float w11 = tx * ty; + + __m128 out = _mm_setzero_ps(); + out = _mm_add_ps(out, _mm_mul_ps(c00, _mm_set1_ps(w00))); + out = _mm_add_ps(out, _mm_mul_ps(c10, _mm_set1_ps(w10))); + out = _mm_add_ps(out, _mm_mul_ps(c01, _mm_set1_ps(w01))); + out = _mm_add_ps(out, _mm_mul_ps(c11, _mm_set1_ps(w11))); + + float packed[4]; + _mm_storeu_ps(packed, out); + return {packed[0], packed[1], packed[2]}; +#else const Vec3fCpp c00 = sample_texel(x0, y0); const Vec3fCpp c10 = sample_texel(x1, y0); const Vec3fCpp c01 = sample_texel(x0, y1); const Vec3fCpp c11 = sample_texel(x1, y1); Vec3fCpp out{}; - out.r = - mix_local(mix_local(c00.r, c10.r, tx), mix_local(c01.r, c11.r, tx), ty); - out.g = - mix_local(mix_local(c00.g, c10.g, tx), mix_local(c01.g, c11.g, tx), ty); - out.b = - mix_local(mix_local(c00.b, c10.b, tx), mix_local(c01.b, c11.b, tx), ty); + out.r = mix_local(mix_local(c00.r, c10.r, tx), mix_local(c01.r, c11.r, tx), + ty); + out.g = mix_local(mix_local(c00.g, c10.g, tx), mix_local(c01.g, c11.g, tx), + ty); + out.b = mix_local(mix_local(c00.b, c10.b, tx), mix_local(c01.b, c11.b, tx), + ty); return out; +#endif } static Vec3fCpp compute_fine_blur_cpp(const ushort* src, int width, int height, - float u, float v) { + float u, float v, + const float* srgb16ToLinear) { Vec3fCpp blur{0.0f, 0.0f, 0.0f}; auto tap = [&](float dx, float dy, float w) { Vec3fCpp s = sample_source_linear_bilinear_cpp( - src, width, height, u + dx / float(width), v + dy / float(height)); + src, width, height, u + dx / float(width), v + dy / float(height), + srgb16ToLinear); blur.r += s.r * w; blur.g += s.g * w; blur.b += s.b * w; @@ -272,11 +324,13 @@ static Vec3fCpp compute_fine_blur_cpp(const ushort* src, int width, int height, } static Vec3fCpp compute_coarse_blur_cpp(const ushort* src, int width, - int height, float u, float v) { + int height, float u, float v, + const float* srgb16ToLinear) { Vec3fCpp blur{0.0f, 0.0f, 0.0f}; auto tap = [&](float dx, float dy, float w) { Vec3fCpp s = sample_source_linear_bilinear_cpp( - src, width, height, u + dx / float(width), v + dy / float(height)); + src, width, height, u + dx / float(width), v + dy / float(height), + srgb16ToLinear); blur.r += s.r * w; blur.g += s.g * w; blur.b += s.b * w; @@ -726,6 +780,7 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, float widths[8] = {35.0f / 360.0f, 45.0f / 360.0f, 40.0f / 360.0f, 90.0f / 360.0f, 60.0f / 360.0f, 60.0f / 360.0f, 55.0f / 360.0f, 50.0f / 360.0f}; + const auto& linearLut = srgb16_to_linear_lut_cpp(); QImage output(width, height, QImage::Format_RGB888); @@ -737,20 +792,17 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, uchar* scanline = output.scanLine(y); for (int x = 0; x < width; ++x) { int i = y * width + x; - float r = src[i * 3] / 65535.0f; - float g = src[i * 3 + 1] / 65535.0f; - float b = src[i * 3 + 2] / 65535.0f; - // 0. Initial sRGB to Linear (Since RawEngine develops with default gamma) - r = srgb_to_linear_f(r); - g = srgb_to_linear_f(g); - b = srgb_to_linear_f(b); + float r = linearLut[src[i * 3]]; + float g = linearLut[src[i * 3 + 1]]; + float b = linearLut[src[i * 3 + 2]]; const float u = (float(x) + 0.5f) / float(width); const float v = (float(y) + 0.5f) / float(height); - Vec3fCpp blurredFine = compute_fine_blur_cpp(src, width, height, u, v); + Vec3fCpp blurredFine = + compute_fine_blur_cpp(src, width, height, u, v, linearLut.data()); Vec3fCpp blurredCoarse = - compute_coarse_blur_cpp(src, width, height, u, v); + compute_coarse_blur_cpp(src, width, height, u, v, linearLut.data()); // 1. WB & Exposure r *= r_wb * exp_mult; diff --git a/src/engine/RawEngine.cpp b/src/engine/RawEngine.cpp index ad712ef..0d304de 100644 --- a/src/engine/RawEngine.cpp +++ b/src/engine/RawEngine.cpp @@ -16,6 +16,9 @@ #include #include #include +#if defined(__AVX2__) || defined(__SSE2__) || defined(_M_X64) || defined(_M_IX86_FP) +#include +#endif #include "../components/ToneLutProvider.h" #include "../managers/AppStateManager.h" @@ -159,10 +162,32 @@ static Vec3fHist clamp_vec3_hist(const Vec3fHist& c, float lo, float hi) { std::clamp(c.b, lo, hi)}; } -static float bilinear_channel_hist(const ushort* src, int width, int height, - float u, float v, int ch) { +#if defined(__SSE2__) || defined(_M_X64) || defined(_M_IX86_FP) +static inline __m128 load_rgb16_norm_hist_sse(const ushort* p) { + constexpr float invU16 = 1.0f / 65535.0f; + return _mm_mul_ps(_mm_set_ps(0.0f, float(p[2]), float(p[1]), float(p[0])), + _mm_set1_ps(invU16)); +} +#endif + +#if defined(__AVX2__) +static inline __m128 load_rgb16_norm_hist_avx2(const ushort* p) { + alignas(16) uint16_t lanes[8] = {p[0], p[1], p[2], 0, 0, 0, 0, 0}; + const __m128i packed16 = + _mm_load_si128(reinterpret_cast(lanes)); + const __m256i expanded32 = _mm256_cvtepu16_epi32(packed16); + const __m256 asFloat = _mm256_mul_ps(_mm256_cvtepi32_ps(expanded32), + _mm256_set1_ps(1.0f / 65535.0f)); + return _mm256_castps256_ps128(asFloat); +} +#endif + +static Vec3fHist sample_source_linear_bilinear_hist(const ushort* src, + int width, int height, + float u, float v) { u = std::clamp(u, 0.0f, 1.0f); v = std::clamp(v, 0.0f, 1.0f); + const float xf = u * float(width) - 0.5f; const float yf = v * float(height) - 0.5f; const int x0 = int(std::floor(xf)); @@ -171,32 +196,87 @@ static float bilinear_channel_hist(const ushort* src, int width, int height, const int y1 = y0 + 1; const float tx = xf - float(x0); const float ty = yf - float(y0); - auto sample = [src, width, height, ch](int x, int y) { + + auto sample_ptr = [src, width, height](int x, int y) { x = std::clamp(x, 0, width - 1); y = std::clamp(y, 0, height - 1); - return src[(y * width + x) * 3 + ch] / 65535.0f; + return src + (y * width + x) * 3; }; - const float c00 = sample(x0, y0); - const float c10 = sample(x1, y0); - const float c01 = sample(x0, y1); - const float c11 = sample(x1, y1); - return lerp(lerp(c00, c10, tx), lerp(c01, c11, tx), ty); -} -static Vec3fHist sample_source_linear_bilinear_hist(const ushort* src, - int width, int height, - float u, float v) { - return {bilinear_channel_hist(src, width, height, u, v, 0), - bilinear_channel_hist(src, width, height, u, v, 1), - bilinear_channel_hist(src, width, height, u, v, 2)}; + const ushort* p00 = sample_ptr(x0, y0); + const ushort* p10 = sample_ptr(x1, y0); + const ushort* p01 = sample_ptr(x0, y1); + const ushort* p11 = sample_ptr(x1, y1); + +#if defined(__AVX2__) + const __m128 c00 = load_rgb16_norm_hist_avx2(p00); + const __m128 c10 = load_rgb16_norm_hist_avx2(p10); + const __m128 c01 = load_rgb16_norm_hist_avx2(p01); + const __m128 c11 = load_rgb16_norm_hist_avx2(p11); + + const __m256 cTopBottom0 = + _mm256_insertf128_ps(_mm256_castps128_ps256(c00), c01, 1); + const __m256 cTopBottom1 = + _mm256_insertf128_ps(_mm256_castps128_ps256(c10), c11, 1); + const __m256 txV = _mm256_set1_ps(tx); + const __m256 oneMinusTxV = _mm256_set1_ps(1.0f - tx); + const __m256 horiz = + _mm256_add_ps(_mm256_mul_ps(cTopBottom0, oneMinusTxV), + _mm256_mul_ps(cTopBottom1, txV)); + + const __m256 top = _mm256_permute2f128_ps(horiz, horiz, 0x00); + const __m256 bottom = _mm256_permute2f128_ps(horiz, horiz, 0x11); + const __m256 out = _mm256_add_ps(_mm256_mul_ps(top, _mm256_set1_ps(1.0f - ty)), + _mm256_mul_ps(bottom, _mm256_set1_ps(ty))); + + alignas(16) float packed[4]; + _mm_store_ps(packed, _mm256_castps256_ps128(out)); + return {packed[0], packed[1], packed[2]}; +#elif defined(__SSE2__) || defined(_M_X64) || defined(_M_IX86_FP) + const __m128 c00 = load_rgb16_norm_hist_sse(p00); + const __m128 c10 = load_rgb16_norm_hist_sse(p10); + const __m128 c01 = load_rgb16_norm_hist_sse(p01); + const __m128 c11 = load_rgb16_norm_hist_sse(p11); + + const __m128 txV = _mm_set1_ps(tx); + const __m128 oneMinusTxV = _mm_set1_ps(1.0f - tx); + const __m128 top = + _mm_add_ps(_mm_mul_ps(c00, oneMinusTxV), _mm_mul_ps(c10, txV)); + const __m128 bottom = + _mm_add_ps(_mm_mul_ps(c01, oneMinusTxV), _mm_mul_ps(c11, txV)); + const __m128 out = _mm_add_ps(_mm_mul_ps(top, _mm_set1_ps(1.0f - ty)), + _mm_mul_ps(bottom, _mm_set1_ps(ty))); + + alignas(16) float packed[4]; + _mm_store_ps(packed, out); + return {packed[0], packed[1], packed[2]}; +#else + constexpr float invU16 = 1.0f / 65535.0f; + const Vec3fHist c00{p00[0] * invU16, p00[1] * invU16, p00[2] * invU16}; + const Vec3fHist c10{p10[0] * invU16, p10[1] * invU16, p10[2] * invU16}; + const Vec3fHist c01{p01[0] * invU16, p01[1] * invU16, p01[2] * invU16}; + const Vec3fHist c11{p11[0] * invU16, p11[1] * invU16, p11[2] * invU16}; + + const float topR = lerp(c00.r, c10.r, tx); + const float topG = lerp(c00.g, c10.g, tx); + const float topB = lerp(c00.b, c10.b, tx); + const float bottomR = lerp(c01.r, c11.r, tx); + const float bottomG = lerp(c01.g, c11.g, tx); + const float bottomB = lerp(c01.b, c11.b, tx); + return {lerp(topR, bottomR, ty), lerp(topG, bottomG, ty), + lerp(topB, bottomB, ty)}; +#endif } static Vec3fHist compute_fine_blur_hist(const ushort* src, int width, int height, float u, float v) { Vec3fHist blur{0.0f, 0.0f, 0.0f}; + const float invW = 1.0f / float(width); + const float invH = 1.0f / float(height); auto tap = [&](float dx, float dy, float w) { - Vec3fHist s = sample_source_linear_bilinear_hist( - src, width, height, u + dx / float(width), v + dy / float(height)); + Vec3fHist s = sample_source_linear_bilinear_hist(src, width, height, + u + dx * invW, + v + dy * invH); blur.r += s.r * w; blur.g += s.g * w; blur.b += s.b * w; @@ -229,9 +309,12 @@ static Vec3fHist compute_fine_blur_hist(const ushort* src, int width, static Vec3fHist compute_coarse_blur_hist(const ushort* src, int width, int height, float u, float v) { Vec3fHist blur{0.0f, 0.0f, 0.0f}; + const float invW = 1.0f / float(width); + const float invH = 1.0f / float(height); auto tap = [&](float dx, float dy, float w) { - Vec3fHist s = sample_source_linear_bilinear_hist( - src, width, height, u + dx / float(width), v + dy / float(height)); + Vec3fHist s = sample_source_linear_bilinear_hist(src, width, height, + u + dx * invW, + v + dy * invH); blur.r += s.r * w; blur.g += s.g * w; blur.b += s.b * w; From f73864cdd9ee8a771b9b50626e066840a82b7a0b Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Wed, 8 Apr 2026 23:59:45 +0200 Subject: [PATCH 26/29] readme --- README.md | 70 +++++++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 1eea799..5493a7d 100644 --- a/README.md +++ b/README.md @@ -6,44 +6,42 @@ ## What is this and who is this for? -Photon is an open source RAW image editor, born to (_try to_) replace basic Adobe Lightroom® functionalities, focused on ease of use and performance. -I'm an occasional photographer and lately I've been using the Lightroom mobile version to edit my photos as it is free and has all the features I want, except for panorama stitching, so I thought it was a good idea to attempt to create something to fit my needs and be finally free from Adobe. +Photon is an open source RAW image editor, focused on ease of use and performance. +I'm an occasional photographer and lately I've been using the mobile version of Lightroom to edit my photos as it is free and has all the features I need, but unfortunately it does not run on Linux. -If you are looking for a modern quick photo editor, this might be for you. On the other hand, if you want advanced AI features, local adjustment and so on, either pay for Lightroom or try [RapidRaw](https://github.com/CyberTimon/RapidRAW), which looks very promising. +If you are looking for a modern quick photo editor, this might be for you. On the other hand, if you want advanced AI features, local adjustments and so on, either pay for Lightroom or try [RapidRaw](https://github.com/CyberTimon/RapidRAW), which looks very promising. ## Current state -Not having much experience with both Qt6 and how images are processed, I used both Gemini and Copilot to kickstart the project, especially to implement what could have taken months and months of full time work, which I cannot afford right now. - -The project is in an advanced state and most of the functionalities listed below as completed work good enough for me, so I decided to step back from automatic programming/vibe coding/whatever and start to implement and refine what's missing manually, to both asses the code quality produced up until now (I would be a liar if I say that I diligently reviewed all the AI output...) and to actually keep my skills sharp in these funny times. - -Here's what works and what is still in the backlog: - -| Feature | Status | -| :------------------------------------------ | :----: | -| RAW Decoding (LibRaw) | ✅ | -| GPU-Accelerated Rendering (Vulkan/RHI) | ✅ | -| Non-Destructive Editing (JSON Sidecars) | ✅ | -| Exposure & Contrast | ✅ | -| Vibrance & Saturation | ✅ | -| 8-Band HSL Adjustments | ✅ | -| Color Grading (Shadows/Midtones/Highlights) | ✅ | -| Film Grain & Vignette | ✅ | -| Live Histogram (RGB/Luma) | ✅ | -| Undo/Redo History | ✅ | -| Preset System | ✅ | -| EXIF Metadata & Orientation | ✅ | -| Hybrid Denoising (BM3D + GPU NLM) | ✅ | -| Interactive Viewport (Pan & Zoom) | ✅ | -| Image Export (JPEG/TIFF) | ✅ | -| Theme Customization (Light/Dark/Accents) | ✅ | -| Crop & Transform Tools | ✅ | -| Tone Curve (Spline UI) | ✅ | -| Batch Copy & Paste | ✅ | -| Lens Correction (Lensfun) | 🔁 | -| Panorama Stitching | 🔁 | -| HDR merge | 🔁 | -| Import/Export presetes | 🔁 | +Not having much experience with both Qt6 and how RAW images work, I used both Gemini and Copilot to kickstart the project, shrinking down months of full time research and work. + +> [!NOTE] +> From the first day of development I wanted to implement things as fast as possible to get a working application and start editing my photos on Linux, for this reason I skipped chores and code hygiene practices, but now I will slow down to clean up the project and fix all the little things and inconsistencies that annoy me. + +Features: + +- [x] RAW Decoding (LibRaw) +- [x] GPU-Accelerated Rendering (Vulkan/RHI) +- [x] Non-Destructive Editing (JSON Sidecars) +- [x] Exposure & Contrast +- [x] Vibrance & Saturation +- [x] 8-Band HSL Adjustments +- [x] Color Grading (Shadows/Midtones/Highlights) +- [x] Film Grain & Vignette +- [x] Live Histogram (RGB/Luma) +- [x] Undo/Redo History +- [x] Preset System +- [x] EXIF Metadata & Orientation +- [x] Hybrid Denoising (BM3D + GPU NLM) +- [x] Image Export (JPEG/TIFF) +- [x] Theme Customization (Light/Dark/Accents) +- [x] Crop & Transform Tools +- [x] Tone Curve (Spline UI) +- [x] Batch Copy & Paste +- [x] Panorama Stitching +- [ ] Lens Correction (Lensfun) +- [ ] HDR merge +- [ ] Import/Export presetes ## Getting Started @@ -77,5 +75,5 @@ make -j$(nproc) ## Why another editing tool? I've always used Lightroom to edit my photos and I never found a valid alternative: tools like Rawtherapee and Darktable are for sure very capable and powerful, but I find them unnecessary complex to perform simple edits. -Searching for alternatives on GitHub I found RapidRaw, a very promising editor with a stunning UI and some very powerful capabilities. I give it a shot and I really liked it, especially the UX that allowed me to quickly edit my last shooting session. However, while the editing workflow is exceptional, I found the performance disappointing, even on a laptop with a dedicated GPU: the preview takes a lot of time to render, the adjustment are applied slowly and the overall experience is laggy. -For these reasons I decided to start this journey, choosing to use QT6, which I think it's a better tool for implementing an high performance photo editor. +Searching for alternatives on GitHub I found RapidRaw, a very promising editor with a stunning UI and some very powerful capabilities. I gave it a shot and I really liked it, especially the UX that allowed me to quickly edit my last shooting session. However, while the editing workflow is exceptional, I found the performance disappointing, even on a laptop with a dedicated GPU: the preview takes a lot of time to render, the adjustment are applied slowly and the overall experience is laggy. +For these reasons I decided to start this journey, choosing to use QT6 and C++, which I think it's a better tool for implementing an high performance photo editor. From d1097bcc340afec1de358533b88748269cd1d5c2 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Thu, 9 Apr 2026 11:11:18 +0200 Subject: [PATCH 27/29] minor fixes --- content/views/App.qml | 86 +++++++++++++++++++++++++++++++----- src/engine/RawEngine.cpp | 4 +- tests/auto/tst_RawEngine.cpp | 26 +++++++++++ 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/content/views/App.qml b/content/views/App.qml index 7c28b3b..1e22dfb 100644 --- a/content/views/App.qml +++ b/content/views/App.qml @@ -23,6 +23,7 @@ Window { property int ratingFilter: 0 property int ratingOperator: 2 readonly property var ratingOperatorLabels: ["=", ">", "≥", "<", "≤"] + property int filmstripRestoreGeneration: 0 property var copiedSettings: ({}) property string contextMenuSourcePath: "" PhotonToastManager { id: toaster } @@ -87,15 +88,38 @@ Window { // Function to refresh the file list function refreshFiles() { - rawFilesModel.clear(); - + var previousFilmstripX = filmstripList ? filmstripList.contentX : 0 // Scan for RAW files in the current folder var files = fileScanner.scanForRawFiles(AppState.currentFolder); - if (!files) return; + if (!files) { + rawFilesModel.clear(); + return; + } + + // Filter to mirror Library view behavior + var filtered = []; + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var r = file.rating || 0; + var match = true; + if (window.ratingFilter > 0) { + switch (window.ratingOperator) { + case 0: match = (r === window.ratingFilter); break; + case 1: match = (r > window.ratingFilter); break; + case 2: match = (r >= window.ratingFilter); break; + case 3: match = (r < window.ratingFilter); break; + case 4: match = (r <= window.ratingFilter); break; + } + } else if (window.ratingFilter === 0 && window.ratingOperator === 0) { + match = (r === 0); + } + + if (match) filtered.push(file); + } // Sort to match library view order var dir = window.sortAscending ? 1 : -1; - files.sort(function(a, b) { + filtered.sort(function(a, b) { switch (window.sortProperty) { case 0: return dir * a.name.localeCompare(b.name); case 1: @@ -107,8 +131,34 @@ Window { } }); - for (var i = 0; i < files.length; i++) { - var file = files[i]; + // Fast path: keep model (and filmstrip scroll) stable when ordering doesn't change. + var sameOrder = rawFilesModel.count === filtered.length; + if (sameOrder) { + for (var k = 0; k < filtered.length; k++) { + if (rawFilesModel.get(k).path !== filtered[k].path) { + sameOrder = false; + break; + } + } + } + if (sameOrder) { + for (var m = 0; m < filtered.length; m++) { + var current = rawFilesModel.get(m); + var updated = filtered[m]; + var updatedRating = updated.rating || 0; + if (current.name !== updated.name) rawFilesModel.setProperty(m, "name", updated.name); + if (current.size !== updated.size) rawFilesModel.setProperty(m, "size", updated.size); + if (current.modified !== updated.modified) rawFilesModel.setProperty(m, "modified", updated.modified); + if (current.rating !== updatedRating) rawFilesModel.setProperty(m, "rating", updatedRating); + } + return; + } + + var restoreGeneration = ++window.filmstripRestoreGeneration; + rawFilesModel.clear(); + + for (var j = 0; j < filtered.length; j++) { + var file = filtered[j]; rawFilesModel.append({ "path": file.path, "name": file.name, @@ -120,6 +170,23 @@ Window { // Pre-generate thumbnails thumbnailProvider.generateThumbnailAsync(file.path); } + + if (filmstripList) { + Qt.callLater(function() { + if (restoreGeneration !== window.filmstripRestoreGeneration) + return; + var maxContentX = Math.max(0, filmstripList.contentWidth - filmstripList.width); + filmstripList.contentX = Math.max(0, Math.min(previousFilmstripX, maxContentX)); + + // Apply once more on the next cycle to override delayed ListView relayouts. + Qt.callLater(function() { + if (restoreGeneration !== window.filmstripRestoreGeneration) + return; + var maxContentX2 = Math.max(0, filmstripList.contentWidth - filmstripList.width); + filmstripList.contentX = Math.max(0, Math.min(previousFilmstripX, maxContentX2)); + }); + }); + } } // Function to get all file paths in the model @@ -257,6 +324,8 @@ Window { onSortPropertyChanged: refreshFiles() onSortAscendingChanged: refreshFiles() + onRatingFilterChanged: refreshFiles() + onRatingOperatorChanged: refreshFiles() // Global keyboard shortcuts for rating and navigation Item { @@ -306,8 +375,6 @@ Window { Item { anchors.fill: parent - // Hover area to show topbar in Develop view (if we want it there, but currently topbar is hidden in Develop) - // For now, we disable the hover functionality as requested for Library/Settings. MouseArea { id: topbarHoverArea anchors.top: parent.top @@ -315,9 +382,6 @@ Window { width: viewportContainer.width height: 100 hoverEnabled: true - // Only enabled in Develop view if we want hover-to-show there, - // but the topbar is explicitly hidden in Develop view (visible: ... check). - // So we disable this entirely for now to follow the "removing hover functionality" request. enabled: false onEntered: window.showTopbar = true onExited: { diff --git a/src/engine/RawEngine.cpp b/src/engine/RawEngine.cpp index 0d304de..12f04d5 100644 --- a/src/engine/RawEngine.cpp +++ b/src/engine/RawEngine.cpp @@ -736,8 +736,8 @@ void RawEngine::setSource(const QString& source) { m_histogramUpdatePending = false; m_metadata.clear(); m_orientation = 1; - m_exposure = 0.0f; - m_contrast = 1.0f; + setExposure(0.0f); + setContrast(1.0f); m_hasDenoisedResult = false; // Clear geometry bake state diff --git a/tests/auto/tst_RawEngine.cpp b/tests/auto/tst_RawEngine.cpp index e428d6f..16146f0 100644 --- a/tests/auto/tst_RawEngine.cpp +++ b/tests/auto/tst_RawEngine.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -11,6 +12,7 @@ class TestRawEngine : public QObject { void testLoadInvalidFile(); void testLoadValidFile(); void testProperties(); + void testSwitchingSourceResetsExposureAndContrast(); void testApplyGeometryTransformsStraightenKeepsFullFrame(); void testApplyGeometryTransformsCropRectOnRotatedFrame(); void testApplyGeometryTransformsCropPreservesAspectAndFocus(); @@ -64,6 +66,30 @@ void TestRawEngine::testProperties() { QCOMPARE(vignetteSpy.count(), 1); } +void TestRawEngine::testSwitchingSourceResetsExposureAndContrast() { + QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); + + RawEngine engine; + QSignalSpy exposureSpy(&engine, &RawEngine::exposureChanged); + QSignalSpy contrastSpy(&engine, &RawEngine::contrastChanged); + + engine.setSource(tempDir.filePath("first.arw")); + engine.setExposure(1.5f); + engine.setContrast(1.3f); + QCOMPARE(engine.exposure(), 1.5f); + QCOMPARE(engine.contrast(), 1.3f); + + const int exposureSignalsBeforeSwitch = exposureSpy.count(); + const int contrastSignalsBeforeSwitch = contrastSpy.count(); + + engine.setSource(tempDir.filePath("second.arw")); + QCOMPARE(engine.exposure(), 0.0f); + QCOMPARE(engine.contrast(), 1.0f); + QVERIFY(exposureSpy.count() > exposureSignalsBeforeSwitch); + QVERIFY(contrastSpy.count() > contrastSignalsBeforeSwitch); +} + void TestRawEngine::testApplyGeometryTransformsStraightenKeepsFullFrame() { QImage input(200, 100, QImage::Format_RGBA64); input.fill(Qt::black); From 653282a8d30c33eb3d67957f06792c5792b89d05 Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Thu, 9 Apr 2026 12:17:21 +0200 Subject: [PATCH 28/29] minor fixes --- src/components/RawViewport.frag | 4 ++-- src/engine/ImageDeveloper.cpp | 4 ++-- src/engine/RawEngine.cpp | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/RawViewport.frag b/src/components/RawViewport.frag index 92765fc..f60fb76 100644 --- a/src/components/RawViewport.frag +++ b/src/components/RawViewport.frag @@ -531,7 +531,7 @@ float pv_tent_weight(float value, float center, float halfWidth) { return max(1.0 - abs(value - center) / max(halfWidth, PV_EPS), 0.0); } -vec3 apply_pv2012_tone_ranges( +vec3 apply_photon0001_tone_ranges( vec3 color, vec3 blurredFine, vec3 blurredCoarse, @@ -881,7 +881,7 @@ void main() float sceneWhiteNorm = max(ubuf.sceneWhite * exposure, 1e-4); vec3 blurredFineTone = apply_white_balance(blurredFine, ubuf.temperature / 100.0, ubuf.tint / 100.0) * exposure; vec3 blurredCoarseTone = apply_white_balance(blurredCoarse, ubuf.temperature / 100.0, ubuf.tint / 100.0) * exposure; - color = apply_pv2012_tone_ranges( + color = apply_photon0001_tone_ranges( color, blurredFineTone, blurredCoarseTone, diff --git a/src/engine/ImageDeveloper.cpp b/src/engine/ImageDeveloper.cpp index dccd05e..81f2242 100644 --- a/src/engine/ImageDeveloper.cpp +++ b/src/engine/ImageDeveloper.cpp @@ -433,7 +433,7 @@ static float pv_tent_weight_cpp(float value, float center, float halfWidth) { 1.0f - std::abs(value - center) / std::max(halfWidth, PV_EPS_CPP), 0.0f); } -static Vec3fCpp apply_pv2012_tone_ranges_cpp( +static Vec3fCpp apply_photon0001_tone_ranges_cpp( const Vec3fCpp& color, const Vec3fCpp& blurredFine, const Vec3fCpp& blurredCoarse, float highlightsAmt, float shadowsAmt, float whitesAmt, float blacksAmt, float clarityAmt, float sceneWhiteNorm) { @@ -828,7 +828,7 @@ QImage ImageDeveloper::develop(const ushort* src, int width, int height, blurredCoarse.g * g_wb * exp_mult, blurredCoarse.b * b_wb * exp_mult}; const float sceneWhiteNorm = std::max(sceneWhite * exp_mult, 1e-4f); - color = apply_pv2012_tone_ranges_cpp( + color = apply_photon0001_tone_ranges_cpp( color, blurredFineTone, blurredCoarseTone, high / 100.0f, shad / 100.0f, whites / 100.0f, blacks / 100.0f, clarity / 100.0f, sceneWhiteNorm); diff --git a/src/engine/RawEngine.cpp b/src/engine/RawEngine.cpp index 12f04d5..4a936de 100644 --- a/src/engine/RawEngine.cpp +++ b/src/engine/RawEngine.cpp @@ -414,7 +414,7 @@ static float pv_tent_weight_hist(float value, float center, float halfWidth) { 1.0f - std::abs(value - center) / std::max(halfWidth, PV_EPS_HIST), 0.0f); } -static Vec3fHist apply_pv2012_tone_ranges_hist( +static Vec3fHist apply_photon0001_tone_ranges_hist( const Vec3fHist& color, const Vec3fHist& blurredFine, const Vec3fHist& blurredCoarse, float highlightsAmt, float shadowsAmt, float whitesAmt, float blacksAmt, float clarityAmt, float sceneWhiteNorm) { @@ -1885,7 +1885,7 @@ void RawEngine::requestHistogramUpdate() { blurredCoarse.g * g_wb * exp_mult, blurredCoarse.b * b_wb * exp_mult}; const float sceneWhiteNorm = std::max(sceneWhite * exp_mult, 1e-4f); - color = apply_pv2012_tone_ranges_hist( + color = apply_photon0001_tone_ranges_hist( color, blurredFineTone, blurredCoarseTone, high / 100.0f, shad / 100.0f, whites / 100.0f, blacks / 100.0f, clarity / 100.0f, sceneWhiteNorm); From ec2224cdbca1d8a98ffd0e067c4f4482b76f304a Mon Sep 17 00:00:00 2001 From: Skiby7 Date: Thu, 9 Apr 2026 12:25:02 +0200 Subject: [PATCH 29/29] minor fixes --- src/engine/RawEngine.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/RawEngine.h b/src/engine/RawEngine.h index acc4e50..75ac1fe 100644 --- a/src/engine/RawEngine.h +++ b/src/engine/RawEngine.h @@ -760,7 +760,7 @@ class RawEngine : public QObject { int m_denoisedWidth = 0; int m_denoisedHeight = 0; QRectF m_denoisedRoi{0, 0, 1, 1}; - mutable QRecursiveMutex m_processorMutex; + mutable QMutex m_processorMutex; std::atomic m_abortDenoise{false}; std::atomic m_currentLoadId{0}; bool m_hasDenoisedResult = false;