Plot profile - slitless spectroscopy

Got an idea for something that SharpCap should do? Share it here.
Forum rules
'+1' posts are welcome in this area of the forums to indicate your support for a particular feature suggestion. Suggestions that get the most +1's will be seriously considered for inclusion in future versions of SharpCap.
Post Reply
MattTexas23
Posts: 12
Joined: Sun Sep 03, 2023 6:11 am

Plot profile - slitless spectroscopy

#1

Post by MattTexas23 »

Sharpcap is so very close to being a killer slitless spectroscopy data acquisition platform... there is just one tiny feature missing here.

This year Slitless spectoscopy is getting a lot more traction, with data analysis tools like Rspec, SpecINT and Bass Project readily available. AAVSO now has a monthly meeting on slitless spectrosopy (like with the SA100 for example), also people have used the Seestar for slitless spectroscopy. The trick is however, to capture a (faint) diffraction pattern accurately; in focus (not the star but the spectrum features), not over exposed, and relatively close (so it can be XY shift stacked afterwards). I use Sharpcap Quick capture to create SER files that I stack in SIRIL and process in Bass Project. However, to see if my focus and exposure is right, I need to go through my whole workflow today, in the field (hot or cold nights), its not optimal. So usually I end up collecting more data and throwing some away the next day. Unfortunate!

Here is what could change my life.
- allow us to select a region of interest, and plot the pixel values along the columns (gray values or green channel) vs. pixels

Most people in spectro use MONO, so that is easy to "sum". For OSC images, it would have to be debayered in that ROI and turned to grey scale (NTSC formula : 0.299 ∙ Red + 0.587 ∙ Green + 0.114 ∙ Blue); it seems to be similar to "MONO Bin" on some OSC astro cameras. If debayering is hard, it would also be very workable to only allow this feature in PNGs. Because this is for setup only, and data acquisition in SER/FITS will not change the choices for focus, gain, exposure.

I have looked in ImageJ - there you can load up a fits (MONO) or PNG (OSC or MONO) and Analyze\Profile. It gives you the file attached in this post. I would look at how deep the Balmer lines are, deep means well in focus. Using ImageJ however, I have to look at the latest file from Sharpcap and keep opening the file in ImageJ. I am considering to write a small python script that would look at the latest file and "remember" the ROI to plot - a hack on a hack.

Would this be a new feature in Sharpcap? Or is there a way to use existing functionality to do the same?

with kind regards,

Matt
Attachments
ImageJSirius.png
ImageJSirius.png (224.21 KiB) Viewed 263 times
User avatar
admin
Site Admin
Posts: 13370
Joined: Sat Feb 11, 2017 3:52 pm
Location: Vale of the White Horse, UK
Contact:

Re: Plot profile - slitless spectroscopy

#2

Post by admin »

Hi Matt,

thanks for explaining this. Can I make sure that I understand the request properly...

Basically, for the selection area, SharpCap would look at each column of pixels in the selection and pick a value for that column - this might be either the average pixel value above some background level or (maybe easier) the maximum pixel value in the column. Then plot a graph of that information.

It doesn't sound very complicated when written like that, so I just want to make sure I'm seeing all of the problem and not just part of it ;)

cheers,

Robin
MattTexas23
Posts: 12
Joined: Sun Sep 03, 2023 6:11 am

Re: Plot profile - slitless spectroscopy

#3

Post by MattTexas23 »

Thanks Robin,

yes that's absolutely it; just taking the max of all the pixels per column in the selection area. You could make it more advanced by subtraction the background (for example subtract the average of the first 10 pixels from the edges), but at this stage that is not very important and would just bring more complexity.

The reason why this (just column sum or max) is so powerful is that in slitless spectroscopy it is best practice to always have our slitless diffraction spectra exactly horizontal on the CMOS. We do that by (in hardware) lining up the diffraction grating with the CMOS sensor.

thank you,

Matt
Jean-Francois
Posts: 402
Joined: Sun Oct 13, 2019 10:52 am
Location: Germany

Re: Plot profile - slitless spectroscopy

#4

Post by Jean-Francois »

Hello Matt,

You can have a look at this script ...

Code: Select all


