Browse Source

Version 2.0.0 work

v2
Xerbo 1 year ago
parent
commit
8d6e8be8f4
No known key found for this signature in database GPG Key ID: 34103F6D8F11CEB0
80 changed files with 2240 additions and 2186 deletions
  1. +39
    -12
      .gitignore
  2. +2
    -2
      .gitmodules
  3. +67
    -63
      CMakeLists.txt
  4. +15
    -16
      CONTRIBUTING.md
  5. +65
    -58
      README.md
  6. +490
    -0
      aptdec-cli/main.c
  7. +153
    -0
      aptdec-cli/pngio.c
  8. +41
    -0
      aptdec-cli/pngio.h
  9. +8
    -3
      aptdec-cli/util.c
  10. +10
    -12
      aptdec-cli/util.h
  11. +3
    -3
      build_windows.bat
  12. +1
    -2
      build_windows.sh
  13. +0
    -19
      cmake/FindLibSndFile.cmake
  14. +65
    -3
      libaptdec/algebra.c
  15. +14
    -3
      libaptdec/algebra.h
  16. +121
    -0
      libaptdec/calibration.c
  17. +6
    -4
      libaptdec/calibration.h
  18. +94
    -0
      libaptdec/color.c
  19. +237
    -0
      libaptdec/dsp.c
  20. +175
    -0
      libaptdec/effects.c
  21. +109
    -0
      libaptdec/filter.c
  22. +14
    -2
      libaptdec/filter.h
  23. +236
    -0
      libaptdec/image.c
  24. +169
    -0
      libaptdec/include/aptdec.h
  25. +40
    -0
      libaptdec/util.c
  26. +29
    -0
      libaptdec/util.h
  27. BIN
      luts/N19-HRPT-Falsecolor.png
  28. BIN
      luts/WXtoImg-BD.png
  29. BIN
      luts/WXtoImg-CC.png
  30. BIN
      luts/WXtoImg-EC.png
  31. BIN
      luts/WXtoImg-HE.png
  32. BIN
      luts/WXtoImg-HF.png
  33. BIN
      luts/WXtoImg-JF.png
  34. BIN
      luts/WXtoImg-JJ.png
  35. BIN
      luts/WXtoImg-MB.png
  36. BIN
      luts/WXtoImg-MD.png
  37. BIN
      luts/WXtoImg-N15-HVC.png
  38. BIN
      luts/WXtoImg-N18-HVC.png
  39. BIN
      luts/WXtoImg-N19-HVC.png
  40. BIN
      luts/WXtoImg-NO.png
  41. BIN
      luts/WXtoImg-TA.png
  42. BIN
      luts/WXtoImg-ZA.png
  43. BIN
      luts/WXtoImg-class.png
  44. BIN
      luts/WXtoImg-fire.png
  45. BIN
      luts/WXtoImg-sea.png
  46. BIN
      palettes/N19-HRPT-Falsecolor.png
  47. BIN
      palettes/WXtoImg-BD.png
  48. BIN
      palettes/WXtoImg-CC.png
  49. BIN
      palettes/WXtoImg-EC.png
  50. BIN
      palettes/WXtoImg-HE.png
  51. BIN
      palettes/WXtoImg-HF.png
  52. BIN
      palettes/WXtoImg-JF.png
  53. BIN
      palettes/WXtoImg-JJ.png
  54. BIN
      palettes/WXtoImg-MB.png
  55. BIN
      palettes/WXtoImg-MD.png
  56. BIN
      palettes/WXtoImg-NO.png
  57. BIN
      palettes/WXtoImg-TA.png
  58. BIN
      palettes/WXtoImg-ZA.png
  59. BIN
      palettes/WXtoImg-fire.png
  60. BIN
      palettes/WXtoImg-sea.png
  61. +0
    -126
      src/apt.h
  62. +0
    -1
      src/argparse
  63. +0
    -101
      src/calibration.c
  64. +0
    -83
      src/color.c
  65. +0
    -3
      src/color.h
  66. +0
    -60
      src/common.h
  67. +0
    -258
      src/dsp.c
  68. +0
    -62
      src/filter.c
  69. +0
    -388
      src/image.c
  70. +0
    -2
      src/image.h
  71. +0
    -56
      src/libs/median.c
  72. +0
    -316
      src/main.c
  73. +0
    -411
      src/pngio.c
  74. +0
    -11
      src/pngio.h
  75. +0
    -80
      src/taps.h
  76. BIN
      util/PrecipitationPalette.png
  77. BIN
      util/TempPalette.png
  78. BIN
      util/Temperature.png
  79. +37
    -0
      util/img2gradient.py
  80. +0
    -26
      util/img2pal.py

