Skip to content

Commit 5e4f8a9

Browse files
committed
GiGE: Fix binning
At least for FLIR GiGE cameras, changing binning changes the apparent size of the chip, such that the region of interest is configured using a smaller number of pixels. This appears to be contrary to how INDI expects binning to be configured. Following the CCD Simulator (hopefully it is a decent example of implementing a INDI::CCD device), it rather appears that the region of interest is still expected to be set using the entire available Width/Height. It therefore appears necessary to mitigate this mismatch since (at least for ekos) the client appears to mess up the apparent image if binning is configured but a smaller region is selected, but in reality the entire avaialble sensor was binned. So, for now, even when binning is used, this patch makes the driver use the full WidthMax and HeightMax manually divided by the binning to select the region of interest. This means that any use of get_x_offset().val(), get_y_offset().val(), get_width().val(), or get_height().val() from the GigECCD::camera object must be manually multiplied or divided by the values from get_bin_x().val() and get_bin_y().val() as appropriate. Similarly, camera->set_geometry(...) must be given values that are divided by the appropriate binning. It remains to verify this behavior on GiGE cameras from other vendors, or at least to check what the standard says, if anything useful on the matter.
1 parent 936b3a9 commit 5e4f8a9

5 files changed

Lines changed: 127 additions & 32 deletions

File tree