# *******************************************************************************************
# 
# SharpCap script "Betelgeuse_2023-11-13.py"
# 2023/11/13 Jean-Francois
#
# Version: 1.0.0:   First version
#
# The script works only with a SharpCap PRO version.
#
# *******************************************************************************************


import time
import datetime
import math
import clr
clr.AddReference("Oxyplot")
clr.AddReference("OxyPlot.WindowsForms ")
clr.AddReference("System.Drawing")
clr.AddReference("System.Windows.Forms")

import OxyPlot
import System.Drawing
import System.Drawing.Drawing2D
import System.Windows.Forms

from System import Array
from System.Drawing import *
from System.Drawing.Drawing2D import GraphicsPath, CustomLineCap, SmoothingMode, LineJoin
from System.Windows.Forms import *
from System.Threading import Thread, ThreadStart, ApartmentState, ParameterizedThreadStart

Mean = 0.0
t0   = 0.0
M00  = 1
M10  = 1
M01  = 1

threshold = 4000

Time_frame = []
Max_frame  = []
Surf_spot  = []
Surf_value = []
Surf_Ratio = []
dumpdata   = False

whitePen = Pen(Color.White, 1)
Pen.DashPattern.SetValue(whitePen, (5, 5))

if SharpCap.SelectedCamera.Controls.ColourSpace.Value == "MONO8":
    I_min = 50
    I_max = 200
    Max_line   = 220
    Focus_line = 150
if SharpCap.SelectedCamera.Controls.ColourSpace.Value == "MONO16":
    I_min = 20000
    I_max = 40000
    Max_line   = 60000
    Focus_line = 25000

start_pos   = SharpCap.Focusers.SelectedFocuser.Position
#best_focus  = 73200
#max_focus   = 74400
best_focus  = 24500
max_focus   = 25500


def beforeframehandler(sender, args):
    global M00, M10, M01, whitePen
    ROI_X = SharpCap.Transforms.SelectionRect.X
    ROI_Y = SharpCap.Transforms.SelectionRect.Y
    dbitmap = args.Frame.GetDrawableBitmap()
    gbitmap = dbitmap.GetGraphics()
    try:
        radius = int(math.sqrt(Surf_spot[-1]/6.28))
        centerX = ROI_X + M10/M00 - 1
        centerY = ROI_Y + M01/M00 - 1
        print("Radius : ",radius, " Center: ", centerX, "  ", centerY)
        gbitmap.DrawEllipse(whitePen, centerX - radius, centerY - radius, radius + radius, radius + radius)

    except:
        print("Error Bildmap")
    gbitmap.Dispose()
    dbitmap.Dispose()
    return()


def framehandler(sender, args):
    global Mean, t0, Time_frame, Max_frame, Surf_spot, Surf_value, Surf_ratio, dumpdata, start_pos, delta_focus, best_focus, max_focus
    global M00, M10, M01, whitePen, threshold
    form.labelRectangle.Text = "Rectangle: " + str(SharpCap.Transforms.SelectionRect.Width) + " x " \
                                + str(SharpCap.Transforms.SelectionRect.Height)
    if (dumpdata):
        try:
            cutout = args.Frame.CutROI(SharpCap.Transforms.SelectionRect)
            dt = (datetime.datetime.now() - t0).total_seconds()
            Time_frame.append(dt)
            data = cutout.ToFloats(0)
            info = cutout.Info
            temp = Array[System.Single](info.Height,info.Width)
            actual_max = 0
            #sum_spalten = []
            #sum_zeilen = []
            for i in range(info.Height):
                for j in range(info.Width):
                    value = data[i,j]
                    if value > actual_max:
                        actual_max = value
            Max_frame.append(actual_max)

            for i in range(1, info.Height - 1):
                for j in range(1, info.Width - 1):
                    temp[i,j] = data[i,0] + ((j - 1)*(data[i,info.Width-1] - data[i,0])/(info.Width - 1)) + \
                                data[0,j] + ((i - 1)*(data[info.Height-1,j] - data[0,j])/(info.Height - 1))
                    temp[i,j] = data[i,j] - temp[i,j]/2.0

            M00 = 1
            M10 = 0
            M01 = 0
            surface = 0
            value   = 0
            #threshold = 4000
            threshold = int(form.textBoxThreshold.Text)

            for i in range(info.Height):
                for j in range(info.Width):
                    M00 += temp[i,j]
                    M01 += (i + 1) * temp[i,j]
                    M10 += (j + 1) * temp[i,j]
                    if temp[i,j] > threshold:
                        surface += 1
                        value += temp[i,j]

            Surf_spot.append(surface)
            Surf_value.append(value)
