You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

491 lines
16 KiB

  1. /*
  2. * aptdec - A lightweight FOSS (NOAA) APT decoder
  3. * Copyright (C) 2004-2009 Thierry Leconte (F4DWV) 2019-2023 Xerbo (xerbo@protonmail.com)
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. #include <stdio.h>
  19. #include <stdlib.h>
  20. #include <string.h>
  21. #ifndef _MSC_VER
  22. #include <libgen.h>
  23. #else
  24. #include <windows.h>
  25. #endif
  26. #include <math.h>
  27. #include <sndfile.h>
  28. #include <time.h>
  29. #include <signal.h>
  30. #include <float.h>
  31. #include <aptdec.h>
  32. #include "argparse/argparse.h"
  33. #include "pngio.h"
  34. #include "util.h"
  35. // Maximum height of an APT image that can be decoded
  36. #define APTDEC_MAX_HEIGHT 3000
  37. #define STRING_SPLIT(src, dst, delim, n) \
  38. char *dst[n]; \
  39. size_t dst##_len = 0; \
  40. {char *token = strtok(src, delim); \
  41. while (token != NULL && dst##_len < n) { \
  42. dst[dst##_len++] = token; \
  43. token = strtok(NULL, delim); \
  44. }}
  45. typedef struct {
  46. char *type; // Output image type
  47. char *effects; // Effects on the image
  48. int satellite; // The satellite number
  49. int realtime; // Realtime decoding
  50. char *filename; // Output filename
  51. char *lut; // Filename of lut
  52. } options_t;
  53. typedef struct {
  54. SNDFILE *file;
  55. SF_INFO info;
  56. } SNDFILE_t;
  57. // Function declarations
  58. static int process_file(const char *path, options_t *opts);
  59. static int freq_from_filename(const char *filename);
  60. static int satid_from_freq(int freq);
  61. static size_t callback(float *samples, size_t count, void *context);
  62. static void write_line(writer_t *png, float *row);
  63. static volatile int sigint_stop = 0;
  64. void sigint_handler(int signum) {
  65. (void)signum;
  66. sigint_stop = 1;
  67. }
  68. // Basename is unsupported by MSVC
  69. // This implementation is GNU style
  70. #ifdef _MSC_VER
  71. char *basename(const char *filename) {
  72. char *p = strrchr(filename, '/');
  73. return p ? p + 1 : (char *)filename;
  74. }
  75. #endif
  76. int main(int argc, const char **argv) {
  77. char version[128];
  78. get_version(version);
  79. printf("%s\n", version);
  80. // clang-format off
  81. options_t opts = {
  82. .type = "raw",
  83. .effects = "",
  84. .satellite = 0,
  85. .realtime = 0,
  86. .filename = "",
  87. .lut = "",
  88. };
  89. // clang-format on
  90. static const char *const usages[] = {
  91. "aptdec-cli [options] [[--] sources]",
  92. "aptdec-cli [sources]",
  93. NULL,
  94. };
  95. struct argparse_option options[] = {
  96. OPT_HELP(),
  97. OPT_GROUP("Image options"),
  98. OPT_STRING('i', "image", &opts.type, "set output image type (see the README for a list)", NULL, 0, 0),
  99. OPT_STRING('e', "effect", &opts.effects, "add an effect (see the README for a list)", NULL, 0, 0),
  100. OPT_GROUP("Satellite options"),
  101. OPT_INTEGER('s', "satellite", &opts.satellite, "satellite ID, must be either NORAD or between 15 and 19", NULL, 0, 0),
  102. OPT_GROUP("Paths"),
  103. OPT_STRING('l', "lut", &opts.lut, "path to a LUT", NULL, 0, 0),
  104. OPT_STRING('o', "output", &opts.filename, "path of output image", NULL, 0, 0),
  105. OPT_GROUP("Misc"),
  106. OPT_BOOLEAN('r', "realtime", &opts.realtime, "decode in realtime", NULL, 0, 0),
  107. OPT_END(),
  108. };
  109. struct argparse argparse;
  110. argparse_init(&argparse, options, usages, 0);
  111. argparse_describe(&argparse,
  112. "\nA lightweight FOSS NOAA APT satellite imagery decoder.",
  113. "\nSee `README.md` for a full description of command line arguments and `LICENSE` for licensing conditions."
  114. );
  115. argc = argparse_parse(&argparse, argc, argv);
  116. if (argc == 0) {
  117. argparse_usage(&argparse);
  118. }
  119. if (argc > 1 && opts.realtime) {
  120. error("Cannot use -r/--realtime with multiple input files");
  121. }
  122. // Actually decode the files
  123. for (int i = 0; i < argc; i++) {
  124. if (opts.satellite == 25338) opts.satellite = 15;
  125. if (opts.satellite == 28654) opts.satellite = 18;
  126. if (opts.satellite == 33591) opts.satellite = 19;
  127. if (opts.satellite == 0) {
  128. int freq = freq_from_filename(argv[i]);
  129. if (freq == 0) {
  130. opts.satellite = 19;
  131. warning("Satellite not specified, defaulting to NOAA-19");
  132. } else {
  133. opts.satellite = satid_from_freq(freq);
  134. printf("Satellite not specified, choosing to NOAA-%i based on filename\n", opts.satellite);
  135. }
  136. }
  137. if (opts.satellite != 15 && opts.satellite != 18 && opts.satellite != 19) {
  138. error("Invalid satellite ID");
  139. }
  140. process_file(argv[i], &opts);
  141. }
  142. return 0;
  143. }
  144. static int process_file(const char *path, options_t *opts) {
  145. const char *path_basename = basename((char *)path);
  146. const char *dot = strrchr(path_basename, '.');
  147. char name[256];
  148. if (dot == NULL) {
  149. strncpy(name, path_basename, 255);
  150. } else {
  151. strncpy(name, path_basename, clamp_int(dot - path_basename, 0, 255));
  152. }
  153. // Set filename to time when reading from stdin
  154. if (strcmp(name, "-") == 0) {
  155. time_t t = time(NULL);
  156. strcpy(name, ctime(&t));
  157. }
  158. writer_t *realtime_png;
  159. if (opts->realtime) {
  160. char filename[269];
  161. sprintf(filename, "%s-decoding.png", name);
  162. realtime_png = writer_init(filename, APT_REGION_FULL, APTDEC_MAX_HEIGHT, PNG_COLOR_TYPE_GRAY, "Unknown");
  163. // Capture Ctrl+C
  164. signal(SIGINT, sigint_handler);
  165. }
  166. // Create a libsndfile instance
  167. SNDFILE_t audioFile;
  168. audioFile.file = sf_open(path, SFM_READ, &audioFile.info);
  169. if (audioFile.file == NULL) {
  170. error_noexit("Could not open file");
  171. return 0;
  172. }
  173. printf("Input file: %s\n", path_basename);
  174. printf("Input sample rate: %d\n", audioFile.info.samplerate);
  175. // Create a libaptdec instances
  176. aptdec_t *aptdec = aptdec_init(audioFile.info.samplerate);
  177. if (aptdec == NULL) {
  178. sf_close(audioFile.file);
  179. error_noexit("Error initializing libaptdec, sample rate too high/low?");
  180. return 0;
  181. }
  182. // Decode image
  183. float *data = (float *)malloc(APT_IMG_WIDTH * (APTDEC_MAX_HEIGHT+1) * sizeof(float));
  184. size_t rows;
  185. for (rows = 0; rows < APTDEC_MAX_HEIGHT; rows++) {
  186. float *row = &data[rows * APT_IMG_WIDTH];
  187. // Break the loop when there are no more samples or the process has been sent SIGINT
  188. if (aptdec_getrow(aptdec, row, callback, &audioFile) == 0 || sigint_stop) {
  189. break;
  190. }
  191. if (opts->realtime) {
  192. write_line(realtime_png, row);
  193. }
  194. fprintf(stderr, "Row: %zu/%zu\r", rows+1, audioFile.info.frames/audioFile.info.samplerate * 2);
  195. fflush(stderr);
  196. }
  197. printf("\n");
  198. // Close stream
  199. sf_close(audioFile.file);
  200. aptdec_free(aptdec);
  201. if (opts->realtime) {
  202. #pragma GCC diagnostic push
  203. #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
  204. writer_free(realtime_png);
  205. #pragma GCC diagnostic pop
  206. char filename[269];
  207. sprintf(filename, "%s-decoding.png", name);
  208. remove(filename);
  209. }
  210. // Normalize
  211. int error;
  212. apt_image_t img = apt_normalize(data, rows, opts->satellite, &error);
  213. if (error) {
  214. error_noexit("Normalization failed");
  215. return 0;
  216. }
  217. // clang-format off
  218. const char *channel_name[] = { "?", "1", "2", "3A", "4", "5", "3B" };
  219. const char *channel_desc[] = {
  220. "unknown",
  221. "visible (0.58-0.68 um)",
  222. "near-infrared (0.725-1.0 um)",
  223. "near-infrared (1.58-1.64 um)",
  224. "thermal-infrared (10.3-11.3 um)",
  225. "thermal-infrared (11.5-12.5 um)",
  226. "mid-infrared (3.55-3.93 um)"
  227. };
  228. printf("Channel A: %s - %s\n", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  229. printf("Channel B: %s - %s\n", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  230. // clang-format on
  231. STRING_SPLIT(opts->type, images, ",", 10)
  232. STRING_SPLIT(opts->effects, effects, ",", 10)
  233. STRING_SPLIT(opts->filename, filenames, ",", 10)
  234. for (size_t i = 0; i < effects_len; i++) {
  235. if (strcmp(effects[i], "crop") == 0) {
  236. apt_crop(&img);
  237. } else if (strcmp(effects[i], "denoise") == 0) {
  238. apt_denoise(&img, APT_REGION_CHA);
  239. apt_denoise(&img, APT_REGION_CHB);
  240. } else if (strcmp(effects[i], "flip") == 0) {
  241. apt_flip(&img, APT_REGION_CHA);
  242. apt_flip(&img, APT_REGION_CHB);
  243. }
  244. }
  245. for (size_t i = 0; i < images_len; i++) {
  246. const char *base;
  247. if (i < filenames_len) {
  248. base = filenames[i];
  249. } else {
  250. base = name;
  251. }
  252. if (strcmp(images[i], "thermal") == 0) {
  253. if (img.ch[1] >= 4) {
  254. char filename[269];
  255. sprintf(filename, "%s-thermal.png", base);
  256. char description[128];
  257. sprintf(description, "Calibrated thermal image, channel %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  258. // Perform visible calibration
  259. apt_image_t _img = apt_image_clone(img);
  260. apt_calibrate_thermal(&_img, APT_REGION_CHA);
  261. writer_t *writer = writer_init(filename, APT_REGION_CHB, img.rows, PNG_COLOR_TYPE_RGB, description);
  262. writer_write_image_gradient(writer, &_img, temperature_gradient);
  263. writer_free(writer);
  264. free(_img.data);
  265. } else {
  266. error_noexit("Could not generate thermal image, no infrared channel");
  267. }
  268. } else if (strcmp(images[i], "visible") == 0) {
  269. if (img.ch[0] <= 2) {
  270. char filename[269];
  271. sprintf(filename, "%s-visible.png", base);
  272. char description[128];
  273. sprintf(description, "Calibrated visible image, channel %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  274. // Perform visible calibration
  275. apt_image_t _img = apt_image_clone(img);
  276. apt_calibrate_visible(&_img, APT_REGION_CHA);
  277. writer_t *writer = writer_init(filename, APT_REGION_CHA, img.rows, PNG_COLOR_TYPE_GRAY, description);
  278. writer_write_image(writer, &_img);
  279. writer_free(writer);
  280. free(_img.data);
  281. } else {
  282. error_noexit("Could not generate visible image, no visible channel");
  283. }
  284. }
  285. }
  286. for (size_t i = 0; i < effects_len; i++) {
  287. if (strcmp(effects[i], "stretch") == 0) {
  288. apt_stretch(&img, APT_REGION_CHA);
  289. apt_stretch(&img, APT_REGION_CHB);
  290. } else if (strcmp(effects[i], "equalize") == 0) {
  291. apt_equalize(&img, APT_REGION_CHA);
  292. apt_equalize(&img, APT_REGION_CHB);
  293. }
  294. }
  295. for (size_t i = 0; i < images_len; i++) {
  296. const char *base;
  297. if (i < filenames_len) {
  298. base = filenames[i];
  299. } else {
  300. base = name;
  301. }
  302. if (strcmp(images[i], "raw") == 0) {
  303. char filename[269];
  304. sprintf(filename, "%s-raw.png", base);
  305. char description[128];
  306. sprintf(description,
  307. "Raw image, channel %s - %s / %s - %s",
  308. channel_name[img.ch[0]],
  309. channel_desc[img.ch[0]],
  310. channel_name[img.ch[1]],
  311. channel_desc[img.ch[1]]
  312. );
  313. writer_t *writer = writer_init(filename, APT_REGION_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  314. writer_write_image(writer, &img);
  315. writer_free(writer);
  316. } else if (strcmp(images[i], "lut") == 0) {
  317. if (opts->lut != NULL && opts->lut[0] != '\0') {
  318. char filename[269];
  319. sprintf(filename, "%s-lut.png", base);
  320. char description[128];
  321. sprintf(description,
  322. "LUT image, channel %s - %s / %s - %s",
  323. channel_name[img.ch[0]],
  324. channel_desc[img.ch[0]],
  325. channel_name[img.ch[1]],
  326. channel_desc[img.ch[1]]
  327. );
  328. png_colorp lut = (png_colorp)malloc(sizeof(png_color)*256*256);
  329. if (read_lut(opts->lut, lut)) {
  330. writer_t *writer = writer_init(filename, APT_REGION_CHA, img.rows, PNG_COLOR_TYPE_RGB, description);
  331. writer_write_image_lut(writer, &img, lut);
  332. writer_free(writer);
  333. }
  334. free(lut);
  335. } else {
  336. warning("Cannot create LUT image, missing -l/--lut");
  337. }
  338. } else if (strcmp(images[i], "a") == 0) {
  339. char filename[269];
  340. sprintf(filename, "%s-a.png", base);
  341. char description[128];
  342. sprintf(description, "Channel A: %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  343. writer_t *writer = writer_init(filename, APT_REGION_CHA_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  344. writer_write_image(writer, &img);
  345. writer_free(writer);
  346. } else if (strcmp(images[i], "b") == 0) {
  347. char filename[269];
  348. sprintf(filename, "%s-b.png", base);
  349. char description[128];
  350. sprintf(description, "Channel B: %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  351. writer_t *writer = writer_init(filename, APT_REGION_CHB_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  352. writer_write_image(writer, &img);
  353. writer_free(writer);
  354. }
  355. }
  356. free(img.data);
  357. return 1;
  358. }
  359. static int freq_from_filename(const char *filename) {
  360. char frequency_text[10] = {'\0'};
  361. if (strlen(filename) >= 30+4 && strncmp(filename, "gqrx_", 5) == 0) {
  362. memcpy(frequency_text, &filename[21], 9);
  363. return atoi(frequency_text);
  364. } else if (strlen(filename) == 40+4 && strncmp(filename, "SDRSharp_", 9) == 0) {
  365. memcpy(frequency_text, &filename[26], 9);
  366. return atoi(frequency_text);
  367. } else if (strlen(filename) == 37+4 && strncmp(filename, "audio_", 6) == 0) {
  368. memcpy(frequency_text, &filename[6], 9);
  369. return atoi(frequency_text);
  370. }
  371. return 0;
  372. }
  373. static int satid_from_freq(int freq) {
  374. int differences[3] = {
  375. abs(freq - 137620000), // NOAA-15
  376. abs(freq - 137912500), // NOAA-18
  377. abs(freq - 137100000) // NOAA-19
  378. };
  379. int best = 0;
  380. for (size_t i = 0; i < 3; i++) {
  381. if (differences[i] < differences[best]) {
  382. best = i;
  383. }
  384. }
  385. const int lut[3] = {15, 18, 19};
  386. return lut[best];
  387. }
  388. // Read samples from a SNDFILE_t instance (passed through context)
  389. static size_t callback(float *samples, size_t count, void *context) {
  390. SNDFILE_t *file = (SNDFILE_t *)context;
  391. switch (file->info.channels) {
  392. case 1:
  393. return sf_read_float(file->file, samples, count);
  394. case 2: {
  395. float _samples[count * 2];
  396. size_t read = sf_read_float(file->file, _samples, count * 2);
  397. for (size_t i = 0; i < count; i++) {
  398. // Average of left and right
  399. samples[i] = (_samples[i*2] + _samples[i*2 + 1]) / 2.0f;
  400. }
  401. return read / 2;
  402. }
  403. default:
  404. error_noexit("Only mono and stereo audio files are supported\n");
  405. return 0;
  406. }
  407. }
  408. // Write a line with very basic equalization
  409. static void write_line(writer_t *png, float *row) {
  410. float min = FLT_MAX;
  411. float max = FLT_MIN;
  412. for (int i = 0; i < APT_IMG_WIDTH; i++) {
  413. if (row[i] < min) min = row[i];
  414. if (row[i] > max) max = row[i];
  415. }
  416. png_byte pixels[APT_IMG_WIDTH];
  417. for (int i = 0; i < APT_IMG_WIDTH; i++) {
  418. pixels[i] = clamp_int(roundf((row[i]-min) / (max-min) * 255.0f), 0, 255);
  419. }
  420. #pragma GCC diagnostic push
  421. #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
  422. png_write_row(png->png, pixels);
  423. #pragma GCC diagnostic pop
  424. }