#include "loadrunable.h" #include "imageringlist.h" #include #include "imageinfo.h" #include #include #include #include #include #include #include #include #include #include "rawimage.h" #include "starfit.h" #include "wcslib/wcshdr.h" #ifdef COLOR_MANAGMENT #include #endif LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail) : m_file(file), m_receiver(receiver), m_analyzeLevel(level), m_thumbnail(thumbnail) { } void loadExifEntry(ImageInfoData &info, ExifContent *content, ExifTag tag) { char val[1024]; ExifEntry *entry = exif_content_get_entry(content, tag); if(entry) { exif_entry_get_value(entry, val, sizeof(val)); info.info.append({exif_tag_get_title(tag), val}); } } void drawPeaks(QImage &img, const std::vector &peaks) { QPixmap pix = QPixmap::fromImage(img); QPainter painter(&pix); painter.setPen(Qt::red); for(auto peak : peaks) { painter.drawEllipse(QPoint(peak.x(), peak.y()), 5, 5); } img = pix.toImage(); } void drawStars(QImage &img, const std::vector &stars) { QPixmap pix = QPixmap::fromImage(img); QPainter painter(&pix); painter.setPen(Qt::red); for(auto star : stars) { painter.drawEllipse(QPointF(star.m_x, star.m_y), star.hw20X(), star.hw20Y()); } img = pix.toImage(); } void printStarModel(int radius, const std::vector &data, const Star &star) { QString d = "d=["; QString m = "m=["; for(int y=0; y &image) { std::unique_ptr raw = std::make_unique(); raw->open_file(path.toLocal8Bit().data()); raw->imgdata.params.half_size = true; raw->imgdata.params.use_camera_wb = true; raw->imgdata.params.user_flip = 0; if(raw->unpack()) return false; libraw_rawdata_t rawdata = raw->imgdata.rawdata; size_t size = rawdata.sizes.width*rawdata.sizes.height; std::vector out; out.resize(size); size_t d = 0; uint h=rawdata.sizes.top_margin+rawdata.sizes.height; uint w=rawdata.sizes.left_margin+rawdata.sizes.width; size_t pitch = rawdata.sizes.raw_pitch/sizeof(uint16_t); for(size_t i=rawdata.sizes.top_margin;i(rawdata.sizes.width, rawdata.sizes.height, 1, RawImage::UINT16); memcpy(image->data(), &out[0], sizeof(uint16_t)*d); QString shutterSpeed = QString::number(raw->imgdata.other.shutter); if(raw->imgdata.other.shutter < 1) { shutterSpeed = QString("1/%1s").arg(1.0f/raw->imgdata.other.shutter); } info.info.append({QObject::tr("Width"), QString::number(raw->imgdata.sizes.width)}); info.info.append({QObject::tr("Height"), QString::number(raw->imgdata.sizes.height)}); info.info.append({QObject::tr("ISO"), QString::number(raw->imgdata.other.iso_speed)}); info.info.append({QObject::tr("Shutter speed"), shutterSpeed}); #if LIBRAW_MINOR_VERSION>=19 // info.append(StringPair(QObject::tr("Camera temperature"), QString::number(raw.imgdata.other.CameraTemperature))); #endif return true; } int loadFITSHeader(fitsfile *file, ImageInfoData &info) { int imgtype; int naxis; long naxes[3] = {0}; int nexist; int status = 0; char key[FLEN_KEYWORD]; char val[FLEN_VALUE]; char comm[FLEN_COMMENT]; char strval[FLEN_VALUE]; QVariant var; fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status); fits_get_hdrspace(file, &nexist, nullptr, &status); for(int i=1; i<=nexist; i++) { fits_read_keyn(file, i, key, val, comm, &status); fits_read_key(file, TSTRING, key, strval, nullptr, &status); if(status == 0 || status == VALUE_UNDEFINED) { QString string(strval); bool isint; bool isdouble; double vald = string.toDouble(&isdouble); long long vall = string.toLongLong(&isint); if(isint) var = vall; else if(isdouble) var = vald; else if(status == VALUE_UNDEFINED) var = QVariant(); else if(string == "T" || string == "F") var = string == "T"; else var = string; status = 0; info.fitsHeader.append(FITSRecord(key, var, comm)); } else { return status; } } char *header = nullptr; int nrec = 0; const char *exclist[] = {"PV1_1", "PV1_2"}; fits_hdr2str(file, TRUE, (char**)exclist, 2, &header, &nrec, &status); if(status == 0) { info.wcs = std::make_shared(naxes[0], naxes[1], header, nrec); if(!info.wcs->valid())info.wcs.reset(); } fits_free_memory(header, &status); return status; } bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr &image) { fitsfile *file; int status = 0; int type = -1; fits_open_diskfile(&file, path.toLocal8Bit().data(), READONLY, &status); int num = 0; fits_get_num_hdus(file, &num, &status); int imgtype; int naxis; long naxes[3] = {0}; for(int i=1; i <= num; i++) { fits_movabs_hdu(file, i, IMAGE_HDU, &status); fits_get_hdu_type(file, &type, &status); fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status); fits_get_img_equivtype(file, &imgtype, &status); if(type == IMAGE_HDU && naxis >= 2 && naxis <= 3 && status == 0) { RawImage::DataType type; int fitstype; long fpixel[3] = {1,1,1}; switch(imgtype) { case BYTE_IMG: type = RawImage::UINT8; fitstype = TBYTE; break; case SHORT_IMG: type = RawImage::UINT16; fitstype = TSHORT; break; case USHORT_IMG: type = RawImage::UINT16; fitstype = TUSHORT; break; case ULONG_IMG: type = RawImage::UINT32; fitstype = TUINT; break; case FLOAT_IMG: type = RawImage::FLOAT32; fitstype = TFLOAT; break; case DOUBLE_IMG: type = RawImage::FLOAT64; fitstype = TDOUBLE; break; default: info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")}); goto noload; break; } size_t size = naxes[0]*naxes[1]; size_t w = naxes[0]; size_t h = naxes[1]; info.info.append({QObject::tr("Width"), QString::number(naxes[0])}); info.info.append({QObject::tr("Height"), QString::number(naxes[1])}); RawImage img(w, h, naxis == 2 ? 1 : naxes[2], type); uint8_t *data = static_cast(img.data()); for (int i=1; i==1 || i<=naxes[2]; i++) { fpixel[2] = i; fits_read_pix(file, fitstype, fpixel, size, NULL, data + img.size() * RawImage::typeSize(type) * (i-1), NULL, &status); } if(fitstype == TSHORT) { uint16_t *s = static_cast(img.data()); size_t size = img.size() * img.channels(); for(size_t i=0; i(std::move(img)); else image = RawImage::fromPlanar(img); if(image) image->convertToGLFormat(); break; } } noload: if(file) loadFITSHeader(file, info); if(image) { for(auto fits : info.fitsHeader) { if(fits.key == "ROWORDER" && fits.value == "BOTTOM-UP") image->flip(); } } fits_close_file(file, &status); if(status) { char err[100]; fits_get_errstatus(status, err); info.info.append({QObject::tr("Error"), QString(err)}); qDebug() << "Failed to load FITS file" << err; } return true; } bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr &image) { try { LibXISF::XISFReader xisf; xisf.open(path.toLocal8Bit().data()); const LibXISF::Image &xisfImage = xisf.getImage(0); auto fitskeywords = xisfImage.fitsKeywords(); for(auto fits : fitskeywords) { info.fitsHeader.append(fits); } auto imageproperties = xisfImage.imageProperties(); for(auto prop : imageproperties) { info.fitsHeader.append(prop); } info.wcs = std::make_shared(xisfImage.width(), xisfImage.height(), info.fitsHeader); info.info.append({QObject::tr("Width"), QString::number(xisfImage.width())}); info.info.append({QObject::tr("Height"), QString::number(xisfImage.height())}); if(!info.wcs->valid())info.wcs.reset(); RawImage::DataType type; switch(xisfImage.sampleFormat()) { case LibXISF::Image::UInt8: type = RawImage::UINT8; break; case LibXISF::Image::UInt16: type = RawImage::UINT16; break; case LibXISF::Image::UInt32: type = RawImage::UINT32; break; case LibXISF::Image::Float32: type = RawImage::FLOAT32; break; case LibXISF::Image::Float64: type = RawImage::FLOAT64; break; default: break; } LibXISF::Image tmpImage = xisfImage; tmpImage.convertPixelStorageTo(LibXISF::Image::Planar); if(tmpImage.colorSpace() == LibXISF::Image::ColorSpace::Gray) { image = std::make_shared(tmpImage.width(), tmpImage.height(), 1, type); std::memcpy(image->data(), tmpImage.imageData(), tmpImage.imageDataSize() / tmpImage.channelCount()); } else if(tmpImage.channelCount() == 3 || tmpImage.channelCount() == 4) { image = RawImage::fromPlanar(tmpImage.imageData(), tmpImage.width(), tmpImage.height(), tmpImage.channelCount(), type); } if(image) { image->convertToGLFormat(); return true; } } catch (LibXISF::Error &err) { info.info.append(QPair("Error", err.what())); qDebug() << "Failed to load XISF" << err.what(); return false; } info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")}); return false; } void LoadRunable::run() { try { if(!m_thumbnail && !m_receiver->isCurrent()) { return; } QElapsedTimer timer; ImageInfoData info; QFileInfo finfo(m_file); info.info.append({QObject::tr("Filename"), finfo.fileName()}); std::shared_ptr rawImage; if(!loadImage(m_file, info, rawImage)) info.info.append({QObject::tr("Error"), QObject::tr("Failed to load image")}); if(rawImage && !m_thumbnail) { timer.start(); rawImage->calcStats(); const RawImage::Stats &stats = rawImage->imageStats(); qDebug() << "image stats" << timer.restart(); if(rawImage->channels() == 1) { info.info.append({QObject::tr("Mean"), QString::number(stats.m_mean[0])}); info.info.append({QObject::tr("Standart deviation"), QString::number(stats.m_stdDev[0])}); info.info.append({QObject::tr("Median"), QString::number(stats.m_median[0])}); info.info.append({QObject::tr("Minimum"), QString::number(stats.m_min[0])}); info.info.append({QObject::tr("Maximum"), QString::number(stats.m_max[0])}); info.info.append({QObject::tr("MAD"), QString::number(stats.m_mad[0])}); info.info.append({QObject::tr("Saturated"), QString::number(100.0 * stats.m_saturated[0] / rawImage->size()) + "%"}); } else { info.info.append({QObject::tr("Mean"), QString("%1 %2 %3").arg(stats.m_mean[0]).arg(stats.m_mean[1]).arg(stats.m_mean[2])}); info.info.append({QObject::tr("Standart deviation"), QString("%1 %2 %3").arg(stats.m_stdDev[0]).arg(stats.m_stdDev[1]).arg(stats.m_stdDev[2])}); info.info.append({QObject::tr("Median"), QString("%1 %2 %3").arg(stats.m_median[0]).arg(stats.m_median[1]).arg(stats.m_median[2])}); info.info.append({QObject::tr("Minimum"), QString("%1 %2 %3").arg(stats.m_min[0]).arg(stats.m_min[1]).arg(stats.m_min[2])}); info.info.append({QObject::tr("Maximum"), QString("%1 %2 %3").arg(stats.m_max[0]).arg(stats.m_max[1]).arg(stats.m_max[2])}); info.info.append({QObject::tr("MAD"), QString("%1 %2 %3").arg(stats.m_mad[0]).arg(stats.m_mad[1]).arg(stats.m_mad[2])}); info.info.append({QObject::tr("Saturated"), QString("%1 %2 %3%").arg(100.0 * stats.m_saturated[0] / rawImage->size()) .arg(100.0 * stats.m_saturated[1] / rawImage->size()) .arg(100.0 * stats.m_saturated[2] / rawImage->size())}); } } if(m_thumbnail) { if(rawImage && rawImage->valid()) { if(QUALITY_RESIZE) rawImage->resize(THUMB_SIZE, THUMB_SIZE); rawImage->convertToThumbnail(); } QMetaObject::invokeMethod(m_receiver, "thumbnailLoadFinish", Qt::QueuedConnection, Q_ARG(std::shared_ptr, rawImage)); } else { QMetaObject::invokeMethod(m_receiver, "imageLoaded", Qt::QueuedConnection, Q_ARG(std::shared_ptr, rawImage), Q_ARG(ImageInfoData, info)); } } catch(std::exception e) { qDebug() << m_file << e.what(); } } bool readFITSHeader(const QString &path, ImageInfoData &info) { fitsfile *fr; int status = 0; fits_open_diskfile(&fr, path.toLocal8Bit().data(), READONLY, &status); if(fr && status == 0) { status = loadFITSHeader(fr, info); fits_close_file(fr, &status); } return status == 0; } bool readXISFHeader(const QString &path, ImageInfoData &info) { try { LibXISF::XISFReader xisf; xisf.open(path.toLocal8Bit().data()); const LibXISF::Image &image = xisf.getImage(0, false); auto fitskeywords = image.fitsKeywords(); for(auto fits : fitskeywords) { info.fitsHeader.append(fits); } auto imageproperties = image.imageProperties(); for(auto prop : imageproperties) { info.fitsHeader.append(prop); } info.wcs = std::make_shared(image.width(), image.height(), info.fitsHeader); if(!info.wcs->valid())info.wcs.reset(); } catch (LibXISF::Error &err) { qDebug() << err.what(); return false; } return true; } bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr &rawImage) { bool ret = false; QElapsedTimer timer; timer.start(); if(path.endsWith(".CR2", Qt::CaseInsensitive) || path.endsWith(".CR3", Qt::CaseInsensitive) || path.endsWith(".NEF", Qt::CaseInsensitive) || path.endsWith(".DNG", Qt::CaseInsensitive)) { ret = loadRAW(path, info, rawImage); qDebug() << "LoadRAW" << timer.elapsed(); } else if(path.endsWith(".FIT", Qt::CaseInsensitive) || path.endsWith(".FITS", Qt::CaseInsensitive)) { ret = loadFITS(path, info, rawImage); qDebug() << "LoadFITS" << timer.elapsed(); } else if(path.endsWith(".XISF", Qt::CaseInsensitive)) { ret = loadXISF(path, info, rawImage); qDebug() << "LoadXISF" << timer.elapsed(); } else { QImage img(path); #ifdef COLOR_MANAGMENT if(img.colorSpace().isValid() && img.colorSpace() != QColorSpace::SRgb) img.convertToColorSpace(QColorSpace::SRgb); #endif ExifData *exif = exif_data_new_from_file(path.toLocal8Bit().constData()); info.info.append({QObject::tr("Width"), QString::number(img.width())}); info.info.append({QObject::tr("Height"), QString::number(img.height())}); if(exif) { loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_ISO_SPEED_RATINGS); loadExifEntry(info, exif->ifd[EXIF_IFD_EXIF], EXIF_TAG_SHUTTER_SPEED_VALUE); exif_data_free(exif); } rawImage = std::make_shared(img); qDebug() << "LoadQImage" << timer.elapsed(); ret = !img.isNull(); } return ret; } ConvertRunable::ConvertRunable(const QString &in, const QString &out, const QString &format, const ConvertParams ¶ms, QSemaphore *semaphore) : m_infile(in), m_outfile(out), m_format(format), m_params(params), m_semaphore(semaphore) { } void writeFITSImage(fitsfile *fw, std::shared_ptr rawimage, ImageInfoData &imageinfo) { static QStringList skipKeys = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "BZERO", "BSCALE", "EXTEND"}; int status = 0; long firstpix[3] = {1,1,1}; int channels = rawimage->channels(); int naxis = channels == 1 ? 2 : 3; long naxes[3] = {(int)rawimage->width(), (int)rawimage->height(), rawimage->channels()}; std::vector planes; if(channels == 1) planes.push_back(*rawimage); else planes = rawimage->split(); switch(rawimage->type()) { case RawImage::UINT8: fits_create_img(fw, BYTE_IMG, naxis, naxes, &status); for(int i=0; isize(), planes[i].data(), &status); } break; case RawImage::UINT16: fits_create_img(fw, USHORT_IMG, naxis, naxes, &status); for(int i=0; isize(), planes[i].data(), &status); } break; case RawImage::FLOAT32: fits_create_img(fw, FLOAT_IMG, naxis, naxes, &status); for(int i=0; isize(), planes[i].data(), &status); } break; default: return; } for(const FITSRecord &record : imageinfo.fitsHeader) { if(skipKeys.contains(record.key) || record.xisf)continue; bool isdouble; bool isint; bool isbool = record.value.toString() == "T" || record.value.toString() == "F"; double vald = record.value.toDouble(&isdouble); int valb = record.value.toString() == "T"; long long vall = record.value.toLongLong(&isint); QByteArray str = record.value.toString().toLatin1(); if(isdouble) fits_write_key(fw, TDOUBLE, record.key.data(), &vald, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); else if(isint) fits_write_key(fw, TLONGLONG, record.key.data(), &vall, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); else if(isbool) fits_write_key(fw, TLOGICAL, record.key.data(), &valb, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); else if(record.key == "COMMENT") fits_write_comment(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); else if(record.key == "HISTORY") fits_write_history(fw, record.comment.isEmpty() ? nullptr : record.comment.data(), &status); else fits_write_key(fw, TSTRING, record.key.data(), str.isEmpty() ? nullptr : str.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status); } } void ConvertRunable::run() { QSemaphoreReleaser release; if(m_semaphore)release = QSemaphoreReleaser(m_semaphore); ImageInfoData imageinfo; std::shared_ptr rawimage; loadImage(m_infile, imageinfo, rawimage); QFileInfo info(m_outfile); info.dir().mkpath("."); if(rawimage) { if(m_format == "xisf") { try { LibXISF::XISFWriter xisf; int channelCount = rawimage->channels(); LibXISF::Image::SampleFormat sampleFormat; switch(rawimage->type()) { case RawImage::UINT8: sampleFormat = LibXISF::Image::UInt8; break; case RawImage::UINT16: sampleFormat = LibXISF::Image::UInt16; break; case RawImage::FLOAT32: sampleFormat = LibXISF::Image::Float32; break; default: return; } LibXISF::Image image(rawimage->width(), rawimage->height(), channelCount, sampleFormat, channelCount == 1 ? LibXISF::Image::Gray : LibXISF::Image::RGB, LibXISF::Image::Planar); if(channelCount == 1) { std::memcpy(image.imageData(), rawimage->data(), image.imageDataSize()); } else { size_t off = 0; std::vector planes = rawimage->split(); for(const auto &plane : planes) { std::memcpy(image.imageData() + off, plane.data(), plane.size() * RawImage::typeSize(plane.type())); off += plane.size() * RawImage::typeSize(plane.type()); } } for(auto &record : imageinfo.fitsHeader) { if(record.xisf)continue; if(record.value.typeId() == QMetaType::Bool) image.addFITSKeyword({record.key.toStdString(), record.value.toBool() ? "T" : "F", record.comment.toStdString()}); else image.addFITSKeyword({record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()}); } if(m_params.compressionType.startsWith("zstd") && LibXISF::DataBlock::CompressionCodecSupported(LibXISF::DataBlock::ZSTD)) image.setCompression(LibXISF::DataBlock::ZSTD, m_params.compressionLevel); else if(m_params.compressionType.startsWith("lz4hc")) image.setCompression(LibXISF::DataBlock::LZ4HC, m_params.compressionLevel); else if(m_params.compressionType.startsWith("lz4")) image.setCompression(LibXISF::DataBlock::LZ4, m_params.compressionLevel); else if(m_params.compressionType.startsWith("zlib")) image.setCompression(LibXISF::DataBlock::Zlib, m_params.compressionLevel); if(m_params.compressionType.endsWith("+sh")) image.setByteshuffling(true); xisf.writeImage(image); xisf.save(m_outfile.toLocal8Bit().data()); } catch(LibXISF::Error &err) { qDebug() << "Failed to save XISF image" << err.what(); } return; } if(m_format == "fits") { int status = 0; fitsfile *fw; if(QFileInfo(m_outfile).exists())QFile::remove(m_outfile); fits_create_diskfile(&fw, m_outfile.toLocal8Bit().data(), &status); if(!m_params.compressionType.isEmpty()) { if(m_params.compressionType == "gzip") fits_set_compression_type(fw, GZIP_1, &status); else if(m_params.compressionType == "rice") fits_set_compression_type(fw, RICE_1, &status); } writeFITSImage(fw, rawimage, imageinfo); fits_close_file(fw, &status); return; } // if nothing else try QImage { QImage::Format format = QImage::Format_Invalid; int width = rawimage->widthBytes(); switch(rawimage->type()) { case RawImage::UINT8: if(rawimage->channels() == 1)format = QImage::Format_Grayscale8; else if(rawimage->channels() == 3)format = QImage::Format_RGBX8888; else if(rawimage->channels() == 4)format = QImage::Format_RGBA8888; break; case RawImage::UINT16: if(rawimage->channels() == 1)format = QImage::Format_Grayscale16; else if(rawimage->channels() == 3)format = QImage::Format_RGBX64; else if(rawimage->channels() == 4)format = QImage::Format_RGBA64; width *= 2; break; default: return; } if(format == QImage::Format_Invalid)return; QImage qimage(rawimage->width(), rawimage->height(), format); for(uint32_t i=0; i < rawimage->height(); i++) std::memcpy(qimage.scanLine(i), rawimage->data(i), width); qimage.save(m_outfile); } } } ConvertRunable::ConvertParams::ConvertParams(const QVariantMap &map) { bool ok = false; if(map.contains("compressionLevel")) compressionLevel = std::clamp(map["compressionLevel"].toInt(&ok), -1, 100); if(!ok)compressionLevel = -1; if(map.contains("compressionType")) compressionType = map["compressionType"].toString(); }