#            Surf_ratio.append(value/surface)

            print("Max: ", actual_max,  ", Centroid: ", M10/M00, ", ", M01/M00, " surface: ", surface)
#            print("Preceding surface: ", Surf_spot[-2])
            print("Focuser moving :", SharpCap.Focusers.SelectedFocuser.IsMoving)

            form.ShowOxyGraphic()
            cutout.Release()

            if (form.checkbox_focuser.Checked):
                if not SharpCap.Focusers.SelectedFocuser.IsMoving:
                    factor = float(value/Surf_value[0])
                    neu_pos = int((max_focus - best_focus)*math.sqrt(factor) + best_focus)
                    print("Pos: ", neu_pos, " , surface: ", surface, " , value: ", value, " , factor: ", factor)

                    if (factor < 0.5):
                        #neu_pos = int((max_focus - best_focus)*math.sqrt(factor) + best_focus)
                        #Focus_Thread(neu_pos)
                        SharpCap.Focusers.SelectedFocuser.Move(neu_pos)
                        print()
                        print("<0.5    Pos: ", neu_pos, " factor: ", factor)
                        #Surf_value[0] = value
                        #max_focus = neu_pos
                        #time.sleep(0.1)

                    if (factor > 1.1):
                        #neu_pos = int((max_focus - best_focus)*math.sqrt(factor) + best_focus)
                        #Focus_Thread(neu_pos)
                        SharpCap.Focusers.SelectedFocuser.Move(neu_pos)
                        print()
                        print("> 1.1   Pos: ", neu_pos, " factor: ", factor)

        except:
            print("error")
            cutout.Release()
    return()


def Start_Surface(sender, args):
    global Surf_spot, Surf_value, Surf_ratio
    cutout = args.Frame.CutROI(SharpCap.Transforms.SelectionRect)
    data = cutout.ToFloats(0)
    info = cutout.Info
    temp = Array[System.Single](info.Height,info.Width)

    for i in range(1, info.Height - 1):
        for j in range(1, info.Width - 1):
            temp[i,j] = data[i,0] + ((j - 1)*(data[i,info.Width-1] - data[i,0])/(info.Width - 1)) + \
                        data[0,j] + ((i - 1)*(data[info.Height-1,j] - data[0,j])/(info.Height - 1))
            temp[i,j] = data[i,j] - temp[i,j]/2.0

    threshold = 5000
    #threshold = 50
    surface = 0
    value   = 0

    for i in range(info.Height):
        for j in range(info.Width):
            if temp[i,j] > threshold:
                surface += 1
                value += temp[i,j]

    Surf_spot.append(surface)
    Surf_value.append(value)
    Surf_ratio.append(value/surface)
    print("Surface:", Surf_spot, " Value: ", Surf_value, " Ratio: ", Surf_ratio)

    cutout.Release()
    return()


def mean(xs):
    return sum(xs) / len(xs)

def stddev(xs):
    m = sum(xs) / len(xs)
    var = sum(abs(x - m) for x in xs) / len(xs)
    return math.sqrt(var)

