diff --git a/src/doc/builtinplugins.rst b/src/doc/builtinplugins.rst index d77ee5c652..a38b7422a6 100644 --- a/src/doc/builtinplugins.rst +++ b/src/doc/builtinplugins.rst @@ -522,10 +522,33 @@ found at: http://fits.gsfc.nasa.gov/ OpenImageIO supports multiple images in FITS files, and supports the following pixel data types: UINT8, UINT16, UINT32, FLOAT, DOUBLE. -FITS files can store various kinds of arbitrary data arrays, but -OpenImageIO's support of FITS is mostly limited using FITS for image -storage. Currently, OpenImageIO only supports 2D FITS data (images), not 3D -(volume) data, nor 1-D or higher-dimensional arrays. +**Inferring dimensions and color channels** + +FITS files can store arbitrary N-dimensional data arrays (``NAXIS`` / +``NAXISn``), but say nothing about which axes, if any, are "color." +OpenImageIO infers a role for each axis from ``NAXIS`` (trailing axes of +length 1 are dropped first): + +* ``NAXIS`` = 1: a single row (width = NAXIS1, height = 1). +* ``NAXIS`` = 2: an ordinary 2D grayscale image (width = NAXIS1, height = NAXIS2). +* ``NAXIS`` = 3: if NAXIS3 <= 4, a color image, stored as one full-resolution + plane per channel (width = NAXIS1, height = NAXIS2, nchannels = NAXIS3). + Otherwise, if NAXIS3 is more than 4, the file is interpreted as a grayscale + volume (width = NAXIS1, height = NAXIS2, depth = NAXIS3). +* ``NAXIS`` = 4: a color volume (width = NAXIS1, height = NAXIS2, depth = + NAXIS3, nchannels = NAXIS4, again one full-resolution volume per channel). + +This is only a heuristic -- FITS has no way to formally declare an axis as +"color" -- so a genuinely non-color NAXIS3 that happens to be <= 4 long can be +misclassified as color. + +Channel names default to the usual OpenImageIO conventions (``R,G,B``, +``R,G,B,A``, ``Y``, etc.), improved when the header says more: if the +channel axis is a FITS WCS ``STOKES`` axis, channels are named from the +standard Stokes codes (``I``, ``Q``, ``U``, ``V``, ``RR``, ``LL``, ``RL``, +``LR``, ``XX``, ``YY``, ``XY``, ``YX``); otherwise, if every plane has a +``FILTERn`` or ``BANDn`` keyword (an informal convention, not part of the +FITS standard), those values are used instead. diff --git a/src/fits.imageio/fits_pvt.h b/src/fits.imageio/fits_pvt.h index aa27bbbcf5..5dcfc30e0f 100644 --- a/src/fits.imageio/fits_pvt.h +++ b/src/fits.imageio/fits_pvt.h @@ -114,6 +114,22 @@ class FitsInput final : public ImageInput { // converts date in FITS format (YYYY-MM-DD or DD/MM/YY) // to DateTime format std::string convert_date(const std::string& date); + + // Give m_spec.channelnames sensible values: generic defaults (R,G,B,...), + // improved with real per-plane names when the header tells us what the + // channel axis actually is (a STOKES polarization axis, or informal + // FILTERn/BANDn keywords). + void assign_channel_names(); + + // True if multi-channel pixel data is stored as separate per-channel + // blocks -- one full width * height (or width * height * depth for + // NAXIS=4) block per channel -- rather than interleaved. This is the only + // multi-channel layout we recognize; see the comments in + // read_fits_header(). + bool planar_channels() const + { + return (m_naxes == 3 || m_naxes == 4) && m_spec.nchannels > 1; + } }; diff --git a/src/fits.imageio/fitsinput.cpp b/src/fits.imageio/fitsinput.cpp index e09e46aa40..c60c003eb6 100644 --- a/src/fits.imageio/fitsinput.cpp +++ b/src/fits.imageio/fitsinput.cpp @@ -4,11 +4,13 @@ #include +#include #include #include "fits_pvt.h" #include +#include OIIO_PLUGIN_NAMESPACE_BEGIN @@ -88,7 +90,7 @@ FitsInput::open(const std::string& name, ImageSpec& spec) bool -FitsInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/, +FitsInput::read_native_scanline(int subimage, int miplevel, int y, int z, void* data) { lock_guard lock(*this); @@ -100,16 +102,49 @@ FitsInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/, return true; std::vector data_tmp(m_spec.scanline_bytes()); - long scanline_off = (m_spec.height - y) * m_spec.scanline_bytes(); - fseek(m_fd, scanline_off, SEEK_CUR); - size_t n = fread(&data_tmp[0], 1, m_spec.scanline_bytes(), m_fd); - if (n != m_spec.scanline_bytes()) { - if (feof(m_fd)) - errorfmt("Hit end of file unexpectedly (offset={}, scanline {})", - ftell(m_fd), y); - else - errorfmt("read error"); - return false; // Read failed + + if (!planar_channels()) { + size_t scanline_off = (size_t(z) * m_spec.height + (m_spec.height - y)) + * size_t(m_spec.scanline_bytes()); + fseek(m_fd, scanline_off, SEEK_CUR); + size_t n = fread(&data_tmp[0], 1, m_spec.scanline_bytes(), m_fd); + if (n != m_spec.scanline_bytes()) { + if (feof(m_fd)) + errorfmt("Hit end of file unexpectedly (offset={}, scanline {})", + ftell(m_fd), y); + else + errorfmt("read error"); + return false; // Read failed + } + } else { + // Channels are stored as separate contiguous width * height (or + // width * height * depth for NAXIS=4) blocks rather than interleaved, + // so we must gather one row from each block and interleave them into + // the scanline buffer. + size_t comp_size = m_spec.format.size(); + size_t row_bytes = size_t(m_spec.width) * comp_size; + size_t plane_bytes = row_bytes * size_t(m_spec.height) + * size_t(m_spec.depth); + size_t row_off = (size_t(z) * m_spec.height + (m_spec.height - y)) + * row_bytes; + std::vector chan_row(row_bytes); + for (int c = 0; c < m_spec.nchannels; ++c) { + fsetpos(m_fd, &m_filepos); + fseek(m_fd, size_t(c * plane_bytes) + row_off, SEEK_CUR); + size_t n = fread(&chan_row[0], 1, row_bytes, m_fd); + if (n != row_bytes) { + if (feof(m_fd)) + errorfmt( + "Hit end of file unexpectedly (offset={}, scanline {})", + ftell(m_fd), y); + else + errorfmt("read error"); + return false; // Read failed + } + for (int x = 0; x < m_spec.width; ++x) + memcpy(&data_tmp[(size_t(x) * m_spec.nchannels + c) * comp_size], + &chan_row[size_t(x) * comp_size], comp_size); + } } // in FITS image data is stored in big-endian so we have to switch to @@ -199,7 +234,7 @@ FitsInput::set_spec_info() errorfmt("Unsupported FITS BITPIX value {}", m_bitpix); return false; } - if (!check_open(m_spec, { 0, 1 << 20, 0, 1 << 20, 0, 1 << 16, 0, 4 })) + if (!check_open(m_spec, { 0, 1 << 20, 0, 1 << 20, 0, 1 << 16, 0, 12 })) return false; if (!check_compression_ratio(m_spec, Filesystem::file_size(m_filename))) return false; @@ -344,22 +379,29 @@ FitsInput::read_fits_header(void) } else if (m_naxes == 2) { m_spec.width = m_naxis[0]; m_spec.height = m_naxis[1]; - } else if (m_naxes == 3 && m_naxis[0] <= 4) { - // 3D, small number of most-rapidly changing dimension: color image? - m_spec.nchannels = m_naxis[0]; - m_spec.width = m_naxis[1]; - m_spec.height = m_naxis[2]; + } else if (m_naxes == 3 && m_naxis[2] <= 4) { + // 3D, small number of most-slowly changing dimension: color + // image, one full width x height plane per channel (NAXIS3 = + // nchannels). This is the only real-world FITS color-cube + // convention -- see the git history for how we know that. + m_spec.width = m_naxis[0]; + m_spec.height = m_naxis[1]; + m_spec.nchannels = m_naxis[2]; } else if (m_naxes == 3) { - // 3D, large number of most-rapidly changing dimension: volume? + // 3D, no small axis: volume m_spec.width = m_naxis[0]; m_spec.height = m_naxis[1]; m_spec.depth = m_naxis[2]; } else if (m_naxes == 4) { - // 4D... volume + color? - m_spec.nchannels = m_naxis[0]; - m_spec.width = m_naxis[1]; - m_spec.height = m_naxis[2]; - m_spec.depth = m_naxis[3]; + // 4D volume+color: NAXIS1-3 are x,y,z and NAXIS4 = nchannels, + // one full-resolution volume per channel. No real-world + // convention to confirm this against (unlike the 3D color case + // above), but it's the natural extension of the same pattern: + // the slowest-varying axis is the channel axis. + m_spec.width = m_naxis[0]; + m_spec.height = m_naxis[1]; + m_spec.depth = m_naxis[2]; + m_spec.nchannels = m_naxis[3]; } else { errorfmt("Don't know now to read {}-channel FITS image", m_naxes); return false; @@ -369,6 +411,7 @@ FitsInput::read_fits_header(void) m_spec.full_depth = m_spec.depth; m_spec.attribute("oiio:subimages", (int)m_subimages.size()); + assign_channel_names(); // if (m_spec.width < 1 || m_spec.height < 1 || m_spec.depth < 1 || // m_spec.nchannels < 1) { @@ -416,6 +459,88 @@ FitsInput::add_to_spec(const std::string& keyname, const std::string& value) +void +FitsInput::assign_channel_names() +{ + m_spec.default_channel_names(); + if (m_spec.nchannels <= 1) + return; + + // Which physical FITS axis (1-based) is the channel axis, so we know + // which CTYPEn/FILTERn-style keywords describe it? + int axis = 0; + if (m_naxes == 3) + axis = 3; + else if (m_naxes == 4) + axis = 4; // volume+color: channel is the slowest-varying axis + if (!axis) + return; + + // The FITS WCS standard defines a STOKES axis (polarization) with fixed + // integer codes per plane. If that's what this axis is, decode the + // per-plane values via the standard linear WCS mapping and use the Stokes + // names as channel names. + std::string ctype = m_spec.get_string_attribute( + Strutil::format("Ctype{}", axis)); + if (Strutil::iequals(ctype, "STOKES")) { + // Standard FITS WCS linear axis mapping and Stokes codes, see: + // https://fits.gsfc.nasa.gov/standard40/fits_standard40aa.pdf + static const std::map stokes_names + = { { 1, "I" }, { 2, "Q" }, { 3, "U" }, { 4, "V" }, + { -1, "RR" }, { -2, "LL" }, { -3, "RL" }, { -4, "LR" }, + { -5, "XX" }, { -6, "YY" }, { -7, "XY" }, { -8, "YX" } }; + float crval + = m_spec.get_float_attribute(Strutil::format("Crval{}", axis), + 1.0f); + float crpix + = m_spec.get_float_attribute(Strutil::format("Crpix{}", axis), + 1.0f); + float cdelt + = m_spec.get_float_attribute(Strutil::format("Cdelt{}", axis), + 1.0f); + bool all_found = true; + std::vector names(m_spec.nchannels); + for (int c = 0; c < m_spec.nchannels && all_found; ++c) { + int code = int(std::lround(crval + (c + 1 - crpix) * cdelt)); + auto find = stokes_names.find(code); + if (find == stokes_names.end()) + all_found = false; + else + names[c] = find->second; + } + if (all_found) { + for (int c = 0; c < m_spec.nchannels; ++c) + m_spec.channelnames[c] = names[c]; + return; + } + } + + // No standard WCS description of the axis. Some pipelines instead tag + // each plane with an informal FILTERn or BANDn keyword (e.g. FILTER1, + // FILTER2, FILTER3 giving the name of the filter used for that plane). + // This isn't part of the FITS standard, just a convention we've seen in + // the wild, so only trust it if every plane has one. + for (const char* prefix : { "Filter", "Band" }) { + std::vector names(m_spec.nchannels); + bool all_found = true; + for (int c = 0; c < m_spec.nchannels && all_found; ++c) { + std::string key = Strutil::format("{}{}", prefix, c + 1); + auto* param = m_spec.find_attribute(key, TypeDesc::STRING); + if (!param) + all_found = false; + else + names[c] = *(const char**)param->data(); + } + if (all_found) { + for (int c = 0; c < m_spec.nchannels; ++c) + m_spec.channelnames[c] = names[c]; + return; + } + } +} + + + void FitsInput::subimage_search() { diff --git a/src/fits.imageio/fitsoutput.cpp b/src/fits.imageio/fitsoutput.cpp index 9c74215b15..b7d9bc7c63 100644 --- a/src/fits.imageio/fitsoutput.cpp +++ b/src/fits.imageio/fitsoutput.cpp @@ -3,6 +3,7 @@ // https://github.com/AcademySoftwareFoundation/OpenImageIO #include +#include #include "fits_pvt.h" @@ -95,10 +96,6 @@ FitsOutput::write_scanline(int y, int /*z*/, TypeDesc format, const void* data, std::vector data_tmp(m_spec.scanline_bytes(), 0); memcpy(&data_tmp[0], data, m_spec.scanline_bytes()); - // computing scanline offset - long scanline_off = (m_spec.height - y) * m_spec.scanline_bytes(); - fseek(m_fd, scanline_off, SEEK_CUR); - // in FITS image data is stored in big-endian so we have to switch to // big-endian on little-endian machines if (littleendian()) { @@ -115,12 +112,37 @@ FitsOutput::write_scanline(int y, int /*z*/, TypeDesc format, const void* data, data_tmp.size() / sizeof(double)); } - size_t byte_count = fwrite(&data_tmp[0], 1, data_tmp.size(), m_fd); + if (m_spec.nchannels == 1) { + // computing scanline offset + long scanline_off = (m_spec.height - y) * m_spec.scanline_bytes(); + fseek(m_fd, scanline_off, SEEK_CUR); + size_t byte_count = fwrite(&data_tmp[0], 1, data_tmp.size(), m_fd); + fsetpos(m_fd, &m_filepos); + return byte_count == data_tmp.size(); + } + // Channels are stored as separate contiguous width x height planes + // (NAXIS3 = nchannels), the only real-world FITS color-cube convention, + // so de-interleave this scanline and write each channel's row into its + // own plane. + size_t comp_size = m_spec.format.size(); + size_t row_bytes = size_t(m_spec.width) * comp_size; + size_t plane_bytes = row_bytes * size_t(m_spec.height); + long row_off = (m_spec.height - y) * long(row_bytes); + std::vector chan_row(row_bytes); + bool ok = true; + for (int c = 0; c < m_spec.nchannels; ++c) { + for (int x = 0; x < m_spec.width; ++x) + memcpy(&chan_row[size_t(x) * comp_size], + &data_tmp[(size_t(x) * m_spec.nchannels + c) * comp_size], + comp_size); + fsetpos(m_fd, &m_filepos); + fseek(m_fd, long(c * plane_bytes) + row_off, SEEK_CUR); + size_t byte_count = fwrite(&chan_row[0], 1, row_bytes, m_fd); + ok &= (byte_count == row_bytes); + } fsetpos(m_fd, &m_filepos); - - //byte_count == data.size --> all written - return byte_count == data_tmp.size(); + return ok; } @@ -264,16 +286,16 @@ FitsOutput::create_basic_header(std::string& header) axes += 1; header += create_card("NAXIS", num2str(axes)); - // now we save NAXIS1 and NAXIS2 - // this keywords represents width and height + // now we save NAXIS1, NAXIS2 (and NAXIS3 for multi-channel images) if (m_spec.nchannels == 1) { header += create_card("NAXIS1", num2str(m_spec.width)); header += create_card("NAXIS2", num2str(m_spec.height)); } else { - // 3D image for color - header += create_card("NAXIS1", num2str(m_spec.nchannels)); - header += create_card("NAXIS2", num2str(m_spec.width)); - header += create_card("NAXIS3", num2str(m_spec.height)); + // One full-resolution plane per channel (NAXIS3 = nchannels), the + // only real-world FITS color-cube convention. + header += create_card("NAXIS1", num2str(m_spec.width)); + header += create_card("NAXIS2", num2str(m_spec.height)); + header += create_card("NAXIS3", num2str(m_spec.nchannels)); } } diff --git a/testsuite/fits/ref/out.txt b/testsuite/fits/ref/out.txt index d42768aeac..7184e01571 100644 --- a/testsuite/fits/ref/out.txt +++ b/testsuite/fits/ref/out.txt @@ -491,6 +491,37 @@ AIPS CLEAN NITER= 12000 PRODUCT=1" oiio:subimages: 1 Comparing "../fits-images/ftt4b/file003.fits" and "file003.fits" PASS +Reading ../fits-images/ftt4b/file006.fits +../fits-images/ftt4b/file006.fits : 512 x 512, 3 channel, uint8 fits + SHA-1: 9FF70B84DD1529A9404B0E80BDE7B334F691F90A + channel list: R, G, B + Blank: 0 + Bscale: 1 + Bzero: 0 + Cdelt3: 1 + Comment: "THIS IS A TEST OF THE 8-BIT PIXEL TYPE. +THIS FULL COLOR IMAGE WAS TAKEN FROM A TAPE OF TEST IMAGES +DISTRIBUTED BY THE IMAGE PROCESSING INSTITUTE AT THE UNIVERSITY OF +SOUTHERN CALIFORNIA. +I SUGGEST 'RGB' BE ADDED TO FITS COORDINATE TYPES. DON WELLS. 05SEP79. +THIS IS INTENDED TO BE USED FOR SPECIFYING TRIPLETS OF IMAGES WHICH +ARE INTENDED TO BE VIEWED WITH COLOR TELEVISION MONITORS OR RECORDED +WITH COLOR HARDCOPY DEVICES (E.G., DICOMED D47)." + Crota3: 0 + Crpix3: 1 + Crval3: 1 + Ctype3: "RGB" + DateTime: "1980:04:18 00:00:00" + Ipps-b/p: 12 + Ipps-id: "RED MANDRILL ("BABOON")" + Ipps-max: 255 + Ipps-min: 0 + Ipps-rf: "D /001" + Object: "MANDRILL" + Origin: "KPNO -- WFITS OF 04/17/80." + oiio:subimages: 1 +Comparing "../fits-images/ftt4b/file006.fits" and "file006.fits" +PASS Reading ../fits-images/ftt4b/file009.fits ../fits-images/ftt4b/file009.fits : 0 x 0, 1 channel, int16 fits SHA-1: DA39A3EE5E6B4B0D3255BFEF95601890AFD80709 @@ -712,6 +743,206 @@ AIPS CLEAN NITER= 12000 PRODUCT=1" oiio:subimages: 1 Comparing "../fits-images/ftt4b/file012.fits" and "file012.fits" PASS +Reading src/rgb.fits +src/rgb.fits : 32 x 32, 3 channel, float fits + SHA-1: F7E36E977DEA527FE023283D9A6E6941BA439AE1 + channel list: R, G, B + A_0_0: 0 + A_0_1: 0 + A_0_2: 6.08313e-08 + A_0_3: 3.51185e-11 + A_1_0: 0 + A_1_1: -7.16346e-09 + A_1_2: -6.52948e-10 + A_2_0: -8.57221e-08 + A_2_1: 6.14197e-13 + A_3_0: -6.39624e-10 + A_order: 3 + Ap_0_0: -0.000178267 + Ap_0_1: 2.44714e-07 + Ap_0_2: -6.08969e-08 + Ap_0_3: -3.54786e-11 + Ap_1_0: -8.42448e-06 + Ap_1_1: 7.38336e-09 + Ap_1_2: 6.61491e-10 + Ap_2_0: 8.6651e-08 + Ap_2_1: -7.33107e-13 + Ap_3_0: 6.46638e-10 + Ap_order: 3 + B_0_0: 0 + B_0_1: 0 + B_0_2: -8.27428e-08 + B_0_3: -6.11542e-10 + B_1_0: 0 + B_1_1: -9.90564e-08 + B_1_2: -1.68675e-11 + B_2_0: -1.88226e-08 + B_2_1: -6.48505e-10 + B_3_0: -2.02193e-12 + B_order: 3 + Bp_0_0: -0.000204257 + Bp_0_1: -5.3092e-06 + Bp_0_2: 8.32734e-08 + Bp_0_3: 6.16666e-10 + Bp_1_0: 1.05632e-08 + Bp_1_1: 9.96989e-08 + Bp_1_2: 1.68664e-11 + Bp_2_0: 1.90495e-08 + Bp_2_1: 6.55252e-10 + Bp_3_0: 2.05389e-12 + Bp_order: 3 + Bscale: 1 + Bzero: 0 + Cdelt1: -0.000776386 + Cdelt2: 0.000776814 + Comment: "FITS (Flexible Image Transport System) format is defined in 'Astronomy +and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H" + Crpix1: -74.5 + Crpix2: -549.5 + Crval1: 85.3218 + Crval2: -2.49444 + Ctype1: "RA---TAN-SIP" + Ctype2: "DEC--TAN-SIP" + Cunit1: "deg" + Cunit2: "deg" + Date-obs: 2025 + DateTime: "2026:08:09 12:28:17" + Dec: -1.93685 + Equinox: 2000 + Expend: 2461030 + Expstart: 2461030 + Exptime: 15 + Extend: "T" + Filter: "Duo-Band" + Focallen: 147.555 + Gain: 60 + History: "mean stacking without rejection, additive+scaling normalized input, norm +alized output, no image weighting, equalized RGB, filter selected +Rotation (0.0deg, cropped=TRUE, clamped=TRUE)" + Instrume: "DWARF 3" + Livetime: 2265 + Lonpole: 180 + Mips-flo: 0 + Objctdec: -1 + Objctra: 5 + Object: "Horsehead Nebula" + Pc1_1: 0.996079 + Pc1_2: -0.0884675 + Pc2_1: 0.0885425 + Pc2_2: 0.996072 + Pltsolvd: "T" + Program: "Siril 1.4.3" + Ra: 85.1877 + Software: "OpenImageIO 3.2.0.2dev : F437187DB9769DA2652357EC017687DCC21D6FCD" + Stackcnt: 151 + Telescop: "DWARF 3" + Xbinning: 1 + Xpixsz: 2 + Ybinning: 1 + Ypixsz: 2 + Oiio:sub: 1 + oiio:subimages: 1 +Reading src/rgb.fits +src/rgb.fits : 32 x 32, 3 channel, float fits + SHA-1: F7E36E977DEA527FE023283D9A6E6941BA439AE1 + channel list: R, G, B + A_0_0: 0 + A_0_1: 0 + A_0_2: 6.08313e-08 + A_0_3: 3.51185e-11 + A_1_0: 0 + A_1_1: -7.16346e-09 + A_1_2: -6.52948e-10 + A_2_0: -8.57221e-08 + A_2_1: 6.14197e-13 + A_3_0: -6.39624e-10 + A_order: 3 + Ap_0_0: -0.000178267 + Ap_0_1: 2.44714e-07 + Ap_0_2: -6.08969e-08 + Ap_0_3: -3.54786e-11 + Ap_1_0: -8.42448e-06 + Ap_1_1: 7.38336e-09 + Ap_1_2: 6.61491e-10 + Ap_2_0: 8.6651e-08 + Ap_2_1: -7.33107e-13 + Ap_3_0: 6.46638e-10 + Ap_order: 3 + B_0_0: 0 + B_0_1: 0 + B_0_2: -8.27428e-08 + B_0_3: -6.11542e-10 + B_1_0: 0 + B_1_1: -9.90564e-08 + B_1_2: -1.68675e-11 + B_2_0: -1.88226e-08 + B_2_1: -6.48505e-10 + B_3_0: -2.02193e-12 + B_order: 3 + Bp_0_0: -0.000204257 + Bp_0_1: -5.3092e-06 + Bp_0_2: 8.32734e-08 + Bp_0_3: 6.16666e-10 + Bp_1_0: 1.05632e-08 + Bp_1_1: 9.96989e-08 + Bp_1_2: 1.68664e-11 + Bp_2_0: 1.90495e-08 + Bp_2_1: 6.55252e-10 + Bp_3_0: 2.05389e-12 + Bp_order: 3 + Bscale: 1 + Bzero: 0 + Cdelt1: -0.000776386 + Cdelt2: 0.000776814 + Comment: "FITS (Flexible Image Transport System) format is defined in 'Astronomy +and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H" + Crpix1: -74.5 + Crpix2: -549.5 + Crval1: 85.3218 + Crval2: -2.49444 + Ctype1: "RA---TAN-SIP" + Ctype2: "DEC--TAN-SIP" + Cunit1: "deg" + Cunit2: "deg" + Date-obs: 2025 + DateTime: "2026:08:09 12:28:17" + Dec: -1.93685 + Equinox: 2000 + Expend: 2461030 + Expstart: 2461030 + Exptime: 15 + Extend: "T" + Filter: "Duo-Band" + Focallen: 147.555 + Gain: 60 + History: "mean stacking without rejection, additive+scaling normalized input, norm +alized output, no image weighting, equalized RGB, filter selected +Rotation (0.0deg, cropped=TRUE, clamped=TRUE)" + Instrume: "DWARF 3" + Livetime: 2265 + Lonpole: 180 + Mips-flo: 0 + Objctdec: -1 + Objctra: 5 + Object: "Horsehead Nebula" + Pc1_1: 0.996079 + Pc1_2: -0.0884675 + Pc2_1: 0.0885425 + Pc2_2: 0.996072 + Pltsolvd: "T" + Program: "Siril 1.4.3" + Ra: 85.1877 + Software: "OpenImageIO 3.2.0.2dev : F437187DB9769DA2652357EC017687DCC21D6FCD" + Stackcnt: 151 + Telescop: "DWARF 3" + Xbinning: 1 + Xpixsz: 2 + Ybinning: 1 + Ypixsz: 2 + Oiio:sub: 1 + oiio:subimages: 1 +Comparing "src/rgb.fits" and "rgb.fits" +PASS oiiotool ERROR: read : "src/broken_no_END.fits": Hit end of file unexpectedly (offset=5760) Full command line was: > oiiotool --info --hash src/broken_no_END.fits diff --git a/testsuite/fits/run.py b/testsuite/fits/run.py index aaf9bd68bf..36b12c2570 100755 --- a/testsuite/fits/run.py +++ b/testsuite/fits/run.py @@ -21,10 +21,12 @@ imagedir = OIIO_TESTSUITE_IMAGEDIR + "/ftt4b" files = [ "file001.fits", "file002.fits", "file003.fits", - "file009.fits", "file012.fits" ] + "file006.fits", "file009.fits", "file012.fits" ] for f in files : command += rw_command (imagedir, f) +command += info_command ("src/rgb.fits") +command += rw_command ("src", "rgb.fits") # Regression tests for broken files command += info_command ("src/broken_no_END.fits", verbose=False, failureok=True) diff --git a/testsuite/fits/src/rgb.fits b/testsuite/fits/src/rgb.fits new file mode 100644 index 0000000000..57da81b732 --- /dev/null +++ b/testsuite/fits/src/rgb.fits @@ -0,0 +1,41 @@ +SIMPLE = T BITPIX = -32 NAXIS = 3 NAXIS1 = 32 NAXIS2 = 32 NAXIS3 = 3 EXTEND = T BZERO = 0 BSCALE = 1 MIPS-FLO= 0 PROGRAM = Siril 1.4.3 DATE = 2026-08-09T12:28:17 DATE-OBS= 2025 EXPTIME = 15 TELESCOP= DWARF 3 FILTER = Duo-Band FOCALLEN= 147.555 XBINNING= 1 YBINNING= 1 XPIXSZ = 2 YPIXSZ = 2 INSTRUME= DWARF 3 GAIN = 60 STACKCNT= 151 LIVETIME= 2265 EXPSTART= 2.46103e+06 EXPEND = 2.46103e+06 OBJECT = Horsehead Nebula OBJCTRA = 5 OBJCTDEC= -1 RA = 85.1877 DEC = -1.93685 CTYPE1 = RA---TAN-SIP CTYPE2 = DEC--TAN-SIP OIIO:SUB= 1 CUNIT1 = deg CUNIT2 = deg EQUINOX = 2000 CRPIX1 = -74.5 CRPIX2 = -549.5 CRVAL1 = 85.3218 CRVAL2 = -2.49444 LONPOLE = 180 CDELT1 = -0.000776386 CDELT2 = 0.000776814 PC1_1 = 0.996079 PC1_2 = -0.0884675 PC2_1 = 0.0885425 PC2_2 = 0.996072 A_ORDER = 3 A_0_0 = 0 A_1_0 = 0 A_0_1 = 0 A_2_0 = -8.57221e-08 A_1_1 = -7.16346e-09 A_0_2 = 6.08313e-08 A_3_0 = -6.39624e-10 A_2_1 = 6.14197e-13 A_1_2 = -6.52948e-10 A_0_3 = 3.51185e-11 B_ORDER = 3 B_0_0 = 0 B_1_0 = 0 B_0_1 = 0 B_2_0 = -1.88226e-08 B_1_1 = -9.90564e-08 B_0_2 = -8.27428e-08 B_3_0 = -2.02193e-12 B_2_1 = -6.48505e-10 B_1_2 = -1.68675e-11 B_0_3 = -6.11542e-10 AP_ORDER= 3 AP_0_0 = -0.000178267 AP_1_0 = -8.42448e-06 AP_0_1 = 2.44714e-07 AP_2_0 = 8.6651e-08 AP_1_1 = 7.38336e-09 AP_0_2 = -6.08969e-08 AP_3_0 = 6.46638e-10 AP_2_1 = -7.33107e-13 AP_1_2 = 6.61491e-10 AP_0_3 = -3.54786e-11 BP_ORDER= 3 BP_0_0 = -0.000204257 BP_1_0 = 1.05632e-08 BP_0_1 = -5.3092e-06 BP_2_0 = 1.90495e-08 BP_1_1 = 9.96989e-08 BP_0_2 = 8.32734e-08 BP_3_0 = 2.05389e-12 BP_2_1 = 6.55252e-10 BP_1_2 = 1.68664e-11 BP_0_3 = 6.16666e-10 PLTSOLVD= T COMMENT FITS (Flexible Image Transport System) format is defined in 'Astronomy COMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H HISTORY mean stacking without rejection, additive+scaling normalized input, normHISTORY alized output, no image weighting, equalized RGB, filter selected HISTORY Rotation (0.0deg, cropped=TRUE, clamped=TRUE) SOFTWARE= OpenImageIO 3.2.0.2dev : F437187DB9769DA2652357EC017687DCC21D6FCD END ;,;7N;=;G%r;M?;N-;Z:;R<;[[#;i";me;{;);a;|;;4;K;=;/;9;;;;;{;.f;p;bn;R;FF;:+;2;Cc;>I;@;F;V;`;h.;p;;;Fb;U;};;d;P(;);J;;;Q;;0i;6; ;;;}7;eA;P;L;v;;%;B;K;Y(-;^`;cO;m4;Gw;A;;;;{;8@;;jo;a;;[I;O;;;;\B;;U;:;;;;al;U;EN;G;P;^;ah;t4;j/;2; ;̤;I;^;x;B;ŕ;Ϻ;?;;ʡ;;;;Ǭ;4F;x;(;;I;;;>u;8;^ ;C;M;_;mZ2;n;o;5;p;};{;b;Ģ;;ɢD;؆;;! ; N;Ke;;8;Z;鮓;;˺;P";?t;;`;y;%;t ;R<;Wp;do;{;E;S;%9;=;);L;܆;];K;U;<h< $< << 6\< q<<<[;쿪;އ;;oo;;v; +;;]qn;n;q;;5;li;8;;\;eC;>F;;;)< <<(<}<Ѫ<"<#v;ܮF;;Hf;c;?;;d`;v;;R;N;l;VA;o;;;< ^"<<<"'W<.{<51gm;9<5<$W <@o H>K w>x>o>^C>- =j>==Zd= >d$>}>>ͽ>ǒ>>Q>Rc=@=G>j>>g? V|?j?+?>> >; ==e= <<~<IY_>l>3E??8?FK?B?-? i>W>Qw> =Ɵ=@?=M~;}O< +,>z>A5?r?8?[M?l?h!?P@5?)>>)>+=~=R}= x,<'8;Ȩ;y;ڏG<<kJ>4>(??F^p?l??|[?bv?9? +N>M>D)=KZ=V = <ޖ<<)T T>]>ȏg?[?@?f%?zc?w?_?6غ? "g>>B==Z=<<<)Ss> ?~?)"?K:??]h?\?F?#k>>>&O=)=S=<ކp<.>)>??"F?2! ?1a?!?r7>>x==F=<=# <Ǵ)t>}>ר>Ƭ??%7> >>>.P,=}=ZP="<<6$V>QL>)>>#>y>><=#=h=:A= +aW<─<*f<<9>10>3*>u=4~=0=a= O< <(<5;:|;D;8;T;Q`;w;;;ܼ;ְ;X<< +;< <$R<#\<'<,OQ<-<)a<)<0+<1<)}<*6<&*<7<H<.< R0;>;i;; L;;J9;{T;;[;p;V< +<%2<^<*(<2;9;ZP;O<"< <<1>>%ʭ>=#>G>Qvh>D>1n>E=t=+=c=d=9/=L<8<Ϸk< +K>>3>\>ʶ>=>>5>0>ϐ>_L>+{)= =R=iP=Vc=/A=+~>C7>>>ɮ#>-x>W>>d>"T>a >i?>i=߁w==~H=L7='#=O<E5c>|>^>ߨ??x?1?? 4>/>~>`>I>h=6=F=b +=7|=<<<7s>hk >C>ߒ.? +DN?a?.!?4@?28?&y?q>I>ľ=>a>(Q=ݺ=|=oI=BU<=N:1>u>???60?Gt?O?M"?@AV?*?dD>>&> B>L>rp=TW=6=QD=)P=FX>>ۻ? ?*?Es?YB?bd?` j?Qs?:?>U> >n>R=Γ=Q=e=6=gQ>>>'?p_?.ʞ?J?_j?i?gA?Yh=?A,?$*?>P>r}U>=ּ\==o;=<=<'<1 F>s>? +?)?E0?Yõ?c?b3?Tp?>?!?ͱ>q>pz>"=n=E=ct=0x= +<ݹ<-r8T">]>>??5?H?Rs?Q?EY?0{?>>0>>\>'=Ŏ='0=M= A=R<<)[w>o>>;]?L?` ?/T?7'?7V?-"??>z><>>N>=:o=~D=7=<<ŋ<G&<@pw>:Zr>3>t>d?~???2?d?>>+>q>'+===k=4= <` +U>@/>n>X>n>> >K>>>١>O>F> =A=?=]=$M =L< i<=< +<6j<_<<mJ>&>J0V>p>)>> />^>4>p>O9>>=_={=_=?_="=>>$ +>@M>]%>fm>S>3> #====Y~=)= ->X>.=ጀ====X +=3_=j<#<v=:=9=-3= 3 = ;< :<7<1?<<<%j0=C=;==t=n=6= : +>%->D>S'$>^>Hă>+>=o==^k="=R{<<%!>a|'>TG>;'>! >>"B>|q>PE> Aw=p=W=E8=<<~G<9<<^< <^~<==8x8==!>+ >p>>B>l4>Ŝ> >*>k> +>B=c=z*=f )=.=m<<<(I>o[>->x?5?? +?-?}>Ƶ>^L>0>%=>B=&)=?=p<'0U>y>B?6?!r?5?=?:Z?*?Ҫ>>{>R>N=4=TB=U<4z<< r<&;A<q<<@zRX>'>腓??2f?I<=?SE?P2???M?$?F>>||>=2=n==/f= <ݛ<<a|J>>-?=?8_?PK?[?X?G?,? +>>>-=­=c=;=L`q>d+>??1?H?TKe?Q?A?'?Ԡ>> >Y=<(==6d= D<0<>5>ו???4Y???=xQ?/?RW>g>8>c>u=u=l=$g T9>a@>>?,? ? l????=> +>Q>9e=뷔=j=U"=2(<_< +1?>]Z>>>dO>%>u>7> e>>lw4>=W=#=D= N<<R<>C[>xOQ>7#>9>U>>>,>4Nq=L=_=w`!=:=k<;>.)>M>j>w=>Y>,,==/=DB=TV0=<_<!.> )><=$`==`=X=*K=%<<< <3<6<.j<"X<mU;b;T \ No newline at end of file