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.
 
 
 
 
 

537 line
18 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. apt_image_t strip(apt_image_t img);
  64. int array_contains(char **array, char *value, size_t n);
  65. static volatile int sigint_stop = 0;
  66. void sigint_handler(int signum) {
  67. (void)signum;
  68. sigint_stop = 1;
  69. }
  70. // Basename is unsupported by MSVC
  71. // This implementation is GNU style
  72. #ifdef _MSC_VER
  73. char *basename(const char *filename) {
  74. char *p = strrchr(filename, '/');
  75. return p ? p + 1 : (char *)filename;
  76. }
  77. #endif
  78. int main(int argc, const char **argv) {
  79. char version[128];
  80. get_version(version);
  81. printf("%s\n", version);
  82. // clang-format off
  83. options_t opts = {
  84. .type = "raw",
  85. .effects = "",
  86. .satellite = 0,
  87. .realtime = 0,
  88. .filename = "",
  89. .lut = "",
  90. };
  91. // clang-format on
  92. static const char *const usages[] = {
  93. "aptdec-cli [options] [[--] sources]",
  94. "aptdec-cli [sources]",
  95. NULL,
  96. };
  97. struct argparse_option options[] = {
  98. OPT_HELP(),
  99. OPT_GROUP("Image options"),
  100. OPT_STRING('i', "image", &opts.type, "set output image type (see the README for a list)", NULL, 0, 0),
  101. OPT_STRING('e', "effect", &opts.effects, "add an effect (see the README for a list)", NULL, 0, 0),
  102. OPT_GROUP("Satellite options"),
  103. OPT_INTEGER('s', "satellite", &opts.satellite, "satellite ID, must be either NORAD or 15/18/19", NULL, 0, 0),
  104. OPT_GROUP("Paths"),
  105. OPT_STRING('l', "lut", &opts.lut, "path to a LUT", NULL, 0, 0),
  106. OPT_STRING('o', "output", &opts.filename, "path of output image", NULL, 0, 0),
  107. OPT_GROUP("Misc"),
  108. OPT_BOOLEAN('r', "realtime", &opts.realtime, "decode in realtime", NULL, 0, 0),
  109. OPT_END(),
  110. };
  111. struct argparse argparse;
  112. argparse_init(&argparse, options, usages, 0);
  113. argparse_describe(&argparse,
  114. "\nA lightweight FOSS NOAA APT satellite imagery decoder.",
  115. "\nSee `README.md` for a full description of command line arguments and `LICENSE` for licensing conditions."
  116. );
  117. argc = argparse_parse(&argparse, argc, argv);
  118. if (argc == 0) {
  119. argparse_usage(&argparse);
  120. }
  121. if (argc > 1 && opts.realtime) {
  122. error("Cannot use -r/--realtime with multiple input files");
  123. }
  124. // Actually decode the files
  125. for (int i = 0; i < argc; i++) {
  126. if (opts.satellite == 25338) opts.satellite = 15;
  127. if (opts.satellite == 28654) opts.satellite = 18;
  128. if (opts.satellite == 33591) opts.satellite = 19;
  129. if (opts.satellite == 0) {
  130. int freq = freq_from_filename(argv[i]);
  131. if (freq == 0) {
  132. opts.satellite = 19;
  133. warning("Satellite not specified, defaulting to NOAA-19");
  134. } else {
  135. opts.satellite = satid_from_freq(freq);
  136. printf("Satellite not specified, choosing to NOAA-%i based on filename\n", opts.satellite);
  137. }
  138. }
  139. if (opts.satellite != 15 && opts.satellite != 18 && opts.satellite != 19) {
  140. error("Invalid satellite ID");
  141. }
  142. process_file(argv[i], &opts);
  143. }
  144. return 0;
  145. }
  146. static int process_file(const char *path, options_t *opts) {
  147. const char *path_basename = basename((char *)path);
  148. const char *dot = strrchr(path_basename, '.');
  149. char name[256];
  150. if (dot == NULL) {
  151. strncpy(name, path_basename, 255);
  152. } else {
  153. strncpy(name, path_basename, clamp_int(dot - path_basename, 0, 255));
  154. }
  155. // Set filename to time when reading from stdin
  156. if (strcmp(name, "-") == 0) {
  157. time_t t = time(NULL);
  158. strcpy(name, ctime(&t));
  159. }
  160. writer_t *realtime_png;
  161. if (opts->realtime) {
  162. char filename[269];
  163. sprintf(filename, "%s-decoding.png", name);
  164. realtime_png = writer_init(filename, APT_REGION_FULL, APTDEC_MAX_HEIGHT, PNG_COLOR_TYPE_GRAY, "Unknown");
  165. // Capture Ctrl+C
  166. signal(SIGINT, sigint_handler);
  167. }
  168. // Create a libsndfile instance
  169. SNDFILE_t audioFile;
  170. audioFile.file = sf_open(path, SFM_READ, &audioFile.info);
  171. if (audioFile.file == NULL) {
  172. error_noexit("Could not open file");
  173. return 0;
  174. }
  175. printf("Input file: %s\n", path_basename);
  176. printf("Input sample rate: %d\n", audioFile.info.samplerate);
  177. // Create a libaptdec instances
  178. aptdec_t *aptdec = aptdec_init(audioFile.info.samplerate);
  179. if (aptdec == NULL) {
  180. sf_close(audioFile.file);
  181. error_noexit("Error initializing libaptdec, sample rate too high/low?");
  182. return 0;
  183. }
  184. // Decode image
  185. float *data = (float *)malloc(APT_IMG_WIDTH * (APTDEC_MAX_HEIGHT+1) * sizeof(float));
  186. size_t rows;
  187. for (rows = 0; rows < APTDEC_MAX_HEIGHT; rows++) {
  188. float *row = &data[rows * APT_IMG_WIDTH];
  189. // Break the loop when there are no more samples or the process has been sent SIGINT
  190. if (aptdec_getrow(aptdec, row, callback, &audioFile) == 0 || sigint_stop) {
  191. break;
  192. }
  193. if (opts->realtime) {
  194. write_line(realtime_png, row);
  195. }
  196. fprintf(stderr, "Row: %zu/%zu\r", rows+1, audioFile.info.frames/audioFile.info.samplerate * 2);
  197. fflush(stderr);
  198. }
  199. printf("\n");
  200. // Close stream
  201. sf_close(audioFile.file);
  202. aptdec_free(aptdec);
  203. if (opts->realtime) {
  204. #pragma GCC diagnostic push
  205. #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
  206. writer_free(realtime_png);
  207. #pragma GCC diagnostic pop
  208. char filename[269];
  209. sprintf(filename, "%s-decoding.png", name);
  210. remove(filename);
  211. }
  212. // Normalize
  213. int error;
  214. apt_image_t img = apt_normalize(data, rows, opts->satellite, &error);
  215. free(data);
  216. if (error) {
  217. error_noexit("Normalization failed");
  218. return 0;
  219. }
  220. // clang-format off
  221. const char *channel_name[] = { "?", "1", "2", "3A", "4", "5", "3B" };
  222. const char *channel_desc[] = {
  223. "unknown",
  224. "visible (0.58-0.68 um)",
  225. "near-infrared (0.725-1.0 um)",
  226. "near-infrared (1.58-1.64 um)",
  227. "thermal-infrared (10.3-11.3 um)",
  228. "thermal-infrared (11.5-12.5 um)",
  229. "mid-infrared (3.55-3.93 um)"
  230. };
  231. printf("Channel A: %s - %s\n", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  232. printf("Channel B: %s - %s\n", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  233. // clang-format on
  234. STRING_SPLIT(opts->type, images, ",", 10)
  235. STRING_SPLIT(opts->effects, effects, ",", 10)
  236. STRING_SPLIT(opts->filename, filenames, ",", 10)
  237. for (size_t i = 0; i < effects_len; i++) {
  238. if (strcmp(effects[i], "crop") == 0) {
  239. apt_crop(&img);
  240. } else if (strcmp(effects[i], "denoise") == 0) {
  241. apt_denoise(&img, APT_REGION_CHA);
  242. apt_denoise(&img, APT_REGION_CHB);
  243. } else if (strcmp(effects[i], "flip") == 0) {
  244. apt_flip(&img, APT_REGION_CHA);
  245. apt_flip(&img, APT_REGION_CHB);
  246. }
  247. }
  248. for (size_t i = 0; i < images_len; i++) {
  249. const char *base;
  250. if (i < filenames_len) {
  251. base = filenames[i];
  252. } else {
  253. base = name;
  254. }
  255. if (strcmp(images[i], "thermal") == 0) {
  256. if (img.ch[1] >= 4) {
  257. char filename[269];
  258. sprintf(filename, "%s-thermal.png", base);
  259. char description[128];
  260. sprintf(description, "Calibrated thermal image, channel %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  261. // Perform visible calibration
  262. apt_image_t _img = apt_image_clone(img);
  263. apt_calibrate_thermal(&_img, APT_REGION_CHA);
  264. writer_t *writer = writer_init(filename, APT_REGION_CHB, img.rows, PNG_COLOR_TYPE_RGB, description);
  265. writer_write_image_gradient(writer, &_img, temperature_gradient);
  266. writer_free(writer);
  267. free(_img.data);
  268. } else {
  269. error_noexit("Could not generate thermal image, no infrared channel");
  270. }
  271. } else if (strcmp(images[i], "visible") == 0) {
  272. if (img.ch[0] <= 2) {
  273. char filename[269];
  274. sprintf(filename, "%s-visible.png", base);
  275. char description[128];
  276. sprintf(description, "Calibrated visible image, channel %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  277. // Perform visible calibration
  278. apt_image_t _img = apt_image_clone(img);
  279. apt_calibrate_visible(&_img, APT_REGION_CHA);
  280. writer_t *writer = writer_init(filename, APT_REGION_CHA, img.rows, PNG_COLOR_TYPE_GRAY, description);
  281. writer_write_image(writer, &_img);
  282. writer_free(writer);
  283. free(_img.data);
  284. } else {
  285. error_noexit("Could not generate visible image, no visible channel");
  286. }
  287. }
  288. }
  289. for (size_t i = 0; i < effects_len; i++) {
  290. if (strcmp(effects[i], "stretch") == 0) {
  291. apt_stretch(&img, APT_REGION_CHA);
  292. apt_stretch(&img, APT_REGION_CHB);
  293. } else if (strcmp(effects[i], "equalize") == 0) {
  294. apt_equalize(&img, APT_REGION_CHA);
  295. apt_equalize(&img, APT_REGION_CHB);
  296. }
  297. }
  298. for (size_t i = 0; i < images_len; i++) {
  299. const char *base;
  300. if (i < filenames_len) {
  301. base = filenames[i];
  302. } else {
  303. base = name;
  304. }
  305. if (strcmp(images[i], "raw") == 0) {
  306. char filename[269];
  307. sprintf(filename, "%s-raw.png", base);
  308. char description[128];
  309. sprintf(description,
  310. "Raw image, channel %s - %s / %s - %s",
  311. channel_name[img.ch[0]],
  312. channel_desc[img.ch[0]],
  313. channel_name[img.ch[1]],
  314. channel_desc[img.ch[1]]
  315. );
  316. writer_t *writer;
  317. if (array_contains(effects, "strip", effects_len)) {
  318. writer = writer_init(filename, (apt_region_t){0, APT_CH_WIDTH*2}, img.rows, PNG_COLOR_TYPE_GRAY, description);
  319. apt_image_t _img = strip(img);
  320. writer_write_image(writer, &_img);
  321. free(_img.data);
  322. } else {
  323. writer = writer_init(filename, APT_REGION_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  324. writer_write_image(writer, &img);
  325. }
  326. writer_free(writer);
  327. } else if (strcmp(images[i], "lut") == 0) {
  328. if (opts->lut != NULL && opts->lut[0] != '\0') {
  329. char filename[269];
  330. sprintf(filename, "%s-lut.png", base);
  331. char description[128];
  332. sprintf(description,
  333. "LUT image, channel %s - %s / %s - %s",
  334. channel_name[img.ch[0]],
  335. channel_desc[img.ch[0]],
  336. channel_name[img.ch[1]],
  337. channel_desc[img.ch[1]]
  338. );
  339. png_colorp lut = (png_colorp)malloc(sizeof(png_color)*256*256);
  340. if (read_lut(opts->lut, lut)) {
  341. writer_t *writer = writer_init(filename, APT_REGION_CHA, img.rows, PNG_COLOR_TYPE_RGB, description);
  342. writer_write_image_lut(writer, &img, lut);
  343. writer_free(writer);
  344. }
  345. free(lut);
  346. } else {
  347. warning("Cannot create LUT image, missing -l/--lut");
  348. }
  349. } else if (strcmp(images[i], "a") == 0) {
  350. char filename[269];
  351. sprintf(filename, "%s-a.png", base);
  352. char description[128];
  353. sprintf(description, "Channel A: %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  354. writer_t *writer;
  355. if (array_contains(effects, "strip", effects_len)) {
  356. writer = writer_init(filename, (apt_region_t){0, APT_CH_WIDTH}, img.rows, PNG_COLOR_TYPE_GRAY, description);
  357. apt_image_t _img = strip(img);
  358. writer_write_image(writer, &_img);
  359. free(_img.data);
  360. } else {
  361. writer = writer_init(filename, APT_REGION_CHA_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  362. writer_write_image(writer, &img);
  363. }
  364. writer_free(writer);
  365. } else if (strcmp(images[i], "b") == 0) {
  366. char filename[269];
  367. sprintf(filename, "%s-b.png", base);
  368. char description[128];
  369. sprintf(description, "Channel B: %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  370. writer_t *writer;
  371. if (array_contains(effects, "strip", effects_len)) {
  372. writer = writer_init(filename, (apt_region_t){APT_CH_WIDTH, APT_CH_WIDTH}, img.rows, PNG_COLOR_TYPE_GRAY, description);
  373. apt_image_t _img = strip(img);
  374. writer_write_image(writer, &_img);
  375. free(_img.data);
  376. } else {
  377. writer = writer_init(filename, APT_REGION_CHB_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  378. writer_write_image(writer, &img);
  379. }
  380. writer_free(writer);
  381. }
  382. }
  383. free(img.data);
  384. return 1;
  385. }
  386. static int freq_from_filename(const char *filename) {
  387. char frequency_text[10] = {'\0'};
  388. if (strlen(filename) >= 30+4 && strncmp(filename, "gqrx_", 5) == 0) {
  389. memcpy(frequency_text, &filename[21], 9);
  390. return atoi(frequency_text);
  391. } else if (strlen(filename) == 40+4 && strncmp(filename, "SDRSharp_", 9) == 0) {
  392. memcpy(frequency_text, &filename[26], 9);
  393. return atoi(frequency_text);
  394. } else if (strlen(filename) == 37+4 && strncmp(filename, "audio_", 6) == 0) {
  395. memcpy(frequency_text, &filename[6], 9);
  396. return atoi(frequency_text);
  397. }
  398. return 0;
  399. }
  400. static int satid_from_freq(int freq) {
  401. int differences[3] = {
  402. abs(freq - 137620000), // NOAA-15
  403. abs(freq - 137912500), // NOAA-18
  404. abs(freq - 137100000) // NOAA-19
  405. };
  406. int best = 0;
  407. for (size_t i = 0; i < 3; i++) {
  408. if (differences[i] < differences[best]) {
  409. best = i;
  410. }
  411. }
  412. const int lut[3] = {15, 18, 19};
  413. return lut[best];
  414. }
  415. // Read samples from a SNDFILE_t instance (passed through context)
  416. static size_t callback(float *samples, size_t count, void *context) {
  417. SNDFILE_t *file = (SNDFILE_t *)context;
  418. switch (file->info.channels) {
  419. case 1:
  420. return sf_read_float(file->file, samples, count);
  421. case 2: {
  422. float _samples[APTDEC_BUFFER_SIZE * 2];
  423. size_t read = sf_read_float(file->file, _samples, count * 2);
  424. for (size_t i = 0; i < count; i++) {
  425. // Average of left and right
  426. samples[i] = (_samples[i*2] + _samples[i*2 + 1]) / 2.0f;
  427. }
  428. return read / 2;
  429. }
  430. default:
  431. error_noexit("Only mono and stereo audio files are supported\n");
  432. return 0;
  433. }
  434. }
  435. // Write a line with very basic equalization
  436. static void write_line(writer_t *png, float *row) {
  437. float min = FLT_MAX;
  438. float max = FLT_MIN;
  439. for (int i = 0; i < APT_IMG_WIDTH; i++) {
  440. if (row[i] < min) min = row[i];
  441. if (row[i] > max) max = row[i];
  442. }
  443. png_byte pixels[APT_IMG_WIDTH];
  444. for (int i = 0; i < APT_IMG_WIDTH; i++) {
  445. pixels[i] = clamp_int(roundf((row[i]-min) / (max-min) * 255.0f), 0, 255);
  446. }
  447. #pragma GCC diagnostic push
  448. #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
  449. png_write_row(png->png, pixels);
  450. #pragma GCC diagnostic pop
  451. }
  452. apt_image_t strip(apt_image_t img) {
  453. uint8_t *data = (uint8_t *)malloc(img.rows * APT_IMG_WIDTH);
  454. for (size_t y = 0; y < img.rows; y++) {
  455. memcpy(&data[y*APT_IMG_WIDTH], &img.data[y*APT_IMG_WIDTH + APT_CHA_OFFSET], APT_CH_WIDTH);
  456. memcpy(&data[y*APT_IMG_WIDTH + APT_CH_WIDTH], &img.data[y*APT_IMG_WIDTH + APT_CHB_OFFSET], APT_CH_WIDTH);
  457. }
  458. img.data = data;
  459. return img;
  460. }
  461. int array_contains(char **array, char *value, size_t n) {
  462. for (size_t i = 0; i < n; i++) {
  463. if (strcmp(array[i], value) == 0) {
  464. return 1;
  465. }
  466. }
  467. return 0;
  468. }