Instrument Neutral Distributed Interface INDI 2.2.5
sky_renderer.cpp
Go to the documentation of this file.
1#include "sky_renderer.h"
2#include "indicom.h"
3#include "locale_compat.h"
4
5#include <algorithm>
6#include <cmath>
7#include <cstdio>
8#include <cstring>
9
10double SkyRenderer::flux(double mag) const
11{
12 if (m_Cfg.limitingMag == m_Cfg.saturationMag)
13 return 1.0;
14 double const z = m_Cfg.limitingMag;
15 double const k = 2.5 * log10(m_Cfg.maxVal) / (m_Cfg.limitingMag - m_Cfg.saturationMag);
16 return pow(10.0, (z - mag) * k / 2.5);
17}
18
19int SkyRenderer::addToPixel(INDI::CCDChip *chip, int x, int y, int val)
20{
21 int const nwidth = chip->getSubW();
22 int const nheight = chip->getSubH();
23
24 x -= chip->getSubX();
25 y -= chip->getSubY();
26
27 if (x < 0 || x >= nwidth || y < 0 || y >= nheight)
28 return 0;
29
30 auto *pt = reinterpret_cast<uint16_t *>(chip->getFrameBuffer());
31 pt += y * nwidth + x;
32
33 int newval = static_cast<int>(pt[0]) + val;
34 if (newval > m_Cfg.maxVal)
35 newval = m_Cfg.maxVal;
36 if (newval > m_MaxPix)
37 m_MaxPix = newval;
38 if (newval < m_MinPix)
39 m_MinPix = newval;
40 pt[0] = static_cast<uint16_t>(newval);
41
42 return 1;
43}
44
45int SkyRenderer::drawImageStar(INDI::CCDChip *chip, float mag, float x, float y, float exp_s)
46{
47 int drew = 0;
48
49 int const subX = chip->getSubX();
50 int const subY = chip->getSubY();
51 int const subW = chip->getSubW() + subX;
52 int const subH = chip->getSubH() + subY;
53
54 if (x < subX || x > subW || y < subY || y > subH)
55 return 0;
56
57 float const totalFlux = static_cast<float>(flux(mag) * exp_s);
58
59 // Compute position-dependent seeing to simulate sensor tilt.
60 // Instead of a global seeing, compute per-star seeing by shifting the
61 // effective focus position based on star location on the sensor.
62 // This causes each tile's V-curve minimum to land at a different focuser
63 // position — which is exactly what the Aberration Inspector detects as tilt.
64 float effectiveSeeing = m_Cfg.seeing;
65 if (m_Cfg.tiltLR != 0.0f || m_Cfg.tiltTB != 0.0f)
66 {
67 float const cx = chip->getXRes() / 2.0f;
68 float const cy = chip->getYRes() / 2.0f;
69 float const dx = (x - cx) / cx; // normalized [-1, +1]
70 float const dy = (y - cy) / cy;
71 // tiltLR/tiltTB in arcsec: directly added to seeing at that position.
72 // This shifts where the V-curve minimum occurs for edge tiles vs center.
73 float const tiltOffset = m_Cfg.tiltLR * dx + m_Cfg.tiltTB * dy;
74 effectiveSeeing += tiltOffset;
75 if (effectiveSeeing < 0.5f)
76 effectiveSeeing = 0.5f; // Floor to prevent degenerate PSF
77 }
78
79 int const boxsizey = static_cast<int>(3.0f * effectiveSeeing / m_ImageScaleY) + 1;
80
81 float const sigma = effectiveSeeing / (2.0f * std::sqrt(2.0f * std::log(2.0f)));
82 float const norm = 1.0f / (sigma * std::sqrt(2.0f * float(M_PI)));
83 float const inv2sig2 = 1.0f / (2.0f * sigma * sigma);
84
85 // Sub-pixel centering: keep the fractional part of the star position so the
86 // PSF is centred on the true (floating-point) location rather than snapped to
87 // the truncated integer pixel. Without this, sub-pixel drift is discarded and
88 // the measured centroid quantises to a lattice (see indilib/indi#2465).
89 float const fracX = x - static_cast<int>(x);
90 float const fracY = y - static_cast<int>(y);
91
92 for (int sy = -boxsizey; sy <= boxsizey; sy++)
93 {
94 for (int sx = -boxsizey; sx <= boxsizey; sx++)
95 {
96 float const ddx = m_ImageScaleX * (sx - fracX);
97 float const ddy = m_ImageScaleY * (sy - fracY);
98 float const dc2 = ddx * ddx + ddy * ddy;
99 float const fa = norm * std::exp(-dc2 * inv2sig2);
100 int const fp = static_cast<int>(fa * totalFlux);
101 if (fp > 0)
102 {
103 if (addToPixel(chip,
104 static_cast<int>(x) + sx,
105 static_cast<int>(y) + sy, fp) != 0)
106 drew = 1;
107 }
108 }
109 }
110 return drew;
111}
112
113void SkyRenderer::applyReadoutNoise(INDI::CCDChip *chip, int bias, int maxNoise)
114{
115 if (maxNoise <= 0)
116 return;
117
118 int const nx = chip->getSubW();
119 int const ny = chip->getSubH();
120 auto *buf = reinterpret_cast<uint16_t *>(chip->getFrameBuffer());
121
122 for (int y = 0; y < ny; y++)
123 for (int x = 0; x < nx; x++)
124 {
125 int newval = static_cast<int>(buf[y * nx + x]) + bias + (random() % maxNoise);
126 if (newval > m_Cfg.maxVal)
127 newval = m_Cfg.maxVal;
128 buf[y * nx + x] = static_cast<uint16_t>(newval);
129 }
130}
131
132void SkyRenderer::drawSkyGlow(INDI::CCDChip *chip, float exp_s)
133{
134 float const skyflux = static_cast<float>(flux(m_Cfg.skyGlow)) * exp_s;
135
136 int const nwidth = chip->getSubW();
137 int const nheight = chip->getSubH();
138
139 auto *pt = reinterpret_cast<uint16_t *>(chip->getFrameBuffer());
140
141 float const vig = std::min(nwidth, nheight) * m_ImageScaleX;
142 float const invVig2 = 1.0f / (vig * vig);
143
144 for (int y = 0; y < nheight; y++)
145 {
146 float const sy = nheight / 2.0f - y;
147
148 for (int x = 0; x < nwidth; x++)
149 {
150 float const sx = nwidth / 2.0f - x;
151
152 float const dc2 = sx * sx * m_ImageScaleX * m_ImageScaleX
153 + sy * sy * m_ImageScaleY * m_ImageScaleY;
154
155 float const fa = std::exp(-2.0f * 0.7f * dc2 * invVig2);
156
157 float fp = (pt[0] + skyflux) * fa;
158
159 if (fp > m_Cfg.maxVal) fp = static_cast<float>(m_Cfg.maxVal);
160 if (fp < pt[0]) fp = pt[0]; // never darken an already-bright pixel
161 if (fp > m_MaxPix) m_MaxPix = static_cast<int>(fp);
162 if (fp < m_MinPix) m_MinPix = static_cast<int>(fp);
163
164 pt[0] = static_cast<uint16_t>(fp);
165 pt++;
166 }
167 }
168}
169
171 INDI::CCDChip *chip,
172 double ra_j2000_deg,
173 double dec_j2000_deg,
174 double focal_length_mm,
175 double rotation_deg,
176 float exposure_s,
177 bool renderStars,
178 double minSearchRadiusArcmin)
179{
180 m_MaxPix = 0;
181 m_MinPix = 65000;
182
183 // Image scale (arcsec/pixel) from focal length and pixel size
184 m_ImageScaleX = static_cast<float>((chip->getPixelSizeX() / focal_length_mm) * 206.3);
185 m_ImageScaleY = static_cast<float>((chip->getPixelSizeY() / focal_length_mm) * 206.3);
186
187 // Plate matrix: maps standard coords to pixel coords, with camera rotation applied
188 double const theta = rotation_deg * (M_PI / 180.0);
189 double const pprx = focal_length_mm / chip->getPixelSizeX() * 1000.0; // pixels per radian, X
190 double const ppry = focal_length_mm / chip->getPixelSizeY() * 1000.0; // pixels per radian, Y
191
192 double const pa = pprx * std::cos(theta);
193 double const pb = ppry * std::sin(theta);
194 double const pd = -pprx * std::sin(theta);
195 double const pe = ppry * std::cos(theta);
196 double const pc = chip->getXRes() / 2.0; // X center pixel
197 double const pf = chip->getYRes() / 2.0; // Y center pixel
198 double const ccdW = chip->getXRes();
199
200 // Field center in radians
201 double const rar = ra_j2000_deg * (M_PI / 180.0);
202 double const decr = dec_j2000_deg * (M_PI / 180.0);
203
204 // GSC search radius (arcmin): half-diagonal of the chip in arcsec, converted to arcmin
205 double radius = std::sqrt(
206 m_ImageScaleX * m_ImageScaleX * chip->getXRes() / 2.0 * chip->getXRes() / 2.0 +
207 m_ImageScaleY * m_ImageScaleY * chip->getYRes() / 2.0 * chip->getYRes() / 2.0) / 60.0;
208 if (minSearchRadiusArcmin > 0 && radius < minSearchRadiusArcmin)
209 radius = minSearchRadiusArcmin;
210
211 // Cap lookup magnitude for very wide fields to limit GSC processing time
212 double lookuplimit = m_Cfg.limitingMag;
213 if (radius > 60)
214 lookuplimit = 11;
215
216 // Clear the frame buffer
217 memset(chip->getFrameBuffer(), 0, chip->getFrameBufferSize());
218
219 int drawn = 0;
220
221 if (renderStars)
222 {
223 AutoCNumeric locale;
224 char gsccmd[250];
225
226 // Handbook of astronomical image processing, eqs. 9.1/9.2 (gnomonic projection)
227 snprintf(gsccmd, sizeof(gsccmd),
228 "gsc -c %8.6f %+8.6f -r %4.1f -m 0 %4.2f -n 3000",
229 range360(ra_j2000_deg),
230 rangeDec(dec_j2000_deg),
231 radius,
232 lookuplimit);
233
234 FILE *pp = popen(gsccmd, "r");
235 if (pp != nullptr)
236 {
237 char line[256];
238
239 while (fgets(line, sizeof(line), pp) != nullptr)
240 {
241 char id[20], plate[6], ob[6];
242 float ra, dec, pose, mag, mage, dist;
243 int band, c, dir;
244
245 int rc = sscanf(line, "%10s %f %f %f %f %f %d %d %4s %2s %f %d",
246 id, &ra, &dec, &pose, &mag, &mage,
247 &band, &c, plate, ob, &dist, &dir);
248 if (rc != 12)
249 continue;
250
251 double const srar = ra * (M_PI / 180.0);
252 double const sdecr = dec * (M_PI / 180.0);
253
254 double const denom = cos(decr) * cos(sdecr) * cos(srar - rar)
255 + sin(decr) * sin(sdecr);
256 if (denom <= 0)
257 continue;
258
259 double const sx = cos(sdecr) * sin(srar - rar) / denom;
260 double const sy = (sin(decr) * cos(sdecr) * cos(srar - rar)
261 - cos(decr) * sin(sdecr)) / denom;
262
263 // Invert horizontally (CW->CCW, origin North)
264 double const ccdx = ccdW - (pa * sx + pb * sy + pc);
265 double const ccdy = pd * sx + pe * sy + pf;
266
267 drawn += drawImageStar(chip, mag,
268 static_cast<float>(ccdx),
269 static_cast<float>(ccdy),
270 exposure_s);
271 }
272 pclose(pp);
273 }
274 else
275 {
276 drawn = -1;
277 }
278 }
279
280 drawSkyGlow(chip, exposure_s);
281
282 return drawn;
283}
The CCDChip class provides functionality of a CCD Chip within a CCD.
Definition indiccdchip.h:39
int getSubX() const
getSubX Get the starting left coordinates (X) of the frame.
Definition indiccdchip.h:99
int getSubY() const
getSubY Get the starting top coordinates (Y) of the frame.
float getPixelSizeY() const
getPixelSizeY Get vertical pixel size in microns.
int getSubH() const
getSubH Get the height of the frame
float getPixelSizeX() const
getPixelSizeX Get horizontal pixel size in microns.
int getXRes() const
getXRes Get the horizontal resolution in pixels of the CCD Chip.
Definition indiccdchip.h:81
uint8_t * getFrameBuffer()
getFrameBuffer Get raw frame buffer of the CCD chip.
int getSubW() const
getSubW Get the width of the frame
int getFrameBufferSize() const
getFrameBufferSize Get allocated frame buffer size to hold the CCD image frame.
int getYRes() const
Get the vertical resolution in pixels of the CCD Chip.
Definition indiccdchip.h:90
int renderFrame(INDI::CCDChip *chip, double ra_j2000_deg, double dec_j2000_deg, double focal_length_mm, double rotation_deg, float exposure_s, bool renderStars=true, double minSearchRadiusArcmin=0)
void applyReadoutNoise(INDI::CCDChip *chip, int bias, int maxNoise)
int drawImageStar(INDI::CCDChip *, float mag, float x, float y, float exp_s)
#define M_PI
Definition dsp.h:52
double ra
double dec
double range360(double r)
range360 Limits an angle to be between 0-360 degrees.
Definition indicom.c:1321
double rangeDec(double decdegrees)
rangeDec Limits declination value to be in -90 to 90 range.
Definition indicom.c:1341
Implementations for common driver routines.
float limitingMag
Definition sky_renderer.h:9
float saturationMag