# *****************************************************************************
def Frame_measurement(self):
    global t0, Time_frame, Max_frame, Surf_spot, Surf_value, Surf_ratio, dumpdata, start_pos, delta_focus, best_focus, de_focus

    self.Monitoring.Text = "Monitoring running"
    self.Monitoring.BackColor = Color.Red
    self.Monitoring.Enabled = False

    Time_frame = []
    Max_frame  = []
    Surf_spot  = []
    Surf_value = []
    Surf_ratio = []

    dumpdata = True
    t0 = datetime.datetime.now()

    print()
    print("Frame Max Monitoring:")
    print("******************")
    start_pos   = SharpCap.Focusers.SelectedFocuser.Position
    best_focus  = int(self.textBoxBestFocus.Text)
    de_focus    = int(self.textBoxDeFocus.Text) #SharpCap.Focusers.SelectedFocuser.Position


    #SharpCap.SelectedCamera.FrameCaptured += Start_Surface
    #time.sleep(0.004*float(SharpCap.SelectedCamera.Controls.Exposure.Value))
    #print("Start Surface : ", Surf_spot)
    #SharpCap.SelectedCamera.FrameCaptured -= Start_Surface

    SharpCap.SelectedCamera.FrameCaptured += framehandler
    if (form.checkbox_surface.Checked):
        SharpCap.SelectedCamera.BeforeFrameDisplay += beforeframehandler

    temp = 0.0
    while temp < float(self.textBoxTime.Text):
        time.sleep(1.0)
        temp += 1.0
        if dumpdata == False:
            break

    print()
    print("Start Position : ", start_pos)
    print("End Position   : ", SharpCap.Focusers.SelectedFocuser.Position)
    print()
    print("Number: ", len(Time_frame))
    print()

    dumpdata = False
    SharpCap.SelectedCamera.FrameCaptured -= framehandler
    if (form.checkbox_surface.Checked):
        SharpCap.SelectedCamera.BeforeFrameDisplay -= beforeframehandler

    print("End Surface : ", Surf_spot)
    print("End Value : ", Surf_value)
    print("End Ratio : ", Surf_ratio)

    print()
    print("Measurement done")

    self.Monitoring.Text = "Start Measurement"
    self.Monitoring.BackColor = Color.Gainsboro
    self.Monitoring.Enabled = True

    #file_path = "C:\\Data\\"
    #file_name = file_path + "Mean_monitoring_" + str(t0).split()[1].split(".")[0].replace(":","-") + ".txt"
    #file = open(file_name, "w")
    #for index in range(len(Time_frame)):
    #    file.write(str("%.4f" % Time_frame[index]) + " " + str("%.4f" % Mean_frame[index]) + "\n")
    #file.close()

    return()

# *****************************************************************************

