mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-06-04 06:54:39 -04:00
APT Demod updates.
Add projection of image on to 3D map. Add support for temperature map. Add support for colour palettes for image enhancements. Fix IR channel names.
This commit is contained in:
@@ -19,17 +19,25 @@
|
||||
#include <algorithm>
|
||||
|
||||
#include <QTime>
|
||||
#include <QBuffer>
|
||||
#include <QDebug>
|
||||
|
||||
#include "maincore.h"
|
||||
#include "util/units.h"
|
||||
|
||||
#include "aptdemod.h"
|
||||
#include "aptdemodimageworker.h"
|
||||
|
||||
#include "SWGMapItem.h"
|
||||
|
||||
MESSAGE_CLASS_DEFINITION(APTDemodImageWorker::MsgConfigureAPTDemodImageWorker, Message)
|
||||
MESSAGE_CLASS_DEFINITION(APTDemodImageWorker::MsgSaveImageToDisk, Message)
|
||||
MESSAGE_CLASS_DEFINITION(APTDemodImageWorker::MsgSetSatelliteName, Message)
|
||||
|
||||
APTDemodImageWorker::APTDemodImageWorker() :
|
||||
APTDemodImageWorker::APTDemodImageWorker(APTDemod *aptDemod) :
|
||||
m_messageQueueToGUI(nullptr),
|
||||
m_aptDemod(aptDemod),
|
||||
m_sgp4(nullptr),
|
||||
m_running(false),
|
||||
m_mutex(QMutex::Recursive)
|
||||
{
|
||||
@@ -51,6 +59,8 @@ APTDemodImageWorker::~APTDemodImageWorker()
|
||||
delete[] m_image.prow[y];
|
||||
delete[] m_tempImage.prow[y];
|
||||
}
|
||||
|
||||
delete m_sgp4;
|
||||
}
|
||||
|
||||
void APTDemodImageWorker::reset()
|
||||
@@ -129,6 +139,8 @@ bool APTDemodImageWorker::handleMessage(const Message& cmd)
|
||||
void APTDemodImageWorker::applySettings(const APTDemodSettings& settings, bool force)
|
||||
{
|
||||
(void) force;
|
||||
|
||||
bool callRecalcCoords = false;
|
||||
bool callProcessImage = false;
|
||||
|
||||
if ((settings.m_cropNoise != m_settings.m_cropNoise) ||
|
||||
@@ -137,14 +149,52 @@ void APTDemodImageWorker::applySettings(const APTDemodSettings& settings, bool f
|
||||
(settings.m_histogramEqualise != m_settings.m_histogramEqualise) ||
|
||||
(settings.m_precipitationOverlay != m_settings.m_precipitationOverlay) ||
|
||||
(settings.m_flip != m_settings.m_flip) ||
|
||||
(settings.m_channels != m_settings.m_channels))
|
||||
(settings.m_channels != m_settings.m_channels) ||
|
||||
(settings.m_transparencyThreshold != m_settings.m_transparencyThreshold) ||
|
||||
(settings.m_opacityThreshold != m_settings.m_opacityThreshold) ||
|
||||
(settings.m_palettes != m_settings.m_palettes) ||
|
||||
(settings.m_palette != m_settings.m_palette) ||
|
||||
(settings.m_horizontalPixelsPerDegree != m_settings.m_horizontalPixelsPerDegree) ||
|
||||
(settings.m_verticalPixelsPerDegree != m_settings.m_verticalPixelsPerDegree))
|
||||
{
|
||||
// Call after settings have been applied
|
||||
callProcessImage = true;
|
||||
}
|
||||
|
||||
if ((settings.m_satTimeOffset != m_settings.m_satTimeOffset) ||
|
||||
(settings.m_satYaw != m_settings.m_satYaw))
|
||||
{
|
||||
callRecalcCoords = true;
|
||||
callProcessImage = true;
|
||||
}
|
||||
|
||||
if (!settings.m_decodeEnabled && m_settings.m_decodeEnabled)
|
||||
{
|
||||
// Decode complete - make sure we do a full image update
|
||||
// so we aren't left we unprocessed lines
|
||||
callProcessImage = true;
|
||||
}
|
||||
|
||||
if (settings.m_palettes != m_settings.m_palettes)
|
||||
{
|
||||
// Load colour palettes
|
||||
m_palettes.clear();
|
||||
for (auto palette : settings.m_palettes)
|
||||
{
|
||||
QImage img;
|
||||
img.load(palette);
|
||||
if ((img.width() != 256) || (img.height() != 256)) {
|
||||
qWarning() << "APT colour palette " << palette << " is not 256x256 pixels - " << img.width() << "x" << img.height();
|
||||
}
|
||||
m_palettes.append(img);
|
||||
}
|
||||
}
|
||||
|
||||
m_settings = settings;
|
||||
|
||||
if (callRecalcCoords) {
|
||||
recalcCoords();
|
||||
}
|
||||
if (callProcessImage) {
|
||||
sendImageToGUI();
|
||||
}
|
||||
@@ -153,25 +203,234 @@ void APTDemodImageWorker::applySettings(const APTDemodSettings& settings, bool f
|
||||
void APTDemodImageWorker::resetDecoder()
|
||||
{
|
||||
m_image.nrow = 0;
|
||||
m_image.zenith = 0;
|
||||
m_tempImage.nrow = 0;
|
||||
m_tempImage.zenith = 0;
|
||||
m_greyImage = QImage(APT_IMG_WIDTH, APT_MAX_HEIGHT, QImage::Format_Grayscale8);
|
||||
m_greyImage.fill(0);
|
||||
m_colourImage = QImage(APT_IMG_WIDTH, APT_MAX_HEIGHT, QImage::Format_RGB888);
|
||||
m_colourImage.fill(0);
|
||||
m_satelliteName = "";
|
||||
m_satCoords.clear();
|
||||
m_pixelCoords.clear();
|
||||
delete m_sgp4;
|
||||
m_sgp4 = nullptr;
|
||||
}
|
||||
|
||||
// Convert Qt QDataTime to QGP4 DateTime
|
||||
static DateTime qDateTimeToDateTime(QDateTime qdt)
|
||||
{
|
||||
QDateTime utc = qdt.toUTC();
|
||||
QDate date = utc.date();
|
||||
QTime time = utc.time();
|
||||
DateTime dt;
|
||||
dt.Initialise(date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second(), time.msec() * 1000);
|
||||
return dt;
|
||||
}
|
||||
|
||||
// Get heading in range [0,360)
|
||||
static double normaliseHeading(double heading)
|
||||
{
|
||||
return fmod(heading + 360.0, 360.0);
|
||||
}
|
||||
|
||||
// Get longitude in range -180,180
|
||||
static double normaliseLongitude(double lon)
|
||||
{
|
||||
return fmod(lon + 540.0, 360.0) - 180.0;
|
||||
}
|
||||
|
||||
// Calculate heading (azimuth) in degrees
|
||||
double APTDemodImageWorker::calcHeading(CoordGeodetic from, CoordGeodetic to) const
|
||||
{
|
||||
// From https://en.wikipedia.org/wiki/Azimuth Section In Geodesy
|
||||
double flattening = 1.0 / 298.257223563; // For WGS84 ellipsoid
|
||||
double eSq = flattening * (2.0 - flattening);
|
||||
double oneMinusESq = (1.0 - flattening) * (1.0 - flattening);
|
||||
|
||||
double tl1 = tan(from.latitude);
|
||||
double tl2 = tan(to.latitude);
|
||||
double n1 = 1.0 + oneMinusESq * tl2 * tl2;
|
||||
double d1 = 1.0 + oneMinusESq * tl1 * tl1;
|
||||
|
||||
double l = to.longitude - from.longitude;
|
||||
|
||||
double alpha;
|
||||
|
||||
if (from.latitude == 0.0)
|
||||
{
|
||||
alpha = atan2(sin(l), (oneMinusESq * tan(to.latitude)));
|
||||
}
|
||||
else
|
||||
{
|
||||
double lambda = oneMinusESq * tan(to.latitude) / tan(from.latitude) + eSq * sqrt(n1/d1);
|
||||
|
||||
alpha = atan2(sin(l), ((lambda - cos(l)) * sin(from.latitude)));
|
||||
}
|
||||
|
||||
double deg = Units::radiansToDegrees(alpha);
|
||||
if (!m_settings.m_northToSouth) {
|
||||
deg += 180.0;
|
||||
}
|
||||
deg = normaliseHeading(deg);
|
||||
return deg;
|
||||
}
|
||||
|
||||
// CoordGeodetic are in radians. Distance in metres. Bearing in radians.
|
||||
// https://www.movable-type.co.uk/scripts/latlong.html
|
||||
// This approximates Earth as spherical. If we need more accurate algorithm, see:
|
||||
// https://www.movable-type.co.uk/scripts/latlong-vincenty.html
|
||||
static void calcRadialEndPoint(CoordGeodetic start, double distance, double bearing, CoordGeodetic &end)
|
||||
{
|
||||
double earthRadius = 6378137.0; // At equator
|
||||
double delta = distance/earthRadius;
|
||||
end.latitude = std::asin(sin(start.latitude)*cos(delta) + cos(start.latitude)*sin(delta)*cos(bearing));
|
||||
end.longitude = start.longitude + std::atan2(sin(bearing)*sin(delta)*cos(start.latitude), cos(delta) - sin(start.latitude)*sin(end.latitude));
|
||||
end.longitude = normaliseLongitude(end.longitude);
|
||||
}
|
||||
|
||||
void APTDemodImageWorker::calcPixelCoords(CoordGeodetic centreCoord, double heading)
|
||||
{
|
||||
// Calculate coordinates of each pixel in a row (swath)
|
||||
// Assume satellite is at centre pixel, and project +-90 degrees from satellite heading
|
||||
// https://www.ncei.noaa.gov/pub/data/satellite/publications/podguides/N-15%20thru%20N-19/pdf/APPENDIX%20J%20Instrument%20Scan%20Properties.pdf
|
||||
// Swath for AVHRR/3 of 2926.6km at 833km altitude over spherical Earth
|
||||
// Some docs say resolution is 4.0km, but it varies as per fig 4.2.3-1 in:
|
||||
// https://www.ncei.noaa.gov/pub/data/satellite/publications/podguides/N-15%20thru%20N-19/pdf/2.1%20Section%204.0%20Real%20Time%20Data%20Systems%20for%20Local%20Users%20.pdf
|
||||
// TODO: Could try to adjust for altitude
|
||||
|
||||
QVector<CoordGeodetic> pixelCoords(APT_CH_WIDTH);
|
||||
pixelCoords[APT_CH_WIDTH/2] = centreCoord;
|
||||
double heading1 = Units::degreesToRadians(heading + m_settings.m_satYaw + 90.0);
|
||||
double heading2 = Units::degreesToRadians(heading + m_settings.m_satYaw - 90.0);
|
||||
for (int i = 1; i <= APT_CH_WIDTH/2; i++)
|
||||
{
|
||||
double distance = i * 2926600.0/APT_CH_WIDTH;
|
||||
calcRadialEndPoint(centreCoord, distance, heading1, pixelCoords[APT_CH_WIDTH/2-i]);
|
||||
calcRadialEndPoint(centreCoord, distance, heading2, pixelCoords[APT_CH_WIDTH/2+i]);
|
||||
}
|
||||
|
||||
if (m_settings.m_northToSouth) {
|
||||
m_pixelCoords.append(pixelCoords);
|
||||
} else {
|
||||
m_pixelCoords.prepend(pixelCoords);
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate all pixel coordiantes as satTimeOffset or satYaw has changed
|
||||
void APTDemodImageWorker::recalcCoords()
|
||||
{
|
||||
if (m_sgp4)
|
||||
{
|
||||
m_satCoords.clear();
|
||||
m_pixelCoords.clear();
|
||||
for (int row = 0; row < m_image.nrow; row++)
|
||||
{
|
||||
QDateTime qdt = m_settings.m_aosDateTime.addMSecs(m_settings.m_satTimeOffset * 1000.0f + row * 500);
|
||||
calcCoords(qdt, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate pixel coordinates for a single row at the given date and time
|
||||
void APTDemodImageWorker::calcCoords(QDateTime qdt, int row)
|
||||
{
|
||||
try
|
||||
{
|
||||
DateTime dt = qDateTimeToDateTime(qdt);
|
||||
|
||||
// Calculate satellite position
|
||||
Eci eci = m_sgp4->FindPosition(dt);
|
||||
|
||||
// Convert satellite position to geodetic coordinates (lat and long)
|
||||
CoordGeodetic geo = eci.ToGeodetic();
|
||||
|
||||
m_satCoords.append(geo);
|
||||
|
||||
// Calculate satellite heading (Could convert eci.Velocity() instead)
|
||||
double heading;
|
||||
if (m_satCoords.size() == 2)
|
||||
{
|
||||
heading = calcHeading(m_satCoords[0], m_satCoords[1]);
|
||||
calcPixelCoords(m_satCoords[0], heading);
|
||||
calcPixelCoords(m_satCoords[1], heading);
|
||||
}
|
||||
else if (m_satCoords.size() > 2)
|
||||
{
|
||||
heading = calcHeading(m_satCoords[row-1], m_satCoords[row]);
|
||||
calcPixelCoords(geo, heading);
|
||||
}
|
||||
}
|
||||
catch (SatelliteException& se)
|
||||
{
|
||||
qDebug() << "APTDemodImageWorker::calcCoord: " << se.what();
|
||||
}
|
||||
catch (DecayedException& de)
|
||||
{
|
||||
qDebug() << "APTDemodImageWorker::calcCoord: " << de.what();
|
||||
}
|
||||
catch (TleException& tlee)
|
||||
{
|
||||
qDebug() << "APTDemodImageWorker::calcCoord: " << tlee.what();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calculate satellite's geodetic coordinates and heading
|
||||
void APTDemodImageWorker::calcCoord(int row)
|
||||
{
|
||||
if (row == 0)
|
||||
{
|
||||
QStringList tle = m_settings.m_tle.trimmed().split("\n");
|
||||
if (tle.size() == 3)
|
||||
{
|
||||
// Initalise SGP4
|
||||
Tle tle(tle[0].toStdString(), tle[1].toStdString(), tle[2].toStdString());
|
||||
m_sgp4 = new SGP4(tle);
|
||||
|
||||
// Output time so we can check time offset from when AOS is signalled
|
||||
qDebug() << "APTDemod: Processing row 0 at " << QDateTime::currentDateTime();
|
||||
|
||||
calcCoords(m_settings.m_aosDateTime, row);
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "APTDemodImageWorker::calcCoord: No TLE for satellite. Is Satellite Tracker running?";
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (m_sgp4 == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Calculate time at which
|
||||
// Don't try to use QDateTime::currentDateTime() as processing & scheduling delays mean
|
||||
// it's not constant and can sometimes even be 0
|
||||
// Lines should be transmitted at 2 per second, so just use number of rows since AOS
|
||||
// We add a user-defined delay to account for delays in transferring SDR data and demodulation
|
||||
QDateTime qdt = m_settings.m_aosDateTime.addMSecs(m_settings.m_satTimeOffset * 1000.0f + row * 500);
|
||||
calcCoords(qdt, row);
|
||||
}
|
||||
}
|
||||
|
||||
void APTDemodImageWorker::processPixels(const float *pixels)
|
||||
{
|
||||
std::copy(pixels, pixels + APT_PROW_WIDTH, m_image.prow[m_image.nrow]);
|
||||
if (m_image.nrow < APT_MAX_HEIGHT)
|
||||
{
|
||||
// Calculate lat and lon of centre of row
|
||||
calcCoord(m_image.nrow);
|
||||
|
||||
if (m_image.nrow % 20 == 0) { // send full image only every 20 lines
|
||||
sendImageToGUI();
|
||||
} else { // else send unprocessed line just to show stg is moving
|
||||
sendLineToGUI();
|
||||
std::copy(pixels, pixels + APT_PROW_WIDTH, m_image.prow[m_image.nrow]);
|
||||
m_image.nrow++;
|
||||
|
||||
if (m_image.nrow % m_settings.m_scanlinesPerImageUpdate == 0) { // send full image only every N lines
|
||||
sendImageToGUI();
|
||||
} else { // else send unprocessed line just to show stg is moving
|
||||
sendLineToGUI();
|
||||
}
|
||||
}
|
||||
|
||||
m_image.nrow++;
|
||||
}
|
||||
|
||||
void APTDemodImageWorker::sendImageToGUI()
|
||||
@@ -180,8 +439,278 @@ void APTDemodImageWorker::sendImageToGUI()
|
||||
if (m_messageQueueToGUI)
|
||||
{
|
||||
QStringList imageTypes;
|
||||
QImage image = processImage(imageTypes);
|
||||
QImage image = processImage(imageTypes, m_settings.m_channels);
|
||||
m_messageQueueToGUI->push(APTDemod::MsgImage::create(image, imageTypes, m_satelliteName));
|
||||
if (m_sgp4) {
|
||||
sendImageToMap(image, imageTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find the value of the pixel closest to the given coordinates
|
||||
// If we have previously found a pixel, we constrain the search to be nearby, in order to speed up the search
|
||||
QRgb APTDemodImageWorker::findNearest(const QImage &image, double latitude, double longitude, int xPrevious, int yPrevious, int &xNearest, int &yNearest) const
|
||||
{
|
||||
double dmin = 360.0 * 360.0 + 90.0 * 90.0;
|
||||
xNearest = -1;
|
||||
yNearest = -1;
|
||||
QRgb p = qRgba(0, 0, 0, 0); // Transparent
|
||||
|
||||
int xMin, xMax;
|
||||
int yMin, yMax;
|
||||
|
||||
int yStartPostCrop;
|
||||
int yEndPostCrop;
|
||||
if (m_settings.m_northToSouth)
|
||||
{
|
||||
yStartPostCrop = abs(m_tempImage.zenith);
|
||||
yEndPostCrop = yStartPostCrop + image.height();
|
||||
}
|
||||
else
|
||||
{
|
||||
yStartPostCrop = m_image.nrow - m_tempImage.nrow - abs(m_tempImage.zenith);
|
||||
yEndPostCrop = yStartPostCrop + image.height();
|
||||
}
|
||||
|
||||
if (xPrevious == -1)
|
||||
{
|
||||
yMin = yStartPostCrop;
|
||||
yMax = yEndPostCrop;
|
||||
xMin = 0;
|
||||
xMax = m_pixelCoords[0].size();
|
||||
}
|
||||
else
|
||||
{
|
||||
int searchRadius = 4;
|
||||
yMin = yPrevious - searchRadius;
|
||||
yMax = yPrevious + searchRadius + 1;
|
||||
xMin = xPrevious - searchRadius;
|
||||
xMax = xPrevious + searchRadius + 1;
|
||||
yMin = std::max(yMin, yStartPostCrop);
|
||||
yMax = std::min(yMax, yEndPostCrop);
|
||||
xMin = std::max(xMin, 0);
|
||||
xMax = std::min(xMax, m_pixelCoords[0].size());
|
||||
}
|
||||
|
||||
const int ySize = yEndPostCrop-1;
|
||||
const int xSize = m_pixelCoords[0].size()-1;
|
||||
for (int y = yMin; y < yMax; y++)
|
||||
{
|
||||
for (int x = xMin; x < xMax; x++)
|
||||
{
|
||||
CoordGeodetic coord = m_pixelCoords[y][x];
|
||||
double dlat = coord.latitude - latitude;
|
||||
double dlon = coord.longitude - longitude;
|
||||
double d = dlat * dlat + dlon * dlon;
|
||||
if (d < dmin)
|
||||
{
|
||||
dmin = d;
|
||||
xNearest = x;
|
||||
yNearest = y;
|
||||
// Only use color of pixel if we're inside the source image
|
||||
if ( ((y != yStartPostCrop) || ((y == yStartPostCrop) && (latitude <= coord.latitude)))
|
||||
&& ((y != ySize) || ((y == ySize) && (latitude >= coord.latitude)))
|
||||
&& ((x != 0) || ((x == 0) && (longitude >= coord.longitude)))
|
||||
&& ((x != xSize) || ((x == xSize) && (longitude <= coord.longitude)))
|
||||
)
|
||||
{
|
||||
p = image.pixel(x, y - yStartPostCrop);
|
||||
}
|
||||
else
|
||||
{
|
||||
p = qRgba(0, 0, 0, 0); // Transparent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
// Calculate bounding box for projected image in terms of latitude and longitude
|
||||
// TODO: Handle crossing of anti-meridian
|
||||
void APTDemodImageWorker::calcBoundingBox(double &east, double &south, double &west, double &north, const QImage &image)
|
||||
{
|
||||
int start;
|
||||
if (m_settings.m_northToSouth) {
|
||||
start = abs(m_tempImage.zenith);
|
||||
} else {
|
||||
start = m_image.nrow - m_tempImage.nrow - abs(m_tempImage.zenith);
|
||||
}
|
||||
int stop = start + image.height();
|
||||
|
||||
east = -M_PI;
|
||||
west = M_PI;
|
||||
north = -M_PI/2.0;
|
||||
south = M_PI/2.0;
|
||||
|
||||
//FILE *f = fopen("coords.txt", "w");
|
||||
for (int y = start; y < stop; y++)
|
||||
{
|
||||
for (int x = 0; x < m_pixelCoords[y].size(); x++)
|
||||
{
|
||||
double latitude = m_pixelCoords[y][x].latitude;
|
||||
double longitude = m_pixelCoords[y][x].longitude;
|
||||
//fprintf(f, "%f,%f ", Units::radiansToDegrees(m_pixelCoords[y][x].latitude), Units::radiansToDegrees(m_pixelCoords[y][x].longitude));
|
||||
south = std::min(latitude, south);
|
||||
north = std::max(latitude, north);
|
||||
east = std::max(longitude, east);
|
||||
west = std::min(longitude, west);
|
||||
}
|
||||
//fprintf(f, "\n");
|
||||
}
|
||||
//fclose(f);
|
||||
}
|
||||
|
||||
// Project satellite image to equidistant cyclindrical projection (Plate Carree) for use on 3D Map
|
||||
// We've previously computed lat and lon for each pixel in satellite image
|
||||
// so we just work through coords in projected image, trying to find closest pixel in satellite image
|
||||
// FIXME: How do we handle sat going over the poles?
|
||||
QImage APTDemodImageWorker::projectImage(const QImage &image)
|
||||
{
|
||||
double east, south, west, north;
|
||||
|
||||
// Calculate bounding box for image tile
|
||||
calcBoundingBox(east, south, west, north, image);
|
||||
m_tileEast = ceil(Units::radiansToDegrees(east));
|
||||
m_tileWest = floor(Units::radiansToDegrees(west));
|
||||
m_tileNorth = ceil(Units::radiansToDegrees(north));
|
||||
m_tileSouth = floor(Units::radiansToDegrees(south));
|
||||
|
||||
double widthDeg = m_tileEast - m_tileWest;
|
||||
double heightDeg = m_tileNorth - m_tileSouth;
|
||||
|
||||
int width = widthDeg * m_settings.m_horizontalPixelsPerDegree;
|
||||
int height = heightDeg * m_settings.m_verticalPixelsPerDegree;
|
||||
|
||||
//image.save("source.png");
|
||||
//FILE *f = fopen("mapping.txt", "w");
|
||||
QImage projection(width, height, QImage::Format_ARGB32);
|
||||
int xNearest, yNearest, xPrevious, yPrevious;
|
||||
xPrevious = -1;
|
||||
yPrevious = -1;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
// Calculate geodetic coords of pixel in projected image
|
||||
double lat = m_tileNorth - (y / (double)m_settings.m_verticalPixelsPerDegree);
|
||||
// Reverse search direction in alternate rows, so we are always seaching
|
||||
// close to previously found pixel
|
||||
if ((y & 1) == 0)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
double lon = m_tileWest + (x / (double)m_settings.m_horizontalPixelsPerDegree);
|
||||
// Find closest pixel in source image
|
||||
QRgb pixel = findNearest(image, Units::degreesToRadians(lat), Units::degreesToRadians(lon), xPrevious, yPrevious, xNearest, yNearest);
|
||||
xPrevious = xNearest;
|
||||
yPrevious = yNearest;
|
||||
projection.setPixel(x, y, pixel);
|
||||
//fprintf(f, "%f,%f,%d,%d,%d ", lat, lon, xNearest, yNearest, pixel==0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int x = width - 1; x >= 0; x--)
|
||||
{
|
||||
double lon = m_tileWest + (x / (double)m_settings.m_horizontalPixelsPerDegree);
|
||||
// Find closest pixel in source image
|
||||
QRgb pixel = findNearest(image, Units::degreesToRadians(lat), Units::degreesToRadians(lon), xPrevious, yPrevious, xNearest, yNearest);
|
||||
xPrevious = xNearest;
|
||||
yPrevious = yNearest;
|
||||
projection.setPixel(x, y, pixel);
|
||||
//fprintf(f, "%f,%f,%d,%d,%d ", lat, lon, xNearest, yNearest, pixel==0);
|
||||
}
|
||||
}
|
||||
//fprintf(f, "\n");
|
||||
}
|
||||
//fclose(f);
|
||||
|
||||
return projection;
|
||||
}
|
||||
|
||||
// Make an image transparent, so when overlaid on 3D map, we can see the underlying terrain
|
||||
// Image is full transparent below m_transparencyThreshold and fully opaque above m_opacityThreshold
|
||||
void APTDemodImageWorker::makeTransparent(QImage &image)
|
||||
{
|
||||
for (int y = 0; y < image.height(); y++)
|
||||
{
|
||||
for (int x = 0; x < image.width(); x++)
|
||||
{
|
||||
QRgb pixel = image.pixel(x, y);
|
||||
int grey = qGray(pixel);
|
||||
if (grey < m_settings.m_transparencyThreshold)
|
||||
{
|
||||
// Make fully transparent
|
||||
pixel = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), 0);
|
||||
image.setPixel(x, y, pixel);
|
||||
}
|
||||
else if (grey < m_settings.m_opacityThreshold)
|
||||
{
|
||||
// Make slightly transparent
|
||||
float opacity = 1.0f - ((m_settings.m_opacityThreshold - grey) / (float)(m_settings.m_opacityThreshold - m_settings.m_transparencyThreshold));
|
||||
opacity = opacity * 255.0f;
|
||||
opacity = std::min(255.0f, opacity);
|
||||
opacity = std::max(0.0f, opacity);
|
||||
pixel = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), (int)std::round(opacity));
|
||||
image.setPixel(x, y, pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APTDemodImageWorker::sendImageToMap(QImage image, QStringList imageTypes)
|
||||
{
|
||||
// Send to Map feature
|
||||
MessagePipes& messagePipes = MainCore::instance()->getMessagePipes();
|
||||
QList<MessageQueue*> *mapMessageQueues = messagePipes.getMessageQueues(m_aptDemod, "mapitems");
|
||||
if (mapMessageQueues)
|
||||
{
|
||||
// Only display one channel on map
|
||||
QImage selectedChannel;
|
||||
if (m_settings.m_channels == APTDemodSettings::BOTH_CHANNELS) {
|
||||
selectedChannel = extractImage(image, APTDemodSettings::CHANNEL_B);
|
||||
} else {
|
||||
selectedChannel = image;
|
||||
}
|
||||
|
||||
// Project image to geodetic coords (lat & lon)
|
||||
selectedChannel = projectImage(selectedChannel);
|
||||
//selectedChannel.save("projected.png");
|
||||
|
||||
// Use alpha channel to remove land & sea
|
||||
makeTransparent(selectedChannel);
|
||||
|
||||
// Encode image as base64 PNG
|
||||
QByteArray ba;
|
||||
QBuffer buffer(&ba);
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
selectedChannel.save(&buffer, "PNG");
|
||||
QByteArray data = ba.toBase64();
|
||||
|
||||
// Create name for the image
|
||||
QString satName = m_satelliteName;
|
||||
satName.replace(" ", "_");
|
||||
QString name = QString("apt_%1_%2").arg(satName).arg(m_settings.m_aosDateTime.toString("yyyyMMdd_hhmmss"));
|
||||
|
||||
// Send name to GUI
|
||||
m_messageQueueToGUI->push(APTDemod::MsgMapImageName::create(name));
|
||||
|
||||
QList<MessageQueue*>::iterator it = mapMessageQueues->begin();
|
||||
for (; it != mapMessageQueues->end(); ++it)
|
||||
{
|
||||
SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem();
|
||||
swgMapItem->setName(new QString(name));
|
||||
swgMapItem->setImage(new QString(data));
|
||||
swgMapItem->setAltitude(3000.0); // Typical cloud height - So it appears above objects on the ground
|
||||
swgMapItem->setType(1);
|
||||
swgMapItem->setImageTileEast(m_tileEast);
|
||||
swgMapItem->setImageTileWest(m_tileWest);
|
||||
swgMapItem->setImageTileNorth(m_tileNorth);
|
||||
swgMapItem->setImageTileSouth(m_tileSouth);
|
||||
|
||||
MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_aptDemod, swgMapItem);
|
||||
(*it)->push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +718,7 @@ void APTDemodImageWorker::sendLineToGUI()
|
||||
{
|
||||
if (m_messageQueueToGUI)
|
||||
{
|
||||
float *pixels = m_image.prow[m_image.nrow];
|
||||
float *pixels = m_image.prow[m_image.nrow-1];
|
||||
uchar *line;
|
||||
APTDemod::MsgLine *msg = APTDemod::MsgLine::create(&line);
|
||||
|
||||
@@ -219,12 +748,12 @@ void APTDemodImageWorker::sendLineToGUI()
|
||||
}
|
||||
}
|
||||
|
||||
QImage APTDemodImageWorker::processImage(QStringList& imageTypes)
|
||||
QImage APTDemodImageWorker::processImage(QStringList& imageTypes, APTDemodSettings::ChannelSelection channels)
|
||||
{
|
||||
copyImage(&m_tempImage, &m_image);
|
||||
|
||||
// Calibrate channels according to wavelength
|
||||
if (m_tempImage.nrow >= APT_CALIBRATION_ROWS)
|
||||
// Calibrate channels according to wavelength (1.7x to stop flickering)
|
||||
if (m_tempImage.nrow >= 1.7 * APT_CALIBRATION_ROWS)
|
||||
{
|
||||
m_tempImage.chA = apt_calibrate(m_tempImage.prow, m_tempImage.nrow, APT_CHA_OFFSET, APT_CH_WIDTH);
|
||||
m_tempImage.chB = apt_calibrate(m_tempImage.prow, m_tempImage.nrow, APT_CHB_OFFSET, APT_CH_WIDTH);
|
||||
@@ -233,9 +762,9 @@ QImage APTDemodImageWorker::processImage(QStringList& imageTypes)
|
||||
"Visible (0.58-0.68 um)",
|
||||
"Near-IR (0.725-1.0 um)",
|
||||
"Near-IR (1.58-1.64 um)",
|
||||
"Mid-infrared (3.55-3.93 um)",
|
||||
"Thermal-infrared (10.3-11.3 um)",
|
||||
"Thermal-infrared (11.5-12.5 um)"
|
||||
"Thermal-infrared (11.5-12.5 um)",
|
||||
"Mid-infrared (3.55-3.93 um)"
|
||||
});
|
||||
|
||||
imageTypes.append(channelTypes[m_tempImage.chA]);
|
||||
@@ -243,8 +772,9 @@ QImage APTDemodImageWorker::processImage(QStringList& imageTypes)
|
||||
}
|
||||
|
||||
// Crop noise due to low elevation at top and bottom of image
|
||||
if (m_settings.m_cropNoise)
|
||||
if (m_settings.m_cropNoise) {
|
||||
m_tempImage.zenith -= apt_cropNoise(&m_tempImage);
|
||||
}
|
||||
|
||||
// Denoise filter
|
||||
if (m_settings.m_denoise)
|
||||
@@ -305,10 +835,65 @@ QImage APTDemodImageWorker::processImage(QStringList& imageTypes)
|
||||
}
|
||||
}
|
||||
}
|
||||
return extractImage(m_colourImage);
|
||||
return extractImage(m_colourImage, channels);
|
||||
}
|
||||
else if (channels == APTDemodSettings::TEMPERATURE)
|
||||
{
|
||||
// Temperature calibration
|
||||
int satnum = 15;
|
||||
if (m_satelliteName == "NOAA 18") {
|
||||
satnum = 18;
|
||||
} else if (m_satelliteName == "NOAA 19") {
|
||||
satnum = 19;
|
||||
}
|
||||
apt_temperature(satnum, &m_tempImage, APT_CHB_OFFSET, APT_CH_WIDTH);
|
||||
|
||||
// Apply colour palette
|
||||
for (int r = 0; r < m_tempImage.nrow; r++)
|
||||
{
|
||||
uchar *l = m_colourImage.scanLine(r);
|
||||
for (int i = 0; i < APT_CH_WIDTH; i++)
|
||||
{
|
||||
float p = m_tempImage.prow[r][i+APT_CHB_OFFSET];
|
||||
uchar q = roundAndClip(p);
|
||||
l[i*3] = apt_TempPalette[q*3];
|
||||
l[i*3+1] = apt_TempPalette[q*3+1];
|
||||
l[i*3+2] = apt_TempPalette[q*3+2];
|
||||
}
|
||||
}
|
||||
return m_colourImage.copy(0, 0, APT_CH_WIDTH, m_tempImage.nrow);
|
||||
}
|
||||
else if (channels == APTDemodSettings::PALETTE)
|
||||
{
|
||||
if ((m_settings.m_palette >= 0) && (m_settings.m_palette < m_palettes.size()))
|
||||
{
|
||||
// Apply colour palette
|
||||
for (int r = 0; r < m_tempImage.nrow; r++)
|
||||
{
|
||||
uchar *l = m_colourImage.scanLine(r);
|
||||
for (int i = 0; i < APT_CH_WIDTH; i++)
|
||||
{
|
||||
float pA = m_tempImage.prow[r][i+APT_CHA_OFFSET];
|
||||
float pB = m_tempImage.prow[r][i+APT_CHB_OFFSET];
|
||||
uchar qA = roundAndClip(pA);
|
||||
uchar qB = roundAndClip(pB);
|
||||
QRgb rgb = m_palettes[m_settings.m_palette].pixel(qA, qB);
|
||||
l[i*3] = qRed(rgb);
|
||||
l[i*3+1] = qGreen(rgb);
|
||||
l[i*3+2] = qBlue(rgb);
|
||||
}
|
||||
}
|
||||
return m_colourImage.copy(0, 0, APT_CH_WIDTH, m_tempImage.nrow);
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "APTDemodImageWorker::processImage - Illegal palette number: " << m_settings.m_palette;
|
||||
return QImage();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Extract grey-scale image
|
||||
for (int r = 0; r < m_tempImage.nrow; r++)
|
||||
{
|
||||
uchar *l = m_greyImage.scanLine(r);
|
||||
@@ -319,43 +904,92 @@ QImage APTDemodImageWorker::processImage(QStringList& imageTypes)
|
||||
l[i] = roundAndClip(p);
|
||||
}
|
||||
}
|
||||
return extractImage(m_greyImage);
|
||||
return extractImage(m_greyImage, channels);
|
||||
}
|
||||
}
|
||||
|
||||
QImage APTDemodImageWorker::extractImage(QImage image)
|
||||
QImage APTDemodImageWorker::extractImage(QImage image, APTDemodSettings::ChannelSelection channels)
|
||||
{
|
||||
if (m_settings.m_channels == APTDemodSettings::BOTH_CHANNELS) {
|
||||
if (channels == APTDemodSettings::BOTH_CHANNELS) {
|
||||
return image.copy(0, 0, APT_IMG_WIDTH, m_tempImage.nrow);
|
||||
} else if (m_settings.m_channels == APTDemodSettings::CHANNEL_A) {
|
||||
} else if (channels == APTDemodSettings::CHANNEL_A) {
|
||||
return image.copy(APT_CHA_OFFSET, 0, APT_CH_WIDTH, m_tempImage.nrow);
|
||||
} else {
|
||||
return image.copy(APT_CHB_OFFSET, 0, APT_CH_WIDTH, m_tempImage.nrow);
|
||||
}
|
||||
}
|
||||
|
||||
void APTDemodImageWorker::prependPath(QString &filename)
|
||||
{
|
||||
if (!m_settings.m_autoSavePath.isEmpty())
|
||||
{
|
||||
if (m_settings.m_autoSavePath.endsWith('/')) {
|
||||
filename = m_settings.m_autoSavePath + filename;
|
||||
} else {
|
||||
filename = m_settings.m_autoSavePath + '/' + filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APTDemodImageWorker::saveImageToDisk()
|
||||
{
|
||||
QStringList imageTypes;
|
||||
QImage image = processImage(imageTypes);
|
||||
QImage image = processImage(imageTypes, APTDemodSettings::BOTH_CHANNELS);
|
||||
|
||||
if (image.height() >= m_settings.m_autoSaveMinScanLines)
|
||||
{
|
||||
QString filename;
|
||||
QDateTime datetime = QDateTime::currentDateTime();
|
||||
filename = QString("apt_%1_%2.png").arg(m_satelliteName.replace(" ", "_")).arg(datetime.toString("yyyyMMdd_hhmm"));
|
||||
QDateTime dateTime;
|
||||
QString dt;
|
||||
if (m_settings.m_aosDateTime.isValid()) {
|
||||
dateTime = m_settings.m_aosDateTime;
|
||||
} else {
|
||||
dateTime = QDateTime::currentDateTime();
|
||||
}
|
||||
dt = dateTime.toString("yyyyMMdd_hhmm");
|
||||
QString sat = m_satelliteName;
|
||||
sat.replace(" ", "_");
|
||||
|
||||
if (!m_settings.m_autoSavePath.isEmpty())
|
||||
if (m_settings.m_saveCombined)
|
||||
{
|
||||
if (m_settings.m_autoSavePath.endsWith('/')) {
|
||||
filename = m_settings.m_autoSavePath + filename;
|
||||
} else {
|
||||
filename = m_settings.m_autoSavePath + '/' + filename;
|
||||
filename = QString("apt_%1_%2.png").arg(sat).arg(dt);
|
||||
prependPath(filename);
|
||||
if (!image.save(filename)) {
|
||||
qCritical() << "Failed to save APT image to: " << filename;
|
||||
}
|
||||
}
|
||||
|
||||
if (!image.save(filename)) {
|
||||
qCritical() << "Failed to save APT image to: " << filename;
|
||||
QImage chA = extractImage(image, APTDemodSettings::CHANNEL_A);
|
||||
QImage chB = extractImage(image, APTDemodSettings::CHANNEL_B);
|
||||
|
||||
if (m_settings.m_saveSeparate)
|
||||
{
|
||||
filename = QString("apt_%1_%2_cha.png").arg(sat).arg(dt);
|
||||
prependPath(filename);
|
||||
if (!chA.save(filename)) {
|
||||
qCritical() << "Failed to save APT image to: " << filename;
|
||||
}
|
||||
filename = QString("apt_%1_%2_chb.png").arg(sat).arg(dt);
|
||||
prependPath(filename);
|
||||
if (!chB.save(filename)) {
|
||||
qCritical() << "Failed to save APT image to: " << filename;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_settings.m_saveProjection)
|
||||
{
|
||||
filename = QString("apt_%1_%2_cha_eqi_cylindrical.png").arg(sat).arg(dt);
|
||||
prependPath(filename);
|
||||
QImage chAProj = projectImage(chA);
|
||||
if (!chAProj.save(filename)) {
|
||||
qCritical() << "Failed to save APT image to: " << filename;
|
||||
}
|
||||
filename = QString("apt_%1_%2_chb_eqi_cylindrical.png").arg(sat).arg(dt);
|
||||
prependPath(filename);
|
||||
QImage chBProj = projectImage(chB);
|
||||
if (!chBProj.save(filename)) {
|
||||
qCritical() << "Failed to save APT image to: " << filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user