diff --git a/kstars/ekos/guide/internalguide/gmath.cpp b/kstars/ekos/guide/internalguide/gmath.cpp index efaee15d0..e02e4a507 100644 --- a/kstars/ekos/guide/internalguide/gmath.cpp +++ b/kstars/ekos/guide/internalguide/gmath.cpp @@ -1,1809 +1,1810 @@ /* Ekos guide tool Copyright (C) 2012 Andrew Stepanenko Modified by Jasem Mutlaq for KStars. This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "gmath.h" #include "imageautoguiding.h" #include "Options.h" #include "fitsviewer/fitsdata.h" #include "fitsviewer/fitsview.h" #include "auxiliary/kspaths.h" #include "ekos_guide_debug.h" +#include #include #include #define DEF_SQR_0 (8 - 0) #define DEF_SQR_1 (16 - 0) #define DEF_SQR_2 (32 - 0) #define DEF_SQR_3 (64 - 0) #define DEF_SQR_4 (128 - 0) const guide_square_t guide_squares[] = { { DEF_SQR_0, DEF_SQR_0 *DEF_SQR_0 * 1.0 }, { DEF_SQR_1, DEF_SQR_1 *DEF_SQR_1 * 1.0 }, { DEF_SQR_2, DEF_SQR_2 *DEF_SQR_2 * 1.0 }, { DEF_SQR_3, DEF_SQR_3 *DEF_SQR_3 * 1.0 }, { DEF_SQR_4, DEF_SQR_4 *DEF_SQR_4 * 1.0 }, { -1, -1 } }; const square_alg_t guide_square_alg[] = { { SMART_THRESHOLD, "Smart" }, { SEP_THRESHOLD, "SEP" }, { CENTROID_THRESHOLD, "Fast" }, { AUTO_THRESHOLD, "Auto" }, { NO_THRESHOLD, "No thresh." }, { -1, { 0 } } }; struct Peak { int x; int y; float val; Peak() = default; Peak(int x_, int y_, float val_) : x(x_), y(y_), val(val_) { } bool operator<(const Peak& rhs) const { return val < rhs.val; } }; // JM: Why not use QPoint? typedef struct { int x, y; } point_t; cgmath::cgmath() : QObject() { // sys... ROT_Z = Ekos::Matrix(0); // sky coord. system vars. star_pos = Vector(0); scr_star_pos = Vector(0); reticle_pos = Vector(0); reticle_orts[0] = Vector(0); reticle_orts[1] = Vector(0); reticle_angle = 0; ditherRate[0] = ditherRate[1] = -1; // processing in_params.reset(); out_params.reset(); channel_ticks[GUIDE_RA] = channel_ticks[GUIDE_DEC] = 0; accum_ticks[GUIDE_RA] = accum_ticks[GUIDE_DEC] = 0; drift[GUIDE_RA] = new double[MAX_ACCUM_CNT]; drift[GUIDE_DEC] = new double[MAX_ACCUM_CNT]; memset(drift[GUIDE_RA], 0, sizeof(double) * MAX_ACCUM_CNT); memset(drift[GUIDE_DEC], 0, sizeof(double) * MAX_ACCUM_CNT); drift_integral[GUIDE_RA] = drift_integral[GUIDE_DEC] = 0; QString logFileName = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + "guide_log.txt"; logFile.setFileName(logFileName); } cgmath::~cgmath() { delete[] drift[GUIDE_RA]; delete[] drift[GUIDE_DEC]; foreach (float *region, referenceRegions) delete[] region; referenceRegions.clear(); } bool cgmath::setVideoParameters(int vid_wd, int vid_ht, int binX, int binY) { if (vid_wd <= 0 || vid_ht <= 0) return false; video_width = vid_wd / binX; video_height = vid_ht / binY; subBinX = binX; subBinY = binY; //set_reticle_params( video_width/2, video_height/2, -1 ); // keep orientation return true; } void cgmath::setGuideView(FITSView *image) { guideView = image; /*if (guideView) { FITSData *image_data = guideView->getImageData(); setDataBuffer(image_data->getImageBuffer()); setVideoParameters(image_data->getWidth(), image_data->getHeight()); }*/ } bool cgmath::setGuiderParameters(double ccd_pix_wd, double ccd_pix_ht, double guider_aperture, double guider_focal) { if (ccd_pix_wd < 0) ccd_pix_wd = 0; if (ccd_pix_ht < 0) ccd_pix_ht = 0; if (guider_focal <= 0) guider_focal = 1; ccd_pixel_width = ccd_pix_wd / 1000.0; // from mkm to mm ccd_pixel_height = ccd_pix_ht / 1000.0; // from mkm to mm aperture = guider_aperture; focal = guider_focal; return true; } void cgmath::getGuiderParameters(double *ccd_pix_wd, double *ccd_pix_ht, double *guider_aperture, double *guider_focal) { *ccd_pix_wd = ccd_pixel_width * 1000.0; *ccd_pix_ht = ccd_pixel_height * 1000.0; *guider_aperture = aperture; *guider_focal = focal; } void cgmath::createGuideLog() { logFile.close(); logFile.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream out(&logFile); out << "Guiding rate,x15 arcsec/sec: " << Options::guidingRate() << endl; out << "Focal,mm: " << focal << endl; out << "Aperture,mm: " << aperture << endl; out << "F/D: " << focal / aperture << endl; out << "Frame #, Time Elapsed (ms), RA Error (arcsec), RA Correction (ms), RA Correction Direction, DEC Error " "(arcsec), DEC Correction (ms), DEC Correction Direction" << endl; logTime.restart(); } bool cgmath::setReticleParameters(double x, double y, double ang) { // check frame ranges if (x < 0) x = 0; if (y < 0) y = 0; if (x >= (double)video_width - 1) x = (double)video_width - 1; if (y >= (double)video_height - 1) y = (double)video_height - 1; reticle_pos = Vector(x, y, 0); if (ang >= 0) reticle_angle = ang; ROT_Z = Ekos::RotateZ(-M_PI * reticle_angle / 180.0); // NOTE!!! sing '-' derotates star coordinate system reticle_orts[0] = Vector(1, 0, 0) * 100; reticle_orts[1] = Vector(0, 1, 0) * 100; reticle_orts[0] = reticle_orts[0] * ROT_Z; reticle_orts[1] = reticle_orts[1] * ROT_Z; return true; } bool cgmath::getReticleParameters(double *x, double *y, double *ang) const { *x = reticle_pos.x; *y = reticle_pos.y; if (ang) *ang = reticle_angle; return true; } int cgmath::getSquareAlgorithmIndex(void) const { return square_alg_idx; } info_params_t cgmath::getInfoParameters(void) const { info_params_t ret; Vector p; ret.aperture = aperture; ret.focal = focal; ret.focal_ratio = focal / aperture; p = Vector(video_width, video_height, 0); p = point2arcsec(p); p /= 60; // convert to minutes ret.fov_wd = p.x; ret.fov_ht = p.y; return ret; } uint32_t cgmath::getTicks(void) const { return ticks; } void cgmath::getStarDrift(double *dx, double *dy) const { *dx = star_pos.x; *dy = star_pos.y; } void cgmath::getStarScreenPosition(double *dx, double *dy) const { *dx = scr_star_pos.x; *dy = scr_star_pos.y; } bool cgmath::reset(void) { // square_alg_idx = AUTO_THRESHOLD; // // sky coord. system vars. // star_pos = Vector(0); // scr_star_pos = Vector(0); // setReticleParameters(video_width / 2, video_height / 2, 0.0); ticks = 0; channel_ticks[GUIDE_RA] = channel_ticks[GUIDE_DEC] = 0; accum_ticks[GUIDE_RA] = accum_ticks[GUIDE_DEC] = 0; drift_integral[GUIDE_RA] = drift_integral[GUIDE_DEC] = 0; out_params.reset(); memset(drift[GUIDE_RA], 0, sizeof(double) * MAX_ACCUM_CNT); memset(drift[GUIDE_DEC], 0, sizeof(double) * MAX_ACCUM_CNT); // cleanup stat vars. sum = 0; return true; } /*void cgmath::move_square( double newx, double newy ) { square_pos.x = newx; square_pos.y = newy; // check frame ranges if (lastBinX == subBinX) { if( square_pos.x < 0 ) square_pos.x = 0; if( square_pos.y < 0 ) square_pos.y = 0; if( square_pos.x+(double)square_size > (double)video_width ) square_pos.x = (double)(video_width - square_size); if( square_pos.y+(double)square_size > (double)video_height ) square_pos.y = (double)(video_height - square_size); } // FITS Image takes center coords if (guide_frame) { guide_frame->setTrackingBoxEnabled(true); //guide_frame->setTrackingBoxCenter(QPointF(square_pos.x+square_size/2, square_pos.y+square_size/2)); guide_frame->setTrackingBox(QRect(square_pos.x, square_pos.y, square_size/subBinX, square_size/subBinY)); } } void cgmath::resize_square( int size_idx ) { if( size_idx < 0 || size_idx >= (int)(sizeof(guide_squares)/sizeof(guide_square_t))-1) return; if (square_size != guide_squares[size_idx].size) { square_pos.x += (square_size-guide_squares[size_idx].size)/2; square_pos.y += (square_size-guide_squares[size_idx].size)/2; } square_size = guide_squares[size_idx].size; square_square = guide_squares[size_idx].square; square_idx = size_idx; // check position if (guide_frame) { guide_frame->setTrackingBoxEnabled(true); //guide_frame->setTrackingBoxSize(QSize(square_size,square_size)); guide_frame->setTrackingBox(QRect(square_pos.x/subBinX, square_pos.y/subBinY, square_size/subBinX, square_size/subBinY)); } }*/ void cgmath::setSquareAlgorithm(int alg_idx) { if (alg_idx < 0 || alg_idx >= (int)(sizeof(guide_square_alg) / sizeof(square_alg_t)) - 1) return; square_alg_idx = alg_idx; in_params.threshold_alg_idx = square_alg_idx; } Vector cgmath::point2arcsec(const Vector &p) const { Vector arcs; // arcs = 3600*180/pi * (pix*ccd_pix_sz) / focal_len arcs.x = 206264.8062470963552 * p.x * ccd_pixel_width / focal; arcs.y = 206264.8062470963552 * p.y * ccd_pixel_height / focal; return arcs; } bool cgmath::calculateAndSetReticle1D(double start_x, double start_y, double end_x, double end_y, int RATotalPulse) { double phi; phi = calculatePhi(start_x, start_y, end_x, end_y); if (phi < 0) return false; setReticleParameters(start_x, start_y, phi); if (RATotalPulse > 0) { double x = end_x - start_x; double y = end_y - start_y; double len = sqrt(x * x + y * y); // Total pulse includes start --> end --> start ditherRate[GUIDE_RA] = RATotalPulse / (2 * len); qCDebug(KSTARS_EKOS_GUIDE) << "Dither RA Rate " << ditherRate[GUIDE_RA] << " ms/Pixel"; } return true; } bool cgmath::calculateAndSetReticle2D(double start_ra_x, double start_ra_y, double end_ra_x, double end_ra_y, double start_dec_x, double start_dec_y, double end_dec_x, double end_dec_y, bool *swap_dec, int RATotalPulse, int DETotalPulse) { double phi_ra = 0; // angle calculated by GUIDE_RA drift double phi_dec = 0; // angle calculated by GUIDE_DEC drift double phi = 0; Vector ra_vect = Normalize(Vector(end_ra_x - start_ra_x, -(end_ra_y - start_ra_y), 0)); Vector dec_vect = Normalize(Vector(end_dec_x - start_dec_x, -(end_dec_y - start_dec_y), 0)); Vector try_increase = dec_vect * Ekos::RotateZ(M_PI / 2); Vector try_decrease = dec_vect * Ekos::RotateZ(-M_PI / 2); double cos_increase = try_increase & ra_vect; double cos_decrease = try_decrease & ra_vect; bool do_increase = cos_increase > cos_decrease ? true : false; phi_ra = calculatePhi(start_ra_x, start_ra_y, end_ra_x, end_ra_y); if (phi_ra < 0) return false; phi_dec = calculatePhi(start_dec_x, start_dec_y, end_dec_x, end_dec_y); if (phi_dec < 0) return false; if (do_increase) phi_dec += 90; else phi_dec -= 90; if (phi_dec > 360) phi_dec -= 360.0; if (phi_dec < 0) phi_dec += 360.0; if (fabs(phi_dec - phi_ra) > 180) { if (phi_ra > phi_dec) phi_ra -= 360; else phi_dec -= 360; } // average angles phi = (phi_ra + phi_dec) / 2; if (phi < 0) phi += 360.0; // check DEC if (swap_dec) *swap_dec = dec_swap = do_increase ? false : true; setReticleParameters(start_ra_x, start_ra_y, phi); if (RATotalPulse > 0) { double x = end_ra_x - start_ra_x; double y = end_ra_y - start_ra_y; double len = sqrt(x * x + y * y); ditherRate[GUIDE_RA] = RATotalPulse / (2 * len); qCDebug(KSTARS_EKOS_GUIDE) << "Dither RA Rate " << ditherRate[GUIDE_RA] << " ms/Pixel"; } if (DETotalPulse > 0) { double x = end_dec_x - start_dec_x; double y = end_dec_y - start_dec_y; double len = sqrt(x * x + y * y); ditherRate[GUIDE_DEC] = DETotalPulse / (2 * len); qCDebug(KSTARS_EKOS_GUIDE) << "Dither DEC Rate " << ditherRate[GUIDE_DEC] << " ms/Pixel"; } return true; } double cgmath::calculatePhi(double start_x, double start_y, double end_x, double end_y) const { double delta_x, delta_y; double phi; delta_x = end_x - start_x; delta_y = -(end_y - start_y); //if( (!Vector(delta_x, delta_y, 0)) < 2.5 ) // JM 2015-12-10: Lower threshold to 1 pixel if ((!Vector(delta_x, delta_y, 0)) < 1) return -1; // 90 or 270 degrees if (fabs(delta_x) < fabs(delta_y) / 1000000.0) { phi = delta_y > 0 ? 90.0 : 270; } else { phi = 180.0 / M_PI * atan2(delta_y, delta_x); if (phi < 0) phi += 360.0; } return phi; } void cgmath::do_ticks(void) { ticks++; channel_ticks[GUIDE_RA]++; channel_ticks[GUIDE_DEC]++; if (channel_ticks[GUIDE_RA] >= MAX_ACCUM_CNT) channel_ticks[GUIDE_RA] = 0; if (channel_ticks[GUIDE_DEC] >= MAX_ACCUM_CNT) channel_ticks[GUIDE_DEC] = 0; accum_ticks[GUIDE_RA]++; accum_ticks[GUIDE_DEC]++; if (accum_ticks[GUIDE_RA] >= in_params.accum_frame_cnt[GUIDE_RA]) accum_ticks[GUIDE_RA] = 0; if (accum_ticks[GUIDE_DEC] >= in_params.accum_frame_cnt[GUIDE_DEC]) accum_ticks[GUIDE_DEC] = 0; } //-------------------- Processing --------------------------- void cgmath::start(void) { ticks = 0; channel_ticks[GUIDE_RA] = channel_ticks[GUIDE_DEC] = 0; accum_ticks[GUIDE_RA] = accum_ticks[GUIDE_DEC] = 0; drift_integral[GUIDE_RA] = drift_integral[GUIDE_DEC] = 0; out_params.reset(); memset(drift[GUIDE_RA], 0, sizeof(double) * MAX_ACCUM_CNT); memset(drift[GUIDE_DEC], 0, sizeof(double) * MAX_ACCUM_CNT); // cleanup stat vars. sum = 0; preview_mode = false; if (focal > 0 && aperture > 0) createGuideLog(); // Create reference Image if (imageGuideEnabled) { foreach (float *region, referenceRegions) delete[] region; referenceRegions.clear(); referenceRegions = partitionImage(); reticle_pos = Vector(0, 0, 0); } } void cgmath::stop(void) { preview_mode = true; } void cgmath::suspend(bool mode) { suspended = mode; } bool cgmath::isSuspended(void) const { return suspended; } bool cgmath::isStarLost(void) const { return lost_star; } void cgmath::setLostStar(bool is_lost) { lost_star = is_lost; } float *cgmath::createFloatImage(FITSData *target) const { FITSData *imageData = target; if (imageData == nullptr) imageData = guideView->getImageData(); // #1 Convert to float array // We only process 1st plane if it is a color image uint32_t imgSize = imageData->width() * imageData->height(); float *imgFloat = new float[imgSize]; if (imgFloat == nullptr) { qCritical() << "Not enough memory for float image array!"; return nullptr; } switch (imageData->property("dataType").toInt()) { case TBYTE: { uint8_t *buffer = imageData->getImageBuffer(); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TSHORT: { int16_t *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TUSHORT: { uint16_t *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TLONG: { int32_t *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TULONG: { uint32_t *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TFLOAT: { float *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TLONGLONG: { int64_t *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; case TDOUBLE: { double *buffer = reinterpret_cast(imageData->getImageBuffer()); for (uint32_t i = 0; i < imgSize; i++) imgFloat[i] = buffer[i]; } break; default: delete[] imgFloat; return nullptr; } return imgFloat; } QVector cgmath::partitionImage() const { QVector regions; FITSData *imageData = guideView->getImageData(); float *imgFloat = createFloatImage(); if (imgFloat == nullptr) return regions; const uint16_t width = imageData->width(); const uint16_t height = imageData->height(); uint8_t xRegions = floor(width / regionAxis); uint8_t yRegions = floor(height / regionAxis); // Find number of regions to divide the image //uint8_t regions = xRegions * yRegions; float *regionPtr = imgFloat; for (uint8_t i = 0; i < yRegions; i++) { for (uint8_t j = 0; j < xRegions; j++) { // Allocate space for one region float *oneRegion = new float[regionAxis * regionAxis]; // Create points to region and current location of the source image in the desired region float *oneRegionPtr = oneRegion, *imgFloatPtr = regionPtr + j * regionAxis; // copy from image to region line by line for (uint32_t line = 0; line < regionAxis; line++) { memcpy(oneRegionPtr, imgFloatPtr, regionAxis); oneRegionPtr += regionAxis; imgFloatPtr += width; } regions.append(oneRegion); } // Move regionPtr block by (width * regionAxis) elements regionPtr += width * regionAxis; } // We're done with imgFloat delete[] imgFloat; return regions; } void cgmath::setRegionAxis(const uint32_t &value) { regionAxis = value; } Vector cgmath::findLocalStarPosition(void) const { if (useRapidGuide) { return Vector(rapidDX, rapidDY, 0); } FITSData *imageData = guideView->getImageData(); if (imageGuideEnabled) { float xshift = 0, yshift = 0; QVector shifts; float xsum = 0, ysum = 0; QVector imageParition = partitionImage(); if (imageParition.isEmpty()) { qWarning() << "Failed to partiion regions in image!"; return Vector(-1, -1, -1); } if (imageParition.count() != referenceRegions.count()) { qWarning() << "Mismatch between reference regions #" << referenceRegions.count() << "and image parition regions #" << imageParition.count(); // Clear memory in case of mis-match foreach (float *region, imageParition) { delete[] region; } return Vector(-1, -1, -1); } for (uint8_t i = 0; i < imageParition.count(); i++) { ImageAutoGuiding::ImageAutoGuiding1(referenceRegions[i], imageParition[i], regionAxis, &xshift, &yshift); Vector shift(xshift, yshift, -1); qCDebug(KSTARS_EKOS_GUIDE) << "Region #" << i << ": X-Shift=" << xshift << "Y-Shift=" << yshift; xsum += xshift; ysum += yshift; shifts.append(shift); } // Delete partitions foreach (float *region, imageParition) { delete[] region; } imageParition.clear(); float average_x = xsum / referenceRegions.count(); float average_y = ysum / referenceRegions.count(); float median_x = shifts[referenceRegions.count() / 2 - 1].x; float median_y = shifts[referenceRegions.count() / 2 - 1].y; qCDebug(KSTARS_EKOS_GUIDE) << "Average : X-Shift=" << average_x << "Y-Shift=" << average_y; qCDebug(KSTARS_EKOS_GUIDE) << "Median : X-Shift=" << median_x << "Y-Shift=" << median_y; return Vector(median_x, median_y, -1); } switch (imageData->property("dataType").toInt()) { case TBYTE: return findLocalStarPosition(); break; case TSHORT: return findLocalStarPosition(); break; case TUSHORT: return findLocalStarPosition(); break; case TLONG: return findLocalStarPosition(); break; case TULONG: return findLocalStarPosition(); break; case TFLOAT: return findLocalStarPosition(); break; case TLONGLONG: return findLocalStarPosition(); break; case TDOUBLE: return findLocalStarPosition(); break; default: break; } return Vector(-1, -1, -1); } template Vector cgmath::findLocalStarPosition(void) const { static double P0 = 0.906, P1 = 0.584, P2 = 0.365, P3 = 0.117, P4 = 0.049, P5 = -0.05, P6 = -0.064, P7 = -0.074, P8 = -0.094; Vector ret; int i, j; double resx, resy, mass, threshold, pval; T *psrc = nullptr; T *porigin = nullptr; T *pptr; QRect trackingBox = guideView->getTrackingBox(); if (trackingBox.isValid() == false) return Vector(-1, -1, -1); FITSData *imageData = guideView->getImageData(); if (imageData == nullptr) { qCWarning(KSTARS_EKOS_GUIDE) << "Cannot process a nullptr image."; return Vector(-1, -1, -1); } if (square_alg_idx == SEP_THRESHOLD) { int count = imageData->findStars(ALGORITHM_SEP, trackingBox); if (count > 0) { imageData->getHFR(HFR_MAX); Edge *star = imageData->getMaxHFRStar(); if (star) ret = Vector(star->x, star->y, 0); else ret = Vector(-1, -1, -1); //ret = Vector(star->x, star->y, 0) - Vector(trackingBox.x(), trackingBox.y(), 0); } else ret = Vector(-1, -1, -1); return ret; } T *pdata = reinterpret_cast(imageData->getImageBuffer()); qCDebug(KSTARS_EKOS_GUIDE) << "Tracking Square " << trackingBox; double square_square = trackingBox.width() * trackingBox.width(); psrc = porigin = pdata + trackingBox.y() * video_width + trackingBox.x(); resx = resy = 0; threshold = mass = 0; // several threshold adaptive smart agorithms switch (square_alg_idx) { case CENTROID_THRESHOLD: { int width = trackingBox.width(); int height = trackingBox.width(); float i0, i1, i2, i3, i4, i5, i6, i7, i8; int ix = 0, iy = 0; int xM4; T *p; double average, fit, bestFit = 0; int minx = 0; int maxx = width; int miny = 0; int maxy = height; for (int x = minx; x < maxx; x++) for (int y = miny; y < maxy; y++) { i0 = i1 = i2 = i3 = i4 = i5 = i6 = i7 = i8 = 0; xM4 = x - 4; p = psrc + (y - 4) * video_width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y - 3) * video_width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i7 += *p++; i6 += *p++; i7 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y - 2) * video_width + xM4; i8 += *p++; i8 += *p++; i5 += *p++; i4 += *p++; i3 += *p++; i4 += *p++; i5 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y - 1) * video_width + xM4; i8 += *p++; i7 += *p++; i4 += *p++; i2 += *p++; i1 += *p++; i2 += *p++; i4 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y + 0) * video_width + xM4; i8 += *p++; i6 += *p++; i3 += *p++; i1 += *p++; i0 += *p++; i1 += *p++; i3 += *p++; i6 += *p++; i8 += *p++; p = psrc + (y + 1) * video_width + xM4; i8 += *p++; i7 += *p++; i4 += *p++; i2 += *p++; i1 += *p++; i2 += *p++; i4 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y + 2) * video_width + xM4; i8 += *p++; i8 += *p++; i5 += *p++; i4 += *p++; i3 += *p++; i4 += *p++; i5 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y + 3) * video_width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i7 += *p++; i6 += *p++; i7 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; p = psrc + (y + 4) * video_width + xM4; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; i8 += *p++; average = (i0 + i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8) / 85.0; fit = P0 * (i0 - average) + P1 * (i1 - 4 * average) + P2 * (i2 - 4 * average) + P3 * (i3 - 4 * average) + P4 * (i4 - 8 * average) + P5 * (i5 - 4 * average) + P6 * (i6 - 4 * average) + P7 * (i7 - 8 * average) + P8 * (i8 - 48 * average); if (bestFit < fit) { bestFit = fit; ix = x; iy = y; } } if (bestFit > 50) { double sumX = 0; double sumY = 0; double total = 0; for (int y = iy - 4; y <= iy + 4; y++) { p = psrc + y * width + ix - 4; for (int x = ix - 4; x <= ix + 4; x++) { double w = *p++; sumX += x * w; sumY += y * w; total += w; } } if (total > 0) { ret = (Vector(trackingBox.x(), trackingBox.y(), 0) + Vector(sumX / total, sumY / total, 0)); return ret; } } return Vector(-1, -1, -1); } break; // Alexander's Stepanenko smart threshold algorithm case SMART_THRESHOLD: { point_t bbox_lt = { trackingBox.x() - SMART_FRAME_WIDTH, trackingBox.y() - SMART_FRAME_WIDTH }; point_t bbox_rb = { trackingBox.x() + trackingBox.width() + SMART_FRAME_WIDTH, trackingBox.y() + trackingBox.width() + SMART_FRAME_WIDTH }; int offset = 0; // clip frame if (bbox_lt.x < 0) bbox_lt.x = 0; if (bbox_lt.y < 0) bbox_lt.y = 0; if (bbox_rb.x > video_width) bbox_rb.x = video_width; if (bbox_rb.y > video_height) bbox_rb.y = video_height; // calc top bar int box_wd = bbox_rb.x - bbox_lt.x; int box_ht = trackingBox.y() - bbox_lt.y; int pix_cnt = 0; if (box_wd > 0 && box_ht > 0) { pix_cnt += box_wd * box_ht; for (j = bbox_lt.y; j < trackingBox.y(); ++j) { offset = j * video_width; for (i = bbox_lt.x; i < bbox_rb.x; ++i) { pptr = pdata + offset + i; threshold += *pptr; } } } // calc left bar box_wd = trackingBox.x() - bbox_lt.x; box_ht = trackingBox.width(); if (box_wd > 0 && box_ht > 0) { pix_cnt += box_wd * box_ht; for (j = trackingBox.y(); j < trackingBox.y() + box_ht; ++j) { offset = j * video_width; for (i = bbox_lt.x; i < trackingBox.x(); ++i) { pptr = pdata + offset + i; threshold += *pptr; } } } // calc right bar box_wd = bbox_rb.x - trackingBox.x() - trackingBox.width(); box_ht = trackingBox.width(); if (box_wd > 0 && box_ht > 0) { pix_cnt += box_wd * box_ht; for (j = trackingBox.y(); j < trackingBox.y() + box_ht; ++j) { offset = j * video_width; for (i = trackingBox.x() + trackingBox.width(); i < bbox_rb.x; ++i) { pptr = pdata + offset + i; threshold += *pptr; } } } // calc bottom bar box_wd = bbox_rb.x - bbox_lt.x; box_ht = bbox_rb.y - trackingBox.y() - trackingBox.width(); if (box_wd > 0 && box_ht > 0) { pix_cnt += box_wd * box_ht; for (j = trackingBox.y() + trackingBox.width(); j < bbox_rb.y; ++j) { offset = j * video_width; for (i = bbox_lt.x; i < bbox_rb.x; ++i) { pptr = pdata + offset + i; threshold += *pptr; } } } // find maximum double max_val = 0; for (j = 0; j < trackingBox.width(); ++j) { for (i = 0; i < trackingBox.width(); ++i) { pptr = psrc + i; if (*pptr > max_val) max_val = *pptr; } psrc += video_width; } if (pix_cnt != 0) threshold /= (double)pix_cnt; // cut by 10% higher then average threshold if (max_val > threshold) threshold += (max_val - threshold) * SMART_CUT_FACTOR; //log_i("smart thr. = %f cnt = %d", threshold, pix_cnt); break; } // simple adaptive threshold case AUTO_THRESHOLD: { for (j = 0; j < trackingBox.width(); ++j) { for (i = 0; i < trackingBox.width(); ++i) { pptr = psrc + i; threshold += *pptr; } psrc += video_width; } threshold /= square_square; break; } // no threshold subtracion default: { } } psrc = porigin; for (j = 0; j < trackingBox.width(); ++j) { for (i = 0; i < trackingBox.width(); ++i) { pptr = psrc + i; pval = *pptr - threshold; pval = pval < 0 ? 0 : pval; resx += (double)i * pval; resy += (double)j * pval; mass += pval; } psrc += video_width; } if (mass == 0) mass = 1; resx /= mass; resy /= mass; ret = Vector(trackingBox.x(), trackingBox.y(), 0) + Vector(resx, resy, 0); return ret; } void cgmath::process_axes(void) { int cnt = 0; double t_delta = 0; qCDebug(KSTARS_EKOS_GUIDE) << "Processing Axes"; in_params.proportional_gain[0] = Options::rAProportionalGain(); in_params.proportional_gain[1] = Options::dECProportionalGain(); in_params.integral_gain[0] = Options::rAIntegralGain(); in_params.integral_gain[1] = Options::rAIntegralGain(); in_params.derivative_gain[0] = Options::rADerivativeGain(); in_params.derivative_gain[1] = Options::dECDerivativeGain(); in_params.enabled[0] = Options::rAGuideEnabled(); in_params.enabled[1] = Options::dECGuideEnabled(); in_params.min_pulse_length[0] = Options::rAMinimumPulse(); in_params.min_pulse_length[1] = Options::dECMinimumPulse(); in_params.max_pulse_length[0] = Options::rAMaximumPulse(); in_params.max_pulse_length[1] = Options::dECMaximumPulse(); // RA W/E enable // East RA+ enabled? in_params.enabled_axis1[0] = Options::eastRAGuideEnabled(); // West RA- enabled? in_params.enabled_axis2[0] = Options::westRAGuideEnabled(); // DEC N/S enable // North DEC+ enabled? in_params.enabled_axis1[1] = Options::northDECGuideEnabled(); // South DEC- enabled? in_params.enabled_axis2[1] = Options::southDECGuideEnabled(); // process axes... for (int k = GUIDE_RA; k <= GUIDE_DEC; k++) { // zero all out commands out_params.pulse_dir[k] = NO_DIR; if (accum_ticks[k] < in_params.accum_frame_cnt[k] - 1) continue; t_delta = 0; drift_integral[k] = 0; cnt = in_params.accum_frame_cnt[k]; for (int i = 0, idx = channel_ticks[k]; i < cnt; ++i) { t_delta += drift[k][idx]; qCDebug(KSTARS_EKOS_GUIDE) << "At #" << idx << "drift[" << k << "][" << idx << "] = " << drift[k][idx] << " , t_delta: " << t_delta; if (idx > 0) --idx; else idx = MAX_ACCUM_CNT - 1; } for (int i = 0; i < MAX_ACCUM_CNT; ++i) drift_integral[k] += drift[k][i]; out_params.delta[k] = t_delta / (double)cnt; drift_integral[k] /= (double)MAX_ACCUM_CNT; qCDebug(KSTARS_EKOS_GUIDE) << "delta [" << k << "]= " << out_params.delta[k]; qCDebug(KSTARS_EKOS_GUIDE) << "drift_integral[" << k << "]= " << drift_integral[k]; out_params.pulse_length[k] = fabs(out_params.delta[k] * in_params.proportional_gain[k] + drift_integral[k] * in_params.integral_gain[k]); out_params.pulse_length[k] = out_params.pulse_length[k] <= in_params.max_pulse_length[k] ? out_params.pulse_length[k] : in_params.max_pulse_length[k]; qCDebug(KSTARS_EKOS_GUIDE) << "pulse_length [" << k << "]= " << out_params.pulse_length[k]; // calc direction // We do not send pulse if direction is disabled completely, or if direction in a specific axis (e.g. N or S) is disabled if (!in_params.enabled[k] || (out_params.delta[k] > 0 && !in_params.enabled_axis1[k]) || (out_params.delta[k] < 0 && !in_params.enabled_axis2[k])) { out_params.pulse_dir[k] = NO_DIR; out_params.pulse_length[k] = 0; continue; } if (out_params.pulse_length[k] >= in_params.min_pulse_length[k]) { if (k == GUIDE_RA) out_params.pulse_dir[k] = out_params.delta[k] > 0 ? RA_DEC_DIR : RA_INC_DIR; // GUIDE_RA. right dir - decreases GUIDE_RA else { out_params.pulse_dir[k] = out_params.delta[k] > 0 ? DEC_INC_DIR : DEC_DEC_DIR; // GUIDE_DEC. // Reverse DEC direction if we are looking eastward //if (ROT_Z.x[0][0] > 0 || (ROT_Z.x[0][0] ==0 && ROT_Z.x[0][1] > 0)) //out_params.pulse_dir[k] = (out_params.pulse_dir[k] == DEC_INC_DIR) ? DEC_DEC_DIR : DEC_INC_DIR; } } else out_params.pulse_dir[k] = NO_DIR; qCDebug(KSTARS_EKOS_GUIDE) << "Direction : " << get_direction_string(out_params.pulse_dir[k]); } //emit newAxisDelta(out_params.delta[0], out_params.delta[1]); if (Options::guideLogging()) { QTextStream out(&logFile); out << ticks << "," << logTime.elapsed() << "," << out_params.delta[0] << "," << out_params.pulse_length[0] << "," << get_direction_string(out_params.pulse_dir[0]) << "," << out_params.delta[1] << "," << out_params.pulse_length[1] << "," << get_direction_string(out_params.pulse_dir[1]) << endl; } } void cgmath::performProcessing(void) { Vector arc_star_pos, arc_reticle_pos; // do nothing if suspended if (suspended) return; // find guiding star location in scr_star_pos = star_pos = findLocalStarPosition(); if (star_pos.x == -1 || std::isnan(star_pos.x)) { lost_star = true; return; } else lost_star = false; // move square overlay //TODO FIXME //moveSquare( round(star_pos.x) - (double)square_size/(2*subBinX), round(star_pos.y) - (double)square_size/(2*subBinY) ); QVector3D starCenter(star_pos.x, star_pos.y, 0); emit newStarPosition(starCenter, true); if (preview_mode) return; qCDebug(KSTARS_EKOS_GUIDE) << "################## BEGIN PROCESSING ##################"; // translate star coords into sky coord. system // convert from pixels into arcsecs arc_star_pos = point2arcsec(star_pos); arc_reticle_pos = point2arcsec(reticle_pos); qCDebug(KSTARS_EKOS_GUIDE) << "Star X : " << star_pos.x << " Y : " << star_pos.y; qCDebug(KSTARS_EKOS_GUIDE) << "Reticle X : " << reticle_pos.x << " Y :" << reticle_pos.y; qCDebug(KSTARS_EKOS_GUIDE) << "Star RA: " << arc_star_pos.x << " DEC: " << arc_star_pos.y; qCDebug(KSTARS_EKOS_GUIDE) << "Reticle RA: " << arc_reticle_pos.x << " DEC: " << arc_reticle_pos.y; // translate into sky coords. star_pos = arc_star_pos - arc_reticle_pos; star_pos.y = -star_pos.y; // invert y-axis as y picture axis is inverted qCDebug(KSTARS_EKOS_GUIDE) << "-------> BEFORE ROTATION Diff RA: " << star_pos.x << " DEC: " << star_pos.y; star_pos = star_pos * ROT_Z; // both coords are ready for math processing //put coord to drift list drift[GUIDE_RA][channel_ticks[GUIDE_RA]] = star_pos.x; drift[GUIDE_DEC][channel_ticks[GUIDE_DEC]] = star_pos.y; qCDebug(KSTARS_EKOS_GUIDE) << "-------> AFTER ROTATION Diff RA: " << star_pos.x << " DEC: " << star_pos.y; qCDebug(KSTARS_EKOS_GUIDE) << "RA channel ticks: " << channel_ticks[GUIDE_RA] << " DEC channel ticks: " << channel_ticks[GUIDE_DEC]; // make decision by axes process_axes(); // process statistics calc_square_err(); // finally process tickers do_ticks(); qCDebug(KSTARS_EKOS_GUIDE) << "################## FINISH PROCESSING ##################"; } void cgmath::calc_square_err(void) { if (!do_statistics) return; // through MAX_ACCUM_CNT values if (ticks == 0) return; for (int k = GUIDE_RA; k <= GUIDE_DEC; k++) { double sqr_avg = 0; for (int i = 0; i < MAX_ACCUM_CNT; ++i) sqr_avg += drift[k][i] * drift[k][i]; out_params.sigma[k] = sqrt(sqr_avg / (double)MAX_ACCUM_CNT); } } void cgmath::setRapidGuide(bool enable) { useRapidGuide = enable; } double cgmath::getDitherRate(int axis) { if (axis < 0 || axis > 1) return -1; return ditherRate[axis]; } void cgmath::setRapidStarData(double dx, double dy) { rapidDX = dx; rapidDY = dy; } const char *cgmath::get_direction_string(GuideDirection dir) { switch (dir) { case RA_DEC_DIR: return "Decrease RA"; break; case RA_INC_DIR: return "Increase RA"; break; case DEC_DEC_DIR: return "Decrease DEC"; break; case DEC_INC_DIR: return "Increase DEC"; break; default: break; } return "NO DIR"; } bool cgmath::isImageGuideEnabled() const { return imageGuideEnabled; } void cgmath::setImageGuideEnabled(bool value) { imageGuideEnabled = value; } static void psf_conv(float *dst, const float *src, int width, int height) { //dst.Init(src.Size); // A B1 B2 C1 C2 C3 D1 D2 D3 const double PSF[] = { 0.906, 0.584, 0.365, .117, .049, -0.05, -.064, -.074, -.094 }; //memset(dst.px, 0, src.NPixels * sizeof(float)); /* PSF Grid is: D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 D2 D1 D2 D3 D3 D3 D3 D3 C3 C2 C1 C2 C3 D3 D3 D3 D2 C2 B2 B1 B2 C2 D2 D3 D3 D1 C1 B1 A B1 C1 D1 D3 D3 D2 C2 B2 B1 B2 C2 D2 D3 D3 D3 C3 C2 C1 C2 C3 D3 D3 D3 D3 D3 D2 D1 D2 D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 D3 1@A 4@B1, B2, C1, C3, D1 8@C2, D2 44 * D3 */ int psf_size = 4; for (int y = psf_size; y < height - psf_size; y++) { for (int x = psf_size; x < width - psf_size; x++) { float A, B1, B2, C1, C2, C3, D1, D2, D3; #define PX(dx, dy) *(src + width * (y + (dy)) + x + (dx)) A = PX(+0, +0); B1 = PX(+0, -1) + PX(+0, +1) + PX(+1, +0) + PX(-1, +0); B2 = PX(-1, -1) + PX(+1, -1) + PX(-1, +1) + PX(+1, +1); C1 = PX(+0, -2) + PX(-2, +0) + PX(+2, +0) + PX(+0, +2); C2 = PX(-1, -2) + PX(+1, -2) + PX(-2, -1) + PX(+2, -1) + PX(-2, +1) + PX(+2, +1) + PX(-1, +2) + PX(+1, +2); C3 = PX(-2, -2) + PX(+2, -2) + PX(-2, +2) + PX(+2, +2); D1 = PX(+0, -3) + PX(-3, +0) + PX(+3, +0) + PX(+0, +3); D2 = PX(-1, -3) + PX(+1, -3) + PX(-3, -1) + PX(+3, -1) + PX(-3, +1) + PX(+3, +1) + PX(-1, +3) + PX(+1, +3); D3 = PX(-4, -2) + PX(-3, -2) + PX(+3, -2) + PX(+4, -2) + PX(-4, -1) + PX(+4, -1) + PX(-4, +0) + PX(+4, +0) + PX(-4, +1) + PX(+4, +1) + PX(-4, +2) + PX(-3, +2) + PX(+3, +2) + PX(+4, +2); #undef PX int i; const float *uptr; uptr = src + width * (y - 4) + (x - 4); for (i = 0; i < 9; i++) D3 += *uptr++; uptr = src + width * (y - 3) + (x - 4); for (i = 0; i < 3; i++) D3 += *uptr++; uptr += 3; for (i = 0; i < 3; i++) D3 += *uptr++; uptr = src + width * (y + 3) + (x - 4); for (i = 0; i < 3; i++) D3 += *uptr++; uptr += 3; for (i = 0; i < 3; i++) D3 += *uptr++; uptr = src + width * (y + 4) + (x - 4); for (i = 0; i < 9; i++) D3 += *uptr++; double mean = (A + B1 + B2 + C1 + C2 + C3 + D1 + D2 + D3) / 81.0; double PSF_fit = PSF[0] * (A - mean) + PSF[1] * (B1 - 4.0 * mean) + PSF[2] * (B2 - 4.0 * mean) + PSF[3] * (C1 - 4.0 * mean) + PSF[4] * (C2 - 8.0 * mean) + PSF[5] * (C3 - 4.0 * mean) + PSF[6] * (D1 - 4.0 * mean) + PSF[7] * (D2 - 8.0 * mean) + PSF[8] * (D3 - 44.0 * mean); dst[width * y + x] = (float) PSF_fit; } } } static void GetStats(double *mean, double *stdev, int width, const float *img, const QRect& win) { // Determine the mean and standard deviation double sum = 0.0; double a = 0.0; double q = 0.0; double k = 1.0; double km1 = 0.0; const float *p0 = img + win.top() * width + win.left(); for (int y = 0; y < win.height(); y++) { const float *end = p0 + win.height(); for (const float *p = p0; p < end; p++) { double const x = (double) *p; sum += x; double const a0 = a; a += (x - a) / k; q += (x - a0) * (x - a); km1 = k; k += 1.0; } p0 += width; } *mean = sum / km1; *stdev = sqrt(q / km1); } static void RemoveItems(std::set& stars, const std::set& to_erase) { int n = 0; for (std::set::iterator it = stars.begin(); it != stars.end(); n++) { if (to_erase.find(n) != to_erase.end()) { std::set::iterator next = it; ++next; stars.erase(it); it = next; } else ++it; } } // Based on PHD2 algorithm QList cgmath::PSFAutoFind(int extraEdgeAllowance) { //Debug.Write(wxString::Format("Star::AutoFind called with edgeAllowance = %d searchRegion = %d\n", extraEdgeAllowance, searchRegion)); // run a 3x3 median first to eliminate hot pixels //usImage smoothed; //smoothed.CopyFrom(image); //Median3(smoothed); FITSData *smoothed = new FITSData(guideView->getImageData()); smoothed->applyFilter(FITS_MEDIAN); int searchRegion = guideView->getTrackingBox().width(); int subW = smoothed->width(); int subH = smoothed->height(); int size = subW*subH; // convert to floating point float *conv = createFloatImage(smoothed); // run the PSF convolution { float *tmp = new float[size]; memset(tmp, 0, size*sizeof(float)); psf_conv(tmp, conv, subW, subH); delete [] conv; // Swap conv = tmp; } enum { CONV_RADIUS = 4 }; int dw = subW; // width of the downsampled image int dh = subH; // height of the downsampled image QRect convRect(CONV_RADIUS, CONV_RADIUS, dw - 2 * CONV_RADIUS, dh - 2 * CONV_RADIUS); // region containing valid data enum { TOP_N = 100 }; // keep track of the brightest stars std::set stars; // sorted by ascending intensity double global_mean, global_stdev; GetStats(&global_mean, &global_stdev, subW, conv, convRect); //Debug.Write(wxString::Format("AutoFind: global mean = %.1f, stdev %.1f\n", global_mean, global_stdev)); const double threshold = 0.1; //Debug.Write(wxString::Format("AutoFind: using threshold = %.1f\n", threshold)); // find each local maximum int srch = 4; for (int y = convRect.top() + srch; y <= convRect.bottom() - srch; y++) { for (int x = convRect.left() + srch; x <= convRect.right() - srch; x++) { float val = conv[dw * y + x]; bool ismax = false; if (val > 0.0) { ismax = true; for (int j = -srch; j <= srch; j++) { for (int i = -srch; i <= srch; i++) { if (i == 0 && j == 0) continue; if (conv[dw * (y + j) + (x + i)] > val) { ismax = false; break; } } } } if (!ismax) continue; // compare local maximum to mean value of surrounding pixels const int local = 7; double local_mean, local_stdev; QRect localRect(x - local, y - local, 2 * local + 1, 2 * local + 1); localRect = localRect.intersected(convRect); GetStats(&local_mean, &local_stdev, subW, conv, localRect); // this is our measure of star intensity double h = (val - local_mean) / global_stdev; if (h < threshold) { // Debug.Write(wxString::Format("AG: local max REJECT [%d, %d] PSF %.1f SNR %.1f\n", imgx, imgy, val, SNR)); continue; } // coordinates on the original image int downsample =1; int imgx = x * downsample + downsample / 2; int imgy = y * downsample + downsample / 2; stars.insert(Peak(imgx, imgy, h)); if (stars.size() > TOP_N) stars.erase(stars.begin()); } } //for (std::set::const_reverse_iterator it = stars.rbegin(); it != stars.rend(); ++it) //qCDebug(KSTARS_EKOS_GUIDE) << "AutoFind: local max [" << it->x << "," << it->y << "]" << it->val; // merge stars that are very close into a single star { const int minlimitsq = 5 * 5; repeat: for (std::set::const_iterator a = stars.begin(); a != stars.end(); ++a) { std::set::const_iterator b = a; ++b; for (; b != stars.end(); ++b) { int dx = a->x - b->x; int dy = a->y - b->y; int d2 = dx * dx + dy * dy; if (d2 < minlimitsq) { // very close, treat as single star //Debug.Write(wxString::Format("AutoFind: merge [%d, %d] %.1f - [%d, %d] %.1f\n", a->x, a->y, a->val, b->x, b->y, b->val)); // erase the dimmer one stars.erase(a); goto repeat; } } } } // exclude stars that would fit within a single searchRegion box { // build a list of stars to be excluded std::set to_erase; const int extra = 5; // extra safety margin const int fullw = searchRegion + extra; for (std::set::const_iterator a = stars.begin(); a != stars.end(); ++a) { std::set::const_iterator b = a; ++b; for (; b != stars.end(); ++b) { int dx = abs(a->x - b->x); int dy = abs(a->y - b->y); if (dx <= fullw && dy <= fullw) { // stars closer than search region, exclude them both // but do not let a very dim star eliminate a very bright star if (b->val / a->val >= 5.0) { //Debug.Write(wxString::Format("AutoFind: close dim-bright [%d, %d] %.1f - [%d, %d] %.1f\n", a->x, a->y, a->val, b->x, b->y, b->val)); } else { //Debug.Write(wxString::Format("AutoFind: too close [%d, %d] %.1f - [%d, %d] %.1f\n", a->x, a->y, a->val, b->x, b->y, b->val)); to_erase.insert(std::distance(stars.begin(), a)); to_erase.insert(std::distance(stars.begin(), b)); } } } } RemoveItems(stars, to_erase); } // exclude stars too close to the edge { enum { MIN_EDGE_DIST = 40 }; int edgeDist = MIN_EDGE_DIST;//pConfig->Profile.GetInt("/StarAutoFind/MinEdgeDist", MIN_EDGE_DIST); if (edgeDist < searchRegion) edgeDist = searchRegion; edgeDist += extraEdgeAllowance; std::set::iterator it = stars.begin(); while (it != stars.end()) { std::set::iterator next = it; ++next; if (it->x <= edgeDist || it->x >= subW - edgeDist || it->y <= edgeDist || it->y >= subH - edgeDist) { //Debug.Write(wxString::Format("AutoFind: too close to edge [%d, %d] %.1f\n", it->x, it->y, it->val)); stars.erase(it); } it = next; } } QList centers; for (std::set::reverse_iterator it = stars.rbegin(); it != stars.rend(); ++it) { Edge *center = new Edge; center->x = it->x; center->y = it->y; center->val = it->val; centers.append(center); } delete [] conv; delete (smoothed); return centers; } //--------------------------------------------------------------------------------------- cproc_in_params::cproc_in_params() { reset(); } void cproc_in_params::reset(void) { threshold_alg_idx = CENTROID_THRESHOLD; average = true; for (int k = GUIDE_RA; k <= GUIDE_DEC; k++) { enabled[k] = true; accum_frame_cnt[k] = 1; integral_gain[k] = 0; derivative_gain[k] = 0; max_pulse_length[k] = 5000; min_pulse_length[k] = 100; } } cproc_out_params::cproc_out_params() { reset(); } void cproc_out_params::reset(void) { for (int k = GUIDE_RA; k <= GUIDE_DEC; k++) { delta[k] = 0; pulse_dir[k] = NO_DIR; pulse_length[k] = 0; sigma[k] = 0; } } diff --git a/kstars/fitsviewer/fitsdata.cpp b/kstars/fitsviewer/fitsdata.cpp index 0785e3bb1..40d877018 100644 --- a/kstars/fitsviewer/fitsdata.cpp +++ b/kstars/fitsviewer/fitsdata.cpp @@ -1,4654 +1,4668 @@ /*************************************************************************** FITSImage.cpp - FITS Image ------------------- begin : Thu Jan 22 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* * See http://members.aol.com/pkirchg for more details. * ***************************************************************************/ #include "fitsdata.h" #include "sep/sep.h" #include "fpack.h" #include "kstarsdata.h" #include "ksutils.h" #include "Options.h" #include "skymapcomposite.h" #include "auxiliary/ksnotification.h" #include #include #include #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) #include #include #endif +#ifndef KSTARS_LITE +#include "fitshistogram.h" +#endif + #include #include #include #define ZOOM_DEFAULT 100.0 #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 const int MINIMUM_ROWS_PER_CENTER = 3; const QString FITSData::m_TemporaryPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation); #define DIFFUSE_THRESHOLD 0.15 #define MAX_EDGE_LIMIT 10000 #define LOW_EDGE_CUTOFF_1 50 #define LOW_EDGE_CUTOFF_2 10 #define MINIMUM_EDGE_LIMIT 2 bool greaterThan(Edge *s1, Edge *s2) { //return s1->width > s2->width; return s1->sum > s2->sum; } FITSData::FITSData(FITSMode fitsMode): m_Mode(fitsMode) { debayerParams.method = DC1394_BAYER_METHOD_NEAREST; debayerParams.filter = DC1394_COLOR_FILTER_RGGB; debayerParams.offsetX = debayerParams.offsetY = 0; } FITSData::FITSData(const FITSData *other) { debayerParams.method = DC1394_BAYER_METHOD_NEAREST; debayerParams.filter = DC1394_COLOR_FILTER_RGGB; debayerParams.offsetX = debayerParams.offsetY = 0; this->m_Mode = other->m_Mode; this->m_DataType = other->m_DataType; this->m_Channels = other->m_Channels; memcpy(&stats, &(other->stats), sizeof(stats)); m_ImageBuffer = new uint8_t[stats.samples_per_channel*m_Channels*stats.bytesPerPixel]; memcpy(m_ImageBuffer, other->m_ImageBuffer, stats.samples_per_channel*m_Channels*stats.bytesPerPixel); } FITSData::~FITSData() { int status = 0; clearImageBuffers(); if (starCenters.count() > 0) qDeleteAll(starCenters); delete[] wcs_coord; if (objList.count() > 0) qDeleteAll(objList); if (fptr != nullptr) { fits_close_file(fptr, &status); if (m_isTemporary && autoRemoveTemporaryFITS) QFile::remove(m_Filename); } qDeleteAll(records); } QFuture FITSData::loadFITS(const QString &inFilename, bool silent) { int status = 0; qDeleteAll(starCenters); starCenters.clear(); if (fptr != nullptr) { fits_close_file(fptr, &status); // If current file is temporary AND // Auto Remove Temporary File is Set AND // New filename is different from existing filename // THen remove it. We have to check for name since we cannot delete // the same filename and try to open it below! if (m_isTemporary && autoRemoveTemporaryFITS && inFilename != m_Filename) QFile::remove(m_Filename); } m_Filename = inFilename; qCInfo(KSTARS_FITS) << "Loading FITS file " << m_Filename; QFuture result = QtConcurrent::run(this, &FITSData::privateLoad, silent); return result; } bool FITSData::privateLoad(bool silent) { int status = 0, anynull = 0; long naxes[3]; char error_status[512]; QString errMessage; if (m_Filename.endsWith(".fz")) { QString uncompressedFile = QDir::tempPath() + QString("/%1").arg(QUuid::createUuid().toString().remove(QRegularExpression("[-{}]"))); m_isTemporary = true; fpstate fpvar; std::vector arguments = {"funpack", m_Filename.toLatin1().toStdString()}; std::vector arglist; for (const auto& arg : arguments) arglist.push_back((char*)arg.data()); arglist.push_back(nullptr); int argc = arglist.size() - 1; char **argv = arglist.data(); // TODO: Check for errors fp_init (&fpvar); fp_get_param (argc, argv, &fpvar); fp_preflight (argc, argv, FUNPACK, &fpvar); fp_loop (argc, argv, FUNPACK, uncompressedFile.toLatin1().data(), fpvar); m_Filename = uncompressedFile; } else if (m_Filename.startsWith(m_TemporaryPath)) m_isTemporary = true; // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. if (fits_open_diskfile(&fptr, m_Filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); errMessage = i18n("Could not open file %1. Error %2", m_Filename, QString::fromUtf8(error_status)); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } stats.size = QFile(m_Filename).size(); if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); errMessage = i18n("Could not locate image HDU. Error %1", QString::fromUtf8(error_status)); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } if (fits_get_img_param(fptr, 3, &(stats.bitpix), &(stats.ndim), naxes, &status)) { fits_report_error(stderr, status); fits_get_errstatus(status, error_status); errMessage = i18n("FITS file open error (fits_get_img_param): %1", QString::fromUtf8(error_status)); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } if (stats.ndim < 2) { errMessage = i18n("1D FITS images are not supported in KStars."); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } switch (stats.bitpix) { case BYTE_IMG: m_DataType = TBYTE; stats.bytesPerPixel = sizeof(uint8_t); break; case SHORT_IMG: // Read SHORT image as USHORT m_DataType = TUSHORT; stats.bytesPerPixel = sizeof(int16_t); break; case USHORT_IMG: m_DataType = TUSHORT; stats.bytesPerPixel = sizeof(uint16_t); break; case LONG_IMG: // Read LONG image as ULONG m_DataType = TULONG; stats.bytesPerPixel = sizeof(int32_t); break; case ULONG_IMG: m_DataType = TULONG; stats.bytesPerPixel = sizeof(uint32_t); break; case FLOAT_IMG: m_DataType = TFLOAT; stats.bytesPerPixel = sizeof(float); break; case LONGLONG_IMG: m_DataType = TLONGLONG; stats.bytesPerPixel = sizeof(int64_t); break; case DOUBLE_IMG: m_DataType = TDOUBLE; stats.bytesPerPixel = sizeof(double); break; default: errMessage = i18n("Bit depth %1 is not supported.", stats.bitpix); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } if (stats.ndim < 3) naxes[2] = 1; if (naxes[0] == 0 || naxes[1] == 0) { errMessage = i18n("Image has invalid dimensions %1x%2", naxes[0], naxes[1]); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); qCCritical(KSTARS_FITS) << errMessage; return false; } stats.width = naxes[0]; stats.height = naxes[1]; stats.samples_per_channel = stats.width * stats.height; clearImageBuffers(); m_Channels = naxes[2]; // Channels always set to #1 if we are not required to process 3D Cubes // Or if mode is not FITS_NORMAL (guide, focus..etc) if (m_Mode != FITS_NORMAL || !Options::auto3DCube()) m_Channels = 1; //image_buffer = new float[stats.samples_per_channel * channels]; m_ImageBuffer = new uint8_t[stats.samples_per_channel * m_Channels * stats.bytesPerPixel]; //if (image_buffer == nullptr) if (m_ImageBuffer == nullptr) { qCWarning(KSTARS_FITS) << "FITSData: Not enough memory for image_buffer channel. Requested: " << stats.samples_per_channel * m_Channels * stats.bytesPerPixel << " bytes."; clearImageBuffers(); return false; } rotCounter = 0; flipHCounter = 0; flipVCounter = 0; long nelements = stats.samples_per_channel * m_Channels; if (fits_read_img(fptr, m_DataType, 1, nelements, nullptr, m_ImageBuffer, &anynull, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); errMessage = i18n("Error reading image: %1", QString(errmsg)); if (!silent) KSNotification::error(errMessage, i18n("FITS Open")); fits_report_error(stderr, status); qCCritical(KSTARS_FITS) << errMessage; return false; } parseHeader(); if (Options::autoDebayer() && checkDebayer()) { bayerBuffer = m_ImageBuffer; if (debayer()) calculateStats(); } else calculateStats(); WCSLoaded = false; if (m_Mode == FITS_NORMAL || m_Mode == FITS_ALIGN) checkForWCS(); starsSearched = false; return true; } int FITSData::saveFITS(const QString &newFilename) { if (newFilename == m_Filename) return 0; if (m_isCompressed) { KSNotification::error(i18n("Saving compressed files is not supported.")); return -1; } int status = 0, exttype = 0; long nelements; fitsfile *new_fptr; if (HasDebayer) { /* close current file */ if (fits_close_file(fptr, &status)) { fits_report_error(stderr, status); return status; } // Skip "!" in the beginning of the new file name QString finalFileName(newFilename); finalFileName.remove('!'); // Remove first otherwise copy will fail below if file exists QFile::remove(finalFileName); if (!QFile::copy(m_Filename, finalFileName)) { qCCritical(KSTARS_FITS()) << "FITS: Failed to copy " << m_Filename << " to " << finalFileName; fptr = nullptr; return -1; } if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; } m_Filename = finalFileName; //fits_open_image(&fptr, filename.toLatin1(), READONLY, &status); // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. fits_open_diskfile(&fptr, m_Filename.toLatin1(), READONLY, &status); fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status); return 0; } nelements = stats.samples_per_channel * m_Channels; /* Create a new File, overwriting existing*/ if (fits_create_file(&new_fptr, newFilename.toLatin1(), &status)) { fits_report_error(stderr, status); return status; } if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_report_error(stderr, status); return status; } /*if (fits_copy_file(fptr, new_fptr, 0, 1, 1, &status)) { fits_report_error(stderr, status); return status; }*/ if (fits_copy_header(fptr, new_fptr, &status)) { fits_report_error(stderr, status); return status; } /* close current file */ if (fits_close_file(fptr, &status)) { fits_report_error(stderr, status); return status; } status = 0; fptr = new_fptr; if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_report_error(stderr, status); return status; } /* Write Data */ if (fits_write_img(fptr, m_DataType, 1, nelements, m_ImageBuffer, &status)) { fits_report_error(stderr, status); return status; } /* Write keywords */ // Minimum if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(stats.min), "Minimum value", &status)) { fits_report_error(stderr, status); return status; } // Maximum if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(stats.max), "Maximum value", &status)) { fits_report_error(stderr, status); return status; } // NAXIS1 if (fits_update_key(fptr, TUSHORT, "NAXIS1", &(stats.width), "length of data axis 1", &status)) { fits_report_error(stderr, status); return status; } // NAXIS2 if (fits_update_key(fptr, TUSHORT, "NAXIS2", &(stats.height), "length of data axis 2", &status)) { fits_report_error(stderr, status); return status; } // ISO Date if (fits_write_date(fptr, &status)) { fits_report_error(stderr, status); return status; } QString history = QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss")); // History if (fits_write_history(fptr, history.toLatin1(), &status)) { fits_report_error(stderr, status); return status; } int rot = 0, mirror = 0; if (rotCounter > 0) { rot = (90 * rotCounter) % 360; if (rot < 0) rot += 360; } if (flipHCounter % 2 != 0 || flipVCounter % 2 != 0) mirror = 1; if ((rot != 0) || (mirror != 0)) rotWCSFITS(rot, mirror); rotCounter = flipHCounter = flipVCounter = 0; if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; } m_Filename = newFilename; fits_flush_file(fptr, &status); qCInfo(KSTARS_FITS) << "Saved FITS file:" << m_Filename; return status; } void FITSData::clearImageBuffers() { delete[] m_ImageBuffer; m_ImageBuffer = nullptr; bayerBuffer = nullptr; } void FITSData::calculateStats(bool refresh) { // Calculate min max calculateMinMax(refresh); // Get standard deviation and mean in one run switch (m_DataType) { case TBYTE: runningAverageStdDev(); break; case TSHORT: runningAverageStdDev(); break; case TUSHORT: runningAverageStdDev(); break; case TLONG: runningAverageStdDev(); break; case TULONG: runningAverageStdDev(); break; case TFLOAT: runningAverageStdDev(); break; case TLONGLONG: runningAverageStdDev(); break; case TDOUBLE: runningAverageStdDev(); break; default: return; } // FIXME That's not really SNR, must implement a proper solution for this value stats.SNR = stats.mean[0] / stats.stddev[0]; if (refresh && markStars) // Let's try to find star positions again after transformation starsSearched = false; } int FITSData::calculateMinMax(bool refresh) { int status, nfound = 0; status = 0; if ((fptr != nullptr) && !refresh) { if (fits_read_key_dbl(fptr, "DATAMIN", &(stats.min[0]), nullptr, &status) == 0) nfound++; if (fits_read_key_dbl(fptr, "DATAMAX", &(stats.max[0]), nullptr, &status) == 0) nfound++; // If we found both keywords, no need to calculate them, unless they are both zeros if (nfound == 2 && !(stats.min[0] == 0 && stats.max[0] == 0)) return 0; } stats.min[0] = 1.0E30; stats.max[0] = -1.0E30; stats.min[1] = 1.0E30; stats.max[1] = -1.0E30; stats.min[2] = 1.0E30; stats.max[2] = -1.0E30; switch (m_DataType) { case TBYTE: calculateMinMax(); break; case TSHORT: calculateMinMax(); break; case TUSHORT: calculateMinMax(); break; case TLONG: calculateMinMax(); break; case TULONG: calculateMinMax(); break; case TFLOAT: calculateMinMax(); break; case TLONGLONG: calculateMinMax(); break; case TDOUBLE: calculateMinMax(); break; default: break; } //qDebug() << "DATAMIN: " << stats.min << " - DATAMAX: " << stats.max; return 0; } template QPair FITSData::getParitionMinMax(uint32_t start, uint32_t stride) { auto *buffer = reinterpret_cast(m_ImageBuffer); T min = std::numeric_limits::max(); T max = std::numeric_limits::min(); uint32_t end = start+stride; for (uint32_t i = start; i < end; i++) { if (buffer[i] < min) min = buffer[i]; else if (buffer[i] > max) max = buffer[i]; } return qMakePair(min,max); } template void FITSData::calculateMinMax() { //QTime timer; //timer.start(); //if (filename.contains("thread")) //{ T min = std::numeric_limits::max(); T max = std::numeric_limits::min(); // Create N threads const uint8_t nThreads = 16; for (int n=0; n < m_Channels; n++) { uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); // Start location for inspecting elements uint32_t tStart = cStart; // List of futures QList>> futures; for (int i=0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::run(this, &FITSData::getParitionMinMax, tStart, (i == (nThreads-1)) ? fStride : tStride)); tStart += tStride; } // Now wait for results for (int i=0; i < nThreads; i++) { QPair result = futures[i].result(); if (result.first < min) min = result.first; if (result.second > max) max = result.second; } stats.min[n] = min; stats.max[n] = max; } #if 0 } else { T *buffer = reinterpret_cast(imageBuffer); if (channels == 1) { for (unsigned int i = 0; i < stats.samples_per_channel; i++) { if (buffer[i] < stats.min[0]) stats.min[0] = buffer[i]; else if (buffer[i] > stats.max[0]) stats.max[0] = buffer[i]; } } else { int g_offset = stats.samples_per_channel; int b_offset = stats.samples_per_channel * 2; for (unsigned int i = 0; i < stats.samples_per_channel; i++) { if (buffer[i] < stats.min[0]) stats.min[0] = buffer[i]; else if (buffer[i] > stats.max[0]) stats.max[0] = buffer[i]; if (buffer[i + g_offset] < stats.min[1]) stats.min[1] = buffer[i + g_offset]; else if (buffer[i + g_offset] > stats.max[1]) stats.max[1] = buffer[i + g_offset]; if (buffer[i + b_offset] < stats.min[2]) stats.min[2] = buffer[i + b_offset]; else if (buffer[i + b_offset] > stats.max[2]) stats.max[2] = buffer[i + b_offset]; } } } qCInfo(KSTARS_FITS) << filename << "MinMax calculation took" << timer.elapsed() << "ms"; #endif } template QPair FITSData::getSquaredSumAndMean(uint32_t start, uint32_t stride) { uint32_t m_n = 2; double m_oldM = 0, m_newM = 0, m_oldS = 0, m_newS = 0; auto *buffer = reinterpret_cast(m_ImageBuffer); uint32_t end = start + stride; for (uint32_t i = start; i < end; i++) { m_newM = m_oldM + (buffer[i] - m_oldM) / m_n; m_newS = m_oldS + (buffer[i] - m_oldM) * (buffer[i] - m_newM); m_oldM = m_newM; m_oldS = m_newS; m_n++; } return qMakePair(m_newM, m_newS); } template void FITSData::runningAverageStdDev() { //QTime timer; //timer.start(); //if (filename.contains("thread")) //{ // Create N threads const uint8_t nThreads = 16; for (int n=0; n < m_Channels; n++) { uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); // Start location for inspecting elements uint32_t tStart = cStart; // List of futures QList>> futures; for (int i=0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::run(this, &FITSData::getSquaredSumAndMean, tStart, (i == (nThreads-1)) ? fStride : tStride)); tStart += tStride; } double mean=0, squared_sum=0; // Now wait for results for (int i=0; i < nThreads; i++) { QPair result = futures[i].result(); mean += result.first; squared_sum += result.second; } double variance = squared_sum / stats.samples_per_channel; stats.mean[n] = mean/nThreads; stats.stddev[n] = sqrt(variance); } #if 0 } else { T *buffer = reinterpret_cast(imageBuffer); if (channels == 1) { int m_n = 2; double m_oldM = 0, m_newM = 0, m_oldS = 0, m_newS = 0; for (unsigned int i = 1; i < stats.samples_per_channel; i++) { m_newM = m_oldM + (buffer[i] - m_oldM) / m_n; m_newS = m_oldS + (buffer[i] - m_oldM) * (buffer[i] - m_newM); m_oldM = m_newM; m_oldS = m_newS; m_n++; } double variance = (m_n == 2 ? 0 : m_newS / (m_n - 2)); stats.mean[0] = m_newM; stats.stddev[0] = sqrt(variance); } else { int m_n[3] = {2,2,2}; double m_oldM[3] = {0}, m_newM[3] = {0}, m_oldS[3] = {0}, m_newS[3] = {0}; T *rBuffer = buffer; T *gBuffer = buffer + stats.samples_per_channel; T *bBuffer = buffer + stats.samples_per_channel * 2; for (unsigned int i = 1; i < stats.samples_per_channel; i++) { m_newM[0] = m_oldM[0] + (rBuffer[i] - m_oldM[0]) / m_n[0]; m_newS[0] = m_oldS[0] + (rBuffer[i] - m_oldM[0]) * (rBuffer[i] - m_newM[0]); m_oldM[0] = m_newM[0]; m_oldS[0] = m_newS[0]; m_n[0]++; m_newM[1] = m_oldM[1] + (gBuffer[i] - m_oldM[1]) / m_n[1]; m_newS[1] = m_oldS[1] + (gBuffer[i] - m_oldM[1]) * (gBuffer[i] - m_newM[1]); m_oldM[1] = m_newM[1]; m_oldS[1] = m_newS[1]; m_n[1]++; m_newM[2] = m_oldM[2] + (bBuffer[i] - m_oldM[2]) / m_n[2]; m_newS[2] = m_oldS[2] + (bBuffer[i] - m_oldM[2]) * (bBuffer[i] - m_newM[2]); m_oldM[2] = m_newM[2]; m_oldS[2] = m_newS[2]; m_n[2]++; } double variance = (m_n[0] == 2 ? 0 : m_newS[0] / (m_n[0] - 2)); stats.mean[0] = m_newM[0]; stats.stddev[0] = sqrt(variance); variance = (m_n[1] == 2 ? 0 : m_newS[1] / (m_n[1] - 2)); stats.mean[1] = m_newM[1]; stats.stddev[1] = sqrt(variance); variance = (m_n[2] == 2 ? 0 : m_newS[2] / (m_n[2] - 2)); stats.mean[2] = m_newM[2]; stats.stddev[2] = sqrt(variance); } } qCInfo(KSTARS_FITS) << filename << "runningMeanStdDev calculation took" << timer.elapsed() << "ms"; #endif } void FITSData::setMinMax(double newMin, double newMax, uint8_t channel) { stats.min[channel] = newMin; stats.max[channel] = newMax; } bool FITSData::parseHeader() { char *header = nullptr; int status = 0, nkeys=0; if (fits_hdr2str(fptr, 0, nullptr, 0, &header, &nkeys, &status)) { fits_report_error(stderr, status); free(header); return false; } QString recordList = QString(header); for (int i = 0; i < nkeys; i++) { Record *oneRecord = new Record; // Quotes cause issues for simplified below so we're removing them. QString record = recordList.mid(i * 80, 80).remove("'"); QStringList properties = record.split(QRegExp("[=/]")); // If it is only a comment if (properties.size() == 1) { oneRecord->key = properties[0].mid(0, 7); oneRecord->comment = properties[0].mid(8).simplified(); } else { oneRecord->key = properties[0].simplified(); oneRecord->value = properties[1].simplified(); if (properties.size() > 2) oneRecord->comment = properties[2].simplified(); // Try to guess the value. // Test for integer & double. If neither, then leave it as "string". bool ok = false; // Is it Integer? oneRecord->value.toInt(&ok); if (ok) oneRecord->value.convert(QMetaType::Int); else { // Is it double? oneRecord->value.toDouble(&ok); if (ok) oneRecord->value.convert(QMetaType::Double); } } records.append(oneRecord); } free(header); return true; } bool FITSData::getRecordValue(const QString &key, QVariant &value) const { for (Record *oneRecord : records) { if (oneRecord->key == key) { value = oneRecord->value; return true; } } return false; } bool FITSData::checkCollision(Edge *s1, Edge *s2) { int dis; //distance int diff_x = s1->x - s2->x; int diff_y = s1->y - s2->y; dis = std::abs(sqrt(diff_x * diff_x + diff_y * diff_y)); dis -= s1->width / 2; dis -= s2->width / 2; if (dis <= 0) //collision return true; //no collision return false; } int FITSData::findCannyStar(FITSData *data, const QRect &boundary) { switch (data->property("dataType").toInt()) { case TBYTE: return FITSData::findCannyStar(data, boundary); case TSHORT: return FITSData::findCannyStar(data, boundary); case TUSHORT: return FITSData::findCannyStar(data, boundary); case TLONG: return FITSData::findCannyStar(data, boundary); case TULONG: return FITSData::findCannyStar(data, boundary); case TFLOAT: return FITSData::findCannyStar(data, boundary); case TLONGLONG: return FITSData::findCannyStar(data, boundary); case TDOUBLE: return FITSData::findCannyStar(data, boundary); default: break; } return 0; } int FITSData::findStars(StarAlgorithm algorithm, const QRect &trackingBox) { int count = 0; starAlgorithm = algorithm; qDeleteAll(starCenters); starCenters.clear(); switch (algorithm) { case ALGORITHM_SEP: count = findSEPStars(trackingBox); break; case ALGORITHM_GRADIENT: count = findCannyStar(this, trackingBox); break; case ALGORITHM_CENTROID: count = findCentroid(trackingBox); break; case ALGORITHM_THRESHOLD: count = findOneStar(trackingBox); break; } starsSearched = true; return count; } template int FITSData::findCannyStar(FITSData *data, const QRect &boundary) { int subX = qMax(0, boundary.isNull() ? 0 : boundary.x()); int subY = qMax(0, boundary.isNull() ? 0 : boundary.y()); int subW = (boundary.isNull() ? data->width() : boundary.width()); int subH = (boundary.isNull() ? data->height() : boundary.height()); int BBP = data->getBytesPerPixel(); uint16_t dataWidth = data->width(); // #1 Find offsets uint32_t size = subW * subH; uint32_t offset = subX + subY * dataWidth; // #2 Create new buffer auto *buffer = new uint8_t[size * BBP]; // If there is no offset, copy whole buffer in one go if (offset == 0) memcpy(buffer, data->getImageBuffer(), size * BBP); else { uint8_t *dataPtr = buffer; uint8_t *origDataPtr = data->getImageBuffer(); uint32_t lineOffset = 0; // Copy data line by line for (int height = subY; height < (subY + subH); height++) { lineOffset = (subX + height * dataWidth) * BBP; memcpy(dataPtr, origDataPtr + lineOffset, subW * BBP); dataPtr += (subW * BBP); } } // #3 Create new FITSData to hold it auto *boundedImage = new FITSData(); boundedImage->stats.width = subW; boundedImage->stats.height = subH; boundedImage->stats.bitpix = data->stats.bitpix; boundedImage->stats.bytesPerPixel = data->stats.bytesPerPixel; boundedImage->stats.samples_per_channel = size; boundedImage->stats.ndim = 2; boundedImage->setProperty("dataType", data->property("dataType")); // #4 Set image buffer and calculate stats. boundedImage->setImageBuffer(buffer); boundedImage->calculateStats(true); // #5 Apply Median + High Contrast filter to remove noise and move data to non-linear domain boundedImage->applyFilter(FITS_MEDIAN); boundedImage->applyFilter(FITS_HIGH_CONTRAST); // #6 Perform Sobel to find gradients and their directions QVector gradients; QVector directions; // TODO Must trace neighbours and assign IDs to each shape so that they can be centered massed // and discarded whenever necessary. It won't work on noisy images unless this is done. boundedImage->sobel(gradients, directions); QVector ids(gradients.size()); int maxID = boundedImage->partition(subW, subH, gradients, ids); //QVector thresholded = boundedImage->threshold(boundedImage->stats.mean[0], boundedImage->stats.max[0], gradients); // Not needed anymore delete boundedImage; if (maxID == 0) return 0; typedef struct { float massX = 0; float massY = 0; float totalMass = 0; } massInfo; QMap masses; // #7 Calculate center of mass for all detected regions for (int y = 0; y < subH; y++) { for (int x = 0; x < subW; x++) { int index = x + y * subW; int regionID = ids[index]; if (regionID > 0) { float pixel = gradients[index]; masses[regionID].totalMass += pixel; masses[regionID].massX += x * pixel; masses[regionID].massY += y * pixel; } } } // Compare multiple masses, and only select the highest total mass one as the desired star int maxRegionID = 1; int maxTotalMass = masses[1].totalMass; double totalMassRatio = 1e6; for (auto key : masses.keys()) { massInfo oneMass = masses.value(key); if (oneMass.totalMass > maxTotalMass) { totalMassRatio = oneMass.totalMass / maxTotalMass; maxTotalMass = oneMass.totalMass; maxRegionID = key; } } // If image has many regions and there is no significant relative center of mass then it's just noise and no stars // are probably there above a useful threshold. if (maxID > 10 && totalMassRatio < 1.5) return 0; auto *center = new Edge; center->width = -1; center->x = masses[maxRegionID].massX / masses[maxRegionID].totalMass + 0.5; center->y = masses[maxRegionID].massY / masses[maxRegionID].totalMass + 0.5; center->HFR = 1; // Maximum Radius int maxR = qMin(subW - 1, subH - 1) / 2; for (int r = maxR; r > 1; r--) { int pass = 0; for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 36.0) { int testX = center->x + std::cos(theta) * r; int testY = center->y + std::sin(theta) * r; // if out of bound, break; if (testX < 0 || testX >= subW || testY < 0 || testY >= subH) break; if (gradients[testX + testY * subW] > 0) //if (thresholded[testX + testY * subW] > 0) { if (++pass >= 24) { center->width = r * 2; // Break of outer loop r = 0; break; } } } } qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << center->x << " Y: " << center->y << " Width: " << center->width; // If no stars were detected if (center->width == -1) { delete center; return 0; } // 30% fuzzy //center->width += center->width*0.3 * (running_threshold / threshold); double FSum = 0, HF = 0, TF = 0; const double resolution = 1.0 / 20.0; int cen_y = qRound(center->y); double rightEdge = center->x + center->width / 2.0; double leftEdge = center->x - center->width / 2.0; QVector subPixels; subPixels.reserve(center->width / resolution); const T *origBuffer = reinterpret_cast(data->getImageBuffer()) + offset; /*if (Options::fITSLogging()) { QDebug deb = qDebug(); for (int i=0; i < subW; i++) deb << origBuffer[i + cen_y * dataWidth] << ","; }*/ for (double x = leftEdge; x <= rightEdge; x += resolution) { double slice = resolution * (origBuffer[static_cast(floor(x)) + cen_y * dataWidth]); FSum += slice; subPixels.append(slice); } // Half flux HF = FSum / 2.0; int subPixelCenter = (center->width / resolution) / 2; // Start from center TF = subPixels[subPixelCenter]; double lastTF = TF; // Integrate flux along radius axis until we reach half flux //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution) for (int k = 1; k < subPixelCenter; k++) { TF += subPixels[subPixelCenter + k]; TF += subPixels[subPixelCenter - k]; if (TF >= HF) { // We overpassed HF, let's calculate from last TF how much until we reach HF // #1 Accurate calculation, but very sensitive to small variations of flux center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution; // #2 Less accurate calculation, but stable against small variations of flux //center->HFR = (k - 1) * resolution; break; } lastTF = TF; } // Correct center for subX and subY center->x += subX; center->y += subY; data->appendStar(center); qCDebug(KSTARS_FITS) << "Flux: " << FSum << " Half-Flux: " << HF << " HFR: " << center->HFR; return 1; } int FITSData::findOneStar(const QRect &boundary) { switch (m_DataType) { case TBYTE: return findOneStar(boundary); break; case TSHORT: return findOneStar(boundary); break; case TUSHORT: return findOneStar(boundary); break; case TLONG: return findOneStar(boundary); break; case TULONG: return findOneStar(boundary); break; case TFLOAT: return findOneStar(boundary); break; case TLONGLONG: return findOneStar(boundary); break; case TDOUBLE: return findOneStar(boundary); break; default: break; } return 0; } template int FITSData::findOneStar(const QRect &boundary) { if (boundary.isEmpty()) return -1; int subX = boundary.x(); int subY = boundary.y(); int subW = subX + boundary.width(); int subH = subY + boundary.height(); float massX = 0, massY = 0, totalMass = 0; auto *buffer = reinterpret_cast(m_ImageBuffer); // TODO replace magic number with something more useful to understand double threshold = stats.mean[0] * Options::focusThreshold() / 100.0; for (int y = subY; y < subH; y++) { for (int x = subX; x < subW; x++) { T pixel = buffer[x + y * stats.width]; if (pixel > threshold) { totalMass += pixel; massX += x * pixel; massY += y * pixel; } } } qCDebug(KSTARS_FITS) << "FITS: Weighted Center is X: " << massX / totalMass << " Y: " << massY / totalMass; auto *center = new Edge; center->width = -1; center->x = massX / totalMass + 0.5; center->y = massY / totalMass + 0.5; center->HFR = 1; // Maximum Radius int maxR = qMin(subW - 1, subH - 1) / 2; // Critical threshold double critical_threshold = threshold * 0.7; double running_threshold = threshold; while (running_threshold >= critical_threshold) { for (int r = maxR; r > 1; r--) { int pass = 0; for (float theta = 0; theta < 2 * M_PI; theta += (2 * M_PI) / 10.0) { int testX = center->x + std::cos(theta) * r; int testY = center->y + std::sin(theta) * r; // if out of bound, break; if (testX < subX || testX > subW || testY < subY || testY > subH) break; if (buffer[testX + testY * stats.width] > running_threshold) pass++; } //qDebug() << "Testing for radius " << r << " passes # " << pass << " @ threshold " << running_threshold; //if (pass >= 6) if (pass >= 5) { center->width = r * 2; break; } } if (center->width > 0) break; // Increase threshold fuzziness by 10% running_threshold -= running_threshold * 0.1; } // If no stars were detected if (center->width == -1) { delete center; return 0; } // 30% fuzzy //center->width += center->width*0.3 * (running_threshold / threshold); starCenters.append(center); double FSum = 0, HF = 0, TF = 0, min = stats.min[0]; const double resolution = 1.0 / 20.0; int cen_y = qRound(center->y); double rightEdge = center->x + center->width / 2.0; double leftEdge = center->x - center->width / 2.0; QVector subPixels; subPixels.reserve(center->width / resolution); for (double x = leftEdge; x <= rightEdge; x += resolution) { //subPixels[x] = resolution * (image_buffer[static_cast(floor(x)) + cen_y * stats.width] - min); double slice = resolution * (buffer[static_cast(floor(x)) + cen_y * stats.width] - min); FSum += slice; subPixels.append(slice); } // Half flux HF = FSum / 2.0; //double subPixelCenter = center->x - fmod(center->x,resolution); int subPixelCenter = (center->width / resolution) / 2; // Start from center TF = subPixels[subPixelCenter]; double lastTF = TF; // Integrate flux along radius axis until we reach half flux //for (double k=resolution; k < (center->width/(2*resolution)); k += resolution) for (int k = 1; k < subPixelCenter; k++) { TF += subPixels[subPixelCenter + k]; TF += subPixels[subPixelCenter - k]; if (TF >= HF) { // We have two ways to calculate HFR. The first is the correct method but it can get quite variable within 10% due to random fluctuations of the measured star. // The second method is not truly HFR but is much more resistant to noise. // #1 Approximate HFR, accurate and reliable but quite variable to small changes in star flux center->HFR = (k - 1 + ((HF - lastTF) / (TF - lastTF)) * 2) * resolution; // #2 Not exactly HFR, but much more stable //center->HFR = (k*resolution) * (HF/TF); break; } lastTF = TF; } return 1; } /*** Find center of stars and calculate Half Flux Radius */ int FITSData::findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth) { switch (m_DataType) { case TBYTE: return findCentroid(boundary, initStdDev, minEdgeWidth); case TSHORT: return findCentroid(boundary, initStdDev, minEdgeWidth); case TUSHORT: return findCentroid(boundary, initStdDev, minEdgeWidth); case TLONG: return findCentroid(boundary, initStdDev, minEdgeWidth); case TULONG: return findCentroid(boundary, initStdDev, minEdgeWidth); case TFLOAT: return findCentroid(boundary, initStdDev, minEdgeWidth); case TLONGLONG: return findCentroid(boundary, initStdDev, minEdgeWidth); case TDOUBLE: return findCentroid(boundary, initStdDev, minEdgeWidth); default: return -1; } } template int FITSData::findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth) { double threshold = 0, sum = 0, avg = 0, min = 0; int starDiameter = 0; int pixVal = 0; int minimumEdgeCount = MINIMUM_EDGE_LIMIT; auto *buffer = reinterpret_cast(m_ImageBuffer); double JMIndex = 100; #ifndef KSTARS_LITE if (histogram) JMIndex = histogram->getJMIndex(); #endif float dispersion_ratio = 1.5; QList edges; if (JMIndex < DIFFUSE_THRESHOLD) { minEdgeWidth = JMIndex * 35 + 1; minimumEdgeCount = minEdgeWidth - 1; } else { minEdgeWidth = 6; minimumEdgeCount = 4; } while (initStdDev >= 1) { minEdgeWidth--; minimumEdgeCount--; minEdgeWidth = qMax(3, minEdgeWidth); minimumEdgeCount = qMax(3, minimumEdgeCount); if (JMIndex < DIFFUSE_THRESHOLD) { // Taking the average out seems to have better result for noisy images threshold = stats.max[0] - stats.mean[0] * ((MINIMUM_STDVAR - initStdDev) * 0.5 + 1); min = stats.min[0]; if (threshold - min < 0) { threshold = stats.mean[0] * ((MINIMUM_STDVAR - initStdDev) * 0.5 + 1); min = 0; } dispersion_ratio = 1.4 - (MINIMUM_STDVAR - initStdDev) * 0.08; } else { threshold = stats.mean[0] + stats.stddev[0] * initStdDev * (0.3 - (MINIMUM_STDVAR - initStdDev) * 0.05); min = stats.min[0]; // Ratio between centeroid center and edge dispersion_ratio = 1.8 - (MINIMUM_STDVAR - initStdDev) * 0.2; } qCDebug(KSTARS_FITS) << "SNR: " << stats.SNR; qCDebug(KSTARS_FITS) << "The threshold level is " << threshold << "(actual " << threshold - min << ") minimum edge width" << minEdgeWidth << " minimum edge limit " << minimumEdgeCount; threshold -= min; int subX, subY, subW, subH; if (boundary.isNull()) { if (m_Mode == FITS_GUIDE || m_Mode == FITS_FOCUS) { // Only consider the central 70% subX = round(stats.width * 0.15); subY = round(stats.height * 0.15); subW = stats.width - subX; subH = stats.height - subY; } else { // Consider the complete area 100% subX = 0; subY = 0; subW = stats.width; subH = stats.height; } } else { subX = boundary.x(); subY = boundary.y(); subW = subX + boundary.width(); subH = subY + boundary.height(); } // Detect "edges" that are above threshold for (int i = subY; i < subH; i++) { starDiameter = 0; for (int j = subX; j < subW; j++) { pixVal = buffer[j + (i * stats.width)] - min; // If pixel value > threshold, let's get its weighted average if (pixVal >= threshold) { avg += j * pixVal; sum += pixVal; starDiameter++; } // Value < threshold but avg exists else if (sum > 0) { // We found a potential centroid edge if (starDiameter >= minEdgeWidth) { float center = avg / sum + 0.5; if (center > 0) { int i_center = std::floor(center); // Check if center is 10% or more brighter than edge, if not skip if (((buffer[i_center + (i * stats.width)] - min) / (buffer[i_center + (i * stats.width) - starDiameter / 2] - min) >= dispersion_ratio) && ((buffer[i_center + (i * stats.width)] - min) / (buffer[i_center + (i * stats.width) + starDiameter / 2] - min) >= dispersion_ratio)) { qCDebug(KSTARS_FITS) << "Edge center is " << buffer[i_center + (i * stats.width)] - min << " Edge is " << buffer[i_center + (i * stats.width) - starDiameter / 2] - min << " and ratio is " << ((buffer[i_center + (i * stats.width)] - min) / (buffer[i_center + (i * stats.width) - starDiameter / 2] - min)) << " located at X: " << center << " Y: " << i + 0.5; auto *newEdge = new Edge(); newEdge->x = center; newEdge->y = i + 0.5; newEdge->scanned = 0; newEdge->val = buffer[i_center + (i * stats.width)] - min; newEdge->width = starDiameter; newEdge->HFR = 0; newEdge->sum = sum; edges.append(newEdge); } } } // Reset avg = sum = starDiameter = 0; } } } qCDebug(KSTARS_FITS) << "Total number of edges found is: " << edges.count(); // In case of hot pixels if (edges.count() == 1 && initStdDev > 1) { initStdDev--; continue; } if (edges.count() >= MAX_EDGE_LIMIT) { qCWarning(KSTARS_FITS) << "Too many edges, aborting... " << edges.count(); qDeleteAll(edges); return -1; } if (edges.count() >= minimumEdgeCount) break; qDeleteAll(edges); edges.clear(); initStdDev--; } int cen_count = 0; int cen_x = 0; int cen_y = 0; int cen_v = 0; int cen_w = 0; int width_sum = 0; // Let's sort edges, starting with widest qSort(edges.begin(), edges.end(), greaterThan); // Now, let's scan the edges and find the maximum centroid vertically for (int i = 0; i < edges.count(); i++) { qCDebug(KSTARS_FITS) << "# " << i << " Edge at (" << edges[i]->x << "," << edges[i]->y << ") With a value of " << edges[i]->val << " and width of " << edges[i]->width << " pixels. with sum " << edges[i]->sum; // If edge scanned already, skip if (edges[i]->scanned == 1) { qCDebug(KSTARS_FITS) << "Skipping check for center " << i << " because it was already counted"; continue; } qCDebug(KSTARS_FITS) << "Invetigating edge # " << i << " now ..."; // Get X, Y, and Val of edge cen_x = edges[i]->x; cen_y = edges[i]->y; cen_v = edges[i]->sum; cen_w = edges[i]->width; float avg_x = 0; float avg_y = 0; sum = 0; cen_count = 0; // Now let's compare to other edges until we hit a maxima for (int j = 0; j < edges.count(); j++) { if (edges[j]->scanned) continue; if (checkCollision(edges[j], edges[i])) { if (edges[j]->sum >= cen_v) { cen_v = edges[j]->sum; cen_w = edges[j]->width; } edges[j]->scanned = 1; cen_count++; avg_x += edges[j]->x * edges[j]->val; avg_y += edges[j]->y * edges[j]->val; sum += edges[j]->val; continue; } } int cen_limit = (MINIMUM_ROWS_PER_CENTER - (MINIMUM_STDVAR - initStdDev)); if (edges.count() < LOW_EDGE_CUTOFF_1) { if (edges.count() < LOW_EDGE_CUTOFF_2) cen_limit = 1; else cen_limit = 2; } qCDebug(KSTARS_FITS) << "center_count: " << cen_count << " and initstdDev= " << initStdDev << " and limit is " << cen_limit; if (cen_limit < 1) continue; // If centroid count is within acceptable range //if (cen_limit >= 2 && cen_count >= cen_limit) if (cen_count >= cen_limit) { // We detected a centroid, let's init it auto *rCenter = new Edge(); rCenter->x = avg_x / sum; rCenter->y = avg_y / sum; width_sum += rCenter->width; rCenter->width = cen_w; qCDebug(KSTARS_FITS) << "Found a real center with number with (" << rCenter->x << "," << rCenter->y << ")"; // Calculate Total Flux From Center, Half Flux, Full Summation double TF = 0; double HF = 0; double FSum = 0; cen_x = (int)std::floor(rCenter->x); cen_y = (int)std::floor(rCenter->y); if (cen_x < 0 || cen_x > stats.width || cen_y < 0 || cen_y > stats.height) { delete rCenter; continue; } // Complete sum along the radius //for (int k=0; k < rCenter->width; k++) for (int k = rCenter->width / 2; k >= -(rCenter->width / 2); k--) { FSum += buffer[cen_x - k + (cen_y * stats.width)] - min; //qDebug() << image_buffer[cen_x-k+(cen_y*stats.width)] - min; } // Half flux HF = FSum / 2.0; // Total flux starting from center TF = buffer[cen_y * stats.width + cen_x] - min; int pixelCounter = 1; // Integrate flux along radius axis until we reach half flux for (int k = 1; k < rCenter->width / 2; k++) { if (TF >= HF) { qCDebug(KSTARS_FITS) << "Stopping at TF " << TF << " after #" << k << " pixels."; break; } TF += buffer[cen_y * stats.width + cen_x + k] - min; TF += buffer[cen_y * stats.width + cen_x - k] - min; pixelCounter++; } // Calculate weighted Half Flux Radius rCenter->HFR = pixelCounter * (HF / TF); // Store full flux rCenter->val = FSum; qCDebug(KSTARS_FITS) << "HFR for this center is " << rCenter->HFR << " pixels and the total flux is " << FSum; starCenters.append(rCenter); } } if (starCenters.count() > 1 && m_Mode != FITS_FOCUS) { float width_avg = (float)width_sum / starCenters.count(); float lsum = 0, sdev = 0; for (auto ¢er : starCenters) lsum += (center->width - width_avg) * (center->width - width_avg); sdev = (std::sqrt(lsum / (starCenters.count() - 1))) * 4; // Reject stars > 4 * stddev foreach (Edge *center, starCenters) if (center->width > sdev) starCenters.removeOne(center); //foreach(Edge *center, starCenters) //qDebug() << center->x << "," << center->y << "," << center->width << "," << center->val << endl; } // Release memory qDeleteAll(edges); return starCenters.count(); } double FITSData::getHFR(HFRType type) { // This method is less susceptible to noise // Get HFR for the brightest star only, instead of averaging all stars // It is more consistent. // TODO: Try to test this under using a real CCD. if (starCenters.empty()) return -1; if (type == HFR_MAX) { maxHFRStar = nullptr; int maxVal = 0; int maxIndex = 0; for (int i = 0; i < starCenters.count(); i++) { if (starCenters[i]->val > maxVal) { maxIndex = i; maxVal = starCenters[i]->val; } } maxHFRStar = starCenters[maxIndex]; return static_cast(starCenters[maxIndex]->HFR); } double FSum = 0; double avgHFR = 0; // Weighted average HFR for (int i = 0; i < starCenters.count(); i++) { avgHFR += static_cast(starCenters[i]->val * starCenters[i]->HFR); FSum += starCenters[i]->val; } if (FSum != 0) { //qDebug() << "Average HFR is " << avgHFR / FSum << endl; return (avgHFR / FSum); } else return -1; } double FITSData::getHFR(int x, int y) { if (starCenters.empty()) return -1; for (int i = 0; i < starCenters.count(); i++) { if (std::fabs(starCenters[i]->x - x) <= starCenters[i]->width / 2 && std::fabs(starCenters[i]->y - y) <= starCenters[i]->width / 2) { return starCenters[i]->HFR; } } return -1; } void FITSData::applyFilter(FITSScale type, uint8_t *image, double *min, double *max) { if (type == FITS_NONE) return; double dataMin = stats.min[0], dataMax = stats.max[0]; if ((min != nullptr) && *min != -1) dataMin = *min; if ((max != nullptr) && *max != -1) dataMax = *max; switch (type) { case FITS_AUTO_STRETCH: { dataMin = stats.mean[0] - stats.stddev[0]; dataMax = stats.mean[0] + stats.stddev[0] * 3; } break; case FITS_HIGH_CONTRAST: { dataMin = stats.mean[0] + stats.stddev[0]; dataMax = stats.mean[0] + stats.stddev[0] * 3; } break; case FITS_HIGH_PASS: { dataMin = stats.mean[0]; } break; default: break; } switch (m_DataType) { case TBYTE: { dataMin = dataMin < 0 ? 0 : dataMin; dataMax = dataMax > UINT8_MAX ? UINT8_MAX : dataMax; applyFilter(type, image, dataMin, dataMax); } break; case TSHORT: { dataMin = dataMin < INT16_MIN ? INT16_MIN : dataMin; dataMax = dataMax > INT16_MAX ? INT16_MAX : dataMax; applyFilter(type, image, dataMin, dataMax); } break; case TUSHORT: { dataMin = dataMin < 0 ? 0 : dataMin; dataMax = dataMax > UINT16_MAX ? UINT16_MAX : dataMax; applyFilter(type, image, dataMin, dataMax); } break; case TLONG: { dataMin = dataMin < INT_MIN ? INT_MIN : dataMin; dataMax = dataMax > INT_MAX ? INT_MAX : dataMax; applyFilter(type, image, dataMin, dataMax); } break; case TULONG: { dataMin = dataMin < 0 ? 0 : dataMin; dataMax = dataMax > UINT_MAX ? UINT_MAX : dataMax; applyFilter(type, image, dataMin, dataMax); } break; case TFLOAT: { dataMin = dataMin < FLT_MIN ? FLT_MIN : dataMin; dataMax = dataMax > FLT_MAX ? FLT_MAX : dataMax; applyFilter(type, image, dataMin, dataMax); } break; case TLONGLONG: { dataMin = dataMin < LLONG_MIN ? LLONG_MIN : dataMin; dataMax = dataMax > LLONG_MAX ? LLONG_MAX : dataMax; applyFilter(type, image, dataMin, dataMax); } break; case TDOUBLE: { dataMin = dataMin < DBL_MIN ? DBL_MIN : dataMin; dataMax = dataMax > DBL_MAX ? DBL_MAX : dataMax; applyFilter(type, image, dataMin, dataMax); } break; default: return; } if (min != nullptr) *min = dataMin; if (max != nullptr) *max = dataMax; } template void FITSData::applyFilter(FITSScale type, uint8_t *targetImage, double image_min, double image_max) { bool calcStats = false; T *image = nullptr; if (targetImage) image = reinterpret_cast(targetImage); else { image = reinterpret_cast(m_ImageBuffer); calcStats = true; } T min = image_min < std::numeric_limits::min() ? std::numeric_limits::min() : image_min; T max = image_max > std::numeric_limits::max() ? std::numeric_limits::max() : image_max; // Create N threads const uint8_t nThreads = 16; uint32_t width = stats.width; uint32_t height = stats.height; //QTime timer; //timer.start(); switch (type) { case FITS_AUTO: case FITS_LINEAR: case FITS_AUTO_STRETCH: case FITS_HIGH_CONTRAST: case FITS_LOG: case FITS_SQRT: case FITS_HIGH_PASS: { // List of futures QList> futures; double coeff = 0; if (type == FITS_LOG) coeff = max / std::log(1 + max); else if (type == FITS_SQRT) coeff = max / sqrt(max); for (int n=0; n < m_Channels; n++) { if (type == FITS_HIGH_PASS) min = stats.mean[n]; uint32_t cStart = n * stats.samples_per_channel; // Calculate how many elements we process per thread uint32_t tStride = stats.samples_per_channel / nThreads; // Calculate the final stride since we can have some left over due to division above uint32_t fStride = tStride + (stats.samples_per_channel - (tStride * nThreads)); T *runningBuffer = image + cStart; if (type == FITS_LOG) { for (int i=0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer+((i == (nThreads-1)) ? fStride : tStride)), [min,max,coeff](T & a) { a = qBound(min,static_cast(round(coeff * std::log(1 + qBound(min, a, max)))),max); })); runningBuffer += tStride; } } else if (type == FITS_SQRT) { for (int i=0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer+((i == (nThreads-1)) ? fStride : tStride)), [min,max,coeff](T & a) { a = qBound(min,static_cast(round(coeff * a)),max); })); } runningBuffer += tStride; } else { for (int i=0; i < nThreads; i++) { // Run threads futures.append(QtConcurrent::map(runningBuffer, (runningBuffer+((i == (nThreads-1)) ? fStride : tStride)), [min,max](T & a) {a = qBound(min, a,max);})); runningBuffer += tStride; } } } for (int i=0; i < nThreads*m_Channels; i++) futures[i].waitForFinished(); if (calcStats) { stats.min[0] = stats.min[1] = stats.min[2] = min; stats.max[0] = stats.max[1] = stats.max[2] = max; if (type != FITS_AUTO && type != FITS_LINEAR) runningAverageStdDev(); } } break; case FITS_EQUALIZE: { #ifndef KSTARS_LITE if (histogram == nullptr) return; T bufferVal = 0; QVector cumulativeFreq = histogram->getCumulativeFrequency(); double coeff = 255.0 / (height * width); uint32_t row = 0; uint32_t index=0; for (int i = 0; i < m_Channels; i++) { uint32_t offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; bufferVal = (image[index] - min) / histogram->getBinWidth(); if (bufferVal >= cumulativeFreq.size()) bufferVal = cumulativeFreq.size() - 1; image[index] = qBound(min, static_cast(round(coeff * cumulativeFreq[bufferVal])), max); } } } #endif } if (calcStats) calculateStats(true); break; // Based on http://www.librow.com/articles/article-1 case FITS_MEDIAN: { uint8_t BBP = stats.bytesPerPixel; auto *extension = new T[(width + 2) * (height + 2)]; // Check memory allocation if (!extension) return; // Create image extension for (uint32_t ch = 0; ch < m_Channels; ch++) { uint32_t offset = ch * stats.samples_per_channel; uint32_t N = width, M = height; for (uint32_t i = 0; i < M; ++i) { memcpy(extension + (N + 2) * (i + 1) + 1, image + (N * i) + offset, N * BBP); extension[(N + 2) * (i + 1)] = image[N * i + offset]; extension[(N + 2) * (i + 2) - 1] = image[N * (i + 1) - 1 + offset]; } // Fill first line of image extension memcpy(extension, extension + N + 2, (N + 2) * BBP); // Fill last line of image extension memcpy(extension + (N + 2) * (M + 1), extension + (N + 2) * M, (N + 2) * BBP); // Call median filter implementation N = width + 2; M = height + 2; // Move window through all elements of the image for (uint32_t m = 1; m < M - 1; ++m) for (uint32_t n = 1; n < N - 1; ++n) { // Pick up window elements int k = 0; float window[9]; memset(&window[0], 0, 9*sizeof(float)); for (uint32_t j = m - 1; j < m + 2; ++j) for (uint32_t i = n - 1; i < n + 2; ++i) window[k++] = extension[j * N + i]; // Order elements (only half of them) for (uint32_t j = 0; j < 5; ++j) { // Find position of minimum element int mine = j; for (uint32_t l = j + 1; l < 9; ++l) if (window[l] < window[mine]) mine = l; // Put found minimum element in its place const float temp = window[j]; window[j] = window[mine]; window[mine] = temp; } // Get result - the middle element image[(m - 1) * (N - 2) + n - 1 + offset] = window[4]; } } // Free memory delete[] extension; if (calcStats) runningAverageStdDev(); } break; case FITS_ROTATE_CW: rotFITS(90, 0); rotCounter++; break; case FITS_ROTATE_CCW: rotFITS(270, 0); rotCounter--; break; case FITS_FLIP_H: rotFITS(0, 1); flipHCounter++; break; case FITS_FLIP_V: rotFITS(0, 2); flipVCounter++; break; default: break; } #if 0 } else { uint32_t index = 0, row=0, offset=0; switch (type) { case FITS_AUTO: case FITS_LINEAR: { for (uint8_t i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; image[index] = qBound(min, image[index], max); } } } if (calcStats) { stats.min[0] = min; stats.max[0] = max; } } break; case FITS_LOG: { double coeff = max / log(1 + max); for (int i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; image[index] = qBound(min, static_cast(round(coeff * log(1 + qBound(min, image[index], max)))), max); } } } if (calcStats) { stats.min[0] = min; stats.max[0] = max; runningAverageStdDev(); } } break; case FITS_SQRT: { double coeff = max / sqrt(max); for (int i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; image[index] = qBound(min, static_cast(round(coeff * image[index])), max); } } } if (calcStats) { stats.min[0] = min; stats.max[0] = max; runningAverageStdDev(); } } break; // Only difference is how min and max are set case FITS_AUTO_STRETCH: case FITS_HIGH_CONTRAST: { for (uint32_t i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) image[k + row] = qBound(min, image[k + row], max); } } if (calcStats) { stats.min[0] = min; stats.max[0] = max; runningAverageStdDev(); } } break; case FITS_EQUALIZE: { #ifndef KSTARS_LITE if (histogram == nullptr) return; T bufferVal = 0; QVector cumulativeFreq = histogram->getCumulativeFrequency(); double coeff = 255.0 / (height * width); for (uint32_t i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; bufferVal = (image[index] - min) / histogram->getBinWidth(); if (bufferVal >= cumulativeFreq.size()) bufferVal = cumulativeFreq.size() - 1; image[index] = qBound(min, static_cast(round(coeff * cumulativeFreq[bufferVal])), max); } } } #endif } if (calcStats) calculateStats(true); break; case FITS_HIGH_PASS: { min = stats.mean[0]; for (uint32_t i = 0; i < channels; i++) { offset = i * stats.samples_per_channel; for (uint32_t j = 0; j < height; j++) { row = offset + j * width; for (uint32_t k = 0; k < width; k++) { index = k + row; image[index] = qBound(min, image[index], max); } } } if (calcStats) { stats.min[0] = min; stats.max[0] = max; runningAverageStdDev(); } } break; // Based on http://www.librow.com/articles/article-1 case FITS_MEDIAN: { int BBP = stats.bytesPerPixel; T *extension = new T[(width + 2) * (height + 2)]; // Check memory allocation if (!extension) return; // Create image extension for (uint32_t ch = 0; ch < channels; ch++) { offset = ch * stats.samples_per_channel; uint32_t N = width, M = height; for (uint32_t i = 0; i < M; ++i) { memcpy(extension + (N + 2) * (i + 1) + 1, image + (N * i) + offset, N * BBP); extension[(N + 2) * (i + 1)] = image[N * i + offset]; extension[(N + 2) * (i + 2) - 1] = image[N * (i + 1) - 1 + offset]; } // Fill first line of image extension memcpy(extension, extension + N + 2, (N + 2) * BBP); // Fill last line of image extension memcpy(extension + (N + 2) * (M + 1), extension + (N + 2) * M, (N + 2) * BBP); // Call median filter implementation N = width + 2; M = height + 2; // Move window through all elements of the image for (uint32_t m = 1; m < M - 1; ++m) for (uint32_t n = 1; n < N - 1; ++n) { // Pick up window elements int k = 0; float window[9]; memset(&window[0], 0, 9*sizeof(float)); for (uint32_t j = m - 1; j < m + 2; ++j) for (uint32_t i = n - 1; i < n + 2; ++i) window[k++] = extension[j * N + i]; // Order elements (only half of them) for (uint32_t j = 0; j < 5; ++j) { // Find position of minimum element int mine = j; for (uint32_t l = j + 1; l < 9; ++l) if (window[l] < window[mine]) mine = l; // Put found minimum element in its place const float temp = window[j]; window[j] = window[mine]; window[mine] = temp; } // Get result - the middle element image[(m - 1) * (N - 2) + n - 1 + offset] = window[4]; } } // Free memory delete[] extension; if (calcStats) runningAverageStdDev(); } break; case FITS_ROTATE_CW: rotFITS(90, 0); rotCounter++; break; case FITS_ROTATE_CCW: rotFITS(270, 0); rotCounter--; break; case FITS_FLIP_H: rotFITS(0, 1); flipHCounter++; break; case FITS_FLIP_V: rotFITS(0, 2); flipVCounter++; break; case FITS_CUSTOM: default: return; break; } } qCInfo(KSTARS_FITS) << filename << "Apply Filter calculation took" << timer.elapsed() << "ms"; #endif } QList FITSData::getStarCentersInSubFrame(QRect subFrame) const { QList starCentersInSubFrame; for (int i = 0; i < starCenters.count(); i++) { int x = static_cast(starCenters[i]->x); int y = static_cast(starCenters[i]->y); if(subFrame.contains(x,y)) { starCentersInSubFrame.append(starCenters[i]); } } return starCentersInSubFrame; } void FITSData::getCenterSelection(int *x, int *y) { if (starCenters.count() == 0) return; auto *pEdge = new Edge(); pEdge->x = *x; pEdge->y = *y; pEdge->width = 1; foreach (Edge *center, starCenters) if (checkCollision(pEdge, center)) { *x = static_cast(center->x); *y = static_cast(center->y); break; } delete (pEdge); } bool FITSData::checkForWCS() { #ifndef KSTARS_LITE #ifdef HAVE_WCSLIB int status = 0; char *header; int nkeyrec, nreject, nwcs; if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); lastError = errmsg; return false; } if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &nwcs, &wcs)) != 0) { free(header); lastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]); return false; } free(header); if (wcs == nullptr) { //fprintf(stderr, "No world coordinate systems found.\n"); lastError = i18n("No world coordinate systems found."); return false; } // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now. if (wcs->crpix[0] == 0) { lastError = i18n("No world coordinate systems found."); return false; } if ((status = wcsset(wcs)) != 0) { lastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } - HasWCS = true; + HasWCS = true; #endif #endif return HasWCS; } bool FITSData::loadWCS() { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) if (WCSLoaded) { qWarning() << "WCS data already loaded"; return true; } qCDebug(KSTARS_FITS) << "Started WCS Data Processing..."; int status = 0; char *header; int nkeyrec, nreject, nwcs, stat[2]; double imgcrd[2], phi = 0, pixcrd[2], theta = 0, world[2]; int w = width(); int h = height(); if (fits_hdr2str(fptr, 1, nullptr, 0, &header, &nkeyrec, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); lastError = errmsg; return false; } if ((status = wcspih(header, nkeyrec, WCSHDR_all, -3, &nreject, &nwcs, &wcs)) != 0) { free(header); lastError = QString("wcspih ERROR %1: %2.").arg(status).arg(wcshdr_errmsg[status]); return false; } free(header); if (wcs == nullptr) { //fprintf(stderr, "No world coordinate systems found.\n"); lastError = i18n("No world coordinate systems found."); return false; } // FIXME: Call above goes through EVEN if no WCS is present, so we're adding this to return for now. if (wcs->crpix[0] == 0) { lastError = i18n("No world coordinate systems found."); return false; } if ((status = wcsset(wcs)) != 0) { lastError = QString("wcsset error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } delete[] wcs_coord; wcs_coord = new wcs_point[w * h]; if (wcs_coord == nullptr) { lastError = "Not enough memory for WCS data!"; return false; } wcs_point *p = wcs_coord; for (int i = 0; i < h; i++) { for (int j = 0; j < w; j++) { pixcrd[0] = j; pixcrd[1] = i; if ((status = wcsp2s(wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0) { lastError = QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]); } else { p->ra = world[0]; p->dec = world[1]; p++; } } } findObjectsInImage(&world[0], phi, theta, &imgcrd[0], &pixcrd[0], &stat[0]); WCSLoaded = true; HasWCS = true; qCDebug(KSTARS_FITS) << "Finished WCS Data processing..."; return true; #else return false; #endif } bool FITSData::wcsToPixel(SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPointF &wcsImagePoint) { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) int status = 0; int stat[2]; double imgcrd[2], worldcrd[2], pixcrd[2], phi[2], theta[2]; if (wcs == nullptr) { lastError = i18n("No world coordinate systems found."); return false; } worldcrd[0] = wcsCoord.ra0().Degrees(); worldcrd[1] = wcsCoord.dec0().Degrees(); if ((status = wcss2p(wcs, 1, 2, &worldcrd[0], &phi[0], &theta[0], &imgcrd[0], &pixcrd[0], &stat[0])) != 0) { lastError = QString("wcss2p error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } wcsImagePoint.setX(imgcrd[0]); wcsImagePoint.setY(imgcrd[0]); wcsPixelPoint.setX(pixcrd[0]); wcsPixelPoint.setY(pixcrd[1]); return true; #else Q_UNUSED(wcsCoord); Q_UNUSED(wcsPixelPoint); Q_UNUSED(wcsImagePoint); return false; #endif } bool FITSData::pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord) { #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) int status = 0; int stat[2]; double imgcrd[2], phi, pixcrd[2], theta, world[2]; if (wcs == nullptr) { lastError = i18n("No world coordinate systems found."); return false; } pixcrd[0] = wcsPixelPoint.x(); pixcrd[1] = wcsPixelPoint.y(); if ((status = wcsp2s(wcs, 1, 2, &pixcrd[0], &imgcrd[0], &phi, &theta, &world[0], &stat[0])) != 0) { lastError = QString("wcsp2s error %1: %2.").arg(status).arg(wcs_errmsg[status]); return false; } else { wcsCoord.setRA0(world[0] / 15.0); wcsCoord.setDec0(world[1]); } return true; #else Q_UNUSED(wcsPixelPoint); Q_UNUSED(wcsCoord); return false; #endif } #if !defined(KSTARS_LITE) && defined(HAVE_WCSLIB) void FITSData::findObjectsInImage(double world[], double phi, double theta, double imgcrd[], double pixcrd[], int stat[]) { int w = width(); int h = height(); int status = 0; char date[64]; KSNumbers *num = nullptr; if (fits_read_keyword(fptr, "DATE-OBS", date, nullptr, &status) == 0) { QString tsString(date); tsString = tsString.remove('\'').trimmed(); // Add Zulu time to indicate UTC tsString += "Z"; QDateTime ts = QDateTime::fromString(tsString, Qt::ISODate); if (ts.isValid()) num = new KSNumbers(KStarsDateTime(ts).djd()); } if (num == nullptr) num = new KSNumbers(KStarsData::Instance()->ut().djd()); //Set to current time if the above does not work. SkyMapComposite *map = KStarsData::Instance()->skyComposite(); wcs_point *wcs_coord = getWCSCoord(); if (wcs_coord != nullptr) { int size = w * h; objList.clear(); SkyPoint p1; p1.setRA0(dms(wcs_coord[0].ra)); p1.setDec0(dms(wcs_coord[0].dec)); p1.updateCoordsNow(num); SkyPoint p2; p2.setRA0(dms(wcs_coord[size - 1].ra)); p2.setDec0(dms(wcs_coord[size - 1].dec)); p2.updateCoordsNow(num); QList list = map->findObjectsInArea(p1, p2); foreach (SkyObject *object, list) { int type = object->type(); if (object->name() == "star" || type == SkyObject::PLANET || type == SkyObject::ASTEROID || type == SkyObject::COMET || type == SkyObject::SUPERNOVA || type == SkyObject::MOON || type == SkyObject::SATELLITE) { //DO NOT DISPLAY, at least for now, becaus these things move and change. } int x = -100; int y = -100; world[0] = object->ra0().Degrees(); world[1] = object->dec0().Degrees(); if ((status = wcss2p(wcs, 1, 2, &world[0], &phi, &theta, &imgcrd[0], &pixcrd[0], &stat[0])) != 0) { fprintf(stderr, "wcsp2s ERROR %d: %s.\n", status, wcs_errmsg[status]); } else { x = pixcrd[0]; //The X and Y are set to the found position if it does work. y = pixcrd[1]; } if (x > 0 && y > 0 && x < w && y < h) objList.append(new FITSSkyObject(object, x, y)); } } delete (num); } #endif QList FITSData::getSkyObjects() { return objList; } FITSSkyObject::FITSSkyObject(SkyObject *object, int xPos, int yPos) : QObject() { skyObjectStored = object; xLoc = xPos; yLoc = yPos; } SkyObject *FITSSkyObject::skyObject() { return skyObjectStored; } int FITSSkyObject::x() { return xLoc; } int FITSSkyObject::y() { return yLoc; } void FITSSkyObject::setX(int xPos) { xLoc = xPos; } void FITSSkyObject::setY(int yPos) { yLoc = yPos; } int FITSData::getFlipVCounter() const { return flipVCounter; } void FITSData::setFlipVCounter(int value) { flipVCounter = value; } int FITSData::getFlipHCounter() const { return flipHCounter; } void FITSData::setFlipHCounter(int value) { flipHCounter = value; } int FITSData::getRotCounter() const { return rotCounter; } void FITSData::setRotCounter(int value) { rotCounter = value; } /* Rotate an image by 90, 180, or 270 degrees, with an optional * reflection across the vertical or horizontal axis. * verbose generates extra info on stdout. * return nullptr if successful or rotated image. */ template bool FITSData::rotFITS(int rotate, int mirror) { int ny, nx; int x1, y1, x2, y2; uint8_t *rotimage = nullptr; int offset = 0; if (rotate == 1) rotate = 90; else if (rotate == 2) rotate = 180; else if (rotate == 3) rotate = 270; else if (rotate < 0) rotate = rotate + 360; nx = stats.width; ny = stats.height; int BBP = stats.bytesPerPixel; /* Allocate buffer for rotated image */ rotimage = new uint8_t[stats.samples_per_channel * m_Channels * BBP]; if (rotimage == nullptr) { qWarning() << "Unable to allocate memory for rotated image buffer!"; return false; } auto *rotBuffer = reinterpret_cast(rotimage); auto *buffer = reinterpret_cast(m_ImageBuffer); /* Mirror image without rotation */ if (rotate < 45 && rotate > -45) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; for (y1 = 0; y1 < ny; y1++) rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(y1 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } /* Rotate by 90 degrees */ else if (rotate >= 45 && rotate < 135) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = x1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } stats.width = ny; stats.height = nx; } /* Rotate by 180 degrees */ else if (rotate >= 135 && rotate < 225) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) rotBuffer[(y2 * nx) + x1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; for (y1 = 0; y1 < ny; y1++) rotBuffer[(y1 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { y2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { x2 = nx - x1 - 1; rotBuffer[(y2 * nx) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } } /* Rotate by 270 degrees */ else if (rotate >= 225 && rotate < 315) { if (mirror == 1) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) rotBuffer[(x1 * ny) + y1 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } else if (mirror == 2) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = ny - y1 - 1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } else { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { x2 = y1; for (x1 = 0; x1 < nx; x1++) { y2 = nx - x1 - 1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } stats.width = ny; stats.height = nx; } /* If rotating by more than 315 degrees, assume top-bottom reflection */ else if (rotate >= 315 && mirror) { for (int i = 0; i < m_Channels; i++) { offset = stats.samples_per_channel * i; for (y1 = 0; y1 < ny; y1++) { for (x1 = 0; x1 < nx; x1++) { x2 = y1; y2 = x1; rotBuffer[(y2 * ny) + x2 + offset] = buffer[(y1 * nx) + x1 + offset]; } } } } delete[] m_ImageBuffer; m_ImageBuffer = rotimage; return true; } void FITSData::rotWCSFITS(int angle, int mirror) { int status = 0; char comment[100]; double ctemp1, ctemp2, ctemp3, ctemp4, naxis1, naxis2; int WCS_DECIMALS = 6; naxis1 = stats.width; naxis2 = stats.height; if (fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) { // No WCS keywords return; } /* Reset CROTAn and CD matrix if axes have been exchanged */ if (angle == 90) { if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA1", ctemp1 + 90.0, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA2", ctemp1 + 90.0, WCS_DECIMALS, comment, &status); } status = 0; /* Negate rotation angle if mirrored */ if (mirror != 0) { if (!fits_read_key_dbl(fptr, "CROTA1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA1", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CROTA2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CROTA2", -ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "LTM1_1", -ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD1_2", -ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp1, comment, &status)) fits_update_key_dbl(fptr, "CD2_1", -ctemp1, WCS_DECIMALS, comment, &status); } status = 0; /* Unbin CRPIX and CD matrix */ if (!fits_read_key_dbl(fptr, "LTM1_1", &ctemp1, comment, &status)) { if (ctemp1 != 1.0) { if (!fits_read_key_dbl(fptr, "LTM2_2", &ctemp2, comment, &status)) if (ctemp1 == ctemp2) { double ltv1 = 0.0; double ltv2 = 0.0; status = 0; if (!fits_read_key_dbl(fptr, "LTV1", <v1, comment, &status)) fits_delete_key(fptr, "LTV1", &status); if (!fits_read_key_dbl(fptr, "LTV2", <v2, comment, &status)) fits_delete_key(fptr, "LTV2", &status); status = 0; if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CRPIX1", (ctemp3 - ltv1) / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CRPIX2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CRPIX2", (ctemp3 - ltv2) / ctemp1, WCS_DECIMALS, comment, &status); status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD1_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD1_2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD1_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD2_1", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); if (!fits_read_key_dbl(fptr, "CD2_2", &ctemp3, comment, &status)) fits_update_key_dbl(fptr, "CD2_2", ctemp3 / ctemp1, WCS_DECIMALS, comment, &status); status = 0; fits_delete_key(fptr, "LTM1_1", &status); fits_delete_key(fptr, "LTM1_2", &status); } } } status = 0; /* Reset CRPIXn */ if (!fits_read_key_dbl(fptr, "CRPIX1", &ctemp1, comment, &status) && !fits_read_key_dbl(fptr, "CRPIX2", &ctemp2, comment, &status)) { if (mirror != 0) { if (angle == 0) fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); else if (angle == 90) { fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CRPIX1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CRPIX1", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CRPIX1", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis2 - ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CRPIX1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CRPIX2", naxis1 - ctemp1, WCS_DECIMALS, comment, &status); } } } status = 0; /* Reset CDELTn (degrees per pixel) */ if (!fits_read_key_dbl(fptr, "CDELT1", &ctemp1, comment, &status) && !fits_read_key_dbl(fptr, "CDELT2", &ctemp2, comment, &status)) { if (mirror != 0) { if (angle == 0) fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status); else if (angle == 90) { fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CDELT1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CDELT1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CDELT1", -ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp2, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CDELT1", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CDELT2", -ctemp1, WCS_DECIMALS, comment, &status); } } } /* Reset CD matrix, if present */ ctemp1 = 0.0; ctemp2 = 0.0; ctemp3 = 0.0; ctemp4 = 0.0; status = 0; if (!fits_read_key_dbl(fptr, "CD1_1", &ctemp1, comment, &status)) { fits_read_key_dbl(fptr, "CD1_2", &ctemp2, comment, &status); fits_read_key_dbl(fptr, "CD2_1", &ctemp3, comment, &status); fits_read_key_dbl(fptr, "CD2_2", &ctemp4, comment, &status); status = 0; if (mirror != 0) { if (angle == 0) { fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); } else if (angle == 90) { fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CD1_1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status); } } else { if (angle == 90) { fits_update_key_dbl(fptr, "CD1_1", -ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", ctemp1, WCS_DECIMALS, comment, &status); } else if (angle == 180) { fits_update_key_dbl(fptr, "CD1_1", -ctemp1, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp4, WCS_DECIMALS, comment, &status); } else if (angle == 270) { fits_update_key_dbl(fptr, "CD1_1", ctemp4, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD1_2", ctemp3, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_1", -ctemp2, WCS_DECIMALS, comment, &status); fits_update_key_dbl(fptr, "CD2_2", -ctemp1, WCS_DECIMALS, comment, &status); } } } /* Delete any polynomial solution */ /* (These could maybe be switched, but I don't want to work them out yet */ status = 0; if (!fits_read_key_dbl(fptr, "CO1_1", &ctemp1, comment, &status)) { int i; char keyword[16]; for (i = 1; i < 13; i++) { sprintf(keyword, "CO1_%d", i); fits_delete_key(fptr, keyword, &status); } for (i = 1; i < 13; i++) { sprintf(keyword, "CO2_%d", i); fits_delete_key(fptr, keyword, &status); } } } uint8_t *FITSData::getImageBuffer() { return m_ImageBuffer; } void FITSData::setImageBuffer(uint8_t *buffer) { delete[] m_ImageBuffer; m_ImageBuffer = buffer; } bool FITSData::checkDebayer() { int status = 0; char bayerPattern[64]; // Let's search for BAYERPAT keyword, if it's not found we return as there is no bayer pattern in this image if (fits_read_keyword(fptr, "BAYERPAT", bayerPattern, nullptr, &status)) return false; if (stats.bitpix != 16 && stats.bitpix != 8) { KSNotification::error(i18n("Only 8 and 16 bits bayered images supported."), i18n("Debayer error")); return false; } QString pattern(bayerPattern); pattern = pattern.remove('\'').trimmed(); if (pattern == "RGGB") debayerParams.filter = DC1394_COLOR_FILTER_RGGB; else if (pattern == "GBRG") debayerParams.filter = DC1394_COLOR_FILTER_GBRG; else if (pattern == "GRBG") debayerParams.filter = DC1394_COLOR_FILTER_GRBG; else if (pattern == "BGGR") debayerParams.filter = DC1394_COLOR_FILTER_BGGR; // We return unless we find a valid pattern else { KSNotification::error(i18n("Unsupported bayer pattern %1.", pattern), i18n("Debayer error")); return false; } fits_read_key(fptr, TINT, "XBAYROFF", &debayerParams.offsetX, nullptr, &status); fits_read_key(fptr, TINT, "YBAYROFF", &debayerParams.offsetY, nullptr, &status); HasDebayer = true; return true; } void FITSData::getBayerParams(BayerParams *param) { param->method = debayerParams.method; param->filter = debayerParams.filter; param->offsetX = debayerParams.offsetX; param->offsetY = debayerParams.offsetY; } void FITSData::setBayerParams(BayerParams *param) { debayerParams.method = param->method; debayerParams.filter = param->filter; debayerParams.offsetX = param->offsetX; debayerParams.offsetY = param->offsetY; } bool FITSData::debayer() { if (bayerBuffer == nullptr) { int anynull = 0, status = 0; bayerBuffer = m_ImageBuffer; if (fits_read_img(fptr, m_DataType, 1, stats.samples_per_channel, nullptr, bayerBuffer, &anynull, &status)) { char errmsg[512]; fits_get_errstatus(status, errmsg); KSNotification::error(i18n("Error reading image: %1", QString(errmsg)), i18n("Debayer error")); return false; } } switch (m_DataType) { case TBYTE: return debayer_8bit(); case TUSHORT: return debayer_16bit(); default: return false; } return false; } bool FITSData::debayer_8bit() { dc1394error_t error_code; int rgb_size = stats.samples_per_channel * 3 * stats.bytesPerPixel; auto *destinationBuffer = new uint8_t[rgb_size]; if (destinationBuffer == nullptr) { KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } int ds1394_height = stats.height; uint8_t *dc1394_source = bayerBuffer; if (debayerParams.offsetY == 1) { dc1394_source += stats.width; ds1394_height--; } if (debayerParams.offsetX == 1) { dc1394_source++; } error_code = dc1394_bayer_decoding_8bit(dc1394_source, destinationBuffer, stats.width, ds1394_height, debayerParams.filter, debayerParams.method); if (error_code != DC1394_SUCCESS) { KSNotification::error(i18n("Debayer failed (%1)", error_code), i18n("Debayer error")); m_Channels = 1; delete[] destinationBuffer; return false; } if (m_Channels == 1) { delete[] m_ImageBuffer; m_ImageBuffer = new uint8_t[rgb_size]; if (m_ImageBuffer == nullptr) { delete[] destinationBuffer; KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } } // Data in R1G1B1, we need to copy them into 3 layers for FITS uint8_t *rBuff = m_ImageBuffer; uint8_t *gBuff = m_ImageBuffer + (stats.width * stats.height); uint8_t *bBuff = m_ImageBuffer + (stats.width * stats.height * 2); int imax = stats.samples_per_channel * 3 - 3; for (int i = 0; i <= imax; i += 3) { *rBuff++ = destinationBuffer[i]; *gBuff++ = destinationBuffer[i + 1]; *bBuff++ = destinationBuffer[i + 2]; } m_Channels = (m_Mode == FITS_NORMAL) ? 3 : 1; delete[] destinationBuffer; bayerBuffer = nullptr; return true; } bool FITSData::debayer_16bit() { dc1394error_t error_code; int rgb_size = stats.samples_per_channel * 3 * stats.bytesPerPixel; auto *destinationBuffer = new uint8_t[rgb_size]; auto *buffer = reinterpret_cast(bayerBuffer); auto *dstBuffer = reinterpret_cast(destinationBuffer); if (destinationBuffer == nullptr) { KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } int ds1394_height = stats.height; uint16_t *dc1394_source = buffer; if (debayerParams.offsetY == 1) { dc1394_source += stats.width; ds1394_height--; } if (debayerParams.offsetX == 1) { dc1394_source++; } error_code = dc1394_bayer_decoding_16bit(dc1394_source, dstBuffer, stats.width, ds1394_height, debayerParams.filter, debayerParams.method, 16); if (error_code != DC1394_SUCCESS) { KSNotification::error(i18n("Debayer failed (%1)", error_code), i18n("Debayer error")); m_Channels = 1; delete[] destinationBuffer; return false; } if (m_Channels == 1) { delete[] m_ImageBuffer; m_ImageBuffer = new uint8_t[rgb_size]; if (m_ImageBuffer == nullptr) { delete[] destinationBuffer; KSNotification::error(i18n("Unable to allocate memory for temporary bayer buffer."), i18n("Debayer error")); return false; } } buffer = reinterpret_cast(m_ImageBuffer); // Data in R1G1B1, we need to copy them into 3 layers for FITS uint16_t *rBuff = buffer; uint16_t *gBuff = buffer + (stats.width * stats.height); uint16_t *bBuff = buffer + (stats.width * stats.height * 2); int imax = stats.samples_per_channel * 3 - 3; for (int i = 0; i <= imax; i += 3) { *rBuff++ = dstBuffer[i]; *gBuff++ = dstBuffer[i + 1]; *bBuff++ = dstBuffer[i + 2]; } m_Channels = (m_Mode == FITS_NORMAL) ? 3 : 1; delete[] destinationBuffer; bayerBuffer = nullptr; return true; } double FITSData::getADU() const { double adu = 0; for (int i = 0; i < m_Channels; i++) adu += stats.mean[i]; return (adu / static_cast(m_Channels)); } /* CannyDetector, Implementation of Canny edge detector in Qt/C++. * Copyright (C) 2015 Gonzalo Exequiel Pedone * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Email : hipersayan DOT x AT gmail DOT com * Web-Site: http://github.com/hipersayanX/CannyDetector */ #if 0 void FITSData::sobel(const QImage &image, QVector &gradient, QVector &direction) { int size = image.width() * image.height(); gradient.resize(size); direction.resize(size); for (int y = 0; y < image.height(); y++) { size_t yOffset = y * image.width(); const quint8 * grayLine = image.constBits() + yOffset; const quint8 * grayLine_m1 = y < 1? grayLine: grayLine - image.width(); const quint8 * grayLine_p1 = y >= image.height() - 1? grayLine: grayLine + image.width(); int * gradientLine = gradient.data() + yOffset; int * directionLine = direction.data() + yOffset; for (int x = 0; x < image.width(); x++) { int x_m1 = x < 1? x: x - 1; int x_p1 = x >= image.width() - 1? x: x + 1; int gradX = grayLine_m1[x_p1] + 2 * grayLine[x_p1] + grayLine_p1[x_p1] - grayLine_m1[x_m1] - 2 * grayLine[x_m1] - grayLine_p1[x_m1]; int gradY = grayLine_m1[x_m1] + 2 * grayLine_m1[x] + grayLine_m1[x_p1] - grayLine_p1[x_m1] - 2 * grayLine_p1[x] - grayLine_p1[x_p1]; gradientLine[x] = qAbs(gradX) + qAbs(gradY); /* Gradient directions are classified in 4 possible cases * * dir 0 * * x x x * - - - * x x x * * dir 1 * * x x / * x / x * / x x * * dir 2 * * \ x x * x \ x * x x \ * * dir 3 * * x | x * x | x * x | x */ if (gradX == 0 && gradY == 0) directionLine[x] = 0; else if (gradX == 0) directionLine[x] = 3; else { qreal a = 180. * atan(qreal(gradY) / gradX) / M_PI; if (a >= -22.5 && a < 22.5) directionLine[x] = 0; else if (a >= 22.5 && a < 67.5) directionLine[x] = 1; else if (a >= -67.5 && a < -22.5) directionLine[x] = 2; else directionLine[x] = 3; } } } } #endif template void FITSData::sobel(QVector &gradient, QVector &direction) { //int size = image.width() * image.height(); gradient.resize(stats.samples_per_channel); direction.resize(stats.samples_per_channel); for (int y = 0; y < stats.height; y++) { size_t yOffset = y * stats.width; const T *grayLine = reinterpret_cast(m_ImageBuffer) + yOffset; const T *grayLine_m1 = y < 1 ? grayLine : grayLine - stats.width; const T *grayLine_p1 = y >= stats.height - 1 ? grayLine : grayLine + stats.width; float *gradientLine = gradient.data() + yOffset; float *directionLine = direction.data() + yOffset; for (int x = 0; x < stats.width; x++) { int x_m1 = x < 1 ? x : x - 1; int x_p1 = x >= stats.width - 1 ? x : x + 1; int gradX = grayLine_m1[x_p1] + 2 * grayLine[x_p1] + grayLine_p1[x_p1] - grayLine_m1[x_m1] - 2 * grayLine[x_m1] - grayLine_p1[x_m1]; int gradY = grayLine_m1[x_m1] + 2 * grayLine_m1[x] + grayLine_m1[x_p1] - grayLine_p1[x_m1] - 2 * grayLine_p1[x] - grayLine_p1[x_p1]; gradientLine[x] = qAbs(gradX) + qAbs(gradY); /* Gradient directions are classified in 4 possible cases * * dir 0 * * x x x * - - - * x x x * * dir 1 * * x x / * x / x * / x x * * dir 2 * * \ x x * x \ x * x x \ * * dir 3 * * x | x * x | x * x | x */ if (gradX == 0 && gradY == 0) directionLine[x] = 0; else if (gradX == 0) directionLine[x] = 3; else { qreal a = 180. * atan(qreal(gradY) / gradX) / M_PI; if (a >= -22.5 && a < 22.5) directionLine[x] = 0; else if (a >= 22.5 && a < 67.5) directionLine[x] = 2; else if (a >= -67.5 && a < -22.5) directionLine[x] = 1; else directionLine[x] = 3; } } } } int FITSData::partition(int width, int height, QVector &gradient, QVector &ids) { int id = 0; for (int y = 1; y < height - 1; y++) { for (int x = 1; x < width - 1; x++) { int index = x + y * width; float val = gradient[index]; if (val > 0 && ids[index] == 0) { trace(width, height, ++id, gradient, ids, x, y); } } } // Return max id return id; } void FITSData::trace(int width, int height, int id, QVector &image, QVector &ids, int x, int y) { int yOffset = y * width; float *cannyLine = image.data() + yOffset; int *idLine = ids.data() + yOffset; if (idLine[x] != 0) return; idLine[x] = id; for (int j = -1; j < 2; j++) { int nextY = y + j; if (nextY < 0 || nextY >= height) continue; float *cannyLineNext = cannyLine + j * width; for (int i = -1; i < 2; i++) { int nextX = x + i; if (i == j || nextX < 0 || nextX >= width) continue; if (cannyLineNext[nextX] > 0) { // Trace neighbors. trace(width, height, id, image, ids, nextX, nextY); } } } } QString FITSData::getLastError() const { return lastError; } bool FITSData::getAutoRemoveTemporaryFITS() const { return autoRemoveTemporaryFITS; } void FITSData::setAutoRemoveTemporaryFITS(bool value) { autoRemoveTemporaryFITS = value; } #if 0 QVector FITSData::thinning(int width, int height, const QVector &gradient, const QVector &direction) { QVector thinned(gradient.size()); for (int y = 0; y < height; y++) { int yOffset = y * width; const int * gradientLine = gradient.constData() + yOffset; const int * gradientLine_m1 = y < 1? gradientLine: gradientLine - width; const int * gradientLine_p1 = y >= height - 1? gradientLine: gradientLine + width; const int * directionLine = direction.constData() + yOffset; int * thinnedLine = thinned.data() + yOffset; for (int x = 0; x < width; x++) { int x_m1 = x < 1? 0: x - 1; int x_p1 = x >= width - 1? x: x + 1; int direction = directionLine[x]; int pixel = 0; if (direction == 0) { /* x x x * - - - * x x x */ if (gradientLine[x] < gradientLine[x_m1] || gradientLine[x] < gradientLine[x_p1]) pixel = 0; else pixel = gradientLine[x]; } else if (direction == 1) { /* x x / * x / x * / x x */ if (gradientLine[x] < gradientLine_m1[x_p1] || gradientLine[x] < gradientLine_p1[x_m1]) pixel = 0; else pixel = gradientLine[x]; } else if (direction == 2) { /* \ x x * x \ x * x x \ */ if (gradientLine[x] < gradientLine_m1[x_m1] || gradientLine[x] < gradientLine_p1[x_p1]) pixel = 0; else pixel = gradientLine[x]; } else { /* x | x * x | x * x | x */ if (gradientLine[x] < gradientLine_m1[x] || gradientLine[x] < gradientLine_p1[x]) pixel = 0; else pixel = gradientLine[x]; } thinnedLine[x] = pixel; } } return thinned; } QVector FITSData::threshold(int thLow, int thHi, const QVector &image) { QVector thresholded(image.size()); for (int i = 0; i < image.size(); i++) thresholded[i] = image[i] <= thLow? 0: image[i] >= thHi? 255: 127; return thresholded; } QVector FITSData::hysteresis(int width, int height, const QVector &image) { QVector canny(image); for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) trace(width, height, canny, x, y); // Remaining gray pixels becomes black. for (int i = 0; i < canny.size(); i++) if (canny[i] == 127) canny[i] = 0; return canny; } #endif template void FITSData::convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image) { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-align" auto *buffer = (T*)getImageBuffer(); #pragma GCC diagnostic pop const T limit = std::numeric_limits::max(); T bMin = dataMin < 0 ? 0 : dataMin; T bMax = dataMax > limit ? limit : dataMax; uint16_t w = width(); uint16_t h = height(); uint32_t size = w*h; double val; if (channels() == 1) { /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < h; j++) { unsigned char *scanLine = image.scanLine(j); for (int i = 0; i < w; i++) { val = qBound(bMin, buffer[j * w + i], bMax); val = val * scale + zero; scanLine[i] = qBound(0, (unsigned char)val, 255); } } } else { double rval = 0, gval = 0, bval = 0; QRgb value; /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < h; j++) { auto *scanLine = reinterpret_cast((image.scanLine(j))); for (int i = 0; i < w; i++) { rval = qBound(bMin, buffer[j * w + i], bMax); gval = qBound(bMin, buffer[j * w + i + size], bMax); bval = qBound(bMin, buffer[j * w + i + size * 2], bMax); value = qRgb(rval * scale + zero, gval * scale + zero, bval * scale + zero); scanLine[i] = value; } } } } QImage FITSData::FITSToImage(const QString &filename) { QImage fitsImage; double min, max; FITSData data; QFuture future = data.loadFITS(filename); // Wait synchronously future.waitForFinished(); if (future.result() == false) return fitsImage; data.getMinMax(&min, &max); if (min == max) { fitsImage.fill(Qt::white); return fitsImage; } if (data.channels() == 1) { fitsImage = QImage(data.width(), data.height(), QImage::Format_Indexed8); fitsImage.setColorCount(256); for (int i = 0; i < 256; i++) fitsImage.setColor(i, qRgb(i, i, i)); } else { fitsImage = QImage(data.width(), data.height(), QImage::Format_RGB32); } double dataMin = data.stats.mean[0] - data.stats.stddev[0]; double dataMax = data.stats.mean[0] + data.stats.stddev[0] * 3; double bscale = 255. / (dataMax - dataMin); double bzero = (-dataMin) * (255. / (dataMax - dataMin)); // Long way to do this since we do not want to use templated functions here switch (data.property("dataType").toInt()) { case TBYTE: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TSHORT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TUSHORT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TLONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TULONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TFLOAT: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TLONGLONG: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; case TDOUBLE: data.convertToQImage(dataMin, dataMax, bscale, bzero, fitsImage); break; default: break; } return fitsImage; } bool FITSData::createWCSFile(const QString &newWCSFile, double orientation, double ra, double dec, double pixscale) { int status = 0, exttype = 0; long nelements; fitsfile *new_fptr; char errMsg[512]; qCInfo(KSTARS_FITS) << "Creating new WCS file:" << newWCSFile << "with parameters Orientation:" << orientation << "RA:" << ra << "DE:" << dec << "Pixel Scale:" << pixscale; nelements = stats.samples_per_channel * m_Channels; /* Create a new File, overwriting existing*/ if (fits_create_file(&new_fptr, QString('!' + newWCSFile).toLatin1(), &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } if (fits_copy_file(fptr, new_fptr, 1, 1, 1, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* close current file */ if (fits_close_file(fptr, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } status = 0; if (m_isTemporary && autoRemoveTemporaryFITS) { QFile::remove(m_Filename); m_isTemporary = false; qCDebug(KSTARS_FITS) << "Removing FITS File: " << m_Filename; } m_Filename = newWCSFile; m_isTemporary = true; fptr = new_fptr; if (fits_movabs_hdu(fptr, 1, &exttype, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* Write Data */ if (fits_write_img(fptr, m_DataType, 1, nelements, m_ImageBuffer, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } /* Write keywords */ // Minimum if (fits_update_key(fptr, TDOUBLE, "DATAMIN", &(stats.min), "Minimum value", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // Maximum if (fits_update_key(fptr, TDOUBLE, "DATAMAX", &(stats.max), "Maximum value", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // NAXIS1 if (fits_update_key(fptr, TUSHORT, "NAXIS1", &(stats.width), "length of data axis 1", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } // NAXIS2 if (fits_update_key(fptr, TUSHORT, "NAXIS2", &(stats.height), "length of data axis 2", &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } fits_update_key(fptr, TDOUBLE, "OBJCTRA", &ra, "Object RA", &status); fits_update_key(fptr, TDOUBLE, "OBJCTDEC", &dec, "Object DEC", &status); int epoch = 2000; fits_update_key(fptr, TINT, "EQUINOX", &epoch, "Equinox", &status); fits_update_key(fptr, TDOUBLE, "CRVAL1", &ra, "CRVAL1", &status); fits_update_key(fptr, TDOUBLE, "CRVAL2", &dec, "CRVAL1", &status); char radecsys[8] = "FK5"; char ctype1[16] = "RA---TAN"; char ctype2[16] = "DEC--TAN"; fits_update_key(fptr, TSTRING, "RADECSYS", radecsys, "RADECSYS", &status); fits_update_key(fptr, TSTRING, "CTYPE1", ctype1, "CTYPE1", &status); fits_update_key(fptr, TSTRING, "CTYPE2", ctype2, "CTYPE2", &status); double crpix1 = width() / 2.0; double crpix2 = height() / 2.0; fits_update_key(fptr, TDOUBLE, "CRPIX1", &crpix1, "CRPIX1", &status); fits_update_key(fptr, TDOUBLE, "CRPIX2", &crpix2, "CRPIX2", &status); // Arcsecs per Pixel double secpix1 = pixscale; double secpix2 = pixscale; fits_update_key(fptr, TDOUBLE, "SECPIX1", &secpix1, "SECPIX1", &status); fits_update_key(fptr, TDOUBLE, "SECPIX2", &secpix2, "SECPIX2", &status); double degpix1 = secpix1 / 3600.0; double degpix2 = secpix2 / 3600.0; fits_update_key(fptr, TDOUBLE, "CDELT1", °pix1, "CDELT1", &status); fits_update_key(fptr, TDOUBLE, "CDELT2", °pix2, "CDELT2", &status); // Rotation is CW, we need to convert it to CCW per CROTA1 definition double rotation = 360 - orientation; if (rotation > 360) rotation -= 360; fits_update_key(fptr, TDOUBLE, "CROTA1", &rotation, "CROTA1", &status); fits_update_key(fptr, TDOUBLE, "CROTA2", &rotation, "CROTA2", &status); // ISO Date if (fits_write_date(fptr, &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } QString history = QString("Modified by KStars on %1").arg(QDateTime::currentDateTime().toString("yyyy-MM-ddThh:mm:ss")); // History if (fits_write_history(fptr, history.toLatin1(), &status)) { fits_get_errstatus(status, errMsg); lastError = QString(errMsg); fits_report_error(stderr, status); return false; } fits_flush_file(fptr, &status); WCSLoaded = false; qCDebug(KSTARS_FITS) << "Finished creating WCS file: " << newWCSFile; return true; } bool FITSData::contains(const QPointF &point) const { return (point.x() >= 0 && point.y() >= 0 && point.x() <= stats.width && point.y() <= stats.height); } int FITSData::findSEPStars(const QRect &boundary) { int x=0,y=0,w=stats.width,h=stats.height, maxRadius=50; if (!boundary.isNull()) { x = boundary.x(); y = boundary.y(); w = boundary.width(); h = boundary.height(); maxRadius = w; } auto *data = new float[w*h]; switch (stats.bitpix) { case BYTE_IMG: getFloatBuffer(data, x, y, w, h); break; case SHORT_IMG: getFloatBuffer(data, x, y, w, h); break; case USHORT_IMG: getFloatBuffer(data, x, y, w, h); break; case LONG_IMG: getFloatBuffer(data, x, y, w, h); break; case ULONG_IMG: getFloatBuffer(data, x, y, w, h); break; case FLOAT_IMG: delete [] data; data = reinterpret_cast(m_ImageBuffer); break; case LONGLONG_IMG: getFloatBuffer(data, x, y, w, h); break; case DOUBLE_IMG: getFloatBuffer(data, x, y, w, h); break; default: delete [] data; return -1; } float *imback = nullptr; double *flux= nullptr, *fluxerr= nullptr, *area= nullptr; short *flag = nullptr; short flux_flag=0; int status = 0; sep_bkg *bkg = nullptr; sep_catalog *catalog = nullptr; float conv[] = {1,2,1, 2,4,2, 1,2,1}; double flux_fractions[2] = {0}; double requested_frac[2] = { 0.5, 0.99 }; QList edges; // #0 Create SEP Image structure sep_image im = {data, nullptr, nullptr, SEP_TFLOAT, 0, 0, w, h, 0.0, SEP_NOISE_NONE, 1.0, 0.0}; // #1 Background estimate status = sep_background(&im, 64, 64, 3, 3, 0.0, &bkg); if (status != 0) goto exit; // #2 Background evaluation imback = (float *)malloc((w * h)*sizeof(float)); status = sep_bkg_array(bkg, imback, SEP_TFLOAT); if (status != 0) goto exit; // #3 Background subtraction status = sep_bkg_subarray(bkg, im.data, im.dtype); if (status != 0) goto exit; // #4 Source Extraction // Note that we set deblend_cont = 1.0 to turn off deblending. status = sep_extract(&im, 2*bkg->globalrms, SEP_THRESH_ABS, 10, conv, 3, 3, SEP_FILTER_CONV, 32, 1.0, 1, 1.0, &catalog); if (status != 0) goto exit; #if 0 // #4 Aperture photometry im.noise = &(bkg->globalrms); /* set image noise level */ im.ndtype = SEP_TFLOAT; fluxt = flux = (double *)malloc(catalog->nobj * sizeof(double)); fluxerrt = fluxerr = (double *)malloc(catalog->nobj * sizeof(double)); areat = area = (double *)malloc(catalog->nobj * sizeof(double)); flagt = flag = (short *)malloc(catalog->nobj * sizeof(short)); for (int i=0; inobj; i++, fluxt++, fluxerrt++, flagt++, areat++) sep_sum_circle(&im, catalog->x[i], catalog->y[i], 10.0, 5, 0, fluxt, fluxerrt, areat, flagt); #endif // TODO // Must detect edge detection // Must limit to brightest 100 (by flux) centers // Should probably use ellipse to draw instead of simple circle? // Useful for galaxies and also elenogated stars. for (int i=0; inobj; i++) { double flux = catalog->flux[i]; // Get HFR sep_flux_radius(&im, catalog->x[i], catalog->y[i], maxRadius, 5, 0, &flux, requested_frac, 2, flux_fractions, &flux_flag); auto *center = new Edge(); center->x = catalog->x[i]+x+0.5; center->y = catalog->y[i]+y+0.5; center->val = catalog->peak[i]; center->sum = flux; center->HFR = center->width = flux_fractions[0]; if (flux_fractions[1] < maxRadius) center->width = flux_fractions[1]*2; edges.append(center); } // Let's sort edges, starting with widest qSort(edges.begin(), edges.end(), [](const Edge *edge1, const Edge *edge2) -> bool { return edge1->width > edge2->width;}); // Take only the first 100 stars { int starCount = qMin(100, edges.count()); for (int i=0; i < starCount; i++) starCenters.append(edges[i]); } edges.clear(); qCDebug(KSTARS_FITS) << qSetFieldWidth(10) << "#" << "#X" << "#Y" << "#Flux" << "#Width" << "#HFR";; for (int i=0; i < starCenters.count(); i++) qCDebug(KSTARS_FITS) << qSetFieldWidth(10) << i << starCenters[i]->x << starCenters[i]->y << starCenters[i]->sum << starCenters[i]->width << starCenters[i]->HFR; exit: if (stats.bitpix != FLOAT_IMG) delete [] data; sep_bkg_free(bkg); sep_catalog_free(catalog); free(imback); free(flux); free(fluxerr); free(area); free(flag); if (status != 0) { char errorMessage[512]; sep_get_errmsg(status, errorMessage); qCritical(KSTARS_FITS) << errorMessage; return -1; } return starCenters.count(); } template void FITSData::getFloatBuffer(float *buffer, int x, int y, int w, int h) { auto *rawBuffer = reinterpret_cast(m_ImageBuffer); float *floatPtr = buffer; int x2 = x+w; int y2 = y+h; for (int y1=y; y1 < y2; y1++) { int offset = y1*stats.width; for (int x1=x; x1 < x2; x1++) { *floatPtr++ = rawBuffer[offset+x1]; } } } + +void FITSData::saveStatistics(Statistic &other) +{ + other = stats; +} + +void FITSData::restoreStatistics(Statistic &other) +{ + stats = other; +} diff --git a/kstars/fitsviewer/fitsdata.h b/kstars/fitsviewer/fitsdata.h index a29773334..52d407633 100644 --- a/kstars/fitsviewer/fitsdata.h +++ b/kstars/fitsviewer/fitsdata.h @@ -1,444 +1,447 @@ /*************************************************************************** fitsimage.cpp - FITS Image ------------------- begin : Tue Feb 24 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * Some code fragments were adapted from Peter Kirchgessner's FITS plugin* * See http://members.aol.com/pkirchg for more details. * ***************************************************************************/ #pragma once #include "config-kstars.h" #include "bayer.h" #include "fitscommon.h" #ifdef WIN32 // This header must be included before fitsio.h to avoid compiler errors with Visual Studio #include #endif #include #include #include #include #include #ifndef KSTARS_LITE -#include "fitshistogram.h" - #include #ifdef HAVE_WCSLIB #include #endif #endif #define MINIMUM_PIXEL_RANGE 5 #define MINIMUM_STDVAR 5 class QProgressDialog; class SkyObject; class SkyPoint; +class FITSHistogram; typedef struct { float ra; float dec; } wcs_point; class Edge { public: float x; float y; int val; int scanned; float width; float HFR; float sum; }; class FITSSkyObject : public QObject { Q_OBJECT public: explicit FITSSkyObject(SkyObject *object, int xPos, int yPos); SkyObject *skyObject(); int x(); int y(); void setX(int xPos); void setY(int yPos); private: SkyObject *skyObjectStored; int xLoc; int yLoc; }; class FITSData : public QObject { Q_OBJECT // Name of FITS file Q_PROPERTY(QString filename READ filename) // Size of file in bytes Q_PROPERTY(qint64 size READ size) // Width in pixels Q_PROPERTY(quint16 width READ width) // Height in pixels Q_PROPERTY(quint16 height READ height) // FITS MODE --> Normal, Focus, Guide..etc Q_PROPERTY(FITSMode mode MEMBER m_Mode) // 1 channel (grayscale) or 3 channels (RGB) Q_PROPERTY(quint8 channels READ channels) // Data type (BYTE, SHORT, INT..etc) Q_PROPERTY(quint32 dataType MEMBER m_DataType) // Bits per pixel Q_PROPERTY(quint8 bpp READ bpp WRITE setBPP) // Does FITS have WSC header? Q_PROPERTY(bool hasWCS READ hasWCS) // Does FITS have bayer data? Q_PROPERTY(bool hasDebyaer READ hasDebayer) public: explicit FITSData(FITSMode fitsMode = FITS_NORMAL); explicit FITSData(const FITSData *other); ~FITSData(); /** Structure to hold FITS Header records */ typedef struct { QString key; /** FITS Header Key */ QVariant value; /** FITS Header Value */ QString comment; /** FITS Header Comment, if any */ } Record; + /// Stats struct to hold statisical data about the FITS data + typedef struct + { + double min[3] = {0}, max[3] = {0}; + double mean[3] = {0}; + double stddev[3] = {0}; + double median[3] = {0}; + double SNR { 0 }; + int bitpix { 8 }; + int bytesPerPixel { 1 }; + int ndim { 2 }; + int64_t size { 0 }; + uint32_t samples_per_channel { 0 }; + uint16_t width { 0 }; + uint16_t height { 0 }; + } Statistic; + /** * @brief loadFITS Loading FITS file asynchronously. * @param inFilename Path to FITS file (or compressed fits.gz) * @param silent If set, error messages are ignored. If set to false, the error message will get displayed in a popup. * @return A QFuture that can be watched until the async operation is complete. */ QFuture loadFITS(const QString &inFilename, bool silent = true); /* Save FITS */ int saveFITS(const QString &newFilename); /* Rescale image lineary from image_buffer, fit to window if desired */ int rescale(FITSZoom type); /* Calculate stats */ void calculateStats(bool refresh = false); /* Check if a paricular point exists within the image */ bool contains(const QPointF &point) const; // Access functions void clearImageBuffers(); void setImageBuffer(uint8_t *buffer); uint8_t *getImageBuffer(); // Statistics + void saveStatistics(Statistic &other); + void restoreStatistics(Statistic &other); + uint16_t width() const { return stats.width;} uint16_t height() const { return stats.height;} int64_t size() const { return stats.size; } int channels() const { return m_Channels; } double getMin(uint8_t channel = 0) const { return stats.min[channel]; } double getMax(uint8_t channel = 0) const { return stats.max[channel]; } void setMinMax(double newMin, double newMax, uint8_t channel = 0); void getMinMax(double *min, double *max, uint8_t channel = 0) const { *min = stats.min[channel]; *max = stats.max[channel]; } void setStdDev(double value, uint8_t channel = 0) { stats.stddev[channel] = value; } double getStdDev(uint8_t channel = 0) const { return stats.stddev[channel]; } void setMean(double value, uint8_t channel = 0) { stats.mean[channel] = value; } double getMean(uint8_t channel = 0) const { return stats.mean[channel]; } void setMedian(double val, uint8_t channel = 0) { stats.median[channel] = val; } double getMedian(uint8_t channel = 0) const { return stats.median[channel]; } int getBytesPerPixel() const { return stats.bytesPerPixel; } void setSNR(double val) { stats.SNR = val; } double getSNR() const { return stats.SNR; } void setBPP(uint8_t value) { stats.bitpix = value; } uint32_t bpp() const { return stats.bitpix; } double getADU() const; // FITS Record bool getRecordValue(const QString &key, QVariant &value) const; const QList & getRecords() const {return records;} // Star Detection - Native KStars implementation void setStarAlgorithm(StarAlgorithm algorithm){ starAlgorithm = algorithm; } int getDetectedStars() const { return starCenters.count(); } bool areStarsSearched() const { return starsSearched; } void appendStar(Edge *newCenter) { starCenters.append(newCenter); } QList getStarCenters() const { return starCenters; } QList getStarCentersInSubFrame(QRect subFrame) const; int findStars(StarAlgorithm algorithm = ALGORITHM_CENTROID, const QRect &trackingBox = QRect()); void getCenterSelection(int *x, int *y); int findOneStar(const QRect &boundary); // Star Detection - Partially customized Canny edge detection algorithm static int findCannyStar(FITSData *data, const QRect &boundary = QRect()); template static int findCannyStar(FITSData *data, const QRect &boundary); // Use SEP (Sextractor Library) to find stars template void getFloatBuffer(float *buffer, int x, int y, int w, int h); int findSEPStars(const QRect &boundary = QRect()); // Half Flux Radius Edge *getMaxHFRStar() const { return maxHFRStar; } double getHFR(HFRType type = HFR_AVERAGE); double getHFR(int x, int y); // WCS // Check if image has valid WCS header information and set HasWCS accordingly. Call in loadFITS() bool checkForWCS(); // Does image have valid WCS? bool hasWCS() { return HasWCS; } // Load WCS data bool loadWCS(); // Is WCS Image loaded? bool isWCSLoaded() { return WCSLoaded; } wcs_point *getWCSCoord() { return wcs_coord; } /** * @brief wcsToPixel Given J2000 (RA0,DE0) coordinates. Find in the image the corresponding pixel coordinates. * @param wcsCoord Coordinates of target * @param wcsPixelPoint Return XY FITS coordinates * @param wcsImagePoint Return XY Image coordinates * @return True if conversion is successful, false otherwise. */ bool wcsToPixel(SkyPoint &wcsCoord, QPointF &wcsPixelPoint, QPointF &wcsImagePoint); /** * @brief pixelToWCS Convert Pixel coordinates to J2000 world coordinates * @param wcsPixelPoint Pixel coordinates in XY Image space. * @param wcsCoord Store back WCS world coordinate in wcsCoord * @return True if successful, false otherwise. */ bool pixelToWCS(const QPointF &wcsPixelPoint, SkyPoint &wcsCoord); /** * @brief createWCSFile Create a new FITS file given the WCS information supplied. Construct the necessary WCS keywords and save the * new file as the current active file * @param newWCSFile New file name * @param orientation Solver orientation, degrees E of N. * @param ra J2000 Right Ascension * @param dec J2000 Declination * @param pixscale Pixel scale in arcsecs per pixel * @return True if file is successfully created and saved, false otherwise. */ bool createWCSFile(const QString &newWCSFile, double orientation, double ra, double dec, double pixscale); // Debayer bool hasDebayer() { return HasDebayer; } bool debayer(); bool debayer_8bit(); bool debayer_16bit(); void getBayerParams(BayerParams *param); void setBayerParams(BayerParams *param); // Histogram #ifndef KSTARS_LITE void setHistogram(FITSHistogram *inHistogram) { histogram = inHistogram; } #endif // Filter void applyFilter(FITSScale type, uint8_t *image = nullptr, double *min = nullptr, double *max = nullptr); // Rotation counter. We keep count to rotate WCS keywords on save int getRotCounter() const; void setRotCounter(int value); // Filename const QString &filename() const { return m_Filename; } bool isTempFile() const {return m_isTemporary;} bool isCompressed() const {return m_isCompressed;} // Horizontal flip counter. We keep count to rotate WCS keywords on save int getFlipHCounter() const; void setFlipHCounter(int value); // Horizontal flip counter. We keep count to rotate WCS keywords on save int getFlipVCounter() const; void setFlipVCounter(int value); #ifndef KSTARS_LITE #ifdef HAVE_WCSLIB void findObjectsInImage(double world[], double phi, double theta, double imgcrd[], double pixcrd[], int stat[]); #endif #endif QList getSkyObjects(); QList objList; //Does this need to be public?? // Create autostretch image from FITS File static QImage FITSToImage(const QString &m_Filename); bool getAutoRemoveTemporaryFITS() const; void setAutoRemoveTemporaryFITS(bool value); QString getLastError() const; signals: void converted(QImage); private: bool privateLoad(bool silent); void rotWCSFITS(int angle, int mirror); bool checkCollision(Edge *s1, Edge *s2); int calculateMinMax(bool refresh = false); bool checkDebayer(); void readWCSKeys(); // FITS Record bool parseHeader(); //int getFITSRecord(QString &recordList, int &nkeys); // Templated functions template bool debayer(); template bool rotFITS(int rotate, int mirror); // Apply Filter template void applyFilter(FITSScale type, uint8_t *targetImage, double image_min, double image_max); // Star Detect - Centroid template int findCentroid(const QRect &boundary, int initStdDev, int minEdgeWidth); int findCentroid(const QRect &boundary = QRect(), int initStdDev = MINIMUM_STDVAR, int minEdgeWidth = MINIMUM_PIXEL_RANGE); // Star Detect - Threshold template int findOneStar(const QRect &boundary); template void calculateMinMax(); template QPair getParitionMinMax(uint32_t start, uint32_t stride); /* Calculate running average & standard deviation using Welford’s method for computing variance */ template void runningAverageStdDev(); template QPair getSquaredSumAndMean(uint32_t start, uint32_t stride); // Sobel detector by Gonzalo Exequiel Pedone template void sobel(QVector &gradient, QVector &direction); template void convertToQImage(double dataMin, double dataMax, double scale, double zero, QImage &image); // Give unique IDs to each contigous region int partition(int width, int height, QVector &gradient, QVector &ids); void trace(int width, int height, int id, QVector &image, QVector &ids, int x, int y); #if 0 QVector thinning(int width, int height, const QVector &gradient, const QVector &direction); QVector threshold(int thLow, int thHi, const QVector &image); QVector hysteresis(int width, int height, const QVector &image); #endif #ifndef KSTARS_LITE FITSHistogram *histogram { nullptr }; // Pointer to the FITS data histogram #endif /// Pointer to CFITSIO FITS file struct fitsfile *fptr { nullptr }; /// FITS image data type (TBYTE, TUSHORT, TINT, TFLOAT, TLONG, TDOUBLE) uint32_t m_DataType { 0 }; /// Number of channels uint8_t m_Channels { 1 }; /// Generic data image buffer uint8_t *m_ImageBuffer { nullptr }; /// Is this a tempoprary file or one loaded from disk? bool m_isTemporary { false }; /// is this file compress (.fits.fz)? bool m_isCompressed { false }; /// Did we search for stars yet? bool starsSearched { false }; ///Star Selection Algorithm StarAlgorithm starAlgorithm { ALGORITHM_GRADIENT }; /// Do we have WCS keywords in this FITS data? bool HasWCS { false }; /// Is the image debayarable? bool HasDebayer { false }; /// Is WCS data loaded? bool WCSLoaded { false }; /// Do we need to mark stars for the user? bool markStars { false }; - /// Our very own file name QString m_Filename; /// FITS Mode (Normal, WCS, Guide, Focus..etc) FITSMode m_Mode; /// How many times the image was rotated? Useful for WCS keywords rotation on save. int rotCounter { 0 }; /// How many times the image was flipped horizontally? int flipHCounter { 0 }; /// How many times the image was flipped vertically? int flipVCounter { 0 }; /// Pointer to WCS coordinate data, if any. wcs_point *wcs_coord { nullptr }; /// WCS Struct struct wcsprm *wcs { nullptr }; /// All the stars we detected, if any. QList starCenters; QList localStarCenters; /// The biggest fattest star in the image. Edge *maxHFRStar { nullptr }; uint8_t *bayerBuffer { nullptr }; /// Bayer parameters BayerParams debayerParams; - /// Stats struct to hold statisical data about the FITS data - struct - { - double min[3] = {0}, max[3] = {0}; - double mean[3] = {0}; - double stddev[3] = {0}; - double median[3] = {0}; - double SNR { 0 }; - int bitpix { 8 }; - int bytesPerPixel { 1 }; - int ndim { 2 }; - int64_t size { 0 }; - uint32_t samples_per_channel { 0 }; - uint16_t width { 0 }; - uint16_t height { 0 }; - } stats; + Statistic stats; // A list of header records QList records; /// Remove temproray files after closing bool autoRemoveTemporaryFITS { true }; QString lastError; static const QString m_TemporaryPath; }; diff --git a/kstars/fitsviewer/fitsdebayer.cpp b/kstars/fitsviewer/fitsdebayer.cpp index d25a493a6..85e0c2f46 100644 --- a/kstars/fitsviewer/fitsdebayer.cpp +++ b/kstars/fitsviewer/fitsdebayer.cpp @@ -1,76 +1,79 @@ /* FITS Debayer class Copyright (C) 2015 Jasem Mutlaq (mutlaqja@ikarustech.com) This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "fitsdebayer.h" #include "fitsdata.h" #include "fitsview.h" #include "fitsviewer.h" +#include + debayerUI::debayerUI(QDialog *parent) : QDialog(parent) { setupUi(parent); setModal(false); } FITSDebayer::FITSDebayer(FITSViewer *parent) : QDialog(parent) { ui = new debayerUI(this); viewer = parent; - connect(ui->buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(applyDebayer())); + connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, + this, &FITSDebayer::applyDebayer); } void FITSDebayer::applyDebayer() { FITSView *view = viewer->getCurrentView(); if (view) { FITSData *image_data = view->getImageData(); dc1394bayer_method_t method = (dc1394bayer_method_t)ui->methodCombo->currentIndex(); dc1394color_filter_t filter = (dc1394color_filter_t)(ui->filterCombo->currentIndex() + 512); int offsetX = ui->XOffsetSpin->value(); int offsetY = ui->YOffsetSpin->value(); BayerParams param; param.method = method; param.filter = filter; param.offsetX = offsetX; param.offsetY = offsetY; image_data->setBayerParams(¶m); ui->statusEdit->setText(i18n("Processing...")); qApp->processEvents(); if (image_data->debayer()) { ui->statusEdit->setText(i18n("Complete.")); view->rescale(ZOOM_KEEP_LEVEL); view->updateFrame(); } else ui->statusEdit->setText(i18n("Debayer failed.")); } } void FITSDebayer::setBayerParams(BayerParams *param) { ui->methodCombo->setCurrentIndex(param->method); ui->filterCombo->setCurrentIndex(param->filter - 512); ui->XOffsetSpin->setValue(param->offsetX); ui->YOffsetSpin->setValue(param->offsetY); } diff --git a/kstars/fitsviewer/fitshistogram.cpp b/kstars/fitsviewer/fitshistogram.cpp index 2e49b0850..0e71efe6d 100644 --- a/kstars/fitsviewer/fitshistogram.cpp +++ b/kstars/fitsviewer/fitshistogram.cpp @@ -1,829 +1,799 @@ /* FITS Histogram Copyright (C) 2015 Jasem Mutlaq (mutlaqja@ikarustech.com) This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "fitshistogram.h" #include "fits_debug.h" #include "fitsdata.h" #include "fitstab.h" #include "fitsview.h" #include "fitsviewer.h" #include "Options.h" #include #include histogramUI::histogramUI(QDialog *parent) : QDialog(parent) { setupUi(parent); setModal(false); } FITSHistogram::FITSHistogram(QWidget *parent) : QDialog(parent) { ui = new histogramUI(this); tab = dynamic_cast(parent); customPlot = ui->histogramPlot; customPlot->setBackground(QBrush(Qt::black)); customPlot->xAxis->setBasePen(QPen(Qt::white, 1)); customPlot->yAxis->setBasePen(QPen(Qt::white, 1)); customPlot->xAxis->setTickPen(QPen(Qt::white, 1)); customPlot->yAxis->setTickPen(QPen(Qt::white, 1)); customPlot->xAxis->setSubTickPen(QPen(Qt::white, 1)); customPlot->yAxis->setSubTickPen(QPen(Qt::white, 1)); customPlot->xAxis->setTickLabelColor(Qt::white); customPlot->yAxis->setTickLabelColor(Qt::white); customPlot->xAxis->setLabelColor(Qt::white); customPlot->yAxis->setLabelColor(Qt::white); customPlot->xAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); customPlot->yAxis->grid()->setPen(QPen(QColor(140, 140, 140), 1, Qt::DotLine)); customPlot->xAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); customPlot->yAxis->grid()->setSubGridPen(QPen(QColor(80, 80, 80), 1, Qt::DotLine)); customPlot->xAxis->grid()->setZeroLinePen(Qt::NoPen); customPlot->yAxis->grid()->setZeroLinePen(Qt::NoPen); r_graph = customPlot->addGraph(); r_graph->setBrush(QBrush(QColor(170, 40, 80))); r_graph->setPen(QPen(Qt::red)); //r_graph->setLineStyle(QCPGraph::lsImpulse); connect(ui->buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(applyScale())); connect(customPlot, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(updateValues(QMouseEvent*))); connect(ui->minEdit, SIGNAL(valueChanged(double)), this, SLOT(updateLimits(double))); connect(ui->maxEdit, SIGNAL(valueChanged(double)), this, SLOT(updateLimits(double))); connect(customPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), this, SLOT(checkRangeLimit(QCPRange))); } void FITSHistogram::showEvent(QShowEvent *event) { Q_UNUSED(event) syncGUI(); } void FITSHistogram::constructHistogram() { FITSData *image_data = tab->getView()->getImageData(); isGUISynced = false; switch (image_data->property("dataType").toInt()) { case TBYTE: constructHistogram(); break; case TSHORT: constructHistogram(); break; case TUSHORT: constructHistogram(); break; case TLONG: constructHistogram(); break; case TULONG: constructHistogram(); break; case TFLOAT: constructHistogram(); break; case TLONGLONG: constructHistogram(); break; case TDOUBLE: constructHistogram(); break; default: break; } if (isVisible()) syncGUI(); } template void FITSHistogram::constructHistogram() { FITSData *image_data = tab->getView()->getImageData(); uint16_t fits_w = image_data->width(), fits_h = image_data->height(); auto *buffer = reinterpret_cast(image_data->getImageBuffer()); image_data->getMinMax(&fits_min, &fits_max); uint32_t samples = fits_w * fits_h; binCount = sqrt(samples); intensity.fill(0, binCount); r_frequency.fill(0, binCount); cumulativeFrequency.fill(0, binCount); double pixel_range = fits_max - fits_min; binWidth = pixel_range / (binCount - 1); qCDebug(KSTARS_FITS) << "Histogram min:" << fits_min << ", max:" << fits_max << ", range:" << pixel_range << ", binW:" << binWidth << ", bin#:" << binCount; for (int i = 0; i < binCount; i++) intensity[i] = fits_min + (binWidth * i); uint16_t r_id = 0; if (image_data->channels() == 1) { for (uint32_t i = 0; i < samples; i += 4) { r_id = round((buffer[i] - fits_min) / binWidth); r_frequency[r_id >= binCount ? binCount - 1 : r_id] += 4; } } else { g_frequency.fill(0, binCount); b_frequency.fill(0, binCount); int g_offset = samples; int b_offset = samples * 2; for (uint32_t i = 0; i < samples; i += 4) { uint16_t g_id = 0, b_id = 0; r_id = round((buffer[i] - fits_min) / binWidth); r_frequency[r_id >= binCount ? binCount - 1 : r_id] += 4; g_id = round((buffer[i + g_offset] - fits_min) / binWidth); g_frequency[g_id >= binCount ? binCount - 1 : g_id] += 4; b_id = round((buffer[i + b_offset] - fits_min) / binWidth); b_frequency[b_id >= binCount ? binCount - 1 : b_id] += 4; } } // Cumulative Frequency int j = 0; double val = 0; for (int i = 0; i < binCount; i++) { val += r_frequency[j++]; cumulativeFrequency.replace(i, val); } if (image_data->channels() == 1) { for (int i = 0; i < binCount; i++) { if (r_frequency[i] > maxFrequency) maxFrequency = r_frequency[i]; } } else { for (int i = 0; i < binCount; i++) { if (r_frequency[i] > maxFrequency) maxFrequency = r_frequency[i]; if (g_frequency[i] > maxFrequency) maxFrequency = g_frequency[i]; if (b_frequency[i] > maxFrequency) maxFrequency = b_frequency[i]; } } double median = 0; int halfCumulative = cumulativeFrequency[binCount - 1] / 2; for (int i = 0; i < binCount; i++) { if (cumulativeFrequency[i] >= halfCumulative) { median = i * binWidth + fits_min; break; } } // Custom index to indicate the overall constrast of the image JMIndex = cumulativeFrequency[binCount / 8] / cumulativeFrequency[binCount / 4]; qCDebug(KSTARS_FITS) << "FITHistogram: JMIndex " << JMIndex; image_data->setMedian(median); } void FITSHistogram::syncGUI() { if (isGUISynced) return; FITSData *image_data = tab->getView()->getImageData(); ui->meanEdit->setText(QString::number(image_data->getMean())); ui->medianEdit->setText(QString::number(image_data->getMedian())); ui->minEdit->setMinimum(fits_min); ui->minEdit->setMaximum(fits_max - 1); ui->minEdit->setSingleStep(fabs(fits_max - fits_min) / 20.0); ui->minEdit->setValue(fits_min); ui->maxEdit->setMinimum(fits_min + 1); ui->maxEdit->setMaximum(fits_max); ui->maxEdit->setSingleStep(fabs(fits_max - fits_min) / 20.0); ui->maxEdit->setValue(fits_max); r_graph->setData(intensity, r_frequency); if (image_data->channels() > 1) { g_graph = customPlot->addGraph(); b_graph = customPlot->addGraph(); g_graph->setBrush(QBrush(QColor(40, 170, 80))); b_graph->setBrush(QBrush(QColor(80, 40, 170))); g_graph->setPen(QPen(Qt::green)); b_graph->setPen(QPen(Qt::blue)); g_graph->setData(intensity, g_frequency); b_graph->setData(intensity, b_frequency); } customPlot->axisRect(0)->setRangeDrag(Qt::Horizontal); customPlot->axisRect(0)->setRangeZoom(Qt::Horizontal); customPlot->xAxis->setLabel(i18n("Intensity")); customPlot->yAxis->setLabel(i18n("Frequency")); customPlot->xAxis->setRange(fits_min, fits_max); if (maxFrequency > 0) customPlot->yAxis->setRange(0, maxFrequency); customPlot->setInteraction(QCP::iRangeDrag, true); customPlot->setInteraction(QCP::iRangeZoom, true); customPlot->setInteraction(QCP::iSelectPlottables, true); customPlot->replot(); isGUISynced = true; } #if 0 template void FITSHistogram::constructHistogram() { uint16_t fits_w = 0, fits_h = 0; FITSData *image_data = tab->getView()->getImageData(); T *buffer = reinterpret_cast(image_data->getImageBuffer()); image_data->getDimensions(&fits_w, &fits_h); image_data->getMinMax(&fits_min, &fits_max); uint32_t samples = fits_w * fits_h; binCount = sqrt(samples); intensity.fill(0, binCount); r_frequency.fill(0, binCount); cumulativeFrequency.fill(0, binCount); double pixel_range = fits_max - fits_min; binWidth = pixel_range / (binCount - 1); qCDebug(KSTARS_FITS) << "Histogram min:" << fits_min << ", max:" << fits_max << ", range:" << pixel_range << ", binW:" << binWidth << ", bin#:" << binCount; for (int i = 0; i < binCount; i++) intensity[i] = fits_min + (binWidth * i); uint16_t r_id = 0; if (image_data->getNumOfChannels() == 1) { for (uint32_t i = 0; i < samples; i += 4) { r_id = round((buffer[i] - fits_min) / binWidth); r_frequency[r_id >= binCount ? binCount - 1 : r_id] += 4; } } else { g_frequency.fill(0, binCount); b_frequency.fill(0, binCount); int g_offset = samples; int b_offset = samples * 2; for (uint32_t i = 0; i < samples; i += 4) { uint16_t g_id = 0, b_id = 0; r_id = round((buffer[i] - fits_min) / binWidth); r_frequency[r_id >= binCount ? binCount - 1 : r_id] += 4; g_id = round((buffer[i + g_offset] - fits_min) / binWidth); g_frequency[g_id >= binCount ? binCount - 1 : g_id] += 4; b_id = round((buffer[i + b_offset] - fits_min) / binWidth); b_frequency[b_id >= binCount ? binCount - 1 : b_id] += 4; } } // Cumulative Frequency for (int i = 0; i < binCount; i++) for (int j = 0; j <= i; j++) cumulativeFrequency[i] += r_frequency[j]; int maxFrequency = 0; if (image_data->getNumOfChannels() == 1) { for (int i = 0; i < binCount; i++) { if (r_frequency[i] > maxFrequency) maxFrequency = r_frequency[i]; } } else { for (int i = 0; i < binCount; i++) { if (r_frequency[i] > maxFrequency) maxFrequency = r_frequency[i]; if (g_frequency[i] > maxFrequency) maxFrequency = g_frequency[i]; if (b_frequency[i] > maxFrequency) maxFrequency = b_frequency[i]; } } double median = 0; int halfCumulative = cumulativeFrequency[binCount - 1] / 2; for (int i = 0; i < binCount; i++) { if (cumulativeFrequency[i] >= halfCumulative) { median = i * binWidth + fits_min; break; } } // Custom index to indicate the overall constrast of the image JMIndex = cumulativeFrequency[binCount / 8] / cumulativeFrequency[binCount / 4]; qCDebug(KSTARS_FITS) << "FITHistogram: JMIndex " << JMIndex; image_data->setMedian(median); ui->meanEdit->setText(QString::number(image_data->getMean())); ui->medianEdit->setText(QString::number(median)); ui->minEdit->setMinimum(fits_min); ui->minEdit->setMaximum(fits_max - 1); ui->minEdit->setSingleStep(fabs(fits_max - fits_min) / 20.0); ui->minEdit->setValue(fits_min); ui->maxEdit->setMinimum(fits_min + 1); ui->maxEdit->setMaximum(fits_max); ui->maxEdit->setSingleStep(fabs(fits_max - fits_min) / 20.0); ui->maxEdit->setValue(fits_max); r_graph->setData(intensity, r_frequency); if (image_data->getNumOfChannels() > 1) { g_graph = customPlot->addGraph(); b_graph = customPlot->addGraph(); g_graph->setBrush(QBrush(QColor(40, 170, 80))); b_graph->setBrush(QBrush(QColor(80, 40, 170))); g_graph->setPen(QPen(Qt::green)); b_graph->setPen(QPen(Qt::blue)); g_graph->setData(intensity, g_frequency); b_graph->setData(intensity, b_frequency); } customPlot->axisRect(0)->setRangeDrag(Qt::Horizontal); customPlot->axisRect(0)->setRangeZoom(Qt::Horizontal); customPlot->xAxis->setLabel(i18n("Intensity")); customPlot->yAxis->setLabel(i18n("Frequency")); customPlot->xAxis->setRange(fits_min, fits_max); if (maxFrequency > 0) customPlot->yAxis->setRange(0, maxFrequency); customPlot->setInteraction(QCP::iRangeDrag, true); customPlot->setInteraction(QCP::iRangeZoom, true); customPlot->setInteraction(QCP::iSelectPlottables, true); customPlot->replot(); } #endif void FITSHistogram::updateLimits(double value) { if (sender() == ui->minEdit) { if (value > ui->maxEdit->value()) ui->maxEdit->setValue(value + 1); } else if (sender() == ui->maxEdit) { if (value < ui->minEdit->value()) { ui->minEdit->setValue(value); ui->maxEdit->setValue(value + 1); } } } void FITSHistogram::checkRangeLimit(const QCPRange &range) { if (range.lower < fits_min) customPlot->xAxis->setRangeLower(fits_min); else if (range.upper > fits_max) customPlot->xAxis->setRangeUpper(fits_max); } double FITSHistogram::getJMIndex() const { return JMIndex; } void FITSHistogram::applyScale() { double min = ui->minEdit->value(); double max = ui->maxEdit->value(); FITSHistogramCommand *histC; if (ui->logR->isChecked()) type = FITS_LOG; else type = FITS_LINEAR; histC = new FITSHistogramCommand(tab, this, type, min, max); tab->getUndoStack()->push(histC); } void FITSHistogram::applyFilter(FITSScale ftype) { double min = ui->minEdit->value(); double max = ui->maxEdit->value(); FITSHistogramCommand *histC; type = ftype; histC = new FITSHistogramCommand(tab, this, type, min, max); tab->getUndoStack()->push(histC); } QVector FITSHistogram::getCumulativeFrequency() const { return cumulativeFrequency; } void FITSHistogram::updateValues(QMouseEvent *event) { int x = event->x(); double intensity_key = customPlot->xAxis->pixelToCoord(x); if (intensity_key < 0) return; double frequency_val = 0; for (int i = 0; i < binCount; i++) { if (intensity[i] > intensity_key) { frequency_val = r_frequency[i]; break; } } ui->intensityEdit->setText(QString::number(intensity_key)); ui->frequencyEdit->setText(QString::number(frequency_val)); } FITSHistogramCommand::FITSHistogramCommand(QWidget *parent, FITSHistogram *inHisto, FITSScale newType, double lmin, double lmax) { tab = dynamic_cast(parent); type = newType; histogram = inHisto; min = lmin; max = lmax; } FITSHistogramCommand::~FITSHistogramCommand() { delete[] delta; } bool FITSHistogramCommand::calculateDelta(const uint8_t *buffer) { FITSData *image_data = tab->getView()->getImageData(); uint8_t *image_buffer = image_data->getImageBuffer(); int totalPixels = image_data->width() * image_data->height() * image_data->channels(); unsigned long totalBytes = totalPixels * image_data->getBytesPerPixel(); auto *raw_delta = new uint8_t[totalBytes]; if (raw_delta == nullptr) { qWarning() << "Error! not enough memory to create image delta" << endl; return false; } for (unsigned int i = 0; i < totalBytes; i++) raw_delta[i] = buffer[i] ^ image_buffer[i]; compressedBytes = sizeof(uint8_t) * totalBytes + totalBytes / 64 + 16 + 3; delete[] delta; delta = new uint8_t[compressedBytes]; if (delta == nullptr) { delete[] raw_delta; qCCritical(KSTARS_FITS) << "FITSHistogram Error: Ran out of memory compressing delta"; return false; } int r = compress2(delta, &compressedBytes, raw_delta, totalBytes, 5); if (r != Z_OK) { delete[] raw_delta; /* this should NEVER happen */ qCCritical(KSTARS_FITS) << "FITSHistogram Error: Failed to compress raw_delta"; return false; } //qDebug() << "compressed bytes size " << compressedBytes << " bytes" << endl; delete[] raw_delta; return true; } bool FITSHistogramCommand::reverseDelta() { FITSView *image = tab->getView(); FITSData *image_data = image->getImageData(); uint8_t *image_buffer = (image_data->getImageBuffer()); int totalPixels = image_data->width() * image_data->height() * image_data->channels(); unsigned long totalBytes = totalPixels * image_data->getBytesPerPixel(); auto *output_image = new uint8_t[totalBytes]; if (output_image == nullptr) { qWarning() << "Error! not enough memory to create output image" << endl; return false; } auto *raw_delta = new uint8_t[totalBytes]; if (raw_delta == nullptr) { delete[] output_image; qWarning() << "Error! not enough memory to create image delta" << endl; return false; } int r = uncompress(raw_delta, &totalBytes, delta, compressedBytes); if (r != Z_OK) { qCCritical(KSTARS_FITS) << "FITSHistogram compression error in reverseDelta()"; delete[] output_image; delete[] raw_delta; return false; } for (unsigned int i = 0; i < totalBytes; i++) output_image[i] = raw_delta[i] ^ image_buffer[i]; image_data->setImageBuffer(output_image); delete[] raw_delta; return true; } void FITSHistogramCommand::redo() { FITSView *image = tab->getView(); FITSData *image_data = image->getImageData(); uint8_t *image_buffer = image_data->getImageBuffer(); + uint8_t *buffer = nullptr; unsigned int size = image_data->width() * image_data->height() * image_data->channels(); int BBP = image_data->getBytesPerPixel(); QApplication::setOverrideCursor(Qt::WaitCursor); if (delta != nullptr) - { - double min, max, stddev, average, median, snr; - min = image_data->getMin(); - max = image_data->getMax(); - stddev = image_data->getStdDev(); - average = image_data->getMean(); - median = image_data->getMedian(); - snr = image_data->getSNR(); + { + FITSData::Statistic prevStats; + image_data->saveStatistics(prevStats); reverseDelta(); - restoreStats(); + image_data->restoreStatistics(stats); - saveStats(min, max, stddev, average, median, snr); + stats = prevStats; } else { - saveStats(image_data->getMin(), image_data->getMax(), image_data->getStdDev(), image_data->getMean(), - image_data->getMedian(), image_data->getSNR()); + image_data->saveStatistics(stats); // If it's rotation of flip, no need to calculate delta if (type >= FITS_ROTATE_CW && type <= FITS_FLIP_V) { image_data->applyFilter(type, image_buffer); } else { - auto *buffer = new uint8_t[size * BBP]; + buffer = new uint8_t[size * BBP]; if (buffer == nullptr) { qWarning() << "Error! not enough memory to create image buffer in redo()" << endl; QApplication::restoreOverrideCursor(); return; } memcpy(buffer, image_buffer, size * BBP); - double dataMin = min, dataMax = max; + double dataMin = min, dataMax = max; switch (type) { case FITS_AUTO: case FITS_LINEAR: image_data->applyFilter(FITS_LINEAR, nullptr, &dataMin, &dataMax); break; case FITS_LOG: image_data->applyFilter(FITS_LOG, nullptr, &dataMin, &dataMax); break; case FITS_SQRT: image_data->applyFilter(FITS_SQRT, nullptr, &dataMin, &dataMax); break; default: image_data->applyFilter(type); break; } calculateDelta(buffer); delete[] buffer; } } if (histogram != nullptr) { histogram->constructHistogram(); if (tab->getViewer()->isStarsMarked()) image_data->findStars(); } image->pushFilter(type); image->rescale(ZOOM_KEEP_LEVEL); image->updateFrame(); QApplication::restoreOverrideCursor(); } void FITSHistogramCommand::undo() { FITSView *image = tab->getView(); FITSData *image_data = image->getImageData(); QApplication::setOverrideCursor(Qt::WaitCursor); if (delta != nullptr) { - double min, max, stddev, average, median, snr; - min = image_data->getMin(); - max = image_data->getMax(); - stddev = image_data->getStdDev(); - average = image_data->getMean(); - median = image_data->getMedian(); - snr = image_data->getSNR(); + FITSData::Statistic prevStats; + image_data->saveStatistics(prevStats); reverseDelta(); - restoreStats(); + image_data->restoreStatistics(stats); - saveStats(min, max, stddev, average, median, snr); + stats = prevStats; } else { switch (type) { case FITS_ROTATE_CW: image_data->applyFilter(FITS_ROTATE_CCW); break; case FITS_ROTATE_CCW: image_data->applyFilter(FITS_ROTATE_CW); break; case FITS_FLIP_H: case FITS_FLIP_V: image_data->applyFilter(type); break; default: break; } } if (histogram != nullptr) { histogram->constructHistogram(); if (tab->getViewer()->isStarsMarked()) image_data->findStars(); } image->popFilter(); image->rescale(ZOOM_KEEP_LEVEL); image->updateFrame(); QApplication::restoreOverrideCursor(); } QString FITSHistogramCommand::text() const { switch (type) { case FITS_AUTO: return i18n("Auto Scale"); break; case FITS_LINEAR: return i18n("Linear Scale"); break; case FITS_LOG: return i18n("Logarithmic Scale"); break; case FITS_SQRT: return i18n("Square Root Scale"); break; default: if (type - 1 <= FITSViewer::filterTypes.count()) return FITSViewer::filterTypes.at(type - 1); break; } return i18n("Unknown"); } - -void FITSHistogramCommand::saveStats(double min, double max, double stddev, double mean, double median, double SNR) -{ - stats.min = min; - stats.max = max; - stats.stddev = stddev; - stats.mean = mean; - stats.median = median; - stats.SNR = SNR; -} - -void FITSHistogramCommand::restoreStats() -{ - FITSData *image_data = tab->getView()->getImageData(); - - image_data->setMinMax(stats.min, stats.max); - image_data->setStdDev(stats.stddev); - image_data->setMean(stats.mean); - image_data->setMedian(stats.median); -} diff --git a/kstars/fitsviewer/fitshistogram.h b/kstars/fitsviewer/fitshistogram.h index 401898154..e4791d2bb 100644 --- a/kstars/fitsviewer/fitshistogram.h +++ b/kstars/fitsviewer/fitshistogram.h @@ -1,125 +1,110 @@ /* FITS Histogram Copyright (C) 2015 Jasem Mutlaq (mutlaqja@ikarustech.com) This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "fitscommon.h" +#include "fitsdata.h" #include "ui_fitshistogramui.h" #include #include class QMouseEvent; class FITSTab; class histogramUI : public QDialog, public Ui::FITSHistogramUI { Q_OBJECT public: - explicit histogramUI(QDialog *parent = 0); + explicit histogramUI(QDialog *parent = nullptr); }; class FITSHistogram : public QDialog { Q_OBJECT friend class histDrawArea; public: explicit FITSHistogram(QWidget *parent); ~FITSHistogram() = default; void constructHistogram(); void syncGUI(); void applyFilter(FITSScale ftype); double getBinWidth() { return binWidth; } QVector getCumulativeFrequency() const; double getJMIndex() const; protected: void showEvent(QShowEvent *event); public slots: void applyScale(); void updateValues(QMouseEvent *event); void updateLimits(double value); void checkRangeLimit(const QCPRange &range); private: template void constructHistogram(); histogramUI *ui { nullptr }; FITSTab *tab { nullptr }; QVector intensity; QVector r_frequency, g_frequency, b_frequency; QCPGraph *r_graph { nullptr }; QCPGraph *g_graph { nullptr }; QCPGraph *b_graph { nullptr }; QVector cumulativeFrequency; double binWidth { 0 }; double JMIndex { 0 }; double fits_min { 0 }; double fits_max { 0 }; uint16_t binCount { 0 }; int maxFrequency {0}; FITSScale type { FITS_AUTO }; bool isGUISynced { false}; QCustomPlot *customPlot { nullptr }; }; class FITSHistogramCommand : public QUndoCommand { public: FITSHistogramCommand(QWidget *parent, FITSHistogram *inHisto, FITSScale newType, double lmin, double lmax); virtual ~FITSHistogramCommand(); virtual void redo(); virtual void undo(); virtual QString text() const; - private: - /* stats struct to hold statistical data about the FITS data */ - struct - { - double min { 0 }; - double max { 0 }; - double mean { 0 }; - double stddev { 0 }; - double median { 0 }; - double SNR { 0 }; - int bitpix { 0 }; - int ndim { 0 }; - unsigned int size { 0 }; - long dim[2]; - } stats; - + private: bool calculateDelta(const uint8_t *buffer); bool reverseDelta(); - void saveStats(double min, double max, double stddev, double mean, double median, double SNR); - void restoreStats(); + FITSData::Statistic stats; FITSHistogram *histogram { nullptr }; FITSScale type; double min { 0 }; double max { 0 }; unsigned char *delta { nullptr }; unsigned long compressedBytes { 0 }; FITSTab *tab { nullptr }; }; diff --git a/kstars/fitsviewer/fitslabel.cpp b/kstars/fitsviewer/fitslabel.cpp index 7e9247a82..a56ad1c93 100644 --- a/kstars/fitsviewer/fitslabel.cpp +++ b/kstars/fitsviewer/fitslabel.cpp @@ -1,366 +1,367 @@ /* FITS Label Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "fitslabel.h" #include "config-kstars.h" #include "fitsdata.h" #include "fitsview.h" #include "kspopupmenu.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "skymap.h" #ifdef HAVE_INDI #include "basedevice.h" #include "indi/indilistener.h" #endif #include +#include #define BASE_OFFSET 50 #define ZOOM_DEFAULT 100.0 #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 FITSLabel::FITSLabel(FITSView *view, QWidget *parent) : QLabel(parent) { this->view = view; } void FITSLabel::setSize(double w, double h) { width = w; height = h; size = w * h; } bool FITSLabel::getMouseButtonDown() { return mouseButtonDown; } /** This method was added to make the panning function work. If the mouse button is released, it resets mouseButtonDown variable and the mouse cursor. */ void FITSLabel::mouseReleaseEvent(QMouseEvent *e) { Q_UNUSED(e); if (view->getCursorMode() == FITSView::dragCursor) { mouseButtonDown = false; view->updateMouseCursor(); } } /** I added some things to the top of this method to allow panning and Scope slewing to function. If you are in the dragMouse mode and the mousebutton is pressed, The method checks the difference between the location of the last point stored and the current event point to see how the mouse has moved. Then it moves the scrollbars and thus the view to the right location. Then it stores the current point so next time it can do it again. */ void FITSLabel::mouseMoveEvent(QMouseEvent *e) { float scale = (view->getCurrentZoom() / ZOOM_DEFAULT); if (view->getCursorMode() == FITSView::dragCursor && mouseButtonDown) { QPoint newPoint = e->globalPos(); int dx = newPoint.x() - lastMousePoint.x(); int dy = newPoint.y() - lastMousePoint.y(); view->horizontalScrollBar()->setValue(view->horizontalScrollBar()->value() - dx); view->verticalScrollBar()->setValue(view->verticalScrollBar()->value() - dy); lastMousePoint = newPoint; } double x, y; FITSData *view_data = view->getImageData(); uint8_t *buffer = view_data->getImageBuffer(); if (buffer == nullptr) return; x = round(e->x() / scale); y = round(e->y() / scale); x = KSUtils::clamp(x, 1.0, width); y = KSUtils::clamp(y, 1.0, height); emit newStatus(QString("X:%1 Y:%2").arg((int)x).arg((int)y), FITS_POSITION); // Range is 0 to dim-1 when accessing array x -= 1; y -= 1; QString stringValue; switch (view_data->property("dataType").toInt()) { case TBYTE: stringValue = QLocale().toString(buffer[(int)(y * width + x)]); break; case TSHORT: stringValue = QLocale().toString((reinterpret_cast(buffer))[(int)(y * width + x)]); break; case TUSHORT: stringValue = QLocale().toString((reinterpret_cast(buffer))[(int)(y * width + x)]); break; case TLONG: stringValue = QLocale().toString((reinterpret_cast(buffer))[(int)(y * width + x)]); break; case TULONG: stringValue = QLocale().toString((reinterpret_cast(buffer))[(int)(y * width + x)]); break; case TFLOAT: stringValue = QLocale().toString((reinterpret_cast(buffer))[(int)(y * width + x)], 'f', 5); break; case TLONGLONG: stringValue = QLocale().toString(static_cast((reinterpret_cast(buffer))[(int)(y * width + x)])); break; case TDOUBLE: stringValue = QLocale().toString((reinterpret_cast(buffer))[(int)(y * width + x)], 'f', 5); break; default: break; } emit newStatus(stringValue, FITS_VALUE); if (view_data->hasWCS() && view->getCursorMode() != FITSView::selectCursor) { int index = x + y * width; wcs_point *wcs_coord = view_data->getWCSCoord(); if (wcs_coord) { if (index > size) return; ra.setD(wcs_coord[index].ra); dec.setD(wcs_coord[index].dec); emit newStatus(QString("%1 , %2").arg(ra.toHMSString(), dec.toDMSString()), FITS_WCS); } bool objFound = false; foreach (FITSSkyObject *listObject, view_data->objList) { if ((std::abs(listObject->x() - x) < 5 / scale) && (std::abs(listObject->y() - y) < 5 / scale)) { QToolTip::showText(e->globalPos(), listObject->skyObject()->name() + '\n' + listObject->skyObject()->longname(), this); objFound = true; break; } } if (!objFound) QToolTip::hideText(); } double HFR = view->getImageData()->getHFR(x, y); if (HFR > 0) QToolTip::showText(e->globalPos(), i18nc("Half Flux Radius", "HFR: %1", QString::number(HFR, 'g', 3)), this); //setCursor(Qt::CrossCursor); e->accept(); } /** I added some things to the top of this method to allow panning and Scope slewing to function. If in dragMouse mode, the Panning function works by storing the cursor position when the mouse was pressed and setting the mouseButtonDown variable to true. If in ScopeMouse mode and the mouse is clicked, if there is WCS data and a scope is available, the method will verify that you actually do want to slew to the WCS coordinates associated with the click location. If so, it calls the centerTelescope function. */ void FITSLabel::mousePressEvent(QMouseEvent *e) { float scale = (view->getCurrentZoom() / ZOOM_DEFAULT); if (view->getCursorMode() == FITSView::dragCursor) { mouseButtonDown = true; lastMousePoint = e->globalPos(); view->updateMouseCursor(); } else if (e->buttons() & Qt::LeftButton && view->getCursorMode() == FITSView::scopeCursor) { #ifdef HAVE_INDI FITSData *view_data = view->getImageData(); if (view_data->hasWCS()) { wcs_point *wcs_coord = view_data->getWCSCoord(); if (wcs_coord) { double x, y; x = round(e->x() / scale); y = round(e->y() / scale); x = KSUtils::clamp(x, 1.0, width); y = KSUtils::clamp(y, 1.0, height); int index = x + y * width; if (KMessageBox::Continue == KMessageBox::warningContinueCancel( nullptr, "Slewing to Coordinates: \nRA: " + dms(wcs_coord[index].ra).toHMSString() + "\nDec: " + dms(wcs_coord[index].dec).toDMSString(), i18n("Continue Slew"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "continue_slew_warning")) { centerTelescope(wcs_coord[index].ra / 15.0, wcs_coord[index].dec); view->setCursorMode(view->lastMouseMode); view->updateScopeButton(); } } } #endif } double x, y; x = round(e->x() / scale); y = round(e->y() / scale); x = KSUtils::clamp(x, 1.0, width); y = KSUtils::clamp(y, 1.0, height); #ifdef HAVE_INDI FITSData *view_data = view->getImageData(); if (e->buttons() & Qt::RightButton && view->getCursorMode() != FITSView::scopeCursor) { mouseReleaseEvent(e); if (view_data->hasWCS()) { foreach (FITSSkyObject *listObject, view_data->objList) { if ((std::abs(listObject->x() - x) < 5 / scale) && (std::abs(listObject->y() - y) < 5 / scale)) { SkyObject *object = listObject->skyObject(); KSPopupMenu *pmenu; pmenu = new KSPopupMenu(); object->initPopupMenu(pmenu); QList actions = pmenu->actions(); foreach (QAction *action, actions) { if (action->text().left(7) == "Starhop") pmenu->removeAction(action); if (action->text().left(7) == "Angular") pmenu->removeAction(action); if (action->text().left(8) == "Add flag") pmenu->removeAction(action); if (action->text().left(12) == "Attach Label") pmenu->removeAction(action); } pmenu->popup(e->globalPos()); KStars::Instance()->map()->setClickedObject(object); break; } } } if (fabs(view->markerCrosshair.x() - x) <= 15 && fabs(view->markerCrosshair.y() - y) <= 15) emit markerSelected(0, 0); } #endif if (e->buttons() & Qt::LeftButton) { if (view->getCursorMode() == FITSView::selectCursor) emit pointSelected(x, y); else if (view->getCursorMode() == FITSView::crosshairCursor) emit pointSelected(x+5/scale, y+5/scale); } } void FITSLabel::mouseDoubleClickEvent(QMouseEvent *e) { double x, y; x = round(e->x() / (view->getCurrentZoom() / ZOOM_DEFAULT)); y = round(e->y() / (view->getCurrentZoom() / ZOOM_DEFAULT)); x = KSUtils::clamp(x, 1.0, width); y = KSUtils::clamp(y, 1.0, height); emit markerSelected(x, y); return; } void FITSLabel::centerTelescope(double raJ2000, double decJ2000) { #ifdef HAVE_INDI if (INDIListener::Instance()->size() == 0) { KMessageBox::sorry(nullptr, i18n("KStars did not find any active telescopes.")); return; } foreach (ISD::GDInterface *gd, INDIListener::Instance()->getDevices()) { INDI::BaseDevice *bd = gd->getBaseDevice(); if (gd->getType() != KSTARS_TELESCOPE) continue; if (bd == nullptr) continue; if (bd->isConnected() == false) { KMessageBox::error(nullptr, i18n("Telescope %1 is offline. Please connect and retry again.", gd->getDeviceName())); return; } ISD::GDSetCommand SlewCMD(INDI_SWITCH, "ON_COORD_SET", "TRACK", ISS_ON, this); SkyObject selectedObject; selectedObject.setRA0(raJ2000); selectedObject.setDec0(decJ2000); selectedObject.apparentCoord(J2000, KStarsData::Instance()->ut().djd()); gd->setProperty(&SlewCMD); gd->runCommand(INDI_SEND_COORDS, &selectedObject); return; } KMessageBox::sorry(nullptr, i18n("KStars did not find any active telescopes.")); #else Q_UNUSED(raJ2000); Q_UNUSED(decJ2000); #endif } diff --git a/kstars/fitsviewer/fitstab.cpp b/kstars/fitsviewer/fitstab.cpp index 7ece331da..c339e24e2 100644 --- a/kstars/fitsviewer/fitstab.cpp +++ b/kstars/fitsviewer/fitstab.cpp @@ -1,324 +1,324 @@ /*************************************************************************** FITS Tab ------------------- copyright : (C) 2012 by Jasem Mutlaq email : mutlaqja@ikarustech.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "fitstab.h" #include "fitsdata.h" #include "fitshistogram.h" #include "fitsview.h" #include "fitsviewer.h" #include "kstars.h" #include "Options.h" #include "ui_fitsheaderdialog.h" #include "ui_statform.h" #include #include FITSTab::FITSTab(FITSViewer *parent) : QWidget(parent) { viewer = parent; undoStack = new QUndoStack(this); undoStack->setUndoLimit(10); undoStack->clear(); connect(undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(modifyFITSState(bool))); } FITSTab::~FITSTab() { // Make sure it's done histogramFuture.waitForFinished(); //disconnect(); } void FITSTab::saveUnsaved() { if (undoStack->isClean() || view->getMode() != FITS_NORMAL) return; QString caption = i18n("Save Changes to FITS?"); QString message = i18n("The current FITS file has unsaved changes. Would you like to save before closing it?"); int ans = KMessageBox::warningYesNoCancel(nullptr, message, caption, KStandardGuiItem::save(), KStandardGuiItem::discard()); if (ans == KMessageBox::Yes) saveFile(); if (ans == KMessageBox::No) { undoStack->clear(); modifyFITSState(); } } void FITSTab::closeEvent(QCloseEvent *ev) { saveUnsaved(); if (undoStack->isClean()) ev->accept(); else ev->ignore(); } QString FITSTab::getPreviewText() const { return previewText; } void FITSTab::setPreviewText(const QString &value) { previewText = value; } void FITSTab::loadFITS(const QUrl &imageURL, FITSMode mode, FITSScale filter, bool silent) { if (view.get() == nullptr) { view.reset(new FITSView(this, mode, filter)); view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); QVBoxLayout *vlayout = new QVBoxLayout(); vlayout->addWidget(view.get()); setLayout(vlayout); connect(view.get(), &FITSView::newStatus, this, &FITSTab::newStatus); connect(view.get(), &FITSView::debayerToggled, this, &FITSTab::debayerToggled); // On Failure to load connect(view.get(), &FITSView::failed, this, &FITSTab::failed); // On Success loading image connect(view.get(), &FITSView::loaded, [&,filter]() { // If it was already running make sure it's done histogramFuture.waitForFinished(); FITSData *image_data = view->getImageData(); if (histogram == nullptr) { histogram = new FITSHistogram(this); image_data->setHistogram(histogram); } histogramFuture = QtConcurrent::run([&]() {histogram->constructHistogram();}); - if (filter != FITS_NONE) - { - image_data->applyFilter(filter); - view->rescale(ZOOM_KEEP_LEVEL); - } +// if (filter != FITS_NONE) +// { +// image_data->applyFilter(filter); +// view->rescale(ZOOM_KEEP_LEVEL); +// } if (viewer->isStarsMarked()) view->toggleStars(true); view->updateFrame(); emit loaded(); }); } currentURL = imageURL; view->setFilter(filter); view->loadFITS(imageURL.toLocalFile(), silent); } void FITSTab::modifyFITSState(bool clean) { if (clean) { if (undoStack->isClean() == false) undoStack->setClean(); mDirty = false; } else mDirty = true; emit changeStatus(clean); } int FITSTab::saveFITS(const QString &filename) { return view->saveFITS(filename); } void FITSTab::copyFITS() { QApplication::clipboard()->setImage(view->getDisplayImage()); } void FITSTab::histoFITS() { histogram->show(); } void FITSTab::statFITS() { QDialog statDialog; Ui::statForm stat; stat.setupUi(&statDialog); FITSData *image_data = view->getImageData(); stat.widthOUT->setText(QString::number(image_data->width())); stat.heightOUT->setText(QString::number(image_data->height())); stat.bitpixOUT->setText(QString::number(image_data->bpp())); stat.maxOUT->setText(QString::number(image_data->getMax(), 'f', 3)); stat.minOUT->setText(QString::number(image_data->getMin(), 'f', 3)); stat.meanOUT->setText(QString::number(image_data->getMean(), 'f', 3)); stat.stddevOUT->setText(QString::number(image_data->getStdDev(), 'f', 3)); stat.HFROUT->setText(QString::number(image_data->getHFR(), 'f', 3)); stat.medianOUT->setText(QString::number(image_data->getMedian(), 'f', 3)); stat.SNROUT->setText(QString::number(image_data->getSNR(), 'f', 3)); statDialog.exec(); } void FITSTab::headerFITS() { FITSData *image_data = view->getImageData(); QDialog fitsHeaderDialog; Ui::fitsHeaderDialog header; int nkeys = image_data->getRecords().size(); int counter=0; header.setupUi(&fitsHeaderDialog); header.tableWidget->setRowCount(nkeys); for (FITSData::Record *oneRecord : image_data->getRecords()) { QTableWidgetItem *tempItem = new QTableWidgetItem(oneRecord->key); tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); header.tableWidget->setItem(counter, 0, tempItem); tempItem = new QTableWidgetItem(oneRecord->value.toString()); tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); header.tableWidget->setItem(counter, 1, tempItem); tempItem = new QTableWidgetItem(oneRecord->comment); tempItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); header.tableWidget->setItem(counter, 2, tempItem); counter++; } header.tableWidget->resizeColumnsToContents(); fitsHeaderDialog.exec(); } bool FITSTab::saveFile() { QUrl backupCurrent = currentURL; QUrl currentDir(Options::fitsDir()); currentDir.setScheme("file"); if (currentURL.toLocalFile().startsWith(QLatin1String("/tmp/")) || currentURL.toLocalFile().contains("/Temp")) currentURL.clear(); // If no changes made, return. if (mDirty == false && !currentURL.isEmpty()) return false; if (currentURL.isEmpty()) { currentURL = QFileDialog::getSaveFileUrl(KStars::Instance(), i18n("Save FITS"), currentDir, "FITS (*.fits *.fits.gz *.fit)"); // if user presses cancel if (currentURL.isEmpty()) { currentURL = backupCurrent; return false; } if (currentURL.toLocalFile().contains('.') == 0) currentURL.setPath(currentURL.toLocalFile() + ".fits"); // Already display by dialog /*if (QFile::exists(currentURL.toLocalFile())) { int r = KMessageBox::warningContinueCancel(0, i18n( "A file named \"%1\" already exists. " "Overwrite it?", currentURL.fileName() ), i18n( "Overwrite File?" ), KGuiItem(i18n( "&Overwrite" )) ); if(r==KMessageBox::Cancel) return false; }*/ } if (currentURL.isValid()) { int err_status = 0; char err_text[FLEN_STATUS]; if ((err_status = saveFITS('!' + currentURL.toLocalFile())) != 0) { // -1000 = user canceled if (err_status == -1000) return false; fits_get_errstatus(err_status, err_text); // Use KMessageBox or something here KMessageBox::error(nullptr, i18n("FITS file save error: %1", QString::fromUtf8(err_text)), i18n("FITS Save")); return false; } //statusBar()->changeItem(i18n("File saved."), 3); emit newStatus(i18n("File saved to %1", currentURL.url()), FITS_MESSAGE); modifyFITSState(); return true; } else { QString message = i18n("Invalid URL: %1", currentURL.url()); KMessageBox::sorry(nullptr, message, i18n("Invalid URL")); return false; } } bool FITSTab::saveFileAs() { currentURL.clear(); return saveFile(); } void FITSTab::ZoomIn() { QPoint oldCenter = view->getImagePoint(view->viewport()->rect().center()); view->ZoomIn(); view->cleanUpZoom(oldCenter); } void FITSTab::ZoomOut() { QPoint oldCenter = view->getImagePoint(view->viewport()->rect().center()); view->ZoomOut(); view->cleanUpZoom(oldCenter); } void FITSTab::ZoomDefault() { QPoint oldCenter = view->getImagePoint(view->viewport()->rect().center()); view->ZoomDefault(); view->cleanUpZoom(oldCenter); } void FITSTab::tabPositionUpdated() { undoStack->setActive(true); emit newStatus(QString("%1%").arg(view->getCurrentZoom()), FITS_ZOOM); emit newStatus(QString("%1x%2").arg(view->getImageData()->width()).arg(view->getImageData()->height()), FITS_RESOLUTION); } diff --git a/kstars/fitsviewer/fitsview.cpp b/kstars/fitsviewer/fitsview.cpp index 5166a1b59..098060853 100644 --- a/kstars/fitsviewer/fitsview.cpp +++ b/kstars/fitsviewer/fitsview.cpp @@ -1,1831 +1,1862 @@ /* FITS View Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ -#include "fitsview.h" - #include "config-kstars.h" +#include "fitsview.h" #include "fitsdata.h" #include "fitslabel.h" #include "kspopupmenu.h" #include "kstarsdata.h" #include "ksutils.h" #include "Options.h" #include "skymap.h" #include "fits_debug.h" #ifdef HAVE_INDI #include "basedevice.h" #include "indi/indilistener.h" #endif #include #include +#include +#include +#include +#include +#include #define BASE_OFFSET 50 #define ZOOM_DEFAULT 100.0 #define ZOOM_MIN 10 #define ZOOM_MAX 400 #define ZOOM_LOW_INCR 10 #define ZOOM_HIGH_INCR 50 FITSView::FITSView(QWidget *parent, FITSMode fitsMode, FITSScale filterType) : QScrollArea(parent), zoomFactor(1.2) { grabGesture(Qt::PinchGesture); image_frame.reset(new FITSLabel(this)); filter = filterType; mode = fitsMode; setBackgroundRole(QPalette::Dark); markerCrosshair.setX(0); markerCrosshair.setY(0); setBaseSize(740, 530); connect(image_frame.get(), SIGNAL(newStatus(QString,FITSBar)), this, SIGNAL(newStatus(QString,FITSBar))); connect(image_frame.get(), SIGNAL(pointSelected(int,int)), this, SLOT(processPointSelection(int,int))); connect(image_frame.get(), SIGNAL(markerSelected(int,int)), this, SLOT(processMarkerSelection(int,int))); connect(&wcsWatcher, SIGNAL(finished()), this, SLOT(syncWCSState())); connect(&fitsWatcher, &QFutureWatcher::finished, this, &FITSView::loadInFrame); image_frame->setMouseTracking(true); setCursorMode( selectCursor); //This is the default mode because the Focus and Align FitsViews should not be in dragMouse mode noImageLabel = new QLabel(); noImage.load(":/images/noimage.png"); noImageLabel->setPixmap(noImage); noImageLabel->setAlignment(Qt::AlignCenter); this->setWidget(noImageLabel); redScopePixmap = QPixmap(":/icons/center_telescope_red.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); magentaScopePixmap = QPixmap(":/icons/center_telescope_magenta.svg").scaled(32, 32, Qt::KeepAspectRatio, Qt::FastTransformation); //if (fitsMode == FITS_GUIDE) //connect(image_frame.get(), SIGNAL(pointSelected(int,int)), this, SLOT(processPointSelection(int,int))); // Default size //resize(INITIAL_W, INITIAL_H); } FITSView::~FITSView() { fitsWatcher.waitForFinished(); - wcsWatcher.waitForFinished(); + wcsWatcher.waitForFinished(); delete (imageData); } /** This method looks at what mouse mode is currently selected and updates the cursor to match. */ void FITSView::updateMouseCursor() { if (cursorMode == dragCursor) { if (horizontalScrollBar()->maximum() > 0 || verticalScrollBar()->maximum() > 0) { if (!image_frame->getMouseButtonDown()) viewport()->setCursor(Qt::PointingHandCursor); else viewport()->setCursor(Qt::ClosedHandCursor); } else viewport()->setCursor(Qt::CrossCursor); } else if (cursorMode == selectCursor) { viewport()->setCursor(Qt::CrossCursor); } else if (cursorMode == scopeCursor) { viewport()->setCursor(QCursor(redScopePixmap, 10, 10)); } else if (cursorMode == crosshairCursor) { viewport()->setCursor(QCursor(magentaScopePixmap, 10, 10)); } } /** This is how the mouse mode gets set. The default for a FITSView in a FITSViewer should be the dragMouse The default for a FITSView in the Focus or Align module should be the selectMouse The different defaults are accomplished by putting making the actual default mouseMode the selectMouse, but when a FITSViewer loads an image, it immediately makes it the dragMouse. */ void FITSView::setCursorMode(CursorMode mode) { cursorMode = mode; updateMouseCursor(); if (mode == scopeCursor && imageHasWCS()) { if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); } } } void FITSView::resizeEvent(QResizeEvent *event) { if ((imageData == nullptr) && noImageLabel != nullptr) { noImageLabel->setPixmap( noImage.scaled(width() - 20, height() - 20, Qt::KeepAspectRatio, Qt::FastTransformation)); noImageLabel->setFixedSize(width() - 5, height() - 5); } QScrollArea::resizeEvent(event); } #if 0 bool FITSView::loadFITS(const QString &inFilename, bool silent) { if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } QProgressDialog fitsProg(this); bool setBayerParams = false; BayerParams param; if ((imageData != nullptr) && imageData->hasDebayer()) { setBayerParams = true; imageData->getBayerParams(¶m); } // In case loadWCS is still running for previous image data, let's wait until it's over wcsWatcher.waitForFinished(); delete imageData; imageData = nullptr; filterStack.clear(); filterStack.push(FITS_NONE); if (filter != FITS_NONE) filterStack.push(filter); imageData = new FITSData(mode); if (setBayerParams) imageData->setBayerParams(¶m); if (mode == FITS_NORMAL) { fitsProg.setWindowModality(Qt::WindowModal); fitsProg.setLabelText(i18n("Please hold while loading FITS file...")); fitsProg.setWindowTitle(i18n("Loading FITS")); fitsProg.setValue(10); qApp->processEvents(); } if (!imageData->loadFITS(inFilename, silent)) return false; if (mode == FITS_NORMAL) { if (fitsProg.wasCanceled()) return false; else { fitsProg.setValue(65); qApp->processEvents(); } } emit debayerToggled(imageData->hasDebayer()); currentWidth = imageData->width(); currentHeight = imageData->height(); image_width = currentWidth; image_height = currentHeight; image_frame->setSize(image_width, image_height); initDisplayImage(); // Rescale to fits window if (firstLoad) { currentZoom = 100; if (rescale(ZOOM_FIT_WINDOW) != 0) return false; firstLoad = false; } else { if (rescale(ZOOM_KEEP_LEVEL) != 0) return false; } if (mode == FITS_NORMAL) { if (fitsProg.wasCanceled()) return false; else { fitsProg.setValue(100); qApp->processEvents(); } } setAlignment(Qt::AlignCenter); // Load WCS data now if selected and image contains valid WCS header if (imageData->hasWCS() && Options::autoWCS() && (mode == FITS_NORMAL || mode == FITS_ALIGN) && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); } else syncWCSState(); if (isVisible()) emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); if (showStarProfile) { if(floatingToolBar != nullptr) toggleProfileAction->setChecked(true); QTimer::singleShot(100 , this , SLOT(viewStarProfile())); //Need to wait till the Focus module finds stars, if its the Focus module. } updateFrame(); emit imageLoaded(); return true; } #endif void FITSView::loadFITS(const QString &inFilename, bool silent) { if (floatingToolBar != nullptr) { floatingToolBar->setVisible(true); } bool setBayerParams = false; BayerParams param; if ((imageData != nullptr) && imageData->hasDebayer()) { setBayerParams = true; imageData->getBayerParams(¶m); } // In case loadWCS is still running for previous image data, let's wait until it's over wcsWatcher.waitForFinished(); delete imageData; imageData = nullptr; filterStack.clear(); filterStack.push(FITS_NONE); if (filter != FITS_NONE) filterStack.push(filter); imageData = new FITSData(mode); if (setBayerParams) imageData->setBayerParams(¶m); fitsWatcher.setFuture(imageData->loadFITS(inFilename, silent)); } void FITSView::loadInFrame() { // Check if the loading was OK if (fitsWatcher.result() == false) { m_LastError = imageData->getLastError(); emit failed(); return; } // Notify if there is debayer data. emit debayerToggled(imageData->hasDebayer()); // Set current width and height currentWidth = imageData->width(); currentHeight = imageData->height(); image_width = currentWidth; image_height = currentHeight; image_frame->setSize(image_width, image_height); // Init the display image initDisplayImage(); + uint8_t *ASImageBuffer = nullptr; + + if (Options::autoStretch() && (filter == FITS_NONE || (filter >= FITS_ROTATE_CW && filter <= FITS_FLIP_V))) + { + // If we perform autostretch, we need to create a buffer to save the raw image data before + // autostretch filter operation changes the data. + // After rescaling is done, we + uint32_t totalBytes = image_width * image_height *imageData->channels() * imageData->getBytesPerPixel(); + ASImageBuffer = new uint8_t[totalBytes]; + memcpy(ASImageBuffer, imageData->getImageBuffer(), totalBytes); + imageData->applyFilter(FITS_AUTO_STRETCH); + } + else + imageData->applyFilter(filter); + // Rescale to fits window on first load if (firstLoad) { currentZoom = 100; if (rescale(ZOOM_FIT_WINDOW) == false) { m_LastError = i18n("Rescaling image failed."); + delete [] ASImageBuffer; emit failed(); return; } firstLoad = false; } else { if (rescale(ZOOM_KEEP_LEVEL) == false) { m_LastError = i18n("Rescaling image failed."); + delete [] ASImageBuffer; emit failed(); return; } } + // Restore original raw buffer after Autostretch if applicable + if (ASImageBuffer) + { + imageData->setImageBuffer(ASImageBuffer); + } + setAlignment(Qt::AlignCenter); // Load WCS data now if selected and image contains valid WCS header if (imageData->hasWCS() && Options::autoWCS() && (mode == FITS_NORMAL || mode == FITS_ALIGN) && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); } else syncWCSState(); if (isVisible()) emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); if (showStarProfile) { if(floatingToolBar != nullptr) toggleProfileAction->setChecked(true); //Need to wait till the Focus module finds stars, if its the Focus module. QTimer::singleShot(100 , this , SLOT(viewStarProfile())); } scaledImage = QImage(); updateFrame(); emit loaded(); } int FITSView::saveFITS(const QString &newFilename) { return imageData->saveFITS(newFilename); } bool FITSView::rescale(FITSZoom type) { switch (imageData->property("dataType").toInt()) { case TBYTE: return rescale(type); case TSHORT: return rescale(type); case TUSHORT: return rescale(type); case TLONG: return rescale(type); case TULONG: return rescale(type); case TFLOAT: - return rescale(type); + return rescale(type); case TLONGLONG: return rescale(type); case TDOUBLE: return rescale(type); default: break; } return false; } FITSView::CursorMode FITSView::getCursorMode() { return cursorMode; } void FITSView::enterEvent(QEvent *event) { Q_UNUSED(event) if ((floatingToolBar != nullptr) && (imageData != nullptr)) { QPointer eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); QPointer a = new QPropertyAnimation(eff, "opacity"); a->setDuration(500); a->setStartValue(0.2); a->setEndValue(1); a->setEasingCurve(QEasingCurve::InBack); a->start(QPropertyAnimation::DeleteWhenStopped); } } void FITSView::leaveEvent(QEvent *event) { Q_UNUSED(event) if ((floatingToolBar != nullptr) && (imageData != nullptr)) { QPointer eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); QPointer a = new QPropertyAnimation(eff, "opacity"); a->setDuration(500); a->setStartValue(1); a->setEndValue(0.2); a->setEasingCurve(QEasingCurve::OutBack); a->start(QPropertyAnimation::DeleteWhenStopped); } } template bool FITSView::rescale(FITSZoom type) { - double min, max; - bool displayBuffer = false; - if (rawImage.isNull()) return false; uint8_t *image_buffer = imageData->getImageBuffer(); - uint32_t size = imageData->width() * imageData->height(); - int BBP = imageData->getBytesPerPixel(); +#if 0 + int BBP= imageData->getBytesPerPixel(); filter = filterStack.last(); if (Options::autoStretch() && (filter == FITS_NONE || (filter >= FITS_ROTATE_CW && filter <= FITS_FLIP_V))) { image_buffer = new uint8_t[size * imageData->channels() * BBP]; memcpy(image_buffer, imageData->getImageBuffer(), size * imageData->channels() * BBP); displayBuffer = true; double data_min = -1; double data_max = -1; imageData->applyFilter(FITS_AUTO_STRETCH, image_buffer, &data_min, &data_max); min = data_min; max = data_max; } else { imageData->applyFilter(filter); imageData->getMinMax(&min, &max); } +#endif + + scaledImage = QImage(); auto *buffer = reinterpret_cast(image_buffer); - if (fabs(min - max) < 2) + if (imageData->getMin(0) == imageData->getMax(0)) { rawImage.fill(Qt::white); emit newStatus(i18n("Image is saturated."), FITS_MESSAGE); } else { - double bscale = 255. / (max - min); - double bzero = (-min) * (255. / (max - min)); - if (image_height != imageData->height() || image_width != imageData->width()) { image_width = imageData->width(); image_height = imageData->height(); initDisplayImage(); if (isVisible()) emit newStatus(QString("%1x%2").arg(image_width).arg(image_height), FITS_RESOLUTION); } image_frame->setScaledContents(true); currentWidth = rawImage.width(); currentHeight = rawImage.height(); if (imageData->channels() == 1) { + double range = imageData->getMax(0) - imageData->getMin(0); + double bscale = 255. / range; + double bzero = (-imageData->getMin(0)) * (255. / range); + QVector> futures; /* Fill in pixel values using indexed map, linear scale */ for (uint32_t j = 0; j < image_height; j++) - { + { futures.append(QtConcurrent::run([=]() { T *runningBuffer = buffer +j*image_width; uint8_t *scanLine = rawImage.scanLine(j); for (uint32_t i = 0; i < image_width; i++) { //scanLine[i] = qBound(0, static_cast(runningBuffer[i] * bscale + bzero), 255); scanLine[i] = qBound(0.0, runningBuffer[i] * bscale + bzero, 255.0); } })); } for(QFuture future : futures) future.waitForFinished(); } else { QVector> futures; + double bscaleR = 255. / (imageData->getMax(0) - imageData->getMin(0)); + double bzeroR = (-imageData->getMin(0)) * (255. / (imageData->getMax(0) - imageData->getMin(0))); + double bscaleG = 255. / (imageData->getMax(1) - imageData->getMin(1)); + double bzeroG = (-imageData->getMin(1)) * (255. / (imageData->getMax(1) - imageData->getMin(1))); + double bscaleB = 255. / (imageData->getMax(2) - imageData->getMin(2)); + double bzeroB = (-imageData->getMin(2)) * (255. / (imageData->getMax(2) - imageData->getMin(2))); /* Fill in pixel values using indexed map, linear scale */ for (uint32_t j = 0; j < image_height; j++) - { + { futures.append(QtConcurrent::run([=]() { auto *scanLine = reinterpret_cast((rawImage.scanLine(j))); T *runningBufferR = buffer + j*image_width; T *runningBufferG = buffer + j*image_width + size; T *runningBufferB = buffer + j*image_width + size*2; for (uint32_t i = 0; i < image_width; i++) { - scanLine[i] = qRgb(runningBufferR[i] * bscale + bzero, - runningBufferG[i] * bscale + bzero, - runningBufferB[i] * bscale + bzero);; + scanLine[i] = qRgb(runningBufferR[i] * bscaleR + bzeroR, + runningBufferG[i] * bscaleG + bzeroG, + runningBufferB[i] * bscaleB + bzeroB);; } })); } for(QFuture future : futures) future.waitForFinished(); } #if 0 if (imageData->getNumOfChannels() == 1) { /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < image_height; j++) { unsigned char *scanLine = display_image->scanLine(j); for (int i = 0; i < image_width; i++) { val = buffer[j * image_width + i] * bscale + bzero; scanLine[i] = qBound(0.0, val, 255.0); } } } else { double rval = 0, gval = 0, bval = 0; QRgb value; /* Fill in pixel values using indexed map, linear scale */ for (int j = 0; j < image_height; j++) { QRgb *scanLine = reinterpret_cast((display_image->scanLine(j))); for (int i = 0; i < image_width; i++) { rval = buffer[j * image_width + i]; gval = buffer[j * image_width + i + size]; bval = buffer[j * image_width + i + size * 2]; value = qRgb(rval * bscale + bzero, gval * bscale + bzero, bval * bscale + bzero); scanLine[i] = value; } } } #endif - } - - if (displayBuffer) - delete[] image_buffer; + } switch (type) { case ZOOM_FIT_WINDOW: if ((rawImage.width() > width() || rawImage.height() > height())) { double w = baseSize().width() - BASE_OFFSET; double h = baseSize().height() - BASE_OFFSET; if (!firstLoad) { w = viewport()->rect().width() - BASE_OFFSET; h = viewport()->rect().height() - BASE_OFFSET; } // Find the zoom level which will enclose the current FITS in the current window size double zoomX = floor((w / static_cast(currentWidth)) * 100.); double zoomY = floor((h / static_cast(currentHeight)) * 100.); (zoomX < zoomY) ? currentZoom = zoomX : currentZoom = zoomY; currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); if (currentZoom <= ZOOM_MIN) emit actionUpdated("view_zoom_out", false); } else { currentZoom = 100; currentWidth = image_width; currentHeight = image_height; } break; case ZOOM_KEEP_LEVEL: { currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); } break; default: currentZoom = 100; break; } setWidget(image_frame.get()); if (type != ZOOM_KEEP_LEVEL) emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); return true; } void FITSView::ZoomIn() { if (currentZoom >= ZOOM_DEFAULT && Options::limitedResourcesMode()) { emit newStatus(i18n("Cannot zoom in further due to active limited resources mode."), FITS_MESSAGE); return; } if (currentZoom < ZOOM_DEFAULT) currentZoom += ZOOM_LOW_INCR; else currentZoom += ZOOM_HIGH_INCR; emit actionUpdated("view_zoom_out", true); if (currentZoom >= ZOOM_MAX) { currentZoom = ZOOM_MAX; emit actionUpdated("view_zoom_in", false); } currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); } void FITSView::ZoomOut() { if (currentZoom <= ZOOM_DEFAULT) currentZoom -= ZOOM_LOW_INCR; else currentZoom -= ZOOM_HIGH_INCR; if (currentZoom <= ZOOM_MIN) { currentZoom = ZOOM_MIN; emit actionUpdated("view_zoom_out", false); } emit actionUpdated("view_zoom_in", true); currentWidth = image_width * (currentZoom / ZOOM_DEFAULT); currentHeight = image_height * (currentZoom / ZOOM_DEFAULT); updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); } void FITSView::ZoomToFit() { if (rawImage.isNull() == false) { rescale(ZOOM_FIT_WINDOW); updateFrame(); } } void FITSView::updateFrame() -{ +{ bool ok = false; if (currentZoom != ZOOM_DEFAULT) { // Only scale when necessary if (scaledImage.isNull() || currentWidth != lastWidth || currentHeight != lastHeight) { scaledImage = rawImage.scaled(currentWidth, currentHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); lastWidth = currentWidth; lastHeight = currentHeight; } ok = displayPixmap.convertFromImage(scaledImage); } else ok = displayPixmap.convertFromImage(rawImage); if (!ok) return; QPainter painter(&displayPixmap); drawOverlay(&painter); image_frame->setPixmap(displayPixmap); image_frame->resize(currentWidth, currentHeight); } void FITSView::ZoomDefault() { if (image_frame != nullptr) { emit actionUpdated("view_zoom_out", true); emit actionUpdated("view_zoom_in", true); currentZoom = ZOOM_DEFAULT; currentWidth = image_width; currentHeight = image_height; updateFrame(); emit newStatus(QString("%1%").arg(currentZoom), FITS_ZOOM); update(); } } void FITSView::drawOverlay(QPainter *painter) { painter->setRenderHint(QPainter::Antialiasing, Options::useAntialias()); if (markStars) drawStarCentroid(painter); if (trackingBoxEnabled && getCursorMode() != FITSView::scopeCursor) drawTrackingBox(painter); if (!markerCrosshair.isNull()) drawMarker(painter); if (showCrosshair) drawCrosshair(painter); if (showObjects) drawObjectNames(painter); if (showEQGrid) drawEQGrid(painter); if (showPixelGrid) drawPixelGrid(painter); } void FITSView::updateMode(FITSMode fmode) { mode = fmode; } void FITSView::drawMarker(QPainter *painter) { painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")), 2)); painter->setBrush(Qt::NoBrush); float pxperdegree = (currentZoom / ZOOM_DEFAULT) * (57.3 / 1.8); float s1 = 0.5 * pxperdegree; float s2 = pxperdegree; float s3 = 2.0 * pxperdegree; float x0 = markerCrosshair.x() * (currentZoom / ZOOM_DEFAULT); float y0 = markerCrosshair.y() * (currentZoom / ZOOM_DEFAULT); float x1 = x0 - 0.5 * s1; float y1 = y0 - 0.5 * s1; float x2 = x0 - 0.5 * s2; float y2 = y0 - 0.5 * s2; float x3 = x0 - 0.5 * s3; float y3 = y0 - 0.5 * s3; //Draw radial lines painter->drawLine(QPointF(x1, y0), QPointF(x3, y0)); painter->drawLine(QPointF(x0 + s2, y0), QPointF(x0 + 0.5 * s1, y0)); painter->drawLine(QPointF(x0, y1), QPointF(x0, y3)); painter->drawLine(QPointF(x0, y0 + 0.5 * s1), QPointF(x0, y0 + s2)); //Draw circles at 0.5 & 1 degrees painter->drawEllipse(QRectF(x1, y1, s1, s1)); painter->drawEllipse(QRectF(x2, y2, s2, s2)); } void FITSView::drawStarCentroid(QPainter *painter) { painter->setPen(QPen(Qt::red, 2)); // image_data->getStarCenter(); QList starCenters = imageData->getStarCenters(); for (int i = 0; i < starCenters.count(); i++) { int x1 = (starCenters[i]->x - starCenters[i]->width / 2) * (currentZoom / ZOOM_DEFAULT); int y1 = (starCenters[i]->y - starCenters[i]->width / 2) * (currentZoom / ZOOM_DEFAULT); int w = (starCenters[i]->width) * (currentZoom / ZOOM_DEFAULT); painter->drawEllipse(x1, y1, w, w); } } void FITSView::drawTrackingBox(QPainter *painter) { painter->setPen(QPen(Qt::green, 2)); if (trackingBox.isNull()) return; int x1 = trackingBox.x() * (currentZoom / ZOOM_DEFAULT); int y1 = trackingBox.y() * (currentZoom / ZOOM_DEFAULT); int w = trackingBox.width() * (currentZoom / ZOOM_DEFAULT); int h = trackingBox.height() * (currentZoom / ZOOM_DEFAULT); painter->drawRect(x1, y1, w, h); } /** This Method draws a large Crosshair in the center of the image, it is like a set of axes. */ void FITSView::drawCrosshair(QPainter *painter) { float scale = (currentZoom / ZOOM_DEFAULT); QPointF c = QPointF((qreal)image_width / 2 * scale, (qreal)image_height / 2 * scale); float midX = (float)image_width / 2 * scale; float midY = (float)image_height / 2 * scale; float maxX = (float)image_width * scale; float maxY = (float)image_height * scale; float r = 50 * scale; painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")))); //Horizontal Line to Circle painter->drawLine(0, midY, midX - r, midY); //Horizontal Line past Circle painter->drawLine(midX + r, midY, maxX, midY); //Vertical Line to Circle painter->drawLine(midX, 0, midX, midY - r); //Vertical Line past Circle painter->drawLine(midX, midY + r, midX, maxY); //Circles painter->drawEllipse(c, r, r); painter->drawEllipse(c, r / 2, r / 2); } /** This method is intended to draw a pixel grid onto the image. It first determines useful information from the image. Then it draws the axes on the image if the crosshairs are not displayed. Finally it draws the gridlines so that there will be 4 Gridlines on either side of the axes. Note: This has to start drawing at the center not at the edges because the center axes must be in the center of the image. */ void FITSView::drawPixelGrid(QPainter *painter) { float scale = (currentZoom / ZOOM_DEFAULT); double width = image_width * scale; double height = image_height * scale; double cX = width / 2; double cY = height / 2; double deltaX = width / 10; double deltaY = height / 10; //draw the Axes painter->setPen(QPen(Qt::red)); painter->drawText(cX - 30, height - 5, QString::number((int)((cX) / scale))); painter->drawText(width - 30, cY - 5, QString::number((int)((cY) / scale))); if (!showCrosshair) { painter->drawLine(cX, 0, cX, height); painter->drawLine(0, cY, width, cY); } painter->setPen(QPen(Qt::gray)); //Start one iteration past the Center and draw 4 lines on either side of 0 for (int x = deltaX; x < cX - deltaX; x += deltaX) { painter->drawText(cX + x - 30, height - 5, QString::number((int)((cX + x) / scale))); painter->drawText(cX - x - 30, height - 5, QString::number((int)((cX - x) / scale))); painter->drawLine(cX - x, 0, cX - x, height); painter->drawLine(cX + x, 0, cX + x, height); } //Start one iteration past the Center and draw 4 lines on either side of 0 for (int y = deltaY; y < cY - deltaY; y += deltaY) { painter->drawText(width - 30, cY + y - 5, QString::number((int)((cY + y) / scale))); painter->drawText(width - 30, cY - y - 5, QString::number((int)((cY - y) / scale))); painter->drawLine(0, cY + y, width, cY + y); painter->drawLine(0, cY - y, width, cY - y); } } bool FITSView::imageHasWCS() { if (imageData != nullptr) return imageData->hasWCS(); return false; } void FITSView::drawObjectNames(QPainter *painter) { painter->setPen(QPen(QColor(KStarsData::Instance()->colorScheme()->colorNamed("FITSObjectLabelColor")))); float scale = (currentZoom / ZOOM_DEFAULT); foreach (FITSSkyObject *listObject, imageData->getSkyObjects()) { painter->drawRect(listObject->x() * scale - 5, listObject->y() * scale - 5, 10, 10); painter->drawText(listObject->x() * scale + 10, listObject->y() * scale + 10, listObject->skyObject()->name()); } } /** This method will paint EQ Gridlines in an overlay if there is WCS data present. It determines the minimum and maximum RA and DEC, then it uses that information to judge which gridLines to draw. Then it calls the drawEQGridlines methods below to draw gridlines at those specific RA and Dec values. */ void FITSView::drawEQGrid(QPainter *painter) { float scale = (currentZoom / ZOOM_DEFAULT); if (imageData->hasWCS()) { wcs_point *wcs_coord = imageData->getWCSCoord(); if (wcs_coord != nullptr) { int size = image_width * image_height; double maxRA = -1000; double minRA = 1000; double maxDec = -1000; double minDec = 1000; for (int i = 0; i < (size); i++) { double ra = wcs_coord[i].ra; double dec = wcs_coord[i].dec; if (ra > maxRA) maxRA = ra; if (ra < minRA) minRA = ra; if (dec > maxDec) maxDec = dec; if (dec < minDec) minDec = dec; } auto minDecMinutes = (int)(minDec * 12); //This will force the Dec Scale to 5 arc minutes in the loop auto maxDecMinutes = (int)(maxDec * 12); auto minRAMinutes = (int)(minRA / 15.0 * 120.0); //This will force the scale to 1/2 minutes of RA in the loop from 0 to 50 degrees auto maxRAMinutes = (int)(maxRA / 15.0 * 120.0); double raConvert = 15 / 120.0; //This will undo the calculation above to retrieve the actual RA. double decConvert = 1.0 / 12.0; //This will undo the calculation above to retrieve the actual DEC. if (maxDec > 50 || minDec < -50) { minRAMinutes = (int)(minRA / 15.0 * 60.0); //This will force the scale to 1 min of RA from 50 to 80 degrees maxRAMinutes = (int)(maxRA / 15.0 * 60.0); raConvert = 15 / 60.0; } if (maxDec > 80 || minDec < -80) { minRAMinutes = (int)(minRA / 15.0 * 30); //This will force the scale to 2 min of RA from 80 to 85 degrees maxRAMinutes = (int)(maxRA / 15.0 * 30); raConvert = 15 / 30.0; } if (maxDec > 85 || minDec < -85) { minRAMinutes = (int)(minRA / 15.0 * 6); //This will force the scale to 10 min of RA from 85 to 89 degrees maxRAMinutes = (int)(maxRA / 15.0 * 6); raConvert = 15 / 6.0; } if (maxDec >= 89.25 || minDec <= -89.25) { minRAMinutes = (int)(minRA / 15); //This will force the scale to whole hours of RA in the loop really close to the poles maxRAMinutes = (int)(maxRA / 15); raConvert = 15; } painter->setPen(QPen(Qt::yellow)); QPointF pixelPoint, imagePoint, pPoint; //This section draws the RA Gridlines for (int targetRA = minRAMinutes; targetRA <= maxRAMinutes; targetRA++) { painter->setPen(QPen(Qt::yellow)); double target = targetRA * raConvert; if (eqGridPoints.count() != 0) eqGridPoints.clear(); double increment = std::abs((maxDec - minDec) / 100.0); //This will determine how many points to use to create the RA Line for (double targetDec = minDec; targetDec <= maxDec; targetDec += increment) { SkyPoint pointToGet(target / 15.0, targetDec); bool inImage = imageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); if (inImage) { QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); eqGridPoints.append(pt); } } if (eqGridPoints.count() > 1) { for (int i = 1; i < eqGridPoints.count(); i++) painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); QPointF pt = getPointForGridLabel(); if (pt.x() != -100) { if (maxDec > 50 || maxDec < -50) painter->drawText(pt.x(), pt.y(), QString::number(dms(target).hour()) + "h " + QString::number(dms(target).minute()) + '\''); else painter->drawText(pt.x() - 20, pt.y(), QString::number(dms(target).hour()) + "h " + QString::number(dms(target).minute()) + "' " + QString::number(dms(target).second()) + "''"); } } } //This section draws the DEC Gridlines for (int targetDec = minDecMinutes; targetDec <= maxDecMinutes; targetDec++) { if (eqGridPoints.count() != 0) eqGridPoints.clear(); double increment = std::abs((maxRA - minRA) / 100.0); //This will determine how many points to use to create the Dec Line double target = targetDec * decConvert; for (double targetRA = minRA; targetRA <= maxRA; targetRA += increment) { SkyPoint pointToGet(targetRA / 15, targetDec * decConvert); bool inImage = imageData->wcsToPixel(pointToGet, pixelPoint, imagePoint); if (inImage) { QPointF pt(pixelPoint.x() * scale, pixelPoint.y() * scale); eqGridPoints.append(pt); } } if (eqGridPoints.count() > 1) { for (int i = 1; i < eqGridPoints.count(); i++) painter->drawLine(eqGridPoints.value(i - 1), eqGridPoints.value(i)); QPointF pt = getPointForGridLabel(); if (pt.x() != -100) painter->drawText(pt.x(), pt.y(), QString::number(dms(target).degree()) + "° " + QString::number(dms(target).arcmin()) + '\''); } } //This Section Draws the North Celestial Pole if present SkyPoint NCP(0, 90); bool NCPtest = imageData->wcsToPixel(NCP, pPoint, imagePoint); if (NCPtest) { bool NCPinImage = (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); if (NCPinImage) { painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, i18nc("North Celestial Pole", "NCP")); } } //This Section Draws the South Celestial Pole if present SkyPoint SCP(0, -90); bool SCPtest = imageData->wcsToPixel(SCP, pPoint, imagePoint); if (SCPtest) { bool SCPinImage = (pPoint.x() > 0 && pPoint.x() < image_width) && (pPoint.y() > 0 && pPoint.y() < image_height); if (SCPinImage) { painter->fillRect(pPoint.x() * scale - 2, pPoint.y() * scale - 2, 4, 4, KStarsData::Instance()->colorScheme()->colorNamed("TargetColor")); painter->drawText(pPoint.x() * scale + 15, pPoint.y() * scale + 15, i18nc("South Celestial Pole", "SCP")); } } } } } bool FITSView::pointIsInImage(QPointF pt, bool scaled) { float scale = (currentZoom / ZOOM_DEFAULT); if (scaled) return pt.x() < image_width * scale && pt.y() < image_height * scale && pt.x() > 0 && pt.y() > 0; else return pt.x() < image_width && pt.y() < image_height && pt.x() > 0 && pt.y() > 0; } QPointF FITSView::getPointForGridLabel() { float scale = (currentZoom / ZOOM_DEFAULT); //These get the maximum X and Y points in the list that are in the image QPointF maxXPt(image_width * scale / 2, image_height * scale / 2); for (auto& p : eqGridPoints) { if (p.x() > maxXPt.x() && pointIsInImage(p, true)) maxXPt = p; } QPointF maxYPt(image_width * scale / 2, image_height * scale / 2); for (auto& p : eqGridPoints) { if (p.y() > maxYPt.y() && pointIsInImage(p, true)) maxYPt = p; } QPointF minXPt(image_width * scale / 2, image_height * scale / 2); for (auto& p : eqGridPoints) { if (p.x() < minXPt.x() && pointIsInImage(p, true)) minXPt = p; } QPointF minYPt(image_width * scale / 2, image_height * scale / 2); for (auto& p : eqGridPoints) { if (p.y() < minYPt.y() && pointIsInImage(p, true)) minYPt = p; } //This gives preferene to points that are on the right hand side and bottom. //But if the line doesn't intersect the right or bottom, it then tries for the top and left. //If no points are found in the image, it returns a point off the screen //If all else fails, like in the case of a circle on the image, it returns the far right point. if (image_width * scale - maxXPt.x() < 10) { return QPointF( image_width * scale - 50, maxXPt.y() - 10); //This will draw the text on the right hand side, up and to the left of the point where the line intersects } if (image_height * scale - maxYPt.y() < 10) return QPointF( maxYPt.x() - 40, image_height * scale - 10); //This will draw the text on the bottom side, up and to the left of the point where the line intersects if (minYPt.y() * scale < 30) return QPointF( minYPt.x() + 10, 20); //This will draw the text on the top side, down and to the right of the point where the line intersects if (minXPt.x() * scale < 30) return QPointF( 10, minXPt.y() + 20); //This will draw the text on the left hand side, down and to the right of the point where the line intersects if (maxXPt.x() == image_width * scale / 2 && maxXPt.y() == image_height * scale / 2) return QPointF(-100, -100); //All of the points were off the screen return QPoint(maxXPt.x() - 40, maxXPt.y() - 10); } void FITSView::setFirstLoad(bool value) { firstLoad = value; } QPixmap &FITSView::getTrackingBoxPixmap(uint8_t margin) { if (trackingBox.isNull()) return trackingBoxPixmap; int x1 = (trackingBox.x() - margin) * (currentZoom / ZOOM_DEFAULT); int y1 = (trackingBox.y() - margin) * (currentZoom / ZOOM_DEFAULT); int w = (trackingBox.width() + margin*2) * (currentZoom / ZOOM_DEFAULT); int h = (trackingBox.height() + margin*2) * (currentZoom / ZOOM_DEFAULT); trackingBoxPixmap = image_frame->grab(QRect(x1, y1, w, h)); return trackingBoxPixmap; } void FITSView::setTrackingBox(const QRect &rect) { if (rect != trackingBox) { trackingBox = rect; updateFrame(); if(showStarProfile) viewStarProfile(); } } void FITSView::resizeTrackingBox(int newSize) { int x = trackingBox.x() + trackingBox.width()/2; int y = trackingBox.y() + trackingBox.height()/2; int delta = newSize / 2; setTrackingBox(QRect( x - delta, y - delta, newSize, newSize)); } bool FITSView::isCrosshairShown() { return showCrosshair; } bool FITSView::isEQGridShown() { return showEQGrid; } bool FITSView::areObjectsShown() { return showObjects; } bool FITSView::isPixelGridShown() { return showPixelGrid; } void FITSView::toggleCrosshair() { showCrosshair = !showCrosshair; updateFrame(); } void FITSView::toggleEQGrid() { showEQGrid = !showEQGrid; if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); return; } if (image_frame != nullptr) updateFrame(); } void FITSView::toggleObjects() { showObjects = !showObjects; if (!imageData->isWCSLoaded() && !wcsWatcher.isRunning()) { QFuture future = QtConcurrent::run(imageData, &FITSData::loadWCS); wcsWatcher.setFuture(future); return; } if (image_frame != nullptr) updateFrame(); } void FITSView::toggleStars() { toggleStars(!markStars); if (image_frame != nullptr) updateFrame(); } void FITSView::toggleStarProfile() { #ifdef HAVE_DATAVISUALIZATION showStarProfile = !showStarProfile; if(showStarProfile && trackingBoxEnabled) viewStarProfile(); if(toggleProfileAction) toggleProfileAction->setChecked(showStarProfile); if(mode == FITS_NORMAL || mode == FITS_ALIGN) { if(showStarProfile) { setCursorMode(selectCursor); connect(this, SIGNAL(trackingStarSelected(int,int)), this, SLOT(move3DTrackingBox(int,int))); if(floatingToolBar && starProfileWidget) connect(starProfileWidget, SIGNAL(rejected()) , this, SLOT(toggleStarProfile())); if(starProfileWidget) connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)) , this, SLOT(resizeTrackingBox(int))); trackingBox = QRect(0, 0, 128, 128); trackingBoxEnabled = true; updateFrame(); } else { if(getCursorMode() == selectCursor) setCursorMode(dragCursor); disconnect(this, SIGNAL(trackingStarSelected(int,int)), this, SLOT(move3DTrackingBox(int,int))); disconnect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)) , this, SLOT(resizeTrackingBox(int))); if(floatingToolBar) disconnect(starProfileWidget, SIGNAL(rejected()) , this, SLOT(toggleStarProfile())); setTrackingBoxEnabled(false); } } #endif } void FITSView::move3DTrackingBox(int x, int y) { int boxSize = trackingBox.width(); QRect starRect = QRect(x - boxSize / 2 , y - boxSize / 2, boxSize, boxSize); setTrackingBox(starRect); } void FITSView::viewStarProfile() { #ifdef HAVE_DATAVISUALIZATION if(!trackingBoxEnabled) { setTrackingBoxEnabled(true); setTrackingBox(QRect(0, 0, 128, 128)); } if(!starProfileWidget) { starProfileWidget = new StarProfileViewer(this); //This is a band-aid to fix a QT bug with createWindowContainer //It will set the cursor of the Window containing the view that called the Star Profile method to the Arrow Cursor //Note that Ekos Manager is a QDialog and FitsViewer is a KXmlGuiWindow QWidget *superParent = this->parentWidget(); while(superParent->parentWidget()!=0 && !superParent->inherits("QDialog") && !superParent->inherits("KXmlGuiWindow")) superParent=superParent->parentWidget(); superParent->setCursor(Qt::ArrowCursor); //This is the end of the band-aid if(floatingToolBar) connect(starProfileWidget, SIGNAL(rejected()) , this, SLOT(toggleStarProfile())); if(mode == FITS_ALIGN || mode == FITS_NORMAL) { starProfileWidget->enableTrackingBox(true); imageData->setStarAlgorithm(ALGORITHM_CENTROID); connect(starProfileWidget, SIGNAL(sampleSizeUpdated(int)) , this, SLOT(resizeTrackingBox(int))); } } QList starCenters = imageData->getStarCentersInSubFrame(trackingBox); if(starCenters.size() == 0) { // FIXME, the following does not work anymore. //imageData->findStars(&trackingBox, true); // FIXME replacing it with this imageData->findStars(ALGORITHM_CENTROID, trackingBox); starCenters = imageData->getStarCentersInSubFrame(trackingBox); } starProfileWidget->loadData(imageData, trackingBox, starCenters); starProfileWidget->show(); starProfileWidget->raise(); if(markStars) updateFrame(); //this is to update for the marked stars #endif } void FITSView::togglePixelGrid() { showPixelGrid = !showPixelGrid; updateFrame(); } int FITSView::findStars(StarAlgorithm algorithm, const QRect &searchBox) { int count = 0; if(trackingBoxEnabled) count = imageData->findStars(algorithm, trackingBox); else count = imageData->findStars(algorithm, searchBox); return count; } void FITSView::toggleStars(bool enable) { markStars = enable; if (markStars && !imageData->areStarsSearched()) { QApplication::setOverrideCursor(Qt::WaitCursor); emit newStatus(i18n("Finding stars..."), FITS_MESSAGE); qApp->processEvents(); int count = findStars(); if (count >= 0 && isVisible()) emit newStatus(i18np("1 star detected.", "%1 stars detected.", count), FITS_MESSAGE); QApplication::restoreOverrideCursor(); } } void FITSView::processPointSelection(int x, int y) { //if (mode != FITS_GUIDE) //return; //image_data->getCenterSelection(&x, &y); //setGuideSquare(x,y); emit trackingStarSelected(x, y); } void FITSView::processMarkerSelection(int x, int y) { markerCrosshair.setX(x); markerCrosshair.setY(y); updateFrame(); } void FITSView::setTrackingBoxEnabled(bool enable) { if (enable != trackingBoxEnabled) { trackingBoxEnabled = enable; //updateFrame(); } } void FITSView::wheelEvent(QWheelEvent *event) { //This attempts to send the wheel event back to the Scroll Area if it was taken from a trackpad //It should still do the zoom if it is a mouse wheel if (event->source() == Qt::MouseEventSynthesizedBySystem) { QScrollArea::wheelEvent(event); } else { QPoint mouseCenter = getImagePoint(event->pos()); if (event->angleDelta().y() > 0) ZoomIn(); else ZoomOut(); event->accept(); cleanUpZoom(mouseCenter); } } /** This method is intended to keep key locations in an image centered on the screen while zooming. If there is a marker or tracking box, it centers on those. If not, it uses the point called viewCenter that was passed as a parameter. */ void FITSView::cleanUpZoom(QPoint viewCenter) { int x0 = 0; int y0 = 0; double scale = (currentZoom / ZOOM_DEFAULT); if (!markerCrosshair.isNull()) { x0 = markerCrosshair.x() * scale; y0 = markerCrosshair.y() * scale; } else if (trackingBoxEnabled) { x0 = trackingBox.center().x() * scale; y0 = trackingBox.center().y() * scale; } else { x0 = viewCenter.x() * scale; y0 = viewCenter.y() * scale; } ensureVisible(x0, y0, width() / 2, height() / 2); updateMouseCursor(); } /** This method converts a point from the ViewPort Coordinate System to the Image Coordinate System. */ QPoint FITSView::getImagePoint(QPoint viewPortPoint) { QWidget *w = widget(); if (w == nullptr) return QPoint(0, 0); double scale = (currentZoom / ZOOM_DEFAULT); QPoint widgetPoint = w->mapFromParent(viewPortPoint); QPoint imagePoint = QPoint(widgetPoint.x() / scale, widgetPoint.y() / scale); return imagePoint; } void FITSView::initDisplayImage() { if (imageData->channels() == 1) { rawImage = QImage(image_width, image_height, QImage::Format_Indexed8); rawImage.setColorCount(256); for (int i = 0; i < 256; i++) rawImage.setColor(i, qRgb(i, i, i)); } else { rawImage = QImage(image_width, image_height, QImage::Format_RGB32); } } /** The Following two methods allow gestures to work with trackpads. Specifically, we are targeting the pinch events, so that if one is generated, Then the pinchTriggered method will be called. If the event is not a pinch gesture, then the event is passed back to the other event handlers. */ bool FITSView::event(QEvent *event) { if (event->type() == QEvent::Gesture) return gestureEvent(dynamic_cast(event)); return QScrollArea::event(event); } bool FITSView::gestureEvent(QGestureEvent *event) { if (QGesture *pinch = event->gesture(Qt::PinchGesture)) pinchTriggered(dynamic_cast(pinch)); return true; } /** This Method works with Trackpads to use the pinch gesture to scroll in and out It stores a point to keep track of the location where the gesture started so that while you are zooming, it tries to keep that initial point centered in the view. **/ void FITSView::pinchTriggered(QPinchGesture *gesture) { if (!zooming) { zoomLocation = getImagePoint(mapFromGlobal(QCursor::pos())); zooming = true; } if (gesture->state() == Qt::GestureFinished) { zooming = false; } zoomTime++; //zoomTime is meant to slow down the zooming with a pinch gesture. if (zoomTime > 10000) //This ensures zoomtime never gets too big. zoomTime = 0; if (zooming && (zoomTime % 10 == 0)) //zoomTime is set to slow it by a factor of 10. { if (gesture->totalScaleFactor() > 1) ZoomIn(); else ZoomOut(); } cleanUpZoom(zoomLocation); } /*void FITSView::handleWCSCompletion() { //bool hasWCS = wcsWatcher.result(); if(imageData->hasWCS()) this->updateFrame(); emit wcsToggled(imageData->hasWCS()); }*/ void FITSView::syncWCSState() { bool hasWCS = imageData->hasWCS(); bool wcsLoaded = imageData->isWCSLoaded(); if (hasWCS && wcsLoaded) this->updateFrame(); emit wcsToggled(hasWCS); if (toggleEQGridAction != nullptr) toggleEQGridAction->setEnabled(hasWCS); if (toggleObjectsAction != nullptr) toggleObjectsAction->setEnabled(hasWCS); if (centerTelescopeAction != nullptr) centerTelescopeAction->setEnabled(hasWCS); } void FITSView::createFloatingToolBar() { if (floatingToolBar != nullptr) return; floatingToolBar = new QToolBar(this); auto *eff = new QGraphicsOpacityEffect(this); floatingToolBar->setGraphicsEffect(eff); eff->setOpacity(0.2); floatingToolBar->setVisible(false); floatingToolBar->setStyleSheet( "QToolBar{background: rgba(150, 150, 150, 210); border:none; color: yellow}" "QToolButton{background: transparent; border:none; color: yellow}" "QToolButton:hover{background: rgba(200, 200, 200, 255);border:solid; color: yellow}" "QToolButton:checked{background: rgba(110, 110, 110, 255);border:solid; color: yellow}"); floatingToolBar->setFloatable(true); floatingToolBar->setIconSize(QSize(25, 25)); //floatingToolBar->setMovable(true); QAction *action = nullptr; floatingToolBar->addAction(QIcon::fromTheme("zoom-in"), i18n("Zoom In"), this, SLOT(ZoomIn())); floatingToolBar->addAction(QIcon::fromTheme("zoom-out"), i18n("Zoom Out"), this, SLOT(ZoomOut())); floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-best"), i18n("Default Zoom"), this, SLOT(ZoomDefault())); floatingToolBar->addAction(QIcon::fromTheme("zoom-fit-width"), i18n("Zoom to Fit"), this, SLOT(ZoomToFit())); floatingToolBar->addSeparator(); action = floatingToolBar->addAction(QIcon::fromTheme("crosshairs"), i18n("Show Cross Hairs"), this, SLOT(toggleCrosshair())); action->setCheckable(true); action = floatingToolBar->addAction(QIcon::fromTheme("map-flat"), i18n("Show Pixel Gridlines"), this, SLOT(togglePixelGrid())); action->setCheckable(true); toggleStarsAction = floatingToolBar->addAction(QIcon::fromTheme("kstars_stars"), i18n("Detect Stars in Image"), this, SLOT(toggleStars())); toggleStarsAction->setCheckable(true); #ifdef HAVE_DATAVISUALIZATION toggleProfileAction = floatingToolBar->addAction(QIcon::fromTheme("star-profile", QIcon(":/icons/star_profile.svg")), i18n("View Star Profile"), this, SLOT(toggleStarProfile())); toggleProfileAction->setCheckable(true); #endif if (mode == FITS_NORMAL || mode == FITS_ALIGN) { floatingToolBar->addSeparator(); toggleEQGridAction = floatingToolBar->addAction(QIcon::fromTheme("kstars_grid"), i18n("Show Equatorial Gridlines"), this, SLOT(toggleEQGrid())); toggleEQGridAction->setCheckable(true); toggleEQGridAction->setEnabled(false); toggleObjectsAction = floatingToolBar->addAction(QIcon::fromTheme("help-hint"), i18n("Show Objects in Image"), this, SLOT(toggleObjects())); toggleObjectsAction->setCheckable(true); toggleEQGridAction->setEnabled(false); centerTelescopeAction = floatingToolBar->addAction(QIcon::fromTheme("center_telescope", QIcon(":/icons/center_telescope.svg")), i18n("Center Telescope"), this, SLOT(centerTelescope())); centerTelescopeAction->setCheckable(true); centerTelescopeAction->setEnabled(false); } } /** This methood either enables or disables the scope mouse mode so you can slew your scope to coordinates just by clicking the mouse on a spot in the image. */ void FITSView::centerTelescope() { if (imageHasWCS()) { if (getCursorMode() == FITSView::scopeCursor) { setCursorMode(lastMouseMode); } else { lastMouseMode = getCursorMode(); setCursorMode(FITSView::scopeCursor); } updateFrame(); } updateScopeButton(); } void FITSView::updateScopeButton() { if (centerTelescopeAction != nullptr) { if (getCursorMode() == FITSView::scopeCursor) { centerTelescopeAction->setChecked(true); } else { centerTelescopeAction->setChecked(false); } } } /** This method just verifies if INDI is online, a telescope present, and is connected */ bool FITSView::isTelescopeActive() { #ifdef HAVE_INDI if (INDIListener::Instance()->size() == 0) { return false; } foreach (ISD::GDInterface *gd, INDIListener::Instance()->getDevices()) { INDI::BaseDevice *bd = gd->getBaseDevice(); if (gd->getType() != KSTARS_TELESCOPE) continue; if (bd == nullptr) continue; return bd->isConnected(); } return false; #else return false; #endif } void FITSView::setStarsEnabled(bool enable) { markStars = enable; if (floatingToolBar != nullptr) { foreach (QAction *action, floatingToolBar->actions()) { if (action->text() == i18n("Detect Stars in Image")) { action->setChecked(markStars); break; } } } } diff --git a/kstars/fitsviewer/fitsview.h b/kstars/fitsviewer/fitsview.h index 76912c180..65b52d49c 100644 --- a/kstars/fitsviewer/fitsview.h +++ b/kstars/fitsviewer/fitsview.h @@ -1,295 +1,292 @@ /* FITS Label Copyright (C) 2003-2017 Jasem Mutlaq Copyright (C) 2016-2017 Robert Lancaster This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "fitscommon.h" #include #ifdef HAVE_DATAVISUALIZATION #include "starprofileviewer.h" #endif #include #include #include #include #ifdef WIN32 // avoid compiler warning when windows.h is included after fitsio.h #include #endif #include #include #define MINIMUM_PIXEL_RANGE 5 #define MINIMUM_STDVAR 5 class QAction; class QEvent; class QGestureEvent; class QImage; class QLabel; class QPinchGesture; class QResizeEvent; class QToolBar; class FITSData; -class FITSHistogram; class FITSLabel; class FITSView : public QScrollArea { Q_OBJECT public: explicit FITSView(QWidget *parent = nullptr, FITSMode fitsMode = FITS_NORMAL, FITSScale filterType = FITS_NONE); ~FITSView(); typedef enum {dragCursor, selectCursor, scopeCursor, crosshairCursor } CursorMode; /** * @brief loadFITS Loads FITS data and display it in FITSView frame * @param inFilename FITS File name * @param silent if set, error popups are suppressed. * @note If image is successfully, loaded() signal is emitted, otherwise failed() signal is emitted. * Obtain error by calling lastError() */ void loadFITS(const QString &inFilename, bool silent = true); // Save FITS int saveFITS(const QString &newFilename); // Rescale image lineary from image_buffer, fit to window if desired bool rescale(FITSZoom type); // Access functions FITSData *getImageData() const { return imageData; } double getCurrentZoom() const { return currentZoom; } QImage getDisplayImage() const { return rawImage; } const QPixmap &getDisplayPixmap() const { return displayPixmap; } // Tracking square void setTrackingBoxEnabled(bool enable); bool isTrackingBoxEnabled() const { return trackingBoxEnabled; } QPixmap &getTrackingBoxPixmap(uint8_t margin=0); void setTrackingBox(const QRect &rect); const QRect &getTrackingBox() const { return trackingBox; } // last error const QString &lastError() const { return m_LastError; } // Overlay virtual void drawOverlay(QPainter *); // Overlay objects void drawStarCentroid(QPainter *); void drawTrackingBox(QPainter *); void drawMarker(QPainter *); void drawCrosshair(QPainter *); void drawEQGrid(QPainter *); void drawObjectNames(QPainter *painter); void drawPixelGrid(QPainter *painter); bool isCrosshairShown(); bool areObjectsShown(); bool isEQGridShown(); bool isPixelGridShown(); bool imageHasWCS(); void updateFrame(); bool isTelescopeActive(); void enterEvent(QEvent *event); void leaveEvent(QEvent *event); CursorMode getCursorMode(); void setCursorMode(CursorMode mode); void updateMouseCursor(); void updateScopeButton(); void setScopeButton(QAction *action) { centerTelescopeAction = action; } // Zoom related void cleanUpZoom(QPoint viewCenter); QPoint getImagePoint(QPoint viewPortPoint); uint16_t zoomedWidth() { return currentWidth; } uint16_t zoomedHeight() { return currentHeight; } // Star Detection int findStars(StarAlgorithm algorithm = ALGORITHM_CENTROID, const QRect &searchBox = QRect()); void toggleStars(bool enable); void setStarsEnabled(bool enable); // FITS Mode void updateMode(FITSMode fmode); FITSMode getMode() { return mode; } void setFilter(FITSScale newFilter) { filter = newFilter; } void setFirstLoad(bool value); void pushFilter(FITSScale value) { filterStack.push(value); } FITSScale popFilter() { return filterStack.pop(); } // Floating toolbar void createFloatingToolBar(); //void setLoadWCSEnabled(bool value); public slots: void wheelEvent(QWheelEvent *event); void resizeEvent(QResizeEvent *event); void ZoomIn(); void ZoomOut(); void ZoomDefault(); void ZoomToFit(); // Grids void toggleEQGrid(); void toggleObjects(); void togglePixelGrid(); void toggleCrosshair(); // Stars void toggleStars(); void toggleStarProfile(); void viewStarProfile(); void centerTelescope(); void processPointSelection(int x, int y); void processMarkerSelection(int x, int y); void move3DTrackingBox(int x, int y); void resizeTrackingBox(int newSize); protected slots: /** * @brief syncWCSState Update toolbar and actions depending on whether WCS is available or not */ void syncWCSState(); private: bool event(QEvent *event); bool gestureEvent(QGestureEvent *event); void pinchTriggered(QPinchGesture *gesture); template bool rescale(FITSZoom type); double average(); double stddev(); void calculateMaxPixel(double min, double max); void initDisplayImage(); QPointF getPointForGridLabel(); bool pointIsInImage(QPointF pt, bool scaled); void loadInFrame(); public: CursorMode lastMouseMode { selectCursor }; bool isStarProfileShown() { return showStarProfile; } protected: /// WCS Future Watcher QFutureWatcher wcsWatcher; /// FITS Future Watcher QFutureWatcher fitsWatcher; /// Cross hair QPointF markerCrosshair; /// Pointer to FITSData object FITSData *imageData { nullptr }; /// Current zoom level double currentZoom { 0 }; private: QLabel *noImageLabel { nullptr }; QPixmap noImage; QVector eqGridPoints; std::unique_ptr image_frame; uint32_t image_width { 0 }; uint32_t image_height { 0 }; /// Current width due to zoom uint16_t currentWidth { 0 }; uint16_t lastWidth { 0 }; /// Current height due to zoom uint16_t currentHeight { 0 }; uint16_t lastHeight { 0 }; /// Image zoom factor const double zoomFactor; // Original full-size image QImage rawImage; // Scaled images QImage scaledImage; // Actual pixmap after all the overlays QPixmap displayPixmap; - // Histogram - FITSHistogram *histogram { nullptr }; bool firstLoad { true }; bool markStars { false }; bool showStarProfile { false }; bool showCrosshair { false }; bool showObjects { false }; bool showEQGrid { false }; bool showPixelGrid { false }; CursorMode cursorMode { selectCursor }; bool zooming { false }; int zoomTime { 0 }; QPoint zoomLocation; QString filename; FITSMode mode; FITSScale filter; QString m_LastError; QStack filterStack; // Tracking box bool trackingBoxEnabled { false }; QRect trackingBox; QPixmap trackingBoxPixmap; // Scope pixmap QPixmap redScopePixmap; // Magenta Scope Pixmap QPixmap magentaScopePixmap; // Floating toolbar QToolBar *floatingToolBar { nullptr }; QAction *centerTelescopeAction { nullptr }; QAction *toggleEQGridAction { nullptr }; QAction *toggleObjectsAction { nullptr }; QAction *toggleStarsAction { nullptr }; QAction *toggleProfileAction { nullptr }; //Star Profile Viewer #ifdef HAVE_DATAVISUALIZATION StarProfileViewer *starProfileWidget = nullptr; #endif signals: void newStatus(const QString &msg, FITSBar id); void debayerToggled(bool); void wcsToggled(bool); void actionUpdated(const QString &name, bool enable); void trackingStarSelected(int x, int y); void loaded(); void failed(); friend class FITSLabel; }; diff --git a/kstars/fitsviewer/fitsviewer.cpp b/kstars/fitsviewer/fitsviewer.cpp index 7ce3fd630..57d3e7f1d 100644 --- a/kstars/fitsviewer/fitsviewer.cpp +++ b/kstars/fitsviewer/fitsviewer.cpp @@ -1,942 +1,946 @@ /*************************************************************************** FITSViewer.cpp - A FITSViewer for KStars ------------------- begin : Thu Jan 22 2004 copyright : (C) 2004 by Jasem Mutlaq email : mutlaqja@ikarustech.com 2006-03-03 Using CFITSIO, Porting to Qt4 ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "fitsviewer.h" #include "config-kstars.h" #include "fitsdata.h" #include "fitsdebayer.h" #include "fitstab.h" #include "fitsview.h" #include "kstars.h" #include "ksutils.h" #include "Options.h" #ifdef HAVE_INDI #include "indi/indilistener.h" #endif #include #include #include #include +#ifndef KSTARS_LITE +#include "fitshistogram.h" +#endif + #include #define INITIAL_W 785 #define INITIAL_H 640 QStringList FITSViewer::filterTypes = QStringList() << I18N_NOOP("Auto Stretch") << I18N_NOOP("High Contrast") << I18N_NOOP("Equalize") << I18N_NOOP("High Pass") << I18N_NOOP("Median") << I18N_NOOP("Rotate Right") << I18N_NOOP("Rotate Left") << I18N_NOOP("Flip Horizontal") << I18N_NOOP("Flip Vertical"); FITSViewer::FITSViewer(QWidget *parent) : KXmlGuiWindow(parent) { #ifdef Q_OS_OSX if (Options::independentWindowFITS()) setWindowFlags(Qt::Window); else { setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); connect(QApplication::instance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(changeAlwaysOnTop(Qt::ApplicationState))); } #endif fitsTabWidget = new QTabWidget(this); undoGroup = new QUndoGroup(this); lastURL = QUrl(QDir::homePath()); fitsTabWidget->setTabsClosable(true); setWindowIcon(QIcon::fromTheme("kstars_fitsviewer")); setCentralWidget(fitsTabWidget); connect(fitsTabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabFocusUpdated(int))); connect(fitsTabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(closeTab(int))); //These two connections will enable or disable the scope button if a scope is available or not. //Of course this is also dependent on the presence of WCS data in the image. #ifdef HAVE_INDI connect(INDIListener::Instance(), SIGNAL(newTelescope(ISD::GDInterface*)), this, SLOT(updateWCSFunctions())); connect(INDIListener::Instance(), SIGNAL(deviceRemoved(ISD::GDInterface*)), this, SLOT(updateWCSFunctions())); #endif led.setColor(Qt::green); fitsPosition.setAlignment(Qt::AlignCenter); fitsValue.setAlignment(Qt::AlignCenter); //fitsPosition.setFixedWidth(100); //fitsValue.setFixedWidth(100); fitsWCS.setVisible(false); statusBar()->insertPermanentWidget(FITS_WCS, &fitsWCS); statusBar()->insertPermanentWidget(FITS_VALUE, &fitsValue); statusBar()->insertPermanentWidget(FITS_POSITION, &fitsPosition); statusBar()->insertPermanentWidget(FITS_ZOOM, &fitsZoom); statusBar()->insertPermanentWidget(FITS_RESOLUTION, &fitsResolution); statusBar()->insertPermanentWidget(FITS_LED, &led); QAction *action = actionCollection()->addAction("rotate_right", this, SLOT(rotateCW())); action->setText(i18n("Rotate Right")); action->setIcon(QIcon::fromTheme("object-rotate-right")); action = actionCollection()->addAction("rotate_left", this, SLOT(rotateCCW())); action->setText(i18n("Rotate Left")); action->setIcon(QIcon::fromTheme("object-rotate-left")); action = actionCollection()->addAction("flip_horizontal", this, SLOT(flipHorizontal())); action->setText(i18n("Flip Horizontal")); action->setIcon( QIcon::fromTheme("object-flip-horizontal")); action = actionCollection()->addAction("flip_vertical", this, SLOT(flipVertical())); action->setText(i18n("Flip Vertical")); action->setIcon(QIcon::fromTheme("object-flip-vertical")); action = actionCollection()->addAction("image_histogram"); action->setText(i18n("Histogram")); connect(action, SIGNAL(triggered(bool)), SLOT(histoFITS())); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_T)); action->setIcon(QIcon(":/icons/histogram.png")); action = KStandardAction::open(this, SLOT(openFile()), actionCollection()); action->setIcon(QIcon::fromTheme("document-open")); saveFileAction = KStandardAction::save(this, SLOT(saveFile()), actionCollection()); saveFileAction->setIcon(QIcon::fromTheme("document-save")); saveFileAsAction = KStandardAction::saveAs(this, SLOT(saveFileAs()), actionCollection()); saveFileAsAction->setIcon( QIcon::fromTheme("document-save_as")); action = actionCollection()->addAction("fits_header"); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_H)); action->setIcon(QIcon::fromTheme("document-properties")); action->setText(i18n("FITS Header")); connect(action, SIGNAL(triggered(bool)), SLOT(headerFITS())); action = actionCollection()->addAction("fits_debayer"); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_D)); action->setIcon(QIcon::fromTheme("view-preview")); action->setText(i18n("Debayer...")); connect(action, SIGNAL(triggered(bool)), SLOT(debayerFITS())); action = actionCollection()->addAction("image_stretch"); action->setText(i18n("Auto stretch")); connect(action, SIGNAL(triggered(bool)), SLOT(stretchFITS())); actionCollection()->setDefaultShortcut(action, QKeySequence::SelectAll); action->setIcon(QIcon::fromTheme("transform-move")); action = KStandardAction::close(this, SLOT(close()), actionCollection()); action->setIcon(QIcon::fromTheme("window-close")); action = KStandardAction::copy(this, SLOT(copyFITS()), actionCollection()); action->setIcon(QIcon::fromTheme("edit-copy")); action = KStandardAction::zoomIn(this, SLOT(ZoomIn()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-in")); action = KStandardAction::zoomOut(this, SLOT(ZoomOut()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-out")); action = KStandardAction::actualSize(this, SLOT(ZoomDefault()), actionCollection()); action->setIcon(QIcon::fromTheme("zoom-fit-best")); QAction *kundo = KStandardAction::undo(undoGroup, SLOT(undo()), actionCollection()); kundo->setIcon(QIcon::fromTheme("edit-undo")); QAction *kredo = KStandardAction::redo(undoGroup, SLOT(redo()), actionCollection()); kredo->setIcon(QIcon::fromTheme("edit-redo")); connect(undoGroup, SIGNAL(canUndoChanged(bool)), kundo, SLOT(setEnabled(bool))); connect(undoGroup, SIGNAL(canRedoChanged(bool)), kredo, SLOT(setEnabled(bool))); action = actionCollection()->addAction("image_stats"); action->setIcon(QIcon::fromTheme("view-statistics")); action->setText(i18n("Statistics")); connect(action, SIGNAL(triggered(bool)), SLOT(statFITS())); action = actionCollection()->addAction("view_crosshair"); action->setIcon(QIcon::fromTheme("crosshairs")); action->setText(i18n("Show Cross Hairs")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleCrossHair())); action = actionCollection()->addAction("view_pixel_grid"); action->setIcon(QIcon::fromTheme("map-flat")); action->setText(i18n("Show Pixel Gridlines")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(togglePixelGrid())); action = actionCollection()->addAction("view_eq_grid"); action->setIcon(QIcon::fromTheme("kstars_grid")); action->setText(i18n("Show Equatorial Gridlines")); action->setCheckable(true); action->setDisabled(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleEQGrid())); action = actionCollection()->addAction("view_objects"); action->setIcon(QIcon::fromTheme("help-hint")); action->setText(i18n("Show Objects in Image")); action->setCheckable(true); action->setDisabled(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggleObjects())); action = actionCollection()->addAction("center_telescope"); action->setIcon(QIcon(":/icons/center_telescope.svg")); action->setText(i18n("Center Telescope\n*No Telescopes Detected*")); action->setDisabled(true); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(centerTelescope())); action = actionCollection()->addAction("view_zoom_fit"); action->setIcon(QIcon::fromTheme("zoom-fit-width")); action->setText(i18n("Zoom To Fit")); connect(action, SIGNAL(triggered(bool)), SLOT(ZoomToFit())); #ifdef HAVE_DATAVISUALIZATION action = actionCollection()->addAction("toggle_3D_graph"); action->setIcon(QIcon::fromTheme("star_profile", QIcon(":/icons/star_profile.svg"))); action->setText(i18n("View 3D Graph")); action->setCheckable(true); connect(action, SIGNAL(triggered(bool)), SLOT(toggle3DGraph())); #endif action = actionCollection()->addAction("mark_stars"); action->setText(i18n("Mark Stars")); connect(action, SIGNAL(triggered(bool)), SLOT(toggleStars())); QSignalMapper *filterMapper = new QSignalMapper(this); int filterCounter = 1; for (auto& filter : FITSViewer::filterTypes) { action = actionCollection()->addAction(QString("filter%1").arg(filterCounter)); action->setText(i18n(filter.toUtf8().constData())); filterMapper->setMapping(action, filterCounter++); connect(action, SIGNAL(triggered()), filterMapper, SLOT(map())); } connect(filterMapper, SIGNAL(mapped(int)), this, SLOT(applyFilter(int))); /* Create GUI */ createGUI("fitsviewerui.rc"); setWindowTitle(i18n("KStars FITS Viewer")); /* initially resize in accord with KDE rules */ show(); resize(INITIAL_W, INITIAL_H); } void FITSViewer::changeAlwaysOnTop(Qt::ApplicationState state) { if (isVisible()) { if (state == Qt::ApplicationActive) setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); else setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint); show(); } } FITSViewer::~FITSViewer() { // if (KStars::Instance()) // { // for (QPointer fv : KStars::Instance()->getFITSViewersList()) // { // if (fv.data() == this) // { // KStars::Instance()->getFITSViewersList().removeOne(this); // break; // } // } // } fitsTabWidget->disconnect(); qDeleteAll(fitsTabs); fitsTabs.clear(); } void FITSViewer::closeEvent(QCloseEvent * /*event*/) { KStars *ks = KStars::Instance(); if (ks) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); QList viewers = KStars::Instance()->findChildren(); if (a && viewers.count() == 1) { a->setEnabled(false); a->setChecked(false); } } } void FITSViewer::hideEvent(QHideEvent * /*event*/) { KStars *ks = KStars::Instance(); if (ks) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); if (a) { QList viewers = KStars::Instance()->findChildren(); if (viewers.count() <= 1) a->setChecked(false); } } } void FITSViewer::showEvent(QShowEvent * /*event*/) { QAction *a = KStars::Instance()->actionCollection()->action("show_fits_viewer"); if (a) { a->setEnabled(true); a->setChecked(true); } } void FITSViewer::addFITS(const QUrl &imageName, FITSMode mode, FITSScale filter, const QString &previewText, bool silent) { led.setColor(Qt::yellow); QApplication::setOverrideCursor(Qt::WaitCursor); FITSTab *tab = new FITSTab(this); connect(tab, &FITSTab::failed, [&]() { QApplication::restoreOverrideCursor(); led.setColor(Qt::red); if (fitsTabs.size() == 0) { // Close FITS Viewer and let KStars know it is no longer needed in memory. close(); } emit failed(); }); connect(tab, &FITSTab::loaded, [=]() { int tabIndex = fitsTabWidget->indexOf(tab); if (tabIndex != -1) return; lastURL = QUrl(imageName.url(QUrl::RemoveFilename)); QApplication::restoreOverrideCursor(); tab->setPreviewText(previewText); // Connect tab signals connect(tab, &FITSTab::newStatus, this, &FITSViewer::updateStatusBar); connect(tab, &FITSTab::changeStatus, this, &FITSViewer::updateTabStatus); connect(tab, &FITSTab::debayerToggled, this, &FITSViewer::setDebayerAction); // Connect tab view signals connect(tab->getView(), &FITSView::actionUpdated, this, &FITSViewer::updateAction); connect(tab->getView(), &FITSView::wcsToggled, this, &FITSViewer::updateWCSFunctions); switch (mode) { case FITS_NORMAL: fitsTabWidget->addTab(tab, previewText.isEmpty() ? imageName.fileName() : previewText); break; case FITS_CALIBRATE: fitsTabWidget->addTab(tab, i18n("Calibrate")); break; case FITS_FOCUS: fitsTabWidget->addTab(tab, i18n("Focus")); break; case FITS_GUIDE: fitsTabWidget->addTab(tab, i18n("Guide")); break; case FITS_ALIGN: fitsTabWidget->addTab(tab, i18n("Align")); break; } saveFileAction->setEnabled(true); saveFileAsAction->setEnabled(true); undoGroup->addStack(tab->getUndoStack()); fitsTabs.push_back(tab); fitsMap[fitsID] = tab; fitsTabWidget->setCurrentWidget(tab); actionCollection()->action("fits_debayer")->setEnabled(tab->getView()->getImageData()->hasDebayer()); tab->tabPositionUpdated(); tab->setUID(fitsID); led.setColor(Qt::green); updateStatusBar(i18n("Ready."), FITS_MESSAGE); tab->getView()->setCursorMode(FITSView::dragCursor); updateWCSFunctions(); emit loaded(fitsID++); }); tab->loadFITS(imageName, mode, filter, silent); } bool FITSViewer::removeFITS(int fitsUID) { FITSTab *tab = fitsMap.value(fitsUID); if (tab == nullptr) { qCWarning(KSTARS_FITS) << "Cannot find tab with UID " << fitsUID << " in the FITS Viewer"; return false; } int index = fitsTabs.indexOf(tab); if (index >= 0) { closeTab(index); return true; } return false; } void FITSViewer::updateFITS(const QUrl &imageName, int fitsUID, FITSScale filter, bool silent) { FITSTab *tab = fitsMap.value(fitsUID); if (tab == nullptr) { qCWarning(KSTARS_FITS) << "Cannot find tab with UID " << fitsUID << " in the FITS Viewer"; emit failed(); return; } if (tab->isVisible()) led.setColor(Qt::yellow); // On tab load success auto conn = std::make_shared(); *conn = connect(tab, &FITSTab::loaded, this, [=]() { int tabIndex = fitsTabWidget->indexOf(tab); if (tabIndex == -1) return; if (tab->getView()->getMode() == FITS_NORMAL) { if ((imageName.path().startsWith(QLatin1String("/tmp")) || imageName.path().contains("/Temp")) && Options::singlePreviewFITS()) fitsTabWidget->setTabText(tabIndex, tab->getPreviewText().isEmpty() ? i18n("Preview") : tab->getPreviewText()); else fitsTabWidget->setTabText(tabIndex, imageName.fileName()); } tab->getUndoStack()->clear(); if (tab->isVisible()) led.setColor(Qt::green); QObject::disconnect(*conn); emit loaded(tabIndex); }); tab->loadFITS(imageName, tab->getView()->getMode(), filter, silent); } void FITSViewer::tabFocusUpdated(int currentIndex) { if (currentIndex < 0 || fitsTabs.empty()) return; fitsTabs[currentIndex]->tabPositionUpdated(); FITSView *view = fitsTabs[currentIndex]->getView(); view->toggleStars(markStars); if (isVisible()) view->updateFrame(); if (markStars) updateStatusBar(i18np("%1 star detected.", "%1 stars detected.", view->getImageData()->getDetectedStars()), FITS_MESSAGE); else updateStatusBar("", FITS_MESSAGE); if (view->getImageData()->hasDebayer()) { actionCollection()->action("fits_debayer")->setEnabled(true); if (debayerDialog) { BayerParams param; view->getImageData()->getBayerParams(¶m); debayerDialog->setBayerParams(¶m); } } else actionCollection()->action("fits_debayer")->setEnabled(false); updateStatusBar("", FITS_WCS); updateButtonStatus("toggle_3D_graph", "View 3D Graph", getCurrentView()->isStarProfileShown()); updateButtonStatus("view_crosshair", "Cross Hairs", getCurrentView()->isCrosshairShown()); updateButtonStatus("view_eq_grid", "Equatorial Gridines", getCurrentView()->isEQGridShown()); updateButtonStatus("view_objects", "Objects in Image", getCurrentView()->areObjectsShown()); updateButtonStatus("view_pixel_grid", "Pixel Gridines", getCurrentView()->isPixelGridShown()); updateScopeButton(); updateWCSFunctions(); } void FITSViewer::openFile() { QUrl fileURL = QFileDialog::getOpenFileUrl(KStars::Instance(), i18n("Open FITS Image"), lastURL, "FITS (*.fits *.fit)"); if (fileURL.isEmpty()) return; lastURL = QUrl(fileURL.url(QUrl::RemoveFilename)); QString fpath = fileURL.toLocalFile(); QString cpath; // Make sure we don't have it open already, if yes, switch to it foreach (FITSTab *tab, fitsTabs) { cpath = tab->getCurrentURL()->path(); if (fpath == cpath) { fitsTabWidget->setCurrentWidget(tab); return; } } addFITS(fileURL, FITS_NORMAL, FITS_NONE, QString(), false); } void FITSViewer::saveFile() { fitsTabs[fitsTabWidget->currentIndex()]->saveFile(); } void FITSViewer::saveFileAs() { if (fitsTabs.empty()) return; if (fitsTabs[fitsTabWidget->currentIndex()]->saveFileAs() && fitsTabs[fitsTabWidget->currentIndex()]->getView()->getMode() == FITS_NORMAL) fitsTabWidget->setTabText(fitsTabWidget->currentIndex(), fitsTabs[fitsTabWidget->currentIndex()]->getCurrentURL()->fileName()); } void FITSViewer::copyFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->copyFITS(); } void FITSViewer::histoFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->histoFITS(); } void FITSViewer::statFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->statFITS(); } void FITSViewer::stretchFITS() { applyFilter(FITS_AUTO_STRETCH); } void FITSViewer::rotateCW() { applyFilter(FITS_ROTATE_CW); } void FITSViewer::rotateCCW() { applyFilter(FITS_ROTATE_CCW); } void FITSViewer::flipHorizontal() { applyFilter(FITS_FLIP_H); } void FITSViewer::flipVertical() { applyFilter(FITS_FLIP_V); } void FITSViewer::headerFITS() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->headerFITS(); } void FITSViewer::debayerFITS() { if (debayerDialog == nullptr) { debayerDialog = new FITSDebayer(this); } FITSView *view = getCurrentView(); if (view == nullptr) return; BayerParams param; view->getImageData()->getBayerParams(¶m); debayerDialog->setBayerParams(¶m); debayerDialog->show(); } void FITSViewer::updateStatusBar(const QString &msg, FITSBar id) { switch (id) { case FITS_POSITION: fitsPosition.setText(msg); break; case FITS_RESOLUTION: fitsResolution.setText(msg); break; case FITS_ZOOM: fitsZoom.setText(msg); break; case FITS_WCS: fitsWCS.setVisible(true); fitsWCS.setText(msg); break; case FITS_VALUE: fitsValue.setText(msg); break; case FITS_MESSAGE: statusBar()->showMessage(msg); break; default: break; } } void FITSViewer::ZoomIn() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomIn(); } void FITSViewer::ZoomOut() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomOut(); } void FITSViewer::ZoomDefault() { if (fitsTabs.empty()) return; fitsTabs[fitsTabWidget->currentIndex()]->ZoomDefault(); } void FITSViewer::ZoomToFit() { if (fitsTabs.empty()) return; getCurrentView()->ZoomToFit(); } void FITSViewer::updateAction(const QString &name, bool enable) { QAction *toolAction = actionCollection()->action(name); if (toolAction != nullptr) toolAction->setEnabled(enable); } void FITSViewer::updateTabStatus(bool clean) { if (fitsTabs.empty() || (fitsTabWidget->currentIndex() >= fitsTabs.size())) return; if (fitsTabs[fitsTabWidget->currentIndex()]->getView()->getMode() != FITS_NORMAL) return; //QString tabText = fitsImages[fitsTab->currentIndex()]->getCurrentURL()->fileName(); QString tabText = fitsTabWidget->tabText(fitsTabWidget->currentIndex()); fitsTabWidget->setTabText(fitsTabWidget->currentIndex(), clean ? tabText.remove('*') : tabText + '*'); } void FITSViewer::closeTab(int index) { if (fitsTabs.empty()) return; FITSTab *tab = fitsTabs[index]; fitsMap.remove(tab->getUID()); fitsTabs.removeOne(tab); delete tab; if (fitsTabs.empty()) { saveFileAction->setEnabled(false); saveFileAsAction->setEnabled(false); } } /** This is helper function to make it really easy to make the update the state of toggle buttons that either show or hide information in the Current view. This method would get called both when one of them gets pushed and also when tabs are switched. */ void FITSViewer::updateButtonStatus(const QString& action, const QString& item, bool showing) { QAction *a = actionCollection()->action(action); if (a == nullptr) return; if (showing) { a->setText("Hide " + item); a->setChecked(true); } else { a->setText("Show " + item); a->setChecked(false); } } /** This is a method that either enables or disables the WCS based features in the Current View. */ void FITSViewer::updateWCSFunctions() { if (getCurrentView() == nullptr) return; if (getCurrentView()->imageHasWCS()) { actionCollection()->action("view_eq_grid")->setDisabled(false); actionCollection()->action("view_eq_grid")->setText(i18n("Show Equatorial Gridlines")); actionCollection()->action("view_objects")->setDisabled(false); actionCollection()->action("view_objects")->setText(i18n("Show Objects in Image")); if (getCurrentView()->isTelescopeActive()) { actionCollection()->action("center_telescope")->setDisabled(false); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*Ready*")); } else { actionCollection()->action("center_telescope")->setDisabled(true); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*No Telescopes Detected*")); } } else { actionCollection()->action("view_eq_grid")->setDisabled(true); actionCollection()->action("view_eq_grid")->setText(i18n("Show Equatorial Gridlines\n*No WCS Info*")); actionCollection()->action("center_telescope")->setDisabled(true); actionCollection()->action("center_telescope")->setText(i18n("Center Telescope\n*No WCS Info*")); actionCollection()->action("view_objects")->setDisabled(true); actionCollection()->action("view_objects")->setText(i18n("Show Objects in Image\n*No WCS Info*")); } } void FITSViewer::updateScopeButton() { if (getCurrentView()->getCursorMode() == FITSView::scopeCursor) { actionCollection()->action("center_telescope")->setChecked(true); } else { actionCollection()->action("center_telescope")->setChecked(false); } } /** This methood either enables or disables the scope mouse mode so you can slew your scope to coordinates just by clicking the mouse on a spot in the image. */ void FITSViewer::centerTelescope() { getCurrentView()->setScopeButton(actionCollection()->action("center_telescope")); if (getCurrentView()->getCursorMode() == FITSView::scopeCursor) { getCurrentView()->setCursorMode(getCurrentView()->lastMouseMode); } else { getCurrentView()->lastMouseMode = getCurrentView()->getCursorMode(); getCurrentView()->setCursorMode(FITSView::scopeCursor); } updateScopeButton(); } void FITSViewer::toggleCrossHair() { if (fitsTabs.empty()) return; getCurrentView()->toggleCrosshair(); updateButtonStatus("view_crosshair", "Cross Hairs", getCurrentView()->isCrosshairShown()); } void FITSViewer::toggleEQGrid() { if (fitsTabs.empty()) return; getCurrentView()->toggleEQGrid(); updateButtonStatus("view_eq_grid", "Equatorial Gridines", getCurrentView()->isEQGridShown()); } void FITSViewer::toggleObjects() { if (fitsTabs.empty()) return; getCurrentView()->toggleObjects(); updateButtonStatus("view_objects", "Objects in Image", getCurrentView()->areObjectsShown()); } void FITSViewer::togglePixelGrid() { if (fitsTabs.empty()) return; getCurrentView()->togglePixelGrid(); updateButtonStatus("view_pixel_grid", "Pixel Gridines", getCurrentView()->isPixelGridShown()); } void FITSViewer::toggle3DGraph() { if (fitsTabs.empty()) return; getCurrentView()->toggleStarProfile(); updateButtonStatus("toggle_3D_graph", "View 3D Graph", getCurrentView()->isStarProfileShown()); } void FITSViewer::toggleStars() { if (markStars) { markStars = false; actionCollection()->action("mark_stars")->setText(i18n("Mark Stars")); } else { markStars = true; actionCollection()->action("mark_stars")->setText(i18n("Unmark Stars")); } foreach (FITSTab *tab, fitsTabs) { tab->getView()->toggleStars(markStars); tab->getView()->updateFrame(); } } void FITSViewer::applyFilter(int ftype) { if (fitsTabs.empty()) return; QApplication::setOverrideCursor(Qt::WaitCursor); updateStatusBar(i18n("Processing %1...", filterTypes[ftype - 1]), FITS_MESSAGE); qApp->processEvents(); fitsTabs[fitsTabWidget->currentIndex()]->getHistogram()->applyFilter(static_cast(ftype)); qApp->processEvents(); fitsTabs[fitsTabWidget->currentIndex()]->getView()->updateFrame(); QApplication::restoreOverrideCursor(); updateStatusBar(i18n("Ready."), FITS_MESSAGE); } FITSView *FITSViewer::getView(int fitsUID) { FITSTab *tab = fitsMap.value(fitsUID); if (tab) return tab->getView(); return nullptr; } FITSView *FITSViewer::getCurrentView() { if (fitsTabs.empty() || fitsTabWidget->currentIndex() >= fitsTabs.count()) return nullptr; return fitsTabs[fitsTabWidget->currentIndex()]->getView(); } void FITSViewer::setDebayerAction(bool enable) { actionCollection()->addAction("fits_debayer")->setEnabled(enable); } diff --git a/kstars/indi/indiccd.cpp b/kstars/indi/indiccd.cpp index aa997b7a8..50f8ccc0c 100644 --- a/kstars/indi/indiccd.cpp +++ b/kstars/indi/indiccd.cpp @@ -1,2338 +1,2340 @@ /* INDI CCD Copyright (C) 2012 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "indiccd.h" #include "config-kstars.h" #include "indi_debug.h" #include "clientmanager.h" #include "driverinfo.h" #include "guimanager.h" #include "kspaths.h" #include "kstars.h" #include "kstarsdata.h" #include "Options.h" #include "streamwg.h" //#include "ekos/manager.h" #ifdef HAVE_CFITSIO #include "fitsviewer/fitsdata.h" #endif #include +#include +#include #include #ifdef HAVE_LIBRAW #include #endif const QStringList RAWFormats = { "cr2", "crw", "nef", "raf", "dng", "arw" }; namespace ISD { CCDChip::CCDChip(ISD::CCD *ccd, ChipType cType) { baseDevice = ccd->getBaseDevice(); clientManager = ccd->getDriverInfo()->getClientManager(); parentCCD = ccd; type = cType; } FITSView *CCDChip::getImageView(FITSMode imageType) { switch (imageType) { case FITS_NORMAL: return normalImage; case FITS_FOCUS: return focusImage; case FITS_GUIDE: return guideImage; case FITS_CALIBRATE: return calibrationImage; case FITS_ALIGN: return alignImage; } return nullptr; } void CCDChip::setImageView(FITSView *image, FITSMode imageType) { switch (imageType) { case FITS_NORMAL: normalImage = image; break; case FITS_FOCUS: focusImage = image; break; case FITS_GUIDE: guideImage = image; break; case FITS_CALIBRATE: calibrationImage = image; break; case FITS_ALIGN: alignImage = image; break; } if (image) imageData = image->getImageData(); } bool CCDChip::getFrameMinMax(int *minX, int *maxX, int *minY, int *maxY, int *minW, int *maxW, int *minH, int *maxH) { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *arg = IUFindNumber(frameProp, "X"); if (arg == nullptr) return false; if (minX) *minX = arg->min; if (maxX) *maxX = arg->max; arg = IUFindNumber(frameProp, "Y"); if (arg == nullptr) return false; if (minY) *minY = arg->min; if (maxY) *maxY = arg->max; arg = IUFindNumber(frameProp, "WIDTH"); if (arg == nullptr) return false; if (minW) *minW = arg->min; if (maxW) *maxW = arg->max; arg = IUFindNumber(frameProp, "HEIGHT"); if (arg == nullptr) return false; if (minH) *minH = arg->min; if (maxH) *maxH = arg->max; return true; } bool CCDChip::setImageInfo(uint16_t width, uint16_t height, double pixelX, double pixelY, uint8_t bitdepth) { INumberVectorProperty *ccdInfoProp = nullptr; switch (type) { case PRIMARY_CCD: ccdInfoProp = baseDevice->getNumber("CCD_INFO"); break; case GUIDE_CCD: ccdInfoProp = baseDevice->getNumber("GUIDER_INFO"); break; } if (ccdInfoProp == nullptr) return false; ccdInfoProp->np[0].value = width; ccdInfoProp->np[1].value = height; ccdInfoProp->np[2].value = std::hypotf(pixelX, pixelY); ccdInfoProp->np[3].value = pixelX; ccdInfoProp->np[4].value = pixelY; ccdInfoProp->np[5].value = bitdepth; clientManager->sendNewNumber(ccdInfoProp); return true; } bool CCDChip::getPixelSize(double &x, double &y) { INumberVectorProperty *ccdInfoProp = nullptr; switch (type) { case PRIMARY_CCD: ccdInfoProp = baseDevice->getNumber("CCD_INFO"); break; case GUIDE_CCD: ccdInfoProp = baseDevice->getNumber("GUIDER_INFO"); break; } if (ccdInfoProp == nullptr) return false; INumber *pixelX = IUFindNumber(ccdInfoProp, "CCD_PIXEL_SIZE_X"); INumber *pixelY = IUFindNumber(ccdInfoProp, "CCD_PIXEL_SIZE_Y"); if (pixelX == nullptr || pixelY == nullptr) return false; x = pixelX->value; y = pixelY->value; return true; } bool CCDChip::getFrame(int *x, int *y, int *w, int *h) { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *arg = IUFindNumber(frameProp, "X"); if (arg == nullptr) return false; *x = arg->value; arg = IUFindNumber(frameProp, "Y"); if (arg == nullptr) return false; *y = arg->value; arg = IUFindNumber(frameProp, "WIDTH"); if (arg == nullptr) return false; *w = arg->value; arg = IUFindNumber(frameProp, "HEIGHT"); if (arg == nullptr) return false; *h = arg->value; return true; } bool CCDChip::resetFrame() { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (xarg->value == xarg->min && yarg->value == yarg->min && warg->value == warg->max && harg->value == harg->max) return false; xarg->value = xarg->min; yarg->value = yarg->min; warg->value = warg->max; harg->value = harg->max; clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCDChip::setFrame(int x, int y, int w, int h) { INumberVectorProperty *frameProp = nullptr; switch (type) { case PRIMARY_CCD: frameProp = baseDevice->getNumber("CCD_FRAME"); break; case GUIDE_CCD: frameProp = baseDevice->getNumber("GUIDER_FRAME"); break; } if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (xarg->value == x && yarg->value == y && warg->value == w && harg->value == h) return true; xarg->value = x; yarg->value = y; warg->value = w; harg->value = h; clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCDChip::capture(double exposure) { INumberVectorProperty *expProp = nullptr; switch (type) { case PRIMARY_CCD: expProp = baseDevice->getNumber("CCD_EXPOSURE"); break; case GUIDE_CCD: expProp = baseDevice->getNumber("GUIDER_EXPOSURE"); break; } if (expProp == nullptr) return false; expProp->np[0].value = exposure; clientManager->sendNewNumber(expProp); return true; } bool CCDChip::abortExposure() { ISwitchVectorProperty *abortProp = nullptr; switch (type) { case PRIMARY_CCD: abortProp = baseDevice->getSwitch("CCD_ABORT_EXPOSURE"); break; case GUIDE_CCD: abortProp = baseDevice->getSwitch("GUIDER_ABORT_EXPOSURE"); break; } if (abortProp == nullptr) return false; ISwitch *abort = IUFindSwitch(abortProp, "ABORT"); if (abort == nullptr) return false; abort->s = ISS_ON; //captureMode = FITS_NORMAL; clientManager->sendNewSwitch(abortProp); return true; } bool CCDChip::canBin() const { return CanBin; } void CCDChip::setCanBin(bool value) { CanBin = value; } bool CCDChip::canSubframe() const { return CanSubframe; } void CCDChip::setCanSubframe(bool value) { CanSubframe = value; } bool CCDChip::canAbort() const { return CanAbort; } void CCDChip::setCanAbort(bool value) { CanAbort = value; } FITSData *CCDChip::getImageData() const { return imageData; } int CCDChip::getISOIndex() const { ISwitchVectorProperty *isoProp = baseDevice->getSwitch("CCD_ISO"); if (isoProp == nullptr) return -1; return IUFindOnSwitchIndex(isoProp); } bool CCDChip::setISOIndex(int value) { ISwitchVectorProperty *isoProp = baseDevice->getSwitch("CCD_ISO"); if (isoProp == nullptr) return false; IUResetSwitch(isoProp); isoProp->sp[value].s = ISS_ON; clientManager->sendNewSwitch(isoProp); return true; } QStringList CCDChip::getISOList() const { QStringList isoList; ISwitchVectorProperty *isoProp = baseDevice->getSwitch("CCD_ISO"); if (isoProp == nullptr) return isoList; for (int i = 0; i < isoProp->nsp; i++) isoList << isoProp->sp[i].label; return isoList; } bool CCDChip::isCapturing() { INumberVectorProperty *expProp = nullptr; switch (type) { case PRIMARY_CCD: expProp = baseDevice->getNumber("CCD_EXPOSURE"); break; case GUIDE_CCD: expProp = baseDevice->getNumber("GUIDER_EXPOSURE"); break; } if (expProp == nullptr) return false; return (expProp->s == IPS_BUSY); } bool CCDChip::setFrameType(const QString &name) { CCDFrameType fType = FRAME_LIGHT; if (name == "FRAME_LIGHT" || name == "Light") fType = FRAME_LIGHT; else if (name == "FRAME_DARK" || name == "Dark") fType = FRAME_DARK; else if (name == "FRAME_BIAS" || name == "Bias") fType = FRAME_BIAS; else if (name == "FRAME_FLAT" || name == "Flat") fType = FRAME_FLAT; else { qCWarning(KSTARS_INDI) << name << " frame type is unknown." ; return false; } return setFrameType(fType); } bool CCDChip::setFrameType(CCDFrameType fType) { ISwitchVectorProperty *frameProp = nullptr; if (type == PRIMARY_CCD) frameProp = baseDevice->getSwitch("CCD_FRAME_TYPE"); else frameProp = baseDevice->getSwitch("GUIDER_FRAME_TYPE"); if (frameProp == nullptr) return false; ISwitch *ccdFrame = nullptr; if (fType == FRAME_LIGHT) ccdFrame = IUFindSwitch(frameProp, "FRAME_LIGHT"); else if (fType == FRAME_DARK) ccdFrame = IUFindSwitch(frameProp, "FRAME_DARK"); else if (fType == FRAME_BIAS) ccdFrame = IUFindSwitch(frameProp, "FRAME_BIAS"); else if (fType == FRAME_FLAT) ccdFrame = IUFindSwitch(frameProp, "FRAME_FLAT"); if (ccdFrame == nullptr) return false; if (ccdFrame->s == ISS_ON) return true; if (fType != FRAME_LIGHT) captureMode = FITS_CALIBRATE; IUResetSwitch(frameProp); ccdFrame->s = ISS_ON; clientManager->sendNewSwitch(frameProp); return true; } CCDFrameType CCDChip::getFrameType() { CCDFrameType fType = FRAME_LIGHT; ISwitchVectorProperty *frameProp = nullptr; if (type == PRIMARY_CCD) frameProp = baseDevice->getSwitch("CCD_FRAME_TYPE"); else frameProp = baseDevice->getSwitch("GUIDER_FRAME_TYPE"); if (frameProp == nullptr) return fType; ISwitch *ccdFrame = nullptr; ccdFrame = IUFindOnSwitch(frameProp); if (ccdFrame == nullptr) { qCWarning(KSTARS_INDI) << "ISD:CCD Cannot find active frame in CCD!"; return fType; } if (!strcmp(ccdFrame->name, "FRAME_LIGHT")) fType = FRAME_LIGHT; else if (!strcmp(ccdFrame->name, "FRAME_DARK")) fType = FRAME_DARK; else if (!strcmp(ccdFrame->name, "FRAME_FLAT")) fType = FRAME_FLAT; else if (!strcmp(ccdFrame->name, "FRAME_BIAS")) fType = FRAME_BIAS; return fType; } bool CCDChip::setBinning(CCDBinType binType) { switch (binType) { case SINGLE_BIN: return setBinning(1, 1); case DOUBLE_BIN: return setBinning(2, 2); case TRIPLE_BIN: return setBinning(3, 3); case QUADRAPLE_BIN: return setBinning(4, 4); } return false; } CCDBinType CCDChip::getBinning() { CCDBinType binType = SINGLE_BIN; INumberVectorProperty *binProp = nullptr; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return binType; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return binType; switch ((int)horBin->value) { case 2: binType = DOUBLE_BIN; break; case 3: binType = TRIPLE_BIN; break; case 4: binType = QUADRAPLE_BIN; break; default: break; } return binType; } bool CCDChip::getBinning(int *bin_x, int *bin_y) { INumberVectorProperty *binProp = nullptr; *bin_x = *bin_y = 1; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return false; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return false; *bin_x = horBin->value; *bin_y = verBin->value; return true; } bool CCDChip::getMaxBin(int *max_xbin, int *max_ybin) { if (!max_xbin || !max_ybin) return false; INumberVectorProperty *binProp = nullptr; *max_xbin = *max_ybin = 1; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return false; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return false; *max_xbin = horBin->max; *max_ybin = verBin->max; return true; } bool CCDChip::setBinning(int bin_x, int bin_y) { INumberVectorProperty *binProp = nullptr; switch (type) { case PRIMARY_CCD: binProp = baseDevice->getNumber("CCD_BINNING"); break; case GUIDE_CCD: binProp = baseDevice->getNumber("GUIDER_BINNING"); break; } if (binProp == nullptr) return false; INumber *horBin = nullptr, *verBin = nullptr; horBin = IUFindNumber(binProp, "HOR_BIN"); verBin = IUFindNumber(binProp, "VER_BIN"); if (!horBin || !verBin) return false; if (horBin->value == bin_x && verBin->value == bin_y) return true; if (bin_x > horBin->max || bin_y > verBin->max) return false; horBin->value = bin_x; verBin->value = bin_y; clientManager->sendNewNumber(binProp); return true; } CCD::CCD(GDInterface *iPtr) : DeviceDecorator(iPtr) { primaryChip.reset(new CCDChip(this, CCDChip::PRIMARY_CCD)); readyTimer.reset(new QTimer()); readyTimer.get()->setInterval(250); readyTimer.get()->setSingleShot(true); connect(readyTimer.get(), &QTimer::timeout, this, &CCD::ready); } CCD::~CCD() { delete fv; } void CCD::registerProperty(INDI::Property *prop) { if (isConnected()) readyTimer.get()->start(); if (!strcmp(prop->getName(), "GUIDER_EXPOSURE")) { HasGuideHead = true; guideChip.reset(new CCDChip(this, CCDChip::GUIDE_CCD)); } else if (!strcmp(prop->getName(), "CCD_FRAME_TYPE")) { ISwitchVectorProperty *ccdFrame = prop->getSwitch(); primaryChip->clearFrameTypes(); for (int i = 0; i < ccdFrame->nsp; i++) primaryChip->addFrameLabel(ccdFrame->sp[i].label); } else if (!strcmp(prop->getName(), "CCD_FRAME")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) primaryChip->setCanSubframe(true); } else if (!strcmp(prop->getName(), "GUIDER_FRAME")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) guideChip->setCanSubframe(true); } else if (!strcmp(prop->getName(), "CCD_BINNING")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) primaryChip->setCanBin(true); } else if (!strcmp(prop->getName(), "GUIDER_BINNING")) { INumberVectorProperty *np = prop->getNumber(); if (np && np->p != IP_RO) guideChip->setCanBin(true); } else if (!strcmp(prop->getName(), "CCD_ABORT_EXPOSURE")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp && sp->p != IP_RO) primaryChip->setCanAbort(true); } else if (!strcmp(prop->getName(), "GUIDER_ABORT_EXPOSURE")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp && sp->p != IP_RO) guideChip->setCanAbort(true); } else if (!strcmp(prop->getName(), "CCD_TEMPERATURE")) { INumberVectorProperty *np = prop->getNumber(); HasCooler = true; CanCool = (np->p != IP_RO); if (np) emit newTemperatureValue(np->np[0].value); } else if (!strcmp(prop->getName(), "CCD_COOLER")) { // Can turn cooling on/off HasCoolerControl = true; } else if (!strcmp(prop->getName(), "CCD_VIDEO_STREAM")) { // Has Video Stream HasVideoStream = true; } else if (!strcmp(prop->getName(), "CCD_TRANSFER_FORMAT")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp) { ISwitch *format = IUFindSwitch(sp, "FORMAT_NATIVE"); if (format && format->s == ISS_ON) transferFormat = FORMAT_NATIVE; else transferFormat = FORMAT_FITS; } } else if (!strcmp(prop->getName(), "CCD_EXPOSURE_LOOP")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp) { ISwitch *looping = IUFindSwitch(sp, "LOOP_ON"); if (looping && looping->s == ISS_ON) IsLooping = true; else IsLooping = false; } } else if (!strcmp(prop->getName(), "TELESCOPE_TYPE")) { ISwitchVectorProperty *sp = prop->getSwitch(); if (sp) { ISwitch *format = IUFindSwitch(sp, "TELESCOPE_PRIMARY"); if (format && format->s == ISS_ON) telescopeType = TELESCOPE_PRIMARY; else telescopeType = TELESCOPE_GUIDE; } } // try to find gain property, if any else if (gainN == nullptr && prop->getType() == INDI_NUMBER) { // Since gain is spread among multiple property depending on the camera providing it // we need to search in all possible number properties INumberVectorProperty *gainNP = prop->getNumber(); if (gainNP) { for (int i = 0; i < gainNP->nnp; i++) { QString name = QString(gainNP->np[i].name).toLower(); QString label = QString(gainNP->np[i].label).toLower(); if (name == "gain" || label == "gain") { gainN = gainNP->np + i; gainPerm = gainNP->p; break; } } } } DeviceDecorator::registerProperty(prop); } void CCD::processLight(ILightVectorProperty *lvp) { DeviceDecorator::processLight(lvp); } void CCD::processNumber(INumberVectorProperty *nvp) { if (!strcmp(nvp->name, "CCD_EXPOSURE")) { INumber *np = IUFindNumber(nvp, "CCD_EXPOSURE_VALUE"); if (np) emit newExposureValue(primaryChip.get(), np->value, nvp->s); } else if (!strcmp(nvp->name, "CCD_TEMPERATURE")) { HasCooler = true; INumber *np = IUFindNumber(nvp, "CCD_TEMPERATURE_VALUE"); if (np) emit newTemperatureValue(np->value); } else if (!strcmp(nvp->name, "GUIDER_EXPOSURE")) { INumber *np = IUFindNumber(nvp, "GUIDER_EXPOSURE_VALUE"); if (np) emit newExposureValue(guideChip.get(), np->value, nvp->s); } else if (!strcmp(nvp->name, "FPS")) { emit newFPS(nvp->np[0].value, nvp->np[1].value); } else if (!strcmp(nvp->name, "CCD_RAPID_GUIDE_DATA")) { double dx = -1, dy = -1, fit = -1; INumber *np = nullptr; if (nvp->s == IPS_ALERT) { emit newGuideStarData(primaryChip.get(), -1, -1, -1); } else { np = IUFindNumber(nvp, "GUIDESTAR_X"); if (np) dx = np->value; np = IUFindNumber(nvp, "GUIDESTAR_Y"); if (np) dy = np->value; np = IUFindNumber(nvp, "GUIDESTAR_FIT"); if (np) fit = np->value; if (dx >= 0 && dy >= 0 && fit >= 0) emit newGuideStarData(primaryChip.get(), dx, dy, fit); } } else if (!strcmp(nvp->name, "GUIDER_RAPID_GUIDE_DATA")) { double dx = -1, dy = -1, fit = -1; INumber *np = nullptr; if (nvp->s == IPS_ALERT) { emit newGuideStarData(guideChip.get(), -1, -1, -1); } else { np = IUFindNumber(nvp, "GUIDESTAR_X"); if (np) dx = np->value; np = IUFindNumber(nvp, "GUIDESTAR_Y"); if (np) dy = np->value; np = IUFindNumber(nvp, "GUIDESTAR_FIT"); if (np) fit = np->value; if (dx >= 0 && dy >= 0 && fit >= 0) emit newGuideStarData(guideChip.get(), dx, dy, fit); } } DeviceDecorator::processNumber(nvp); } void CCD::processSwitch(ISwitchVectorProperty *svp) { if (!strcmp(svp->name, "CCD_COOLER")) { // Can turn cooling on/off HasCoolerControl = true; emit coolerToggled(svp->sp[0].s == ISS_ON); } else if (QString(svp->name).endsWith("VIDEO_STREAM")) { HasVideoStream = true; if (streamWindow.get() == nullptr && svp->sp[0].s == ISS_ON) { streamWindow.reset(new StreamWG(this)); INumberVectorProperty *streamFrame = baseDevice->getNumber("CCD_STREAM_FRAME"); INumber *w = nullptr, *h = nullptr; if (streamFrame) { w = IUFindNumber(streamFrame, "WIDTH"); h = IUFindNumber(streamFrame, "HEIGHT"); } if (w && h) { streamW = w->value; streamH = h->value; } else { // Only use CCD dimensions if we are receing raw stream and not stream of images (i.e. mjpeg..etc) IBLOBVectorProperty *rawBP = baseDevice->getBLOB("CCD1"); if (rawBP) { int x = 0, y = 0, w = 0, h = 0; int binx = 0, biny = 0; primaryChip->getFrame(&x, &y, &w, &h); primaryChip->getBinning(&binx, &biny); streamW = w / binx; streamH = h / biny; } } streamWindow->setSize(streamW, streamH); } if (streamWindow.get() != nullptr) { connect(streamWindow.get(), &StreamWG::hidden, this, &CCD::StreamWindowHidden, Qt::UniqueConnection); connect(streamWindow.get(), &StreamWG::imageChanged, this, &CCD::newVideoFrame, Qt::UniqueConnection); streamWindow->enableStream(svp->sp[0].s == ISS_ON); emit videoStreamToggled(svp->sp[0].s == ISS_ON); } } else if (!strcmp(svp->name, "CCD_TRANSFER_FORMAT")) { ISwitch *format = IUFindSwitch(svp, "FORMAT_NATIVE"); if (format && format->s == ISS_ON) transferFormat = FORMAT_NATIVE; else transferFormat = FORMAT_FITS; } else if (!strcmp(svp->name, "RECORD_STREAM")) { ISwitch *recordOFF = IUFindSwitch(svp, "RECORD_OFF"); if (recordOFF && recordOFF->s == ISS_ON) { emit videoRecordToggled(false); KNotification::event(QLatin1String("RecordingStopped"), i18n("Video Recording Stopped")); } else { emit videoRecordToggled(true); KNotification::event(QLatin1String("RecordingStarted"), i18n("Video Recording Started")); } } else if (!strcmp(svp->name, "TELESCOPE_TYPE")) { ISwitch *format = IUFindSwitch(svp, "TELESCOPE_PRIMARY"); if (format && format->s == ISS_ON) telescopeType = TELESCOPE_PRIMARY; else telescopeType = TELESCOPE_GUIDE; } else if (!strcmp(svp->name, "CCD_EXPOSURE_LOOP")) { ISwitch *looping = IUFindSwitch(svp, "LOOP_ON"); if (looping && looping->s == ISS_ON) IsLooping = true; else IsLooping = false; } else if (!strcmp(svp->name, "CONNECTION")) { ISwitch *dSwitch = IUFindSwitch(svp, "DISCONNECT"); if (dSwitch && dSwitch->s == ISS_ON && streamWindow.get() != nullptr) { streamWindow->enableStream(false); emit videoStreamToggled(false); streamWindow->close(); streamWindow.reset(); } //emit switchUpdated(svp); //return; } DeviceDecorator::processSwitch(svp); } void CCD::processText(ITextVectorProperty *tvp) { if (!strcmp(tvp->name, "CCD_FILE_PATH")) { IText *filepath = IUFindText(tvp, "FILE_PATH"); if (filepath) emit newRemoteFile(QString(filepath->text)); } DeviceDecorator::processText(tvp); } void CCD::processBLOB(IBLOB *bp) { // Ignore write-only BLOBs since we only receive it for state-change if (bp->bvp->p == IP_WO || bp->size == 0) return; BType = BLOB_OTHER; QString format(bp->format); // If stream, process it first if (format.contains("stream") && streamWindow.get() != nullptr) { if (streamWindow->isStreamEnabled() == false) return; INumberVectorProperty *streamFrame = baseDevice->getNumber("CCD_STREAM_FRAME"); INumber *w = nullptr, *h = nullptr; if (streamFrame) { w = IUFindNumber(streamFrame, "WIDTH"); h = IUFindNumber(streamFrame, "HEIGHT"); } if (w && h) { streamW = w->value; streamH = h->value; } else { int x, y, w, h; int binx, biny; primaryChip->getFrame(&x, &y, &w, &h); primaryChip->getBinning(&binx, &biny); streamW = w / binx; streamH = h / biny; /*IBLOBVectorProperty *rawBP = baseDevice->getBLOB("CCD1"); if (rawBP) { rawBP->bp[0].aux0 = &(streamW); rawBP->bp[0].aux1 = &(streamH); }*/ } //if (streamWindow->getStreamWidth() != streamW || streamWindow->getStreamHeight() != streamH) streamWindow->setSize(streamW, streamH); streamWindow->show(); streamWindow->newFrame(bp); return; } QByteArray fmt = QString(bp->format).toLower().remove('.').toUtf8(); // If it's not FITS or an image, don't process it. if ((QImageReader::supportedImageFormats().contains(fmt))) BType = BLOB_IMAGE; else if (format.contains("fits")) BType = BLOB_FITS; else if (RAWFormats.contains(fmt)) BType = BLOB_RAW; if (BType == BLOB_OTHER) { DeviceDecorator::processBLOB(bp); return; } CCDChip *targetChip = nullptr; if (!strcmp(bp->name, "CCD2")) targetChip = guideChip.get(); else targetChip = primaryChip.get(); QString currentDir; if (targetChip->isBatchMode() == false) currentDir = KSPaths::writableLocation(QStandardPaths::TempLocation); else currentDir = fitsDir.isEmpty() ? Options::fitsDir() : fitsDir; int nr, n = 0; QTemporaryFile tmpFile(QDir::tempPath() + "/fitsXXXXXX"); //if (currentDir.endsWith('/')) //currentDir.truncate(currentDir.size()-1); if (QDir(currentDir).exists() == false) QDir().mkpath(currentDir); QString filename(currentDir); if (filename.endsWith('/') == false) filename.append('/'); // Create temporary name if ANY of the following conditions are met: // 1. file is preview or batch mode is not enabled // 2. file type is not FITS_NORMAL (focus, guide..etc) if (targetChip->isBatchMode() == false || targetChip->getCaptureMode() != FITS_NORMAL) { //tmpFile.setPrefix("fits"); tmpFile.setAutoRemove(false); if (!tmpFile.open()) { qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open " << filename; emit BLOBUpdated(nullptr); return; } QDataStream out(&tmpFile); for (nr = 0; nr < (int)bp->size; nr += n) n = out.writeRawData(static_cast(bp->blob) + nr, bp->size - nr); tmpFile.close(); filename = tmpFile.fileName(); } // Create file name for others else { // IS8601 contains colons but they are illegal under Windows OS, so replacing them with '-' // The timestamp is no longer ISO8601 but it should solve interoperality issues between different OS hosts QString ts = QDateTime::currentDateTime().toString("yyyy-MM-ddThh-mm-ss"); if (seqPrefix.contains("_ISO8601")) { QString finalPrefix = seqPrefix; finalPrefix.replace("ISO8601", ts); filename += finalPrefix + QString("_%1.%2").arg(QString().sprintf("%03d", nextSequenceID), QString(fmt)); } else filename += seqPrefix + (seqPrefix.isEmpty() ? "" : "_") + QString("%1.%2").arg(QString().sprintf("%03d", nextSequenceID), QString(fmt)); QFile fits_temp_file(filename); if (!fits_temp_file.open(QIODevice::WriteOnly)) { qCCritical(KSTARS_INDI) << "ISD:CCD Error: Unable to open " << fits_temp_file.fileName(); emit BLOBUpdated(nullptr); return; } QDataStream out(&fits_temp_file); for (nr = 0; nr < (int)bp->size; nr += n) n = out.writeRawData(static_cast(bp->blob) + nr, bp->size - nr); fits_temp_file.close(); } if (BType == BLOB_FITS) addFITSKeywords(filename); // store file name strncpy(BLOBFilename, filename.toLatin1(), MAXINDIFILENAME); bp->aux0 = targetChip; bp->aux1 = &BType; bp->aux2 = BLOBFilename; if (targetChip->getCaptureMode() == FITS_NORMAL && targetChip->isBatchMode() == true) { KStars::Instance()->statusBar()->showMessage(i18n("%1 file saved to %2", QString(fmt).toUpper(), filename), 0); qCInfo(KSTARS_INDI) << QString(fmt).toUpper() << "file saved to" << filename; } // FIXME: Why is this leaking memory in Valgrind??! KNotification::event(QLatin1String("FITSReceived"), i18n("Image file is received")); /*if (targetChip->showFITS() == false && targetChip->getCaptureMode() == FITS_NORMAL) { emit BLOBUpdated(bp); return; }*/ if (BType == BLOB_IMAGE || BType == BLOB_RAW) { if (BType == BLOB_RAW) { #ifdef HAVE_LIBRAW QString rawFileName = filename; rawFileName = rawFileName.remove(0, rawFileName.lastIndexOf(QLatin1Literal("/"))); QString templateName = QString("%1/%2.XXXXXX").arg(QDir::tempPath(), rawFileName); QTemporaryFile imgPreview(templateName); imgPreview.setAutoRemove(false); imgPreview.open(); imgPreview.close(); QString preview_filename = imgPreview.fileName(); int ret = 0; // Creation of image processing object LibRaw RawProcessor; // Let us open the file if ((ret = RawProcessor.open_file(filename.toLatin1().data())) != LIBRAW_SUCCESS) { KStars::Instance()->statusBar()->showMessage( i18n("Cannot open %1: %2", rawFileName, libraw_strerror(ret))); RawProcessor.recycle(); emit BLOBUpdated(bp); return; } // Let us unpack the image /*if( (ret = RawProcessor.unpack() ) != LIBRAW_SUCCESS) { KStars::Instance()->statusBar()->showMessage(i18n("Cannot unpack_thumb %1: %2", rawFileName, libraw_strerror(ret))); if(LIBRAW_FATAL_ERROR(ret)) { RawProcessor.recycle(); emit BLOBUpdated(bp); return; } // if there has been a non-fatal error, we will try to continue }*/ // Let us unpack the thumbnail if ((ret = RawProcessor.unpack_thumb()) != LIBRAW_SUCCESS) { KStars::Instance()->statusBar()->showMessage( i18n("Cannot unpack_thumb %1: %2", rawFileName, libraw_strerror(ret))); RawProcessor.recycle(); emit BLOBUpdated(bp); return; } else // We have successfully unpacked the thumbnail, now let us write it to a file { //snprintf(thumbfn,sizeof(thumbfn),"%s.%s",av[i],T.tformat == LIBRAW_THUMBNAIL_JPEG ? "thumb.jpg" : "thumb.ppm"); if (LIBRAW_SUCCESS != (ret = RawProcessor.dcraw_thumb_writer(preview_filename.toLatin1().data()))) { KStars::Instance()->statusBar()->showMessage( i18n("Cannot write %s %1: %2", preview_filename, libraw_strerror(ret))); RawProcessor.recycle(); emit BLOBUpdated(bp); return; } } filename = preview_filename; #else // Silenty fail if KStars was not compiled with libraw //KStars::Instance()->statusBar()->showMessage(i18n("Unable to find dcraw and cjpeg. Please install the required tools to convert CR2/NEF to JPEG.")); emit BLOBUpdated(bp); return; #endif } // store file name in strncpy(BLOBFilename, filename.toLatin1(), MAXINDIFILENAME); bp->aux0 = targetChip; bp->aux1 = &BType; bp->aux2 = BLOBFilename; if (Options::useDSLRImageViewer()) { if (imageViewer.isNull()) imageViewer = new ImageViewer(getDeviceName(), KStars::Instance()); imageViewer->loadImage(filename); } } // Unless we have cfitsio, we're done. #ifdef HAVE_CFITSIO if (BType == BLOB_FITS) { QUrl fileURL = QUrl::fromLocalFile(filename); // Get or Create FITSViewer if we are using FITSViewer // or if capture mode is calibrate since for now we are forced to open the file in the viewer // this should be fixed in the future and should only use FITSData if (Options::useFITSViewer() || targetChip->isBatchMode() == false) { if (fv.isNull() && targetChip->getCaptureMode() != FITS_GUIDE && targetChip->getCaptureMode() != FITS_FOCUS && targetChip->getCaptureMode() != FITS_ALIGN) { normalTabID = calibrationTabID = focusTabID = guideTabID = alignTabID = -1; if (Options::singleWindowCapturedFITS()) fv = KStars::Instance()->genericFITSViewer(); else { fv = new FITSViewer(Options::independentWindowFITS() ? nullptr : KStars::Instance()); KStars::Instance()->addFITSViewer(fv); } //connect(fv, SIGNAL(destroyed()), this, SLOT(FITSViewerDestroyed())); //connect(fv, SIGNAL(destroyed()), this, SIGNAL(FITSViewerClosed())); } } FITSScale captureFilter = targetChip->getCaptureFilter(); FITSMode captureMode = targetChip->getCaptureMode(); QString previewTitle; // If image is preview and we should display all captured images in a single tab called "Preview" // Then set the title to "Preview" // Otherwise, the title will be the captured image name if (targetChip->isBatchMode() == false && Options::singlePreviewFITS()) { // If we are displayed all images from all cameras in a single FITS Viewer window // Then we prefix the camera name to the "Preview" string if (Options::singleWindowCapturedFITS()) previewTitle = i18n("%1 Preview", getDeviceName()); else // Otherwise, just use "Preview" previewTitle = i18n("Preview"); } switch (captureMode) { case FITS_NORMAL: case FITS_CALIBRATE: { int *tabID = (captureMode == FITS_NORMAL) ? &normalTabID : &calibrationTabID; // Check if we need to display the image if (Options::useFITSViewer() || targetChip->isBatchMode() == false) { fv->disconnect(this); auto m_Loaded = std::make_shared(); *m_Loaded = connect(fv, &FITSViewer::loaded, [=](int tabIndex) { *tabID = tabIndex; targetChip->setImageView(fv->getView(tabIndex), captureMode); QObject::disconnect(*m_Loaded); emit BLOBUpdated(bp); }); auto m_Failed = std::make_shared(); *m_Failed = connect(fv, &FITSViewer::failed, [=]() { // If opening file fails, we treat it the same as exposure failure and recapture again if possible emit newExposureValue(targetChip, 0, IPS_ALERT); QObject::disconnect(*m_Failed); return; }); if (*tabID == -1 || Options::singlePreviewFITS() == false) fv->addFITS(fileURL, captureMode, captureFilter, previewTitle); else fv->updateFITS(fileURL, *tabID, captureFilter); } else // If not displayed in FITS Viewer then we just inform that a blob was received. emit BLOBUpdated(bp); } break; case FITS_FOCUS: case FITS_GUIDE: case FITS_ALIGN: loadImageInView(bp, targetChip); break; } } else emit BLOBUpdated(bp); #endif } void CCD::loadImageInView(IBLOB *bp, ISD::CCDChip *targetChip) { FITSMode mode = targetChip->getCaptureMode(); FITSView *view = targetChip->getImageView(mode); QString filename = QString(static_cast(bp->aux2)); if (view) { auto m_Loaded = std::make_shared(); *m_Loaded = connect(view, &FITSView::loaded, [=]() { //view->updateFrame(); // FITSViewer is shown if: // Image in preview mode, or useFITSViewre is true; AND // Image type is either NORMAL or CALIBRATION since the rest have their dedicated windows. // NORMAL is used for raw INDI drivers without Ekos. if ( (Options::useFITSViewer() || targetChip->isBatchMode() == false) && (mode == FITS_NORMAL || mode == FITS_CALIBRATE)) fv->show(); QObject::disconnect(*m_Loaded); emit BLOBUpdated(bp); }); auto m_Failed = std::make_shared(); *m_Failed = connect(view, &FITSView::failed, [=]() { QObject::disconnect(*m_Failed); emit newExposureValue(targetChip, 0, IPS_ALERT); return; }); view->setFilter(targetChip->getCaptureFilter()); view->loadFITS(filename, true); } } void CCD::addFITSKeywords(const QString& filename) { #ifdef HAVE_CFITSIO int status = 0; if (filter.isEmpty() == false) { QString key_comment("Filter name"); filter.replace(' ', '_'); fitsfile *fptr = nullptr; #if 0 if (fits_open_image(&fptr, filename.toLatin1(), READWRITE, &status)) { fits_report_error(stderr, status); return; } #endif // Use open diskfile as it does not use extended file names which has problems opening // files with [ ] or ( ) in their names. if (fits_open_diskfile(&fptr, filename.toLatin1(), READONLY, &status)) { fits_report_error(stderr, status); return; } if (fits_movabs_hdu(fptr, 1, IMAGE_HDU, &status)) { fits_report_error(stderr, status); return; } if (fits_update_key_str(fptr, "FILTER", filter.toLatin1().data(), key_comment.toLatin1().data(), &status)) { fits_report_error(stderr, status); return; } fits_close_file(fptr, &status); filter = ""; } #endif } CCD::TransferFormat CCD::getTargetTransferFormat() const { return targetTransferFormat; } void CCD::setTargetTransferFormat(const TransferFormat &value) { targetTransferFormat = value; } void CCD::FITSViewerDestroyed() { fv = nullptr; normalTabID = calibrationTabID = focusTabID = guideTabID = alignTabID = -1; } void CCD::StreamWindowHidden() { if (baseDevice->isConnected()) { // We can have more than one *_VIDEO_STREAM property active so disable them all ISwitchVectorProperty *streamSP = baseDevice->getSwitch("CCD_VIDEO_STREAM"); if (streamSP) { IUResetSwitch(streamSP); streamSP->sp[0].s = ISS_OFF; streamSP->sp[1].s = ISS_ON; streamSP->s = IPS_IDLE; clientManager->sendNewSwitch(streamSP); } streamSP = baseDevice->getSwitch("VIDEO_STREAM"); if (streamSP) { IUResetSwitch(streamSP); streamSP->sp[0].s = ISS_OFF; streamSP->sp[1].s = ISS_ON; streamSP->s = IPS_IDLE; clientManager->sendNewSwitch(streamSP); } streamSP = baseDevice->getSwitch("AUX_VIDEO_STREAM"); if (streamSP) { IUResetSwitch(streamSP); streamSP->sp[0].s = ISS_OFF; streamSP->sp[1].s = ISS_ON; streamSP->s = IPS_IDLE; clientManager->sendNewSwitch(streamSP); } } if (streamWindow.get() != nullptr) streamWindow->disconnect(); } bool CCD::hasGuideHead() { return HasGuideHead; } bool CCD::hasCooler() { return HasCooler; } bool CCD::hasCoolerControl() { return HasCoolerControl; } bool CCD::setCoolerControl(bool enable) { if (HasCoolerControl == false) return false; ISwitchVectorProperty *coolerSP = baseDevice->getSwitch("CCD_COOLER"); if (coolerSP == nullptr) return false; // Cooler ON/OFF ISwitch *coolerON = IUFindSwitch(coolerSP, "COOLER_ON"); ISwitch *coolerOFF = IUFindSwitch(coolerSP, "COOLER_OFF"); if (coolerON == nullptr || coolerOFF == nullptr) return false; coolerON->s = enable ? ISS_ON : ISS_OFF; coolerOFF->s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(coolerSP); return true; } CCDChip *CCD::getChip(CCDChip::ChipType cType) { switch (cType) { case CCDChip::PRIMARY_CCD: return primaryChip.get(); break; case CCDChip::GUIDE_CCD: return guideChip.get(); break; } return nullptr; } bool CCD::setRapidGuide(CCDChip *targetChip, bool enable) { ISwitchVectorProperty *rapidSP = nullptr; ISwitch *enableS = nullptr; if (targetChip == primaryChip.get()) rapidSP = baseDevice->getSwitch("CCD_RAPID_GUIDE"); else rapidSP = baseDevice->getSwitch("GUIDER_RAPID_GUIDE"); if (rapidSP == nullptr) return false; enableS = IUFindSwitch(rapidSP, "ENABLE"); if (enableS == nullptr) return false; // Already updated, return OK if ((enable && enableS->s == ISS_ON) || (!enable && enableS->s == ISS_OFF)) return true; IUResetSwitch(rapidSP); rapidSP->sp[0].s = enable ? ISS_ON : ISS_OFF; rapidSP->sp[1].s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(rapidSP); return true; } bool CCD::configureRapidGuide(CCDChip *targetChip, bool autoLoop, bool sendImage, bool showMarker) { ISwitchVectorProperty *rapidSP = nullptr; ISwitch *autoLoopS = nullptr, *sendImageS = nullptr, *showMarkerS = nullptr; if (targetChip == primaryChip.get()) rapidSP = baseDevice->getSwitch("CCD_RAPID_GUIDE_SETUP"); else rapidSP = baseDevice->getSwitch("GUIDER_RAPID_GUIDE_SETUP"); if (rapidSP == nullptr) return false; autoLoopS = IUFindSwitch(rapidSP, "AUTO_LOOP"); sendImageS = IUFindSwitch(rapidSP, "SEND_IMAGE"); showMarkerS = IUFindSwitch(rapidSP, "SHOW_MARKER"); if (!autoLoopS || !sendImageS || !showMarkerS) return false; // If everything is already set, let's return. if (((autoLoop && autoLoopS->s == ISS_ON) || (!autoLoop && autoLoopS->s == ISS_OFF)) && ((sendImage && sendImageS->s == ISS_ON) || (!sendImage && sendImageS->s == ISS_OFF)) && ((showMarker && showMarkerS->s == ISS_ON) || (!showMarker && showMarkerS->s == ISS_OFF))) return true; autoLoopS->s = autoLoop ? ISS_ON : ISS_OFF; sendImageS->s = sendImage ? ISS_ON : ISS_OFF; showMarkerS->s = showMarker ? ISS_ON : ISS_OFF; clientManager->sendNewSwitch(rapidSP); return true; } void CCD::updateUploadSettings(const QString &remoteDir) { QString filename = seqPrefix + (seqPrefix.isEmpty() ? "" : "_") + QString("XXX"); ITextVectorProperty *uploadSettingsTP = nullptr; IText *uploadT = nullptr; uploadSettingsTP = baseDevice->getText("UPLOAD_SETTINGS"); if (uploadSettingsTP) { uploadT = IUFindText(uploadSettingsTP, "UPLOAD_DIR"); if (uploadT && remoteDir.isEmpty() == false) IUSaveText(uploadT, remoteDir.toLatin1().constData()); uploadT = IUFindText(uploadSettingsTP, "UPLOAD_PREFIX"); if (uploadT) IUSaveText(uploadT, filename.toLatin1().constData()); clientManager->sendNewText(uploadSettingsTP); } } CCD::UploadMode CCD::getUploadMode() { ISwitchVectorProperty *uploadModeSP = nullptr; uploadModeSP = baseDevice->getSwitch("UPLOAD_MODE"); if (uploadModeSP == nullptr) { qWarning() << "No UPLOAD_MODE in CCD driver. Please update driver to INDI compliant CCD driver."; return UPLOAD_CLIENT; } if (uploadModeSP) { ISwitch *modeS = nullptr; modeS = IUFindSwitch(uploadModeSP, "UPLOAD_CLIENT"); if (modeS && modeS->s == ISS_ON) return UPLOAD_CLIENT; modeS = IUFindSwitch(uploadModeSP, "UPLOAD_LOCAL"); if (modeS && modeS->s == ISS_ON) return UPLOAD_LOCAL; modeS = IUFindSwitch(uploadModeSP, "UPLOAD_BOTH"); if (modeS && modeS->s == ISS_ON) return UPLOAD_BOTH; } // Default return UPLOAD_CLIENT; } bool CCD::setUploadMode(UploadMode mode) { ISwitchVectorProperty *uploadModeSP = nullptr; ISwitch *modeS = nullptr; uploadModeSP = baseDevice->getSwitch("UPLOAD_MODE"); if (uploadModeSP == nullptr) { qWarning() << "No UPLOAD_MODE in CCD driver. Please update driver to INDI compliant CCD driver."; return false; } switch (mode) { case UPLOAD_CLIENT: modeS = IUFindSwitch(uploadModeSP, "UPLOAD_CLIENT"); if (modeS == nullptr) return false; if (modeS->s == ISS_ON) return true; break; case UPLOAD_BOTH: modeS = IUFindSwitch(uploadModeSP, "UPLOAD_BOTH"); if (modeS == nullptr) return false; if (modeS->s == ISS_ON) return true; break; case UPLOAD_LOCAL: modeS = IUFindSwitch(uploadModeSP, "UPLOAD_LOCAL"); if (modeS == nullptr) return false; if (modeS->s == ISS_ON) return true; break; } IUResetSwitch(uploadModeSP); modeS->s = ISS_ON; clientManager->sendNewSwitch(uploadModeSP); return true; } bool CCD::getTemperature(double *value) { if (HasCooler == false) return false; INumberVectorProperty *temperatureNP = baseDevice->getNumber("CCD_TEMPERATURE"); if (temperatureNP == nullptr) return false; *value = temperatureNP->np[0].value; return true; } bool CCD::setTemperature(double value) { INumberVectorProperty *nvp = baseDevice->getNumber("CCD_TEMPERATURE"); if (nvp == nullptr) return false; INumber *np = IUFindNumber(nvp, "CCD_TEMPERATURE_VALUE"); if (np == nullptr) return false; np->value = value; clientManager->sendNewNumber(nvp); return true; } bool CCD::setTransformFormat(CCD::TransferFormat format) { if (format == transferFormat) return true; ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_TRANSFER_FORMAT"); if (svp == nullptr) return false; ISwitch *formatFITS = IUFindSwitch(svp, "FORMAT_FITS"); ISwitch *formatNative = IUFindSwitch(svp, "FORMAT_NATIVE"); if (formatFITS == nullptr || formatNative == nullptr) return false; transferFormat = format; formatFITS->s = (transferFormat == FORMAT_FITS) ? ISS_ON : ISS_OFF; formatNative->s = (transferFormat == FORMAT_FITS) ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::setTelescopeType(TelescopeType type) { if (type == telescopeType) return true; ISwitchVectorProperty *svp = baseDevice->getSwitch("TELESCOPE_TYPE"); if (svp == nullptr) return false; ISwitch *typePrimary = IUFindSwitch(svp, "TELESCOPE_PRIMARY"); ISwitch *typeGuide = IUFindSwitch(svp, "TELESCOPE_GUIDE"); if (typePrimary == nullptr || typeGuide == nullptr) return false; telescopeType = type; typePrimary->s = (telescopeType == TELESCOPE_PRIMARY) ? ISS_ON : ISS_OFF; typeGuide->s = (telescopeType == TELESCOPE_PRIMARY) ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); setConfig(SAVE_CONFIG); return true; } bool CCD::setVideoStreamEnabled(bool enable) { if (HasVideoStream == false) return false; ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_VIDEO_STREAM"); if (svp == nullptr) return false; // If already on and enable is set or vice versa no need to change anything we return true if ((enable && svp->sp[0].s == ISS_ON) || (!enable && svp->sp[1].s == ISS_ON)) return true; svp->sp[0].s = enable ? ISS_ON : ISS_OFF; svp->sp[1].s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::resetStreamingFrame() { INumberVectorProperty *frameProp = baseDevice->getNumber("CCD_STREAM_FRAME"); if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (xarg->value == xarg->min && yarg->value == yarg->min && warg->value == warg->max && harg->value == harg->max) return false; xarg->value = xarg->min; yarg->value = yarg->min; warg->value = warg->max; harg->value = harg->max; clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCD::setStreamingFrame(int x, int y, int w, int h) { INumberVectorProperty *frameProp = baseDevice->getNumber("CCD_STREAM_FRAME"); if (frameProp == nullptr) return false; INumber *xarg = IUFindNumber(frameProp, "X"); INumber *yarg = IUFindNumber(frameProp, "Y"); INumber *warg = IUFindNumber(frameProp, "WIDTH"); INumber *harg = IUFindNumber(frameProp, "HEIGHT"); if (xarg && yarg && warg && harg) { if (xarg->value == x && yarg->value == y && warg->value == w && harg->value == h) return true; // N.B. We add offset since the X, Y are relative to whatever streaming frame is currently active xarg->value = qBound(xarg->min, static_cast(x) + xarg->value, xarg->max); yarg->value = qBound(yarg->min, static_cast(y) + yarg->value, yarg->max); warg->value = qBound(warg->min, static_cast(w), warg->max); harg->value = qBound(harg->min, static_cast(h), harg->max); clientManager->sendNewNumber(frameProp); return true; } return false; } bool CCD::isStreamingEnabled() { if (HasVideoStream == false || streamWindow.get() == nullptr) return false; return streamWindow->isStreamEnabled(); } bool CCD::setSERNameDirectory(const QString &filename, const QString &directory) { ITextVectorProperty *tvp = baseDevice->getText("RECORD_FILE"); if (tvp == nullptr) return false; IText *filenameT = IUFindText(tvp, "RECORD_FILE_NAME"); IText *dirT = IUFindText(tvp, "RECORD_FILE_DIR"); if (filenameT == nullptr || dirT == nullptr) return false; IUSaveText(filenameT, filename.toLatin1().data()); IUSaveText(dirT, directory.toLatin1().data()); clientManager->sendNewText(tvp); return true; } bool CCD::getSERNameDirectory(QString &filename, QString &directory) { ITextVectorProperty *tvp = baseDevice->getText("RECORD_FILE"); if (tvp == nullptr) return false; IText *filenameT = IUFindText(tvp, "RECORD_FILE_NAME"); IText *dirT = IUFindText(tvp, "RECORD_FILE_DIR"); if (filenameT == nullptr || dirT == nullptr) return false; filename = QString(filenameT->text); directory = QString(dirT->text); return true; } bool CCD::startRecording() { ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (svp == nullptr) return false; ISwitch *recordON = IUFindSwitch(svp, "RECORD_ON"); if (recordON == nullptr) return false; if (recordON->s == ISS_ON) return true; IUResetSwitch(svp); recordON->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::startDurationRecording(double duration) { INumberVectorProperty *nvp = baseDevice->getNumber("RECORD_OPTIONS"); if (nvp == nullptr) return false; INumber *durationN = IUFindNumber(nvp, "RECORD_DURATION"); if (durationN == nullptr) return false; ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (svp == nullptr) return false; ISwitch *recordON = IUFindSwitch(svp, "RECORD_DURATION_ON"); if (recordON == nullptr) return false; if (recordON->s == ISS_ON) return true; durationN->value = duration; clientManager->sendNewNumber(nvp); IUResetSwitch(svp); recordON->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::startFramesRecording(uint32_t frames) { INumberVectorProperty *nvp = baseDevice->getNumber("RECORD_OPTIONS"); if (nvp == nullptr) return false; INumber *frameN = IUFindNumber(nvp, "RECORD_FRAME_TOTAL"); ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (frameN == nullptr || svp == nullptr) return false; ISwitch *recordON = IUFindSwitch(svp, "RECORD_FRAME_ON"); if (recordON == nullptr) return false; if (recordON->s == ISS_ON) return true; frameN->value = frames; clientManager->sendNewNumber(nvp); IUResetSwitch(svp); recordON->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::stopRecording() { ISwitchVectorProperty *svp = baseDevice->getSwitch("RECORD_STREAM"); if (svp == nullptr) return false; ISwitch *recordOFF = IUFindSwitch(svp, "RECORD_OFF"); if (recordOFF == nullptr) return false; // If already set if (recordOFF->s == ISS_ON) return true; IUResetSwitch(svp); recordOFF->s = ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::setFITSHeader(const QMap &values) { ITextVectorProperty *tvp = baseDevice->getText("FITS_HEADER"); if (tvp == nullptr) return false; QMapIterator i(values); while (i.hasNext()) { i.next(); IText *headerT = IUFindText(tvp, i.key().toLatin1().data()); if (headerT == nullptr) continue; IUSaveText(headerT, i.value().toLatin1().data()); } clientManager->sendNewText(tvp); return true; } bool CCD::setGain(double value) { if (gainN == nullptr) return false; gainN->value = value; clientManager->sendNewNumber(gainN->nvp); return true; } bool CCD::getGain(double *value) { if (gainN == nullptr) return false; *value = gainN->value; return true; } bool CCD::getGainMinMaxStep(double *min, double *max, double *step) { if (gainN == nullptr) return false; *min = gainN->min; *max = gainN->max; *step = gainN->step; return true; } bool CCD::isBLOBEnabled() { return (clientManager->isBLOBEnabled(getDeviceName(), "CCD1")); } bool CCD::setBLOBEnabled(bool enable) { if (enable) { clientManager->setBLOBEnabled(true, getDeviceName(), "CCD1"); clientManager->setBLOBEnabled(true, getDeviceName(), "CCD2"); } else { clientManager->setBLOBEnabled(false, getDeviceName(), "CCD1"); clientManager->setBLOBEnabled(false, getDeviceName(), "CCD2"); } return true; } bool CCD::setExposureLoopingEnabled(bool enable) { // Set value immediately IsLooping = enable; ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_EXPOSURE_LOOP"); if (svp == nullptr) return false; svp->sp[0].s = enable ? ISS_ON : ISS_OFF; svp->sp[1].s = enable ? ISS_OFF : ISS_ON; clientManager->sendNewSwitch(svp); return true; } bool CCD::setExposureLoopCount(uint32_t count) { INumberVectorProperty *nvp = baseDevice->getNumber("CCD_EXPOSURE_LOOP_COUNT"); if (nvp == nullptr) return false; nvp->np[0].value = count; clientManager->sendNewNumber(nvp); return true; } bool CCD::setStreamExposure(double duration) { INumberVectorProperty *nvp = baseDevice->getNumber("STREAMING_EXPOSURE"); if (nvp == nullptr) return false; nvp->np[0].value = duration; clientManager->sendNewNumber(nvp); return true; } bool CCD::getStreamExposure(double *duration) { INumberVectorProperty *nvp = baseDevice->getNumber("STREAMING_EXPOSURE"); if (nvp == nullptr) return false; *duration = nvp->np[0].value; return true; } bool CCD::isCoolerOn() { ISwitchVectorProperty *svp = baseDevice->getSwitch("CCD_COOLER"); if (svp == nullptr) return false; return (svp->sp[0].s == ISS_ON); } }