LOST  0.0.1
LOST: Open-source Star Tracker
io.cpp
Go to the documentation of this file.
1 #include "io.hpp"
2 
3 #include <cairo/cairo.h>
4 #include <stdio.h>
5 #include <inttypes.h>
6 #include <math.h>
7 #include <errno.h>
8 #include <assert.h>
9 #include <stdlib.h>
10 #include <limits.h>
11 #include <unistd.h>
12 
13 #include <vector>
14 #include <string>
15 #include <fstream>
16 #include <iostream>
17 #include <memory>
18 #include <cstring>
19 #include <random>
20 #include <algorithm>
21 #include <map>
22 #include <chrono>
23 
24 #include "attitude-estimators.hpp"
25 #include "attitude-utils.hpp"
26 #include "databases.hpp"
27 #include "decimal.hpp"
28 #include "star-id.hpp"
29 #include "star-utils.hpp"
30 
31 namespace lost {
32 
34 UserSpecifiedOutputStream::UserSpecifiedOutputStream(std::string filePath, bool isBinary) {
35  if (isBinary && isatty(fileno(stdout)) && (filePath == "stdout" || filePath == "-")) {
36  std::cerr << "WARNING: output contains binary contents. Not printed to terminal." << std::endl;
37  filePath = "/dev/null";
38  }
39 
40  if (filePath == "stdout" || filePath == "-") {
41  stream = &std::cout;
42  isFstream = false;
43  } else {
44  std::fstream *fs = new std::fstream();
45  fs->open(filePath, std::fstream::out);
46  stream = fs;
47  isFstream = true;
48  }
49 }
50 
52  if (isFstream) {
53  delete stream;
54  }
55 }
56 
58 std::vector<CatalogStar> BscParse(std::string tsvPath) {
59  std::vector<CatalogStar> result;
60  FILE *file;
61  decimal raj2000, dej2000;
62  int magnitudeHigh, magnitudeLow, name;
63  char weird;
64 
65  file = fopen(tsvPath.c_str(), "r");
66 
67  if (file == NULL) {
68  printf("Error opening file: %s\n", strerror(errno));
69  exit(1); // TODO: do we want any other error handling?
70  return result;
71  }
72 
73  #ifdef LOST_FLOAT_MODE
74  std::string format = "%f|%f|%d|%c|%d.%d";
75  #else
76  std::string format = "%lf|%lf|%d|%c|%d.%d";
77  #endif
78 
79  while (EOF != fscanf(file, format.c_str(),
80  &raj2000, &dej2000,
81  &name, &weird,
82  &magnitudeHigh, &magnitudeLow)) {
83  result.push_back(CatalogStar(DegToRad(raj2000),
84  DegToRad(dej2000),
85  magnitudeHigh*100 + (magnitudeHigh < 0 ? -magnitudeLow : magnitudeLow),
86  name));
87  }
88 
89  fclose(file);
90  assert(result.size() > 9000); // basic sanity check
91  return result;
92 }
93 
94 #ifndef DEFAULT_BSC_PATH
95 #define DEFAULT_BSC_PATH "bright-star-catalog.tsv"
96 #endif
97 
99 const Catalog &CatalogRead() {
100  static bool readYet = false;
101  static std::vector<CatalogStar> catalog;
102 
103  if (!readYet) {
104  readYet = true;
105  char *tsvPath = getenv("LOST_BSC_PATH");
106  catalog = BscParse(tsvPath ? tsvPath : DEFAULT_BSC_PATH);
107  // perform essential narrowing
108  // remove all stars with exactly the same position as another, keeping the one with brighter magnitude
109  std::sort(catalog.begin(), catalog.end(), [](const CatalogStar &a, const CatalogStar &b) {
110  return a.spatial.x < b.spatial.x;
111  });
112  for (int i = catalog.size()-1; i > 0; i--) {
113  if ((catalog[i].spatial - catalog[i-1].spatial).Magnitude() < DECIMAL(5e-5)) { // 70 stars removed at this threshold.
114  if (catalog[i].magnitude > catalog[i-1].magnitude) {
115  catalog.erase(catalog.begin() + i);
116  } else {
117  catalog.erase(catalog.begin() + i - 1);
118  }
119  }
120  }
121  }
122  return catalog;
123 }
124 
127 unsigned char *SurfaceToGrayscaleImage(cairo_surface_t *cairoSurface) {
128  int width, height;
129  unsigned char *result;
130  uint32_t *cairoImage, pixel;
131 
132  if (cairo_image_surface_get_format(cairoSurface) != CAIRO_FORMAT_ARGB32 &&
133  cairo_image_surface_get_format(cairoSurface) != CAIRO_FORMAT_RGB24) {
134  puts("Can't convert weird image formats to grayscale.");
135  return NULL;
136  }
137 
138  width = cairo_image_surface_get_width(cairoSurface);
139  height = cairo_image_surface_get_height(cairoSurface);
140 
141  result = new unsigned char[width*height];
142  cairoImage = (uint32_t *)cairo_image_surface_get_data(cairoSurface);
143 
144  for (int i = 0; i < height * width; i++) {
145  pixel = cairoImage[i];
146  // use "luminosity" method of grayscaling
147  result[i] = round(
148  (pixel>>16 &0xFF) *0.21 +
149  (pixel>>8 &0xFF) *0.71 +
150  (pixel &0xFF) *0.07);
151  }
152 
153  return result;
154 }
155 
156 cairo_surface_t *GrayscaleImageToSurface(const unsigned char *image,
157  const int width, const int height) {
158  // cairo's 8-bit type isn't BW, it's an alpha channel only, which would look a bit weird lol
159  cairo_surface_t *result = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
160  uint32_t *resultData = (uint32_t *)cairo_image_surface_get_data(result);
161  // hopefully unnecessary
162  cairo_surface_flush(result);
163  for (long i = 0; i < width * height; i++) {
164  // equal r, g, and b components
165  resultData[i] = (image[i] << 16) + (image[i] << 8) + (image[i]);
166  }
167  cairo_surface_mark_dirty(result);
168  return result;
169 }
170 
178 void SurfacePlot(std::string description,
179  cairo_surface_t *cairoSurface,
180  const Stars &stars,
181  const StarIdentifiers *starIds,
182  const Catalog *catalog,
183  const Attitude *attitude,
184  double red,
185  double green,
186  double blue,
187  double alpha,
188  // if true, don't use catalog name
189  bool rawStarIndexes = false) {
190  cairo_t *cairoCtx;
191  std::string metadata = description + " ";
192 
193  cairoCtx = cairo_create(cairoSurface);
194  cairo_set_source_rgba(cairoCtx, red, green, blue, alpha);
195  cairo_set_line_width(cairoCtx, 1.0);
196  cairo_set_antialias(cairoCtx, CAIRO_ANTIALIAS_NONE);
197  cairo_font_options_t *cairoFontOptions = cairo_font_options_create();
198  cairo_font_options_set_antialias(cairoFontOptions, CAIRO_ANTIALIAS_NONE);
199  cairo_set_font_options(cairoCtx, cairoFontOptions);
200  cairo_text_extents_t cairoTextExtents;
201  cairo_text_extents(cairoCtx, "1234567890", &cairoTextExtents);
202  decimal textHeight = cairoTextExtents.height;
203 
204  for (const Star &centroid : stars) {
205  // plot the box around the star
206  if (centroid.radiusX > DECIMAL(0.0)) {
207  decimal radiusX = centroid.radiusX;
208  decimal radiusY = centroid.radiusY > DECIMAL(0.0) ?
209  centroid.radiusY : radiusX;
210 
211  // Rectangles should be entirely /outside/ the radius of the star, so the star is
212  // fully visible.
213  cairo_rectangle(cairoCtx,
214  centroid.position.x - radiusX,
215  centroid.position.y - radiusY,
216  radiusX * 2,
217  radiusY * 2);
218  cairo_stroke(cairoCtx);
219  } else {
220  cairo_rectangle(cairoCtx,
221  DECIMAL_FLOOR(centroid.position.x),
222  DECIMAL_FLOOR(centroid.position.y),
223  1, 1);
224  cairo_fill(cairoCtx);
225  }
226  }
227 
228  metadata += std::to_string(stars.size()) + " centroids ";
229 
230  if (starIds != NULL) {
231  assert(catalog != NULL);
232 
233  for (const StarIdentifier &starId : *starIds) {
234  const Star &centroid = stars[starId.starIndex];
235  cairo_move_to(cairoCtx,
236 
237  centroid.radiusX > DECIMAL(0.0)
238  ? centroid.position.x + centroid.radiusX + 3
239  : centroid.position.x + 8,
240 
241  centroid.radiusY > DECIMAL(0.0)
242  ? centroid.position.y - centroid.radiusY + textHeight
243  : centroid.position.y + 10);
244 
245  int plotName = rawStarIndexes ? starId.catalogIndex : (*catalog)[starId.catalogIndex].name;
246  cairo_show_text(cairoCtx, std::to_string(plotName).c_str());
247  }
248  metadata += std::to_string(starIds->size()) + " identified ";
249  }
250 
251  if (attitude != NULL) {
252  EulerAngles spherical = attitude->ToSpherical();
253  metadata +=
254  "RA: " + std::to_string(RadToDeg(spherical.ra)) + " " +
255  "DE: " + std::to_string(RadToDeg(spherical.de)) + " " +
256  "Roll: " + std::to_string(RadToDeg(spherical.roll)) + " ";
257  }
258 
259  // plot metadata
260  cairo_move_to(cairoCtx, 3, 3 + textHeight);
261  cairo_show_text(cairoCtx, metadata.c_str());
262 
263  cairo_font_options_destroy(cairoFontOptions);
264  cairo_destroy(cairoCtx);
265 }
266 
267 // ALGORITHM PROMPTERS
268 typedef CentroidAlgorithm *(*CentroidAlgorithmFactory)();
269 typedef StarIdAlgorithm *(*StarIdAlgorithmFactory)();
270 typedef AttitudeEstimationAlgorithm *(*AttitudeEstimationAlgorithmFactory)();
271 
272 typedef StarIdAlgorithm *(*StarIdAlgorithmFactory)();
273 
274 typedef AttitudeEstimationAlgorithm *(*AttitudeEstimationAlgorithmFactory)();
275 
277  return SerializeContext(values.swapIntegerEndianness, values.swapDecimalEndianness);
278 }
279 
281  MultiDatabaseDescriptor dbEntries;
282 
283  SerializeContext catalogSer = serFromDbValues(values);
284  // TODO decide why we have this inclMagnitude and inclName and if we should change that
285  SerializeCatalog(&catalogSer, catalog, false, true);
286  dbEntries.emplace_back(kCatalogMagicValue, catalogSer.buffer);
287 
288  if (values.kvector) {
289  decimal minDistance = DegToRad(values.kvectorMinDistance);
290  decimal maxDistance = DegToRad(values.kvectorMaxDistance);
291  long numBins = values.kvectorNumDistanceBins;
292  SerializeContext ser = serFromDbValues(values);
293  SerializePairDistanceKVector(&ser, catalog, minDistance, maxDistance, numBins);
294  dbEntries.emplace_back(PairDistanceKVectorDatabase::kMagicValue, ser.buffer);
295  } else {
296  std::cerr << "No database builder selected -- no database generated." << std::endl;
297  exit(1);
298  }
299 
300  return dbEntries;
301 }
302 
304 std::ostream &operator<<(std::ostream &os, const Camera &camera) {
305  os << "camera_focal_length " << camera.FocalLength() << std::endl
306  << "camera_fov " << camera.Fov() << std::endl
307  << "camera_resolution_x " << camera.XResolution() << std::endl
308  << "camera_resolution_y " << camera.YResolution() << std::endl;
309  // TODO: principal point
310  return os;
311 }
312 
313 // PIPELINE INPUT STUFF
314 
319 decimal FocalLengthFromOptions(const PipelineOptions &values, int xResolution) {
320  if ((values.pixelSize != -1) ^ (values.focalLength != 0)) {
321  std::cerr << "ERROR: Exactly one of --pixel-size or --focal-length were set." << std::endl;
322  exit(1);
323  }
324 
325  // no surefire way to see if the fov was set on command line, so we just check if it was changed from default.
326  if (values.pixelSize != -1 && values.fov != 20) {
327  std::cerr << "ERROR: Both focal length and FOV were provided. We only need one of the two methods (pixel size + focal length, or fov) to determine fov, please only provide one." << std::endl;
328  exit(1);
329  }
330 
331  if (values.pixelSize == -1) {
332  return FovToFocalLength(DegToRad(values.fov), xResolution);
333  } else {
334  return values.focalLength * 1000 / values.pixelSize;
335  }
336 }
337 
340 cairo_surface_t *PipelineInput::InputImageSurface() const {
341  const Image *inputImage = InputImage();
342  return GrayscaleImageToSurface(inputImage->image, inputImage->width, inputImage->height);
343 }
344 
345 // /**
346 // * Pipeline input for an image that's already been identified using Astrometry.net.
347 // * @todo Not implemented yet.
348 // */
349 // class AstrometryPipelineInput : public PipelineInput {
350 // public:
351 // explicit AstrometryPipelineInput(const std::string &path);
352 
353 // const Image *InputImage() const { return &image; };
354 // const Attitude *InputAttitude() const { return &attitude; };
355 // private:
356 // Image image;
357 // Attitude attitude;
358 // };
359 
366 PngPipelineInput::PngPipelineInput(cairo_surface_t *cairoSurface, Camera camera, const Catalog &catalog)
367  : camera(camera), catalog(catalog) {
368 
369  image.image = SurfaceToGrayscaleImage(cairoSurface);
370  image.width = cairo_image_surface_get_width(cairoSurface);
371  image.height = cairo_image_surface_get_height(cairoSurface);
372 }
373 
375  delete[] image.image;
376 }
377 
380  // I'm not sure why, but i can't get an initializer list to work here. Probably something to do
381  // with copying unique ptrs
382  PipelineInputList result;
383  cairo_surface_t *cairoSurface = NULL;
384  std::string pngPath = values.png;
385 
386  cairoSurface = cairo_image_surface_create_from_png(pngPath.c_str());
387  std::cerr << "PNG Read status: " << cairo_status_to_string(cairo_surface_status(cairoSurface)) << std::endl;
388  if (cairoSurface == NULL || cairo_surface_status(cairoSurface) != CAIRO_STATUS_SUCCESS) {
389  exit(1);
390  }
391 
392  int xResolution = cairo_image_surface_get_width(cairoSurface);
393  int yResolution = cairo_image_surface_get_height(cairoSurface);
394  decimal focalLengthPixels = FocalLengthFromOptions(values, xResolution);
395  Camera cam = Camera(focalLengthPixels, xResolution, yResolution);
396 
397  result.push_back(std::unique_ptr<PipelineInput>(new PngPipelineInput(cairoSurface, cam, CatalogRead())));
398  cairo_surface_destroy(cairoSurface);
399  return result;
400 }
401 
402 // AstrometryPipelineInput::AstrometryPipelineInput(const std::string &path) {
403 // // create from path, TODO
404 // }
405 
406 // PipelineInputList PromptAstrometryPipelineInput() {
407 // // TODO: why does it let us do a reference to an ephemeral return value?
408 // std::string path = Prompt<std::string>("Astrometry download directory");
409 // PipelineInputList result;
410 // result.push_back(std::unique_ptr<PipelineInput>(new AstrometryPipelineInput(path)));
411 // return result;
412 // }
413 
415 class GeneratedStar : public Star {
416 public:
417  GeneratedStar(Star star, decimal peakBrightness, Vec2 motionBlurDelta)
418  : Star(star), peakBrightness(peakBrightness), delta(motionBlurDelta) { };
419 
422 
425 };
426 
427 // In the equations for pixel brightness both with motion blur enabled and disabled, we don't need
428 // any constant scaling factor outside the integral because when d0=0, the brightness at the center
429 // will be zero without any scaling. The scaling factor you usually see on a Normal distribution is
430 // so that the Normal distribution integrates to one over the real line, making it a probability
431 // distribution. But we want the /peak/ to be one, not the integral. motion blur enabled
432 
443 static decimal MotionBlurredPixelBrightness(const Vec2 &pixel, const GeneratedStar &generatedStar,
444  decimal t, decimal stddev) {
445  const Vec2 &p0 = generatedStar.position;
446  const Vec2 &delta = generatedStar.delta;
447  const Vec2 d0 = p0 - pixel;
448  return generatedStar.peakBrightness
449  * stddev*DECIMAL_SQRT(DECIMAL_M_PI) / (DECIMAL_SQRT(2)*delta.Magnitude())
450  * DECIMAL_EXP(DECIMAL_POW(d0.x*delta.x + d0.y*delta.y, 2) / (2*stddev*stddev*delta.MagnitudeSq())
451  - d0.MagnitudeSq() / (2*stddev*stddev))
452  * DECIMAL_ERF((t*delta.MagnitudeSq() + d0.x*delta.x + d0.y*delta.y) / (stddev*DECIMAL_SQRT(2)*delta.Magnitude()));
453 }
454 
456 static decimal StaticPixelBrightness(const Vec2 &pixel, const GeneratedStar &generatedStar,
457  decimal t, decimal stddev) {
458  const Vec2 d0 = generatedStar.position - pixel;
459  return generatedStar.peakBrightness * t * DECIMAL_EXP(-d0.MagnitudeSq() / (2 * stddev * stddev));
460 }
461 
471 static decimal CentroidImagingProbability(decimal mag, decimal cutoffMag) {
472  decimal brightness = MagToBrightness(mag);
473  decimal cutoffBrightness = MagToBrightness(cutoffMag);
474  decimal stddev = cutoffBrightness/DECIMAL(5.0);
475  // CDF of Normal distribution with given mean and stddev
476  return 1 - (DECIMAL(0.5) * (1 + DECIMAL_ERF((cutoffBrightness-brightness)/(stddev*DECIMAL_SQRT(2.0)))));
477 }
478 
479 const int kMaxBrightness = 255;
480 
486  Attitude attitude,
487  Camera camera,
488  std::default_random_engine *rng,
489 
490  bool centroidsOnly,
491  decimal zeroMagTotalPhotons,
492  decimal starSpreadStdDev,
493  decimal saturationPhotons,
494  decimal darkCurrent,
495  decimal readNoiseStdDev,
496  Attitude motionBlurDirection, // applied on top of the attitude
497  decimal exposureTime,
498  decimal readoutTime, // zero for no rolling shutter
499  bool shotNoise,
500  int oversampling,
501  int numFalseStars,
502  int falseStarMinMagnitude,
503  int falseStarMaxMagnitude,
504  int cutoffMag,
505  decimal perturbationStddev)
506  : camera(camera), attitude(attitude), catalog(catalog) {
507 
508  assert(falseStarMaxMagnitude <= falseStarMinMagnitude);
509  assert(perturbationStddev >= DECIMAL(0.0));
510 
511  image.width = camera.XResolution();
512  image.height = camera.YResolution();
513  // number of true photons each pixel receives.
514 
515  assert(oversampling >= 1);
516  int oversamplingPerAxis = DECIMAL_CEIL(DECIMAL_SQRT(oversampling));
517  if (oversamplingPerAxis*oversamplingPerAxis != oversampling) {
518  std::cerr << "WARNING: oversampling was not a perfect square. Rounding up to "
519  << oversamplingPerAxis*oversamplingPerAxis << "." << std::endl;
520  }
521  assert(exposureTime > 0);
522  bool motionBlurEnabled = abs(motionBlurDirection.GetQuaternion().Angle()) > DECIMAL(0.001);
523  Quaternion motionBlurDirectionQ = motionBlurDirection.GetQuaternion();
524  // attitude at the middle of exposure time
525  Quaternion currentAttitude = attitude.GetQuaternion();
526  // attitude 1 time unit after middle of exposure
527  Quaternion futureAttitude = motionBlurDirectionQ*currentAttitude;
528  std::vector<GeneratedStar> generatedStars;
529 
530  // a star with 1 photon has peak density 1/(2pi sigma^2), because 2d gaussian formula. Then just
531  // multiply up proportionally!
532  decimal zeroMagPeakPhotonDensity = zeroMagTotalPhotons / (2*DECIMAL_M_PI * starSpreadStdDev*starSpreadStdDev);
533 
534  // TODO: Is it 100% correct to just copy the standard deviation in both dimensions?
535  std::normal_distribution<decimal> perturbation1DDistribution(DECIMAL(0.0), perturbationStddev);
536 
537  Catalog catalogWithFalse = catalog;
538 
539  std::uniform_real_distribution<decimal> uniformDistribution(DECIMAL(0.0), DECIMAL(1.0));
540  std::uniform_int_distribution<int> magnitudeDistribution(falseStarMaxMagnitude, falseStarMinMagnitude);
541  for (int i = 0; i < numFalseStars; i++) {
542  decimal ra = uniformDistribution(*rng) * 2*DECIMAL_M_PI;
543  // to be uniform around sphere. Borel-Kolmogorov paradox is calling
544  decimal de = DECIMAL_ASIN(uniformDistribution(*rng)*2 - 1);
545  decimal magnitude = magnitudeDistribution(*rng);
546 
547  catalogWithFalse.push_back(CatalogStar(ra, de, magnitude, -1));
548  }
549 
550  for (int i = 0; i < (int)catalogWithFalse.size(); i++) {
551  bool isTrueStar = i < (int)catalog.size();
552 
553  const CatalogStar &catalogStar = catalogWithFalse[i];
554  Vec3 rotated = attitude.Rotate(catalogWithFalse[i].spatial);
555  if (rotated.x <= 0) {
556  continue;
557  }
558  Vec2 camCoords = camera.SpatialToCamera(rotated);
559 
560  if (camera.InSensor(camCoords)) {
561  Vec3 futureSpatial = futureAttitude.Rotate(catalogWithFalse[i].spatial);
562  Vec2 delta = camera.SpatialToCamera(futureSpatial) - camCoords;
563  if (!motionBlurEnabled) {
564  delta = {0, 0}; // avoid decimaling point funny business
565  }
566  // radiant intensity, in photons per time unit per pixel, at the center of the star.
567  decimal peakBrightnessPerTime = zeroMagPeakPhotonDensity * MagToBrightness(catalogStar.magnitude);
568  decimal interestingThreshold = DECIMAL(0.05); // we don't need to check pixels that are expected to
569  // receive this many photons or fewer.
570  // inverse of the function defining the Gaussian distribution: Find out how far from the
571  // mean we'll have to go until the number of photons is less than interestingThreshold
572  decimal radius = DECIMAL_CEIL(DECIMAL_SQRT(-DECIMAL_LOG(interestingThreshold/peakBrightnessPerTime/exposureTime)*2*DECIMAL_M_PI*starSpreadStdDev*starSpreadStdDev));
573  Star star = Star(camCoords.x, camCoords.y,
574  radius, radius,
575  // important to invert magnitude here, so that centroid magnitude becomes larger for brighter stars.
576  // It's possible to make it so that the magnitude is always positive too, but allowing weirder magnitudes helps keep star-id algos honest about their assumptions on magnitude.
577  // we don't use its magnitude anywhere else in generation; peakBrightness was already calculated.
578  -catalogStar.magnitude);
579  generatedStars.push_back(GeneratedStar(star, peakBrightnessPerTime, delta));
580 
581  // Now add the star to the input and expected lists.
582  // We do actually want to add false stars as well, because:
583  // a) A centroider isn't any worse because it picks up a false star that looks exactly like a normal star, so why should we exclude them from compare-centroids?
584  // b) We want to feed false stars into star-ids to evaluate their false-star resilience without running centroiding.
585 
586  // Add all stars to expected, cause that's how we roll
587  expectedStars.push_back(star);
588  // and provide expected identifications for all of them
589  if (isTrueStar) {
590  expectedStarIds.push_back(StarIdentifier(expectedStars.size()-1, i));
591  }
592 
593  // for input, though, add perturbation and stuff.
594  Star inputStar = star;
595  if (perturbationStddev > DECIMAL(0.0)) {
596  // clamp to within 2 standard deviations for some reason:
597  inputStar.position.x += std::max(std::min(perturbation1DDistribution(*rng), 2*perturbationStddev), -2*perturbationStddev);
598  inputStar.position.y += std::max(std::min(perturbation1DDistribution(*rng), 2*perturbationStddev), -2*perturbationStddev);
599  }
600  // If it got perturbed outside of the sensor, don't add it.
601  if (camera.InSensor(inputStar.position)
602  // and also don't add it if it's too dim.
603  && (cutoffMag >= 10000 // but always add the star if the cutoff is very high
604  || !isTrueStar // and always add the false stars
605  || std::bernoulli_distribution(CentroidImagingProbability(catalogStar.magnitude, cutoffMag))(*rng))) {
606  inputStars.push_back(inputStar);
607  if (isTrueStar) {
608  inputStarIds.push_back(StarIdentifier(inputStars.size()-1, i));
609  }
610  }
611  }
612  }
613 
614  if (centroidsOnly) {
615  // we're outta here
616  return;
617  }
618 
619  std::vector<decimal> photonsBuffer(image.width*image.height, 0);
620 
621  for (const GeneratedStar &star : generatedStars) {
622  // delta will be exactly (0,0) when motion blur disabled
623  Vec2 earliestPosition = star.position - star.delta*(exposureTime/DECIMAL(2.0) + readoutTime/DECIMAL(2.0));
624  Vec2 latestPosition = star.position + star.delta*(exposureTime/DECIMAL(2.0) + readoutTime/DECIMAL(2.0));
625  int xMin = std::max(0, (int)std::min(earliestPosition.x - star.radiusX, latestPosition.x - star.radiusX));
626  int xMax = std::min(image.width-1, (int)std::max(earliestPosition.x + star.radiusX, latestPosition.x + star.radiusX));
627  int yMin = std::max(0, (int)std::min(earliestPosition.y - star.radiusX, latestPosition.y - star.radiusX));
628  int yMax = std::min(image.height-1, (int)std::max(earliestPosition.y + star.radiusX, latestPosition.y + star.radiusX));
629 
630  // peak brightness is measured in photons per time unit per pixel, so if oversampling, we
631  // need to convert units to photons per time unit per sample
632  decimal oversamplingBrightnessFactor = oversamplingPerAxis*oversamplingPerAxis;
633 
634  // the star.x and star.y refer to the pixel whose top left corner the star should appear at
635  // (and fractional amounts are relative to the corner). When we color a pixel, we ideally
636  // would integrate the intensity of the star over that pixel, but we can make do by sampling
637  // the intensity of the star at the /center/ of the pixel, ie, star.x+.5 and star.y+.5
638  for (int xPixel = xMin; xPixel <= xMax; xPixel++) {
639  for (int yPixel = yMin; yPixel <= yMax; yPixel++) {
640  // offset of beginning & end of readout compared to beginning & end of readout for
641  // center row
642  decimal readoutOffset = readoutTime * (yPixel - image.height/DECIMAL(2.0)) / image.height;
643  decimal tStart = -exposureTime/DECIMAL(2.0) + readoutOffset;
644  decimal tEnd = exposureTime/DECIMAL(2.0) + readoutOffset;
645 
646  // loop through all samples in the current pixel
647  for (int xSample = 0; xSample < oversamplingPerAxis; xSample++) {
648  for (int ySample = 0; ySample < oversamplingPerAxis; ySample++) {
649  decimal x = xPixel + (xSample+DECIMAL(0.5))/oversamplingPerAxis;
650  decimal y = yPixel + (ySample+DECIMAL(0.5))/oversamplingPerAxis;
651 
652  decimal curPhotons;
653  if (motionBlurEnabled) {
654  curPhotons =
655  (MotionBlurredPixelBrightness({x, y}, star, tEnd, starSpreadStdDev)
656  - MotionBlurredPixelBrightness({x, y}, star, tStart, starSpreadStdDev))
657  / oversamplingBrightnessFactor;
658  } else {
659  curPhotons = StaticPixelBrightness({x, y}, star, exposureTime, starSpreadStdDev)
660  / oversamplingBrightnessFactor;
661  }
662 
663  assert(DECIMAL(0.0) <= curPhotons);
664 
665  photonsBuffer[xPixel + yPixel*image.width] += curPhotons;
666  }
667  }
668  }
669  }
670  }
671 
672  std::normal_distribution<decimal> readNoiseDist(DECIMAL(0.0), readNoiseStdDev);
673 
674  // convert from photon counts to observed pixel brightnesses, applying noise and such.
675  imageData = std::vector<unsigned char>(image.width*image.height);
676  image.image = imageData.data();
677  for (int i = 0; i < image.width * image.height; i++) {
678  decimal curBrightness = 0;
679 
680  // dark current (Constant)
681  curBrightness += darkCurrent;
682 
683  // read noise (Gaussian)
684  curBrightness += readNoiseDist(*rng);
685 
686  // shot noise (Poisson), and quantize
687  long quantizedPhotons;
688  if (shotNoise) {
689  // with GNU libstdc++, it keeps sampling from the distribution until it's within the min-max
690  // range. This is problematic if the mean is far above the max long value, because then it
691  // might have to sample many many times (and furthermore, the results won't be useful
692  // anyway)
693  decimal photons = photonsBuffer[i];
694  if (photons > DECIMAL(LONG_MAX) - DECIMAL(3.0) * DECIMAL_SQRT(LONG_MAX)) {
695  std::cout << "ERROR: One of the pixels had too many photons. Generated image would not be physically accurate, exiting." << std::endl;
696  exit(1);
697  }
698  std::poisson_distribution<long> shotNoiseDist(photonsBuffer[i]);
699  quantizedPhotons = shotNoiseDist(*rng);
700  } else {
701  quantizedPhotons = round(photonsBuffer[i]);
702  }
703  curBrightness += quantizedPhotons / saturationPhotons;
704 
705  // std::clamp not introduced until C++17, so we avoid it.
706  decimal clampedBrightness = std::max(std::min(curBrightness, DECIMAL(1.0)), DECIMAL(0.0));
707  imageData[i] = floor(clampedBrightness * kMaxBrightness); // TODO: off-by-one, 256?
708  }
709 }
710 
715 static Attitude RandomAttitude(std::default_random_engine* pReng) {
716  std::uniform_real_distribution<decimal> randomAngleDistribution(0, 1);
717 
718  // normally the ranges of the Ra and Dec are:
719  // Dec: [-90 deg, 90 deg] --> [-pi/2 rad, pi/2 rad], where negative means south
720  // of the celestial equator and positive means north
721  // Ra: [0 deg, 360 deg] --> [0 rad, 2pi rad ]
722  // Roll: [0 rad, 2 pi rad]
723 
724  decimal randomRa = 2 * DECIMAL_M_PI * randomAngleDistribution(*pReng);
725  decimal randomDec = (DECIMAL_M_PI / 2) - acos(1 - 2 * randomAngleDistribution(*pReng)); //acos returns a decimal in range [0, pi]
726  decimal randomRoll = 2 * DECIMAL_M_PI * randomAngleDistribution(*pReng);
727 
728  Attitude randAttitude = Attitude(SphericalToQuaternion(randomRa, randomDec, randomRoll));
729 
730  return randAttitude;
731 }
732 
735  // TODO: prompt for attitude, imagewidth, etc and then construct a GeneratedPipelineInput
736 
737  int seed;
738 
739  // time based seed if option specified
740  if (values.timeSeed) {
741  seed = time(0);
742  } else {
743  seed = values.generateSeed;
744  }
745 
746  std::default_random_engine attitudeRng(seed);
747  std::default_random_engine noiseRng(seed);
748 
749  // TODO: allow random angle generation?
750  Attitude attitude = Attitude(SphericalToQuaternion(DegToRad(values.generateRa),
751  DegToRad(values.generateDe),
752  DegToRad(values.generateRoll)));
753 
754  Attitude motionBlurDirection = Attitude(SphericalToQuaternion(DegToRad(values.generateBlurRa),
755  DegToRad(values.generateBlurDe),
756  DegToRad(values.generateBlurRoll)));
757  PipelineInputList result;
758 
759  decimal focalLength = FocalLengthFromOptions(values, values.generateXRes);
760 
761 
762  for (int i = 0; i < values.generate; i++) {
763 
764 
765  Attitude inputAttitude;
766  if (values.generateRandomAttitudes) {
767  inputAttitude = RandomAttitude(&attitudeRng);
768  } else {
769  inputAttitude = attitude;
770  }
771 
773  CatalogRead(),
774  inputAttitude,
775  Camera(focalLength, values.generateXRes, values.generateYRes),
776  &noiseRng,
777 
778  values.generateCentroidsOnly,
779  values.generateZeroMagPhotons,
780  values.generateSpreadStdDev,
781  values.generateSaturationPhotons,
782  values.generateDarkCurrent,
783  values.generateReadNoiseStdDev,
784  motionBlurDirection,
785  values.generateExposure,
786  values.generateReadoutTime,
787  values.generateShotNoise,
788  values.generateOversampling,
789  values.generateNumFalseStars,
790  (values.generateFalseMinMag * 100),
791  (values.generateFalseMaxMag * 100),
792  (values.generateCutoffMag * 100),
793  values.generatePerturbationStddev);
794 
795  result.push_back(std::unique_ptr<PipelineInput>(curr));
796 
797 
798  }
799 
800 
801 
802  return result;
803 }
804 
806 
809 
810  if (values.png != "") {
811  return GetPngPipelineInput(values);
812  } else {
813  return GetGeneratedPipelineInput(values);
814  }
815 }
816 
822  StarIdAlgorithm *starIdAlgorithm,
823  AttitudeEstimationAlgorithm *attitudeEstimationAlgorithm,
824  unsigned char *database)
825  : Pipeline() {
826  if (centroidAlgorithm) {
827  this->centroidAlgorithm = std::unique_ptr<CentroidAlgorithm>(centroidAlgorithm);
828  }
829  if (starIdAlgorithm) {
830  this->starIdAlgorithm = std::unique_ptr<StarIdAlgorithm>(starIdAlgorithm);
831  }
832  if (attitudeEstimationAlgorithm) {
833  this->attitudeEstimationAlgorithm = std::unique_ptr<AttitudeEstimationAlgorithm>(attitudeEstimationAlgorithm);
834  }
835  if (database) {
836  this->database = std::unique_ptr<unsigned char[]>(database);
837  }
838 }
839 
840 
843  Pipeline result;
844 
845  // TODO: more flexible or sth
846  // TODO: don't allow setting star-id until database is set, and perhaps limit the star-id
847  // choices to those compatible with the database?
848  //
849 
850  // centroid algorithm stage
851  if (values.centroidAlgo == "dummy") {
852  result.centroidAlgorithm = std::unique_ptr<CentroidAlgorithm>(new DummyCentroidAlgorithm(values.centroidDummyNumStars));
853  } else if (values.centroidAlgo == "cog") {
854  result.centroidAlgorithm = std::unique_ptr<CentroidAlgorithm>(new CenterOfGravityAlgorithm());
855  } else if (values.centroidAlgo == "iwcog") {
856  result.centroidAlgorithm = std::unique_ptr<CentroidAlgorithm>(new IterativeWeightedCenterOfGravityAlgorithm());
857  } else if (values.centroidAlgo != "") {
858  std::cout << "Illegal centroid algorithm." << std::endl;
859  exit(1);
860  }
861 
862  // centroid magnitude filter stage
863  if (values.centroidMagFilter > 0) result.centroidMinMagnitude = values.centroidMagFilter;
864  if (values.centroidFilterBrightest > 0) result.centroidMinStars = values.centroidFilterBrightest;
865 
866  // database stage
867  if (values.databasePath != "") {
868  std::fstream fs;
869  fs.open(values.databasePath, std::fstream::in | std::fstream::binary);
870  fs.seekg(0, fs.end);
871  long length = fs.tellg();
872  fs.seekg(0, fs.beg);
873  if (fs.fail()) {
874  std::cerr << "Error reading database! " << strerror(errno) << std::endl;
875  exit(1);
876  }
877  std::cerr << "Reading " << length << " bytes of database" << std::endl;
878  result.database = std::unique_ptr<unsigned char[]>(new unsigned char[length]);
879  fs.read((char *)result.database.get(), length);
880  std::cerr << "Done" << std::endl;
881  }
882 
883  if (values.idAlgo == "dummy") {
884  result.starIdAlgorithm = std::unique_ptr<StarIdAlgorithm>(new DummyStarIdAlgorithm());
885  } else if (values.idAlgo == "gv") {
886  result.starIdAlgorithm = std::unique_ptr<StarIdAlgorithm>(new GeometricVotingStarIdAlgorithm(DegToRad(values.angularTolerance)));
887  } else if (values.idAlgo == "py") {
888  result.starIdAlgorithm = std::unique_ptr<StarIdAlgorithm>(new PyramidStarIdAlgorithm(DegToRad(values.angularTolerance), values.estimatedNumFalseStars, values.maxMismatchProb, 1000));
889  } else if (values.idAlgo != "") {
890  std::cout << "Illegal id algorithm." << std::endl;
891  exit(1);
892  }
893 
894  if (values.attitudeAlgo == "dqm") {
895  result.attitudeEstimationAlgorithm = std::unique_ptr<AttitudeEstimationAlgorithm>(new DavenportQAlgorithm());
896  } else if (values.attitudeAlgo == "triad") {
897  result.attitudeEstimationAlgorithm = std::unique_ptr<AttitudeEstimationAlgorithm>(new TriadAlgorithm());
898  } else if (values.attitudeAlgo == "quest") {
899  result.attitudeEstimationAlgorithm = std::unique_ptr<AttitudeEstimationAlgorithm>(new QuestAlgorithm());
900  } else if (values.attitudeAlgo != "") {
901  std::cout << "Illegal attitude algorithm." << std::endl;
902  exit(1);
903  }
904 
905  return result;
906 }
907 
914  // Start executing the pipeline at the first stage that has both input and an algorithm. From
915  // there, execute each successive stage of the pipeline using the output of the last stage
916  // (human centipede) until there are no more stages set.
917  PipelineOutput result;
918 
919  const Image *inputImage = input.InputImage();
920  const Stars *inputStars = input.InputStars();
921  const StarIdentifiers *inputStarIds = input.InputStarIds();
922 
923  // if database is provided, that's where we get catalog from.
924  if (database) {
925  MultiDatabase multiDatabase(database.get());
926  const unsigned char *catalogBuffer = multiDatabase.SubDatabasePointer(kCatalogMagicValue);
927  if (catalogBuffer != NULL) {
928  DeserializeContext des(catalogBuffer);
929  result.catalog = DeserializeCatalog(&des, NULL, NULL);
930  } else {
931  std::cerr << "WARNING: That database does not include a catalog. Proceeding with the full catalog." << std::endl;
932  result.catalog = input.GetCatalog();
933  }
934  } else {
935  result.catalog = input.GetCatalog();
936  }
937 
938  if (centroidAlgorithm && inputImage) {
939 
940  // run centroiding, keeping track of the time it takes
941  std::chrono::time_point<std::chrono::steady_clock> start = std::chrono::steady_clock::now();
942 
943  // TODO: we should probably modify Go to just take an image argument
944  Stars unfilteredStars = centroidAlgorithm->Go(inputImage->image, inputImage->width, inputImage->height);
945 
946  std::chrono::time_point<std::chrono::steady_clock> end = std::chrono::steady_clock::now();
947  result.centroidingTimeNs = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
948 
949  // MAGNITUDE FILTERING
950  int minMagnitude = centroidMinMagnitude;
951  if (centroidMinStars > 0
952  // don't need to filter if we don't even have that many stars
953  && centroidMinStars < (int)unfilteredStars.size()) {
954 
955  Stars magSortedStars = unfilteredStars;
956  // sort descending
957  std::sort(magSortedStars.begin(), magSortedStars.end(), [](const Star &a, const Star &b) { return a.magnitude > b.magnitude; });
958  minMagnitude = std::max(minMagnitude, magSortedStars[centroidMinStars - 1].magnitude);
959  }
960  // determine the minimum magnitude according to sorted stars
961  Stars *filteredStars = new std::vector<Star>();
962  for (const Star &star : unfilteredStars) {
963  assert(star.magnitude >= 0); // catalog stars can have negative magnitude, but by our
964  // conventions, centroids shouldn't.
965  if (star.magnitude >= minMagnitude) {
966  filteredStars->push_back(star);
967  }
968  }
969  result.stars = std::unique_ptr<Stars>(filteredStars);
970  inputStars = filteredStars;
971 
972  // any starid set up to this point needs to be discarded, because it's based on input
973  // centroids instead of our new centroids.
974  inputStarIds = NULL;
975  result.starIds = NULL;
976  } else if (centroidAlgorithm) {
977  std::cerr << "ERROR: Centroid algorithm specified, but no input image to run it on." << std::endl;
978  exit(1);
979  }
980 
981  if (starIdAlgorithm && database && inputStars && input.InputCamera()) {
982  // TODO: don't copy the vector!
983  std::chrono::time_point<std::chrono::steady_clock> start = std::chrono::steady_clock::now();
984 
985  result.starIds = std::unique_ptr<StarIdentifiers>(new std::vector<StarIdentifier>(
986  starIdAlgorithm->Go(database.get(), *inputStars, result.catalog, *input.InputCamera())));
987 
988  std::chrono::time_point<std::chrono::steady_clock> end = std::chrono::steady_clock::now();
989  result.starIdTimeNs = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
990 
991  inputStarIds = result.starIds.get();
992  } else if (starIdAlgorithm) {
993  std::cerr << "ERROR: Star ID algorithm specified but cannot run because database, centroids, or camera are missing." << std::endl;
994  exit(1);
995  }
996 
997  if (attitudeEstimationAlgorithm && inputStarIds && input.InputCamera()) {
998  assert(inputStars); // ensure that starIds doesn't exist without stars
999  std::chrono::time_point<std::chrono::steady_clock> start = std::chrono::steady_clock::now();
1000 
1001  result.attitude = std::unique_ptr<Attitude>(
1002  new Attitude(attitudeEstimationAlgorithm->Go(*input.InputCamera(), *inputStars, result.catalog, *inputStarIds)));
1003 
1004  std::chrono::time_point<std::chrono::steady_clock> end = std::chrono::steady_clock::now();
1005  result.attitudeEstimationTimeNs = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
1006  } else if (attitudeEstimationAlgorithm) {
1007  std::cerr << "ERROR: Attitude estimation algorithm set, but either star IDs or camera are missing. One reason this can happen: Setting a centroid algorithm and attitude algorithm, but no star-id algorithm -- that can't work because the input star-ids won't properly correspond to the output centroids!" << std::endl;
1008  exit(1);
1009  }
1010 
1011  return result;
1012 }
1013 
1015 std::vector<PipelineOutput> Pipeline::Go(const PipelineInputList &inputs) {
1016  std::vector<PipelineOutput> result;
1017 
1018 
1019  for (const std::unique_ptr<PipelineInput> &input : inputs) {
1020  result.push_back(Go(*input));
1021  }
1022 
1023  return result;
1024 }
1025 
1027 // COMPARISON //
1029 
1035 public:
1042 
1047 
1053 
1054  // We no longer have a num missing because often generated stars have too low of a signal-to-noise ratio and the centroid algo won't pick them up.
1055 };
1056 
1061 static std::multimap<int, int> FindClosestCentroids(decimal threshold,
1062  const Stars &one,
1063  const Stars &two) {
1064  std::multimap<int, int> result;
1065 
1066  for (int i = 0; i < (int)one.size(); i++) {
1067  std::vector<std::pair<decimal, int>> closest;
1068  for (int k = 0; k < (int)two.size(); k++) {
1069  decimal currDistance = (one[i].position - two[k].position).Magnitude();
1070  if (currDistance <= threshold) {
1071  closest.emplace_back(currDistance, k);
1072  }
1073  }
1074  std::sort(closest.begin(), closest.end());
1075  for (const std::pair<decimal, int> &pair : closest) {
1076  result.emplace(i, pair.second);
1077  }
1078  }
1079 
1080  return result;
1081 }
1082 
1089  const Stars &expected,
1090  const Stars &actual) {
1091 
1092  // TODO: Somehow penalize when multiple centroids correspond to the same expected star (i.e.,
1093  // one star turned into multiple centroids). That should probably be considered an extra
1094  // centroid, but rn it isn't.
1095 
1096  CentroidComparison result;
1097  // maps from indexes in each list to the closest centroid from other list
1098  std::multimap<int, int> actualToExpected = FindClosestCentroids(threshold, actual, expected);
1099 
1100  for (int i = 0; i < (int)actualToExpected.size(); i++) {
1101  auto closest = actualToExpected.find(i);
1102  if (closest == actualToExpected.end()) {
1103  result.numExtraCentroids++;
1104  } else {
1105  result.meanError += (actual[i].position - expected[closest->second].position).Magnitude();
1106  result.numCorrectCentroids++;
1107  }
1108  }
1109  result.meanError /= result.numCorrectCentroids;
1110 
1111  return result;
1112 }
1113 
1114 CentroidComparison CentroidComparisonsCombine(std::vector<CentroidComparison> comparisons) {
1115  assert(comparisons.size() > 0);
1116 
1117  CentroidComparison result;
1118 
1119  for (const CentroidComparison &comparison : comparisons) {
1120  result.meanError += comparison.meanError;
1121  result.numCorrectCentroids += comparison.numCorrectCentroids;
1122  result.numExtraCentroids += comparison.numExtraCentroids;
1123  }
1124 
1125  result.meanError /= comparisons.size();
1126  result.numExtraCentroids /= comparisons.size();
1127  result.numCorrectCentroids /= comparisons.size();
1128 
1129  return result;
1130 }
1131 
1132 // (documentation in hpp)
1134  // use these to map indices to names for the respective lists of StarIdentifiers
1135  const Catalog &expectedCatalog, const Catalog &actualCatalog,
1136  decimal centroidThreshold,
1137  const Stars &expectedStars, const Stars &inputStars) {
1138 
1139  StarIdComparison result = {
1140  0, // correct
1141  0, // incorrect
1142  0, // total
1143  };
1144 
1145  // EXPECTED STAR IDS
1146 
1147  // map from expected star indices to expected catalog indices (basically flattening the expected star-ids)
1148  std::vector<int> expectedCatalogIndices(expectedStars.size(), -1);
1149  for (const StarIdentifier &starId : expected) {
1150  assert(0 <= starId.starIndex && starId.starIndex <= (int)expectedStars.size());
1151  assert(0 <= starId.catalogIndex && starId.catalogIndex <= (int)expectedCatalog.size());
1152  expectedCatalogIndices[starId.starIndex] = starId.catalogIndex;
1153  }
1154 
1155  // FIND NEAREST CENTROIDS
1156 
1157  std::multimap<int, int> inputToExpectedCentroids = FindClosestCentroids(centroidThreshold, inputStars, expectedStars);
1158  // std::multimap<int, int> expectedToInputCentroids = FindClosestCentroids(centroidThreshold, expectedStars, inputStars);
1159 
1160  // COMPUTE TOTAL
1161  // Count the number of expected stars with at least one input star near them
1162  for (int i = 0; i < (int)inputStars.size(); i++) {
1163  // make sure there's at least one expected star near this input star which has an identification
1164  auto closestRange = inputToExpectedCentroids.equal_range(i);
1165  bool found = false;
1166  for (auto it = closestRange.first; it != closestRange.second; it++) {
1167  if (expectedCatalogIndices[it->second] != -1) {
1168  found = true;
1169  break;
1170  }
1171  }
1172  if (found) {
1173  result.numTotal++;
1174  }
1175  }
1176 
1177  // COMPUTE CORRECT AND INCORRECT
1178 
1179  std::vector<bool> identifiedInputCentroids(inputStars.size(), false);
1180  for (const StarIdentifier &starId : actual) {
1181  // as later, there shouldn't be duplicate starIndex. This indicates a bug in the star-id algorithm, not comparison code.
1182  assert(!identifiedInputCentroids[starId.starIndex]);
1183  identifiedInputCentroids[starId.starIndex] = true;
1184  assert(0 <= starId.starIndex && starId.starIndex <= (int)inputStars.size());
1185  assert(0 <= starId.catalogIndex && starId.catalogIndex <= (int)actualCatalog.size());
1186 
1187  // Check that there's at least one expected centroid in range which agrees with your identification.
1188  auto expectedCentroidsInRange = inputToExpectedCentroids.equal_range(starId.starIndex);
1189  bool found = false;
1190  for (auto it = expectedCentroidsInRange.first; it != expectedCentroidsInRange.second; it++) {
1191  int expectedCatalogIndex = expectedCatalogIndices[it->second];
1192  if (expectedCatalogIndex != -1
1193  && expectedCatalog[expectedCatalogIndex].name == actualCatalog[starId.catalogIndex].name) {
1194 
1195  result.numCorrect++;
1196  found = true;
1197  break;
1198  }
1199  }
1200 
1201  // Either there's no expected centroid in range, or none of them agree with the identification.
1202  if (!found) {
1203  result.numIncorrect++;
1204  }
1205  }
1206 
1207  return result;
1208 }
1209 
1211 // PIPELINE OUTPUT //
1213 
1214 typedef void (*PipelineComparator)(std::ostream &os,
1215  const PipelineInputList &,
1216  const std::vector<PipelineOutput> &,
1217  const PipelineOptions &);
1218 
1220 static cairo_status_t OstreamPlotter(void *closure, const unsigned char *data, unsigned int length) {
1221  std::ostream *os = (std::ostream *)closure;
1222  os->write((const char *)data, length);
1223  return CAIRO_STATUS_SUCCESS;
1224 }
1225 
1227 static void PipelineComparatorPlotRawInput(std::ostream &os,
1228  const PipelineInputList &expected,
1229  const std::vector<PipelineOutput> &,
1230  const PipelineOptions &) {
1231 
1232  cairo_surface_t *cairoSurface = expected[0]->InputImageSurface();
1233  cairo_surface_write_to_png_stream(cairoSurface, OstreamPlotter, &os);
1234  cairo_surface_destroy(cairoSurface);
1235 }
1236 
1238 // TODO: should probably use Expected methods, not Input methods, because future PipelineInputs could add noise to the result of the Input methods.
1239 static void PipelineComparatorPlotInput(std::ostream &os,
1240  const PipelineInputList &expected,
1241  const std::vector<PipelineOutput> &,
1242  const PipelineOptions &) {
1243  cairo_surface_t *cairoSurface = expected[0]->InputImageSurface();
1244  assert(expected[0]->InputStars() != NULL);
1245  SurfacePlot("pipeline input",
1246  cairoSurface,
1247  *expected[0]->InputStars(),
1248  expected[0]->InputStarIds(),
1249  &expected[0]->GetCatalog(),
1250  expected[0]->InputAttitude(),
1251  // green
1252  0.0, 1.0, 0.0, 0.6);
1253  cairo_surface_write_to_png_stream(cairoSurface, OstreamPlotter, &os);
1254  cairo_surface_destroy(cairoSurface);
1255 }
1256 
1257 static void PipelineComparatorPlotExpected(std::ostream &os,
1258  const PipelineInputList &expected,
1259  const std::vector<PipelineOutput> &,
1260  const PipelineOptions &) {
1261  cairo_surface_t *cairoSurface = expected[0]->InputImageSurface();
1262  assert(expected[0]->ExpectedStars() != NULL);
1263  SurfacePlot("expected output",
1264  cairoSurface,
1265  *expected[0]->ExpectedStars(),
1266  expected[0]->ExpectedStarIds(),
1267  &expected[0]->GetCatalog(),
1268  expected[0]->ExpectedAttitude(),
1269  // blu
1270  0.2, 0.5, 1.0, 0.7);
1271  cairo_surface_write_to_png_stream(cairoSurface, OstreamPlotter, &os);
1272  cairo_surface_destroy(cairoSurface);
1273 }
1274 
1276 static void PipelineComparatorCentroids(std::ostream &os,
1277  const PipelineInputList &expected,
1278  const std::vector<PipelineOutput> &actual,
1279  const PipelineOptions &values) {
1280  int size = (int)expected.size();
1281 
1282  decimal threshold = values.centroidCompareThreshold;
1283 
1284  std::vector<CentroidComparison> comparisons;
1285  for (int i = 0; i < size; i++) {
1286  comparisons.push_back(CentroidsCompare(threshold,
1287  *(expected[i]->ExpectedStars()),
1288  *(actual[i].stars)));
1289  }
1290 
1291  CentroidComparison result = CentroidComparisonsCombine(comparisons);
1292  os << "centroids_num_correct " << result.numCorrectCentroids << std::endl
1293  << "centroids_num_extra " << result.numExtraCentroids << std::endl
1294  << "centroids_mean_error " << result.meanError << std::endl;
1295 }
1296 
1297 static void PrintCentroids(const std::string &prefix,
1298  std::ostream &os,
1299  const Catalog &catalog,
1300  const std::vector<Stars> &starses,
1301  // May be NULL. Should be the only the first starId, because we don't have any reasonable aggregative action to perform.
1302  const StarIdentifiers *starIds) {
1303  assert(starses.size() > 0);
1304  decimal avgNumStars = 0;
1305  for (const Stars &stars : starses) {
1306  avgNumStars += stars.size();
1307  }
1308  avgNumStars /= starses.size();
1309 
1310  os << "num_" << prefix << "_centroids " << avgNumStars << std::endl;
1311  if (starses.size() == 1) {
1312  const Stars &stars = starses[0];
1313  for (int i = 0; i < (int)stars.size(); i++) {
1314  os << prefix << "_centroid_" << i << "_x " << stars[i].position.x << std::endl;
1315  os << prefix << "_centroid_" << i << "_y " << stars[i].position.y << std::endl;
1316  if (starIds) {
1317  for (const StarIdentifier &starId : *starIds) {
1318  if (starId.starIndex == i) {
1319  os << prefix << "_centroid_" << i << "_id " << catalog[starId.catalogIndex].name << std::endl;
1320  }
1321  }
1322  }
1323  }
1324  }
1325 }
1326 
1328 static void PipelineComparatorPrintExpectedCentroids(std::ostream &os,
1329  const PipelineInputList &expected,
1330  const std::vector<PipelineOutput> &, // actual
1331  const PipelineOptions &) {
1332  assert(expected.size() > 0);
1333  assert(expected[0]->ExpectedStars());
1334 
1335  std::vector<Stars> expectedStarses;
1336  for (const auto &input : expected) {
1337  expectedStarses.push_back(*input->ExpectedStars());
1338  }
1339  PrintCentroids("expected",
1340  os,
1341  expected[0]->GetCatalog(),
1342  expectedStarses,
1343  expected[0]->ExpectedStarIds());
1344 }
1345 
1346 static void PipelineComparatorPrintInputCentroids(std::ostream &os,
1347  const PipelineInputList &expected,
1348  const std::vector<PipelineOutput> &, // actual
1349  const PipelineOptions &) {
1350  assert(expected.size() > 0);
1351  assert(expected[0]->InputStars());
1352 
1353  std::vector<Stars> inputStarses;
1354  for (const auto &input : expected) {
1355  inputStarses.push_back(*input->InputStars());
1356  }
1357  PrintCentroids("input",
1358  os,
1359  expected[0]->GetCatalog(),
1360  inputStarses,
1361  expected[0]->InputStarIds());
1362 }
1363 
1364 static void PipelineComparatorPrintActualCentroids(std::ostream &os,
1365  const PipelineInputList &expected, // expected
1366  const std::vector<PipelineOutput> &actual,
1367  const PipelineOptions &values) {
1368  assert(actual.size() > 0);
1369  assert(actual[0].stars);
1370 
1371  std::vector<Stars> actualStarses;
1372  for (const auto &output : actual) {
1373  actualStarses.push_back(*output.stars);
1374  }
1375  PrintCentroids("actual",
1376  os,
1377  actual[0].catalog,
1378  actualStarses,
1379  actual[0].starIds.get());
1380 
1381  if (expected.size() == 1 && expected[0]->ExpectedStars() && expected[0]->ExpectedStarIds()) {
1382  // also print expected ID of each one
1383  const Stars &actualStars = *actual[0].stars;
1384  const Stars &expectedStars = *expected[0]->ExpectedStars();
1385  std::multimap<int, int> actualToExpectedCentroids = FindClosestCentroids(values.centroidCompareThreshold, actualStars, expectedStars);
1386  for (int i = 0; i < (int)actualStars.size(); i++) {
1387  auto range = actualToExpectedCentroids.equal_range(i);
1388  auto it = range.first;
1389  auto end = range.second;
1390  for (int j = 0;
1391  it != end;
1392  j++, it++) {
1393 
1394  int expectedCentroidIndex = it->second;
1395  bool foundIt = false; // just to be sure
1396  for (const StarIdentifier &starId : *expected[0]->ExpectedStarIds()) {
1397  if (starId.starIndex == expectedCentroidIndex) {
1398  assert(!foundIt);
1399  int expectedName = expected[0]->GetCatalog()[starId.catalogIndex].name;
1400  std::cout << "actual_centroid_" << i << "_expected_id_" << j << " " << expectedName << std::endl;
1401  foundIt = true;
1402  }
1403  }
1404  }
1405  }
1406  }
1407 }
1408 
1412  const PipelineInputList &expected,
1413  const std::vector<PipelineOutput> &actual,
1414  const PipelineOptions &) {
1415  const Stars &stars = actual[0].stars ? *actual[0].stars : *expected[0]->InputStars();
1416  StarIdentifiers identifiers;
1417  for (int i = 0; i < (int)stars.size(); i++) {
1418  identifiers.push_back(StarIdentifier(i, i));
1419  }
1420  cairo_surface_t *cairoSurface = expected[0]->InputImageSurface();
1421  SurfacePlot("centroid indices (input)",
1422  cairoSurface,
1423  stars,
1424  &identifiers,
1425  &actual[0].catalog,
1426  NULL,
1427  // orange
1428  1.0, 0.5, 0.0, 0.5,
1429  // don't resolve names
1430  true);
1431  cairo_surface_write_to_png_stream(cairoSurface, OstreamPlotter, &os);
1432  cairo_surface_destroy(cairoSurface);
1433 }
1434 
1436 static void PipelineComparatorPlotOutput(std::ostream &os,
1437  const PipelineInputList &expected,
1438  const std::vector<PipelineOutput> &actual,
1439  const PipelineOptions &) {
1440  // don't need to worry about mutating the surface; InputImageSurface returns a fresh one
1441  cairo_surface_t *cairoSurface = expected[0]->InputImageSurface();
1442  SurfacePlot("pipeline output",
1443  cairoSurface,
1444  actual[0].stars ? *actual[0].stars : *expected[0]->InputStars(),
1445  actual[0].starIds.get(),
1446  &actual[0].catalog,
1447  actual[0].attitude.get(),
1448  // red
1449  1.0, 0.0, 0.0, 0.5);
1450  cairo_surface_write_to_png_stream(cairoSurface, OstreamPlotter, &os);
1451  cairo_surface_destroy(cairoSurface);
1452 }
1453 
1455 static void PipelineComparatorStarIds(std::ostream &os,
1456  const PipelineInputList &expected,
1457  const std::vector<PipelineOutput> &actual,
1458  const PipelineOptions &values) {
1459  int numImagesCorrect = 0;
1460  int numImagesIncorrect = 0;
1461  int numImagesTotal = expected.size();
1462  for (int i = 0; i < numImagesTotal; i++) {
1463  // since the actual star IDs exist, it must have gotten input from somewhere!
1464  // TODO: overhaul: It seems that in these comparators there should be a more fundamental way to figure out the input that was actually sent to a stage.
1465  // I.e., instead of having expected and actual arguments, have some sort of PipelineRunSummary object, where the InputStars method looks at actual, then input.
1466  assert(actual[i].stars.get() || expected[i]->InputStars());
1467 
1468  const Stars &inputStars = actual[i].stars.get()
1469  ? *actual[i].stars.get()
1470  : *expected[i]->InputStars();
1471  StarIdComparison comparison =
1472  StarIdsCompare(*expected[i]->ExpectedStarIds(), *actual[i].starIds,
1473  expected[i]->GetCatalog(), actual[i].catalog,
1474  values.centroidCompareThreshold, *expected[i]->ExpectedStars(), inputStars);
1475 
1476  if (numImagesTotal == 1) {
1477  os << "starid_num_correct " << comparison.numCorrect << std::endl;
1478  os << "starid_num_incorrect " << comparison.numIncorrect << std::endl;
1479  os << "starid_num_total " << comparison.numTotal << std::endl;
1480  }
1481 
1482  if (comparison.numCorrect > 0 && comparison.numIncorrect == 0) {
1483  numImagesCorrect++;
1484  }
1485  if (comparison.numIncorrect > 0) {
1486  numImagesIncorrect++;
1487  }
1488  }
1489 
1490  // A "correct" image is one where at least two stars are correctly id'd and none are incorrectly id'd
1491  os << "starid_num_images_correct " << numImagesCorrect << std::endl;
1492  os << "starid_num_images_incorrect " << numImagesIncorrect << std::endl;
1493 }
1494 
1495 static void PrintAttitude(std::ostream &os, const std::string &prefix, const Attitude &attitude) {
1496  if (attitude.IsKnown()) {
1497  os << prefix << "attitude_known 1" << std::endl;
1498 
1499  EulerAngles spherical = attitude.ToSpherical();
1500  os << prefix << "attitude_ra " << RadToDeg(spherical.ra) << std::endl;
1501  os << prefix << "attitude_de " << RadToDeg(spherical.de) << std::endl;
1502  os << prefix << "attitude_roll " << RadToDeg(spherical.roll) << std::endl;
1503 
1504  Quaternion q = attitude.GetQuaternion();
1505  os << prefix << "attitude_i " << q.i << std::endl;
1506  os << prefix << "attitude_j " << q.j << std::endl;
1507  os << prefix << "attitude_k " << q.k << std::endl;
1508  os << prefix << "attitude_real " << q.real << std::endl;
1509 
1510  } else {
1511  os << prefix << "attitude_known 0" << std::endl;
1512  }
1513 }
1514 
1516 static void PipelineComparatorPrintAttitude(std::ostream &os,
1517  const PipelineInputList &,
1518  const std::vector<PipelineOutput> &actual,
1519  const PipelineOptions &) {
1520  assert(actual.size() == 1);
1521  assert(actual[0].attitude);
1522  PrintAttitude(os, "", *actual[0].attitude);
1523 }
1524 
1525 static void PipelineComparatorPrintExpectedAttitude(std::ostream &os,
1526  const PipelineInputList &expected,
1527  const std::vector<PipelineOutput> &,
1528  const PipelineOptions &) {
1529  assert(expected.size() == 1);
1530  assert(expected[0]->ExpectedAttitude());
1531  PrintAttitude(os, "expected_", *expected[0]->ExpectedAttitude());
1532 }
1533 
1535 static void PipelineComparatorAttitude(std::ostream &os,
1536  const PipelineInputList &expected,
1537  const std::vector<PipelineOutput> &actual,
1538  const PipelineOptions &values) {
1539 
1540  // TODO: use Wahba loss function (maybe average per star) instead of just angle. Also break
1541  // apart roll error from boresight error. This is just quick and dirty for testing
1542 
1543  decimal angleThreshold = DegToRad(values.attitudeCompareThreshold);
1544 
1545  decimal attitudeErrorSum = 0.0f;
1546  int numCorrect = 0;
1547  int numIncorrect = 0;
1548 
1549  for (int i = 0; i < (int)expected.size(); i++) {
1550  if (actual[i].attitude->IsKnown()) {
1551  Quaternion expectedQuaternion = expected[i]->ExpectedAttitude()->GetQuaternion();
1552  Quaternion actualQuaternion = actual[i].attitude->GetQuaternion();
1553  decimal attitudeError = (expectedQuaternion * actualQuaternion.Conjugate()).SmallestAngle();
1554  assert(attitudeError >= 0);
1555 
1556  if (attitudeError <= angleThreshold) {
1557  attitudeErrorSum += attitudeError;
1558  numCorrect++;
1559  } else {
1560  numIncorrect++;
1561  }
1562  }
1563  }
1564 
1565  decimal attitudeErrorMean = DECIMAL(attitudeErrorSum) / numCorrect;
1566  decimal fractionCorrect = DECIMAL(numCorrect) / expected.size();
1567  decimal fractionIncorrect = DECIMAL(numIncorrect) / expected.size();
1568 
1569  os << "attitude_error_mean " << attitudeErrorMean << std::endl;
1570  os << "attitude_availability " << fractionCorrect << std::endl;
1571  os << "attitude_error_rate " << fractionIncorrect << std::endl;
1572 }
1573 
1574 static void PrintTimeStats(std::ostream &os, const std::string &prefix, const std::vector<long long> &times) {
1575  assert(times.size() > 0);
1576 
1577  // print average, min, max, and 95% max
1578  long long sum = 0;
1579  long long min = LONG_MAX;
1580  long long max = 0;
1581  for (int i = 0; i < (int)times.size(); i++) {
1582  assert(times[i] > 0);
1583  sum += times[i];
1584  min = std::min(min, times[i]);
1585  max = std::max(max, times[i]);
1586  }
1587  long average = sum / times.size();
1588  std::vector<long long> sortedTimes = times;
1589  std::sort(sortedTimes.begin(), sortedTimes.end());
1590  // what really is the 95th percentile? Being conservative, we want to pick a value that at least
1591  // 95% of the times are less than. This means: (1) finding the number of times, (2) Finding
1592  // Math.ceil(0.95 * numTimes), and (3) subtracting 1 to get the index.
1593  int ninetyFiveIndex = (int)std::ceil(0.95 * times.size()) - 1;
1594  assert(ninetyFiveIndex >= 0);
1595  long long ninetyFifthPercentile = sortedTimes[ninetyFiveIndex];
1596 
1597  os << prefix << "_average_ns " << average << std::endl;
1598  os << prefix << "_min_ns " << min << std::endl;
1599  os << prefix << "_max_ns " << max << std::endl;
1600  os << prefix << "_95%_ns " << ninetyFifthPercentile << std::endl;
1601 }
1602 
1604 static void PipelineComparatorPrintSpeed(std::ostream &os,
1605  const PipelineInputList &,
1606  const std::vector<PipelineOutput> &actual,
1607  const PipelineOptions &) {
1608  std::vector<long long> centroidingTimes;
1609  std::vector<long long> starIdTimes;
1610  std::vector<long long> attitudeTimes;
1611  std::vector<long long> totalTimes;
1612  for (int i = 0; i < (int)actual.size(); i++) {
1613  long long totalTime = 0;
1614  if (actual[i].centroidingTimeNs > 0) {
1615  centroidingTimes.push_back(actual[i].centroidingTimeNs);
1616  totalTime += actual[i].centroidingTimeNs;
1617  }
1618  if (actual[i].starIdTimeNs > 0) {
1619  starIdTimes.push_back(actual[i].starIdTimeNs);
1620  totalTime += actual[i].starIdTimeNs;
1621  }
1622  if (actual[i].attitudeEstimationTimeNs > 0) {
1623  attitudeTimes.push_back(actual[i].attitudeEstimationTimeNs);
1624  totalTime += actual[i].attitudeEstimationTimeNs;
1625  }
1626  totalTimes.push_back(totalTime);
1627  }
1628  if (centroidingTimes.size() > 0) {
1629  PrintTimeStats(os, "centroiding", centroidingTimes);
1630  }
1631  if (starIdTimes.size() > 0) {
1632  PrintTimeStats(os, "starid", starIdTimes);
1633  }
1634  if (attitudeTimes.size() > 0) {
1635  PrintTimeStats(os, "attitude", attitudeTimes);
1636  }
1637  if (centroidingTimes.size() > 0 || starIdTimes.size() > 0 || attitudeTimes.size() > 0) {
1638  PrintTimeStats(os, "total", totalTimes);
1639  }
1640 }
1641 
1642 // TODO: add these debug comparators back in!
1643 // void PipelineComparatorPrintPairDistance(std::ostream &os,
1644 // const PipelineInputList &expected,
1645 // const std::vector<PipelineOutput> &actual) {
1646 // int index1 = Prompt<int>("Index of first star");
1647 // int index2 = Prompt<int>("Index of second star");
1648 
1649 // const Camera &camera = *expected[0]->InputCamera();
1650 // const Stars &stars = *actual[0].stars;
1651 
1652 // assert(index1 >= 0 && index2 >= 0 && index1 < (int)stars.size() && index2 < (int)stars.size());
1653 // os << "pair_distance " << Angle(camera.CameraToSpatial(stars[index1].position),
1654 // camera.CameraToSpatial(stars[index2].position))
1655 // << std::endl;
1656 // }
1657 
1658 // void PipelineComparatorPrintPyramidDistances(std::ostream &os,
1659 // const PipelineInputList &expected,
1660 // const std::vector<PipelineOutput> &actual) {
1661 // int index1 = Prompt<int>("Catalog name/index of first star");
1662 // int index2 = Prompt<int>("Catalog name/index of second star");
1663 // int index3 = Prompt<int>("Catalog name/index of third star");
1664 // int index4 = Prompt<int>("Catalog name/index of four star");
1665 
1666 // const Camera &camera = *expected[0]->InputCamera();
1667 // const Stars &stars = *actual[0].stars;
1668 
1669 // Vec3 spatial1 = camera.CameraToSpatial(stars[index1].position);
1670 // Vec3 spatial2 = camera.CameraToSpatial(stars[index2].position);
1671 // Vec3 spatial3 = camera.CameraToSpatial(stars[index3].position);
1672 // Vec3 spatial4 = camera.CameraToSpatial(stars[index4].position);
1673 
1674 // std::cout << "pair_distance_12 " << Angle(spatial1, spatial2) << std::endl;
1675 // std::cout << "pair_distance_13 " << Angle(spatial1, spatial3) << std::endl;
1676 // std::cout << "pair_distance_14 " << Angle(spatial1, spatial4) << std::endl;
1677 // std::cout << "pair_distance_23 " << Angle(spatial2, spatial3) << std::endl;
1678 // std::cout << "pair_distance_24 " << Angle(spatial2, spatial4) << std::endl;
1679 // std::cout << "pair_distance_34 " << Angle(spatial3, spatial4) << std::endl;
1680 // }
1681 
1682 // void PipelineComparatorPrintTripleAngle(std::ostream &os,
1683 // const PipelineInputList &expected,
1684 // const std::vector<PipelineOutput> &actual) {
1685 // int index1 = Prompt<int>("Index of first star");
1686 // int index2 = Prompt<int>("Index of second star");
1687 // int index3 = Prompt<int>("Index of third star");
1688 
1689 // const Camera &camera = *expected[0]->InputCamera();
1690 // const Stars &stars = *actual[0].stars;
1691 
1692 // assert(index1 >= 0 && index1 < (int)stars.size());
1693 // assert(index2 >= 0 && index2 < (int)stars.size());
1694 // assert(index3 >= 0 && index3 < (int)stars.size());
1695 
1696 // // TODO, when merging with nondimensional branch
1697 // }
1698 
1704  const std::vector<PipelineOutput> &actual,
1705  const PipelineOptions &values) {
1706  if (actual.size() == 0) {
1707  std::cerr << "ERROR: No output! Did you specify any input images? Try --png or --generate." << std::endl;
1708  exit(1);
1709  }
1710 
1711  assert(expected.size() == actual.size() && expected.size() > 0);
1712 
1713  // TODO: Remove the asserts and print out more reasonable error messages.
1714 
1715 #define LOST_PIPELINE_COMPARE(precondition, errmsg, comparator, path, isBinary) do { \
1716  if (precondition) { \
1717  UserSpecifiedOutputStream pos(path, isBinary); \
1718  comparator(pos.Stream(), expected, actual, values); \
1719  } else { \
1720  std::cerr << "ERROR: Comparator not applicable: " << errmsg << std::endl; \
1721  exit(1); \
1722  } \
1723  } while (0)
1724 
1725  if (values.plotRawInput != "") {
1726  LOST_PIPELINE_COMPARE(expected[0]->InputImage() && expected.size() == 1,
1727  "--plot-raw-input requires exactly 1 input image, but " + std::to_string(expected.size()) + " many were provided.",
1728  PipelineComparatorPlotRawInput, values.plotRawInput, true);
1729  }
1730 
1731  if (values.plotInput != "") {
1732  LOST_PIPELINE_COMPARE(expected[0]->InputImage() && expected.size() == 1 && expected[0]->InputStars(),
1733  "--plot-input requires exactly 1 input image, and for centroids to be available on that input image. " + std::to_string(expected.size()) + " many input images were provided.",
1734  PipelineComparatorPlotInput, values.plotInput, true);
1735  }
1736  if (values.plotExpected != "") {
1737  LOST_PIPELINE_COMPARE(expected[0]->InputImage() && expected.size() == 1 && expected[0]->ExpectedStars(),
1738  "--plot-expected-input requires exactly 1 input image, and for expected centroids to be available on that input image. " + std::to_string(expected.size()) + " many input images were provided.",
1739  PipelineComparatorPlotExpected, values.plotExpected, true);
1740  }
1741  if (values.plotOutput != "") {
1742  LOST_PIPELINE_COMPARE(actual.size() == 1 && (actual[0].stars || actual[0].starIds),
1743  "--plot-output requires exactly 1 output image, and for either centroids or star IDs to be available on that output image. " + std::to_string(actual.size()) + " many output images were provided.",
1744  PipelineComparatorPlotOutput, values.plotOutput, true);
1745  }
1746  if (values.printExpectedCentroids != "") {
1747  LOST_PIPELINE_COMPARE(expected[0]->ExpectedStars(),
1748  "--print-expected-centroids requires at least 1 input with expected centroids. " + std::to_string(expected.size()) + " many input images were provided.",
1749  PipelineComparatorPrintExpectedCentroids, values.printExpectedCentroids, false);
1750  }
1751  if (values.printInputCentroids != "") {
1752  LOST_PIPELINE_COMPARE(expected[0]->InputStars(),
1753  "--print-input-centroids requires at least 1 input with centroids. " + std::to_string(expected.size()) + " many input images were provided.",
1754  PipelineComparatorPrintInputCentroids, values.printInputCentroids, false);
1755  }
1756  if (values.printActualCentroids != "") {
1757  LOST_PIPELINE_COMPARE(actual[0].stars,
1758  "--print-actual-centroids requires at least 1 output image, and for centroids to be available on the output images. " + std::to_string(actual.size()) + " many output images were provided.",
1759  PipelineComparatorPrintActualCentroids, values.printActualCentroids, false);
1760  }
1761  if (values.plotCentroidIndices != "") {
1762  LOST_PIPELINE_COMPARE(expected.size() == 1 && expected[0]->InputImage(),
1763  "--plot-centroid-indices requires exactly 1 input with image. " + std::to_string(expected.size()) + " many inputs were provided.",
1764  PipelineComparatorPlotCentroidIndices, values.plotCentroidIndices, true);
1765  }
1766  if (values.compareCentroids != "") {
1767  LOST_PIPELINE_COMPARE(actual[0].stars && expected[0]->ExpectedStars() && values.centroidCompareThreshold,
1768  "--compare-centroids requires at least 1 output image, and for expected centroids to be available on the input image. " + std::to_string(actual.size()) + " many output images were provided.",
1769  PipelineComparatorCentroids, values.compareCentroids, false);
1770  }
1771  if (values.compareStarIds != "") {
1772  LOST_PIPELINE_COMPARE(expected[0]->ExpectedStarIds() && actual[0].starIds && expected[0]->ExpectedStars(),
1773  "--compare-star-ids requires at least 1 output image, and for expected star IDs and centroids to be available on the input image. " + std::to_string(actual.size()) + " many output images were provided.",
1774  PipelineComparatorStarIds, values.compareStarIds, false);
1775  }
1776  if (values.printAttitude != "") {
1777  LOST_PIPELINE_COMPARE(actual[0].attitude && actual.size() == 1,
1778  "--print-attitude requires exactly 1 output image, and for attitude to be available on that output image. " + std::to_string(actual.size()) + " many output images were provided.",
1779  PipelineComparatorPrintAttitude, values.printAttitude, false);
1780  }
1781  if (values.printExpectedAttitude != "") {
1782  LOST_PIPELINE_COMPARE(expected[0]->ExpectedAttitude() && expected.size() == 1,
1783  "--print-expected-attitude requires exactly 1 input image, and for expected attitude to be available on that input image. " + std::to_string(expected.size()) + " many input images were provided.",
1784  PipelineComparatorPrintExpectedAttitude, values.printExpectedAttitude, false);
1785  }
1786  if (values.compareAttitudes != "") {
1787  LOST_PIPELINE_COMPARE(actual[0].attitude && expected[0]->ExpectedAttitude() && values.attitudeCompareThreshold,
1788  "--compare-attitudes requires at least 1 output image, and for expected attitude to be available on the input image. " + std::to_string(actual.size()) + " many output images were provided.",
1789  PipelineComparatorAttitude, values.compareAttitudes, false);
1790  }
1791  if (values.printSpeed != "") {
1792  LOST_PIPELINE_COMPARE(actual.size() > 0,
1793  // I don't think this should ever actually happen??
1794  "--print-speed requires at least 1 output image. " + std::to_string(actual.size()) + " many output images were provided.",
1795  PipelineComparatorPrintSpeed, values.printSpeed, false);
1796  }
1797 
1798 #undef LOST_PIPELINE_COMPARE
1799 }
1800 
1801 // TODO: Add CLI options for all the inspectors!
1802 
1803 // typedef void (*CatalogInspector)(const Catalog &);
1804 
1805 // static std::vector<const CatalogStar *> PromptCatalogStars(const Catalog &catalog, int howMany) {
1806 // std::vector<const CatalogStar *> result;
1807 // for (int i = 0; i < howMany; i++) {
1808 // int name = Prompt<int>("Catalog name of " + std::to_string(i) + "-th star");
1809 // const CatalogStar *star = findNamedStar(catalog, name);
1810 // if (star == NULL) {
1811 // std::cerr << "Star not found!" << std::endl;
1812 // exit(1);
1813 // }
1814 // result.push_back(star);
1815 // }
1816 // return result;
1817 // }
1818 
1819 // void InspectPairDistance(const Catalog &catalog) {
1820 // auto stars = PromptCatalogStars(catalog, 2);
1821 
1822 // // TODO: not cout, prompt for an ostream in inspect and pass argument
1823 // std::cout << Angle(stars[0]->spatial, stars[1]->spatial) << std::endl;
1824 // }
1825 
1826 // void InspectPyramidDistances(const Catalog &catalog) {
1827 // auto stars = PromptCatalogStars(catalog, 4);
1828 
1829 // std::cout << "pair_distance_01 " << Angle(stars[0]->spatial, stars[1]->spatial) << std::endl;
1830 // std::cout << "pair_distance_02 " << Angle(stars[0]->spatial, stars[2]->spatial) << std::endl;
1831 // std::cout << "pair_distance_03 " << Angle(stars[0]->spatial, stars[3]->spatial) << std::endl;
1832 // std::cout << "pair_distance_12 " << Angle(stars[1]->spatial, stars[2]->spatial) << std::endl;
1833 // std::cout << "pair_distance_13 " << Angle(stars[1]->spatial, stars[3]->spatial) << std::endl;
1834 // std::cout << "pair_distance_23 " << Angle(stars[2]->spatial, stars[3]->spatial) << std::endl;
1835 // }
1836 
1837 // void InspectTripleAngle(const Catalog &catalog) {
1838 // auto stars = PromptCatalogStars(catalog, 3);
1839 
1840 // // TODO
1841 // }
1842 
1843 // void InspectFindStar(const Catalog &catalog) {
1844 // std::string raStr = PromptLine("Right Ascension");
1845 
1846 // decimal raRadians;
1847 
1848 // int raHours, raMinutes;
1849 // decimal raSeconds;
1850 // int raFormatTime = sscanf(raStr.c_str(), "%dh %dm %fs", &raHours, &raMinutes, &raSeconds);
1851 
1852 // decimal raDeg;
1853 // int raFormatDeg = sscanf(raStr.c_str(), "%f", &raDeg);
1854 
1855 // if (raFormatTime == 3) {
1856 // raRadians = (raHours * 2*DECIMAL_M_PI/24) + (raMinutes * 2*DECIMAL_M_PI/24/60) + (raSeconds * 2*DECIMAL_M_PI/24/60/60);
1857 // } else if (raFormatDeg == 1) {
1858 // raRadians = DegToRad(raFormatDeg);
1859 // } else {
1860 // std::cerr << "Invalid right ascension format. Do \"09h 38m 29.8754s\" or a number of degrees." << std::endl;
1861 // exit(1);
1862 // }
1863 
1864 // std::string deStr = PromptLine("Declination");
1865 
1866 // decimal deRadians;
1867 
1868 // int deDegPart, deMinPart;
1869 // decimal deSecPart;
1870 // char dummy[8];
1871 // int deFormatParts = sscanf(deStr.c_str(), "%d%s %d%s %f%s", &deDegPart, dummy, &deMinPart, dummy, &deSecPart, dummy);
1872 
1873 // decimal deDeg;
1874 // int deFormatDeg = sscanf(deStr.c_str(), "%f", &deDeg);
1875 
1876 // if (deFormatParts == 6) {
1877 // deRadians = DegToRad(deDegPart + (decimal)deMinPart/60 + (decimal)deSecPart/60/60);
1878 // } else if (deFormatDeg == 1) {
1879 // deRadians = DegToRad(deFormatDeg);
1880 // } else {
1881 // std::cerr << "Invalid declination format." << std::endl;
1882 // exit(1);
1883 // }
1884 
1885 // // find the star
1886 
1887 // decimal tolerance = 0.001;
1888 // Vec3 userSpatial = SphericalToSpatial(raRadians, deRadians);
1889 // int i = 0;
1890 // for (const CatalogStar &curStar : catalog) {
1891 // if ((curStar.spatial - userSpatial).Magnitude() < tolerance) {
1892 // std::cout << "found_star_" << i << " " << curStar.name << std::endl;
1893 // std::cout << "fonud_star_magnitude_" << i << " " << curStar.magnitude << std::endl;
1894 // i++;
1895 // }
1896 // }
1897 // if (i == 0) {
1898 // std::cerr << "No stars found" << std::endl;
1899 // }
1900 // }
1901 
1902 // void InspectPrintStar(const Catalog &catalog) {
1903 // auto stars = PromptCatalogStars(catalog, 1);
1904 // decimal ra, de;
1905 // SpatialToSpherical(stars[0]->spatial, &ra, &de);
1906 
1907 // std::cout << "star_ra " << RadToDeg(ra) << std::endl;
1908 // std::cout << "star_de " << RadToDeg(de) << std::endl;
1909 // }
1910 
1911 // void InspectCatalog() {
1912 // InteractiveChoice<CatalogInspector> inspectorChoice;
1913 // inspectorChoice.Register("pair_distance", "pair distance angle", InspectPairDistance);
1914 // inspectorChoice.Register("pyramid_distances", "all pair distances in pyramid", InspectPyramidDistances);
1915 // inspectorChoice.Register("triple_angle", "inner angle of a triangle", InspectTripleAngle);
1916 // inspectorChoice.Register("find_star", "find a star name based on ra/de", InspectFindStar);
1917 // inspectorChoice.Register("print_star", "print coordinates of a star", InspectPrintStar);
1918 // (*inspectorChoice.Prompt("Inspect the catalog"))(CatalogRead());
1919 // }
1920 
1921 } // namespace lost
An attitude estimation algorithm estimates the orientation of the camera based on identified stars.
The attitude (orientation) of a spacecraft.
EulerAngles ToSpherical() const
Get the euler angles from the attitude, converting from whatever format is stored.
Vec3 Rotate(const Vec3 &) const
Convert a vector from the reference frame to the body frame.
Quaternion GetQuaternion() const
Get the quaternion representing the attitude, converting from whatever format is stored.
A full description of a camera. Enough information to reconstruct the camera matrix and then some.
Definition: camera.hpp:9
Vec2 SpatialToCamera(const Vec3 &) const
Converts from a 3D point in space to a 2D point on the camera sensor.
Definition: camera.cpp:15
bool InSensor(const Vec2 &vector) const
Returns whether a given pixel is actually in the camera's field of view.
Definition: camera.cpp:51
int XResolution() const
Width of the sensor in pixels.
Definition: camera.hpp:38
int YResolution() const
Height of the sensor in pixels.
Definition: camera.hpp:40
decimal Fov() const
Horizontal field of view in radians.
Definition: camera.cpp:66
decimal FocalLength() const
Focal length in pixels.
Definition: camera.hpp:42
A star from the Bright Star Catalog.
Definition: star-utils.hpp:12
int magnitude
The magnitude of the star, with the decimal point shifted two places right.
Definition: star-utils.hpp:37
A simple, fast, and pretty decent centroid algorithm.
Definition: centroiders.hpp:37
An algorithm that detects the (x,y) coordinates of bright points in an image, called "centroids".
Definition: centroiders.hpp:12
The result of comparing actual and expected centroids Used for debugging and benchmarking.
Definition: io.cpp:1034
decimal numExtraCentroids
Stars in actual but not expected.
Definition: io.cpp:1052
decimal numCorrectCentroids
Number of actual stars within the centroiding threshold of an expected star.
Definition: io.cpp:1046
decimal meanError
Average distance from actual to expected centroids (in pixels) Only correct centroids are considered ...
Definition: io.cpp:1041
Commannd line options when using the database command.
Definition: io.hpp:286
A slow but reliable attitude estimation algorithm.
A centroid algorithm for debugging that returns random centroids.
Definition: centroiders.hpp:25
A star-id algorithm that returns random results. For debugging.
Definition: star-id.hpp:26
A "human-readable" way to represent a 3d rotation or orientation.
decimal roll
How far we roll counterclockwise. Roll is performed last (after yaw and pitch).
decimal de
Declination. How far we pitch up (or down if negative). Pitch is performed second,...
decimal ra
Right ascension. How far we yaw left. Yaw is performed first.
A pipeline input which is generated (fake image).
Definition: io.hpp:125
GeneratedPipelineInput(const Catalog &, Attitude, Camera, std::default_random_engine *, bool centroidsOnly, decimal observedReferenceBrightness, decimal starSpreadStdDev, decimal sensitivity, decimal darkCurrent, decimal readNoiseStdDev, Attitude motionBlurDirection, decimal exposureTime, decimal readoutTime, bool shotNoise, int oversampling, int numFalseStars, int falseMinMagnitude, int falseMaxMagnitude, int cutoffMag, decimal perturbationStddev)
Create a generated pipeline input.
Definition: io.cpp:485
A star used in simulated image generation. Contains extra data about how to simulate the star.
Definition: io.cpp:415
decimal peakBrightness
the brightness density per time unit at the center of the star. 0.0 is black, 1.0 is white.
Definition: io.cpp:418
GeneratedStar(Star star, decimal peakBrightness, Vec2 motionBlurDelta)
Definition: io.cpp:417
Vec2 delta
(only meaningful with motion blur) Where the star will appear one time unit in the future.
Definition: io.cpp:424
A star-id algorithm based on assigning votes for each centroid-catalog pair then choosing the highest...
Definition: star-id.hpp:35
An 8-bit grayscale 2d image.
Definition: io.hpp:63
unsigned char * image
The raw pixel data in the image.
Definition: io.hpp:69
int height
Definition: io.hpp:72
int width
Definition: io.hpp:71
A more complicated centroid algorithm which doesn't perform much better than CenterOfGravityAlgorithm...
Definition: centroiders.hpp:47
const unsigned char * SubDatabasePointer(int32_t magicValue) const
MultiDatabase memory layout:
Definition: databases.cpp:316
static const int32_t kMagicValue
Magic value to use when storing inside a MultiDatabase.
Definition: databases.hpp:70
A set of algorithms that describes all or part of the star-tracking "pipeline".
Definition: io.hpp:233
Pipeline()=default
PipelineOutput Go(const PipelineInput &)
Run all stages of a pipeline.
Definition: io.cpp:913
Represents the input and expected outputs of a pipeline run.
Definition: io.hpp:94
virtual const Catalog & GetCatalog() const =0
The catalog to which catalog indexes returned from other methods refer.
virtual const StarIdentifiers * InputStarIds() const
The centroid indices in the StarIdentifiers returned from InputStarIds should be indices into InputSt...
Definition: io.hpp:104
virtual const Image * InputImage() const
Definition: io.hpp:98
cairo_surface_t * InputImageSurface() const
Convert the InputImage() output into a cairo surface.
Definition: io.cpp:340
virtual const Stars * InputStars() const
Definition: io.hpp:101
virtual const Camera * InputCamera() const
Definition: io.hpp:107
The command line options passed when running a pipeline.
Definition: io.hpp:81
A pipeline input created by reading a PNG from a file on disk.
Definition: io.hpp:167
PngPipelineInput(cairo_surface_t *, Camera, const Catalog &)
A pipeline input coming from an image with no extra metadata.
Definition: io.cpp:366
The "de facto" star-id algorithm used in many real-world missions.
Definition: star-id.hpp:52
A quaternion is a common way to represent a 3d rotation.
decimal Angle() const
How many radians the rotation represented by this quaternion has.
Vec3 Rotate(const Vec3 &) const
Rotate a 3d vector according to the rotation represented by the quaternion.
A faster and just as accurate attitude estimator as the Davenport Q algorithm.
std::vector< unsigned char > buffer
A "centroid" detected in an image.
Definition: star-utils.hpp:49
decimal radiusY
Approximate vertical radius of the bright area in pixels.
Definition: star-utils.hpp:65
Vec2 position
The (x,y) pixel coordinates in the image (top left is 0,0)
Definition: star-utils.hpp:58
decimal radiusX
Approximate horizontal radius of the bright area in pixels.
Definition: star-utils.hpp:63
A star idenification algorithm.
Definition: star-id.hpp:16
Records that a certain Star (detected in the image) corresponds to a certain CatalogStar.
Definition: star-utils.hpp:78
A fast attitude estimator which only takes into account information from two stars.
UserSpecifiedOutputStream(std::string filePath, bool isBinary)
Create a PromptedOutputStream which will output to the given file.
Definition: io.cpp:34
Three dimensional vector with decimaling point components.
double decimal
Definition: decimal.hpp:11
#define DECIMAL_ASIN(x)
Definition: decimal.hpp:55
#define DECIMAL_LOG(x)
Definition: decimal.hpp:41
#define DECIMAL(x)
Definition: decimal.hpp:21
#define DECIMAL_POW(base, power)
Definition: decimal.hpp:39
#define DECIMAL_M_PI
Definition: decimal.hpp:29
#define DECIMAL_CEIL(x)
Definition: decimal.hpp:47
#define DECIMAL_SQRT(x)
Definition: decimal.hpp:40
#define DECIMAL_ERF(x)
Definition: decimal.hpp:43
#define DECIMAL_EXP(x)
Definition: decimal.hpp:42
#define DECIMAL_FLOOR(x)
Definition: decimal.hpp:48
#define DEFAULT_BSC_PATH
Definition: io.cpp:95
#define LOST_PIPELINE_COMPARE(precondition, errmsg, comparator, path, isBinary)
LOST starting point.
decimal FovToFocalLength(decimal xFov, decimal xResolution)
Definition: camera.cpp:58
CentroidComparison CentroidComparisonsCombine(std::vector< CentroidComparison > comparisons)
Definition: io.cpp:1114
unsigned char * SurfaceToGrayscaleImage(cairo_surface_t *cairoSurface)
Convert a colored Cairo image surface into a row-major array of grayscale pixels.
Definition: io.cpp:127
decimal FocalLengthFromOptions(const PipelineOptions &values, int xResolution)
Calculate the focal length, in pixels, based on the given command line options.
Definition: io.cpp:319
const int32_t kCatalogMagicValue
Definition: databases.hpp:13
std::vector< StarIdentifier > StarIdentifiers
Definition: star-utils.hpp:102
void SerializeCatalog(SerializeContext *ser, const Catalog &catalog, bool inclMagnitude, bool inclName)
Serialize the catalog to buffer.
Definition: star-utils.cpp:107
std::vector< CatalogStar > BscParse(std::string tsvPath)
Parse the bright star catalog from the TSV file on disk.
Definition: io.cpp:58
Catalog DeserializeCatalog(DeserializeContext *des, bool *inclMagnitudeReturn, bool *inclNameReturn)
Deserialize a catalog.
Definition: star-utils.cpp:123
const Catalog & CatalogRead()
Read and parse the full catalog from disk. If called multiple times, will re-use the first result.
Definition: io.cpp:99
decimal MagToBrightness(int mag)
returns some relative brightness measure, which is proportional to the total number of photons receiv...
Definition: star-utils.cpp:147
std::vector< Star > Stars
Definition: star-utils.hpp:101
decimal DegToRad(decimal deg)
PipelineInputList(* PipelineInputFactory)()
Definition: io.cpp:805
Quaternion SphericalToQuaternion(decimal ra, decimal dec, decimal roll)
Return a quaternion that will reorient the coordinate axes so that the x-axis points at the given rig...
decimal RadToDeg(decimal rad)
void PipelineComparatorPlotCentroidIndices(std::ostream &os, const PipelineInputList &expected, const std::vector< PipelineOutput > &actual, const PipelineOptions &)
Plot an annotated image where centroids are annotated with their centroid index.
Definition: io.cpp:1411
PipelineInputList GetPngPipelineInput(const PipelineOptions &values)
Create a PngPipelineInput using command line options.
Definition: io.cpp:379
void SurfacePlot(std::string description, cairo_surface_t *cairoSurface, const Stars &stars, const StarIdentifiers *starIds, const Catalog *catalog, const Attitude *attitude, double red, double green, double blue, double alpha, bool rawStarIndexes=false)
Plot information about an image onto the image using Cairo.
Definition: io.cpp:178
StarIdComparison StarIdsCompare(const StarIdentifiers &expected, const StarIdentifiers &actual, const Catalog &expectedCatalog, const Catalog &actualCatalog, decimal centroidThreshold, const Stars &expectedStars, const Stars &inputStars)
Compare expected and actual star identifications.
Definition: io.cpp:1133
void SerializePairDistanceKVector(SerializeContext *ser, const Catalog &catalog, decimal minDistance, decimal maxDistance, long numBins)
Serialize a pair-distance KVector into buffer.
Definition: databases.cpp:198
const int kMaxBrightness
Definition: io.cpp:479
std::vector< MultiDatabaseEntry > MultiDatabaseDescriptor
Definition: databases.hpp:123
void(* PipelineComparator)(std::ostream &os, const PipelineInputList &, const std::vector< PipelineOutput > &, const PipelineOptions &)
Definition: io.cpp:1214
MultiDatabaseDescriptor GenerateDatabases(const Catalog &catalog, const DatabaseOptions &values)
Appropriately create descriptors for all requested databases according to command-line options.
Definition: io.cpp:280
SerializeContext serFromDbValues(const DatabaseOptions &values)
Definition: io.cpp:276
std::vector< std::unique_ptr< PipelineInput > > PipelineInputList
Definition: io.hpp:162
PipelineInputList GetPipelineInput(const PipelineOptions &values)
Come up with a list of pipeline inputs based on command line options.
Definition: io.cpp:808
void PipelineComparison(const PipelineInputList &expected, const std::vector< PipelineOutput > &actual, const PipelineOptions &values)
Print or otherwise analyze the results of (perhaps multiple) runs of a star tracking pipeline.
Definition: io.cpp:1703
Pipeline SetPipeline(const PipelineOptions &values)
Create a pipeline from command line options.
Definition: io.cpp:842
CentroidComparison CentroidsCompare(decimal threshold, const Stars &expected, const Stars &actual)
Compare expected and actual centroids.
Definition: io.cpp:1088
PipelineInputList GetGeneratedPipelineInput(const PipelineOptions &values)
Create a GeneratedPipelineInput based on the command line options in values
Definition: io.cpp:734
std::vector< CatalogStar > Catalog
Definition: star-utils.hpp:100
cairo_surface_t * GrayscaleImageToSurface(const unsigned char *image, const int width, const int height)
Definition: io.cpp:156
std::ostream & operator<<(std::ostream &os, const Camera &camera)
Print information about the camera in machine and human-readable form.
Definition: io.cpp:304
The result of running a pipeline.
Definition: io.hpp:190
Catalog catalog
The catalog that the indices in starIds refer to.
Definition: io.hpp:205
long long centroidingTimeNs
How many nanoseconds the centroiding stage of the pipeline took.
Definition: io.hpp:197
std::unique_ptr< Stars > stars
Definition: io.hpp:191
long long starIdTimeNs
Definition: io.hpp:198
long long attitudeEstimationTimeNs
Definition: io.hpp:199
std::unique_ptr< Attitude > attitude
Definition: io.hpp:193
std::unique_ptr< StarIdentifiers > starIds
Definition: io.hpp:192
The result of comparing an actual star identification with the true star idenification,...
Definition: io.hpp:209
int numCorrect
The number of centroids in the image which are close to an expected centroid that had an expected ide...
Definition: io.hpp:212
int numTotal
The number of centroids sufficiently close to a true expected star.
Definition: io.hpp:220
int numIncorrect
The number of centroids which were either:
Definition: io.hpp:217
A two dimensional vector with decimaling point components.
decimal MagnitudeSq() const
The square of the magnitude.
decimal Magnitude() const