+ 39
- 12
.gitignore View File

@@ -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/

+ 2
- 2
.gitmodules View File

@@ -1,3 +1,3 @@
[submodule "src/argparse"]
path = src/argparse
[submodule "aptdec-cli/argparse"]
path = aptdec-cli/argparse
url = https://github.com/cofyc/argparse

+ 67
- 63
CMakeLists.txt View File

@@ -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)

+ 15
- 16
CONTRIBUTING.md View File

@@ -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

+ 65
- 58
README.md View File

@@ -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



+ 490
- 0
aptdec-cli/main.c View File

@@ -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
}

+ 153
- 0
aptdec-cli/pngio.c View File

@@ -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;
}

+ 41
- 0
aptdec-cli/pngio.h View File

@@ -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

src/util.c → aptdec-cli/util.c View File

@@ -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());
}

src/util.h → aptdec-cli/util.h View File

@@ -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

+ 3
- 3
build_windows.bat View File

@@ -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
- 2
build_windows.sh View File

@@ -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 ../



+ 0
- 19
cmake/FindLibSndFile.cmake View File

@@ -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)

src/algebra.c → libaptdec/algebra.c View File

@@ -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);
}

src/algebra.h → libaptdec/algebra.h View File

@@ -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

+ 121
- 0
libaptdec/calibration.c View File

@@ -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];
}
}

src/calibration.h → libaptdec/calibration.h View File

@@ -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

+ 94
- 0
libaptdec/color.c View File

@@ -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
};

+ 237
- 0
libaptdec/dsp.c View File

@@ -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;
}

+ 175
- 0
libaptdec/effects.c View File

@@ -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;
}

+ 109
- 0
libaptdec/filter.c View File

@@ -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);
}

src/filter.h → libaptdec/filter.h View File

@@ -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

+ 236
- 0
libaptdec/image.c View File

@@ -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;
}

+ 169
- 0
libaptdec/include/aptdec.h View File

@@ -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

+ 40
- 0
libaptdec/util.c View File

@@ -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;
}

+ 29
- 0
libaptdec/util.h View File

@@ -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

BIN
luts/N19-HRPT-Falsecolor.png View File

Before After
Width: 256  |  Height: 256  |  Size: 16 KiB

BIN
luts/WXtoImg-BD.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.6 KiB

BIN
luts/WXtoImg-CC.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.6 KiB

BIN
luts/WXtoImg-EC.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.6 KiB

BIN
luts/WXtoImg-HE.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.7 KiB

BIN
luts/WXtoImg-HF.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.6 KiB

BIN
luts/WXtoImg-JF.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.6 KiB

BIN
luts/WXtoImg-JJ.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.8 KiB

BIN
luts/WXtoImg-MB.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.6 KiB

BIN
luts/WXtoImg-MD.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.6 KiB

BIN
palettes/WXtoImg-N15-HVC.png → luts/WXtoImg-N15-HVC.png View File

Before After
Width: 256  |  Height: 256  |  Size: 15 KiB Width: 256  |  Height: 256  |  Size: 12 KiB

BIN
palettes/WXtoImg-N18-HVC.png → luts/WXtoImg-N18-HVC.png View File

Before After
Width: 256  |  Height: 256  |  Size: 15 KiB Width: 256  |  Height: 256  |  Size: 11 KiB

BIN
palettes/WXtoImg-N19-HVC.png → luts/WXtoImg-N19-HVC.png View File

Before After
Width: 256  |  Height: 256  |  Size: 15 KiB Width: 256  |  Height: 256  |  Size: 12 KiB