class MonitoringMeanMenuForm(Form):
    def __init__(self):
        self.SuspendLayout()
        self.InitializeComponent()
        self.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi
        self.AutoScaleDimensions = SizeF(96, 96)
        self.ResumeLayout()

    def InitializeComponent(self):
        global t0, Time_frame, Max_frame, Surf_spot, Surf_value, Surf_ratio, dumpdata, start_pos, delta_focus, best_focus, de_focus

        self.Text = "Frame Mean Monitoring"
        self.ClientSize = System.Drawing.Size(700, 450)
        self.TopMost = True

        self.Monitoring  = Button()
        self.labelTime   = Label()
        self.textBoxTime = TextBox()
        self.labelThreshold   = Label()
        self.textBoxThreshold = TextBox()
        self.button_StartStop = Button()
        self.button_Exit      = Button()
        self.labelBestFocus   = Label()
        self.textBoxBestFocus = TextBox()
        self.labelDeFocus     = Label()
        self.textBoxDeFocus   = TextBox()
        self.labelRectangle   = Label()
        self.checkbox_surface = CheckBox()
        self.checkbox_focuser = CheckBox()

        self.Monitoring.Text = "Start Measurement"
        self.Monitoring.Location = Point(20, 10)
        self.Monitoring.Click += self.Frame_monitor
        self.Monitoring.AutoSize = True

        self.labelTime.Text = "Measurement time [s]:"
        self.labelTime.Location = Point(150, 14)
        self.labelTime.Name = "labelTime"
        self.labelTime.AutoSize = True

        self.textBoxTime.Text = "1"
        self.textBoxTime.Location = Point(280, 12)
        self.textBoxTime.Name = "textBoxTime"
        self.textBoxTime.Size = Size(40, 20)
        self.textBoxTime.AutoSize = True

        self.labelThreshold.Text = "Threshold:"
        self.labelThreshold.Location = Point(350, 14)
        self.labelThreshold.Name = "labelThreshold"
        self.labelThreshold.AutoSize = True

        self.textBoxThreshold.Text = str(threshold)
        self.textBoxThreshold.Location = Point(415, 12)
        self.textBoxThreshold.Name = "textBoxTime"
        self.textBoxThreshold.Size = Size(40, 20)
        self.textBoxThreshold.AutoSize = True

        self.button_StartStop.Text = "Start"
        self.button_StartStop.Location = Point(500, 10)
        self.button_StartStop.Click += self.StartStop
        self.button_StartStop.AutoSize = True

        self.button_Exit.Text = "Exit"
        self.button_Exit.Location = Point(600, 10)
        self.button_Exit.Click += self.Exit
        self.button_Exit.AutoSize = True

        self.labelBestFocus.Text = "Best Focus:"
        self.labelBestFocus.Location = Point(20, 50)
        self.labelBestFocus.Name = "labelBestFocus"
        self.labelBestFocus.AutoSize = True

        self.textBoxBestFocus.Text = str(best_focus)
        self.textBoxBestFocus.Location = Point(90, 48)
        self.textBoxBestFocus.Name = "textBoxBestFocus"
        self.textBoxBestFocus.Size = Size(60, 20)

        self.labelDeFocus.Text = "De-Focus:"
        self.labelDeFocus.Location = Point(180, 50)
        self.labelDeFocus.Name = "labelBestFocus"
        self.labelDeFocus.AutoSize = True

        self.textBoxDeFocus.Text = str(max_focus)
        self.textBoxDeFocus.Location = Point(250, 48)
        self.textBoxDeFocus.Name = "textBoxDeFocus"
        self.textBoxDeFocus.Size = Size(60, 20)

        self.labelRectangle.Text = "Rectangle:"
        self.labelRectangle.Location = Point(350, 50)
        self.labelRectangle.Name = "labelRectangle"
        self.labelRectangle.AutoSize = True

        self.checkbox_surface.Text = "View Spot Surface"
        self.checkbox_surface.Location = Point(500, 50)
        self.checkbox_surface.AutoSize = True

        self.checkbox_focuser.Text = "Move Focuser"
        self.checkbox_focuser.Location = Point(500, 75)
        self.checkbox_focuser.AutoSize = True

        self.plot = OxyPlot.WindowsForms.PlotView()
        self.plot.Location = System.Drawing.Point(10, 100)
        self.plot.Size = System.Drawing.Size(680, 340)

        self.Controls.Add(self.Monitoring)
        self.Controls.Add(self.labelTime)
        self.Controls.Add(self.textBoxTime)
        self.Controls.Add(self.labelThreshold)
        self.Controls.Add(self.textBoxThreshold)
        self.Controls.Add(self.button_StartStop)
        self.Controls.Add(self.button_Exit)
        self.Controls.Add(self.labelBestFocus)
        self.Controls.Add(self.textBoxBestFocus)
        self.Controls.Add(self.labelDeFocus)
        self.Controls.Add(self.textBoxDeFocus)
        self.Controls.Add(self.labelRectangle)
        self.Controls.Add(self.checkbox_surface)
        self.Controls.Add(self.checkbox_focuser)
        self.Controls.Add(self.plot)

        Time_frame = []
        Max_frame  = []
        Surf_spot  = []
        Surf_value = []
        Surf_ratio = []
        t0 = datetime.datetime.now()

        SharpCap.Transforms.SelectTransform("ROI Selection")    # Show the red selection rectangle

    def ShowOxyGraphic(self):
        global Time_frame, Mean_frame

        self.plotmodel = OxyPlot.PlotModel()
        if SharpCap.SelectedCamera.Controls.ColourSpace.Value == "MONO8":
            self.YAxis = OxyPlot.Axes.LinearAxis(Position = OxyPlot.Axes.AxisPosition.Right, Minimum = 0.0, Maximum = 260)
        if SharpCap.SelectedCamera.Controls.ColourSpace.Value == "MONO16":
            self.YAxis = OxyPlot.Axes.LinearAxis(Position = OxyPlot.Axes.AxisPosition.Right, Minimum = 0.0, Maximum = 66000)

        self.points_Max  = []

        for i in range(0,len(Time_frame)):
            self.points_Max.Add(OxyPlot.DataPoint(Time_frame[i],Max_frame[i]))

        self.serie = OxyPlot.Series.LineSeries()
        self.serie.Color  = OxyPlot.OxyColors.Red
        self.serie.MarkerSize = 1
        self.serie.MarkerType = OxyPlot.MarkerType.Circle
        self.serie.StrokeThickness = 1
        self.serie.ItemsSource = self.points_Max

        self.plotmodel.Axes.Add(self.YAxis)
        self.plotmodel.Series.Add(self.serie)

        self.plot.Model = self.plotmodel

    def Frame_monitor(self, sender, event):
        th = Thread(ParameterizedThreadStart(Frame_measurement))
        th.SetApartmentState(ApartmentState.STA)
        th.Start(self)

    def Exit(self, sender, event):
        print("Exit monitoring script")
        SharpCap.Transforms.SelectTransform(None)               # Hide the red selection rectangle
        SharpCap.SelectedCamera.FrameCaptured -= framehandler
        self.Close()

    def StartStop(self, sender, event):
        global dumpdata
        if (self.button_StartStop.Text == "Start"):
            self.button_StartStop.Text = "Stop"
            print("Start")
            self.checkbox_surface.Enabled = False
            self.checkbox_focuser.Enabled = False
            dumpdata = True
            SharpCap.SelectedCamera.FrameCaptured += framehandler
            if (form.checkbox_surface.Checked):
                SharpCap.SelectedCamera.BeforeFrameDisplay += beforeframehandler

        else: #self.button_StartStop.Text == "Stop"
            self.button_StartStop.Text = "Start"
            print("Stop")
            self.checkbox_surface.Enabled = True
            self.checkbox_focuser.Enabled = True
            dumpdata = False
            SharpCap.SelectedCamera.FrameCaptured -= framehandler
            if (form.checkbox_surface.Checked):
                SharpCap.SelectedCamera.BeforeFrameDisplay -= beforeframehandler


