diff --git a/pkg/fits/fits.go b/pkg/fits/fits.go index d50c05d..18b21c3 100644 --- a/pkg/fits/fits.go +++ b/pkg/fits/fits.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/fits +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package fits +/*****************************************************************************************************************/ + import ( "bytes" "encoding/binary" @@ -16,9 +26,14 @@ import ( "github.com/observerly/iris/pkg/utils" ) +/*****************************************************************************************************************/ + const FITS_STANDARD = "FITS Standard 4.0" -// FITS Image struct: +/*****************************************************************************************************************/ + +// Represents a FITS Image +// // @see https://fits.gsfc.nasa.gov/fits_primer.html // @see https://fits.gsfc.nasa.gov/standard40/fits_standard40aa-le.pdf type FITSImage struct { @@ -36,7 +51,10 @@ type FITSImage struct { Stats *stats.Stats // Image statistics (mean, min, max, stdDev etc) } -// FITS Observation struct: +/*****************************************************************************************************************/ + +// Represents a FITS Observation +// // @see https://fits.gsfc.nasa.gov/standard40/fits_standard40aa-le.pdf type FITSObservation struct { DateObs time.Time `json:"dateObs"` // Date of observation e.g., 2022-05-15 @@ -54,12 +72,17 @@ type FITSObservation struct { Observer string `json:"observer"` // Who acquired the data } +/*****************************************************************************************************************/ + +// Represents a FITS Observer type FITSObserver struct { Latitude float32 `json:"latitude"` // Latitude of the observer Longitude float32 `json:"longitude"` // Longitude of the observer Elevation float32 `json:"elevation"` // Elevation of the observer } +/*****************************************************************************************************************/ + // Creates a new instance of FITS image initialized with empty header func NewFITSImage(naxis int32, naxis1 int32, naxis2 int32, adu int32) *FITSImage { h := NewFITSHeader(naxis, naxis1, naxis2) @@ -97,6 +120,8 @@ func NewFITSImage(naxis int32, naxis1 int32, naxis2 int32, adu int32) *FITSImage } } +/*****************************************************************************************************************/ + // Creates a new instance of FITS image initialized from an io.Reader: func NewFITSImageFromReader(r io.Reader) *FITSImage { // Construct a blank FITS Image: @@ -112,6 +137,8 @@ func NewFITSImageFromReader(r io.Reader) *FITSImage { return f } +/*****************************************************************************************************************/ + // Creates a new instance of FITS image from given 2D exposure array // (Data is not copied, allocated if nil. naxisn is deep copied) func NewFITSImageFrom2DData(ex [][]uint32, naxis int32, naxis1 int32, naxis2 int32, adu int32) *FITSImage { @@ -168,6 +195,8 @@ func NewFITSImageFrom2DData(ex [][]uint32, naxis int32, naxis1 int32, naxis2 int } } +/*****************************************************************************************************************/ + func (f *FITSImage) AddObservationEntry(observation *FITSObservation) *FITSImage { f.Header.Dates["DATE-OBS"] = struct { Value string @@ -295,6 +324,8 @@ func (f *FITSImage) AddObservationEntry(observation *FITSObservation) *FITSImage return f } +/*****************************************************************************************************************/ + func (f *FITSImage) AddObserverEntry(observer *FITSObserver) *FITSImage { f.Header.Floats["LATITUDE"] = struct { Value float32 @@ -323,6 +354,8 @@ func (f *FITSImage) AddObserverEntry(observer *FITSObserver) *FITSImage { return f } +/*****************************************************************************************************************/ + func (f *FITSImage) ExtractHFR(radius float32, sigma float32, starInOut float32) float32 { se := photometry.NewStarsExtractor(f.Data, int(f.Naxisn[0]), int(f.Naxisn[1]), radius, f.ADU) @@ -333,6 +366,8 @@ func (f *FITSImage) ExtractHFR(radius float32, sigma float32, starInOut float32) return se.HFR } +/*****************************************************************************************************************/ + func (f *FITSImage) ReadFromFile(fp string) error { // Check that the filename is not empty: if fp == "" { @@ -355,6 +390,8 @@ func (f *FITSImage) ReadFromFile(fp string) error { return f.Read(file) } +/*****************************************************************************************************************/ + // Read the FITS image from the given file. func (f *FITSImage) Read(r io.Reader) error { // Read Header: @@ -435,6 +472,8 @@ func (f *FITSImage) Read(r io.Reader) error { return nil } +/*****************************************************************************************************************/ + // Writes an in-memory FITS image to an io.Writer output stream func (f *FITSImage) WriteToBuffer() (*bytes.Buffer, error) { buf := new(bytes.Buffer) @@ -456,6 +495,8 @@ func (f *FITSImage) WriteToBuffer() (*bytes.Buffer, error) { return buf, nil } +/*****************************************************************************************************************/ + // Writes FITS binary body data in network byte order to buffer func writeFloat32ArrayToBuffer(buf *bytes.Buffer, data []float32) (*bytes.Buffer, error) { err := binary.Write(buf, binary.BigEndian, data) @@ -486,16 +527,10 @@ func writeFloat32ArrayToBuffer(buf *bytes.Buffer, data []float32) (*bytes.Buffer return buf, nil } -/* -* - - Reads the FITS binary data from the given io.Reader stream and returns a - slice of float32 values, or error - - Note: The data is read in network byte order and only supports 32-bitpix data +/*****************************************************************************************************************/ -* -*/ +// Reads the FITS binary data from the given io.Reader stream and returns a slice of float32 values, or error. +// Note: The data is read in network byte order and only supports 32-bitpix data func readData(r io.Reader, bitpix int32, pixels int32) ([]float32, error) { data := make([]float32, pixels) @@ -543,7 +578,11 @@ func readData(r io.Reader, bitpix int32, pixels int32) ([]float32, error) { return data, err } +/*****************************************************************************************************************/ + // Reads FITS binary body float32 data in network byte order from buffer func readFloat32ArrayFromBuffer(buf *bytes.Buffer, data []float32) error { return binary.Read(buf, binary.BigEndian, data) } + +/*****************************************************************************************************************/ diff --git a/pkg/fits/fits_test.go b/pkg/fits/fits_test.go index 2c777d6..9bddafc 100644 --- a/pkg/fits/fits_test.go +++ b/pkg/fits/fits_test.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/fits +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package fits +/*****************************************************************************************************************/ + import ( "bytes" "image" @@ -11,6 +21,8 @@ import ( "time" ) +/*****************************************************************************************************************/ + func GetTestDataFromImage() ([][]uint32, image.Rectangle) { f, err := os.Open("../../images/noise16.jpeg") @@ -46,6 +58,8 @@ func GetTestDataFromImage() ([][]uint32, image.Rectangle) { return data, bounds } +/*****************************************************************************************************************/ + func TestNewDefaultFITSImageHeaderEnd(t *testing.T) { var img = NewFITSImage(2, 600, 800, 65535) @@ -58,6 +72,8 @@ func TestNewDefaultFITSImageHeaderEnd(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewDefaultFITSImageBScale(t *testing.T) { var img = NewFITSImage(2, 600, 800, 65535) @@ -70,6 +86,8 @@ func TestNewDefaultFITSImageBScale(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFITSImageFromReader(t *testing.T) { // Attempt to open the file from the given filepath: file, err := os.Open("../../samples/noise16.fits") @@ -114,6 +132,8 @@ func TestNewFITSImageFromReader(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFITSImageFrom2DDataID(t *testing.T) { var ex = [][]uint32{ {1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -145,6 +165,8 @@ func TestNewFITSImageFrom2DDataID(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFITSImageFrom2DDataPixels(t *testing.T) { var ex = [][]uint32{ {1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -176,6 +198,8 @@ func TestNewFITSImageFrom2DDataPixels(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFITSImageFrom2DDataData(t *testing.T) { var ex = [][]uint32{ {1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -207,6 +231,8 @@ func TestNewFITSImageFrom2DDataData(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFITSImageFrom2DDataWriteFloatData(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -233,6 +259,8 @@ func TestNewFITSImageFrom2DDataWriteFloatData(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFITSImageFrom2DDataWrite(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -267,6 +295,9 @@ func TestNewFITSImageFrom2DDataWrite(t *testing.T) { t.Errorf("Error writing image: %s", err) } } + +/*****************************************************************************************************************/ + func TestNewFITSImageFrom2DStats(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -303,6 +334,8 @@ func TestNewFITSImageFrom2DStats(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFITSRead(t *testing.T) { var fit = NewFITSImage(2, 1, 1, 65535) @@ -370,6 +403,8 @@ func TestNewFITSRead(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFITSFromFile(t *testing.T) { var fit = NewFITSImage(2, 1, 1, 65535) @@ -413,6 +448,8 @@ func TestNewFITSFromFile(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFindStarsFrom2DData(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -441,6 +478,8 @@ func TestNewFindStarsFrom2DData(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewAddObservationEntry(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -528,6 +567,8 @@ func TestNewAddObservationEntry(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewAddObserverEntry(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -584,3 +625,5 @@ func TestNewAddObserverEntry(t *testing.T) { t.Errorf("Error writing image: %s", err) } } + +/*****************************************************************************************************************/ diff --git a/pkg/fits/header.go b/pkg/fits/header.go index 7553840..9094d05 100644 --- a/pkg/fits/header.go +++ b/pkg/fits/header.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/fits +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package fits +/*****************************************************************************************************************/ + import ( "bytes" "fmt" @@ -10,29 +20,41 @@ import ( "time" ) +/*****************************************************************************************************************/ + // Regular expression parser for FITS header lines: var re *regexp.Regexp = compileFITSHeaderRegEx() +/*****************************************************************************************************************/ + type FITSHeaderBool struct { Value bool Comment string } +/*****************************************************************************************************************/ + type FITSHeaderInt struct { Value int32 Comment string } +/*****************************************************************************************************************/ + type FITSHeaderFloat struct { Value float32 Comment string } +/*****************************************************************************************************************/ + type FITSHeaderString struct { Value string Comment string } +/*****************************************************************************************************************/ + // FITS Header struct: type FITSHeader struct { Bitpix int32 @@ -50,6 +72,8 @@ type FITSHeader struct { Length int32 } +/*****************************************************************************************************************/ + // Create a new instance of FITS header: func NewFITSHeader(naxis int32, naxis1 int32, naxis2 int32) FITSHeader { h := FITSHeader{ @@ -106,6 +130,8 @@ func NewFITSHeader(naxis int32, naxis1 int32, naxis2 int32) FITSHeader { return h } +/*****************************************************************************************************************/ + func (h *FITSHeader) Read(r io.Reader) error { block := make([]byte, 2880) @@ -139,10 +165,11 @@ func (h *FITSHeader) Read(r io.Reader) error { return nil } -/* -Writes a FITS header according to the FITS standard to output bytes buffer -@see https://fits.gsfc.nasa.gov/standard40/fits_standard40aa-le.pdf -*/ +/*****************************************************************************************************************/ + +// Writes a FITS header according to the FITS standard to output bytes buffer +// +// @see https://fits.gsfc.nasa.gov/standard40/fits_standard40aa-le.pdf func (h *FITSHeader) WriteToBuffer(buf *bytes.Buffer) (*bytes.Buffer, error) { // SIMPLE needs to be the leading HDR value: writeBool(buf, "SIMPLE", true, FITS_STANDARD) @@ -194,6 +221,8 @@ func (h *FITSHeader) WriteToBuffer(buf *bytes.Buffer) (*bytes.Buffer, error) { return buf, nil } +/*****************************************************************************************************************/ + // Reads a FITS header line by line and returns a FITSHeader struct func (h *FITSHeader) ParseLine(subNames []string, subValues [][]byte) error { // The KEY will always be a string of maximum 8 characters: @@ -329,6 +358,8 @@ func (h *FITSHeader) ParseLine(subNames []string, subValues [][]byte) error { return nil } +/*****************************************************************************************************************/ + // Writes a FITS header boolean T/F value func writeBool(w io.Writer, key string, value bool, comment string) { if len(key) > 8 { @@ -350,6 +381,8 @@ func writeBool(w io.Writer, key string, value bool, comment string) { fmt.Fprintf(w, "%-8s= %20s / %-47s", key, v, comment) } +/*****************************************************************************************************************/ + // Writes a FITS header string value, with escaping and continuations if necessary. func writeString(w io.Writer, key, value, comment string) { if len(key) > 8 { @@ -378,6 +411,8 @@ func writeString(w io.Writer, key, value, comment string) { } } +/*****************************************************************************************************************/ + // Writes a FITS header integer value func writeInt(w io.Writer, key string, value int32, comment string) { if len(key) > 8 { @@ -391,6 +426,8 @@ func writeInt(w io.Writer, key string, value int32, comment string) { fmt.Fprintf(w, "%-8s= %20d / %-47s", key, value, comment) } +/*****************************************************************************************************************/ + // Writes a FITS header float value func writeFloat(w io.Writer, key string, value float32, comment string) { if len(key) > 8 { @@ -450,6 +487,8 @@ func compileFITSHeaderRegEx() *regexp.Regexp { return regexp.MustCompile(lineRe) } +/*****************************************************************************************************************/ + // This function adds a line feed character to the end of each 80-byte chunk in the data array. func (h *FITSHeader) AddLineFeedCharacteToHeaderRow(data []byte, lineEnding string) []byte { // Create a new buffer: @@ -474,3 +513,5 @@ func (h *FITSHeader) AddLineFeedCharacteToHeaderRow(data []byte, lineEnding stri return b.Bytes() } + +/*****************************************************************************************************************/ diff --git a/pkg/fits/header_test.go b/pkg/fits/header_test.go index a3a4171..13378a3 100644 --- a/pkg/fits/header_test.go +++ b/pkg/fits/header_test.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/fits +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package fits +/*****************************************************************************************************************/ + import ( "bytes" "os" @@ -8,6 +18,8 @@ import ( "testing" ) +/*****************************************************************************************************************/ + func TestNewDefaultFITSHeaderEnd(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -20,6 +32,8 @@ func TestNewDefaultFITSHeaderEnd(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewDefaultFITSHeaderWriteBoolean(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -41,6 +55,8 @@ func TestNewDefaultFITSHeaderWriteBoolean(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewDefaultFITSHeaderWriteString(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -62,6 +78,8 @@ func TestNewDefaultFITSHeaderWriteString(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewDefaultFITSHeaderWriteStringContinue(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -83,6 +101,8 @@ func TestNewDefaultFITSHeaderWriteStringContinue(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewDefaultFITSHeaderWriteDate(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -104,6 +124,8 @@ func TestNewDefaultFITSHeaderWriteDate(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewDefaultFITSHeaderWriteInt(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -128,6 +150,8 @@ func TestNewDefaultFITSHeaderWriteInt(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewDefaultFITSHeaderWriteFloat(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -152,6 +176,8 @@ func TestNewDefaultFITSHeaderWriteFloat(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewDefaultFITSHeaderWriteEnd(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -181,10 +207,14 @@ func TestNewDefaultFITSHeaderWriteEnd(t *testing.T) { } } +/*****************************************************************************************************************/ + func GetRegexSubValuesAndSubNames(str []byte) ([][]byte, []string) { return re.FindSubmatch(str), re.SubexpNames() } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderRegExpSIMPLEKey(t *testing.T) { values, names := GetRegexSubValuesAndSubNames([]byte("SIMPLE = T / FITS Standard 4.0")) @@ -208,6 +238,8 @@ func TestCompileFITSHeaderRegExpSIMPLEKey(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderRegExpSIMPLEValue(t *testing.T) { values, names := GetRegexSubValuesAndSubNames([]byte("SIMPLE = T / FITS Standard 4.0")) @@ -231,6 +263,8 @@ func TestCompileFITSHeaderRegExpSIMPLEValue(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderRegExpSIMPLEComment(t *testing.T) { values, names := GetRegexSubValuesAndSubNames([]byte("SIMPLE = T / FITS Standard 4.0")) @@ -254,6 +288,8 @@ func TestCompileFITSHeaderRegExpSIMPLEComment(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderRegExpNAXIS1Key(t *testing.T) { values, names := GetRegexSubValuesAndSubNames([]byte("NAXIS1 = 6000 / Number of pixels in axis 1")) @@ -277,6 +313,8 @@ func TestCompileFITSHeaderRegExpNAXIS1Key(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderRegExpNAXIS1Value(t *testing.T) { values, names := GetRegexSubValuesAndSubNames([]byte("NAXIS1 = 6000 / Number of pixels in axis 1")) @@ -306,6 +344,8 @@ func TestCompileFITSHeaderRegExpNAXIS1Value(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderRegExpNAXIS1Comment(t *testing.T) { values, names := GetRegexSubValuesAndSubNames([]byte("NAXIS1 = 6000 / [1] Length of data axis 1")) @@ -329,6 +369,8 @@ func TestCompileFITSHeaderRegExpNAXIS1Comment(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderRegExpSENSORKey(t *testing.T) { values, names := GetRegexSubValuesAndSubNames([]byte("SENSOR = 'Monochrome' / ASCOM Alpaca Sensor Type")) @@ -352,6 +394,8 @@ func TestCompileFITSHeaderRegExpSENSORKey(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderRegExpSENSORValue(t *testing.T) { values, names := GetRegexSubValuesAndSubNames([]byte("SENSOR = 'Monochrome' / ASCOM Alpaca Sensor Type")) @@ -375,6 +419,8 @@ func TestCompileFITSHeaderRegExpSENSORValue(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderRegExpSENSORComment(t *testing.T) { values, names := GetRegexSubValuesAndSubNames([]byte("SENSOR = 'Monochrome' / ASCOM Alpaca Sensor Type")) @@ -398,6 +444,8 @@ func TestCompileFITSHeaderRegExpSENSORComment(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderRegExpEND(t *testing.T) { values, names := GetRegexSubValuesAndSubNames([]byte("END")) @@ -421,6 +469,8 @@ func TestCompileFITSHeaderRegExpEND(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderParseLineBool(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -443,6 +493,8 @@ func TestCompileFITSHeaderParseLineBool(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderParseLineInt32(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -465,6 +517,8 @@ func TestCompileFITSHeaderParseLineInt32(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderParseLineFloat32(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -487,6 +541,8 @@ func TestCompileFITSHeaderParseLineFloat32(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderParseLineString(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -509,6 +565,8 @@ func TestCompileFITSHeaderParseLineString(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCompileFITSHeaderParseDate(t *testing.T) { var header = NewFITSHeader(2, 600, 800) @@ -531,6 +589,8 @@ func TestCompileFITSHeaderParseDate(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestReadHeaderFromFile(t *testing.T) { // Attempt to open the file from the given filename: file, err := os.Open("../../samples/noise16.fits") @@ -602,3 +662,5 @@ func TestReadHeaderFromFile(t *testing.T) { t.Errorf("ReadHeaderFromFile() expected Length to be divisible by 2880: but got %v", h.Length) } } + +/*****************************************************************************************************************/ diff --git a/pkg/frames/bias.go b/pkg/frames/bias.go index 284e3fb..aa907c0 100644 --- a/pkg/frames/bias.go +++ b/pkg/frames/bias.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/frames +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package frames +/*****************************************************************************************************************/ + import ( "time" @@ -7,20 +17,16 @@ import ( "github.com/observerly/iris/pkg/utils" ) -/* -NewMasterBiasFrame() +/*****************************************************************************************************************/ -Creates a new master bias frame from a slice of bias frames. - -The idea of a bias frame is to take a series of exposures with the shutter closed, -for the shortest exposure resolution supported by the camera with no light falling -on the sensor. The resulting images are then averaged to produce a master bias frame. - -The master bias frame is then created by taking the mean of all the bias frames. - -@retuns a new FITSImage containing the master bias frame. -@see Image Calibration & Stack Woodhouse, C. (2017). The Astrophotography Manual. Taylor & Francis. p.203 -*/ +// Creates a new master bias frame from a slice of bias frames. +// The idea of a bias frame is to take a series of exposures with the shutter closed, for the shortest exposure +// resolution supported by the camera with no light falling on the sensor. The resulting images are then averaged +// to produce a master bias frame, which is then subtracted from all subsequent images to remove the bias noise. +// +// The master bias frame is then created by taking the mean of all the bias frames. +// +// @see Image Calibration & Stack Woodhouse, C. (2017). The Astrophotography Manual. Taylor & Francis. p.203 func NewMasterBiasFrame(frames []fits.FITSImage, naxis int32, naxis1 int32, naxis2 int32, adu int32, resolution float32) (*MasterFrame, error) { pixels := naxis1 * naxis2 @@ -92,3 +98,5 @@ func NewMasterBiasFrame(frames []fits.FITSImage, naxis int32, naxis1 int32, naxi CreatedTimestamp: time.Now().Unix(), }, nil } + +/*****************************************************************************************************************/ diff --git a/pkg/frames/bias_test.go b/pkg/frames/bias_test.go index 88a257e..ef81576 100644 --- a/pkg/frames/bias_test.go +++ b/pkg/frames/bias_test.go @@ -1,11 +1,23 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/frames +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package frames +/*****************************************************************************************************************/ + import ( "testing" "github.com/observerly/iris/pkg/fits" ) +/*****************************************************************************************************************/ + func TestNewMasterBiasFrame(t *testing.T) { var bias = [][]uint32{ {1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -59,6 +71,8 @@ func TestNewMasterBiasFrame(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestApplyFrameToMasterBiasFrame(t *testing.T) { var bias = [][]uint32{ {1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -154,3 +168,5 @@ func TestApplyFrameToMasterBiasFrame(t *testing.T) { t.Errorf("NewMasterBiasFrame() failed: expected data[0] of 6, got %f", masterBias.Combined.Data[0]) } } + +/*****************************************************************************************************************/ diff --git a/pkg/histogram/histogram.go b/pkg/histogram/histogram.go index f0e87db..447eb2b 100644 --- a/pkg/histogram/histogram.go +++ b/pkg/histogram/histogram.go @@ -1,22 +1,35 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/histogram +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package histogram +/*****************************************************************************************************************/ + import ( "image" "github.com/observerly/iris/pkg/utils" ) +/*****************************************************************************************************************/ + const hsize8 = 256 +/*****************************************************************************************************************/ + const hsize16 = 65535 -/* - HistogramGray +/*****************************************************************************************************************/ - Computes the histogram for a grayscale image, and bins the pixel values according to their accumulated count. +// Computes the histogram for a grayscale image, and bins the pixel values according to their accumulated count. +// +// Returns an array of 256 uint64 values containing a distribution of the pixel values. - Returns an array of 256 uint64 values containing a distribution of the pixel values. -*/ func HistogramGray(img *image.Gray) [hsize8]uint64 { bounds := img.Bounds() @@ -32,12 +45,14 @@ func HistogramGray(img *image.Gray) [hsize8]uint64 { return res } +/*****************************************************************************************************************/ + /* - HistogramGray16 +HistogramGray16 - Computes the histogram for a 16 bit grayscale image, and bins the pixel values according to their accumulated count. +Computes the histogram for a 16 bit grayscale image, and bins the pixel values according to their accumulated count. - Returns an array of 256 uint64 values containing a distribution of the pixel values. +Returns an array of 256 uint64 values containing a distribution of the pixel values. */ func HistogramGray16(img *image.Gray16) [hsize16]uint64 { bounds := img.Bounds() diff --git a/pkg/histogram/histogram_test.go b/pkg/histogram/histogram_test.go index 87289de..3eb1dd8 100644 --- a/pkg/histogram/histogram_test.go +++ b/pkg/histogram/histogram_test.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/histogram +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package histogram +/*****************************************************************************************************************/ + import ( "image" "image/color" @@ -9,6 +19,8 @@ import ( "testing" ) +/*****************************************************************************************************************/ + func TestNewHistogramGray(t *testing.T) { f, err := os.Open("../../images/noise.jpeg") @@ -55,6 +67,8 @@ func TestNewHistogramGray(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewHistogramGray16(t *testing.T) { f, err := os.Open("../../images/noise16.jpeg") @@ -102,3 +116,5 @@ func TestNewHistogramGray16(t *testing.T) { t.Errorf("Histogram maximum is not 256") } } + +/*****************************************************************************************************************/ diff --git a/pkg/iris/monochrome.go b/pkg/iris/monochrome.go index 2712f92..950661a 100644 --- a/pkg/iris/monochrome.go +++ b/pkg/iris/monochrome.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package iris +/*****************************************************************************************************************/ + import ( "bytes" "image" @@ -11,6 +21,8 @@ import ( "github.com/observerly/iris/pkg/photometry" ) +/*****************************************************************************************************************/ + type MonochromeExposure struct { Width int Height int @@ -26,6 +38,8 @@ type MonochromeExposure struct { Pixels int } +/*****************************************************************************************************************/ + func NewMonochromeExposure(exposure [][]uint32, adu int32, xs int, ys int) MonochromeExposure { pixels := xs * ys @@ -46,6 +60,8 @@ func NewMonochromeExposure(exposure [][]uint32, adu int32, xs int, ys int) Monoc return mono } +/*****************************************************************************************************************/ + func (m *MonochromeExposure) GetBuffer(img *image.Gray) (bytes.Buffer, error) { var buff bytes.Buffer @@ -58,6 +74,8 @@ func (m *MonochromeExposure) GetBuffer(img *image.Gray) (bytes.Buffer, error) { return buff, nil } +/*****************************************************************************************************************/ + func (m *MonochromeExposure) GetFITSImage() *fits.FITSImage { f := fits.NewFITSImageFrom2DData( m.Raw, @@ -78,6 +96,8 @@ func (m *MonochromeExposure) GetFITSImage() *fits.FITSImage { return f } +/*****************************************************************************************************************/ + func (m *MonochromeExposure) GetOtsuThresholdValue(img *image.Gray, size image.Point, histogram [256]uint64) uint8 { var threshold uint8 @@ -119,22 +139,10 @@ func (m *MonochromeExposure) GetOtsuThresholdValue(img *image.Gray, size image.P return threshold } -/* - PreprocessImageArray() - - Preprocesses an ASCOM Alpaca Image Array to a m.Raw 2D array of uint32 values. - Converts the 2D array of uint16 values to a 2D array of uint32 values. - - @returns a bytes.Buffer containing the preprocessed image. - @see https://ascom-standards.org/api/#/Camera%20Specific%20Methods/get_camera__device_number__imagearray - - "... "column-major" order (column changes most rapidly) from the image's row and column - perspective, while, from the array's perspective, serialisation is actually effected in - "row-major" order (rightmost index changes most rapidly). This unintuitive outcome arises - because the ASCOM Camera Interface specification defines the image column dimension as - the rightmost array dimension." +/*****************************************************************************************************************/ -*/ +// Preprocesses an ASCOM Alpaca Image Array to a m.Raw 2D array of uint32 values. Converts the 2D array of +// uint16 values to a 2D array of uint32 values, returning a bytes.Buffer containing the preprocessed image. func (m *MonochromeExposure) PreprocessImageArray(xs int, ys int) (bytes.Buffer, error) { // Switch the columns and rows in the image: ex := make([][]uint32, xs) @@ -168,6 +176,8 @@ func (m *MonochromeExposure) PreprocessImageArray(xs int, ys int) (bytes.Buffer, return m.Preprocess() } +/*****************************************************************************************************************/ + func (m *MonochromeExposure) Preprocess() (bytes.Buffer, error) { bounds := m.Image.Bounds() @@ -184,6 +194,8 @@ func (m *MonochromeExposure) Preprocess() (bytes.Buffer, error) { return m.GetBuffer(m.Image) } +/*****************************************************************************************************************/ + func (m *MonochromeExposure) ApplyNoiseReduction() (bytes.Buffer, error) { bounds := m.Image.Bounds() @@ -213,6 +225,8 @@ func (m *MonochromeExposure) ApplyNoiseReduction() (bytes.Buffer, error) { return m.GetBuffer(m.Image) } +/*****************************************************************************************************************/ + func (m *MonochromeExposure) ApplyOtsuThreshold() (bytes.Buffer, error) { bounds := m.Image.Bounds() @@ -241,3 +255,5 @@ func (m *MonochromeExposure) ApplyOtsuThreshold() (bytes.Buffer, error) { return m.GetBuffer(m.Otsu) } + +/*****************************************************************************************************************/ diff --git a/pkg/iris/monochrome16.go b/pkg/iris/monochrome16.go index 4a73683..eddff8f 100644 --- a/pkg/iris/monochrome16.go +++ b/pkg/iris/monochrome16.go @@ -1,3 +1,11 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package iris import ( @@ -119,20 +127,19 @@ func (m *Monochrome16Exposure) GetOtsuThresholdValue(img *image.Gray16, size ima } /* - PreprocessImageArray() - - Preprocesses an ASCOM Alpaca Image Array to a m.Raw 2D array of uint32 values. - Converts the 2D array of uint16 values to a 2D array of uint32 values. + PreprocessImageArray() - @returns a bytes.Buffer containing the preprocessed image. - @see https://ascom-standards.org/api/#/Camera%20Specific%20Methods/get_camera__device_number__imagearray + Preprocesses an ASCOM Alpaca Image Array to a m.Raw 2D array of uint32 values. + Converts the 2D array of uint16 values to a 2D array of uint32 values. - "... "column-major" order (column changes most rapidly) from the image's row and column - perspective, while, from the array's perspective, serialisation is actually effected in - "row-major" order (rightmost index changes most rapidly). This unintuitive outcome arises - because the ASCOM Camera Interface specification defines the image column dimension as - the rightmost array dimension." + @returns a bytes.Buffer containing the preprocessed image. + @see https://ascom-standards.org/api/#/Camera%20Specific%20Methods/get_camera__device_number__imagearray + "... "column-major" order (column changes most rapidly) from the image's row and column + perspective, while, from the array's perspective, serialisation is actually effected in + "row-major" order (rightmost index changes most rapidly). This unintuitive outcome arises + because the ASCOM Camera Interface specification defines the image column dimension as + the rightmost array dimension." */ func (m *Monochrome16Exposure) PreprocessImageArray(xs int, ys int) (bytes.Buffer, error) { // Switch the columns and rows in the image: diff --git a/pkg/iris/monochrome16_test.go b/pkg/iris/monochrome16_test.go index e31de60..a65e454 100644 --- a/pkg/iris/monochrome16_test.go +++ b/pkg/iris/monochrome16_test.go @@ -1,15 +1,28 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package iris +/*****************************************************************************************************************/ + import ( "encoding/json" "image/jpeg" - "io/ioutil" "os" "testing" ) +/*****************************************************************************************************************/ + var ex16 = [][]uint32{} +/*****************************************************************************************************************/ + func TestNewMonochrome16ExposureWidth(t *testing.T) { mono := NewMonochrome16Exposure(ex16, 1, 800, 600) @@ -22,6 +35,8 @@ func TestNewMonochrome16ExposureWidth(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochrome16ExposureHeight(t *testing.T) { mono := NewMonochrome16Exposure(ex16, 1, 800, 600) @@ -34,6 +49,8 @@ func TestNewMonochrome16ExposureHeight(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochrome16ExposurePixels(t *testing.T) { mono := NewMonochrome16Exposure(ex16, 1, 800, 600) @@ -46,6 +63,8 @@ func TestNewMonochrome16ExposurePixels(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochrome16ExposureGetBuffer(t *testing.T) { mono := NewMonochrome16Exposure(ex16, 1, 800, 600) @@ -56,6 +75,8 @@ func TestNewMonochrome16ExposureGetBuffer(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochrome16ExposurePreprocess4x4(t *testing.T) { var ex = [][]uint32{ {6, 6, 6, 6}, @@ -110,6 +131,8 @@ func TestNewMonochrome16ExposurePreprocess4x4(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochrome16ExposurePreprocess16x16(t *testing.T) { var ex = [][]uint32{ {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -176,6 +199,8 @@ func TestNewMonochrome16ExposurePreprocess16x16(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewNoiseExtractorGaussianNoise16PngImage(t *testing.T) { f, err := os.Open("../../images/noise16.jpeg") @@ -244,6 +269,8 @@ func TestNewNoiseExtractorGaussianNoise16PngImage(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochrome16ExposureOtsuThreshold(t *testing.T) { var ex = [][]uint32{ {6, 6, 6, 6, 6, 6, 6, 6, 9, 6, 6, 6, 6, 6, 6, 6}, @@ -312,6 +339,8 @@ func TestNewMonochrome16ExposureOtsuThreshold(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochrome16ExposureNoiseReduction16x16(t *testing.T) { var ex = [][]uint32{ {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -388,6 +417,8 @@ func TestNewMonochrome16ExposureNoiseReduction16x16(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochrome16ExposureGetFITSImage(t *testing.T) { f, err := os.Open("../../images/noise16.jpeg") @@ -476,6 +507,8 @@ func TestNewMonochrome16ExposureGetFITSImage(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochrome16ExposureFromASCOMGetFITSImage(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -488,7 +521,7 @@ func TestNewMonochrome16ExposureFromASCOMGetFITSImage(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-monochrome.json") + file, err := os.ReadFile("../../data/m42-800x600-monochrome.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -557,3 +590,5 @@ func TestNewMonochrome16ExposureFromASCOMGetFITSImage(t *testing.T) { t.Errorf("Error writing image: %s", err) } } + +/*****************************************************************************************************************/ diff --git a/pkg/iris/monochrome_test.go b/pkg/iris/monochrome_test.go index f9dff43..5366724 100644 --- a/pkg/iris/monochrome_test.go +++ b/pkg/iris/monochrome_test.go @@ -1,17 +1,30 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package iris +/*****************************************************************************************************************/ + import ( "encoding/json" "image/jpeg" - "io/ioutil" "os" "testing" "github.com/observerly/iris/pkg/histogram" ) +/*****************************************************************************************************************/ + var ex = [][]uint32{} +/*****************************************************************************************************************/ + func TestNewMonochromeExposureWidth(t *testing.T) { mono := NewMonochromeExposure(ex, 1, 800, 600) @@ -24,6 +37,8 @@ func TestNewMonochromeExposureWidth(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochromeExposureHeight(t *testing.T) { mono := NewMonochromeExposure(ex, 1, 800, 600) @@ -36,6 +51,8 @@ func TestNewMonochromeExposureHeight(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochromeExposurePixels(t *testing.T) { mono := NewMonochromeExposure(ex, 1, 800, 600) @@ -48,6 +65,8 @@ func TestNewMonochromeExposurePixels(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochromeExposurePreprocess4x4(t *testing.T) { var ex = [][]uint32{ {6, 6, 6, 6}, @@ -102,6 +121,8 @@ func TestNewMonochromeExposurePreprocess4x4(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochromeExposurePreprocess16x16(t *testing.T) { var ex = [][]uint32{ {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -168,6 +189,8 @@ func TestNewMonochromeExposurePreprocess16x16(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochromeExposureOtsuThreshold(t *testing.T) { var ex = [][]uint32{ {6, 6, 6, 6, 6, 6, 6, 6, 9, 6, 6, 6, 6, 6, 6, 6}, @@ -236,6 +259,8 @@ func TestNewMonochromeExposureOtsuThreshold(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochromeExposureNoiseReduction16x16(t *testing.T) { var ex = [][]uint32{ {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -312,6 +337,8 @@ func TestNewMonochromeExposureNoiseReduction16x16(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochromeExposureHistogramGray(t *testing.T) { var ex = [][]uint32{ {1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -359,6 +386,8 @@ func TestNewMonochromeExposureHistogramGray(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewNoiseExtractorGaussianNoisePngImage(t *testing.T) { f, err := os.Open("../../images/noise.jpeg") @@ -427,6 +456,8 @@ func TestNewNoiseExtractorGaussianNoisePngImage(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochrome16NoiseExtractorGaussianNoise16PngImage(t *testing.T) { f, err := os.Open("../../images/noise.jpeg") @@ -495,6 +526,8 @@ func TestNewMonochrome16NoiseExtractorGaussianNoise16PngImage(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochromeExposureGetFITSImage(t *testing.T) { f, err := os.Open("../../images/noise.jpeg") @@ -539,6 +572,7 @@ func TestNewMonochromeExposureGetFITSImage(t *testing.T) { if fit.Data == nil { t.Errorf("Expected the FITS image data to be instantiated successfully, but got nil") + return } if len(fit.Data) != bounds.Dx()*bounds.Dy() { @@ -583,6 +617,8 @@ func TestNewMonochromeExposureGetFITSImage(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewMonochromeExposureFromASCOMGetFITSImage(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -595,7 +631,7 @@ func TestNewMonochromeExposureFromASCOMGetFITSImage(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-monochrome.json") + file, err := os.ReadFile("../../data/m42-800x600-monochrome.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -621,6 +657,7 @@ func TestNewMonochromeExposureFromASCOMGetFITSImage(t *testing.T) { if fit.Data == nil { t.Errorf("Expected the FITS image data to be instantiated successfully, but got nil") + return } if len(fit.Data) != xs*ys { @@ -664,3 +701,5 @@ func TestNewMonochromeExposureFromASCOMGetFITSImage(t *testing.T) { t.Errorf("Error writing image: %s", err) } } + +/*****************************************************************************************************************/ diff --git a/pkg/iris/rggb.go b/pkg/iris/rggb.go index d83ea3d..0a855af 100644 --- a/pkg/iris/rggb.go +++ b/pkg/iris/rggb.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package iris +/*****************************************************************************************************************/ + import ( "bytes" "fmt" @@ -13,6 +23,8 @@ import ( "github.com/observerly/iris/pkg/photometry" ) +/*****************************************************************************************************************/ + type RGGBExposure struct { Width int Height int @@ -27,11 +39,15 @@ type RGGBExposure struct { Pixels int } +/*****************************************************************************************************************/ + type RGGBColor struct { Name string Channel []float32 } +/*****************************************************************************************************************/ + func NewRGGBExposure(exposure [][]uint32, adu int32, xs int, ys int, cfa string) *RGGBExposure { img := image.NewRGBA(image.Rect(0, 0, xs, ys)) @@ -47,9 +63,9 @@ func NewRGGBExposure(exposure [][]uint32, adu int32, xs int, ys int, cfa string) } } -/** - Accepts a CFA (Color Filter Array) string, e.g., "RGGB" and returns the Bayering Matrix offset -**/ +/*****************************************************************************************************************/ + +// Accepts a CFA (Color Filter Array) string, e.g., "RGGB" and returns the Bayering Matrix offset func (b *RGGBExposure) GetBayerMatrixOffset() (xOffset int, yOffset int, err error) { switch strings.ToLower(b.ColourFilterArray) { case "rggb": @@ -65,6 +81,8 @@ func (b *RGGBExposure) GetBayerMatrixOffset() (xOffset int, yOffset int, err err } } +/*****************************************************************************************************************/ + func (b *RGGBExposure) GetBuffer(img *image.RGBA) (bytes.Buffer, error) { var buff bytes.Buffer @@ -77,9 +95,9 @@ func (b *RGGBExposure) GetBuffer(img *image.RGBA) (bytes.Buffer, error) { return buff, nil } -/** - Convert an R or G or B channel to a Monochrome exposure -**/ +/*****************************************************************************************************************/ + +// Converts an R, G, or B channel to a Monochrome 16 bit exposure func (b *RGGBExposure) GetMonochrome() MonochromeExposure { // Create a 2D array of the specific RGB channel from flattened 1D color channel array: raw := make([][]uint32, b.Height) @@ -110,9 +128,9 @@ func (b *RGGBExposure) GetMonochrome() MonochromeExposure { return m } -/** - Convert an R or G or B channel to a FITS standard image -**/ +/*****************************************************************************************************************/ + +// Converts a R, G, or B channel to a FITS standard image func (b *RGGBExposure) GetFITSImageForChannel(color RGGBColor) *fits.FITSImage { // Create a 2D array of the specific RGB channel from flattened 1D color channel array: raw := make([][]uint32, b.Height) @@ -152,9 +170,9 @@ func (b *RGGBExposure) GetFITSImageForChannel(color RGGBColor) *fits.FITSImage { return f } -/** - Return each R, G, B FITS standard images -**/ +/*****************************************************************************************************************/ + +// Returns each of the R, G and B channels as FITS images func (b *RGGBExposure) GetFITSImages() (*fits.FITSImage, *fits.FITSImage, *fits.FITSImage) { var wg sync.WaitGroup @@ -206,9 +224,9 @@ func (b *RGGBExposure) GetFITSImages() (*fits.FITSImage, *fits.FITSImage, *fits. return <-R, <-G, <-B } -/** - Perform Debayering w/ Bilinear Interpolation Technique -**/ +/*****************************************************************************************************************/ + +// Performs a Debayering with a bilinear interpolation technique. func (b *RGGBExposure) DebayerBilinearInterpolation() error { var wg sync.WaitGroup @@ -302,22 +320,10 @@ func (b *RGGBExposure) DebayerBilinearInterpolation() error { return nil } -/* - PreprocessImageArray() +/*****************************************************************************************************************/ - Preprocesses an ASCOM Alpaca Image Array to a m.Raw 2D array of uint32 values. - Converts the 2D array of uint16 values to a 2D array of uint32 values. - - @returns a bytes.Buffer containing the preprocessed image. - @see https://ascom-standards.org/api/#/Camera%20Specific%20Methods/get_camera__device_number__imagearray - - "... "column-major" order (column changes most rapidly) from the image's row and column - perspective, while, from the array's perspective, serialisation is actually effected in - "row-major" order (rightmost index changes most rapidly). This unintuitive outcome arises - because the ASCOM Camera Interface specification defines the image column dimension as - the rightmost array dimension." - -*/ +// Preprocesses an ASCOM Alpaca Image Array to a m.Raw 2D array of uint32 values. Converts the 2D array of uint16 +// values to a 2D array of uint32 values, returning a bytes.Buffer containing the preprocessed image. func (b *RGGBExposure) PreprocessImageArray(xs int, ys int) (bytes.Buffer, error) { // Switch the columns and rows in the image: ex := make([][]uint32, xs) @@ -348,3 +354,5 @@ func (b *RGGBExposure) Preprocess() (bytes.Buffer, error) { return b.GetBuffer(b.Image) } + +/*****************************************************************************************************************/ diff --git a/pkg/iris/rggb64.go b/pkg/iris/rggb64.go index 7351c14..e9956d1 100644 --- a/pkg/iris/rggb64.go +++ b/pkg/iris/rggb64.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package iris +/*****************************************************************************************************************/ + import ( "bytes" "fmt" @@ -13,6 +23,8 @@ import ( "github.com/observerly/iris/pkg/photometry" ) +/*****************************************************************************************************************/ + type RGGB64Exposure struct { Width int Height int @@ -27,11 +39,15 @@ type RGGB64Exposure struct { Pixels int } +/*****************************************************************************************************************/ + type RGGB64Color struct { Name string Channel []float32 } +/*****************************************************************************************************************/ + func NewRGGB64Exposure(exposure [][]uint32, adu int32, xs int, ys int, cfa string) *RGGB64Exposure { img := image.NewRGBA64(image.Rect(0, 0, xs, ys)) @@ -47,9 +63,9 @@ func NewRGGB64Exposure(exposure [][]uint32, adu int32, xs int, ys int, cfa strin } } -/** - Accepts a CFA (Color Filter Array) string, e.g., "RGGB" and returns the Bayering Matrix offset -**/ +/*****************************************************************************************************************/ + +// Accepts a CFA (Color Filter Array) string, e.g., "RGGB" and returns the Bayering Matrix offset func (b *RGGB64Exposure) GetBayerMatrixOffset() (xOffset int, yOffset int, err error) { switch strings.ToLower(b.ColourFilterArray) { case "rggb": @@ -65,6 +81,8 @@ func (b *RGGB64Exposure) GetBayerMatrixOffset() (xOffset int, yOffset int, err e } } +/*****************************************************************************************************************/ + func (b *RGGB64Exposure) GetBuffer(img *image.RGBA64) (bytes.Buffer, error) { var buff bytes.Buffer @@ -77,9 +95,9 @@ func (b *RGGB64Exposure) GetBuffer(img *image.RGBA64) (bytes.Buffer, error) { return buff, nil } -/** - Convert an R or G or B channel to a Monochrome 16 bit exposure -**/ +/*****************************************************************************************************************/ + +// Converts an R, G, or B channel to a Monochrome 16 bit exposure func (b *RGGB64Exposure) GetMonochrome() Monochrome16Exposure { // Create a 2D array of the specific RGB channel from flattened 1D color channel array: raw := make([][]uint32, b.Height) @@ -110,9 +128,9 @@ func (b *RGGB64Exposure) GetMonochrome() Monochrome16Exposure { return m } -/** - Convert an R or G or B channel to a FITS standard image -**/ +/*****************************************************************************************************************/ + +// Converts a R, G, or B channel to a FITS standard image func (b *RGGB64Exposure) GetFITSImageForChannel(color RGGB64Color) *fits.FITSImage { // Create a 2D array of the specific RGB channel from flattened 1D color channel array: raw := make([][]uint32, b.Height) @@ -152,9 +170,9 @@ func (b *RGGB64Exposure) GetFITSImageForChannel(color RGGB64Color) *fits.FITSIma return f } -/** - Return each R, G, B FITS standard images -**/ +/*****************************************************************************************************************/ + +// Returns each of the R, G and B channels as FITS images func (b *RGGB64Exposure) GetFITSImages() (*fits.FITSImage, *fits.FITSImage, *fits.FITSImage) { var wg sync.WaitGroup @@ -206,9 +224,9 @@ func (b *RGGB64Exposure) GetFITSImages() (*fits.FITSImage, *fits.FITSImage, *fit return <-R, <-G, <-B } -/** - Perform Debayering w/ Bilinear Interpolation Technique -**/ +/*****************************************************************************************************************/ + +// Performs a Debayering with a bilinear interpolation technique. func (b *RGGB64Exposure) DebayerBilinearInterpolation() error { var wg sync.WaitGroup @@ -302,22 +320,10 @@ func (b *RGGB64Exposure) DebayerBilinearInterpolation() error { return nil } -/* - PreprocessImageArray() +/*****************************************************************************************************************/ - Preprocesses an ASCOM Alpaca Image Array to a m.Raw 2D array of uint32 values. - Converts the 2D array of uint16 values to a 2D array of uint32 values. - - @returns a bytes.Buffer containing the preprocessed image. - @see https://ascom-standards.org/api/#/Camera%20Specific%20Methods/get_camera__device_number__imagearray - - "... "column-major" order (column changes most rapidly) from the image's row and column - perspective, while, from the array's perspective, serialisation is actually effected in - "row-major" order (rightmost index changes most rapidly). This unintuitive outcome arises - because the ASCOM Camera Interface specification defines the image column dimension as - the rightmost array dimension." - -*/ +// Preprocesses an ASCOM Alpaca Image Array to a m.Raw 2D array of uint32 values. Converts the 2D array of uint16 +// values to a 2D array of uint32 values, returning a bytes.Buffer containing the preprocessed image. func (b *RGGB64Exposure) PreprocessImageArray(xs int, ys int) (bytes.Buffer, error) { // Switch the columns and rows in the image: ex := make([][]uint32, xs) @@ -348,3 +354,5 @@ func (b *RGGB64Exposure) Preprocess() (bytes.Buffer, error) { return b.GetBuffer(b.Image) } + +/*****************************************************************************************************************/ diff --git a/pkg/iris/rggb64_test.go b/pkg/iris/rggb64_test.go index 890a248..62687cc 100644 --- a/pkg/iris/rggb64_test.go +++ b/pkg/iris/rggb64_test.go @@ -1,13 +1,24 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package iris +/*****************************************************************************************************************/ + import ( "encoding/json" "image/jpeg" - "io/ioutil" "os" "testing" ) +/*****************************************************************************************************************/ + func TestNewRGGB64ExposureWidth(t *testing.T) { rggb := NewRGGB64Exposure(ex, 1, 800, 600, "RGGB") @@ -20,6 +31,8 @@ func TestNewRGGB64ExposureWidth(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64ExposureHeight(t *testing.T) { rggb := NewRGGB64Exposure(ex, 1, 800, 600, "RGGB") @@ -32,6 +45,8 @@ func TestNewRGGB64ExposureHeight(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64ExpsourePixels(t *testing.T) { rggb := NewRGGB64Exposure(ex, 1, 800, 600, "RGGB") @@ -44,6 +59,8 @@ func TestNewRGGB64ExpsourePixels(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64GetBayerMatrixOffset(t *testing.T) { rggb := NewRGGB64Exposure(ex, 1, 800, 600, "RGGB") @@ -62,6 +79,8 @@ func TestNewRGGB64GetBayerMatrixOffset(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewGRBG64GetBayerMatrixOffset(t *testing.T) { rggb := NewRGGB64Exposure(ex, 1, 800, 600, "GRBG") @@ -80,6 +99,8 @@ func TestNewGRBG64GetBayerMatrixOffset(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewGBRG64GetBayerMatrixOffset(t *testing.T) { rggb := NewRGGB64Exposure(ex, 1, 800, 600, "GBRG") @@ -98,6 +119,8 @@ func TestNewGBRG64GetBayerMatrixOffset(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewBGGR64GetBayerMatrixOffset(t *testing.T) { rggb := NewRGGB64Exposure(ex, 1, 800, 600, "BGGR") @@ -116,6 +139,8 @@ func TestNewBGGR64GetBayerMatrixOffset(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64GetBayerMatrixOffsetInvalid(t *testing.T) { rggb := NewRGGB64Exposure(ex, 1, 800, 600, "INVALID") @@ -126,6 +151,8 @@ func TestNewRGGB64GetBayerMatrixOffsetInvalid(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64ExposureGetBuffer(t *testing.T) { rggb := NewRGGB64Exposure(ex, 1, 800, 600, "RGGB") @@ -136,6 +163,8 @@ func TestNewRGGB64ExposureGetBuffer(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64DebayerBilinearInterpolation(t *testing.T) { var ex = [][]uint32{ {123, 6, 117, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -176,6 +205,8 @@ func TestNewRGGB64DebayerBilinearInterpolation(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64Preprocess(t *testing.T) { var ex = [][]uint32{ {123, 6, 117, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -205,6 +236,8 @@ func TestNewRGGB64Preprocess(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64ExposureRGBChannelDebayered(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -217,7 +250,7 @@ func TestNewRGGB64ExposureRGBChannelDebayered(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -251,6 +284,9 @@ func TestNewRGGB64ExposureRGBChannelDebayered(t *testing.T) { t.Errorf("Expected the B channel to be %d pixels, but got %d", w*h, len(rggb.R)) } } + +/*****************************************************************************************************************/ + func TestNewRGGB64ExposureDebayerBilinearInterpolation(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -263,7 +299,7 @@ func TestNewRGGB64ExposureDebayerBilinearInterpolation(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -307,6 +343,8 @@ func TestNewRGGB64ExposureDebayerBilinearInterpolation(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64ExposurePreprocessImageArray(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -319,7 +357,7 @@ func TestNewRGGB64ExposurePreprocessImageArray(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -369,6 +407,8 @@ func TestNewRGGB64ExposurePreprocessImageArray(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64ExposureGetFITSImageForRedChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -381,7 +421,7 @@ func TestNewRGGB64ExposureGetFITSImageForRedChannel(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -460,6 +500,8 @@ func TestNewRGGB64ExposureGetFITSImageForRedChannel(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64ExposureGetFITSImageForGreenChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -472,7 +514,7 @@ func TestNewRGGB64ExposureGetFITSImageForGreenChannel(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -551,6 +593,8 @@ func TestNewRGGB64ExposureGetFITSImageForGreenChannel(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64ExposureGetFITSImageForBlueChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -563,7 +607,7 @@ func TestNewRGGB64ExposureGetFITSImageForBlueChannel(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -642,6 +686,8 @@ func TestNewRGGB64ExposureGetFITSImageForBlueChannel(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64ExposureGetFITSImages(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -654,7 +700,7 @@ func TestNewRGGB64ExposureGetFITSImages(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -735,6 +781,8 @@ func TestNewRGGB64ExposureGetFITSImages(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGB64ExposureGetMonochrome16FromColorChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -747,7 +795,7 @@ func TestNewRGGB64ExposureGetMonochrome16FromColorChannel(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -812,3 +860,5 @@ func TestNewRGGB64ExposureGetMonochrome16FromColorChannel(t *testing.T) { t.Errorf("Error writing image: %s", err) } } + +/*****************************************************************************************************************/ diff --git a/pkg/iris/rggb_test.go b/pkg/iris/rggb_test.go index 382f7a0..4d05a06 100644 --- a/pkg/iris/rggb_test.go +++ b/pkg/iris/rggb_test.go @@ -1,13 +1,22 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package iris import ( "encoding/json" "image/jpeg" - "io/ioutil" "os" "testing" ) +/*****************************************************************************************************************/ + func TestNewRGGBExposureWidth(t *testing.T) { rggb := NewRGGBExposure(ex, 1, 800, 600, "RGGB") @@ -20,6 +29,8 @@ func TestNewRGGBExposureWidth(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBExposureHeight(t *testing.T) { rggb := NewRGGBExposure(ex, 1, 800, 600, "RGGB") @@ -32,6 +43,8 @@ func TestNewRGGBExposureHeight(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBExpsourePixels(t *testing.T) { rggb := NewRGGBExposure(ex, 1, 800, 600, "RGGB") @@ -44,6 +57,8 @@ func TestNewRGGBExpsourePixels(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBGetBayerMatrixOffset(t *testing.T) { rggb := NewRGGBExposure(ex, 1, 800, 600, "RGGB") @@ -62,6 +77,8 @@ func TestNewRGGBGetBayerMatrixOffset(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewGRBGGetBayerMatrixOffset(t *testing.T) { rggb := NewRGGBExposure(ex, 1, 800, 600, "GRBG") @@ -80,6 +97,8 @@ func TestNewGRBGGetBayerMatrixOffset(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewGBRGGetBayerMatrixOffset(t *testing.T) { rggb := NewRGGBExposure(ex, 1, 800, 600, "GBRG") @@ -98,6 +117,8 @@ func TestNewGBRGGetBayerMatrixOffset(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewBGGRGetBayerMatrixOffset(t *testing.T) { rggb := NewRGGBExposure(ex, 1, 800, 600, "BGGR") @@ -116,6 +137,8 @@ func TestNewBGGRGetBayerMatrixOffset(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBGetBayerMatrixOffsetInvalid(t *testing.T) { rggb := NewRGGBExposure(ex, 1, 800, 600, "INVALID") @@ -126,6 +149,8 @@ func TestNewRGGBGetBayerMatrixOffsetInvalid(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBDebayerBilinearInterpolation(t *testing.T) { var ex = [][]uint32{ {123, 6, 117, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -166,6 +191,8 @@ func TestNewRGGBDebayerBilinearInterpolation(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBPreprocess(t *testing.T) { var ex = [][]uint32{ {123, 6, 117, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -195,6 +222,8 @@ func TestNewRGGBPreprocess(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBExposureRGBChannelDebayered(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -207,7 +236,7 @@ func TestNewRGGBExposureRGBChannelDebayered(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -241,6 +270,9 @@ func TestNewRGGBExposureRGBChannelDebayered(t *testing.T) { t.Errorf("Expected the B channel to be %d pixels, but got %d", w*h, len(rggb.R)) } } + +/*****************************************************************************************************************/ + func TestNewRGGBExposureDebayerBilinearInterpolation(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -253,7 +285,7 @@ func TestNewRGGBExposureDebayerBilinearInterpolation(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -296,6 +328,9 @@ func TestNewRGGBExposureDebayerBilinearInterpolation(t *testing.T) { t.Errorf("Expected the image buffer to be saved successfully, but got %q", err) } } + +/*****************************************************************************************************************/ + func TestNewRGGBExposurePreprocessImageArray(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -308,7 +343,7 @@ func TestNewRGGBExposurePreprocessImageArray(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -358,6 +393,8 @@ func TestNewRGGBExposurePreprocessImageArray(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBExposureGetFITSImageForRedChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -370,7 +407,7 @@ func TestNewRGGBExposureGetFITSImageForRedChannel(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -449,6 +486,8 @@ func TestNewRGGBExposureGetFITSImageForRedChannel(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBExposureGetFITSImageForGreenChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -461,7 +500,7 @@ func TestNewRGGBExposureGetFITSImageForGreenChannel(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -540,6 +579,8 @@ func TestNewRGGBExposureGetFITSImageForGreenChannel(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBExposureGetFITSImageForBlueChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -552,7 +593,7 @@ func TestNewRGGBExposureGetFITSImageForBlueChannel(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -631,6 +672,8 @@ func TestNewRGGBExposureGetFITSImageForBlueChannel(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBExposureGetFITSImages(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -643,7 +686,7 @@ func TestNewRGGBExposureGetFITSImages(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -724,6 +767,8 @@ func TestNewRGGBExposureGetFITSImages(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRGGBExposureGetMonochromeFromColorChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -736,7 +781,7 @@ func TestNewRGGBExposureGetMonochromeFromColorChannel(t *testing.T) { SensorType string `json:"sensorType"` } - file, err := ioutil.ReadFile("../../data/m42-800x600-rggb.json") + file, err := os.ReadFile("../../data/m42-800x600-rggb.json") if err != nil { t.Errorf("Error opening from JSON data: %s", err) @@ -801,3 +846,5 @@ func TestNewRGGBExposureGetMonochromeFromColorChannel(t *testing.T) { t.Errorf("Error writing image: %s", err) } } + +/*****************************************************************************************************************/ diff --git a/pkg/palette/palette.go b/pkg/palette/palette.go index bde7ca7..a6038d0 100644 --- a/pkg/palette/palette.go +++ b/pkg/palette/palette.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/palette +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package palette +/*****************************************************************************************************************/ + import ( "fmt" "sync" @@ -7,11 +17,15 @@ import ( "github.com/observerly/iris/pkg/utils" ) +/*****************************************************************************************************************/ + type PaletteChannel struct { Data []float32 Fraction float32 } +/*****************************************************************************************************************/ + type Palette struct { Name string R []PaletteChannel @@ -19,18 +33,10 @@ type Palette struct { B []PaletteChannel } -/* -* +/*****************************************************************************************************************/ - FromPalette takes a colour palette and returns the red, green and blue - channels as a slice of float32, which can be constructed into an image - or into FITS data. - - @param p *Palette - the colour palette to construct the channels from - @returns []float32, []float32, []float32, error - the red, green and blue channels, and any error - -* -*/ +// FromPalette takes a colour palette and returns the red, green and blue channels as a slice of float32, +// which can be constructed into an image or into FITS data. func FromPalette(p *Palette) (r, g, b []float32, err error) { var wg sync.WaitGroup @@ -102,17 +108,9 @@ func FromPalette(p *Palette) (r, g, b []float32, err error) { return <-R, <-G, <-B, <-errors } -/* -* - - combinePaletteChannel takes a slice of PaletteChannel and combines them - into a single slice of float32. +/*****************************************************************************************************************/ - @param c []PaletteChannel - the slice of PaletteChannel to combine - @returns []float32, error - the combined slice of float32, and any error - -* -*/ +// combinePaletteChannel takes a slice of PaletteChannel and combines them into a single slice of float32. func combinePaletteChannel(channel []PaletteChannel) ([]float32, error) { // Take each channel of the palette, and their respective constituents, and multiply them by the fraction: fraction := float32(0.0) @@ -148,3 +146,5 @@ func combinePaletteChannel(channel []PaletteChannel) ([]float32, error) { return d, nil } + +/*****************************************************************************************************************/ diff --git a/pkg/palette/palette_test.go b/pkg/palette/palette_test.go index 0322c6a..60e405e 100644 --- a/pkg/palette/palette_test.go +++ b/pkg/palette/palette_test.go @@ -1,7 +1,19 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/palette +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package palette +/*****************************************************************************************************************/ + import "testing" +/*****************************************************************************************************************/ + func TestCombineRedChannelSimple(t *testing.T) { red := []PaletteChannel{ { @@ -37,6 +49,8 @@ func TestCombineRedChannelSimple(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCombineGreenChannelSimple(t *testing.T) { green := []PaletteChannel{ { @@ -72,6 +86,8 @@ func TestCombineGreenChannelSimple(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCombineBlueChannelSimple(t *testing.T) { blue := []PaletteChannel{ { @@ -107,6 +123,8 @@ func TestCombineBlueChannelSimple(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestFromPaletteSimple(t *testing.T) { red := []PaletteChannel{ { @@ -168,3 +186,5 @@ func TestFromPaletteSimple(t *testing.T) { t.Errorf("expected 80 in red channel, got %v", r[2]) } } + +/*****************************************************************************************************************/ diff --git a/pkg/photometry/convolution.go b/pkg/photometry/convolution.go index bef357c..faead0b 100644 --- a/pkg/photometry/convolution.go +++ b/pkg/photometry/convolution.go @@ -1,7 +1,19 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/photometry +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package photometry +/*****************************************************************************************************************/ + import "math" +/*****************************************************************************************************************/ + func BiLinearConvolveRedChannel(raw []uint32, w, h, xOffset, yOffset, x, y uint32) []float32 { R := make([]float32, int(x)*int(y)) @@ -40,6 +52,8 @@ func BiLinearConvolveRedChannel(raw []uint32, w, h, xOffset, yOffset, x, y uint3 return R } +/*****************************************************************************************************************/ + func BiLinearConvolveGreenChannel(raw []uint32, w, h, xOffset, yOffset, x, y uint32) []float32 { G := make([]float32, int(x)*int(y)) @@ -91,6 +105,8 @@ func BiLinearConvolveGreenChannel(raw []uint32, w, h, xOffset, yOffset, x, y uin return G } +/*****************************************************************************************************************/ + func BiLinearConvolveBlueChannel(raw []uint32, w, h, xOffset, yOffset, x, y uint32) []float32 { B := make([]float32, int(x)*int(y)) @@ -128,3 +144,5 @@ func BiLinearConvolveBlueChannel(raw []uint32, w, h, xOffset, yOffset, x, y uint return B } + +/*****************************************************************************************************************/ diff --git a/pkg/photometry/convolution_test.go b/pkg/photometry/convolution_test.go index 4ecbd25..a3decac 100644 --- a/pkg/photometry/convolution_test.go +++ b/pkg/photometry/convolution_test.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/photometry +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package photometry +/*****************************************************************************************************************/ + import ( "encoding/json" "image" @@ -8,6 +18,8 @@ import ( "testing" ) +/*****************************************************************************************************************/ + func TestBiLinearConvolveRedChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -66,6 +78,8 @@ func TestBiLinearConvolveRedChannel(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestBiLinearConvolveGreenChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -124,6 +138,8 @@ func TestBiLinearConvolveGreenChannel(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestBiLinearConvolveBlueChannel(t *testing.T) { type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` @@ -181,3 +197,5 @@ func TestBiLinearConvolveBlueChannel(t *testing.T) { } } } + +/*****************************************************************************************************************/ diff --git a/pkg/photometry/noise.go b/pkg/photometry/noise.go index 2d4b4ed..de0b7de 100644 --- a/pkg/photometry/noise.go +++ b/pkg/photometry/noise.go @@ -1,7 +1,19 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/photometry +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package photometry +/*****************************************************************************************************************/ + import "math" +/*****************************************************************************************************************/ + type NoiseExtractor struct { Width int // Width of a line in the underlying data array (for noise) Height int // Height of the underlying data array (for noise) @@ -9,6 +21,8 @@ type NoiseExtractor struct { Data []float32 // The underlying data array } +/*****************************************************************************************************************/ + func NewNoiseExtractor(data []float32, xs int, ys int) *NoiseExtractor { pixels := xs * ys @@ -24,11 +38,11 @@ func NewNoiseExtractor(data []float32, xs int, ys int) *NoiseExtractor { } } -/* - GetGaussianNoise() +/*****************************************************************************************************************/ - From J. Immerkær, “Fast Noise Variance Estimation”, Computer Vision and Image Understanding, Vol. 64, No. 2, pp. 300-302, Sep. 1996. -*/ +// Calculates the Gaussian noise in the image using a 3x3 kernel. +// +// From J. Immerkær, “Fast Noise Variance Estimation”, Computer Vision and Image Understanding, Vol. 64, No. 2, pp. 300-302, Sep. 1996. func (n *NoiseExtractor) GetGaussianNoise() float64 { // Weights for the 3x3 noise estimate kernel: weight := []int32{ @@ -80,3 +94,5 @@ func (n *NoiseExtractor) GetGaussianNoise() float64 { return fr * noise } + +/*****************************************************************************************************************/ diff --git a/pkg/photometry/noise_test.go b/pkg/photometry/noise_test.go index 11d5292..7132311 100644 --- a/pkg/photometry/noise_test.go +++ b/pkg/photometry/noise_test.go @@ -1,9 +1,21 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/photometry +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package photometry +/*****************************************************************************************************************/ + import ( "testing" ) +/*****************************************************************************************************************/ + func TestNewNoiseExtractor(t *testing.T) { var ex = [][]uint32{ {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -48,6 +60,8 @@ func TestNewNoiseExtractor(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewNoiseExtractorGaussianNoise(t *testing.T) { var ex = [][]uint32{ {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -101,3 +115,5 @@ func TestNewNoiseExtractorGaussianNoise(t *testing.T) { t.Errorf("Noise is %f, expected <= 255", noise) } } + +/*****************************************************************************************************************/ diff --git a/pkg/photometry/stars_test.go b/pkg/photometry/stars_test.go index b66243f..f33ecd2 100644 --- a/pkg/photometry/stars_test.go +++ b/pkg/photometry/stars_test.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/photometry +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package photometry +/*****************************************************************************************************************/ + import ( "image" "image/jpeg" @@ -11,6 +21,8 @@ import ( "github.com/observerly/iris/pkg/utils" ) +/*****************************************************************************************************************/ + func GetTestDataFromImage() ([][]uint32, image.Rectangle) { f, err := os.Open("../../images/noise16.jpeg") @@ -46,6 +58,8 @@ func GetTestDataFromImage() ([][]uint32, image.Rectangle) { return data, bounds } +/*****************************************************************************************************************/ + func TestNewStarsExtractor(t *testing.T) { var ex = [][]uint32{ {123, 6, 117, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -106,6 +120,8 @@ func TestNewStarsExtractor(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewGetBrightPixels(t *testing.T) { var ex = [][]uint32{ {123, 6, 117, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}, @@ -150,6 +166,8 @@ func TestNewGetBrightPixels(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewGetBrightPixelsFrom2DData(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -178,6 +196,8 @@ func TestNewGetBrightPixelsFrom2DData(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewRejectBadPixelsFrom2DData(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -214,6 +234,8 @@ func TestNewRejectBadPixelsFrom2DData(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFilterOverlappingStarsFrom2DData(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -256,6 +278,8 @@ func TestNewFilterOverlappingStarsFrom2DData(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewShiftToBrightestPixelStarsFrom2DData(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -304,6 +328,8 @@ func TestNewShiftToBrightestPixelStarsFrom2DData(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewExtractAndFilterHalfFluxRadiusStarsFrom2DData(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -360,6 +386,8 @@ func TestNewExtractAndFilterHalfFluxRadiusStarsFrom2DData(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewFindStarsFrom2DData(t *testing.T) { data, bounds := GetTestDataFromImage() @@ -403,3 +431,5 @@ func TestNewFindStarsFrom2DData(t *testing.T) { t.Error("Expected to calculate HFR to an accuracy of 0.1, but got ", s.HFR) } } + +/*****************************************************************************************************************/ diff --git a/pkg/qsort/qsort.go b/pkg/qsort/qsort.go index d9262ec..a5de5f5 100644 --- a/pkg/qsort/qsort.go +++ b/pkg/qsort/qsort.go @@ -1,12 +1,18 @@ -package qsort +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/qsort +// @license Copyright © 2021-2025 observerly -/* - Partitions an array of float32 with the middle pivot element, and returns the pivot index. +/*****************************************************************************************************************/ - Values less than the pivot are moved left of the pivot, those greater are moved right. +package qsort - Array must not contain IEEE NaN -*/ +/*****************************************************************************************************************/ + +// Partition an array of float32 with the middle pivot element, and return the pivot index. +// +// Values less than the pivot are moved left of the pivot, those greater are moved right. func QPartitionFloat32(a []float32) int { left, right := 0, len(a)-1 @@ -37,11 +43,9 @@ func QPartitionFloat32(a []float32) int { } } -/* - Sort an array of float32 in ascending order. +/*****************************************************************************************************************/ - Array must not contain IEEE NaN -*/ +// Quick sort an array of float32 in ascending order. func QSortFloat32(a []float32) { if len(a) > 1 { index := QPartitionFloat32(a) @@ -50,11 +54,9 @@ func QSortFloat32(a []float32) { } } -/* - Select kth lowest element from an array of float32. Partially reorders the array. +/*****************************************************************************************************************/ - Array must not contain IEEE NaN -*/ +// Select kth lowest element from an array of float32. Partially reorders the array. func QSelectFloat32(a []float32, k int) float32 { left, right := 0, len(a)-1 @@ -92,27 +94,23 @@ func QSelectFloat32(a []float32, k int) float32 { right = index } else { left = index + 1 - k = k - offset + k -= offset } } return a[left] } -/* - Select first quartile of an array of float32. Partially reorders the array. +/*****************************************************************************************************************/ - Array must not contain IEEE NaN -*/ +// Selects the first quartile of an array of float32 and partially reorders the array. func QSelectFirstQuartileFloat32(a []float32) float32 { return QSelectFloat32(a, (len(a)>>2)+1) } -/* - Select median of an array of float32. Partially reorders the array. +/*****************************************************************************************************************/ - Array must not contain IEEE NaN -*/ +// Selects the median of an array of float32 and partially reorders the array. func QSelectMedianFloat32(a []float32) float32 { // Quickly select the midpoint element: k := (len(a) >> 1) + 1 @@ -137,3 +135,5 @@ func QSelectMedianFloat32(a []float32) float32 { // Return average of the upper and lower elements: return 0.5 * (lower + upper) } + +/*****************************************************************************************************************/ diff --git a/pkg/qsort/qsort_test.go b/pkg/qsort/qsort_test.go index 912155b..f2bbe7b 100644 --- a/pkg/qsort/qsort_test.go +++ b/pkg/qsort/qsort_test.go @@ -1,9 +1,21 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/qsort +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package qsort +/*****************************************************************************************************************/ + import ( "testing" ) +/*****************************************************************************************************************/ + func TestQPartitionFloat32(t *testing.T) { a := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -18,6 +30,8 @@ func TestQPartitionFloat32(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestQPartitionFloat32DispersedRandom(t *testing.T) { a := []float32{10, 12, 23, 23, 16, 23, 21, 16} @@ -32,6 +46,8 @@ func TestQPartitionFloat32DispersedRandom(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestQSortFloat32(t *testing.T) { a := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -44,6 +60,8 @@ func TestQSortFloat32(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestQSortFloat32DispersedRandom(t *testing.T) { a := []float32{10, 12, 23, 23, 16, 23, 21, 16} @@ -82,6 +100,8 @@ func TestQSortFloat32DispersedRandom(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestQSelectFloat32(t *testing.T) { a := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -92,6 +112,8 @@ func TestQSelectFloat32(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestQSelectFloat32DispersedRandom(t *testing.T) { a := []float32{10, 12, 23, 23, 16, 23, 21, 16} @@ -102,6 +124,8 @@ func TestQSelectFloat32DispersedRandom(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestQSelectFirstQuartileFloat32(t *testing.T) { a := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -112,6 +136,8 @@ func TestQSelectFirstQuartileFloat32(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestQSelectFirstQuartileFloat32DispersedRandom(t *testing.T) { a := []float32{10, 12, 23, 23, 16, 23, 21, 16} @@ -122,6 +148,8 @@ func TestQSelectFirstQuartileFloat32DispersedRandom(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestQSelectMedianFloat32Odd(t *testing.T) { a := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -134,6 +162,8 @@ func TestQSelectMedianFloat32Odd(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestQSelectMedianFloat32Even(t *testing.T) { a := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} @@ -146,6 +176,8 @@ func TestQSelectMedianFloat32Even(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestQSelectMedianFloat32DispersedRandom(t *testing.T) { a := []float32{10, 12, 23, 23, 16, 23, 21, 16} @@ -157,3 +189,5 @@ func TestQSelectMedianFloat32DispersedRandom(t *testing.T) { t.Errorf("median should be 18.5, but got %v", median) } } + +/*****************************************************************************************************************/ diff --git a/pkg/statistics/stats.go b/pkg/statistics/stats.go index ca73781..7e72df5 100644 --- a/pkg/statistics/stats.go +++ b/pkg/statistics/stats.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/stats +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package stats +/*****************************************************************************************************************/ + import ( "math" "sort" @@ -8,7 +18,9 @@ import ( "github.com/observerly/iris/pkg/utils" ) -// Statistics on data arrays, calculated on demand +/*****************************************************************************************************************/ + +// Statistics on data arrays, calculated on demand. type Stats struct { Width int // Width of a line in the underlying data array (for noise) Data []float32 // The underlying data array @@ -23,6 +35,8 @@ type Stats struct { Noise float32 // Noise Estimation } +/*****************************************************************************************************************/ + func NewStats(data []float32, adu int32, xs int) *Stats { NaN32 := float32(math.NaN()) @@ -51,6 +65,8 @@ func NewStats(data []float32, adu int32, xs int) *Stats { } } +/*****************************************************************************************************************/ + func calcMinMeanMax(data []float32) (min float32, mean float32, max float32) { mmin, mmean, mmax := float32(data[0]), float32(0), float32(data[0]) @@ -67,6 +83,8 @@ func calcMinMeanMax(data []float32) (min float32, mean float32, max float32) { return mmin, float32(mmean / float32(len(data))), mmax } +/*****************************************************************************************************************/ + func calcMeanStdDevVar(data []float32) (mean float32, stddev float32, variance float32) { xvar, mmean := float32(0), float32(0) @@ -88,6 +106,8 @@ func calcMeanStdDevVar(data []float32) (mean float32, stddev float32, variance f return mmean, stddev, xvar } +/*****************************************************************************************************************/ + func calcMinMeanMaxStdDevVar(data []float32) (min float32, mean float32, max float32, stddev float32, variance float32) { mmin, mmean, mmax, xvar := float32(data[0]), float32(0), float32(data[0]), float32(0) @@ -117,6 +137,8 @@ func calcMinMeanMaxStdDevVar(data []float32) (min float32, mean float32, max flo return mmin, mmean, mmax, stddev, xvar } +/*****************************************************************************************************************/ + func calcMedian(data []float32) float32 { p := make([]float64, len(data)) @@ -133,27 +155,22 @@ func calcMedian(data []float32) float32 { return float32(p[len(p)/2]) } -/* - FastMedian +/*****************************************************************************************************************/ - Calculates fast median of the data sample -*/ +// FastMedian calculates the median of the data sample func (s *Stats) FastMedian() float32 { median := qsort.QSelectMedianFloat32(s.Data) return median } -/* - FastApproxMedian - - Calculates fast approximate median of the (presumably large) data by - sub-sampling the given number of values and taking the median of that. +/*****************************************************************************************************************/ - Note: this is not a statistically correct median, but it is fast and - should be good enough for most purposes. The sub-sampling is done - by randomly selecting sub-values from the data array using a random - number generator pinned to the maximum of the data array. -*/ +// Calculates fast approximate median of the (presumably large) data by sub-sampling the given number of values +// and taking the median of that. +// +// N.B. This is not a statistically correct median, but it is fast and should be good enough for most purposes. +// The sub-sampling is done by randomly selecting sub-values from the data array using a random number generator +// pinned to the maximum of the data array. func (s *Stats) FastApproxMedian(sample []float32) float32 { rng := utils.RNG{} @@ -171,19 +188,16 @@ func (s *Stats) FastApproxMedian(sample []float32) float32 { return median } -/* - FastApproxQn - - Calculates fast approximate Qn scale estimate of the (presumably large) data by - sub-sampling the given number of pairs and taking the first quartile of that. +/*****************************************************************************************************************/ - Note: this is not a statistically correct median, but it is fast and - should be good enough for most purposes. The sub-sampling is done - by randomly selecting sub-values from the data array using a random - number generator pinned to the maximum of the data array. - - @see http://web.ipac.caltech.edu/staff/fmasci/home/astro_refs/BetterThanMAD.pdf -*/ +// Calculates fast approximate Qn scale estimate of the (presumably large) data by sub-sampling the given number +// of pairs. +// +// N.B. This is not a statistically correct median, but it is fast and should be good enough for most purposes. +// The sub-sampling is done by randomly selecting sub-values from the data array using a random number generator +// pinned to the maximum of the data array. +// +// @see http://web.ipac.caltech.edu/staff/fmasci/home/astro_refs/BetterThanMAD.pdf func (s *Stats) FastApproxQn(sample []float32) float32 { rng := utils.RNG{} @@ -204,16 +218,18 @@ func (s *Stats) FastApproxQn(sample []float32) float32 { return qn } +/*****************************************************************************************************************/ + /* - FastApproxBoundedMedian +FastApproxBoundedMedian - Calculates fast approximate median of the (presumably large) data by - sub-sampling the given number of values and taking the median of that. +Calculates fast approximate median of the (presumably large) data by +sub-sampling the given number of values and taking the median of that. - Note: this is not a statistically correct median, but it is fast and - should be good enough for most purposes. The sub-sampling is done - by randomly selecting sub-values from the data array using a random - number generator pinned to the maximum of the data array. +Note: this is not a statistically correct median, but it is fast and +should be good enough for most purposes. The sub-sampling is done +by randomly selecting sub-values from the data array using a random +number generator pinned to the maximum of the data array. */ func (s *Stats) FastApproxBoundedMedian(sample []float32, lowerBound, higherBound float32) float32 { rng := utils.RNG{} @@ -239,17 +255,14 @@ func (s *Stats) FastApproxBoundedMedian(sample []float32, lowerBound, higherBoun return median } -/* - FastApproxBoundedQn - - Calculates fast approximate Qn scale estimate of the (presumably large) data by - sub-sampling the given number of pairs and taking the first quartile of that. +/*****************************************************************************************************************/ - Note: this is not a statistically correct median, but it is fast and - should be good enough for most purposes. The sub-sampling is done - by randomly selecting sub-values from the data array using a random - number generator pinned to the maximum of the data array. -*/ +// Calculates fast approximate Qn scale estimate of the (presumably large) data by sub-sampling the given number +// of pairs and taking the first quartile of that. +// +// N.B This is not a statistically correct median, but it is fast and should be good enough for most purposes. +// The sub-sampling is done by randomly selecting sub-values from the data array using a random number generator +// pinned to the maximum of the data array. func (s *Stats) FastApproxBoundedQn(sample []float32, lowerBound, higherBound float32) float32 { rng := utils.RNG{} @@ -283,14 +296,12 @@ func (s *Stats) FastApproxBoundedQn(sample []float32, lowerBound, higherBound fl return qn } -/* - FastApproxSigmaClippedMedianAndQn +/*****************************************************************************************************************/ - @returns a rapid robust estimation of location and scale. Uses a fast approximate - median based on randomized sub-sampling, iteratively sigma clipped with a fast - approximate Qn based on random sampling. Exits once the absolute change in - location and scale is below epsilon. -*/ +// Calculates the fast approximate sigma-clipped median and Qn scale estimate of the data, returning a rapid +// estimation of location and scale. Uses a fast approximate median based on randomized sub-sampling, iteratively +// sigma clipped with a fast approximate Qn based on random sampling. Exits once the absolute change in location +// and scale is below epsilon. func (s *Stats) FastApproxSigmaClippedMedianAndQn() (float32, float32) { sample := make([]float32, int(float32(len(s.Data))*0.8)) @@ -327,3 +338,5 @@ func (s *Stats) FastApproxSigmaClippedMedianAndQn() (float32, float32) { location, scale = nlocation, nscale } } + +/*****************************************************************************************************************/ diff --git a/pkg/statistics/stats_test.go b/pkg/statistics/stats_test.go index 2a24271..af9a12f 100644 --- a/pkg/statistics/stats_test.go +++ b/pkg/statistics/stats_test.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/stats +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package stats +/*****************************************************************************************************************/ + import ( "encoding/json" "io/ioutil" @@ -7,6 +17,8 @@ import ( "testing" ) +/*****************************************************************************************************************/ + type CameraExposure struct { BayerXOffset int32 `json:"bayerXOffset"` BayerYOffset int32 `json:"bayerYOffset"` @@ -18,6 +30,8 @@ type CameraExposure struct { SensorType string `json:"sensorType"` } +/*****************************************************************************************************************/ + func GetTestData(xs int, ys int) []float32 { file, err := ioutil.ReadFile("../../data/m42-800x600-monochrome.json") @@ -55,6 +69,8 @@ func GetTestData(xs int, ys int) []float32 { return data } +/*****************************************************************************************************************/ + func TestCalculateMinMeanMax(t *testing.T) { data := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -73,6 +89,8 @@ func TestCalculateMinMeanMax(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCalculateMeanStdDevVar(t *testing.T) { data := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -91,6 +109,8 @@ func TestCalculateMeanStdDevVar(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCalculateMinMeanMaxStdDevVar(t *testing.T) { data := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -117,6 +137,8 @@ func TestCalculateMinMeanMaxStdDevVar(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCalculateMedianOdd(t *testing.T) { data := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -127,6 +149,8 @@ func TestCalculateMedianOdd(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCalculateMedianEven(t *testing.T) { data := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} @@ -137,6 +161,8 @@ func TestCalculateMedianEven(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestCalculateMedianDispersedRandom(t *testing.T) { data := []float32{10, 12, 23, 23, 16, 23, 21, 16} @@ -147,6 +173,8 @@ func TestCalculateMedianDispersedRandom(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewStats(t *testing.T) { data := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -173,6 +201,8 @@ func TestNewStats(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewStatsMonochromeExposure(t *testing.T) { xs := 800 @@ -199,6 +229,8 @@ func TestNewStatsMonochromeExposure(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewStatsFastMedianFloat32(t *testing.T) { data := []float32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -211,6 +243,8 @@ func TestNewStatsFastMedianFloat32(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestFastApproxMedian(t *testing.T) { xs := 800 @@ -235,6 +269,8 @@ func TestFastApproxMedian(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestFastApproxQn(t *testing.T) { xs := 800 @@ -255,6 +291,8 @@ func TestFastApproxQn(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestFastApproxBoundedMedian(t *testing.T) { xs := 800 @@ -285,6 +323,8 @@ func TestFastApproxBoundedMedian(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestFastApproxBoundedQn(t *testing.T) { xs := 800 @@ -315,6 +355,8 @@ func TestFastApproxBoundedQn(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestFastApproxSigmaClippedMedianAndQn(t *testing.T) { xs := 800 @@ -343,6 +385,8 @@ func TestFastApproxSigmaClippedMedianAndQn(t *testing.T) { } } +/*****************************************************************************************************************/ + func TestNewStatsMonochrome16Exposure(t *testing.T) { xs := 800 @@ -368,3 +412,5 @@ func TestNewStatsMonochrome16Exposure(t *testing.T) { t.Errorf("stddev should be 10592.966, but got %v", stats.StdDev) } } + +/*****************************************************************************************************************/ diff --git a/pkg/utils/array.go b/pkg/utils/array.go index 7172eab..9c85e44 100644 --- a/pkg/utils/array.go +++ b/pkg/utils/array.go @@ -18,12 +18,7 @@ import ( /*****************************************************************************************************************/ -/* -Add - -Computes the element-wise sum of arrays a and b and stores in array s "sum", -that is, s[i]=a[i]+b[i]. -*/ +// Computes the element-wise sum of arrays a and b and stores in array s "sum", that is, s[i]=a[i]+b[i]. func AddFloat32Array(a, b []float32) ([]float32, error) { if len(a) != len(b) { return nil, errors.New("to add arrays they must be of same length") @@ -40,12 +35,7 @@ func AddFloat32Array(a, b []float32) ([]float32, error) { /*****************************************************************************************************************/ -/* -Subtract - -Computes the element-wise difference of arrays a and b -and stores in array d "divide", that is, d[i]=a[i]-b[i]. -*/ +// Computes the element-wise difference of arrays a and b and stores in array d "divide", that is, d[i]=a[i]-b[i]. func SubtractFloat32Array(a, b []float32) ([]float32, error) { if len(a) != len(b) { return nil, errors.New("to subtract arrays they must be of same length") @@ -62,12 +52,7 @@ func SubtractFloat32Array(a, b []float32) ([]float32, error) { /*****************************************************************************************************************/ -/* -Multiply - -Computes the element-wise product of arrays a and b and stores -in array p "product", that is, m[i]=a[i]*b[i]. -*/ +// Computes the element-wise product of arrays a and b and stores in array p "product", that is, m[i]=a[i]*b[i]. func MultiplyFloat32Array(a, b []float32) ([]float32, error) { if len(a) != len(b) { return nil, errors.New("to multiply arrays they must be of same length") @@ -84,12 +69,8 @@ func MultiplyFloat32Array(a, b []float32) ([]float32, error) { /*****************************************************************************************************************/ -/* -Divide - -Computes the element-wise division of arrays a and b, scaled -with bMean and stores in array d "divide", that is, d[i]=a[i]*bMax/b[i]. -*/ +// Computes the element-wise division of arrays a and b, scaled with bMean and stores in array d "divide", +// that is, d[i]=a[i]*bMax/b[i]. func DivideFloat32Array(a, b []float32, bMax float32) ([]float32, error) { if len(a) != len(b) { return nil, errors.New("to divide arrays they must be of same length") @@ -112,12 +93,7 @@ func DivideFloat32Array(a, b []float32, bMax float32) ([]float32, error) { /*****************************************************************************************************************/ -/* -Average - -Computes the average of array a and stores in array m "mean", -that is, m[i]=mean(a). If a is empty, m is nil. -*/ +// Computes the average of array a and stores in array m "mean", that is, m[i]=mean(a). If a is empty, m is nil. func AverageFloat32Array(a []float32) (float32, error) { if len(a) == 0 { return 0, errors.New("cannot compute the average of an empty array") @@ -134,12 +110,7 @@ func AverageFloat32Array(a []float32) (float32, error) { /*****************************************************************************************************************/ -/* -Mean - -Computes the mean of array a and stores in array m "mean", -that is, m[i]=mean(a). If a is empty, m is nil. -*/ +// Computes the mean of array a and stores in array m "mean", that is, m[i]=mean(a). If a is empty, m is nil. func MeanFloat32Arrays(a [][]float32) ([]float32, error) { if len(a) == 0 { return nil, errors.New("to divide arrays they must be of same length") @@ -169,11 +140,7 @@ func MeanFloat32Arrays(a [][]float32) ([]float32, error) { /*****************************************************************************************************************/ -/* -Flatten2DUInt32Array - -Flattens a 2D array of uint32 into a 1D array of float32. -*/ +// Flattens a 2D array of uint32 into a 1D array of float32. func Flatten2DUInt32Array(a [][]uint32) []float32 { f := make([]float32, 0) @@ -188,11 +155,7 @@ func Flatten2DUInt32Array(a [][]uint32) []float32 { /*****************************************************************************************************************/ -/* -Flatten2DFloat64Array - -Flattens a 2D array of float64 into a 1D array of float64. -*/ +// Flattens a 2D array of float64 into a 1D array of float64. func Flatten2DFloat64Array(a [][]float64) []float64 { f := make([]float64, 0) @@ -205,11 +168,7 @@ func Flatten2DFloat64Array(a [][]float64) []float64 { /*****************************************************************************************************************/ -/* -BoundsFloat32Array - -Computes the minimum and maximum values of array a. -*/ +// Computes the minimum and maximum values of array a. func BoundsFloat32Array(a []float32) (float32, float32) { // Set the initial min and max values: min, max := float32(math.MaxFloat32-1), float32(0.0) diff --git a/pkg/vcurve/vcurve.go b/pkg/vcurve/vcurve.go index b1aefe8..67e2d70 100644 --- a/pkg/vcurve/vcurve.go +++ b/pkg/vcurve/vcurve.go @@ -1,5 +1,15 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/vcurve +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package vcurve +/*****************************************************************************************************************/ + import ( "math" @@ -8,17 +18,23 @@ import ( "gonum.org/v1/gonum/stat" ) +/*****************************************************************************************************************/ + // Point is a data point with x and y coordinates. type Point struct { X float64 Y float64 } +/*****************************************************************************************************************/ + // VCurve is a struct that holds the data points for the V-curve. type VCurve struct { Points []Point } +/*****************************************************************************************************************/ + // VCurveParams is a struct that holds the parameters for the V-curve model optisation and the data points. type VCurveParams struct { A float64 @@ -29,13 +45,13 @@ type VCurveParams struct { Y []float64 } -/* -NewHyperbolicVCurve - -Creates a new VCurve object ready for applying the Levenberg-Marquardt iterative optimization technique. +/*****************************************************************************************************************/ -The VCurve object is initialized with the data points, and initial guesses for the parameters are calculated from the input data. -*/ +// NewHyperbolicVCurve +// +// Creates a new VCurve object ready for applying the Levenberg-Marquardt iterative optimization technique. +// The VCurve object is initialized with the data points, and initial guesses for the parameters are calculated +// from the input data. func NewHyperbolicVCurve(data VCurve) *VCurveParams { // Preallocate slices with the exact required capacity dataX := make([]float64, 0, len(data.Points)) @@ -66,12 +82,16 @@ func NewHyperbolicVCurve(data VCurve) *VCurveParams { } } +/*****************************************************************************************************************/ + // This is the hyperbolic function that we want to fit to the data. func hyperbolicFunction(x float64, params VCurveParams) float64 { a, b, c, d := params.A, params.B, params.C, params.D return b*math.Sqrt(1+math.Pow((x-c)/a, 2)) + d } +/*****************************************************************************************************************/ + // objectiveFunc is the least squares objective function that accepts dataX and dataY. func objectiveFunc(dataX, dataY []float64) func(params []float64) float64 { // If we do not have the same number of x and y data points, we cannot fit the model. @@ -100,11 +120,10 @@ func objectiveFunc(dataX, dataY []float64) func(params []float64) float64 { } } -/* -LevenbergMarquardtOptimisation +/*****************************************************************************************************************/ -LevenbergMarquardtOptimisation optimizes the hyperbolic function using the Levenberg-Marquardt algorithm. -*/ +// LevenbergMarquardtOptimisation +// Optimizes the hyperbolic function using the Levenberg-Marquardt algorithm. func (p *VCurveParams) LevenbergMarquardtOptimisation() (VCurveParams, error) { // Setting up the optimizer: problem := optimize.Problem{ @@ -135,3 +154,5 @@ func (p *VCurveParams) LevenbergMarquardtOptimisation() (VCurveParams, error) { D: result.X[3], }, nil } + +/*****************************************************************************************************************/ diff --git a/pkg/vcurve/vcurve_test.go b/pkg/vcurve/vcurve_test.go index db01046..a2ea260 100644 --- a/pkg/vcurve/vcurve_test.go +++ b/pkg/vcurve/vcurve_test.go @@ -1,9 +1,21 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/iris/vcurve +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + package vcurve +/*****************************************************************************************************************/ + import ( "testing" ) +/*****************************************************************************************************************/ + var ( points = []Point{ {X: 29000, Y: 40.5}, @@ -30,6 +42,8 @@ var ( } ) +/*****************************************************************************************************************/ + func TestNewHyperbolicVCurve(t *testing.T) { v := NewHyperbolicVCurve(VCurve{ Points: points, @@ -59,6 +73,8 @@ func TestNewHyperbolicVCurve(t *testing.T) { } } +/*****************************************************************************************************************/ + // Test for V-curve fitting with hyperbolic model func TestHyperbolicVCurveLevenbergMarquardtOptimisation(t *testing.T) { v := NewHyperbolicVCurve(VCurve{ @@ -96,3 +112,5 @@ func TestHyperbolicVCurveLevenbergMarquardtOptimisation(t *testing.T) { t.Errorf("D should be close to %v, but got %v", -1.7965, d) } } + +/*****************************************************************************************************************/