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 <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
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
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;
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(),
81 &name, &weird,
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");
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
128 int width, height;
129 unsigned char *result;
131
134 puts("Can't convert weird image formats to grayscale.");
135 return NULL;
136 }
137
140
141 result = new unsigned char[width*height];
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
161 // hopefully unnecessary
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 }
168 return result;
169}
170
178void SurfacePlot(std::string description,
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) {
191 std::string metadata = description + " ";
192
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.
214 centroid.position.x - radiusX,
215 centroid.position.y - radiusY,
216 radiusX * 2,
217 radiusY * 2);
219 } else {
221 DECIMAL_FLOOR(centroid.position.x),
222 DECIMAL_FLOOR(centroid.position.y),
223 1, 1);
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];
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
262
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
282
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;
295 } else {
296 std::cerr << "No database builder selected -- no database generated." << std::endl;
297 exit(1);
298 }
299
300 return dbEntries;
301}
302
304std::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
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
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
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
384 std::string pngPath = values.png;
385
387 std::cerr << "PNG Read status: " << cairo_status_to_string(cairo_surface_status(cairoSurface)) << std::endl;
389 exit(1);
390 }
391
395 Camera cam = Camera(focalLengthPixels, xResolution, yResolution);
396
397 result.push_back(std::unique_ptr<PipelineInput>(new PngPipelineInput(cairoSurface, cam, CatalogRead())));
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
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
443static decimal MotionBlurredPixelBrightness(const Vec2 &pixel, const GeneratedStar &generatedStar,
445 const Vec2 &p0 = generatedStar.position;
446 const Vec2 &delta = generatedStar.delta;
447 const Vec2 d0 = p0 - pixel;
448 return generatedStar.peakBrightness
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
456static decimal StaticPixelBrightness(const Vec2 &pixel, const GeneratedStar &generatedStar,
458 const Vec2 d0 = generatedStar.position - pixel;
459 return generatedStar.peakBrightness * t * DECIMAL_EXP(-d0.MagnitudeSq() / (2 * stddev * stddev));
460}
461
471static decimal CentroidImagingProbability(decimal mag, decimal cutoffMag) {
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
479const int kMaxBrightness = 255;
480
486 Attitude attitude,
487 Camera camera,
488 std::default_random_engine *rng,
489
490 bool centroidsOnly,
496 Attitude motionBlurDirection, // applied on top of the attitude
498 decimal readoutTime, // zero for no rolling shutter
499 bool shotNoise,
500 int oversampling,
501 int numFalseStars,
504 int cutoffMag,
506 : camera(camera), attitude(attitude), catalog(catalog) {
507
510
511 image.width = camera.XResolution();
512 image.height = camera.YResolution();
513 // number of true photons each pixel receives.
514
515 assert(oversampling >= 1);
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);
524 // attitude at the middle of exposure time
526 // attitude 1 time unit after middle of exposure
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!
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++) {
543 // to be uniform around sphere. Borel-Kolmogorov paradox is calling
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
554 Vec3 rotated = attitude.Rotate(catalogWithFalse[i].spatial);
555 if (rotated.x <= 0) {
556 continue;
557 }
559
560 if (camera.InSensor(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.
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
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:
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
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;
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++) {
651
653 if (motionBlurEnabled) {
654 curPhotons =
655 (MotionBlurredPixelBrightness({x, y}, star, tEnd, starSpreadStdDev)
656 - MotionBlurredPixelBrightness({x, y}, star, tStart, starSpreadStdDev))
658 } else {
659 curPhotons = StaticPixelBrightness({x, y}, star, exposureTime, starSpreadStdDev)
661 }
662
663 assert(DECIMAL(0.0) <= curPhotons);
664
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++) {
679
680 // dark current (Constant)
682
683 // read noise (Gaussian)
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)
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]);
700 } else {
702 }
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
715static 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
725 decimal randomDec = (DECIMAL_M_PI / 2) - acos(1 - 2 * randomAngleDistribution(*pReng)); //acos returns a decimal in range [0, pi]
727
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?
751 DegToRad(values.generateDe),
752 DegToRad(values.generateRoll)));
753
755 DegToRad(values.generateBlurDe),
756 DegToRad(values.generateBlurRoll)));
758
759 decimal focalLength = FocalLengthFromOptions(values, values.generateXRes);
760
761
762 for (int i = 0; i < values.generate; i++) {
763
764
766 if (values.generateRandomAttitudes) {
767 inputAttitude = RandomAttitude(&attitudeRng);
768 } else {
769 inputAttitude = attitude;
770 }
771
773 CatalogRead(),
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,
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 != "") {
812 } else {
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
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.
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) {
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
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
1015std::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
1035public:
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
1061static 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
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
1115 assert(comparisons.size() > 0);
1116
1118
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
1137 const Stars &expectedStars, const Stars &inputStars) {
1138
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.
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++) {
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
1214typedef void (*PipelineComparator)(std::ostream &os,
1215 const PipelineInputList &,
1216 const std::vector<PipelineOutput> &,
1217 const PipelineOptions &);
1218
1220static 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
1227static void PipelineComparatorPlotRawInput(std::ostream &os,
1229 const std::vector<PipelineOutput> &,
1230 const PipelineOptions &) {
1231
1232 cairo_surface_t *cairoSurface = expected[0]->InputImageSurface();
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.
1239static void PipelineComparatorPlotInput(std::ostream &os,
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",
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);
1255}
1256
1257static void PipelineComparatorPlotExpected(std::ostream &os,
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",
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);
1273}
1274
1276static void PipelineComparatorCentroids(std::ostream &os,
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++) {
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
1297static 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
1328static void PipelineComparatorPrintExpectedCentroids(std::ostream &os,
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(),
1343 expected[0]->ExpectedStarIds());
1344}
1345
1346static void PipelineComparatorPrintInputCentroids(std::ostream &os,
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(),
1361 expected[0]->InputStarIds());
1362}
1363
1364static 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,
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
1413 const std::vector<PipelineOutput> &actual,
1414 const PipelineOptions &) {
1415 const Stars &stars = actual[0].stars ? *actual[0].stars : *expected[0]->InputStars();
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)",
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);
1433}
1434
1436static void PipelineComparatorPlotOutput(std::ostream &os,
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",
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);
1452}
1453
1455static void PipelineComparatorStarIds(std::ostream &os,
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) {
1484 }
1485 if (comparison.numIncorrect > 0) {
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
1495static 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
1516static 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
1525static void PipelineComparatorPrintExpectedAttitude(std::ostream &os,
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
1535static void PipelineComparatorAttitude(std::ostream &os,
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
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
1558 numCorrect++;
1559 } else {
1560 numIncorrect++;
1561 }
1562 }
1563 }
1564
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
1574static 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);
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
1604static 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 != "") {
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.
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: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.
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.
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:421
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...
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 Image * InputImage() const
Definition io.hpp:98
cairo_surface_t * InputImageSurface() const
Convert the InputImage() output into a cairo surface.
Definition io.cpp:340
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.
A faster and just as accurate attitude estimator as the Davenport Q algorithm.
A "centroid" detected in an image.
Vec2 position
The (x,y) pixel coordinates in the image (top left is 0,0)
A star idenification algorithm.
Definition star-id.hpp:16
Records that a certain Star (detected in the image) corresponds to a certain CatalogStar.
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
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: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
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:1133
void SerializePairDistanceKVector(SerializeContext *ser, const Catalog &catalog, decimal minDistance, decimal maxDistance, long numBins)
Serialize a pair-distance KVector into buffer.
void SerializePrimitive(SerializeContext *ser, const T &val)
const int kMaxBrightness
Definition io.cpp:479
std::vector< MultiDatabaseEntry > MultiDatabaseDescriptor
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
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
The result of comparing an actual star identification with the true star idenification,...
Definition io.hpp:209
A two dimensional vector with decimaling point components.
decimal MagnitudeSq() const
The square of the magnitude.
decimal Magnitude() const