def Measurement_Stop(self, sender):
    global dumpdata
    dumpdata = False
    SharpCap.SelectedCamera.FrameCaptured -= framehandler
    SharpCap.SelectedCamera.BeforeFrameDisplay -= beforeframehandler
    print("Stop")
    

#def launch_form():
form = MonitoringMeanMenuForm()
form.StartPosition = FormStartPosition.CenterScreen
form.TopMost = True
form.FormClosing += Measurement_Stop
form.Show()

#SharpCap.AddCustomButton(" Mean__Monitoring  |", None, "Mean__Monitoring", launch_form)
Betelgeuse_test_focus.png
Betelgeuse_test_focus.png (103.95 KiB) Viewed 208 times
It was a script that I started to write before the occultation of Betelgeuse last December.
The idea was to change the focus of the telescope during the occultation.
A fainter star than Betelgeuse was focused (for example: Best Focus at 24500) and then move to Betelgeuse
Defocus the telescope until no overexposure (in this case defocus at 25500).
During the occultation, the focuser should refocus (from 25500 to 24500 and back) during the dimming of the spot.

The script was not completely finish and during November and December *zero* night for testing !
So I did not use the script during the occultation of Betelgeuse.

In the script ... you can see (and learn) how to catch a frame and perform some processing.
The script activates the selection red rectangle. You can move and re-dimension it with the mouse.
The graphic shows the maximum value of the spot from the red rectangle.
You can modify it for showing the average value of the spectrum.

The focuser part can be deleted. If you have something ... I can do a "review" of your script :-)

Regards,
Jean-Francois
Jean-Francois
Posts: 402
Joined: Sun Oct 13, 2019 10:52 am
Location: Germany

Re: Plot profile - slitless spectroscopy

#5

Post by Jean-Francois »

One point more ...
The graphic shows the maximum value on the red rectangle during the movement of the rectangle over the image during 10 seconds.
MattTexas23
Posts: 12
Joined: Sun Sep 03, 2023 6:11 am

Re: Plot profile - slitless spectroscopy

#6

Post by MattTexas23 »

Many thanks Jean-Francois! I have to chew a bit on this, very helpful!
Post Reply