BIN
luts/WXtoImg-NO.png View File

Before After
Width: 256  |  Height: 256  |  Size: 2.0 KiB

BIN
luts/WXtoImg-TA.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.5 KiB

BIN
luts/WXtoImg-ZA.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.5 KiB

BIN
palettes/WXtoImg-class.png → luts/WXtoImg-class.png View File

Before After
Width: 256  |  Height: 256  |  Size: 13 KiB Width: 256  |  Height: 256  |  Size: 10 KiB

BIN
luts/WXtoImg-fire.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.5 KiB

BIN
luts/WXtoImg-sea.png View File

Before After
Width: 256  |  Height: 256  |  Size: 1.5 KiB

BIN
palettes/N19-HRPT-Falsecolor.png View File

Before After
Width: 256  |  Height: 256  |  Size: 16 KiB

BIN
palettes/WXtoImg-BD.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.9 KiB

BIN
palettes/WXtoImg-CC.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.9 KiB

BIN
palettes/WXtoImg-EC.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.9 KiB

BIN
palettes/WXtoImg-HE.png View File

Before After
Width: 256  |  Height: 256  |  Size: 5.0 KiB

BIN
palettes/WXtoImg-HF.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.9 KiB

BIN
palettes/WXtoImg-JF.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.9 KiB

BIN
palettes/WXtoImg-JJ.png View File

Before After
Width: 256  |  Height: 256  |  Size: 5.0 KiB

BIN
palettes/WXtoImg-MB.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.9 KiB

BIN
palettes/WXtoImg-MD.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.9 KiB

BIN
palettes/WXtoImg-NO.png View File

Before After
Width: 256  |  Height: 256  |  Size: 5.3 KiB

BIN
palettes/WXtoImg-TA.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.8 KiB

BIN
palettes/WXtoImg-ZA.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.8 KiB

BIN
palettes/WXtoImg-fire.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.8 KiB

BIN
palettes/WXtoImg-sea.png View File

Before After
Width: 256  |  Height: 256  |  Size: 4.8 KiB

+ 0
- 126
src/apt.h View File

@@ -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

+ 0
- 1
src/argparse

@@ -1 +0,0 @@
Subproject commit c612dc03958cdbd538ca306d61853b643a435933

+ 0
- 101
src/calibration.c View File

@@ -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];
}
}

+ 0
- 83
src/color.c View File

@@ -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"};

+ 0
- 3
src/color.h View File

@@ -1,3 +0,0 @@
#include "common.h"

#define MCOMPOSITE(m1, a1, m2, a2) (m1 * a1 + m2 * a2 * (1 - a1))

+ 0
- 60
src/common.h View File

@@ -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

+ 0
- 258
src/dsp.c View File

@@ -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;
}

+ 0
- 62
src/filter.c View File

@@ -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;
}

+ 0
- 388
src/image.c View File

@@ -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);
}
}
}

+ 0
- 2
src/image.h View File

@@ -1,2 +0,0 @@
#include "apt.h"
#include "common.h"

+ 0
- 56
src/libs/median.c View File

@@ -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

+ 0
- 316
src/main.c View File

@@ -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);
}
}

+ 0
- 411
src/pngio.c View File

@@ -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);
}

+ 0
- 11
src/pngio.h View File

@@ -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();

+ 0
- 80
src/taps.h View File

@@ -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]))

BIN
util/PrecipitationPalette.png View File

Before After
Width: 1  |  Height: 58  |  Size: 1.4 KiB Width: 1  |  Height: 58  |  Size: 199 B

BIN
util/TempPalette.png View File

Before After
Width: 1  |  Height: 256  |  Size: 901 B Width: 1  |  Height: 256  |  Size: 454 B

BIN
util/Temperature.png View File

Before After
Width: 298  |  Height: 68  |  Size: 2.0 KiB Width: 298  |  Height: 68  |  Size: 1.6 KiB

+ 37
- 0
util/img2gradient.py View File

@@ -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};")

+ 0
- 26
util/img2pal.py View File

@@ -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};")

Loading…
Cancel
Save