indi-gige/src/ArvGeneric.cpp

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ bool ArvGeneric::_get_bounds(void (*fn_arv_bounds)(::ArvCamera *, T *min, T *max
137137
return true;
138138
}
139139

140+
template <typename T>
141+
bool ArvGeneric::_get_incr(T (*fn_arv_incr)(::ArvCamera *, GError**), min_max_property<T> *prop)
142+
{
143+
prop->set_increment(CALL_RETURN(fn_arv_incr, this->camera));
144+
return true;
145+
}
146+
140147
bool ArvGeneric::is_exposing_single()
141148
{
142149
bool single = this->single_acquisition_active.load();
@@ -238,17 +245,24 @@ bool ArvGeneric::_set_initial_config()
238245
return true;
239246
}
240247

248+
#define _GET_BOUNDS(T, feature, prop) \
249+
this->_get_bounds<T>(arv_camera_get_ ## feature ## _bounds, &this->cam.prop)
250+
#define _GET_BOUNDS_INCR(T, feature, prop) \
251+
_GET_BOUNDS(T, feature, prop); \
252+
this->_get_incr<T>(arv_camera_get_ ## feature ## _increment, &this->cam.prop)
253+
254+
241255
bool ArvGeneric::_get_initial_config()
242256
{
243-
this->_get_bounds<gint>(arv_camera_get_x_binning_bounds, &this->cam.bin_x);
244-
this->_get_bounds<gint>(arv_camera_get_y_binning_bounds, &this->cam.bin_y);
245-
this->_get_bounds<gint>(arv_camera_get_x_offset_bounds, &this->cam.x_offset);
246-
this->_get_bounds<gint>(arv_camera_get_y_offset_bounds, &this->cam.y_offset);
247-
this->_get_bounds<gint>(arv_camera_get_width_bounds, &this->cam.width);
248-
this->_get_bounds<gint>(arv_camera_get_height_bounds, &this->cam.height);
249-
this->_get_bounds<double>(arv_camera_get_frame_rate_bounds, &this->cam.frame_rate);
250-
this->_get_bounds<double>(arv_camera_get_exposure_time_bounds, &this->cam.exposure);
251-
this->_get_bounds<double>(arv_camera_get_gain_bounds, &this->cam.gain);
257+
_GET_BOUNDS_INCR(gint, x_binning, bin_x);
258+
_GET_BOUNDS_INCR(gint, y_binning, bin_y);
259+
_GET_BOUNDS_INCR(gint, x_offset, x_offset);
260+
_GET_BOUNDS_INCR(gint, y_offset, y_offset);
261+
_GET_BOUNDS_INCR(gint, width, width);
262+
_GET_BOUNDS_INCR(gint, height, height);
263+
_GET_BOUNDS(double, frame_rate, frame_rate);
264+
_GET_BOUNDS(double, exposure_time, exposure);
265+
_GET_BOUNDS(double, gain, gain);
252266

253267
/* No GVCP call for this..., specializations of this class could make this
254268
* read-only by setting min=max=val (see BlackFly implementation). */
@@ -275,19 +289,17 @@ void ArvGeneric::set_geometry(int const x, int const y, int const w, int const h
275289
this->cam.height.val());
276290
}
277291

278-
void ArvGeneric::update_geometry(void)
292+
std::tuple<int,int,int,int> ArvGeneric::update_geometry(void)
279293
{
280-
gint x, y, w, h, binx, biny;
294+
gint x, y, w, h;
281295

282296
CALL(arv_camera_get_region, this->camera, &x, &y, &w, &h);
283-
CALL(arv_camera_get_binning, this->camera, &binx, &biny);
284297

285298
this->cam.x_offset.set(x);
286299
this->cam.y_offset.set(y);
287300
this->cam.width.set(w);
288301
this->cam.height.set(h);
289-
this->cam.bin_x.set(binx);
290-
this->cam.bin_y.set(biny);
302+
return std::tuple<int,int,int,int>(x, y, w, h);
291303
}
292304

293305
void ArvGeneric::set_bin(int const bin_x, int const bin_y)
@@ -298,6 +310,17 @@ void ArvGeneric::set_bin(int const bin_x, int const bin_y)
298310
CALL(arv_camera_set_binning, this->camera, this->cam.bin_x.val(), this->cam.bin_y.val());
299311
}
300312

313+
std::pair<int,int> ArvGeneric::update_bin()
314+
{
315+
/* read this back to ensure we know what binning was actually set. For at
316+
* least some cameras, a binning of 3 gets promoted to a binning of 4. */
317+
gint binx, biny;
318+
CALL(arv_camera_get_binning, this->camera, &binx, &biny);
319+
this->cam.bin_x.set(binx);
320+
this->cam.bin_y.set(biny);
321+
return std::pair<int,int>(binx, biny);
322+
}
323+
301324
template <typename T>
302325
void ArvGeneric::set_cam_exposure_property(
303326
void (*arv_set)(::ArvCamera *, T, GError**),

indi-gige/src/ArvGeneric.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ class ArvGeneric : public arv::ArvCamera
6464
virtual double get_float(const char * feature) override;
6565

6666
virtual void set_bin(int const bin_x, int const bin_y) override;
67+
virtual std::pair<int,int> update_bin(void) override;
6768
virtual void set_geometry(int const x, int const y, int const w, int const h) override;
68-
virtual void update_geometry(void) override;
69+
virtual std::tuple<int,int,int,int> update_geometry(void) override;
6970
virtual void set_exposure_time(double const val) override;
7071
virtual void set_gain(double const val) override;
7172

@@ -83,6 +84,8 @@ class ArvGeneric : public arv::ArvCamera
8384
private:
8485
template <typename T>
8586
bool _get_bounds(void (*fn_arv_bounds)(::ArvCamera *, T *min, T *max, GError**), min_max_property<T> *prop);
87+
template <typename T>
88+
bool _get_incr(T (*fn_arv_incr)(::ArvCamera *, GError**), min_max_property<T> *prop);
8689

8790
/** Set a given property where it should not be changed during a
8891
* single-frame acquisition and attemping to do so should cause the

indi-gige/src/ArvInterface.h

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <cstddef>
2424

2525
#include <string>
26+
#include <utility>
2627
#include <functional>
2728

2829
namespace arv
@@ -40,12 +41,13 @@ template <class T>
4041
class min_max_property
4142
{
4243
public:
43-
min_max_property() {}
44-
min_max_property(T const min, T const max, T const val)
44+
min_max_property() : _min(0), _max(0), _val(0), _incr(0) {}
45+
min_max_property(T const min, T const max, T const val, T const incr = T(0))
4546
{
4647
this->_min = min;
4748
this->_max = max;
4849
this->_val = val;
50+
this->_incr = incr;
4951
}
5052

5153
void update(T const min, T const max)
@@ -54,8 +56,19 @@ class min_max_property
5456
this->_max = max;
5557
}
5658

57-
void set(T const new_val)
59+
void set_increment(T const incr)
5860
{
61+
this->_incr = incr;
62+
}
63+
64+
/** Set a new value for the property, obeying max/min limits and following
65+
* incremental restrictions if set (i.e. if not 0). */
66+
void set(T new_val)
67+
{
68+
if (this->_incr != T(0))
69+
// enforce alignment to increments
70+
new_val = int((new_val - this->_min)/this->_incr) * this->_incr + this->_min;
71+
5972
if (new_val > this->_max)
6073
this->_val = this->_max;
6174
else if (new_val < this->_min)
@@ -68,9 +81,10 @@ class min_max_property
6881
T val() { return this->_val; }
6982
T min() { return this->_min; }
7083
T max() { return this->_max; }
84+
T incr() { return this->_incr; }
7185

7286
private:
73-
T _min, _max, _val;
87+
T _min, _max, _val, _incr;
7488
};
7589

7690
class ArvCamera
@@ -113,8 +127,15 @@ class ArvCamera
113127

114128
/* Set geometry */
115129
virtual void set_bin(int const bin_x, int const bin_y) = 0;
130+
/** update this class information of binning from the camera hardware.
131+
* @return pair of binning values for x and y.
132+
*/
133+
virtual std::pair<int,int> update_bin(void) = 0;
116134
virtual void set_geometry(int const x, int const y, int const w, int const h) = 0;
117-
virtual void update_geometry(void) = 0;
135+
/** update this class information of geometry from the camera hardware.
136+
* @return tuple of (x, y, w, h).
137+
*/
138+
virtual std::tuple<int,int,int,int> update_geometry(void) = 0;
118139

119140
/* Set exposure */
120141
virtual void set_exposure_time(double const val) = 0;

indi-gige/src/indi_gige.cpp

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,31 @@
4545
#define TIMER_US_TO_S (1000000)
4646
#define CAPS (CCD_CAN_ABORT | CCD_CAN_BIN | CCD_CAN_SUBFRAME | CCD_HAS_STREAMING)
4747

48+
/* NOTE ABOUT BINNING:
49+
* At least for FLIR GiGE cameras, changing binning changes the apparent size of
50+
* the chip, such that the region of interest is configured using a smaller
51+
* number of pixels. This appears to be contrary to how INDI expects binning to
52+
* be configured. Following the CCD Simulator (if that is a decent example of
53+
* implementing a INDI::CCD device, it rather appears that the region of
54+
* interest is still expected to be set using the entire available Width/Height.
55+
* It therefore appears necessary to mitigate this mismatch since (at least for
56+
* ekos) the client appears to mess up the apparent image if binning is
57+
* configured but a smaller region is selected, but in reality the entire
58+
* avaialble sensor was binned.
59+
*
60+
* So, for now (as of 26 Feb 2026), even when binning is used, this driver will
61+
* use the full MaxWidth and MaxHeight manually divided by the binning to select
62+
* the region of interest.
63+
*
64+
* This means that any use of get_x_offset().val(), get_y_offset().val(),
65+
* get_width().val(), or get_height().val() from the GigECCD::camera object must
66+
* be manually multiplied or divided by the values from get_bin_x().val() and
67+
* get_bin_y().val() as appropriate. Similarly, camera->set_geometry(...) must
68+
* be given values that are divided by the appropriate binning.
69+
*
70+
* TODO: verify this behavior on GiGE cameras from other vendors.
71+
*/
72+
4873
static class Loader
4974
{
5075
std::deque<std::unique_ptr<GigECCD>> cameras;
@@ -85,18 +110,23 @@ bool GigECCD::_update_geometry(void)
85110
{
86111
std::lock_guard<std::recursive_mutex> lock(this->camera_mutex);
87112
/* Get actual values */
88-
this->camera->update_geometry();
113+
auto [x, y, w, h] = this->camera->update_geometry();
114+
115+
/* As per note at begin of this file, we manually manage the region of
116+
* interest conversion between the camera and INDI because the camera
117+
* changes the apparent region of interest available depending on binning,
118+
* while INDI does not. */
119+
auto bin_x = this->camera->get_bin_x().val(),
120+
bin_y = this->camera->get_bin_y().val();
89121

90122
/* Sync these with INDI */
91-
PrimaryCCD.setBin(this->camera->get_bin_x().val(), this->camera->get_bin_y().val());
92-
PrimaryCCD.setFrame(this->camera->get_x_offset().val(), this->camera->get_y_offset().val(),
93-
this->camera->get_width().val(), this->camera->get_height().val());
123+
INDI::CCD::UpdateCCDFrame(x*bin_x, y*bin_y, w*bin_x, h*bin_y);
94124

95125
/* Sanity checks, reserve buffers */
96-
[[maybe_unused]] int const width = this->camera->get_width().val();
97-
[[maybe_unused]] int const height = this->camera->get_height().val();
98126
int const frame_byte_size = this->camera->get_frame_byte_size();
99-
int const indi_bufsize = PrimaryCCD.getSubW() * PrimaryCCD.getSubH() * PrimaryCCD.getBPP() / 8;
127+
int const indi_bufsize = (PrimaryCCD.getSubW() / PrimaryCCD.getBinX())
128+
* (PrimaryCCD.getSubH() / PrimaryCCD.getBinY())
129+
* PrimaryCCD.getBPP() / 8;
100130

101131
if (indi_bufsize != frame_byte_size)
102132
{
@@ -111,7 +141,6 @@ bool GigECCD::_update_geometry(void)
111141
}
112142

113143
this->Streamer->setPixelFormat(INDI_MONO, PrimaryCCD.getBPP());
114-
this->Streamer->setSize(PrimaryCCD.getXRes(), PrimaryCCD.getYRes());
115144
return true;
116145
}
117146

@@ -173,6 +202,7 @@ bool GigECCD::updateProperties()
173202
this->camera->get_bpp().val(), this->camera->get_pixel_pitch().val(),
174203
this->camera->get_pixel_pitch().val());
175204

205+
this->_update_bin();
176206
(void)this->_update_geometry();
177207
this->timer_id = this->SetTimer(this->getCurrentPollingPeriod());
178208
}
@@ -461,10 +491,19 @@ bool GigECCD::ISNewNumber(const char *dev, const char *name, double values[], ch
461491

462492
bool GigECCD::UpdateCCDFrame(int x, int y, int w, int h)
463493
{
464-
LOGF_INFO("%s x=%i y=%i w=%i h=%i", __PRETTY_FUNCTION__, x, y, w, h);
465-
466494
std::lock_guard<std::recursive_mutex> lock(this->camera_mutex);
467-
this->camera->set_geometry(x, y, w, h);
495+
496+
/* As per note at begin of this file, we manually manage the region of
497+
* interest conversion between the camera and INDI because the camera
498+
* changes the apparent region of interest available depending on binning,
499+
* while INDI does not. */
500+
auto bin_x = this->camera->get_bin_x().val(),
501+
bin_y = this->camera->get_bin_y().val();
502+
503+
LOGF_INFO("%s x=%i y=%i w=%i h=%i binx=%i biny=%i",
504+
__PRETTY_FUNCTION__, x, y, w, h, bin_x, bin_y);
505+
506+
this->camera->set_geometry(x/bin_x, y/bin_y, w/bin_x, h/bin_y);
468507
return this->_update_geometry();
469508
}
470509

@@ -473,9 +512,17 @@ bool GigECCD::UpdateCCDBin(int binx, int biny)
473512
LOGF_INFO("%s binx=%i biny=%i", __PRETTY_FUNCTION__, binx, biny);
474513
std::lock_guard<std::recursive_mutex> lock(this->camera_mutex);
475514
camera->set_bin(binx, biny);
515+
this->_update_bin();
476516
return UpdateCCDFrame(PrimaryCCD.getSubX(), PrimaryCCD.getSubY(), PrimaryCCD.getSubW(), PrimaryCCD.getSubH());
477517
}
478518

519+
void GigECCD::_update_bin(void) {
520+
auto [bx, by] = this->camera->update_bin(); /* Get actual values */
521+
522+
/* Sync actuals values to INDI */
523+
INDI::CCD::UpdateCCDBin(bx, by);
524+
}
525+
479526
bool GigECCD::UpdateCCDFrameType(INDI::CCDChip::CCD_FRAME fType)
480527
{
481528
LOGF_INFO("%s", __PRETTY_FUNCTION__);

indi-gige/src/indi_gige.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ class GigECCD : public INDI::CCD
6565
private:
6666
void _delete_indi_properties(void);
6767
void _update_indi_properties(void);
68-
bool _update_geometry(void);
68+
void _update_bin(void); /// update binning to INDI from hardware
69+
bool _update_geometry(void); /// update geometry to INDI from hardware
6970
void _update_image(uint8_t const *const data, size_t size);
7071

7172
void _handle_failed(void);

0 commit comments

Comments
 (0)