Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 

492 рядки
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 15/18/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. free(data);
  214. if (error) {
  215. error_noexit("Normalization failed");
  216. return 0;
  217. }
  218. // clang-format off
  219. const char *channel_name[] = { "?", "1", "2", "3A", "4", "5", "3B" };
  220. const char *channel_desc[] = {
  221. "unknown",
  222. "visible (0.58-0.68 um)",
  223. "near-infrared (0.725-1.0 um)",
  224. "near-infrared (1.58-1.64 um)",
  225. "thermal-infrared (10.3-11.3 um)",
  226. "thermal-infrared (11.5-12.5 um)",
  227. "mid-infrared (3.55-3.93 um)"
  228. };
  229. printf("Channel A: %s - %s\n", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  230. printf("Channel B: %s - %s\n", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  231. // clang-format on
  232. STRING_SPLIT(opts->type, images, ",", 10)
  233. STRING_SPLIT(opts->effects, effects, ",", 10)
  234. STRING_SPLIT(opts->filename, filenames, ",", 10)
  235. for (size_t i = 0; i < effects_len; i++) {
  236. if (strcmp(effects[i], "crop") == 0) {
  237. apt_crop(&img);
  238. } else if (strcmp(effects[i], "denoise") == 0) {
  239. apt_denoise(&img, APT_REGION_CHA);
  240. apt_denoise(&img, APT_REGION_CHB);
  241. } else if (strcmp(effects[i], "flip") == 0) {
  242. apt_flip(&img, APT_REGION_CHA);
  243. apt_flip(&img, APT_REGION_CHB);
  244. }
  245. }
  246. for (size_t i = 0; i < images_len; i++) {
  247. const char *base;
  248. if (i < filenames_len) {
  249. base = filenames[i];
  250. } else {
  251. base = name;
  252. }
  253. if (strcmp(images[i], "thermal") == 0) {
  254. if (img.ch[1] >= 4) {
  255. char filename[269];
  256. sprintf(filename, "%s-thermal.png", base);
  257. char description[128];
  258. sprintf(description, "Calibrated thermal image, channel %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  259. // Perform visible calibration
  260. apt_image_t _img = apt_image_clone(img);
  261. apt_calibrate_thermal(&_img, APT_REGION_CHA);
  262. writer_t *writer = writer_init(filename, APT_REGION_CHB, img.rows, PNG_COLOR_TYPE_RGB, description);
  263. writer_write_image_gradient(writer, &_img, temperature_gradient);
  264. writer_free(writer);
  265. free(_img.data);
  266. } else {
  267. error_noexit("Could not generate thermal image, no infrared channel");
  268. }
  269. } else if (strcmp(images[i], "visible") == 0) {
  270. if (img.ch[0] <= 2) {
  271. char filename[269];
  272. sprintf(filename, "%s-visible.png", base);
  273. char description[128];
  274. sprintf(description, "Calibrated visible image, channel %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  275. // Perform visible calibration
  276. apt_image_t _img = apt_image_clone(img);
  277. apt_calibrate_visible(&_img, APT_REGION_CHA);
  278. writer_t *writer = writer_init(filename, APT_REGION_CHA, img.rows, PNG_COLOR_TYPE_GRAY, description);
  279. writer_write_image(writer, &_img);
  280. writer_free(writer);
  281. free(_img.data);
  282. } else {
  283. error_noexit("Could not generate visible image, no visible channel");
  284. }
  285. }
  286. }
  287. for (size_t i = 0; i < effects_len; i++) {
  288. if (strcmp(effects[i], "stretch") == 0) {
  289. apt_stretch(&img, APT_REGION_CHA);
  290. apt_stretch(&img, APT_REGION_CHB);
  291. } else if (strcmp(effects[i], "equalize") == 0) {
  292. apt_equalize(&img, APT_REGION_CHA);
  293. apt_equalize(&img, APT_REGION_CHB);
  294. }
  295. }
  296. for (size_t i = 0; i < images_len; i++) {
  297. const char *base;
  298. if (i < filenames_len) {
  299. base = filenames[i];
  300. } else {
  301. base = name;
  302. }
  303. if (strcmp(images[i], "raw") == 0) {
  304. char filename[269];
  305. sprintf(filename, "%s-raw.png", base);
  306. char description[128];
  307. sprintf(description,
  308. "Raw image, channel %s - %s / %s - %s",
  309. channel_name[img.ch[0]],
  310. channel_desc[img.ch[0]],
  311. channel_name[img.ch[1]],
  312. channel_desc[img.ch[1]]
  313. );
  314. writer_t *writer = writer_init(filename, APT_REGION_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  315. writer_write_image(writer, &img);
  316. writer_free(writer);
  317. } else if (strcmp(images[i], "lut") == 0) {
  318. if (opts->lut != NULL && opts->lut[0] != '\0') {
  319. char filename[269];
  320. sprintf(filename, "%s-lut.png", base);
  321. char description[128];
  322. sprintf(description,
  323. "LUT image, channel %s - %s / %s - %s",
  324. channel_name[img.ch[0]],
  325. channel_desc[img.ch[0]],
  326. channel_name[img.ch[1]],
  327. channel_desc[img.ch[1]]
  328. );
  329. png_colorp lut = (png_colorp)malloc(sizeof(png_color)*256*256);
  330. if (read_lut(opts->lut, lut)) {
  331. writer_t *writer = writer_init(filename, APT_REGION_CHA, img.rows, PNG_COLOR_TYPE_RGB, description);
  332. writer_write_image_lut(writer, &img, lut);
  333. writer_free(writer);
  334. }
  335. free(lut);
  336. } else {
  337. warning("Cannot create LUT image, missing -l/--lut");
  338. }
  339. } else if (strcmp(images[i], "a") == 0) {
  340. char filename[269];
  341. sprintf(filename, "%s-a.png", base);
  342. char description[128];
  343. sprintf(description, "Channel A: %s - %s", channel_name[img.ch[0]], channel_desc[img.ch[0]]);
  344. writer_t *writer = writer_init(filename, APT_REGION_CHA_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  345. writer_write_image(writer, &img);
  346. writer_free(writer);
  347. } else if (strcmp(images[i], "b") == 0) {
  348. char filename[269];
  349. sprintf(filename, "%s-b.png", base);
  350. char description[128];
  351. sprintf(description, "Channel B: %s - %s", channel_name[img.ch[1]], channel_desc[img.ch[1]]);
  352. writer_t *writer = writer_init(filename, APT_REGION_CHB_FULL, img.rows, PNG_COLOR_TYPE_GRAY, description);
  353. writer_write_image(writer, &img);
  354. writer_free(writer);
  355. }
  356. }
  357. free(img.data);
  358. return 1;
  359. }
  360. static int freq_from_filename(const char *filename) {
  361. char frequency_text[10] = {'\0'};
  362. if (strlen(filename) >= 30+4 && strncmp(filename, "gqrx_", 5) == 0) {
  363. memcpy(frequency_text, &filename[21], 9);
  364. return atoi(frequency_text);
  365. } else if (strlen(filename) == 40+4 && strncmp(filename, "SDRSharp_", 9) == 0) {
  366. memcpy(frequency_text, &filename[26], 9);
  367. return atoi(frequency_text);
  368. } else if (strlen(filename) == 37+4 && strncmp(filename, "audio_", 6) == 0) {
  369. memcpy(frequency_text, &filename[6], 9);
  370. return atoi(frequency_text);
  371. }
  372. return 0;
  373. }
  374. static int satid_from_freq(int freq) {
  375. int differences[3] = {
  376. abs(freq - 137620000), // NOAA-15
  377. abs(freq - 137912500), // NOAA-18
  378. abs(freq - 137100000) // NOAA-19
  379. };
  380. int best = 0;
  381. for (size_t i = 0; i < 3; i++) {
  382. if (differences[i] < differences[best]) {
  383. best = i;
  384. }
  385. }
  386. const int lut[3] = {15, 18, 19};
  387. return lut[best];
  388. }
  389. // Read samples from a SNDFILE_t instance (passed through context)
  390. static size_t callback(float *samples, size_t count, void *context) {
  391. SNDFILE_t *file = (SNDFILE_t *)context;
  392. switch (file->info.channels) {
  393. case 1:
  394. return sf_read_float(file->file, samples, count);
  395. case 2: {
  396. float _samples[APTDEC_BUFFER_SIZE * 2];
  397. size_t read = sf_read_float(file->file, _samples, count * 2);
  398. for (size_t i = 0; i < count; i++) {
  399. // Average of left and right
  400. samples[i] = (_samples[i*2] + _samples[i*2 + 1]) / 2.0f;
  401. }
  402. return read / 2;
  403. }
  404. default:
  405. error_noexit("Only mono and stereo audio files are supported\n");
  406. return 0;
  407. }
  408. }
  409. // Write a line with very basic equalization
  410. static void write_line(writer_t *png, float *row) {
  411. float min = FLT_MAX;
  412. float max = FLT_MIN;
  413. for (int i = 0; i < APT_IMG_WIDTH; i++) {
  414. if (row[i] < min) min = row[i];
  415. if (row[i] > max) max = row[i];
  416. }
  417. png_byte pixels[APT_IMG_WIDTH];
  418. for (int i = 0; i < APT_IMG_WIDTH; i++) {
  419. pixels[i] = clamp_int(roundf((row[i]-min) / (max-min) * 255.0f), 0, 255);
  420. }
  421. #pragma GCC diagnostic push
  422. #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
  423. png_write_row(png->png, pixels);
  424. #pragma GCC diagnostic pop
  425. }