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