@@ -1,5 +1,9 @@ | |||
# Created by https://www.gitignore.io/api/c | |||
# Edit at https://www.gitignore.io/?templates=c | |||
# Created by https://www.toptal.com/developers/gitignore/api/c,cmake | |||
# Edit at https://www.toptal.com/developers/gitignore?templates=c,cmake | |||
### C ### | |||
# Prerequisites | |||
*.d | |||
# Object files | |||
*.o | |||
@@ -26,6 +30,7 @@ | |||
*.dll | |||
*.so | |||
*.so.* | |||
*.dylib | |||
# Executables | |||
*.exe | |||
@@ -33,6 +38,7 @@ | |||
*.app | |||
*.i*86 | |||
*.x86_64 | |||
*.hex | |||
# Debug files | |||
*.dSYM/ | |||
@@ -40,19 +46,40 @@ | |||
*.idb | |||
*.pdb | |||
# Program specifics | |||
# Kernel Module Compile Results | |||
*.mod* | |||
*.cmd | |||
.tmp_versions/ | |||
modules.order | |||
Module.symvers | |||
Mkfile.old | |||
dkms.conf | |||
### CMake ### | |||
CMakeLists.txt.user | |||
CMakeCache.txt | |||
CMakeFiles | |||
CMakeScripts | |||
Testing | |||
Makefile | |||
cmake_install.cmake | |||
install_manifest.txt | |||
compile_commands.json | |||
CTestTestfile.cmake | |||
_deps | |||
### CMake Patch ### | |||
# External projects | |||
*-prefix/ | |||
# End of https://www.toptal.com/developers/gitignore/api/c,cmake | |||
*.png | |||
!textlogo.png | |||
!palettes/*.png | |||
!luts/*.png | |||
!util/*.png | |||
*.wav | |||
aptdec | |||
*.ogg | |||
.vscode/ | |||
build/ | |||
libpng/ | |||
zlib/ | |||
winbuild/ | |||
winpath/ | |||
libsndfile-1.0.29-win64.zip | |||
libsndfile-1.0.29-win64/ |
@@ -1,3 +1,3 @@ | |||
[submodule "src/argparse"] | |||
path = src/argparse | |||
[submodule "aptdec-cli/argparse"] | |||
path = aptdec-cli/argparse | |||
url = https://github.com/cofyc/argparse |
@@ -1,81 +1,86 @@ | |||
cmake_minimum_required (VERSION 3.0.0) | |||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") | |||
cmake_minimum_required(VERSION 3.0.0) | |||
project(aptdec C) | |||
set(CMAKE_C_STANDARD 99) | |||
set(CMAKE_C_EXTENSIONS OFF) | |||
# Get version | |||
find_package(Git) | |||
if (GIT_FOUND) | |||
execute_process(COMMAND ${GIT_EXECUTABLE} describe --tag WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} OUTPUT_VARIABLE GIT_TAG OUTPUT_STRIP_TRAILING_WHITESPACE) | |||
set(VERSION "${GIT_TAG}") | |||
else() | |||
set(VERSION "Unknown") | |||
endif() | |||
project(aptdec C) | |||
include(GNUInstallDirs) | |||
# libpng | |||
# aptdec-cli | |||
find_package(PNG) | |||
# libsndfile | |||
find_package(LibSndFile) | |||
set(LIB_C_SOURCE_FILES src/color.c src/dsp.c src/filter.c src/image.c src/algebra.c src/libs/median.c src/util.c src/calibration.c) | |||
set(EXE_C_SOURCE_FILES src/main.c src/pngio.c src/argparse/argparse.c src/util.c) | |||
set(LIB_C_HEADER_FILES src/apt.h) | |||
# Link with static library for aptdec executable, so we don't need to set the path | |||
add_library(aptstatic STATIC ${LIB_C_SOURCE_FILES}) | |||
# Create shared library for 3rd party apps | |||
add_library(apt SHARED ${LIB_C_SOURCE_FILES}) | |||
set_target_properties(apt PROPERTIES PUBLIC_HEADER ${LIB_C_HEADER_FILES}) | |||
add_compile_definitions(PALETTE_DIR="${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}/palettes") | |||
if (PNG_FOUND AND LIBSNDFILE_FOUND) | |||
add_executable(aptdec ${EXE_C_SOURCE_FILES}) | |||
include_directories(${PNG_PNG_INCLUDE_DIR}) | |||
include_directories(${LIBSNDFILE_INCLUDE_DIR}) | |||
target_link_libraries(aptdec PRIVATE PNG::PNG) | |||
target_link_libraries(aptdec PRIVATE ${LIBSNDFILE_LIBRARY}) | |||
target_link_libraries(aptdec PRIVATE aptstatic) | |||
if (MSVC) | |||
target_compile_options(aptdec PRIVATE /D_CRT_SECURE_NO_WARNINGS=1 /DAPT_API_STATIC) | |||
find_path(SNDFILE_INCLUDE_DIR sndfile.h) | |||
find_library(SNDFILE_LIBRARIES NAMES sndfile libsndfile PATH) | |||
if(PNG_FOUND AND SNDFILE_LIBRARIES AND SNDFILE_INCLUDE_DIR) | |||
set(APTDEC_CLI_SOURCE_FILES | |||
aptdec-cli/argparse/argparse.c | |||
aptdec-cli/main.c | |||
aptdec-cli/pngio.c | |||
aptdec-cli/util.c | |||
) | |||
add_executable(aptdec-cli ${APTDEC_CLI_SOURCE_FILES}) | |||
target_compile_definitions(aptdec-cli PRIVATE "VERSION=\"${VERSION}\"") | |||
target_include_directories(aptdec-cli PRIVATE ${PNG_INCLUDE_DIRS}) | |||
target_include_directories(aptdec-cli PRIVATE ${SNDFILE_INCLUDE_DIR}) | |||
target_include_directories(aptdec-cli PRIVATE libaptdec/include/) | |||
target_link_libraries(aptdec-cli PRIVATE ${PNG_LIBRARIES}) | |||
target_link_libraries(aptdec-cli PRIVATE ${SNDFILE_LIBRARIES}) | |||
target_link_libraries(aptdec-cli PRIVATE aptdec) | |||
if(MSVC) | |||
target_compile_options(aptdec-cli PRIVATE /D_CRT_SECURE_NO_WARNINGS=1) | |||
else() | |||
# Math | |||
target_link_libraries(aptdec PRIVATE m) | |||
# Throw errors on warnings on release builds | |||
if(CMAKE_BUILD_TYPE MATCHES "Release") | |||
target_compile_options(aptdec PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) | |||
else() | |||
target_compile_options(aptdec PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) | |||
endif() | |||
target_link_libraries(aptdec-cli PRIVATE m) | |||
target_compile_options(aptdec-cli PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) | |||
endif() | |||
else() | |||
MESSAGE(WARNING "Only building apt library, as not all of the required libraries were found for aptdec.") | |||
message(WARNING "Not building aptdec-cli as some/all required dependencies are missing, libaptdec will still be built") | |||
endif() | |||
if (MSVC) | |||
target_compile_options(apt PRIVATE /D_CRT_SECURE_NO_WARNINGS=1 /DAPT_API_EXPORT) | |||
target_compile_options(aptstatic PRIVATE /D_CRT_SECURE_NO_WARNINGS=1 /DAPT_API_STATIC) | |||
# libaptdec | |||
set(LIBAPTDEC_HEADER_FILES libaptdec/include/aptdec.h) | |||
set(LIBAPTDEC_SOURCE_FILES | |||
libaptdec/algebra.c | |||
libaptdec/calibration.c | |||
libaptdec/color.c | |||
libaptdec/dsp.c | |||
libaptdec/filter.c | |||
libaptdec/image.c | |||
libaptdec/util.c | |||
libaptdec/effects.c | |||
) | |||
add_library(aptdec SHARED ${LIBAPTDEC_SOURCE_FILES}) | |||
set_target_properties(aptdec PROPERTIES PUBLIC_HEADER ${LIBAPTDEC_HEADER_FILES}) | |||
target_include_directories(aptdec PRIVATE libaptdec/include/) | |||
target_compile_definitions(aptdec PRIVATE "VERSION=\"${VERSION}\"") | |||
if(MSVC) | |||
target_compile_options(aptdec PRIVATE /D_CRT_SECURE_NO_WARNINGS=1 /DAPTDEC_API_EXPORT) | |||
else() | |||
# Math | |||
target_link_libraries(apt PRIVATE m) | |||
target_link_libraries(aptstatic PRIVATE m) | |||
if(CMAKE_BUILD_TYPE MATCHES "Release") | |||
target_compile_options(apt PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) | |||
else() | |||
target_compile_options(apt PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) | |||
endif() | |||
target_link_libraries(aptdec PRIVATE m) | |||
target_compile_options(aptdec PRIVATE -Wall -Wextra -pedantic -Wno-missing-field-initializers) | |||
endif() | |||
# TODO: get this from git | |||
set(PROJECT_VERSION "1.7.0") | |||
# CPack | |||
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") | |||
# Packaging | |||
set(CPACK_PACKAGE_VERSION "${VERSION}") | |||
set(CPACK_PACKAGE_NAME "aptdec") | |||
set(CPACK_PACKAGE_CONTACT "Xerbo (xerbo@protonmail.com)") | |||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "NOAA APT satellite imagery decoder") | |||
set(CPACK_PACKAGE_DESCRIPTION "Aptdec is a FOSS program that decodes images transmitted by NOAA weather satellites. These satellites transmit constantly (among other things) medium resolution (4km/px) images of the earth over a analog mode called APT.") | |||
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64") | |||
set(CPACK_PACKAGE_DESCRIPTION "Aptdec is a FOSS library/program that decodes images transmitted by the NOAA POES weather satellites. These satellites transmit constantly (among other things) medium resolution (4km/px) images of the earth over a analog mode called APT.") | |||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) | |||
IF(NOT WIN32) | |||
if(NOT WIN32) | |||
set(CPACK_GENERATOR "DEB;TGZ") | |||
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}.${CMAKE_SYSTEM_PROCESSOR}") | |||
else() | |||
@@ -89,11 +94,10 @@ else() | |||
endif() | |||
endif() | |||
if (TARGET aptdec) | |||
if(TARGET aptdec-cli) | |||
install(TARGETS aptdec RUNTIME DESTINATION bin) | |||
install(DIRECTORY "${PROJECT_SOURCE_DIR}/palettes" DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}) | |||
endif() | |||
install(TARGETS apt PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/apt LIBRARY DESTINATION lib) | |||
install(TARGETS aptdec PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/aptdec LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) | |||
include(CPack) |
@@ -1,28 +1,27 @@ | |||
# How To Contribute | |||
Thank you for showing interest to contributing to aptdec, these guidelines layout how you should go about reporting issues and contributing. | |||
Thanks for showing an interest in improving aptdec, these guidelines outline how you should go about contributing to the project. | |||
## Did you Find A Bug? | |||
## Did you find a bug? | |||
1. Ensure the bug was not already reported by searching on GitHub under Issues. | |||
2. If you're unable to find an closed issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible and an audio file that demonstrates what is unexpected behaviour. | |||
3. If possible, use the report templates to create the issue. | |||
1. Make sure the bug has not already been reported | |||
2. If an issue already exists, leave a thumbs up to "bump" the issue | |||
3. If an issue doesn't exist, open a new one with a clear title and description and, if relevant, any files that cause the behavior | |||
## Do you have a patch that fixes a bug? | |||
## Do you have a patch that fixes a bug/adds a feature? | |||
1. Fork the repository and push your changes on a new branch. | |||
2. Add your changes on that branch, make sure to use sensible commit names. | |||
3. Open a new GitHub pull request to pull into master. | |||
4. Ensure the pull request description clearly describes the problem and solution. Include the relevant issue number if applicable. | |||
1. Fork this repository and put your changes on a *new branch* | |||
2. Make sure to use sensible commit names | |||
3. Open a new pull request into master | |||
If you don't have a GitHub account, you can email me the patch at `xerbo (at) protonmail (dot) com` | |||
## Coding style | |||
- Whitespaces should be denoted with tabs | |||
- This is FOSS software, consider that people will read your code, so make it easily readable. | |||
- If you're making major changes, make sure to create an issue to discuss it. | |||
The coding style of LeanHRPT is based off the [Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) with minor modifications (see `.clang-format`), in addition to this all files should use LF line endings and end in a newline. Use American english (i.e. "color", not "colour"). | |||
## Commit message style | |||
- Keep titles under 80 characters to prevent wrapping. | |||
- Make sure the commit message is descriptive of the change, dont be afraid to go into detail. | |||
- Split up large changes into multiple commits. | |||
- Keep titles short to prevent wrapping (descriptions exist) | |||
- Split up large changes into multiple commits | |||
- Never use hastags for sequential counting in commits, as this interferes with issue/PR referencing |
@@ -6,58 +6,63 @@ Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 20 | |||
## Introduction | |||
Aptdec is a FOSS program that decodes images transmitted by NOAA weather satellites. These satellites transmit constantly (among other things) medium resolution (4km/px) images of the earth over a analog mode called APT. | |||
These transmissions can easily be received with a cheap SDR and simple antenna. Then the transmission can be decoded in narrow FM mode. | |||
Aptdec is a FOSS library/program that decodes images transmitted by the NOAA POES weather satellites. These satellites transmit constantly (among other things) medium resolution (4km/px) images of the earth over a analog mode called APT. | |||
These transmissions can easily be received with a cheap SDR and simple antenna, the transmission can be demodulated in narrow FM mode. | |||
Aptdec can turn the audio recordings into PNG images and generate images such as: | |||
Aptdec can turn the audio into PNG images and generate other products such as: | |||
- Raw image: both channels with full telemetry included | |||
- Individual channel: one of the channels form the image | |||
- Temperature image: a temperature compensated image derived from the IR channel | |||
- Palleted image: a image where the color is derived from a palette (false color, etc) | |||
- Raw image: both channels (including telemetry) | |||
- Individual channel: one channel (including telemetry) | |||
- Visible image: a calibrated visible image of either channel 1 or 2 | |||
- Thermal image: a calibrated thermal image from channel B | |||
- LUT image: a image where the color is derived from a LUT (used for false color, etc) | |||
The input audio format can be anything supported by `libsndfile` (although only tested with WAV and FLAC). Sample rate doesn't matter, although lower samples rates will process faster. | |||
The input audio format can be anything supported by `libsndfile` (although only tested with WAV, FLAC and Ogg Vorbis). While sample rate doesn't matter, it is recommended to use 16640 Hz (4x oversampling). | |||
## Quick start | |||
Grab a release from [Releases](https://github.com/Xerbo/aptdec/releases) or compile from source: | |||
```sh | |||
sudo apt install cmake git gcc libsndfile-dev libpng-dev | |||
git clone --recursive https://github.com/Xerbo/aptdec.git && cd aptdec | |||
cmake -B build | |||
cmake --build build | |||
# Resulting binary is build/aptdec | |||
# Resulting binary is build/aptdec-cli | |||
``` | |||
In place builds are not supported. | |||
## Examples | |||
To create an image from `gqrx_20200527_115730_137914960.wav` (output filename will be `gqrx_20200527_115730_137914960-r.png`) | |||
To create an image from `gqrx_20200527_115730_137914960.wav` (output filename will be `gqrx_20200527_115730_137914960-raw.png`) | |||
```sh | |||
./aptdec gqrx_20200527_115730_137914960.wav | |||
aptdec-cli gqrx_20200527_115730_137914960.wav | |||
``` | |||
To manually set the output filename | |||
To manually specify the output filename | |||
```sh | |||
./aptdec -o image.png gqrx_20200527_115730_137914960.wav | |||
aptdec-cli -o image.png gqrx_20200527_115730_137914960.wav | |||
``` | |||
Decode all WAV files in the current directory and put them in `images` | |||
Decode all WAV files in the current directory: | |||
```sh | |||
mkdir images && ./aptdec -d images *.wav | |||
aptdec-cli *.wav | |||
``` | |||
Apply a denoise filter (see [Post-Processing Effects](#post-processing-effects) for a full list of post-processing effects) | |||
```sh | |||
./aptdec -e d gqrx_20200527_115730_137914960.wav | |||
aptdec-cli -e denoise gqrx_20200527_115730_137914960.wav | |||
``` | |||
Create a temperature compensated image for NOAA 18 | |||
Create a calibrated IR image from NOAA 18 | |||
```sh | |||
./aptdec -i t -s 18 gqrx_20200527_115730_137914960.wav | |||
aptdec-cli -i thermal gqrx_20200527_115730_137914960.wav | |||
``` | |||
Apply a falsecolor palette | |||
Apply a falsecolor LUT | |||
```sh | |||
./aptdec -i p -p palettes/WXtoImg-N18-HVC.png gqrx_20200527_115730_137914960.wav | |||
aptdec-cli -i lut -l luts/WXtoImg-N18-HVC.png gqrx_20200527_115730_137914960.wav | |||
``` | |||
## Usage | |||
@@ -65,72 +70,74 @@ Apply a falsecolor palette | |||
### Arguments | |||
``` | |||
-i [r|a|b|t|m|p] Output type (stackable) | |||
-e [t|h|l|d|p|f] Effects (stackable) | |||
-o <path> Output filename | |||
-d <path> Destination directory | |||
-s (15-19) Satellite number | |||
-p <path> Path to palette | |||
-r Realtime decode | |||
-g Gamma adjustment (1.0 = off) | |||
-h, --help show a help message and exit | |||
-i, --image=<str> set output image type (see below) | |||
-e, --effect=<str> add an effect (see below) | |||
-g, --gamma=<flt> gamma adjustment (1.0 = off) | |||
-s, --satellite=<int> satellite ID, must be between 15, 18 or 19 or NORAD | |||
-l, --lut=<str> path to a LUT | |||
-o, --output=<str> path of output image | |||
-r, --realtime decode in realtime | |||
``` | |||
### Image output types | |||
- `r`: Raw Image | |||
- `a`: Channel A | |||
- `b`: Channel B | |||
- `t`: Temperature | |||
- `p`: Palleted | |||
- `raw`: Raw Image | |||
- `a`: Channel A (including telemetry) | |||
- `b`: Channel B (including telemetry) | |||
- `thermal`: Calibrated thermal (MWIR/LWIR) image | |||
- `visible`: calibrated visible/NIR image | |||
- `lut`: LUT image, see also `-l/--lut` | |||
### Post-Processing Effects | |||
- `t`: Crop telemetry (only effects raw image) | |||
- `h`: Histogram equalise | |||
- `l`: Linear equalise | |||
- `d`: Denoise | |||
- `p`: Precipitation overlay | |||
- `f`: Flip image (for northbound passes) | |||
- `c`: Crop noise from ends of image | |||
- `strip`: Strip telemetry (only effects raw/a/b images) | |||
- `equalize`: Histogram equalise | |||
- `stretch`: Linear equalise | |||
- `denoise`: Denoise | |||
- ~~`precipitation`: Precipitation overlay~~ | |||
- `flip`: Flip image (for northbound passes) | |||
- `crop`: Crop noise from ends of image | |||
## Realtime decoding | |||
Aptdec even supports decoding in realtime. The following decodes the audio coming from the audio device `pulseaudio alsa_output.pci-0000_00_1b.0.analog-stereo` | |||
Aptdec supports decoding in realtime. The following captures and decodes audio from the `pipewire` interface: | |||
```sh | |||
arecord -f cd -D pipewire | aptdec -r - | |||
``` | |||
mkfifo /tmp/aptaudio | |||
aptdec -r /tmp/aptaudio | |||
sox -t pulseaudio alsa_output.pci-0000_00_1b.0.analog-stereo.monitor -c 1 -t wav /tmp/aptaudio | |||
or directly from an SDR: | |||
```sh | |||
rtl_fm -f 137.1M -g 40 -s 40k | sox -t raw -r 40k -e signed-integer -b 16 - -t wav - | aptdec -r - | |||
``` | |||
To stop the decode and calibrate the image simply kill the `sox` process. | |||
Image data will be streamed to `CURRENT_TIME.png` (deleted when finished). To stop the decode and normalize the image simply `Ctrl+C` the process. | |||
## Palette formatting | |||
## LUT format | |||
Palettes are just simple PNG images, 256x256px in size with 24bit RGB color. The X axis represents the value of Channel A and the Y axis the value of Channel B. | |||
LUT's are just plain PNG images, 256x256px in size with 24bit RGB color. The X axis represents the value of Channel A and the Y axis the value of Channel B. | |||
## Building for Windows | |||
You can cross build for Windows from Linux with the `build_windows.sh` script, you will need the following: | |||
``` | |||
You can cross build for Windows from Linux (recommended) using with the following commands: | |||
```sh | |||
sudo apt install wget cmake make mingw-w64 git unzip | |||
./build_windows.sh | |||
``` | |||
To build natively on Windows using MSVC, you will also need: git, ninja and cmake. Then run: | |||
To build natively on Windows using MSVC, you will need: git, ninja and cmake. Then run: | |||
``` | |||
.\build_windows.bat | |||
``` | |||
If you just wish to build libaptdec on Windows, libpng and libsndfile aren't needed. | |||
## Further Reading | |||
[User's Guide for Building and Operating | |||
Environmental Satellite Receiving Stations](https://noaasis.noaa.gov/NOAASIS/pubs/Users_Guide-Building_Receive_Stations_March_2009.pdf) | |||
If you only want to build libaptdec, libpng and libsndfile aren't needed. | |||
[NOAA KLM coefficients](https://web.archive.org/web/20141220021557/https://www.ncdc.noaa.gov/oa/pod-guide/ncdc/docs/klm/tables.htm) | |||
## References | |||
[NOAA Satellite specifications and more information](https://www1.ncdc.noaa.gov/pub/data/satellite/publications/podguides/N-15%20thru%20N-19/pdf/) | |||
- [User's Guide for Building and Operating Environmental Satellite Receiving Stations](https://noaasis.noaa.gov/NOAASIS/pubs/Users_Guide-Building_Receive_Stations_March_2009.pdf) | |||
- [NOAA KLM Users Guide](https://archive.org/details/noaa-klm-guide) | |||
## License | |||
@@ -0,0 +1,490 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#ifndef _MSC_VER | |||
#include <libgen.h> | |||
#else | |||
#include <windows.h> | |||
#endif | |||
#include <math.h> | |||
#include <sndfile.h> | |||
#include <time.h> | |||
#include <signal.h> | |||
#include <float.h> | |||
#include <aptdec.h> | |||
#include "argparse/argparse.h" | |||
#include "pngio.h" | |||
#include "util.h" | |||
// Maximum height of an APT image that can be decoded | |||
#define APTDEC_MAX_HEIGHT 3000 | |||
#define STRING_SPLIT(src, dst, delim, n) \ | |||
char *dst[n]; \ | |||
size_t dst##_len = 0; \ | |||
{char *token = strtok(src, delim); \ | |||
while (token != NULL && dst##_len < n) { \ | |||
dst[dst##_len++] = token; \ | |||
token = strtok(NULL, delim); \ | |||
}} | |||
typedef struct { | |||
char *type; // Output image type | |||
char *effects; // Effects on the image | |||
int satellite; // The satellite number | |||
int realtime; // Realtime decoding | |||
char *filename; // Output filename | |||
char *lut; // Filename of lut | |||
} options_t; | |||
typedef struct { | |||
SNDFILE *file; | |||
SF_INFO info; | |||
} SNDFILE_t; | |||
// Function declarations | |||
static int process_file(const char *path, options_t *opts); | |||
static int freq_from_filename(const char *filename); | |||
static int satid_from_freq(int freq); | |||
static size_t callback(float *samples, size_t count, void *context); | |||
static void write_line(writer_t *png, float *row); | |||
static volatile int sigint_stop = 0; | |||
void sigint_handler(int signum) { | |||
(void)signum; | |||
sigint_stop = 1; | |||
} | |||
// Basename is unsupported by MSVC | |||
// This implementation is GNU style | |||
#ifdef _MSC_VER | |||
char *basename(const char *filename) { | |||
char *p = strrchr(filename, '/'); | |||
return p ? p + 1 : (char *)filename; | |||
} | |||
#endif | |||
int main(int argc, const char **argv) { | |||
char version[128]; | |||
get_version(version); | |||
printf("%s\n", version); | |||
// clang-format off | |||
options_t opts = { | |||
.type = "raw", | |||
.effects = "", | |||
.satellite = 0, | |||
.realtime = 0, | |||
.filename = "", | |||
.lut = "", | |||
}; | |||
// clang-format on | |||
static const char *const usages[] = { | |||
"aptdec-cli [options] [[--] sources]", | |||
"aptdec-cli [sources]", | |||
NULL, | |||
}; | |||
struct argparse_option options[] = { | |||
OPT_HELP(), | |||
OPT_GROUP("Image options"), | |||
OPT_STRING('i', "image", &opts.type, "set output image type (see the README for a list)", NULL, 0, 0), | |||
OPT_STRING('e', "effect", &opts.effects, "add an effect (see the README for a list)", NULL, 0, 0), | |||
OPT_GROUP("Satellite options"), | |||
OPT_INTEGER('s', "satellite", &opts.satellite, "satellite ID, must be either NORAD or between 15 and 19", NULL, 0, 0), | |||
OPT_GROUP("Paths"), | |||
OPT_STRING('l', "lut", &opts.lut, "path to a LUT", NULL, 0, 0), | |||
OPT_STRING('o', "output", &opts.filename, "path of output image", NULL, 0, 0), | |||
OPT_GROUP("Misc"), | |||
OPT_BOOLEAN('r', "realtime", &opts.realtime, "decode in realtime", NULL, 0, 0), | |||
OPT_END(), | |||
}; | |||
struct argparse argparse; | |||
argparse_init(&argparse, options, usages, 0); | |||
argparse_describe(&argparse, | |||
"\nA lightweight FOSS NOAA APT satellite imagery decoder.", | |||
"\nSee `README.md` for a full description of command line arguments and `LICENSE` for licensing conditions." | |||
); | |||
argc = argparse_parse(&argparse, argc, argv); | |||
if (argc == 0) { | |||
argparse_usage(&argparse); | |||
} | |||
if (argc > 1 && opts.realtime) { | |||
error("Cannot use -r/--realtime with multiple input files"); | |||
} | |||
// Actually decode the files | |||
for (int i = 0; i < argc; i++) { | |||
if (opts.satellite == 25338) opts.satellite = 15; | |||
if (opts.satellite == 28654) opts.satellite = 18; | |||
if (opts.satellite == 33591) opts.satellite = 19; | |||
if (opts.satellite == 0) { | |||
int freq = freq_from_filename(argv[i]); | |||
if (freq == 0) { | |||
opts.satellite = 19; | |||
warning("Satellite not specified, defaulting to NOAA-19"); | |||
} else { | |||
opts.satellite = satid_from_freq(freq); | |||
printf("Satellite not specified, choosing to NOAA-%i based on filename\n", opts.satellite); | |||
} | |||
} | |||
if (opts.satellite != 15 && opts.satellite != 18 && opts.satellite != 19) { | |||
error("Invalid satellite ID"); | |||
} | |||
process_file(argv[i], &opts); | |||
} | |||
return 0; | |||
} | |||
static int process_file(const char *path, options_t *opts) { | |||
const char *path_basename = basename((char *)path); | |||
const char *dot = strrchr(path_basename, '.'); | |||
char name[256]; | |||
if (dot == NULL) { | |||
strncpy(name, path_basename, 255); | |||
} else { | |||
strncpy(name, path_basename, clamp_int(dot - path_basename, 0, 255)); | |||
} | |||
// Set filename to time when reading from stdin | |||
if (strcmp(name, "-") == 0) { | |||
time_t t = time(NULL); | |||
strcpy(name, ctime(&t)); | |||
} | |||
writer_t *realtime_png; | |||
if (opts->realtime) { | |||
char filename[269]; | |||
sprintf(filename, "%s-decoding.png", name); | |||
realtime_png = writer_init(filename, APT_REGION_FULL, APTDEC_MAX_HEIGHT, PNG_COLOR_TYPE_GRAY, "Unknown"); | |||
// Capture Ctrl+C | |||
signal(SIGINT, sigint_handler); | |||
} | |||
// Create a libsndfile instance | |||
SNDFILE_t audioFile; | |||
audioFile.file = sf_open(path, SFM_READ, &audioFile.info); | |||
if (audioFile.file == NULL) { | |||
error_noexit("Could not open file"); | |||
return 0; | |||
} | |||
printf("Input file: %s\n", path_basename); | |||
printf("Input sample rate: %d\n", audioFile.info.samplerate); | |||
// Create a libaptdec instances | |||
aptdec_t *aptdec = aptdec_init(audioFile.info.samplerate); | |||
if (aptdec == NULL) { | |||
sf_close(audioFile.file); | |||
error_noexit("Error initializing libaptdec, sample rate too high/low?"); | |||
return 0; | |||
} | |||
// Decode image | |||
float *data = (float *)malloc(APT_IMG_WIDTH * (APTDEC_MAX_HEIGHT+1) * sizeof(float)); | |||
size_t rows; | |||
for (rows = 0; rows < APTDEC_MAX_HEIGHT; rows++) { | |||
float *row = &data[rows * APT_IMG_WIDTH]; | |||
// Break the loop when there are no more samples or the process has been sent SIGINT | |||
if (aptdec_getrow(aptdec, row, callback, &audioFile) == 0 || sigint_stop) { | |||
break; | |||
} | |||
if (opts->realtime) { | |||
write_line(realtime_png, row); | |||
} | |||
fprintf(stderr, "Row: %zu/%zu\r", rows+1, audioFile.info.frames/audioFile.info.samplerate * 2); | |||
fflush(stderr); | |||
} | |||
printf("\n"); | |||
// Close stream | |||
sf_close(audioFile.file); | |||
aptdec_free(aptdec); | |||
if (opts->realtime) { | |||
#pragma GCC diagnostic push | |||
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" | |||
writer_free(realtime_png); | |||
#pragma GCC diagnostic pop | |||
char filename[269]; | |||
sprintf(filename, "%s-decoding.png", name); | |||
remove(filename); | |||
} | |||
// Normalize | |||
int error; | |||
apt_image_t img = apt_normalize(data, rows, opts->satellite, &error); | |||
if (error) { | |||
error_noexit("Normalization failed"); | |||
return 0; | |||
} | |||
// clang-format off | |||
const char *channel_name[] = { "?", "1", "2", "3A", "4", "5", "3B" }; | |||
const char *channel_desc[] = { | |||
"unknown", | |||
"visible (0.58-0.68 um)", | |||
"near-infrared (0.725-1.0 um)", | |||
"near-infrared (1.58-1.64 um)", | |||
"thermal-infrared (10.3-11.3 um)", | |||
"thermal-infrared (11.5-12.5 um)", | |||
"mid-infrared (3.55-3.93 um)" | |||
}; | |||
printf("Channel A: %s - %s\n", channel_name[img.ch[0]], channel_desc[img.ch[0]]); | |||
printf("Channel B: %s - %s\n", channel_name[img.ch[1]], channel_desc[img.ch[1]]); | |||
// clang-format on | |||
STRING_SPLIT(opts->type, images, ",", 10) | |||
STRING_SPLIT(opts->effects, effects, ",", 10) | |||
STRING_SPLIT(opts->filename, filenames, ",", 10) | |||
for (size_t i = 0; i < effects_len; i++) { | |||
if (strcmp(effects[i], "crop") == 0) { | |||
apt_crop(&img); | |||
} else if (strcmp(effects[i], "denoise") == 0) { | |||
apt_denoise(&img, APT_REGION_CHA); | |||
apt_denoise(&img, APT_REGION_CHB); | |||
} else if (strcmp(effects[i], "flip") == 0) { | |||
apt_flip(&img, APT_REGION_CHA); | |||
apt_flip(&img, APT_REGION_CHB); | |||
} | |||
} | |||
for (size_t i = 0; i < images_len; i++) { | |||
const char *base; | |||
if (i < filenames_len) { | |||
base = filenames[i]; | |||
} else { | |||
base = name; | |||
} | |||
if (strcmp(images[i], "thermal") == 0) { | |||
if (img.ch[1] >= 4) { | |||
char filename[269]; | |||
sprintf(filename, "%s-thermal.png", base); | |||
char description[128]; | |||
sprintf(description, "Calibrated thermal image, channel %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]); | |||
// Perform visible calibration | |||
apt_image_t _img = apt_image_clone(img); | |||
apt_calibrate_thermal(&_img, APT_REGION_CHA); | |||
writer_t *writer = writer_init(filename, APT_REGION_CHB, img.rows, PNG_COLOR_TYPE_RGB, description); | |||
writer_write_image_gradient(writer, &_img, temperature_gradient); | |||
writer_free(writer); | |||
free(_img.data); | |||
} else { | |||
error_noexit("Could not generate thermal image, no infrared channel"); | |||
} | |||
} else if (strcmp(images[i], "visible") == 0) { | |||
if (img.ch[0] <= 2) { | |||
char filename[269]; | |||
sprintf(filename, "%s-visible.png", base); | |||
char description[128]; | |||
sprintf(description, "Calibrated visible image, channel %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]); | |||
// Perform visible calibration | |||
apt_image_t _img = apt_image_clone(img); | |||
apt_calibrate_visible(&_img, APT_REGION_CHA); | |||
writer_t *writer = writer_init(filename, APT_REGION_CHA, img.rows, PNG_COLOR_TYPE_GRAY, description); | |||
writer_write_image(writer, &_img); | |||
writer_free(writer); | |||
free(_img.data); | |||
} else { | |||
error_noexit("Could not generate visible image, no visible channel"); | |||
} | |||
} | |||
} | |||
for (size_t i = 0; i < effects_len; i++) { | |||
if (strcmp(effects[i], "stretch") == 0) { | |||
apt_stretch(&img, APT_REGION_CHA); | |||
apt_stretch(&img, APT_REGION_CHB); | |||
} else if (strcmp(effects[i], "equalize") == 0) { | |||
apt_equalize(&img, APT_REGION_CHA); | |||
apt_equalize(&img, APT_REGION_CHB); | |||
} | |||
} | |||
for (size_t i = 0; i < images_len; i++) { | |||
const char *base; | |||
if (i < filenames_len) { | |||
base = filenames[i]; | |||
} else { | |||
base = name; | |||
} | |||
if (strcmp(images[i], "raw") == 0) { | |||
char filename[269]; | |||
sprintf(filename, "%s-raw.png", base); | |||
char description[128]; | |||
sprintf(description, | |||
"Raw image, channel %s - %s / %s - %s", | |||
channel_name[img.ch[0]], | |||
channel_desc[img.ch[0]], | |||
channel_name[img.ch[1]], | |||
channel_desc[img.ch[1]] | |||
); | |||
writer_t *writer = writer_init(filename, APT_REGION_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description); | |||
writer_write_image(writer, &img); | |||
writer_free(writer); | |||
} else if (strcmp(images[i], "lut") == 0) { | |||
if (opts->lut != NULL && opts->lut[0] != '\0') { | |||
char filename[269]; | |||
sprintf(filename, "%s-lut.png", base); | |||
char description[128]; | |||
sprintf(description, | |||
"LUT image, channel %s - %s / %s - %s", | |||
channel_name[img.ch[0]], | |||
channel_desc[img.ch[0]], | |||
channel_name[img.ch[1]], | |||
channel_desc[img.ch[1]] | |||
); | |||
png_colorp lut = (png_colorp)malloc(sizeof(png_color)*256*256); | |||
if (read_lut(opts->lut, lut)) { | |||
writer_t *writer = writer_init(filename, APT_REGION_CHA, img.rows, PNG_COLOR_TYPE_RGB, description); | |||
writer_write_image_lut(writer, &img, lut); | |||
writer_free(writer); | |||
} | |||
free(lut); | |||
} else { | |||
warning("Cannot create LUT image, missing -l/--lut"); | |||
} | |||
} else if (strcmp(images[i], "a") == 0) { | |||
char filename[269]; | |||
sprintf(filename, "%s-a.png", base); | |||
char description[128]; | |||
sprintf(description, "Channel A: %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]); | |||
writer_t *writer = writer_init(filename, APT_REGION_CHA_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description); | |||
writer_write_image(writer, &img); | |||
writer_free(writer); | |||
} else if (strcmp(images[i], "b") == 0) { | |||
char filename[269]; | |||
sprintf(filename, "%s-b.png", base); | |||
char description[128]; | |||
sprintf(description, "Channel B: %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]); | |||
writer_t *writer = writer_init(filename, APT_REGION_CHB_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description); | |||
writer_write_image(writer, &img); | |||
writer_free(writer); | |||
} | |||
} | |||
free(img.data); | |||
return 1; | |||
} | |||
static int freq_from_filename(const char *filename) { | |||
char frequency_text[10] = {'\0'}; | |||
if (strlen(filename) >= 30+4 && strncmp(filename, "gqrx_", 5) == 0) { | |||
memcpy(frequency_text, &filename[21], 9); | |||
return atoi(frequency_text); | |||
} else if (strlen(filename) == 40+4 && strncmp(filename, "SDRSharp_", 9) == 0) { | |||
memcpy(frequency_text, &filename[26], 9); | |||
return atoi(frequency_text); | |||
} else if (strlen(filename) == 37+4 && strncmp(filename, "audio_", 6) == 0) { | |||
memcpy(frequency_text, &filename[6], 9); | |||
return atoi(frequency_text); | |||
} | |||
return 0; | |||
} | |||
static int satid_from_freq(int freq) { | |||
int differences[3] = { | |||
abs(freq - 137620000), // NOAA-15 | |||
abs(freq - 137912500), // NOAA-18 | |||
abs(freq - 137100000) // NOAA-19 | |||
}; | |||
int best = 0; | |||
for (size_t i = 0; i < 3; i++) { | |||
if (differences[i] < differences[best]) { | |||
best = i; | |||
} | |||
} | |||
const int lut[3] = {15, 18, 19}; | |||
return lut[best]; | |||
} | |||
// Read samples from a SNDFILE_t instance (passed through context) | |||
static size_t callback(float *samples, size_t count, void *context) { | |||
SNDFILE_t *file = (SNDFILE_t *)context; | |||
switch (file->info.channels) { | |||
case 1: | |||
return sf_read_float(file->file, samples, count); | |||
case 2: { | |||
float _samples[count * 2]; | |||
size_t read = sf_read_float(file->file, _samples, count * 2); | |||
for (size_t i = 0; i < count; i++) { | |||
// Average of left and right | |||
samples[i] = (_samples[i*2] + _samples[i*2 + 1]) / 2.0f; | |||
} | |||
return read / 2; | |||
} | |||
default: | |||
error_noexit("Only mono and stereo audio files are supported\n"); | |||
return 0; | |||
} | |||
} | |||
// Write a line with very basic equalization | |||
static void write_line(writer_t *png, float *row) { | |||
float min = FLT_MAX; | |||
float max = FLT_MIN; | |||
for (int i = 0; i < APT_IMG_WIDTH; i++) { | |||
if (row[i] < min) min = row[i]; | |||
if (row[i] > max) max = row[i]; | |||
} | |||
png_byte pixels[APT_IMG_WIDTH]; | |||
for (int i = 0; i < APT_IMG_WIDTH; i++) { | |||
pixels[i] = clamp_int(roundf((row[i]-min) / (max-min) * 255.0f), 0, 255); | |||
} | |||
#pragma GCC diagnostic push | |||
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" | |||
png_write_row(png->png, pixels); | |||
#pragma GCC diagnostic pop | |||
} |
@@ -0,0 +1,153 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include "pngio.h" | |||
#include <png.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include "util.h" | |||
writer_t *writer_init(const char *filename, apt_region_t region, uint32_t height, int color, char *channel) { | |||
writer_t *png = (writer_t *)malloc(sizeof(writer_t)); | |||
png->region = region; | |||
// Create writer | |||
png->png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); | |||
if (!png->png) { | |||
error_noexit("Could not create PNG write struct"); | |||
return NULL; | |||
} | |||
png->info = png_create_info_struct(png->png); | |||
if (!png->info) { | |||
error_noexit("Could not create PNG info struct"); | |||
return NULL; | |||
} | |||
png_set_IHDR(png->png, png->info, png->region.width, height, 8, color, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); | |||
char version[128]; | |||
int version_len = get_version(version); | |||
png_text text[] = { | |||
{PNG_TEXT_COMPRESSION_NONE, "Software", version, version_len}, | |||
{PNG_TEXT_COMPRESSION_NONE, "Channel", channel, strlen(channel)} | |||
}; | |||
png_set_text(png->png, png->info, text, 2); | |||
png->file = fopen(filename, "wb"); | |||
if (!png->file) { | |||
error_noexit("Could not open PNG file"); | |||
return NULL; | |||
} | |||
png_init_io(png->png, png->file); | |||
png_write_info(png->png, png->info); | |||
printf("Writing %s...\n", filename); | |||
return png; | |||
} | |||
void writer_write_image(writer_t *png, const apt_image_t *img) { | |||
for (size_t y = 0; y < img->rows; y++) { | |||
png_write_row(png->png, &img->data[y*APT_IMG_WIDTH + png->region.offset]); | |||
} | |||
} | |||
void writer_write_image_gradient(writer_t *png, const apt_image_t *img, const uint32_t *gradient) { | |||
for (size_t y = 0; y < img->rows; y++) { | |||
png_color pixels[APT_IMG_WIDTH]; | |||
for (size_t x = 0; x < APT_IMG_WIDTH; x++) { | |||
apt_rgb_t pixel = apt_gradient(gradient, img->data[y*APT_IMG_WIDTH + x + png->region.offset]); | |||
pixels[x] = (png_color){ pixel.r, pixel.g, pixel.b }; | |||
} | |||
png_write_row(png->png, (png_bytep)pixels); | |||
} | |||
} | |||
void writer_write_image_lut(writer_t *png, const apt_image_t *img, const png_colorp lut) { | |||
for (size_t y = 0; y < img->rows; y++) { | |||
png_color pixels[APT_CH_WIDTH]; | |||
for (size_t x = 0; x < APT_CH_WIDTH; x++) { | |||
uint8_t a = img->data[y*APT_IMG_WIDTH + x + APT_CHA_OFFSET]; | |||
uint8_t b = img->data[y*APT_IMG_WIDTH + x + APT_CHB_OFFSET]; | |||
pixels[x] = lut[b*256 + a]; | |||
} | |||
png_write_row(png->png, (png_bytep)pixels); | |||
} | |||
} | |||
void writer_free(writer_t *png) { | |||
png_write_end(png->png, png->info); | |||
png_destroy_write_struct(&png->png, &png->info); | |||
fclose(png->file); | |||
free(png); | |||
} | |||
int read_lut(const char *filename, png_colorp out) { | |||
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); | |||
if (!png) { | |||
error_noexit("Could not create PNG read struct"); | |||
return 0; | |||
} | |||
png_infop info = png_create_info_struct(png); | |||
if (!info) { | |||
error_noexit("Could not create PNG info struct"); | |||
return 0; | |||
} | |||
FILE *file = fopen(filename, "rb"); | |||
if (!file) { | |||
error_noexit("Cannot open LUT"); | |||
return 0; | |||
} | |||
png_init_io(png, file); | |||
// Read metadata | |||
png_read_info(png, info); | |||
uint32_t width = png_get_image_width(png, info); | |||
uint32_t height = png_get_image_height(png, info); | |||
png_byte color_type = png_get_color_type(png, info); | |||
png_byte bit_depth = png_get_bit_depth(png, info); | |||
if (width != 256 && height != 256) { | |||
error_noexit("LUT must be 256x256"); | |||
return 0; | |||
} | |||
if (bit_depth != 8) { | |||
error_noexit("LUT must be 8 bit"); | |||
return 0; | |||
} | |||
if (color_type != PNG_COLOR_TYPE_RGB) { | |||
error_noexit("LUT must be RGB"); | |||
return 0; | |||
} | |||
for (uint32_t i = 0; i < height; i++) { | |||
png_read_row(png, (png_bytep)&out[i*width], NULL); | |||
} | |||
png_read_end(png, info); | |||
png_destroy_read_struct(&png, &info, NULL); | |||
fclose(file); | |||
return 1; | |||
} |
@@ -0,0 +1,41 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#ifndef APTDEC_CLI_PNGIO_H_ | |||
#define APTDEC_CLI_PNGIO_H_ | |||
#include <aptdec.h> | |||
#include <png.h> | |||
typedef struct { | |||
png_structp png; | |||
png_infop info; | |||
FILE *file; | |||
apt_region_t region; | |||
} writer_t; | |||
writer_t *writer_init(const char *filename, apt_region_t region, uint32_t height, int color, char *channel); | |||
void writer_free(writer_t *png); | |||
void writer_write_image(writer_t *png, const apt_image_t *img); | |||
void writer_write_image_gradient(writer_t *png, const apt_image_t *img, const uint32_t *gradient); | |||
void writer_write_image_lut(writer_t *png, const apt_image_t *img, const png_colorp lut); | |||
int read_lut(const char *filename, png_colorp out); | |||
#endif |
@@ -1,6 +1,6 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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 | |||
@@ -18,6 +18,7 @@ | |||
#include "util.h" | |||
#include <aptdec.h> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
@@ -28,10 +29,12 @@ void error_noexit(const char *text) { | |||
fprintf(stderr, "\033[31mError: %s\033[0m\n", text); | |||
#endif | |||
} | |||
void error(const char *text) { | |||
error_noexit(text); | |||
exit(1); | |||
} | |||
void warning(const char *text) { | |||
#ifdef _WIN32 | |||
fprintf(stderr, "Warning: %s\r\n", text); | |||
@@ -40,10 +43,12 @@ void warning(const char *text) { | |||
#endif | |||
} | |||
float clamp(float x, float hi, float lo) { | |||
int clamp_int(int x, int lo, int hi) { | |||
if (x > hi) return hi; | |||
if (x < lo) return lo; | |||
return x; | |||
} | |||
float clamp_half(float x, float hi) { return clamp(x, hi, -hi); } | |||
int get_version(char *str) { | |||
return sprintf(str, "aptdec-cli %s using libaptdec %s", VERSION, aptdec_get_version()); | |||
} |
@@ -1,6 +1,6 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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 | |||
@@ -16,19 +16,17 @@ | |||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include <stdio.h> | |||
#define M_PIf 3.14159265358979323846f | |||
#define M_TAUf (M_PIf * 2.0f) | |||
#ifndef APTDEC_CLI_UTIL_H_ | |||
#define APTDEC_CLI_UTIL_H_ | |||
#define MIN(a, b) (((a) < (b)) ? (a) : (b)) | |||
#define MAX(a, b) (((a) > (b)) ? (a) : (b)) | |||
#include <stddef.h> | |||
#ifndef UTIL_H | |||
#define UTIL_H | |||
float clamp(float x, float hi, float lo); | |||
float clamp_half(float x, float hi); | |||
void error(const char *text); | |||
void error_noexit(const char *text); | |||
__attribute__((noreturn)) void error(const char *text); | |||
void warning(const char *text); | |||
int clamp_int(int x, int lo, int hi); | |||
int get_version(char *str); | |||
#endif |
@@ -2,7 +2,7 @@ REM Build using Visual Studio 2019 on Windows | |||
REM Additional tools needed: git, cmake and ninja | |||
REM Build zlib | |||
git clone https://github.com/madler/zlib | |||
git clone -b v1.2.13 https://github.com/madler/zlib | |||
cd zlib | |||
mkdir build | |||
cd build | |||
@@ -11,7 +11,7 @@ ninja install | |||
cd ../../ | |||
REM Build libpng | |||
git clone https://github.com/glennrp/libpng | |||
git clone -b v1.6.39 https://github.com/glennrp/libpng | |||
cd libpng | |||
mkdir build | |||
cd build | |||
@@ -20,7 +20,7 @@ ninja install | |||
cd ../.. | |||
REM Build libsndfile - Could build Vorbis, FLAC and Opus first for extra support | |||
git clone https://github.com/libsndfile/libsndfile | |||
git clone -b 1.1.0 https://github.com/libsndfile/libsndfile | |||
cd libsndfile | |||
mkdir build | |||
cd build | |||
@@ -1,4 +1,4 @@ | |||
#!/bin/bash | |||
#!/usr/bin/env bash | |||
# Cross compile for Windows from Linux | |||
TEMP_PATH="$(pwd)/winpath" | |||
@@ -39,7 +39,6 @@ cp "libsndfile-1.0.29-win64/bin/sndfile.dll" $TEMP_PATH/bin/sndfile.dll | |||
cp "libsndfile-1.0.29-win64/include/sndfile.h" $TEMP_PATH/include/sndfile.h | |||
cp "libsndfile-1.0.29-win64/lib/sndfile.lib" $TEMP_PATH/lib/sndfile.lib | |||
# Copy DLL's into root for CPack | |||
cp $TEMP_PATH/bin/*.dll ../ | |||
@@ -1,19 +0,0 @@ | |||
# Find libsndfile | |||
FIND_PATH(LIBSNDFILE_INCLUDE_DIR sndfile.h) | |||
SET(LIBSNDFILE_NAMES ${LIBSNDFILE_NAMES} sndfile libsndfile) | |||
FIND_LIBRARY(LIBSNDFILE_LIBRARY NAMES ${LIBSNDFILE_NAMES} PATH) | |||
IF(LIBSNDFILE_INCLUDE_DIR AND LIBSNDFILE_LIBRARY) | |||
SET(LIBSNDFILE_FOUND TRUE) | |||
ENDIF(LIBSNDFILE_INCLUDE_DIR AND LIBSNDFILE_LIBRARY) | |||
IF(LIBSNDFILE_FOUND) | |||
IF(NOT LibSndFile_FIND_QUIETLY) | |||
MESSAGE(STATUS "Found LibSndFile: ${LIBSNDFILE_LIBRARY}") | |||
ENDIF(NOT LibSndFile_FIND_QUIETLY) | |||
ELSE(LIBSNDFILE_FOUND) | |||
IF(LibSndFile_FIND_REQUIRED) | |||
MESSAGE(FATAL_ERROR "Could not find sndfile") | |||
ENDIF(LibSndFile_FIND_REQUIRED) | |||
ENDIF(LIBSNDFILE_FOUND) |
@@ -1,6 +1,6 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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 | |||
@@ -18,6 +18,7 @@ | |||
#include "algebra.h" | |||
#include <stdlib.h> | |||
#include <math.h> | |||
// Find the best linear equation to estimate the value of the | |||
@@ -51,6 +52,67 @@ linear_t linear_regression(const float *independent, const float *dependent, siz | |||
return (linear_t){a, b}; | |||
} | |||
float linear_calc(float x, linear_t line) { return x * line.a + line.b; } | |||
float standard_deviation(const float *data, size_t len) { | |||
float mean = 0.0f; | |||
for (size_t i = 0; i < len; i++) { | |||
mean += data[i]; | |||
} | |||
mean /= (float)len; | |||
float deviation_mean = 0.0f; | |||
for (size_t i = 0; i < len; i++) { | |||
float deviation = data[i] - mean; | |||
deviation_mean += deviation * deviation; | |||
} | |||
return sqrtf(deviation_mean / (float)len); | |||
} | |||
float sumf(const float *x, size_t len) { | |||
float sum = 0.0f; | |||
for (size_t i = 0; i < len; i++) { | |||
sum += x[i]; | |||
} | |||
return sum; | |||
} | |||
float meanf(const float *x, size_t len) { | |||
return sumf(x, len) / (float)len; | |||
} | |||
void normalizef(float *x, size_t len) { | |||
float sum = sumf(x, len); | |||
for (size_t i = 0; i < len; i++) { | |||
x[i] /= sum; | |||
} | |||
} | |||
static int sort_func(const void *a, const void *b) { | |||
return *(float *)b > *(float *)a ? 1 : -1; | |||
} | |||
float medianf(float *data, size_t len) { | |||
qsort(data, len, sizeof(float), sort_func); | |||
float quadratic_calc(float x, quadratic_t quadratic) { return x * x * quadratic.a + x * quadratic.b + quadratic.c; } | |||
if (len % 2 == 0) { | |||
return (data[len/2] + data[len/2 - 1]) / 2.0f; | |||
} else { | |||
return data[len/2]; | |||
} | |||
} | |||
float linear_calc(float x, linear_t line) { | |||
return x * line.a + line.b; | |||
} | |||
float quadratic_calc(float x, quadratic_t quadratic) { | |||
return x*x * quadratic.a + x * quadratic.b + quadratic.c; | |||
} | |||
float sincf(float x) { | |||
if (x == 0.0f) { | |||
return 1.0f; | |||
} | |||
return sinf(M_PIf * x) / (M_PIf * x); | |||
} |
@@ -1,6 +1,6 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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 | |||
@@ -16,10 +16,13 @@ | |||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
*/ | |||
#ifndef APTDEC_ALGEBRA_H | |||
#define APTDEC_ALGEBRA_H | |||
#ifndef LIBAPTDEC_ALGEBRA_H_ | |||
#define LIBAPTDEC_ALGEBRA_H_ | |||
#include <stddef.h> | |||
#define M_PIf 3.14159265358979323846f | |||
#define M_TAUf (M_PIf * 2.0f) | |||
// A linear equation in the form of y(x) = ax + b | |||
typedef struct { | |||
float a, b; | |||
@@ -31,8 +34,16 @@ typedef struct { | |||
} quadratic_t; | |||
linear_t linear_regression(const float *independent, const float *dependent, size_t len); | |||
float standard_deviation(const float *data, size_t len); | |||
float sumf(const float *x, size_t len); | |||
float meanf(const float *x, size_t len); | |||
void normalizef(float *x, size_t len); | |||
// NOTE: Modifies input array | |||
float medianf(float *data, size_t len); | |||
float linear_calc(float x, linear_t line); | |||
float quadratic_calc(float x, quadratic_t quadratic); | |||
float sincf(float x); | |||
#endif |
@@ -0,0 +1,121 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include "calibration.h" | |||
#include "util.h" | |||
// clang-format off | |||
const calibration_t calibration[3] = { | |||
{ | |||
.name = "NOAA-15", | |||
.prt = { | |||
{ 1.36328e-06f, 0.051045f, 276.60157f }, // PRT 1 | |||
{ 1.47266e-06f, 0.050909f, 276.62531f }, // PRT 2 | |||
{ 1.47656e-06f, 0.050907f, 276.67413f }, // PRT 3 | |||
{ 1.47656e-06f, 0.050966f, 276.59258f } // PRT 4 | |||
}, | |||
.visible = { | |||
{ | |||
.low = { 0.0568f, -2.1874f }, | |||
.high = { 0.1633f, -54.9928f }, | |||
.cutoff = 496.0f | |||
}, { | |||
.low = { 0.0596f, -2.4096f }, | |||
.high = { 0.1629f, -55.2436f }, | |||
.cutoff = 511.0f | |||
} | |||
}, | |||
.rad = { | |||
{ 925.4075f, 0.337810f, 0.998719f }, // Channel 4 | |||
{ 839.8979f, 0.304558f, 0.999024f }, // Channel 5 | |||
{ 2695.9743f, 1.621256f, 0.998015f } // Channel 3B | |||
}, | |||
.cor = { | |||
{ -4.50f, { 0.0004524f, -0.0932f, 4.76f } }, // Channel 4 | |||
{ -3.61f, { 0.0002811f, -0.0659f, 3.83f } }, // Channel 5 | |||
{ 0.0f, { 0.0f, 0.0f , 0.0f } } // Channel 3B | |||
} | |||
}, { | |||
.name = "NOAA-18", | |||
.prt = { | |||
{ 1.657e-06f, 0.05090f, 276.601f }, // PRT 1 | |||
{ 1.482e-06f, 0.05101f, 276.683f }, // PRT 2 | |||
{ 1.313e-06f, 0.05117f, 276.565f }, // PRT 3 | |||
{ 1.484e-06f, 0.05103f, 276.615f } // PRT 4 | |||
}, | |||
.visible = { | |||
{ | |||
.low = { 0.06174f, -2.434f }, | |||
.high = { 0.1841f, -63.31f }, | |||
.cutoff = 501.54f | |||
}, { | |||
.low = { 0.07514f, -2.960f }, | |||
.high = { 0.2254f, -78.55f }, | |||
.cutoff = 500.40f | |||
} | |||
}, | |||
.rad = { | |||
{ 928.1460f, 0.436645f, 0.998607f }, // Channel 4 | |||
{ 833.2532f, 0.253179f, 0.999057f }, // Channel 5 | |||
{ 2659.7952f, 1.698704f, 0.996960f } // Channel 3B | |||
}, | |||
.cor = { | |||
{ -5.53f, { 0.00052337f, -0.11069f, 5.82f } }, // Channel 4 | |||
{ -2.22f, { 0.00017715f, -0.04360f, 2.67f } }, // Channel 5 | |||
{ 0.0f, { 0.0f, 0.0f, 0.0f } } // Channel 3B | |||
} | |||
}, { | |||
.name = "NOAA-19", | |||
.prt = { | |||
{ 1.405783e-06f, 0.051111f, 276.6067f }, // PRT 1 | |||
{ 1.496037e-06f, 0.051090f, 276.6119f }, // PRT 2 | |||
{ 1.496990e-06f, 0.051033f, 276.6311f }, // PRT 3 | |||
{ 1.493110e-06f, 0.051058f, 276.6268f } // PRT 4 | |||
}, | |||
.visible = { | |||
{ | |||
.low = { 0.05555f, -2.159f }, | |||
.high = { 0.1639f, -56.33f }, | |||
.cutoff = 496.43f | |||
}, { | |||
.low = { 0.06614f, -2.565f }, | |||
.high = { 0.1970f, -68.01f }, | |||
.cutoff = 500.37f | |||
} | |||
}, | |||
.rad = { | |||
{ 928.9f, 0.53959f, 0.998534f }, // Channel 4 | |||
{ 831.9f, 0.36064f, 0.998913f }, // Channel 5 | |||
{ 2670.0f, 1.67396f, 0.997364f } // Channel 3B | |||
}, | |||
.cor = { | |||
{ -5.49f, { 0.00054668f, -0.11187f, 5.70f } }, // Channel 4 | |||
{ -3.39f, { 0.00024985f, -0.05991f, 3.58f } }, // Channel 5 | |||
{ 0.0f, { 0.0f, 0.0f, 0.0f } } // Channel 3B | |||
} | |||
} | |||
}; | |||
calibration_t get_calibration(apt_satellite_t satid) { | |||
switch (satid) { | |||
case NOAA15: return calibration[0]; | |||
case NOAA18: return calibration[1]; | |||
default: return calibration[2]; | |||
} | |||
} |
@@ -1,6 +1,6 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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 | |||
@@ -16,9 +16,11 @@ | |||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
*/ | |||
#ifndef APTDEC_CALIBRATION_H | |||
#define APTDEC_CALIBRATION_H | |||
#ifndef LIBAPTDEC_CALIBRATION_H_ | |||
#define LIBAPTDEC_CALIBRATION_H_ | |||
#include "algebra.h" | |||
#include <aptdec.h> | |||
typedef struct { | |||
char *name; | |||
@@ -50,6 +52,6 @@ static const float C1 = 1.1910427e-5f; | |||
// Second radiation constant (cm-K) | |||
static const float C2 = 1.4387752f; | |||
calibration_t get_calibration(int satid); | |||
calibration_t get_calibration(apt_satellite_t satid); | |||
#endif |
@@ -0,0 +1,94 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include <aptdec.h> | |||
#include "util.h" | |||
#define MCOMPOSITE(m1, a1, m2, a2) (m1 * a1 + m2 * a2 * (1 - a1)) | |||
// clang-format off | |||
apt_rgb_t apt_gradient(const uint32_t *gradient, uint8_t val) { | |||
return (apt_rgb_t) { | |||
(gradient[val] & 0x00FF0000) >> 16, | |||
(gradient[val] & 0x0000FF00) >> 8, | |||
(gradient[val] & 0x000000FF) | |||
}; | |||
} | |||
apt_rgb_t apt_composite_rgb(apt_rgb_t top, float top_alpha, apt_rgb_t bottom, float bottom_alpha) { | |||
return (apt_rgb_t) { | |||
MCOMPOSITE(top.r, top_alpha, bottom.r, bottom_alpha), | |||
MCOMPOSITE(top.g, top_alpha, bottom.g, bottom_alpha), | |||
MCOMPOSITE(top.b, top_alpha, bottom.b, bottom_alpha) | |||
}; | |||
} | |||
// Taken from WXtoImg | |||
const uint32_t temperature_gradient[256] = { | |||
0x45008F, 0x460091, 0x470092, 0x480094, 0x490096, 0x4A0098, 0x4B009B, | |||
0x4D009D, 0x4E00A0, 0x5000A2, 0x5100A5, 0x5200A7, 0x5400AA, 0x5600AE, | |||
0x5700B1, 0x5800B4, 0x5A00B7, 0x5C00BA, 0x5E00BD, 0x5F00C0, 0x6100C4, | |||
0x6400C8, 0x6600CB, 0x6800CE, 0x6900D1, 0x6800D4, 0x6500D7, 0x6300DA, | |||
0x6100DD, 0x5D00E1, 0x5B00E4, 0x5900E6, 0x5600E9, 0x5300EB, 0x5000EE, | |||
0x4D00F0, 0x4900F3, 0x4700FC, 0x4300FA, 0x3100BF, 0x200089, 0x200092, | |||
0x1E0095, 0x1B0097, 0x19009A, 0x17009C, 0x15009E, 0x1200A0, 0x0F00A3, | |||
0x0F02A5, 0x0E06A8, 0x0E0AAB, 0x0E0DAD, 0x0E11B1, 0x0D15B4, 0x0D18B7, | |||
0x0D1CBA, 0x0B21BD, 0x0A25C0, 0x0A29C3, 0x092DC6, 0x0833CA, 0x0736CD, | |||
0x073BD0, 0x0741D3, 0x0545D6, 0x044BD9, 0x0450DC, 0x0355DE, 0x025DE2, | |||
0x0161E5, 0x0066E7, 0x006CEA, 0x0072EC, 0x0078EE, 0x007DF0, 0x0082F3, | |||
0x008DFC, 0x0090FA, 0x0071BF, 0x005489, 0x005C91, 0x006194, 0x006496, | |||
0x006897, 0x006D99, 0x00719B, 0x00759D, 0x00799F, 0x007EA0, 0x0082A2, | |||
0x0087A4, 0x008CA6, 0x0092AA, 0x0096AC, 0x009CAE, 0x00A2B1, 0x00A6B3, | |||
0x00AAB5, 0x00ADB7, 0x00B1BA, 0x00B6BE, 0x00BAC0, 0x00BEC2, 0x00C2C5, | |||
0x00C6C6, 0x00CAC9, 0x00CCCA, 0x00CFCB, 0x00D2CC, 0x00D4CC, 0x00D6CC, | |||
0x00D9CB, 0x00DBCB, 0x00DECB, 0x00E0CB, 0x00E2CC, 0x00EAD2, 0x00EACF, | |||
0x00B9A4, 0x008E7A, 0x01947C, 0x049779, 0x079975, 0x099B71, 0x0D9D6B, | |||
0x109F67, 0x12A163, 0x15A35F, 0x17A559, 0x1AA855, 0x1DAA50, 0x20AC4B, | |||
0x24AF45, 0x28B241, 0x2BB53B, 0x2EB835, 0x31BA30, 0x34BD2B, 0x39BF24, | |||
0x3FC117, 0x49C508, 0x4FC801, 0x4FCA00, 0x4ECD00, 0x4ECF00, 0x4FD200, | |||
0x54D500, 0x5DD800, 0x68DB00, 0x6EDD00, 0x74DF00, 0x7AE200, 0x7FE400, | |||
0x85E700, 0x8BE900, 0x8FEB00, 0x9BF300, 0x9EF200, 0x7EBB00, 0x608A00, | |||
0x689200, 0x6D9500, 0x719600, 0x759800, 0x7B9A00, 0x7F9D00, 0x839F00, | |||
0x87A100, 0x8CA200, 0x8FA500, 0x92A700, 0x96A900, 0x9AAD00, 0x9DB000, | |||
0xA1B200, 0xA5B500, 0xA9B700, 0xADBA00, 0xB2BD00, 0xB6BF00, 0xBBC300, | |||
0xBFC600, 0xC3C800, 0xC8CB00, 0xCCCE00, 0xD0D100, 0xD3D200, 0xD5D400, | |||
0xD9D400, 0xDCD400, 0xDED500, 0xE1D500, 0xE3D500, 0xE6D400, 0xE8D100, | |||
0xEACE00, 0xF2CF00, 0xF2CA00, 0xBB9900, 0x8A6E00, 0x927200, 0x957200, | |||
0x977100, 0x9A7000, 0x9C6E00, 0x9E6D00, 0xA06B00, 0xA36A00, 0xA56800, | |||
0xA86700, 0xAB6600, 0xAE6500, 0xB26300, 0xB46100, 0xB75F00, 0xBA5D00, | |||
0xBD5C00, 0xC05900, 0xC35700, 0xC65400, 0xCA5000, 0xCD4D00, 0xD04A00, | |||
0xD34700, 0xD64300, 0xD94000, 0xDC3D00, 0xDE3900, 0xE23300, 0xE52F00, | |||
0xE72C00, 0xEA2800, 0xEC2300, 0xEF1F00, 0xF11A00, 0xF31400, 0xFB0F00, | |||
0xFA0D00, 0xC10500, 0x8E0000, 0x970000, 0x9B0000, 0x9E0000, 0xA10000, | |||
0xA50000, 0xA90000, 0xAD0000, 0xB10000, 0xB60000, 0xBA0000, 0xBD0000, | |||
0xC20000, 0xC80000, 0xCC0000, 0xCC0000 | |||
}; | |||
// Taken from WXtoImg | |||
const uint32_t precipitation_gradient[58] = { | |||
0x088941, 0x00C544, 0x00D12C, 0x00E31C, 0x00F906, 0x14FF00, 0x3EFF00, | |||
0x5DFF00, 0x80FF00, 0xABFF00, 0xCDFE00, 0xF8FF00, 0xFFE600, 0xFFB800, | |||
0xFF9800, 0xFF7500, 0xFF4900, 0xFE2600, 0xFF0400, 0xDF0000, 0xA80000, | |||
0x870000, 0x5A0000, 0x390000, 0x110000, 0x0E1010, 0x232222, 0x333333, | |||
0x414141, 0x535353, 0x606060, 0x6E6E6E, 0x808080, 0x8E8E8E, 0xA0A0A0, | |||
0xAEAEAE, 0xC0C0C0, 0xCECECE, 0xDCDCDC, 0xEFEFEF, 0xFAFAFA, 0xFFFFFF, | |||
0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, | |||
0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, | |||
0xFFFFFF, 0xFFFFFF | |||
}; |
@@ -0,0 +1,237 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include <float.h> | |||
#include <math.h> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include <aptdec.h> | |||
#include "filter.h" | |||
#include "util.h" | |||
#include "algebra.h" | |||
#define BUFFER_SIZE 16384 | |||
#define LOW_PASS_SIZE 101 | |||
#define CARRIER_FREQ 2400.0f | |||
#define MAX_CARRIER_OFFSET 10.0f | |||
typedef struct { | |||
float alpha; | |||
float beta; | |||
float min_freq; | |||
float max_freq; | |||
float freq; | |||
float phase; | |||
} pll_t; | |||
typedef struct { | |||
float *ring_buffer; | |||
size_t ring_size; | |||
float *taps; | |||
size_t ntaps; | |||
} fir_t; | |||
struct aptdec_t { | |||
float sample_rate; | |||
float sync_frequency; | |||
pll_t *pll; | |||
fir_t *hilbert; | |||
float low_pass[LOW_PASS_SIZE]; | |||
}; | |||
char *aptdec_get_version(void) { | |||
return VERSION; | |||
} | |||
fir_t *fir_init(size_t max_size, size_t ntaps) { | |||
fir_t *fir = (fir_t *)malloc(sizeof(fir_t)); | |||
fir->ntaps = ntaps; | |||
fir->ring_size = max_size + ntaps; | |||
fir->taps = (float *)malloc(ntaps * sizeof(float)); | |||
fir->ring_buffer = (float *)malloc((max_size + ntaps) * sizeof(float)); | |||
return fir; | |||
} | |||
void fir_free(fir_t *fir) { | |||
free(fir->ring_buffer); | |||
free(fir->taps); | |||
free(fir); | |||
} | |||
pll_t *pll_init(float alpha, float beta, float min_freq, float max_freq, float sample_rate) { | |||
pll_t *pll = (pll_t *)malloc(sizeof(pll_t)); | |||
pll->alpha = alpha; | |||
pll->beta = beta; | |||
pll->min_freq = M_TAUf * min_freq / sample_rate; | |||
pll->max_freq = M_TAUf * max_freq / sample_rate; | |||
pll->phase = 0.0f; | |||
pll->freq = 0.0f; | |||
return pll; | |||
} | |||
aptdec_t *aptdec_init(float sample_rate) { | |||
if (sample_rate > 96000 || sample_rate < (CARRIER_FREQ + APT_IMG_WIDTH) * 2.0f) { | |||
return NULL; | |||
} | |||
aptdec_t *apt = (aptdec_t *)malloc(sizeof(aptdec_t)); | |||
apt->sample_rate = sample_rate; | |||
apt->sync_frequency = 1.0f; | |||
// PLL configuration | |||
// https://www.trondeau.com/blog/2011/8/13/control-loop-gain-values.html | |||
float damp = 0.7f; | |||
float bw = 0.005f; | |||
float alpha = (4.0f * damp * bw) / (1.0f + 2.0f * damp * bw + bw * bw); | |||
float beta = (4.0f * bw * bw) / (1.0f + 2.0f * damp * bw + bw * bw); | |||
apt->pll = pll_init(alpha, beta, CARRIER_FREQ-MAX_CARRIER_OFFSET, CARRIER_FREQ+MAX_CARRIER_OFFSET, sample_rate); | |||
if (apt->pll == NULL) { | |||
free(apt); | |||
return NULL; | |||
} | |||
// Hilbert transform | |||
apt->hilbert = fir_init(BUFFER_SIZE, 31); | |||
if (apt->hilbert == NULL) { | |||
free(apt->pll); | |||
free(apt); | |||
return NULL; | |||
} | |||
design_hilbert(apt->hilbert->taps, apt->hilbert->ntaps); | |||
design_low_pass(apt->low_pass, apt->sample_rate, (2080.0f + CARRIER_FREQ) / 2.0f, LOW_PASS_SIZE); | |||
return apt; | |||
} | |||
void aptdec_free(aptdec_t *apt) { | |||
fir_free(apt->hilbert); | |||
free(apt->pll); | |||
free(apt); | |||
} | |||
static complexf_t pll_work(pll_t *pll, complexf_t in) { | |||
// Internal oscillator (90deg offset) | |||
complexf_t osc = complex_build(cosf(pll->phase), -sinf(pll->phase)); | |||
in = complex_multiply(in, osc); | |||
// Error detector | |||
float error = cargf(in); | |||
// Loop filter (single pole IIR) | |||
pll->freq += pll->beta * error; | |||
pll->freq = clamp(pll->freq, pll->min_freq, pll->max_freq); | |||
pll->phase += pll->freq + (pll->alpha * error); | |||
pll->phase = remainderf(pll->phase, M_TAUf); | |||
return in; | |||
} | |||
static int am_demod(aptdec_t *apt, float *out, size_t count, aptdec_callback_t callback, void *context) { | |||
size_t read = callback(&apt->hilbert->ring_buffer[apt->hilbert->ntaps], count, context); | |||
for (size_t i = 0; i < read; i++) { | |||
complexf_t sample = hilbert_transform(&apt->hilbert->ring_buffer[i], apt->hilbert->taps, apt->hilbert->ntaps); | |||
out[i] = crealf(pll_work(apt->pll, sample)); | |||
} | |||
memcpy(apt->hilbert->ring_buffer, &apt->hilbert->ring_buffer[read], apt->hilbert->ntaps*sizeof(float)); | |||
return read; | |||
} | |||
static int get_pixels(aptdec_t *apt, float *out, size_t count, aptdec_callback_t callback, void *context) { | |||
static float buffer[BUFFER_SIZE]; | |||
static size_t n = BUFFER_SIZE; | |||
static float offset = 0.0; | |||
float ratio = apt->sample_rate / (4160.0f * apt->sync_frequency); | |||
for (size_t i = 0; i < count; i++) { | |||
// Get more samples if there are less than `LOW_PASS_SIZE` available | |||
if (n + LOW_PASS_SIZE > BUFFER_SIZE) { | |||
memcpy(buffer, &buffer[n], (BUFFER_SIZE-n) * sizeof(float)); | |||
size_t read = am_demod(apt, &buffer[BUFFER_SIZE-n], n, callback, context); | |||
if (read != n) { | |||
return i; | |||
} | |||
n = 0; | |||
} | |||
out[i] = interpolating_convolve(&buffer[n], apt->low_pass, LOW_PASS_SIZE, offset); | |||
// Do not question the sacred code | |||
int shift = ceilf(ratio - offset); | |||
offset = shift + offset - ratio; | |||
n += shift; | |||
} | |||
return count; | |||
} | |||
const float sync_pattern[] = {-1, -1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, | |||
1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 0}; | |||
#define SYNC_SIZE (sizeof(sync_pattern)/sizeof(sync_pattern[0])) | |||
// Get an entire row of pixels, aligned with sync markers | |||
int aptdec_getrow(aptdec_t *apt, float *row, aptdec_callback_t callback, void *context) { | |||
static float pixels[APT_IMG_WIDTH + SYNC_SIZE + 2]; | |||
// Wrap the circular buffer | |||
memcpy(pixels, &pixels[APT_IMG_WIDTH], (SYNC_SIZE + 2) * sizeof(float)); | |||
// Get a lines worth (APT_IMG_WIDTH) of samples | |||
if (get_pixels(apt, &pixels[SYNC_SIZE + 2], APT_IMG_WIDTH, callback, context) != APT_IMG_WIDTH) { | |||
return 0; | |||
} | |||
// Error detector | |||
float left = FLT_MIN; | |||
float middle = FLT_MIN; | |||
float right = FLT_MIN; | |||
size_t phase = 0; | |||
for (size_t i = 0; i < APT_IMG_WIDTH; i++) { | |||
float _left = convolve(&pixels[i + 0], sync_pattern, SYNC_SIZE); | |||
float _middle = convolve(&pixels[i + 1], sync_pattern, SYNC_SIZE); | |||
float _right = convolve(&pixels[i + 2], sync_pattern, SYNC_SIZE); | |||
if (_middle > middle) { | |||
left = _left; | |||
middle = _middle; | |||
right = _right; | |||
phase = i + 1; | |||
} | |||
} | |||
// Frequency | |||
float bias = (left / middle) - (right / middle); | |||
apt->sync_frequency = 1.0f + bias / APT_IMG_WIDTH / 2.0f; | |||
// Phase | |||
memcpy(&row[APT_IMG_WIDTH], &pixels[phase], (APT_IMG_WIDTH - phase) * sizeof(float)); | |||
memcpy(&row[APT_IMG_WIDTH - phase], pixels, phase * sizeof(float)); | |||
return 1; | |||
} |
@@ -0,0 +1,175 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include <aptdec.h> | |||
#include <string.h> | |||
#include <stdio.h> | |||
#include <math.h> | |||
#include "algebra.h" | |||
#include "util.h" | |||
#include "filter.h" | |||
void apt_equalize(apt_image_t *img, apt_region_t region) { | |||
// Plot histogram | |||
size_t histogram[256] = {0}; | |||
for (size_t y = 0; y < img->rows; y++) { | |||
for (size_t x = 0; x < region.width; x++) { | |||
histogram[img->data[y * APT_IMG_WIDTH + x + region.offset]]++; | |||
} | |||
} | |||
// Calculate cumulative frequency | |||
size_t sum = 0, cf[256] = {0}; | |||
for (int i = 0; i < 256; i++) { | |||
sum += histogram[i]; | |||
cf[i] = sum; | |||
} | |||
// Apply histogram | |||
int area = img->rows * region.width; | |||
for (size_t y = 0; y < img->rows; y++) { | |||
for (size_t x = 0; x < region.width; x++) { | |||
int k = (int)img->data[y * APT_IMG_WIDTH + x + region.offset]; | |||
img->data[y * APT_IMG_WIDTH + x + region.offset] = (255.0f / area) * cf[k]; | |||
} | |||
} | |||
} | |||
// Brightness calibrate, including telemetry | |||
static void image_apply_linear(uint8_t *data, int rows, int offset, int width, linear_t regr) { | |||
for (int y = 0; y < rows; y++) { | |||
for (int x = 0; x < width; x++) { | |||
float pv = linear_calc(data[y * APT_IMG_WIDTH + x + offset], regr); | |||
data[y * APT_IMG_WIDTH + x + offset] = clamp_int(roundf(pv), 0, 255); | |||
} | |||
} | |||
} | |||
void apt_stretch(apt_image_t *img, apt_region_t region) { | |||
// Plot histogram | |||
size_t histogram[256] = { 0 }; | |||
for (size_t y = 0; y < img->rows; y++) { | |||
for (size_t x = 0; x < region.width; x++) { | |||
histogram[img->data[y*APT_IMG_WIDTH + x + region.offset]]++; | |||
} | |||
} | |||
// Calculate cumulative frequency | |||
size_t sum = 0; | |||
size_t cf[256] = { 0 }; | |||
for (size_t i = 0; i < 256; i++) { | |||
sum += histogram[i]; | |||
cf[i] = sum; | |||
} | |||
// Find min/max points (1st percentile) | |||
int min = -1, max = -1; | |||
for (size_t i = 0; i < 256; i++) { | |||
if ((float)cf[i] / (float)sum < 0.01f && min == -1) { | |||
min = i; | |||
} | |||
if ((float)cf[i] / (float)sum > 0.99f && max == -1) { | |||
max = i; | |||
break; | |||
} | |||
} | |||
float a = 255.0f / (max - min); | |||
float b = a * -min; | |||
image_apply_linear(img->data, img->rows, region.offset, region.width, (linear_t){a, b}); | |||
} | |||
// Median denoise (with deviation threshold) | |||
void apt_denoise(apt_image_t *img, apt_region_t region) { | |||
for (size_t y = 1; y < img->rows - 1; y++) { | |||
for (size_t x = 1; x < region.width - 1; x++) { | |||
float pixels[9]; | |||
int pixeln = 0; | |||
for (int y2 = -1; y2 < 2; y2++) { | |||
for (int x2 = -1; x2 < 2; x2++) { | |||
pixels[pixeln++] = img->data[(y + y2) * APT_IMG_WIDTH + (x + region.offset) + x2]; | |||
} | |||
} | |||
if (standard_deviation(pixels, 9) > 15) { | |||
img->data[y * APT_IMG_WIDTH + x + region.offset] = medianf(pixels, 9); | |||
} | |||
} | |||
} | |||
} | |||
// Flips a channel, for northbound passes | |||
void apt_flip(apt_image_t *img, apt_region_t region) { | |||
for (size_t y = 1; y < img->rows; y++) { | |||
for (int x = 1; x < ceil(region.width / 2.0f); x++) { | |||
// Flip top-left & bottom-right | |||
swap_uint8( | |||
&img->data[(img->rows - y) * APT_IMG_WIDTH + region.offset + x], | |||
&img->data[y * APT_IMG_WIDTH + region.offset + (region.width - x)] | |||
); | |||
} | |||
} | |||
} | |||
// Calculate crop to remove noise from the start and end of an image | |||
#define NOISE_THRESH 2600.0 | |||
#include "filter.h" | |||
int apt_crop(apt_image_t *img) { | |||
const float sync_pattern[] = {-1, -1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, | |||
1, 1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, 0}; | |||
float spc_rows[img->rows]; | |||
int startCrop = 0; | |||
int endCrop = img->rows; | |||
for (size_t y = 0; y < img->rows; y++) { | |||
float temp[39]; | |||
for (size_t i = 0; i < 39; i++) { | |||
temp[i] = img->data[y * APT_IMG_WIDTH + i]; | |||
} | |||
spc_rows[y] = convolve(temp, &sync_pattern[0], 39); | |||
} | |||
// Find ends | |||
for (size_t y = 0; y < img->rows - 1; y++) { | |||
if (spc_rows[y] > NOISE_THRESH) { | |||
endCrop = y; | |||
} | |||
} | |||
for (size_t y = img->rows; y > 0; y--) { | |||
if (spc_rows[y] > NOISE_THRESH) { | |||
startCrop = y; | |||
} | |||
} | |||
printf("Crop rows: %i -> %i\n", startCrop, endCrop); | |||
// Ignore the noisy rows at the end | |||
img->rows = (endCrop - startCrop); | |||
// Remove the noisy rows at start | |||
memmove(img->data, &img->data[startCrop * APT_IMG_WIDTH], img->rows * APT_IMG_WIDTH * sizeof(float)); | |||
return startCrop; | |||
} |
@@ -0,0 +1,109 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include "filter.h" | |||
#include <math.h> | |||
// SSE2 intrinsics | |||
#ifdef __x86_64__ | |||
#include <emmintrin.h> | |||
#endif | |||
#include "algebra.h" | |||
// Blackman window | |||
// https://en.wikipedia.org/wiki/Window_function#Blackman_window | |||
static float blackman(float n, size_t ntaps) { | |||
n = (M_PIf * n) / (float)(ntaps - 1); | |||
return 0.42f - 0.5f*cosf(2 * n) + 0.08f*cosf(4 * n); | |||
} | |||
// Sinc low pass with blackman window. | |||
// https://tomroelandts.com/articles/how-to-create-a-simple-low-pass-filter | |||
void design_low_pass(float *taps, float samp_rate, float cutoff, size_t ntaps) { | |||
for (size_t i = 0; i < ntaps; i++) { | |||
int x = i - ntaps/2; | |||
taps[i] = sincf(2.0f * cutoff/samp_rate * (float)x); | |||
taps[i] *= blackman(i, ntaps); | |||
} | |||
// Achieve unity gain | |||
normalizef(taps, ntaps); | |||
} | |||
// Hilbert filter with blackman window. | |||
// https://www.recordingblogs.com/wiki/hilbert-transform | |||
void design_hilbert(float *taps, size_t ntaps) { | |||
for (size_t i = 0; i < ntaps; i++) { | |||
int x = i - ntaps/2; | |||
if (x % 2 == 0) { | |||
taps[i] = 0.0f; | |||
} else { | |||
taps[i] = 2.0f / (M_PIf * (float)x); | |||
taps[i] *= blackman(i, ntaps); | |||
} | |||
} | |||
// Achieve unity gain | |||
normalizef(taps, ntaps); | |||
} | |||
float convolve(const float *in, const float *taps, size_t len) { | |||
#ifdef __SSE2__ | |||
__m128 sum = _mm_setzero_ps(); | |||
size_t i; | |||
for (i = 0; i < len - 3; i += 4) { | |||
__m128 _taps = _mm_loadu_ps(&taps[i]); | |||
__m128 _in = _mm_loadu_ps(&in[i]); | |||
sum = _mm_add_ps(sum, _mm_mul_ps(_taps, _in)); | |||
} | |||
float residual = 0.0f; | |||
for (; i < len; i++) { | |||
residual += in[i] * taps[i]; | |||
} | |||
__attribute__((aligned(16))) float _sum[4]; | |||
_mm_store_ps(_sum, sum); | |||
return _sum[0] + _sum[1] + _sum[2] + _sum[3] + residual; | |||
#else | |||
float sum = 0.0f; | |||
for (size_t i = 0; i < len; i++) { | |||
sum += in[i] * taps[i]; | |||
} | |||
return sum; | |||
#endif | |||
} | |||
complexf_t hilbert_transform(const float *in, const float *taps, size_t len) { | |||
return complex_build(in[len / 2], convolve(in, taps, len)); | |||
} | |||
float interpolating_convolve(const float *in, const float *taps, size_t len, float offset) { | |||
float _taps[len]; | |||
for (size_t i = 0; i < len; i++) { | |||
float next = (i == len-1) ? 0.0f : taps[i+1]; | |||
_taps[i] = taps[i]*(1.0f-offset) + next*offset; | |||
} | |||
return convolve(in, _taps, len); | |||
} |
@@ -1,6 +1,6 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.com) | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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 | |||
@@ -16,15 +16,27 @@ | |||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
*/ | |||
#ifndef LIBAPTDEC_FILTER_H_ | |||
#define LIBAPTDEC_FILTER_H_ | |||
#include <complex.h> | |||
#include <stddef.h> | |||
#ifdef _MSC_VER | |||
typedef _Fcomplex complexf_t; | |||
#define complex_build(real, imag) _FCbuild(real, imag) | |||
#define complex_multiply(a, b) _FCmulcc(a, b) | |||
#else | |||
typedef complex float complexf_t; | |||
#define complex_build(real, imag) ((real) + (imag)*I) | |||
#define complex_multiply(a, b) ((a) * (b)) | |||
#endif | |||
void design_low_pass(float *taps, float samp_rate, float cutoff, size_t ntaps); | |||
void design_hilbert(float *taps, size_t ntaps); | |||
float convolve(const float *in, const float *taps, size_t len); | |||
complexf_t hilbert_transform(const float *in, const float *taps, size_t len); | |||
float interpolating_convolve(const float *in, const float *taps, size_t len, float offset, float delta); | |||
float interpolating_convolve(const float *in, const float *taps, size_t len, float offset); | |||
#endif |
@@ -0,0 +1,236 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include <math.h> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include <float.h> | |||
#include "algebra.h" | |||
#include <aptdec.h> | |||
#include "util.h" | |||
#include "calibration.h" | |||
#define APT_COUNT_RATIO (1023.0f/255.0f) | |||
apt_image_t apt_image_clone(apt_image_t img) { | |||
apt_image_t _img = img; | |||
_img.data = (uint8_t *)malloc(APT_IMG_WIDTH * img.rows); | |||
memcpy(_img.data, img.data, APT_IMG_WIDTH * img.rows); | |||
return _img; | |||
} | |||
static void decode_telemetry(const float *data, size_t rows, size_t offset, float *wedges) { | |||
// Calculate row average | |||
float telemetry_rows[rows]; | |||
for (size_t y = 0; y < rows; y++) { | |||
telemetry_rows[y] = meanf(&data[y*APT_IMG_WIDTH + offset + APT_CH_WIDTH], APT_TELEMETRY_WIDTH); | |||
} | |||
// Calculate relative telemetry offset (via step detection, i.e. wedge 8 to 9) | |||
size_t telemetry_offset = 0; | |||
float max_difference = 0.0f; | |||
for (size_t y = APT_WEDGE_HEIGHT; y <= rows - APT_WEDGE_HEIGHT; y++) { | |||
float difference = sumf(&telemetry_rows[y - APT_WEDGE_HEIGHT], APT_WEDGE_HEIGHT) - sumf(&telemetry_rows[y], APT_WEDGE_HEIGHT); | |||
// Find the maximum difference | |||
if (difference > max_difference) { | |||
max_difference = difference; | |||
telemetry_offset = (y + 64) % APT_FRAME_LEN; | |||
} | |||
} | |||
// Find the least noisy frame (via standard deviation) | |||
float best_noise = FLT_MAX; | |||
size_t best_frame = 0; | |||
for (size_t y = telemetry_offset; y < rows; y += APT_FRAME_LEN) { | |||
float noise = 0.0f; | |||
for (size_t i = 0; i < APT_FRAME_WEDGES; i++) { | |||
noise += standard_deviation(&telemetry_rows[y + i*APT_WEDGE_HEIGHT], APT_WEDGE_HEIGHT); | |||
} | |||
if (noise < best_noise) { | |||
best_noise = noise; | |||
best_frame = y; | |||
} | |||
} | |||
for (size_t i = 0; i < APT_FRAME_WEDGES; i++) { | |||
wedges[i] = meanf(&telemetry_rows[best_frame + i*APT_WEDGE_HEIGHT], APT_WEDGE_HEIGHT); | |||
} | |||
} | |||
static float average_spc(apt_image_t *img, size_t offset) { | |||
float rows[img->rows]; | |||
float average = 0.0f; | |||
for (size_t y = 0; y < img->rows; y++) { | |||
float row_average = 0.0f; | |||
for (size_t x = 0; x < APT_SPC_WIDTH; x++) { | |||
row_average += img->data[y*APT_IMG_WIDTH + offset - APT_SPC_WIDTH + x]; | |||
} | |||
row_average /= (float)APT_SPC_WIDTH; | |||
rows[y] = row_average; | |||
average += row_average; | |||
} | |||
average /= (float)img->rows; | |||
float weighted_average = 0.0f; | |||
size_t n = 0; | |||
for (size_t y = 0; y < img->rows; y++) { | |||
if (fabsf(rows[y] - average) < 50.0f) { | |||
weighted_average += rows[y]; | |||
n++; | |||
} | |||
} | |||
return weighted_average / (float)n; | |||
} | |||
apt_image_t apt_normalize(const float *data, size_t rows, apt_satellite_t satellite, int *error) { | |||
apt_image_t img; | |||
img.rows = rows; | |||
img.satellite = satellite; | |||
*error = 0; | |||
if (rows < APTDEC_NORMALIZE_ROWS) { | |||
*error = -1; | |||
return img; | |||
} | |||
// Decode and average wedges | |||
float wedges[APT_FRAME_WEDGES]; | |||
float wedges_cha[APT_FRAME_WEDGES]; | |||
float wedges_chb[APT_FRAME_WEDGES]; | |||
decode_telemetry(data, rows, APT_CHA_OFFSET, wedges_cha); | |||
decode_telemetry(data, rows, APT_CHB_OFFSET, wedges_chb); | |||
for (size_t i = 0; i < APT_FRAME_WEDGES; i++) { | |||
wedges[i] = (wedges_cha[i] + wedges_chb[i]) / 2.0f; | |||
} | |||
// Calculate normalization | |||
const float reference[9] = { 31, 63, 95, 127, 159, 191, 223, 255, 0 }; | |||
linear_t normalization = linear_regression(wedges, reference, 9); | |||
if (normalization.a < 0.0f) { | |||
*error = -1; | |||
return img; | |||
} | |||
// Normalize telemetry | |||
for (size_t i = 0; i < APT_FRAME_WEDGES; i++) { | |||
img.telemetry[0][i] = linear_calc(wedges_cha[i], normalization); | |||
img.telemetry[1][i] = linear_calc(wedges_chb[i], normalization); | |||
} | |||
// Decode channel ID wedges | |||
img.ch[0] = roundf(img.telemetry[0][15] / 32.0f); | |||
img.ch[1] = roundf(img.telemetry[1][15] / 32.0f); | |||
if (img.ch[0] < 1 || img.ch[0] > 6) img.ch[0] = AVHRR_CHANNEL_UNKNOWN; | |||
if (img.ch[1] < 1 || img.ch[1] > 6) img.ch[1] = AVHRR_CHANNEL_UNKNOWN; | |||
// Normalize and quantize image data | |||
img.data = (uint8_t *)malloc(rows * APT_IMG_WIDTH); | |||
for (size_t i = 0; i < rows * APT_IMG_WIDTH; i++) { | |||
float count = linear_calc(data[i], normalization); | |||
img.data[i] = clamp_int(roundf(count), 0, 255); | |||
} | |||
// Get space brightness | |||
img.space_view[0] = average_spc(&img, APT_CHA_OFFSET); | |||
img.space_view[1] = average_spc(&img, APT_CHB_OFFSET); | |||
return img; | |||
} | |||
static void make_thermal_lut(apt_image_t *img, avhrr_channel_t ch, int satellite, float *lut) { | |||
ch -= 4; | |||
const calibration_t calibration = get_calibration(satellite); | |||
const float Ns = calibration.cor[ch].Ns; | |||
const float Vc = calibration.rad[ch].vc; | |||
const float A = calibration.rad[ch].A; | |||
const float B = calibration.rad[ch].B; | |||
// Compute PRT temperature | |||
float T[4]; | |||
for (size_t n = 0; n < 4; n++) { | |||
T[n] = quadratic_calc(img->telemetry[1][n + 9] * APT_COUNT_RATIO, calibration.prt[n]); | |||
} | |||
float Tbb = meanf(T, 4); // Blackbody temperature | |||
float Tbbstar = A + Tbb * B; // Effective blackbody temperature | |||
float Nbb = C1 * pow(Vc, 3) / (expf(C2 * Vc / Tbbstar) - 1.0f); // Blackbody radiance | |||
float Cs = img->space_view[1] * APT_COUNT_RATIO; | |||
float Cb = img->telemetry[1][14] * APT_COUNT_RATIO; | |||
for (size_t i = 0; i < 256; i++) { | |||
float Nl = Ns + (Nbb - Ns) * (Cs - i * APT_COUNT_RATIO) / (Cs - Cb); // Linear radiance estimate | |||
float Nc = quadratic_calc(Nl, calibration.cor[ch].quadratic); // Non-linear correction | |||
float Ne = Nl + Nc; // Corrected radiance | |||
float Testar = C2 * Vc / logf(C1 * powf(Vc, 3) / Ne + 1.0); // Equivalent black body temperature | |||
float Te = (Testar - A) / B; // Temperature (kelvin) | |||
// Convert to celsius | |||
lut[i] = Te - 273.15; | |||
} | |||
} | |||
int apt_calibrate_thermal(apt_image_t *img, apt_region_t region) { | |||
if (img->ch[1] != AVHRR_CHANNEL_4 && img->ch[1] != AVHRR_CHANNEL_5 && img->ch[1] != AVHRR_CHANNEL_3B) { | |||
return 1; | |||
} | |||
float lut[256]; | |||
make_thermal_lut(img, img->ch[1], img->satellite, lut); | |||
for (size_t y = 0; y < img->rows; y++) { | |||
for (size_t x = 0; x < region.width; x++) { | |||
float temperature = lut[img->data[y * APT_IMG_WIDTH + region.offset + x]]; | |||
img->data[y * APT_IMG_WIDTH + region.offset + x] = clamp_int(roundf((temperature + 100.0) / 160.0 * 255.0), 0, 255); | |||
} | |||
} | |||
return 0; | |||
} | |||
static float calibrate_pixel_visible(float value, int channel, calibration_t cal) { | |||
if (value > cal.visible[channel].cutoff) { | |||
return linear_calc(value * APT_COUNT_RATIO, cal.visible[channel].high) / 100.0f * 255.0f; | |||
} else { | |||
return linear_calc(value * APT_COUNT_RATIO, cal.visible[channel].low) / 100.0f * 255.0f; | |||
} | |||
} | |||
int apt_calibrate_visible(apt_image_t *img, apt_region_t region) { | |||
if (img->ch[0] != AVHRR_CHANNEL_1 && img->ch[0] != AVHRR_CHANNEL_2) { | |||
return 1; | |||
} | |||
calibration_t calibration = get_calibration(img->satellite); | |||
for (size_t y = 0; y < img->rows; y++) { | |||
for (size_t x = 0; x < region.width; x++) { | |||
float albedo = calibrate_pixel_visible(img->data[y * APT_IMG_WIDTH + region.offset + x], img->ch[0]-1, calibration); | |||
img->data[y * APT_IMG_WIDTH + region.offset + x] = clamp_int(roundf(albedo), 0, 255); | |||
} | |||
} | |||
return 0; | |||
} |
@@ -0,0 +1,169 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.com) | |||
* Copyright (C) 2021 Jon Beniston (M7RCE) | |||
* | |||
* 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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#ifndef APTDEC_H_ | |||
#define APTDEC_H_ | |||
#include <stddef.h> | |||
#include <stdint.h> | |||
#ifdef __cplusplus | |||
extern "C" { | |||
#endif | |||
#if defined(__GNUC__) && (__GNUC__ >= 4) | |||
# define APTDEC_API __attribute__((visibility("default"))) | |||
#elif defined(_MSC_VER) | |||
# ifdef APTDEC_API_EXPORT | |||
# define APTDEC_API __declspec(dllexport) | |||
# else | |||
# define APTDEC_API __declspec(dllimport) | |||
# endif | |||
#else | |||
# define APTDEC_API | |||
#endif | |||
// Height of a single telemetry wedge | |||
#define APT_WEDGE_HEIGHT 8 | |||
// Numbers of wedges in a frame | |||
#define APT_FRAME_WEDGES 16 | |||
// Height of a telemetry frame | |||
#define APT_FRAME_LEN (APT_WEDGE_HEIGHT * APT_FRAME_WEDGES) | |||
// Width of the overall image | |||
#define APT_IMG_WIDTH 2080 | |||
// Width of sync marker | |||
#define APT_SYNC_WIDTH 39 | |||
// Width of space view | |||
#define APT_SPC_WIDTH 47 | |||
// Width of telemetry | |||
#define APT_TELEMETRY_WIDTH 45 | |||
// Width of a single video channel | |||
#define APT_CH_WIDTH 909 | |||
// Offset to channel A video data | |||
#define APT_CHA_OFFSET (APT_SYNC_WIDTH + APT_SPC_WIDTH) | |||
// Offset to channel B video data | |||
#define APT_CHB_OFFSET (APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_CH_WIDTH + APT_TELEMETRY_WIDTH + APT_SYNC_WIDTH + APT_SPC_WIDTH) | |||
// Number of rows needed for apt_normalize to (reliably) work | |||
#define APTDEC_NORMALIZE_ROWS (APT_FRAME_LEN * 2) | |||
// Channel 1: visible (0.58-0.68 um) | |||
// Channel 2: near-IR (0.725-1.0 um) | |||
// Channel 3A: near-IR (1.58-1.64 um) | |||
// Channel 3B: mid-infrared (3.55-3.93 um) | |||
// Channel 4: thermal-infrared (10.3-11.3 um) | |||
// Channel 5: thermal-infrared (11.5-12.5 um) | |||
typedef enum apt_channel { | |||
AVHRR_CHANNEL_UNKNOWN, | |||
AVHRR_CHANNEL_1, | |||
AVHRR_CHANNEL_2, | |||
AVHRR_CHANNEL_3A, | |||
AVHRR_CHANNEL_4, | |||
AVHRR_CHANNEL_5, | |||
AVHRR_CHANNEL_3B | |||
} avhrr_channel_t; | |||
typedef enum apt_satellite { | |||
NOAA15, | |||
NOAA18, | |||
NOAA19 | |||
} apt_satellite_t; | |||
typedef struct { | |||
uint8_t *data; // Image data | |||
size_t rows; // Number of rows | |||
// Telemetry | |||
apt_satellite_t satellite; | |||
avhrr_channel_t ch[2]; | |||
float space_view[2]; | |||
float telemetry[2][16]; | |||
} apt_image_t; | |||
typedef struct { | |||
uint8_t r, g, b; | |||
} apt_rgb_t; | |||
typedef struct { | |||
size_t offset; | |||
size_t width; | |||
} apt_region_t; | |||
typedef struct aptdec_t aptdec_t; | |||
// Callback function to get samples | |||
// `context` is the same as passed to aptdec_getrow | |||
typedef size_t (*aptdec_callback_t)(float *samples, size_t count, void *context); | |||
// Clone an apt_image_t struct | |||
// Useful for calibration | |||
apt_image_t apt_image_clone(apt_image_t img); | |||
// Returns version of libaptdec in git tag format | |||
// i.e. v2.0.0 or v2.0.0-1-xxxxxx | |||
APTDEC_API char *aptdec_get_version(void); | |||
// Create and destroy libaptdec instances | |||
// If aptdec_init fails it will return NULL | |||
APTDEC_API aptdec_t *aptdec_init(float sample_rate); | |||
APTDEC_API void aptdec_free(aptdec_t *apt); | |||
// Normalize and quantize raw image data | |||
// Data is arranged so that each row starts at APT_IMG_WIDTH*y | |||
APTDEC_API apt_image_t apt_normalize(const float *data, size_t rows, apt_satellite_t satellite, int *error); | |||
// Get an entire row of pixels | |||
// Requires that `row` has enough space to store APT_IMG_WIDTH*2 | |||
// Returns 0 when `callback` return value != count | |||
APTDEC_API int aptdec_getrow(aptdec_t *apt, float *row, aptdec_callback_t callback, void *context); | |||
// Calibrate channels | |||
APTDEC_API int apt_calibrate_thermal(apt_image_t *img, apt_region_t region); | |||
APTDEC_API int apt_calibrate_visible(apt_image_t *img, apt_region_t region); | |||
APTDEC_API void apt_denoise (apt_image_t *img, apt_region_t region); | |||
APTDEC_API void apt_flip (apt_image_t *img, apt_region_t region); | |||
APTDEC_API void apt_stretch (apt_image_t *img, apt_region_t region); | |||
APTDEC_API void apt_equalize(apt_image_t *img, apt_region_t region); | |||
APTDEC_API int apt_crop (apt_image_t *img); | |||
// Composite two RGB values as layers, in most cases bottom_a will be 1.0f | |||
APTDEC_API apt_rgb_t apt_composite_rgb(apt_rgb_t top, float top_a, apt_rgb_t bottom, float bottom_a); | |||
// Apply a gradient such as temperature_gradient | |||
// If gradient is less than 256 elements it is the callers responsibility | |||
// that `val` does not exceed the length of the gradient | |||
APTDEC_API apt_rgb_t apt_gradient(const uint32_t *gradient, uint8_t val); | |||
static const apt_region_t APT_REGION_CHA = { APT_CHA_OFFSET, APT_CH_WIDTH }; | |||
static const apt_region_t APT_REGION_CHB = { APT_CHB_OFFSET, APT_CH_WIDTH }; | |||
static const apt_region_t APT_REGION_CHA_FULL = { 0, APT_IMG_WIDTH/2 }; | |||
static const apt_region_t APT_REGION_CHB_FULL = { APT_IMG_WIDTH/2, APT_IMG_WIDTH/2 }; | |||
static const apt_region_t APT_REGION_FULL = { 0, APT_IMG_WIDTH }; | |||
extern const uint32_t temperature_gradient[256]; | |||
extern const uint32_t precipitation_gradient[58]; | |||
#ifdef __cplusplus | |||
} | |||
#endif | |||
#endif |
@@ -0,0 +1,40 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include "util.h" | |||
#include <stdlib.h> | |||
#include <math.h> | |||
float clamp(float x, float lo, float hi) { | |||
if (x > hi) return hi; | |||
if (x < lo) return lo; | |||
return x; | |||
} | |||
int clamp_int(int x, int lo, int hi) { | |||
if (x > hi) return hi; | |||
if (x < lo) return lo; | |||
return x; | |||
} | |||
void swap_uint8(uint8_t *a, uint8_t *b) { | |||
uint8_t tmp = *a; | |||
*a = *b; | |||
*b = tmp; | |||
} |
@@ -0,0 +1,29 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2023 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#ifndef LIBAPTDEC_UTIL_H_ | |||
#define LIBAPTDEC_UTIL_H_ | |||
#include <stdint.h> | |||
float clamp(float x, float lo, float hi); | |||
int clamp_int(int x, int lo, int hi); | |||
void swap_uint8(uint8_t *a, uint8_t *b); | |||
#endif |
@@ -1,126 +0,0 @@ | |||
/* | |||
* This file is part of Aptdec. | |||
* Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 | |||
* Copyright (c) 2021 Jon Beniston (M7RCE) | |||
* | |||
* Aptdec 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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
#ifndef APT_H | |||
#define APT_H | |||
#ifdef __cplusplus | |||
extern "C" { | |||
#endif | |||
#if defined(__GNUC__) && (__GNUC__ >= 4) | |||
#define APT_API __attribute__((visibility("default"))) | |||
#elif defined(_MSC_VER) | |||
#ifdef APT_API_EXPORT | |||
#define APT_API __declspec(dllexport) | |||
#elif APT_API_STATIC | |||
#define APT_API | |||
#else if | |||
#define APT_API __declspec(dllimport) | |||
#endif | |||
#else | |||
#define APT_API | |||
#endif | |||
// Maximum height of an APT image in number of rows | |||
#define APT_MAX_HEIGHT 3000 | |||
// Width in pixels of sync | |||
#define APT_SYNC_WIDTH 39 | |||
// Width in pixels of space | |||
#define APT_SPC_WIDTH 47 | |||
// Width in pixels of telemetry | |||
#define APT_TELE_WIDTH 45 | |||
// Width in pixels of a single channel image | |||
#define APT_CH_WIDTH 909 | |||
#define APT_FRAME_LEN 128 | |||
#define APT_CH_OFFSET (APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_CH_WIDTH + APT_TELE_WIDTH) | |||
// Width in pixels of full frame, including sync, space, images and telemetry | |||
#define APT_IMG_WIDTH 2080 | |||
// Offset in pixels to channel A | |||
#define APT_CHA_OFFSET (APT_SYNC_WIDTH + APT_SPC_WIDTH) | |||
// Offset in pixels to channel B | |||
#define APT_CHB_OFFSET (APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_CH_WIDTH + APT_TELE_WIDTH + APT_SYNC_WIDTH + APT_SPC_WIDTH) | |||
#define APT_TOTAL_TELE (APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_TELE_WIDTH + APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_TELE_WIDTH) | |||
// Number of rows required for apt_calibrate | |||
#define APT_CALIBRATION_ROWS 192 | |||
// Channel ID returned by apt_calibrate | |||
// NOAA-15: https://nssdc.gsfc.nasa.gov/nmc/experiment/display.action?id=1998-030A-01 | |||
// Channel 1: visible (0.58-0.68 um) | |||
// Channel 2: near-IR (0.725-1.0 um) | |||
// Channel 3A: near-IR (1.58-1.64 um) | |||
// Channel 3B: mid-infrared (3.55-3.93 um) | |||
// Channel 4: thermal-infrared (10.3-11.3 um) | |||
// Channel 5: thermal-infrared (11.5-12.5 um) | |||
typedef enum apt_channel { | |||
APT_CHANNEL_UNKNOWN, | |||
APT_CHANNEL_1, | |||
APT_CHANNEL_2, | |||
APT_CHANNEL_3A, | |||
APT_CHANNEL_4, | |||
APT_CHANNEL_5, | |||
APT_CHANNEL_3B | |||
} apt_channel_t; | |||
// Width in elements of apt_image_t.prow arrays | |||
#define APT_PROW_WIDTH 2150 | |||
// apt_getpixelrow callback function to get audio samples. | |||
// context is the same as passed to apt_getpixelrow. | |||
typedef int (*apt_getsamples_t)(void *context, float *samples, int count); | |||
typedef struct { | |||
float *prow[APT_MAX_HEIGHT]; // Row buffers | |||
int nrow; // Number of rows | |||
int zenith; // Row in image where satellite reaches peak elevation | |||
apt_channel_t chA, chB; // ID of each channel | |||
char name[256]; // Stripped filename | |||
char *palette; // Filename of palette | |||
} apt_image_t; | |||
typedef struct { | |||
float r, g, b; | |||
} apt_rgb_t; | |||
int APT_API apt_init(double sample_rate); | |||
int APT_API apt_getpixelrow(float *pixelv, int nrow, int *zenith, int reset, apt_getsamples_t getsamples, void *context); | |||
void APT_API apt_histogramEqualise(float **prow, int nrow, int offset, int width); | |||
void APT_API apt_linearEnhance(float **prow, int nrow, int offset, int width); | |||
apt_channel_t APT_API apt_calibrate(float **prow, int nrow, int offset, int width); | |||
void APT_API apt_denoise(float **prow, int nrow, int offset, int width); | |||
void APT_API apt_flipImage(apt_image_t *img, int width, int offset); | |||
int APT_API apt_cropNoise(apt_image_t *img); | |||
void APT_API apt_calibrate_thermal(int satnum, apt_image_t *img, int offset, int width); | |||
void APT_API apt_calibrate_visible(int satnum, apt_image_t *img, int offset, int width); | |||
// Moved to apt_calibrate_thermal | |||
#define apt_temperature apt_calibrate_thermal | |||
apt_rgb_t APT_API apt_applyPalette(char *palette, int val); | |||
apt_rgb_t APT_API apt_RGBcomposite(apt_rgb_t top, float top_a, apt_rgb_t bottom, float bottom_a); | |||
extern char APT_API apt_TempPalette[256 * 3]; | |||
extern char APT_API apt_PrecipPalette[58 * 3]; | |||
#ifdef __cplusplus | |||
} | |||
#endif | |||
#endif |
@@ -1 +0,0 @@ | |||
Subproject commit c612dc03958cdbd538ca306d61853b643a435933 |
@@ -1,101 +0,0 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2019-2022 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include "calibration.h" | |||
#include "util.h" | |||
const calibration_t calibration[3] = {{.name = "NOAA-15", | |||
.prt = | |||
{ | |||
{1.36328e-06f, 0.051045f, 276.60157f}, // PRT 1 | |||
{1.47266e-06f, 0.050909f, 276.62531f}, // PRT 2 | |||
{1.47656e-06f, 0.050907f, 276.67413f}, // PRT 3 | |||
{1.47656e-06f, 0.050966f, 276.59258f} // PRT 4 | |||
}, | |||
.visible = {{.low = {0.0568f, -2.1874f}, .high = {0.1633f, -54.9928f}, .cutoff = 496.0f}, | |||
{.low = {0.0596f, -2.4096f}, .high = {0.1629f, -55.2436f}, .cutoff = 511.0f}}, | |||
.rad = | |||
{ | |||
{925.4075f, 0.337810f, 0.998719f}, // Channel 4 | |||
{839.8979f, 0.304558f, 0.999024f}, // Channel 5 | |||
{2695.9743f, 1.621256f, 0.998015f} // Channel 3B | |||
}, | |||
.cor = | |||
{ | |||
{-4.50f, {0.0004524f, -0.0932f, 4.76f}}, // Channel 4 | |||
{-3.61f, {0.0002811f, -0.0659f, 3.83f}}, // Channel 5 | |||
{0.0f, {0.0f, 0.0f, 0.0f}} // Channel 3B | |||
}}, | |||
{.name = "NOAA-18", | |||
.prt = | |||
{ | |||
{1.657e-06f, 0.05090f, 276.601f}, // PRT 1 | |||
{1.482e-06f, 0.05101f, 276.683f}, // PRT 2 | |||
{1.313e-06f, 0.05117f, 276.565f}, // PRT 3 | |||
{1.484e-06f, 0.05103f, 276.615f} // PRT 4 | |||
}, | |||
.visible = {{.low = {0.06174f, -2.434f}, .high = {0.1841f, -63.31f}, .cutoff = 501.54f}, | |||
{.low = {0.07514f, -2.960f}, .high = {0.2254f, -78.55f}, .cutoff = 500.40f}}, | |||
.rad = | |||
{ | |||
{928.1460f, 0.436645f, 0.998607f}, // Channel 4 | |||
{833.2532f, 0.253179f, 0.999057f}, // Channel 5 | |||
{2659.7952f, 1.698704f, 0.996960f} // Channel 3B | |||
}, | |||
.cor = | |||
{ | |||
{-5.53f, {0.00052337f, -0.11069f, 5.82f}}, // Channel 4 | |||
{-2.22f, {0.00017715f, -0.04360f, 2.67f}}, // Channel 5 | |||
{0.0f, {0.0f, 0.0f, 0.0f}} // Channel 3B | |||
}}, | |||
{.name = "NOAA-19", | |||
.prt = | |||
{ | |||
{1.405783e-06f, 0.051111f, 276.6067f}, // PRT 1 | |||
{1.496037e-06f, 0.051090f, 276.6119f}, // PRT 2 | |||
{1.496990e-06f, 0.051033f, 276.6311f}, // PRT 3 | |||
{1.493110e-06f, 0.051058f, 276.6268f} // PRT 4 | |||
}, | |||
.visible = {{.low = {0.05555f, -2.159f}, .high = {0.1639f, -56.33f}, .cutoff = 496.43f}, | |||
{.low = {0.06614f, -2.565f}, .high = {0.1970f, -68.01f}, .cutoff = 500.37f}}, | |||
.rad = | |||
{ | |||
{928.9f, 0.53959f, 0.998534f}, // Channel 4 | |||
{831.9f, 0.36064f, 0.998913f}, // Channel 5 | |||
{2670.0f, 1.67396f, 0.997364f} // Channel 3B | |||
}, | |||
.cor = { | |||
{-5.49f, {0.00054668f, -0.11187f, 5.70f}}, // Channel 4 | |||
{-3.39f, {0.00024985f, -0.05991f, 3.58f}}, // Channel 5 | |||
{0.0f, {0.0f, 0.0f, 0.0f}} // Channel 3B | |||
}}}; | |||
calibration_t get_calibration(int satid) { | |||
switch (satid) { | |||
case 15: | |||
return calibration[0]; | |||
case 18: | |||
return calibration[1]; | |||
case 19: | |||
return calibration[2]; | |||
default: | |||
error("Invalid satid in get_calibration()"); /* following is only to shut up the compiler */ | |||
return calibration[0]; | |||
} | |||
} |
@@ -1,83 +0,0 @@ | |||
/* | |||
* This file is part of Aptdec. | |||
* Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 | |||
* | |||
* Aptdec 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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
#include "color.h" | |||
#include "apt.h" | |||
apt_rgb_t apt_applyPalette(char *palette, int val) { | |||
return (apt_rgb_t){palette[(int)CLIP(val, 0, 255) * 3 + 0], palette[(int)CLIP(val, 0, 255) * 3 + 1], | |||
palette[(int)CLIP(val, 0, 255) * 3 + 2]}; | |||
} | |||
apt_rgb_t apt_RGBcomposite(apt_rgb_t top, float top_a, apt_rgb_t bottom, float bottom_a) { | |||
return (apt_rgb_t){MCOMPOSITE(top.r, top_a, bottom.r, bottom_a), MCOMPOSITE(top.g, top_a, bottom.g, bottom_a), | |||
MCOMPOSITE(top.b, top_a, bottom.b, bottom_a)}; | |||
} | |||
// The "I totally didn't just steal this from WXtoImg" palette | |||
char apt_TempPalette[256 * 3] = { | |||
"\x45\x0\x8f\x46\x0\x91\x47\x0\x92\x48\x0\x94\x49\x0\x96\x4a\x0\x98\x4b\x0\x9b\x4d\x0\x9d" | |||
"\x4e\x0\xa0\x50\x0\xa2\x51\x0\xa5\x52\x0\xa7\x54\x0\xaa\x56\x0\xae\x57\x0\xb1" | |||
"\x58\x0\xb4\x5a\x0\xb7\x5c\x0\xba\x5e\x0\xbd\x5f\x0\xc0\x61\x0\xc4\x64\x0\xc8" | |||
"\x66\x0\xcb\x68\x0\xce\x69\x0\xd1\x68\x0\xd4\x65\x0\xd7\x63\x0\xda\x61\x0\xdd" | |||
"\x5d\x0\xe1\x5b\x0\xe4\x59\x0\xe6\x56\x0\xe9\x53\x0\xeb\x50\x0\xee\x4d\x0\xf0" | |||
"\x49\x0\xf3\x47\x0\xfc\x43\x0\xfa\x31\x0\xbf\x20\x0\x89\x20\x0\x92\x1e\x0\x95" | |||
"\x1b\x0\x97\x19\x0\x9a\x17\x0\x9c\x15\x0\x9e\x12\x0\xa0\xf\x0\xa3\xf\x2\xa5" | |||
"\xe\x6\xa8\xe\xa\xab\xe\xd\xad\xe\x11\xb1\xd\x15\xb4\xd\x18\xb7\xd\x1c\xba" | |||
"\xb\x21\xbd\xa\x25\xc0\xa\x29\xc3\x9\x2d\xc6\x8\x33\xca\x7\x36\xcd\x7\x3b\xd0" | |||
"\x7\x41\xd3\x5\x45\xd6\x4\x4b\xd9\x4\x50\xdc\x3\x55\xde\x2\x5d\xe2\x1\x61\xe5" | |||
"\x0\x66\xe7\x0\x6c\xea\x0\x72\xec\x0\x78\xee\x0\x7d\xf0\x0\x82\xf3\x0\x8d\xfc" | |||
"\x0\x90\xfa\x0\x71\xbf\x0\x54\x89\x0\x5c\x91\x0\x61\x94\x0\x64\x96\x0\x68\x97" | |||
"\x0\x6d\x99\x0\x71\x9b\x0\x75\x9d\x0\x79\x9f\x0\x7e\xa0\x0\x82\xa2\x0\x87\xa4" | |||
"\x0\x8c\xa6\x0\x92\xaa\x0\x96\xac\x0\x9c\xae\x0\xa2\xb1\x0\xa6\xb3\x0\xaa\xb5" | |||
"\x0\xad\xb7\x0\xb1\xba\x0\xb6\xbe\x0\xba\xc0\x0\xbe\xc2\x0\xc2\xc5\x0\xc6\xc6" | |||
"\x0\xca\xc9\x0\xcc\xca\x0\xcf\xcb\x0\xd2\xcc\x0\xd4\xcc\x0\xd6\xcc\x0\xd9\xcb" | |||
"\x0\xdb\xcb\x0\xde\xcb\x0\xe0\xcb\x0\xe2\xcc\x0\xea\xd2\x0\xea\xcf\x0\xb9\xa4" | |||
"\x0\x8e\x7a\x1\x94\x7c\x4\x97\x79\x7\x99\x75\x9\x9b\x71\xd\x9d\x6b\x10\x9f\x67" | |||
"\x12\xa1\x63\x15\xa3\x5f\x17\xa5\x59\x1a\xa8\x55\x1d\xaa\x50\x20\xac\x4b\x24\xaf\x45" | |||
"\x28\xb2\x41\x2b\xb5\x3b\x2e\xb8\x35\x31\xba\x30\x34\xbd\x2b\x39\xbf\x24\x3f\xc1\x17" | |||
"\x49\xc5\x8\x4f\xc8\x1\x4f\xca\x0\x4e\xcd\x0\x4e\xcf\x0\x4f\xd2\x0\x54\xd5\x0" | |||
"\x5d\xd8\x0\x68\xdb\x0\x6e\xdd\x0\x74\xdf\x0\x7a\xe2\x0\x7f\xe4\x0\x85\xe7\x0" | |||
"\x8b\xe9\x0\x8f\xeb\x0\x9b\xf3\x0\x9e\xf2\x0\x7e\xbb\x0\x60\x8a\x0\x68\x92\x0" | |||
"\x6d\x95\x0\x71\x96\x0\x75\x98\x0\x7b\x9a\x0\x7f\x9d\x0\x83\x9f\x0\x87\xa1\x0" | |||
"\x8c\xa2\x0\x8f\xa5\x0\x92\xa7\x0\x96\xa9\x0\x9a\xad\x0\x9d\xb0\x0\xa1\xb2\x0" | |||
"\xa5\xb5\x0\xa9\xb7\x0\xad\xba\x0\xb2\xbd\x0\xb6\xbf\x0\xbb\xc3\x0\xbf\xc6\x0" | |||
"\xc3\xc8\x0\xc8\xcb\x0\xcc\xce\x0\xd0\xd1\x0\xd3\xd2\x0\xd5\xd4\x0\xd9\xd4\x0" | |||
"\xdc\xd4\x0\xde\xd5\x0\xe1\xd5\x0\xe3\xd5\x0\xe6\xd4\x0\xe8\xd1\x0\xea\xce\x0" | |||
"\xf2\xcf\x0\xf2\xca\x0\xbb\x99\x0\x8a\x6e\x0\x92\x72\x0\x95\x72\x0\x97\x71\x0" | |||
"\x9a\x70\x0\x9c\x6e\x0\x9e\x6d\x0\xa0\x6b\x0\xa3\x6a\x0\xa5\x68\x0\xa8\x67\x0" | |||
"\xab\x66\x0\xae\x65\x0\xb2\x63\x0\xb4\x61\x0\xb7\x5f\x0\xba\x5d\x0\xbd\x5c\x0" | |||
"\xc0\x59\x0\xc3\x57\x0\xc6\x54\x0\xca\x50\x0\xcd\x4d\x0\xd0\x4a\x0\xd3\x47\x0" | |||
"\xd6\x43\x0\xd9\x40\x0\xdc\x3d\x0\xde\x39\x0\xe2\x33\x0\xe5\x2f\x0\xe7\x2c\x0" | |||
"\xea\x28\x0\xec\x23\x0\xef\x1f\x0\xf1\x1a\x0\xf3\x14\x0\xfb\xf\x0\xfa\xd\x0" | |||
"\xc1\x5\x0\x8e\x0\x0\x97\x0\x0\x9b\x0\x0\x9e\x0\x0\xa1\x0\x0\xa5\x0\x0" | |||
"\xa9\x0\x0\xad\x0\x0\xb1\x0\x0\xb6\x0\x0\xba\x0\x0\xbd\x0\x0\xc2\x0\x0" | |||
"\xc8\x0\x0\xcc\x0\x0\xcc\x0\x0"}; | |||
char apt_PrecipPalette[58 * 3] = { | |||
"\x8\x89\x41\x0\xc5\x44\x0\xd1\x2c\x0\xe3\x1c\x0\xf9\x6\x14\xff\x0\x3e\xff\x0\x5d\xff\x0" | |||
"\x80\xff\x0\xab\xff\x0\xcd\xfe\x0\xf8\xff\x0\xff\xe6\x0\xff\xb8\x0\xff\x98\x0" | |||
"\xff\x75\x0\xff\x49\x0\xfe\x26\x0\xff\x4\x0\xdf\x0\x0\xa8\x0\x0\x87\x0\x0" | |||
"\x5a\x0\x0\x39\x0\x0\x11\x0\x0\xe\x10\x10\x23\x22\x22\x33\x33\x33\x41\x41\x41" | |||
"\x53\x53\x53\x60\x60\x60\x6e\x6e\x6e\x80\x80\x80\x8e\x8e\x8e\xa0\xa0\xa0\xae\xae\xae" | |||
"\xc0\xc0\xc0\xce\xce\xce\xdc\xdc\xdc\xef\xef\xef\xfa\xfa\xfa\xff\xff\xff\xff\xff\xff" | |||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" | |||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" | |||
"\xff\xff\xff"}; |
@@ -1,3 +0,0 @@ | |||
#include "common.h" | |||
#define MCOMPOSITE(m1, a1, m2, a2) (m1 * a1 + m2 * a2 * (1 - a1)) |
@@ -1,60 +0,0 @@ | |||
/* | |||
* This file is part of Aptdec. | |||
* Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 | |||
* | |||
* Aptdec 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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
// Constants | |||
#define VERSION "Aptdec; (c) 2004-2009 Thierry Leconte F4DWV, Xerbo (xerbo@protonmail.com) 2019-2022" | |||
// Useful macros | |||
#define CLIP(v, lo, hi) (v > hi ? hi : (v > lo ? v : lo)) | |||
#define CONTAINS(str, char) (strchr(str, (int)char) != NULL) | |||
// Typedefs | |||
#ifndef STRUCTS_DEFINED | |||
#define STRUCTS_DEFINED | |||
typedef struct { | |||
char *type; // Output image type | |||
char *effects; // Effects on the image | |||
int satnum; // The satellite number | |||
char *path; // Output directory | |||
int realtime; // Realtime decoding | |||
char *filename; // Output filename | |||
char *palette; // Filename of palette | |||
float gamma; // Gamma | |||
} options_t; | |||
enum imagetypes { | |||
Raw_Image = 'r', | |||
Palleted = 'p', | |||
Temperature = 't', | |||
Channel_A = 'a', | |||
Channel_B = 'b', | |||
Distribution = 'd', | |||
Visible = 'v' | |||
}; | |||
enum effects { | |||
Crop_Telemetry = 't', | |||
Histogram_Equalise = 'h', | |||
Denoise = 'd', | |||
Precipitation_Overlay = 'p', | |||
Flip_Image = 'f', | |||
Linear_Equalise = 'l', | |||
Crop_Noise = 'c' | |||
}; | |||
#endif |
@@ -1,258 +0,0 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2022 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include <math.h> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include "apt.h" | |||
#include "filter.h" | |||
#include "taps.h" | |||
#include "util.h" | |||
// Block sizes | |||
#define BLKAMP 32768 | |||
#define BLKIN 32768 | |||
#define CARRIER_FREQ 2400.0 | |||
#define MAX_CARRIER_OFFSET 20.0 | |||
#define RSMULT 15 | |||
#define Fi (APT_IMG_WIDTH * 2 * RSMULT) | |||
static float _sample_rate; | |||
static float offset = 0.0; | |||
static float FreqLine = 1.0; | |||
static float oscillator_freq; | |||
static float pll_alpha; | |||
static float pll_beta; | |||
// Initalise and configure PLL | |||
int apt_init(double sample_rate) { | |||
if (sample_rate > Fi) return 1; | |||
if (sample_rate < APT_IMG_WIDTH * 2) return -1; | |||
_sample_rate = sample_rate; | |||
// Pll configuration | |||
pll_alpha = 50 / _sample_rate; | |||
pll_beta = pll_alpha * pll_alpha / 2.0; | |||
oscillator_freq = CARRIER_FREQ / sample_rate; | |||
return 0; | |||
} | |||
static float pll(complexf_t in) { | |||
static float oscillator_phase = 0.0; | |||
// Internal oscillator | |||
#ifdef _MSC_VER | |||
complexf_t osc = _FCbuild(cos(oscillator_phase), -sin(oscillator_phase)); | |||
in = _FCmulcc(in, osc); | |||
#else | |||
complexf_t osc = cos(oscillator_phase) + -sin(oscillator_phase) * I; | |||
in *= osc; | |||
#endif | |||
// Error detector | |||
float error = cargf(in); | |||
// Adjust frequency and phase | |||
oscillator_freq += pll_beta * error; | |||
oscillator_freq = clamp_half(oscillator_freq, (CARRIER_FREQ + MAX_CARRIER_OFFSET) / _sample_rate); | |||
oscillator_phase += M_TAUf * (pll_alpha * error + oscillator_freq); | |||
oscillator_phase = remainderf(oscillator_phase, M_TAUf); | |||
return crealf(in); | |||
} | |||
// Convert samples into pixels | |||
static int getamp(float *ampbuff, int count, apt_getsamples_t getsamples, void *context) { | |||
static float inbuff[BLKIN]; | |||
static int idxin = 0; | |||
static int nin = 0; | |||
for (int n = 0; n < count; n++) { | |||
float I2, Q; | |||
// Get some more samples when needed | |||
if (nin < HILBERT_FILTER_SIZE * 2 + 2) { | |||
// Number of samples read | |||
int res; | |||
memmove(inbuff, &(inbuff[idxin]), nin * sizeof(float)); | |||
idxin = 0; | |||
// Read some samples | |||
res = getsamples(context, &(inbuff[nin]), BLKIN - nin); | |||
nin += res; | |||
// Make sure there is enough samples to continue | |||
if (nin < HILBERT_FILTER_SIZE * 2 + 2) return n; | |||
} | |||
// Process read samples into a brightness value | |||
complexf_t sample = hilbert_transform(&inbuff[idxin], hilbert_filter, HILBERT_FILTER_SIZE); | |||
ampbuff[n] = pll(sample); | |||
// Increment current sample | |||
idxin++; | |||
nin--; | |||
} | |||
return count; | |||
} | |||
// Sub-pixel offsetting | |||
int getpixelv(float *pvbuff, int count, apt_getsamples_t getsamples, void *context) { | |||
// Amplitude buffer | |||
static float ampbuff[BLKAMP]; | |||
static int nam = 0; | |||
static int idxam = 0; | |||
float mult; | |||
// Gaussian resampling factor | |||
mult = (float)Fi / _sample_rate * FreqLine; | |||
int m = (int)(LOW_PASS_SIZE / mult + 1); | |||
for (int n = 0; n < count; n++) { | |||
int shift; | |||
if (nam < m) { | |||
int res; | |||
memmove(ampbuff, &(ampbuff[idxam]), nam * sizeof(float)); | |||
idxam = 0; | |||
res = getamp(&(ampbuff[nam]), BLKAMP - nam, getsamples, context); | |||
nam += res; | |||
if (nam < m) return n; | |||
} | |||
pvbuff[n] = interpolating_convolve(&(ampbuff[idxam]), low_pass, LOW_PASS_SIZE, offset, mult) * mult * 256.0; | |||
shift = ((int)floor((RSMULT - offset) / mult)) + 1; | |||
offset = shift * mult + offset - RSMULT; | |||
idxam += shift; | |||
nam -= shift; | |||
} | |||
return count; | |||
} | |||
// Get an entire row of pixels, aligned with sync markers | |||
int apt_getpixelrow(float *pixelv, int nrow, int *zenith, int reset, apt_getsamples_t getsamples, void *context) { | |||
static float pixels[APT_IMG_WIDTH + SYNC_PATTERN_SIZE]; | |||
static size_t npv; | |||
static int synced = 0; | |||
static float max = 0.0; | |||
static float minDoppler = 1000000000, previous = 0; | |||
if (reset) synced = 0; | |||
float corr, ecorr, lcorr; | |||
int res; | |||
// Move the row buffer into the the image buffer | |||
if (npv > 0) memmove(pixelv, pixels, npv * sizeof(float)); | |||
// Get the sync line | |||
if (npv < SYNC_PATTERN_SIZE + 2) { | |||
res = getpixelv(&(pixelv[npv]), SYNC_PATTERN_SIZE + 2 - npv, getsamples, context); | |||
npv += res; | |||
if (npv < SYNC_PATTERN_SIZE + 2) return 0; | |||
} | |||
// Calculate the frequency offset | |||
ecorr = convolve(pixelv, sync_pattern, SYNC_PATTERN_SIZE); | |||
corr = convolve(&pixelv[1], sync_pattern, SYNC_PATTERN_SIZE - 1); | |||
lcorr = convolve(&pixelv[2], sync_pattern, SYNC_PATTERN_SIZE - 2); | |||
FreqLine = 1.0 + ((ecorr - lcorr) / corr / APT_IMG_WIDTH / 4.0); | |||
float val = fabs(lcorr - ecorr) * 0.25 + previous * 0.75; | |||
if (val < minDoppler && nrow > 10) { | |||
minDoppler = val; | |||
*zenith = nrow; | |||
} | |||
previous = fabs(lcorr - ecorr); | |||
// The point in which the pixel offset is recalculated | |||
if (corr < 0.75 * max) { | |||
synced = 0; | |||
FreqLine = 1.0; | |||
} | |||
max = corr; | |||
if (synced < 8) { | |||
int mshift; | |||
static int lastmshift; | |||
if (npv < APT_IMG_WIDTH + SYNC_PATTERN_SIZE) { | |||
res = getpixelv(&(pixelv[npv]), APT_IMG_WIDTH + SYNC_PATTERN_SIZE - npv, getsamples, context); | |||
npv += res; | |||
if (npv < APT_IMG_WIDTH + SYNC_PATTERN_SIZE) return 0; | |||
} | |||
// Test every possible position until we get the best result | |||
mshift = 0; | |||
for (int shift = 0; shift < APT_IMG_WIDTH; shift++) { | |||
float corr; | |||
corr = convolve(&(pixelv[shift + 1]), sync_pattern, SYNC_PATTERN_SIZE); | |||
if (corr > max) { | |||
mshift = shift; | |||
max = corr; | |||
} | |||
} | |||
// Stop rows dissapearing into the void | |||
int mshiftOrig = mshift; | |||
if (abs(lastmshift - mshift) > 3 && nrow != 0) { | |||
mshift = 0; | |||
} | |||
lastmshift = mshiftOrig; | |||
// If we are already as aligned as we can get, just continue | |||
if (mshift == 0) { | |||
synced++; | |||
} else { | |||
memmove(pixelv, &(pixelv[mshift]), (npv - mshift) * sizeof(float)); | |||
npv -= mshift; | |||
synced = 0; | |||
FreqLine = 1.0; | |||
} | |||
} | |||
// Get the rest of this row | |||
if (npv < APT_IMG_WIDTH) { | |||
res = getpixelv(&(pixelv[npv]), APT_IMG_WIDTH - npv, getsamples, context); | |||
npv += res; | |||
if (npv < APT_IMG_WIDTH) return 0; | |||
} | |||
// Move the sync lines into the output buffer with the calculated offset | |||
if (npv == APT_IMG_WIDTH) { | |||
npv = 0; | |||
} else { | |||
memmove(pixels, &(pixelv[APT_IMG_WIDTH]), (npv - APT_IMG_WIDTH) * sizeof(float)); | |||
npv -= APT_IMG_WIDTH; | |||
} | |||
return 1; | |||
} |
@@ -1,62 +0,0 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2022 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
#include "filter.h" | |||
#include <math.h> | |||
#include "util.h" | |||
float convolve(const float *in, const float *taps, size_t len) { | |||
float sum = 0.0; | |||
for (size_t i = 0; i < len; i++) { | |||
sum += in[i] * taps[i]; | |||
} | |||
return sum; | |||
} | |||
complexf_t hilbert_transform(const float *in, const float *taps, size_t len) { | |||
float i = 0.0; | |||
float q = 0.0; | |||
for (size_t k = 0; k < len; k++) { | |||
q += in[2 * k] * taps[k]; | |||
i += in[2 * k]; | |||
} | |||
i = in[len - 1] - (i / len); | |||
#ifdef _MSC_VER | |||
return _FCbuild(i, q); | |||
#else | |||
return i + q * I; | |||
#endif | |||
} | |||
float interpolating_convolve(const float *in, const float *taps, size_t len, float offset, float delta) { | |||
float out = 0.0; | |||
float n = offset; | |||
for (size_t i = 0; i < (len - 1) / delta - 1; n += delta, i++) { | |||
int k = (int)floor(n); | |||
float alpha = n - k; | |||
out += in[i] * (taps[k] * (1.0f - alpha) + taps[k + 1] * alpha); | |||
} | |||
return out; | |||
} |
@@ -1,388 +0,0 @@ | |||
/* | |||
* This file is part of Aptdec. | |||
* Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 | |||
* | |||
* Aptdec 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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
#include "image.h" | |||
#include <math.h> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include "algebra.h" | |||
#include "apt.h" | |||
#include "util.h" | |||
static linear_t compute_regression(float *wedges) { | |||
// { 0.106, 0.215, 0.324, 0.433, 0.542, 0.652, 0.78, 0.87, 0.0 } | |||
const float teleramp[9] = {31.07, 63.02, 94.96, 126.9, 158.86, 191.1, 228.62, 255.0, 0.0}; | |||
return linear_regression(wedges, teleramp, 9); | |||
} | |||
static float tele[16]; | |||
static float Cs; | |||
void apt_histogramEqualise(float **prow, int nrow, int offset, int width) { | |||
// Plot histogram | |||
int histogram[256] = {0}; | |||
for (int y = 0; y < nrow; y++) | |||
for (int x = 0; x < width; x++) histogram[(int)CLIP(prow[y][x + offset], 0, 255)]++; | |||
// Calculate cumulative frequency | |||
long sum = 0, cf[256] = {0}; | |||
for (int i = 0; i < 255; i++) { | |||
sum += histogram[i]; | |||
cf[i] = sum; | |||
} | |||
// Apply histogram | |||
int area = nrow * width; | |||
for (int y = 0; y < nrow; y++) { | |||
for (int x = 0; x < width; x++) { | |||
int k = (int)prow[y][x + offset]; | |||
prow[y][x + offset] = (256.0f / area) * cf[k]; | |||
} | |||
} | |||
} | |||
void apt_linearEnhance(float **prow, int nrow, int offset, int width) { | |||
// Plot histogram | |||
int histogram[256] = {0}; | |||
for (int y = 0; y < nrow; y++) | |||
for (int x = 0; x < width; x++) histogram[(int)CLIP(prow[y][x + offset], 0, 255)]++; | |||
// Find min/max points | |||
int min = -1, max = -1; | |||
for (int i = 5; i < 250; i++) { | |||
if (histogram[i] / width / (nrow / 255.0) > 0.1) { | |||
if (min == -1) min = i; | |||
max = i; | |||
} | |||
} | |||
// Stretch the brightness into the new range | |||
for (int y = 0; y < nrow; y++) { | |||
for (int x = 0; x < width; x++) { | |||
prow[y][x + offset] = (prow[y][x + offset] - min) / (max - min) * 255.0f; | |||
prow[y][x + offset] = CLIP(prow[y][x + offset], 0.0f, 255.0f); | |||
} | |||
} | |||
} | |||
// Brightness calibrate, including telemetry | |||
void calibrateImage(float **prow, int nrow, int offset, int width, linear_t regr) { | |||
offset -= APT_SYNC_WIDTH + APT_SPC_WIDTH; | |||
for (int y = 0; y < nrow; y++) { | |||
for (int x = 0; x < width + APT_SYNC_WIDTH + APT_SPC_WIDTH + APT_TELE_WIDTH; x++) { | |||
float pv = linear_calc(prow[y][x + offset], regr); | |||
prow[y][x + offset] = CLIP(pv, 0, 255); | |||
} | |||
} | |||
} | |||
float teleNoise(float *wedges) { | |||
float pattern[9] = {31.07, 63.02, 94.96, 126.9, 158.86, 191.1, 228.62, 255.0, 0.0}; | |||
float noise = 0; | |||
for (int i = 0; i < 9; i++) noise += fabs(wedges[i] - pattern[i]); | |||
return noise; | |||
} | |||
// Get telemetry data for thermal calibration | |||
apt_channel_t apt_calibrate(float **prow, int nrow, int offset, int width) { | |||
float teleline[APT_MAX_HEIGHT] = {0.0}; | |||
float wedge[16]; | |||
linear_t regr[APT_MAX_HEIGHT / APT_FRAME_LEN + 1]; | |||
int telestart, mtelestart = 0; | |||
int channel = -1; | |||
// The minimum rows required to decode a full frame | |||
if (nrow < APT_CALIBRATION_ROWS) { | |||
error_noexit("Telemetry decoding error, not enough rows"); | |||
return APT_CHANNEL_UNKNOWN; | |||
} | |||
// Calculate average of a row of telemetry | |||
for (int y = 0; y < nrow; y++) { | |||
for (int x = 3; x < 43; x++) teleline[y] += prow[y][x + offset + width]; | |||
teleline[y] /= 40.0; | |||
} | |||
/* Wedge 7 is white and 8 is black, this will have the largest | |||
* difference in brightness, this can be used to find the current | |||
* position within the telemetry. | |||
*/ | |||
float max = 0.0f; | |||
for (int n = nrow / 3 - 64; n < 2 * nrow / 3 - 64; n++) { | |||
float df; | |||
// (sum 4px below) - (sum 4px above) | |||
df = (float)((teleline[n - 4] + teleline[n - 3] + teleline[n - 2] + teleline[n - 1]) - | |||
(teleline[n + 0] + teleline[n + 1] + teleline[n + 2] + teleline[n + 3])); | |||
// Find the maximum difference | |||
if (df > max) { | |||
mtelestart = n; | |||
max = df; | |||
} | |||
} | |||
telestart = (mtelestart + 64) % APT_FRAME_LEN; | |||
// Make sure that theres at least one full frame in the image | |||
if (nrow < telestart + APT_FRAME_LEN) { | |||
error_noexit("Telemetry decoding error, not enough rows"); | |||
return APT_CHANNEL_UNKNOWN; | |||
} | |||
// Find the least noisy frame | |||
float minNoise = -1; | |||
int bestFrame = -1; | |||
for (int n = telestart, k = 0; n < nrow - APT_FRAME_LEN; n += APT_FRAME_LEN, k++) { | |||
int j; | |||
for (j = 0; j < 16; j++) { | |||
int i; | |||
wedge[j] = 0.0; | |||
for (i = 1; i < 7; i++) wedge[j] += teleline[n + j * 8 + i]; | |||
wedge[j] /= 6; | |||
} | |||
float noise = teleNoise(wedge); | |||
if (noise < minNoise || minNoise == -1) { | |||
minNoise = noise; | |||
bestFrame = k; | |||
// Compute & apply regression on the wedges | |||
regr[k] = compute_regression(wedge); | |||
for (int j = 0; j < 16; j++) tele[j] = linear_calc(wedge[j], regr[k]); | |||
/* Compare the channel ID wedge to the reference | |||
* wedges, the wedge with the closest match will | |||
* be the channel ID | |||
*/ | |||
float min = -1; | |||
for (int j = 0; j < 6; j++) { | |||
float df = (float)(tele[15] - tele[j]); | |||
df *= df; | |||
if (df < min || min == -1) { | |||
channel = j; | |||
min = df; | |||
} | |||
} | |||
// Find the brightness of the minute marker, I don't really know what for | |||
Cs = 0.0; | |||
int i, j = n; | |||
for (i = 0, j = n; j < n + APT_FRAME_LEN; j++) { | |||
float csline = 0.0; | |||
for (int l = 3; l < 43; l++) csline += prow[n][l + offset - APT_SPC_WIDTH]; | |||
csline /= 40.0; | |||
if (csline > 50.0) { | |||
Cs += csline; | |||
i++; | |||
} | |||
} | |||
Cs = linear_calc((Cs / i), regr[k]); | |||
} | |||
} | |||
if (bestFrame == -1) { | |||
error_noexit("Something has gone very wrong, please file a bug report"); | |||
return APT_CHANNEL_UNKNOWN; | |||
} | |||
calibrateImage(prow, nrow, offset, width, regr[bestFrame]); | |||
return (apt_channel_t)(channel + 1); | |||
} | |||
extern float quick_select(float arr[], int n); | |||
// Biased median denoise, pretyt ugly | |||
#define TRIG_LEVEL 40 | |||
void apt_denoise(float **prow, int nrow, int offset, int width) { | |||
for (int y = 2; y < nrow - 2; y++) { | |||
for (int x = offset + 1; x < offset + width - 1; x++) { | |||
if (prow[y][x + 1] - prow[y][x] > TRIG_LEVEL || prow[y][x - 1] - prow[y][x] > TRIG_LEVEL || | |||
prow[y + 1][x] - prow[y][x] > TRIG_LEVEL || prow[y - 1][x] - prow[y][x] > TRIG_LEVEL) { | |||
prow[y][x] = quick_select((float[]){prow[y + 2][x - 1], prow[y + 2][x], prow[y + 2][x + 1], prow[y + 1][x - 1], | |||
prow[y + 1][x], prow[y + 1][x + 1], prow[y - 1][x - 1], prow[y - 1][x], | |||
prow[y - 1][x + 1], prow[y - 2][x - 1], prow[y - 2][x], prow[y - 2][x + 1]}, | |||
12); | |||
} | |||
} | |||
} | |||
} | |||
#undef TRIG_LEVEL | |||
// Flips a channel, for northbound passes | |||
void apt_flipImage(apt_image_t *img, int width, int offset) { | |||
for (int y = 1; y < img->nrow; y++) { | |||
for (int x = 1; x < ceil(width / 2.0); x++) { | |||
// Flip top-left & bottom-right | |||
float buffer = img->prow[img->nrow - y][offset + x]; | |||
img->prow[img->nrow - y][offset + x] = img->prow[y][offset + (width - x)]; | |||
img->prow[y][offset + (width - x)] = buffer; | |||
} | |||
} | |||
} | |||
// Calculate crop to reomve noise from the start and end of an image | |||
int apt_cropNoise(apt_image_t *img) { | |||
#define NOISE_THRESH 180.0 | |||
// Average value of minute marker | |||
float spc_rows[APT_MAX_HEIGHT] = {0.0}; | |||
int startCrop = 0; | |||
int endCrop = img->nrow; | |||
for (int y = 0; y < img->nrow; y++) { | |||
for (int x = 0; x < APT_SPC_WIDTH; x++) { | |||
spc_rows[y] += img->prow[y][x + (APT_CHB_OFFSET - APT_SPC_WIDTH)]; | |||
} | |||
spc_rows[y] /= APT_SPC_WIDTH; | |||
// Skip minute markings | |||
if (spc_rows[y] < 10) { | |||
spc_rows[y] = spc_rows[y - 1]; | |||
} | |||
} | |||
// 3 row average | |||
for (int y = 0; y < img->nrow; y++) { | |||
spc_rows[y] = (spc_rows[y + 1] + spc_rows[y + 2] + spc_rows[y + 3]) / 3; | |||
// img.prow[y][0] = spc_rows[y]; | |||
} | |||
// Find ends | |||
for (int y = 0; y < img->nrow - 1; y++) { | |||
if (spc_rows[y] > NOISE_THRESH) { | |||
endCrop = y; | |||
} | |||
} | |||
for (int y = img->nrow; y > 0; y--) { | |||
if (spc_rows[y] > NOISE_THRESH) { | |||
startCrop = y; | |||
} | |||
} | |||
// printf("Crop rows: %i -> %i\n", startCrop, endCrop); | |||
// Remove the noisy rows at start | |||
for (int y = 0; y < img->nrow - startCrop; y++) { | |||
memmove(img->prow[y], img->prow[y + startCrop], sizeof(float) * APT_PROW_WIDTH); | |||
} | |||
// Ignore the noisy rows at the end | |||
img->nrow = (endCrop - startCrop); | |||
return startCrop; | |||
} | |||
// --- Visible and Temperature Calibration --- // | |||
#include "calibration.h" | |||
typedef struct { | |||
float Nbb; | |||
float Cs; | |||
float Cb; | |||
int ch; | |||
} tempparam_t; | |||
// IR channel temperature compensation | |||
tempparam_t tempcomp(float t[16], int ch, int satnum) { | |||
tempparam_t tpr; | |||
tpr.ch = ch - 4; | |||
const calibration_t calibration = get_calibration(satnum); | |||
const float Vc = calibration.rad[tpr.ch].vc; | |||
const float A = calibration.rad[tpr.ch].A; | |||
const float B = calibration.rad[tpr.ch].B; | |||
// Compute PRT temperature | |||
float T[4]; | |||
for (size_t n = 0; n < 4; n++) { | |||
T[n] = quadratic_calc(t[n + 9] * 4.0, calibration.prt[n]); | |||
} | |||
float Tbb = (T[0] + T[1] + T[2] + T[3]) / 4.0; // Blackbody temperature | |||
float Tbbstar = A + Tbb * B; // Effective blackbody temperature | |||
tpr.Nbb = C1 * pow(Vc, 3) / (expf(C2 * Vc / Tbbstar) - 1.0f); // Blackbody radiance | |||
tpr.Cs = 246.4 * 4.0; // FIXME | |||
tpr.Cb = t[14] * 4.0; | |||
return tpr; | |||
} | |||
// IR channel temperature calibration | |||
static float tempcal(float Ce, int satnum, tempparam_t tpr) { | |||
const calibration_t calibration = get_calibration(satnum); | |||
const float Ns = calibration.cor[tpr.ch].Ns; | |||
const float Vc = calibration.rad[tpr.ch].vc; | |||
const float A = calibration.rad[tpr.ch].A; | |||
const float B = calibration.rad[tpr.ch].B; | |||
float Nl = Ns + (tpr.Nbb - Ns) * (tpr.Cs - Ce * 4.0) / (tpr.Cs - tpr.Cb); // Linear radiance estimate | |||
float Nc = quadratic_calc(Nl, calibration.cor[tpr.ch].quadratic); // Non-linear correction | |||
float Ne = Nl + Nc; // Corrected radiance | |||
float Testar = C2 * Vc / logf(C1 * powf(Vc, 3) / Ne + 1.0); // Equivlent black body temperature | |||
float Te = (Testar - A) / B; // Temperature (kelvin) | |||
// Convert to celsius | |||
Te -= 273.15; | |||
// Rescale to 0-255 for -100°C to +60°C | |||
return (Te + 100.0) / 160.0 * 255.0; | |||
} | |||
// Temperature calibration wrapper | |||
void apt_calibrate_thermal(int satnum, apt_image_t *img, int offset, int width) { | |||
tempparam_t temp = tempcomp(tele, img->chB, satnum); | |||
for (int y = 0; y < img->nrow; y++) { | |||
for (int x = 0; x < width; x++) { | |||
img->prow[y][x + offset] = (float)tempcal(img->prow[y][x + offset], satnum, temp); | |||
} | |||
} | |||
} | |||
float calibrate_pixel(float value, int channel, calibration_t cal) { | |||
if (value > cal.visible[channel].cutoff) { | |||
return linear_calc(value * 4.0f, cal.visible[channel].high) * 255.0f / 100.0f; | |||
} else { | |||
return linear_calc(value * 4.0f, cal.visible[channel].low) * 255.0f / 100.0f; | |||
} | |||
} | |||
void apt_calibrate_visible(int satnum, apt_image_t *img, int offset, int width) { | |||
const calibration_t calibration = get_calibration(satnum); | |||
int channel = img->chA - 1; | |||
for (int y = 0; y < img->nrow; y++) { | |||
for (int x = 0; x < width; x++) { | |||
img->prow[y][x + offset] = clamp(calibrate_pixel(img->prow[y][x + offset], channel, calibration), 255.0f, 0.0f); | |||
} | |||
} | |||
} |
@@ -1,2 +0,0 @@ | |||
#include "apt.h" | |||
#include "common.h" |
@@ -1,56 +0,0 @@ | |||
/* | |||
* This Quickselect routine is based on the algorithm described in | |||
* "Numerical recipes in C", Second Edition, | |||
* Cambridge University Press, 1992, Section 8.5, ISBN 0-521-43108-5 | |||
* This code by Nicolas Devillard - 1998. Public domain. | |||
*/ | |||
#define ELEM_SWAP(a, b) { register float t = (a); (a) = (b); (b) = t; } | |||
float quick_select(float arr[], int n) { | |||
int low, median, high; | |||
int middle, ll, hh; | |||
low = 0; high = n-1; median = (low + high) / 2; | |||
for (;;) { | |||
if (high <= low) /* One element only */ | |||
return arr[median] ; | |||
if (high == low + 1) { /* Two elements only */ | |||
if (arr[low] > arr[high]) | |||
ELEM_SWAP(arr[low], arr[high]); | |||
return arr[median]; | |||
} | |||
/* Find median of low, middle and high items; swap into position low */ | |||
middle = (low + high) / 2; | |||
if (arr[middle] > arr[high]) ELEM_SWAP(arr[middle], arr[high]); | |||
if (arr[low] > arr[high]) ELEM_SWAP(arr[low], arr[high]); | |||
if (arr[middle] > arr[low]) ELEM_SWAP(arr[middle], arr[low]); | |||
/* Swap low item (now in position middle) into position (low+1) */ | |||
ELEM_SWAP(arr[middle], arr[low+1]); | |||
/* Nibble from each end towards middle, swapping items when stuck */ | |||
ll = low + 1; | |||
hh = high; | |||
for (;;) { | |||
do ll++; while (arr[low] > arr[ll]); | |||
do hh--; while (arr[hh] > arr[low]); | |||
if (hh < ll) | |||
break; | |||
ELEM_SWAP(arr[ll], arr[hh]); | |||
} | |||
/* Swap middle item (in position low) back into correct position */ | |||
ELEM_SWAP(arr[low], arr[hh]); | |||
/* Re-set active partition */ | |||
if (hh <= median) | |||
low = ll; | |||
if (hh >= median) | |||
high = hh - 1; | |||
} | |||
} | |||
#undef ELEM_SWAP |
@@ -1,316 +0,0 @@ | |||
/* | |||
* This file is part of Aptdec. | |||
* Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 | |||
* | |||
* Aptdec 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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#ifndef _MSC_VER | |||
#include <libgen.h> | |||
#else | |||
#include <windows.h> | |||
#endif | |||
#include <errno.h> | |||
#include <math.h> | |||
#include <sndfile.h> | |||
#include <time.h> | |||
#include "apt.h" | |||
#include "argparse/argparse.h" | |||
#include "color.h" | |||
#include "common.h" | |||
#include "image.h" | |||
#include "pngio.h" | |||
#include "util.h" | |||
// Audio file | |||
static SNDFILE *audioFile; | |||
// Number of channels in audio file | |||
int channels = 1; | |||
// Function declarations | |||
static int initsnd(char *filename); | |||
int getsamples(void *context, float *samples, int nb); | |||
static int processAudio(char *filename, options_t *opts); | |||
#ifdef _MSC_VER | |||
// Functions not supported by MSVC | |||
static char *dirname(char *path) { | |||
static char dir[MAX_PATH]; | |||
_splitpath(path, NULL, dir, NULL, NULL); | |||
return dir; | |||
} | |||
static char *basename(char *path) { | |||
static char base[MAX_PATH]; | |||
_splitpath(path, NULL, NULL, base, NULL); | |||
return base; | |||
} | |||
#endif | |||
int main(int argc, const char **argv) { | |||
options_t opts = { | |||
.type = "r", .effects = "", .satnum = 19, .path = ".", .realtime = 0, .filename = "", .palette = "", .gamma = 1.0}; | |||
static const char *const usages[] = { | |||
"aptdec [options] [[--] sources]", | |||
"aptdec [sources]", | |||
NULL, | |||
}; | |||
struct argparse_option options[] = { | |||
OPT_HELP(), | |||
OPT_GROUP("Image options"), | |||
OPT_STRING('i', "image", &opts.type, "set output image type (see the README for a list)", NULL, 0, 0), | |||
OPT_STRING('e', "effect", &opts.effects, "add an effect (see the README for a list)", NULL, 0, 0), | |||
OPT_FLOAT('g', "gamma", &opts.gamma, "gamma adjustment (1.0 = off)", NULL, 0, 0), | |||
OPT_GROUP("Satellite options"), | |||
OPT_INTEGER('s', "satellite", &opts.satnum, "satellite ID, must be between 15 and 19", NULL, 0, 0), | |||
OPT_GROUP("Paths"), | |||
OPT_STRING('p', "palette", &opts.palette, "path to a palette", NULL, 0, 0), | |||
OPT_STRING('o', "filename", &opts.filename, "filename of the output image", NULL, 0, 0), | |||
OPT_STRING('d', "output", &opts.path, "output directory (must exist first)", NULL, 0, 0), | |||
OPT_GROUP("Misc"), | |||
OPT_BOOLEAN('r', "realtime", &opts.realtime, "decode in realtime", NULL, 0, 0), | |||
OPT_END(), | |||
}; | |||
struct argparse argparse; | |||
argparse_init(&argparse, options, usages, 0); | |||
argparse_describe( | |||
&argparse, "\nA lightweight FOSS NOAA APT satellite imagery decoder.", | |||
"\nSee `README.md` for a full description of command line arguments and `LICENSE` for licensing conditions."); | |||
argc = argparse_parse(&argparse, argc, argv); | |||
if (argc == 0) { | |||
argparse_usage(&argparse); | |||
} | |||
// Actually decode the files | |||
for (int i = 0; i < argc; i++) { | |||
char *filename = strdup(argv[i]); | |||
processAudio(filename, &opts); | |||
} | |||
return 0; | |||
} | |||
static int processAudio(char *filename, options_t *opts) { | |||
// Image info struct | |||
apt_image_t img; | |||
// Mapping between wedge value and channel ID | |||
static struct { | |||
char *id[7]; | |||
char *name[7]; | |||
} ch = {{"?", "1", "2", "3A", "4", "5", "3B"}, | |||
{"unknown", "visble", "near-infrared", "near-infrared", "thermal-infrared", "thermal-infrared", "mid-infrared"}}; | |||
// Buffer for image channel | |||
char desc[60]; | |||
// Parse file path | |||
char path[256], extension[32]; | |||
strcpy(path, filename); | |||
strcpy(path, dirname(path)); | |||
sscanf(basename(filename), "%255[^.].%31s", img.name, extension); | |||
if (opts->realtime) { | |||
// Set output filename to current time when in realtime mode | |||
time_t t; | |||
time(&t); | |||
strncpy(img.name, ctime(&t), 24); | |||
// Init a row writer | |||
initWriter(opts, &img, APT_IMG_WIDTH, APT_MAX_HEIGHT, "Unprocessed realtime image", "r"); | |||
} | |||
if (strcmp(extension, "png") == 0) { | |||
// Read PNG into image buffer | |||
printf("Reading %s\n", filename); | |||
if (readRawImage(filename, img.prow, &img.nrow) == 0) { | |||
exit(EPERM); | |||
} | |||
} else { | |||
// Attempt to open the audio file | |||
if (initsnd(filename) == 0) exit(EPERM); | |||
// Build image | |||
// TODO: multithreading, would require some sort of input buffer | |||
for (img.nrow = 0; img.nrow < APT_MAX_HEIGHT; img.nrow++) { | |||
// Allocate memory for this row | |||
img.prow[img.nrow] = (float *)malloc(sizeof(float) * APT_PROW_WIDTH); | |||
// Write into memory and break the loop when there are no more samples to read | |||
if (apt_getpixelrow(img.prow[img.nrow], img.nrow, &img.zenith, (img.nrow == 0), getsamples, NULL) == 0) break; | |||
if (opts->realtime) pushRow(img.prow[img.nrow], APT_IMG_WIDTH); | |||
fprintf(stderr, "Row: %d\r", img.nrow); | |||
fflush(stderr); | |||
} | |||
// Close stream | |||
sf_close(audioFile); | |||
} | |||
if (opts->realtime) closeWriter(); | |||
printf("Total rows: %d\n", img.nrow); | |||
// Calibrate | |||
img.chA = apt_calibrate(img.prow, img.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); | |||
img.chB = apt_calibrate(img.prow, img.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); | |||
printf("Channel A: %s (%s)\n", ch.id[img.chA], ch.name[img.chA]); | |||
printf("Channel B: %s (%s)\n", ch.id[img.chB], ch.name[img.chB]); | |||
// Crop noise from start and end of image | |||
if (CONTAINS(opts->effects, Crop_Noise)) { | |||
img.zenith -= apt_cropNoise(&img); | |||
} | |||
// Denoise | |||
if (CONTAINS(opts->effects, Denoise)) { | |||
apt_denoise(img.prow, img.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); | |||
apt_denoise(img.prow, img.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); | |||
} | |||
// Flip, for northbound passes | |||
if (CONTAINS(opts->effects, Flip_Image)) { | |||
apt_flipImage(&img, APT_CH_WIDTH, APT_CHA_OFFSET); | |||
apt_flipImage(&img, APT_CH_WIDTH, APT_CHB_OFFSET); | |||
} | |||
// Temperature | |||
if (CONTAINS(opts->type, Temperature) && img.chB >= 4) { | |||
// Create another buffer as to not modify the orignal | |||
apt_image_t tmpimg = img; | |||
for (int i = 0; i < img.nrow; i++) { | |||
tmpimg.prow[i] = (float *)malloc(sizeof(float) * APT_PROW_WIDTH); | |||
memcpy(tmpimg.prow[i], img.prow[i], sizeof(float) * APT_PROW_WIDTH); | |||
} | |||
// Perform temperature calibration | |||
apt_calibrate_thermal(opts->satnum, &tmpimg, APT_CHB_OFFSET, APT_CH_WIDTH); | |||
ImageOut(opts, &tmpimg, APT_CHB_OFFSET, APT_CH_WIDTH, "Temperature", Temperature, (char *)apt_TempPalette); | |||
} | |||
// Visible | |||
if (CONTAINS(opts->type, Visible) && img.chA <= 2) { | |||
// Create another buffer as to not modify the orignal | |||
apt_image_t tmpimg = img; | |||
for (int i = 0; i < img.nrow; i++) { | |||
tmpimg.prow[i] = (float *)malloc(sizeof(float) * APT_PROW_WIDTH); | |||
memcpy(tmpimg.prow[i], img.prow[i], sizeof(float) * APT_PROW_WIDTH); | |||
} | |||
// Perform visible calibration | |||
apt_calibrate_visible(opts->satnum, &tmpimg, APT_CHA_OFFSET, APT_CH_WIDTH); | |||
ImageOut(opts, &tmpimg, APT_CHA_OFFSET, APT_CH_WIDTH, "Visible", Visible, NULL); | |||
} | |||
// Linear equalise | |||
if (CONTAINS(opts->effects, Linear_Equalise)) { | |||
apt_linearEnhance(img.prow, img.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); | |||
apt_linearEnhance(img.prow, img.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); | |||
} | |||
// Histogram equalise | |||
if (CONTAINS(opts->effects, Histogram_Equalise)) { | |||
apt_histogramEqualise(img.prow, img.nrow, APT_CHA_OFFSET, APT_CH_WIDTH); | |||
apt_histogramEqualise(img.prow, img.nrow, APT_CHB_OFFSET, APT_CH_WIDTH); | |||
} | |||
// Raw image | |||
if (CONTAINS(opts->type, Raw_Image)) { | |||
sprintf(desc, "%s (%s) & %s (%s)", ch.id[img.chA], ch.name[img.chA], ch.id[img.chB], ch.name[img.chB]); | |||
ImageOut(opts, &img, 0, APT_IMG_WIDTH, desc, Raw_Image, NULL); | |||
} | |||
// Palette image | |||
if (CONTAINS(opts->type, Palleted)) { | |||
img.palette = opts->palette; | |||
strcpy(desc, "Palette composite"); | |||
ImageOut(opts, &img, APT_CHA_OFFSET, APT_CH_WIDTH, desc, Palleted, NULL); | |||
} | |||
// Channel A | |||
if (CONTAINS(opts->type, Channel_A)) { | |||
sprintf(desc, "%s (%s)", ch.id[img.chA], ch.name[img.chA]); | |||
ImageOut(opts, &img, APT_CHA_OFFSET, APT_CH_WIDTH, desc, Channel_A, NULL); | |||
} | |||
// Channel B | |||
if (CONTAINS(opts->type, Channel_B)) { | |||
sprintf(desc, "%s (%s)", ch.id[img.chB], ch.name[img.chB]); | |||
ImageOut(opts, &img, APT_CHB_OFFSET, APT_CH_WIDTH, desc, Channel_B, NULL); | |||
} | |||
return 1; | |||
} | |||
float *samplebuf; | |||
static int initsnd(char *filename) { | |||
SF_INFO infwav; | |||
int res; | |||
// Open audio file | |||
infwav.format = 0; | |||
audioFile = sf_open(filename, SFM_READ, &infwav); | |||
if (audioFile == NULL) { | |||
error_noexit("Could not file"); | |||
return 0; | |||
} | |||
res = apt_init(infwav.samplerate); | |||
printf("Input file: %s\n", filename); | |||
if (res < 0) { | |||
error_noexit("Input sample rate too low"); | |||
return 0; | |||
} else if (res > 0) { | |||
error_noexit("Input sample rate too high"); | |||
return 0; | |||
} | |||
printf("Input sample rate: %d\n", infwav.samplerate); | |||
channels = infwav.channels; | |||
samplebuf = (float *)malloc(sizeof(float) * 32768 * channels); | |||
return 1; | |||
} | |||
// Read samples from the audio file | |||
int getsamples(void *context, float *samples, int nb) { | |||
(void)context; | |||
if (channels == 1) { | |||
return (int)sf_read_float(audioFile, samples, nb); | |||
} else if (channels == 2) { | |||
// Stereo channels are interleaved | |||
int samplesRead = (int)sf_read_float(audioFile, samplebuf, nb * channels); | |||
for (int i = 0; i < nb; i++) { | |||
samples[i] = samplebuf[i * channels]; | |||
} | |||
return samplesRead / channels; | |||
} else { | |||
printf("Only mono and stereo input files are supported\n"); | |||
exit(1); | |||
} | |||
} |
@@ -1,411 +0,0 @@ | |||
/* | |||
* This file is part of Aptdec. | |||
* Copyright (c) 2004-2009 Thierry Leconte (F4DWV), Xerbo (xerbo@protonmail.com) 2019-2022 | |||
* | |||
* Aptdec 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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
#include "pngio.h" | |||
#include <math.h> | |||
#include <png.h> | |||
#include <stdint.h> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include "util.h" | |||
int readRawImage(char *filename, float **prow, int *nrow) { | |||
FILE *fp = fopen(filename, "rb"); | |||
printf("%s", filename); | |||
if (!fp) { | |||
error_noexit("Cannot open image"); | |||
return 0; | |||
} | |||
// Create reader | |||
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); | |||
if (!png) { | |||
fclose(fp); | |||
return 0; | |||
} | |||
png_infop info = png_create_info_struct(png); | |||
if (!info) { | |||
fclose(fp); | |||
return 0; | |||
} | |||
png_init_io(png, fp); | |||
// Read info from header | |||
png_read_info(png, info); | |||
int width = png_get_image_width(png, info); | |||
int height = png_get_image_height(png, info); | |||
png_byte color_type = png_get_color_type(png, info); | |||
png_byte bit_depth = png_get_bit_depth(png, info); | |||
// Check the image | |||
if (width != APT_IMG_WIDTH) { | |||
error_noexit("Raw image must be 2080px wide"); | |||
return 0; | |||
} else if (bit_depth != 8) { | |||
error_noexit("Raw image must have 8 bit color"); | |||
return 0; | |||
} else if (color_type != PNG_COLOR_TYPE_GRAY) { | |||
error_noexit("Raw image must be grayscale"); | |||
return 0; | |||
} | |||
// Create row buffers | |||
png_bytep *PNGrows = NULL; | |||
PNGrows = (png_bytep *)malloc(sizeof(png_bytep) * height); | |||
for (int y = 0; y < height; y++) PNGrows[y] = (png_byte *)malloc(png_get_rowbytes(png, info)); | |||
// Read image | |||
png_read_image(png, PNGrows); | |||
// Tidy up | |||
fclose(fp); | |||
png_destroy_read_struct(&png, &info, NULL); | |||
// Put into prow | |||
*nrow = height; | |||
for (int y = 0; y < height; y++) { | |||
prow[y] = (float *)malloc(sizeof(float) * width); | |||
for (int x = 0; x < width; x++) prow[y][x] = (float)PNGrows[y][x]; | |||
} | |||
return 1; | |||
} | |||
int readPalette(char *filename, apt_rgb_t **pixels) { | |||
FILE *fp = fopen(filename, "rb"); | |||
if (!fp) { | |||
error_noexit("Cannot open palette"); | |||
return 0; | |||
} | |||
// Create reader | |||
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); | |||
if (!png) { | |||
fclose(fp); | |||
return 0; | |||
} | |||
png_infop info = png_create_info_struct(png); | |||
if (!info) { | |||
fclose(fp); | |||
return 0; | |||
} | |||
png_init_io(png, fp); | |||
// Read info from header | |||
png_read_info(png, info); | |||
int width = png_get_image_width(png, info); | |||
int height = png_get_image_height(png, info); | |||
png_byte color_type = png_get_color_type(png, info); | |||
png_byte bit_depth = png_get_bit_depth(png, info); | |||
// Check the image | |||
if (width != 256 && height != 256) { | |||
error_noexit("Palette must be 256x256"); | |||
return 0; | |||
} else if (bit_depth != 8) { | |||
error_noexit("Palette must be 8 bit color"); | |||
return 0; | |||
} else if (color_type != PNG_COLOR_TYPE_RGB) { | |||
error_noexit("Palette must be RGB"); | |||
return 0; | |||
} | |||
// Create row buffers | |||
png_bytep *PNGrows = NULL; | |||
PNGrows = (png_bytep *)malloc(sizeof(png_bytep) * height); | |||
for (int y = 0; y < height; y++) PNGrows[y] = (png_byte *)malloc(png_get_rowbytes(png, info)); | |||
// Read image | |||
png_read_image(png, PNGrows); | |||
// Tidy up | |||
fclose(fp); | |||
png_destroy_read_struct(&png, &info, NULL); | |||
// Put into crow | |||
for (int y = 0; y < height; y++) { | |||
pixels[y] = (apt_rgb_t *)malloc(sizeof(apt_rgb_t) * width); | |||
for (int x = 0; x < width; x++) | |||
pixels[y][x] = (apt_rgb_t){PNGrows[y][x * 3], PNGrows[y][x * 3 + 1], PNGrows[y][x * 3 + 2]}; | |||
} | |||
return 1; | |||
} | |||
void prow2crow(float **prow, int nrow, char *palette, apt_rgb_t **crow) { | |||
for (int y = 0; y < nrow; y++) { | |||
crow[y] = (apt_rgb_t *)malloc(sizeof(apt_rgb_t) * APT_IMG_WIDTH); | |||
for (int x = 0; x < APT_IMG_WIDTH; x++) { | |||
if (palette == NULL) | |||
crow[y][x].r = crow[y][x].g = crow[y][x].b = prow[y][x]; | |||
else | |||
crow[y][x] = apt_applyPalette(palette, prow[y][x]); | |||
} | |||
} | |||
} | |||
int applyUserPalette(float **prow, int nrow, char *filename, apt_rgb_t **crow) { | |||
apt_rgb_t *pal_row[256]; | |||
if (!readPalette(filename, pal_row)) { | |||
error_noexit("Could not read palette"); | |||
return 0; | |||
} | |||
for (int y = 0; y < nrow; y++) { | |||
for (int x = 0; x < APT_CH_WIDTH; x++) { | |||
int cha = CLIP(prow[y][x + APT_CHA_OFFSET], 0, 255); | |||
int chb = CLIP(prow[y][x + APT_CHB_OFFSET], 0, 255); | |||
crow[y][x + APT_CHA_OFFSET] = pal_row[chb][cha]; | |||
} | |||
} | |||
return 1; | |||
} | |||
int ImageOut(options_t *opts, apt_image_t *img, int offset, int width, char *desc, char chid, char *palette) { | |||
char outName[512]; | |||
if (opts->filename == NULL || opts->filename[0] == '\0') { | |||
sprintf(outName, "%s/%s-%c.png", opts->path, img->name, chid); | |||
} else { | |||
sprintf(outName, "%s/%s", opts->path, opts->filename); | |||
} | |||
png_text meta[] = {{PNG_TEXT_COMPRESSION_NONE, "Software", VERSION, sizeof(VERSION)}, | |||
{PNG_TEXT_COMPRESSION_NONE, "Channel", desc, sizeof(desc)}, | |||
{PNG_TEXT_COMPRESSION_NONE, "Description", "NOAA satellite image", 20}}; | |||
// Parse image type | |||
int greyscale = 1; | |||
switch (chid) { | |||
case Palleted: | |||
greyscale = 0; | |||
break; | |||
case Temperature: | |||
greyscale = 0; | |||
break; | |||
case Raw_Image: | |||
break; | |||
case Channel_A: | |||
break; | |||
case Channel_B: | |||
break; | |||
} | |||
// Parse effects | |||
int crop_telemetry = 0; | |||
for (unsigned long int i = 0; i < strlen(opts->effects); i++) { | |||
switch (opts->effects[i]) { | |||
case Crop_Telemetry: | |||
if (width == 2080) { | |||
width -= APT_TOTAL_TELE; | |||
offset += APT_SYNC_WIDTH + APT_SPC_WIDTH; | |||
crop_telemetry = 1; | |||
} | |||
break; | |||
case Precipitation_Overlay: | |||
greyscale = 0; | |||
break; | |||
case Flip_Image: | |||
break; | |||
case Denoise: | |||
break; | |||
case Histogram_Equalise: | |||
break; | |||
case Linear_Equalise: | |||
break; | |||
case Crop_Noise: | |||
break; | |||
default: { | |||
char text[100]; | |||
sprintf(text, "Unrecognised effect, \"%c\"", opts->effects[i]); | |||
warning(text); | |||
break; | |||
} | |||
} | |||
} | |||
FILE *pngfile; | |||
// Create writer | |||
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); | |||
if (!png_ptr) { | |||
png_destroy_write_struct(&png_ptr, (png_infopp)NULL); | |||
error_noexit("Could not create a PNG writer"); | |||
return 0; | |||
} | |||
png_infop info_ptr = png_create_info_struct(png_ptr); | |||
if (!info_ptr) { | |||
png_destroy_write_struct(&png_ptr, (png_infopp)NULL); | |||
error_noexit("Could not create a PNG writer"); | |||
return 0; | |||
} | |||
if (greyscale) { | |||
// Greyscale image | |||
png_set_IHDR(png_ptr, info_ptr, width, img->nrow, 8, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE, | |||
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); | |||
} else { | |||
// 8 bit RGB image | |||
png_set_IHDR(png_ptr, info_ptr, width, img->nrow, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, | |||
PNG_FILTER_TYPE_DEFAULT); | |||
} | |||
png_set_text(png_ptr, info_ptr, meta, 3); | |||
png_set_pHYs(png_ptr, info_ptr, 3636, 3636, PNG_RESOLUTION_METER); | |||
// Init I/O | |||
pngfile = fopen(outName, "wb"); | |||
if (!pngfile) { | |||
error_noexit("Could not open PNG for writing"); | |||
return 1; | |||
} | |||
png_init_io(png_ptr, pngfile); | |||
png_write_info(png_ptr, info_ptr); | |||
// Move prow into crow, crow ~ color rows, if required | |||
apt_rgb_t *crow[APT_MAX_HEIGHT]; | |||
if (!greyscale) { | |||
prow2crow(img->prow, img->nrow, palette, crow); | |||
} | |||
// Apply a user provided color palette | |||
if (chid == Palleted) { | |||
applyUserPalette(img->prow, img->nrow, opts->palette, crow); | |||
} | |||
// Precipitation overlay | |||
if (CONTAINS(opts->effects, Precipitation_Overlay)) { | |||
for (int y = 0; y < img->nrow; y++) { | |||
for (int x = 0; x < APT_CH_WIDTH; x++) { | |||
if (img->prow[y][x + APT_CHB_OFFSET] >= 198) | |||
crow[y][x + APT_CHB_OFFSET] = crow[y][x + APT_CHA_OFFSET] = | |||
apt_applyPalette(apt_PrecipPalette, img->prow[y][x + APT_CHB_OFFSET] - 198); | |||
} | |||
} | |||
} | |||
printf("Writing %s", outName); | |||
// Float power macro (for gamma adjustment) | |||
#define POWF(a, b) (b == 1.0 ? a : exp(b * log(a))) | |||
float a = POWF(255, opts->gamma) / 255; | |||
// Build image | |||
for (int y = 0; y < img->nrow; y++) { | |||
png_color pix[APT_IMG_WIDTH]; // Color | |||
png_byte mpix[APT_IMG_WIDTH]; // Mono | |||
int skip = 0; | |||
for (int x = 0; x < width; x++) { | |||
if (crop_telemetry && x == APT_CH_WIDTH) skip += APT_TELE_WIDTH + APT_SYNC_WIDTH + APT_SPC_WIDTH; | |||
if (greyscale) { | |||
mpix[x] = POWF(img->prow[y][x + skip + offset], opts->gamma) / a; | |||
} else { | |||
pix[x] = (png_color){POWF(crow[y][x + skip + offset].r, opts->gamma) / a, | |||
POWF(crow[y][x + skip + offset].g, opts->gamma) / a, | |||
POWF(crow[y][x + skip + offset].b, opts->gamma) / a}; | |||
} | |||
} | |||
if (greyscale) { | |||
png_write_row(png_ptr, (png_bytep)mpix); | |||
} else { | |||
png_write_row(png_ptr, (png_bytep)pix); | |||
} | |||
} | |||
// Tidy up | |||
png_write_end(png_ptr, info_ptr); | |||
fclose(pngfile); | |||
printf("\nDone\n"); | |||
png_destroy_write_struct(&png_ptr, &info_ptr); | |||
return 1; | |||
} | |||
// TODO: clean up everthing below this comment | |||
png_structp rt_png_ptr; | |||
png_infop rt_info_ptr; | |||
FILE *rt_pngfile; | |||
int initWriter(options_t *opts, apt_image_t *img, int width, int height, char *desc, char *chid) { | |||
char outName[384]; | |||
sprintf(outName, "%s/%s-%s.png", opts->path, img->name, chid); | |||
png_text meta[] = {{PNG_TEXT_COMPRESSION_NONE, "Software", VERSION, sizeof(VERSION)}, | |||
{PNG_TEXT_COMPRESSION_NONE, "Channel", desc, sizeof(desc)}, | |||
{PNG_TEXT_COMPRESSION_NONE, "Description", "NOAA satellite image", 20}}; | |||
// Create writer | |||
rt_png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); | |||
if (!rt_png_ptr) { | |||
png_destroy_write_struct(&rt_png_ptr, (png_infopp)NULL); | |||
error_noexit("Could not create a PNG writer"); | |||
return 0; | |||
} | |||
rt_info_ptr = png_create_info_struct(rt_png_ptr); | |||
if (!rt_info_ptr) { | |||
png_destroy_write_struct(&rt_png_ptr, (png_infopp)NULL); | |||
error_noexit("Could not create a PNG writer"); | |||
return 0; | |||
} | |||
// Greyscale image | |||
png_set_IHDR(rt_png_ptr, rt_info_ptr, width, height, 8, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, | |||
PNG_FILTER_TYPE_DEFAULT); | |||
png_set_text(rt_png_ptr, rt_info_ptr, meta, 3); | |||
// Channel = 25cm wide | |||
png_set_pHYs(rt_png_ptr, rt_info_ptr, 3636, 3636, PNG_RESOLUTION_METER); | |||
// Init I/O | |||
rt_pngfile = fopen(outName, "wb"); | |||
if (!rt_pngfile) { | |||
error_noexit("Could not open PNG for writing"); | |||
return 0; | |||
} | |||
png_init_io(rt_png_ptr, rt_pngfile); | |||
png_write_info(rt_png_ptr, rt_info_ptr); | |||
// Turn off compression | |||
png_set_compression_level(rt_png_ptr, 0); | |||
return 1; | |||
} | |||
void pushRow(float *row, int width) { | |||
png_byte pix[APT_IMG_WIDTH]; | |||
for (int i = 0; i < width; i++) pix[i] = row[i]; | |||
png_write_row(rt_png_ptr, (png_bytep)pix); | |||
} | |||
void closeWriter() { | |||
png_write_end(rt_png_ptr, rt_info_ptr); | |||
fclose(rt_pngfile); | |||
png_destroy_write_struct(&rt_png_ptr, &rt_info_ptr); | |||
} |
@@ -1,11 +0,0 @@ | |||
#include "apt.h" | |||
#include "common.h" | |||
int readRawImage(char *filename, float **prow, int *nrow); | |||
int readPalette(char *filename, apt_rgb_t **pixels); | |||
void prow2crow(float **prow, int nrow, char *palette, apt_rgb_t **crow); | |||
int applyUserPalette(float **prow, int nrow, char *filename, apt_rgb_t **crow); | |||
int ImageOut(options_t *opts, apt_image_t *img, int offset, int width, char *desc, char chid, char *palette); | |||
int initWriter(options_t *opts, apt_image_t *img, int width, int height, char *desc, char *chid); | |||
void pushRow(float *row, int width); | |||
void closeWriter(); |
@@ -1,80 +0,0 @@ | |||
/* | |||
* aptdec - A lightweight FOSS (NOAA) APT decoder | |||
* Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2022 Xerbo (xerbo@protonmail.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. | |||
* | |||
* 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 <https://www.gnu.org/licenses/>. | |||
*/ | |||
static const float hilbert_filter[] = {0.0205361, 0.0219524, 0.0235785, 0.0254648, 0.0276791, 0.0303152, 0.0335063, | |||
0.0374482, 0.0424413, 0.0489708, 0.0578745, 0.0707355, 0.0909457, 0.127324, | |||
0.212207, 0.63662, -0.63662, -0.212207, -0.127324, -0.0909457, -0.0707355, | |||
-0.0578745, -0.0489708, -0.0424413, -0.0374482, -0.0335063, -0.0303152, -0.0276791, | |||
-0.0254648, -0.0235785, -0.0219524, -0.0205361}; | |||
#define HILBERT_FILTER_SIZE (sizeof(hilbert_filter) / sizeof(hilbert_filter[0])) | |||
static const float low_pass[] = { | |||
-3.37279e-04, -8.80292e-06, -3.96418e-04, -1.78544e-04, -5.27511e-04, -3.75376e-04, -6.95337e-04, -5.93148e-04, -8.79730e-04, | |||
-8.15327e-04, -1.05669e-03, -1.01377e-03, -1.19836e-03, -1.15443e-03, -1.26937e-03, -1.20955e-03, -1.23904e-03, -1.15302e-03, | |||
-1.08660e-03, -9.64235e-04, -8.02450e-04, -6.46202e-04, -3.95376e-04, -2.18096e-04, 1.11906e-04, 2.89567e-04, 6.67167e-04, | |||
8.19039e-04, 1.21725e-03, 1.30556e-03, 1.69365e-03, 1.68588e-03, 2.03277e-03, 1.90159e-03, 2.18455e-03, 1.90833e-03, | |||
2.12100e-03, 1.69052e-03, 1.77484e-03, 1.42542e-03, 1.18292e-03, 8.66979e-04, 5.54161e-04, 2.15793e-04, -1.11623e-04, | |||
-4.35173e-04, -7.27194e-04, -9.91551e-04, -1.20407e-03, -1.37032e-03, -1.46991e-03, -1.51120e-03, -1.48008e-03, -1.39047e-03, | |||
-1.23115e-03, -1.02128e-03, -7.60099e-04, -4.68008e-04, -1.46339e-04, 1.80867e-04, 5.11244e-04, 8.19243e-04, 1.09739e-03, | |||
1.32668e-03, 1.50632e-03, 1.61522e-03, 1.66246e-03, 1.62390e-03, 1.52430e-03, 1.34273e-03, 1.10736e-03, 8.10335e-04, | |||
4.76814e-04, 1.13622e-04, -2.64150e-04, -6.26595e-04, -9.95436e-04, -1.27846e-03, -1.54080e-03, -1.74292e-03, -1.86141e-03, | |||
-1.89318e-03, -1.83969e-03, -1.69770e-03, -1.47938e-03, -1.18696e-03, -8.37003e-04, -4.39507e-04, -1.56907e-05, 4.19904e-04, | |||
8.43172e-04, 1.23827e-03, 1.58411e-03, 1.86382e-03, 2.06312e-03, 2.17177e-03, 2.18121e-03, 2.08906e-03, 1.89772e-03, | |||
1.61153e-03, 1.24507e-03, 8.13976e-04, 3.29944e-04, -1.74591e-04, -6.83619e-04, -1.17826e-03, -1.61659e-03, -2.00403e-03, | |||
-2.29070e-03, -2.49179e-03, -2.56546e-03, -2.53448e-03, -2.37032e-03, -2.10060e-03, -1.72140e-03, -1.24542e-03, -7.15425e-04, | |||
-1.24964e-04, 4.83736e-04, 1.08328e-03, 1.64530e-03, 2.14503e-03, 2.55400e-03, 2.85589e-03, 3.02785e-03, 3.06271e-03, | |||
2.95067e-03, 2.69770e-03, 2.30599e-03, 1.79763e-03, 1.18587e-03, 5.04003e-04, -2.23591e-04, -9.57591e-04, -1.66939e-03, | |||
-2.31717e-03, -2.87636e-03, -3.31209e-03, -3.60506e-03, -3.73609e-03, -3.69208e-03, -3.44913e-03, -3.06572e-03, -2.50229e-03, | |||
-1.80630e-03, -1.00532e-03, -1.22305e-04, 7.83910e-04, 1.69402e-03, 2.53826e-03, 3.30312e-03, 3.91841e-03, 4.38017e-03, | |||
4.63546e-03, 4.68091e-03, 4.50037e-03, 4.09614e-03, 3.47811e-03, 2.67306e-03, 1.70418e-03, 6.20542e-04, -5.36994e-04, | |||
-1.70981e-03, -2.84712e-03, -3.88827e-03, -4.78659e-03, -5.48593e-03, -5.95049e-03, -6.14483e-03, -6.05118e-03, -5.65829e-03, | |||
-4.97525e-03, -4.01796e-03, -2.82224e-03, -1.43003e-03, 1.00410e-04, 1.71169e-03, 3.31983e-03, 4.87796e-03, 6.23237e-03, | |||
7.31013e-03, 8.20642e-03, 8.67374e-03, 8.77681e-03, 8.43444e-03, 7.66794e-03, 6.46827e-03, 4.87294e-03, 2.92923e-03, | |||
6.98913e-04, -1.72126e-03, -4.24785e-03, -6.75380e-03, -9.13309e-03, -1.12532e-02, -1.30038e-02, -1.42633e-02, -1.49338e-02, | |||
-1.49145e-02, -1.41484e-02, -1.25761e-02, -1.01870e-02, -6.97432e-03, -2.97910e-03, 1.75386e-03, 7.11899e-03, 1.30225e-02, | |||
1.93173e-02, 2.58685e-02, 3.24965e-02, 3.90469e-02, 4.53316e-02, 5.11931e-02, 5.64604e-02, 6.09924e-02, 6.46584e-02, | |||
6.73547e-02, 6.90049e-02, 6.97096e-02, 6.90049e-02, 6.73547e-02, 6.46584e-02, 6.09924e-02, 5.64604e-02, 5.11931e-02, | |||
4.53316e-02, 3.90469e-02, 3.24965e-02, 2.58685e-02, 1.93173e-02, 1.30225e-02, 7.11899e-03, 1.75386e-03, -2.97910e-03, | |||
-6.97432e-03, -1.01870e-02, -1.25761e-02, -1.41484e-02, -1.49145e-02, -1.49338e-02, -1.42633e-02, -1.30038e-02, -1.12532e-02, | |||
-9.13309e-03, -6.75380e-03, -4.24785e-03, -1.72126e-03, 6.98913e-04, 2.92923e-03, 4.87294e-03, 6.46827e-03, 7.66794e-03, | |||
8.43444e-03, 8.77681e-03, 8.67374e-03, 8.20642e-03, 7.31013e-03, 6.23237e-03, 4.87796e-03, 3.31983e-03, 1.71169e-03, | |||
1.00410e-04, -1.43003e-03, -2.82224e-03, -4.01796e-03, -4.97525e-03, -5.65829e-03, -6.05118e-03, -6.14483e-03, -5.95049e-03, | |||
-5.48593e-03, -4.78659e-03, -3.88827e-03, -2.84712e-03, -1.70981e-03, -5.36994e-04, 6.20542e-04, 1.70418e-03, 2.67306e-03, | |||
3.47811e-03, 4.09614e-03, 4.50037e-03, 4.68091e-03, 4.63546e-03, 4.38017e-03, 3.91841e-03, 3.30312e-03, 2.53826e-03, | |||
1.69402e-03, 7.83910e-04, -1.22305e-04, -1.00532e-03, -1.80630e-03, -2.50229e-03, -3.06572e-03, -3.44913e-03, -3.69208e-03, | |||
-3.73609e-03, -3.60506e-03, -3.31209e-03, -2.87636e-03, -2.31717e-03, -1.66939e-03, -9.57591e-04, -2.23591e-04, 5.04003e-04, | |||
1.18587e-03, 1.79763e-03, 2.30599e-03, 2.69770e-03, 2.95067e-03, 3.06271e-03, 3.02785e-03, 2.85589e-03, 2.55400e-03, | |||
2.14503e-03, 1.64530e-03, 1.08328e-03, 4.83736e-04, -1.24964e-04, -7.15425e-04, -1.24542e-03, -1.72140e-03, -2.10060e-03, | |||
-2.37032e-03, -2.53448e-03, -2.56546e-03, -2.49179e-03, -2.29070e-03, -2.00403e-03, -1.61659e-03, -1.17826e-03, -6.83619e-04, | |||
-1.74591e-04, 3.29944e-04, 8.13976e-04, 1.24507e-03, 1.61153e-03, 1.89772e-03, 2.08906e-03, 2.18121e-03, 2.17177e-03, | |||
2.06312e-03, 1.86382e-03, 1.58411e-03, 1.23827e-03, 8.43172e-04, 4.19904e-04, -1.56907e-05, -4.39507e-04, -8.37003e-04, | |||
-1.18696e-03, -1.47938e-03, -1.69770e-03, -1.83969e-03, -1.89318e-03, -1.86141e-03, -1.74292e-03, -1.54080e-03, -1.27846e-03, | |||
-9.95436e-04, -6.26595e-04, -2.64150e-04, 1.13622e-04, 4.76814e-04, 8.10335e-04, 1.10736e-03, 1.34273e-03, 1.52430e-03, | |||
1.62390e-03, 1.66246e-03, 1.61522e-03, 1.50632e-03, 1.32668e-03, 1.09739e-03, 8.19243e-04, 5.11244e-04, 1.80867e-04, | |||
-1.46339e-04, -4.68008e-04, -7.60099e-04, -1.02128e-03, -1.23115e-03, -1.39047e-03, -1.48008e-03, -1.51120e-03, -1.46991e-03, | |||
-1.37032e-03, -1.20407e-03, -9.91551e-04, -7.27194e-04, -4.35173e-04, -1.11623e-04, 2.15793e-04, 5.54161e-04, 8.66979e-04, | |||
1.18292e-03, 1.42542e-03, 1.77484e-03, 1.69052e-03, 2.12100e-03, 1.90833e-03, 2.18455e-03, 1.90159e-03, 2.03277e-03, | |||
1.68588e-03, 1.69365e-03, 1.30556e-03, 1.21725e-03, 8.19039e-04, 6.67167e-04, 2.89567e-04, 1.11906e-04, -2.18096e-04, | |||
-3.95376e-04, -6.46202e-04, -8.02450e-04, -9.64235e-04, -1.08660e-03, -1.15302e-03, -1.23904e-03, -1.20955e-03, -1.26937e-03, | |||
-1.15443e-03, -1.19836e-03, -1.01377e-03, -1.05669e-03, -8.15327e-04, -8.79730e-04, -5.93148e-04, -6.95337e-04, -3.75376e-04, | |||
-5.27511e-04, -1.78544e-04, -3.96418e-04, -8.80292e-06, -3.37279e-04}; | |||
#define LOW_PASS_SIZE (sizeof(low_pass) / sizeof(low_pass[0])) | |||
static const float sync_pattern[] = {-14, -14, -14, 18, 18, -14, -14, 18, 18, -14, -14, 18, 18, -14, -14, 18, | |||
18, -14, -14, 18, 18, -14, -14, 18, 18, -14, -14, 18, 18, -14, -14, -14}; | |||
#define SYNC_PATTERN_SIZE (sizeof(sync_pattern) / sizeof(sync_pattern[0])) |
@@ -0,0 +1,37 @@ | |||
#!/usr/bin/python3 | |||
import sys | |||
from PIL import Image | |||
""" | |||
Converts a PNG into a gradient compatible | |||
with aptdec. Requires Pillow: | |||
pip3 install Pillow | |||
""" | |||
if len(sys.argv) == 1: | |||
print("Usage: {} filename.png".format(sys.argv[0])) | |||
exit() | |||
image = Image.open(sys.argv[1]) | |||
pixels = image.load() | |||
if len(pixels[0, 0]) != 3: | |||
print("Image must be RGB") | |||
exit() | |||
if image.size[0] != 1: | |||
print("Image must be 1px wide") | |||
exit() | |||
print("uint32_t gradient[{}] = {{\n ".format(image.size[1]), end="") | |||
for y in range(image.size[1]): | |||
print("0x" + "".join("{:02X}".format(a) for a in pixels[0, y]), end="") | |||
if y != image.size[1] - 1: | |||
print(", ", end="") | |||
if y % 7 == 6: | |||
print("\n ", end="") | |||
print("\n};") |
@@ -1,26 +0,0 @@ | |||
#!/usr/bin/python3 | |||
import sys | |||
from PIL import Image | |||
''' | |||
Converts a PNG into a palette compatible | |||
with aptdec. Requires PIL: | |||
pip3 install Pillow | |||
''' | |||
if len(sys.argv) == 1: | |||
print("Usage: python3 {} filename.png".format(sys.argv[0])) | |||
exit() | |||
image = Image.open(sys.argv[1]) | |||
pixels = image.load() | |||
sys.stdout.write("char palette[{}*3] = {{\n \"".format(image.size[1])) | |||
for y in range(1, image.size[1]+1): | |||
sys.stdout.write(''.join('\\x{:02x}'.format(a) for a in pixels[0, y-1])) | |||
if(y % 7 == 0 and y != 0): | |||
sys.stdout.write("\"\n \"") | |||
print("\"\n};") |