Wednesday, September 21, 2022

Robotic Paramount MyT Polar Alignment Part 2 - Software Automation

This post is obsolete, please go here for the updated version


Having built the motor controls, I wanted to see if there is a way to automate analysis of a star in the Accurate Polar Alignment process, and even move it into the center of the image using polar axes' motion.  After some programming work, the process is working well, and appears to be quite reliable.  Here are the parts:

1. Image acquisition - using Ken Sturrock's PySkyX_ks library one can easily obtain an image from camera using their preferred filter

2. Import image data using astropy.io fits.getdata() function 

3. Process the image with Gaussian Blur to remove hot pixels , then the alignment star is the brightest spot, done as as 2 lines of code with OpenCV, and you get the coordinates

4. Center of the image is just image size divided by 2 in both directions

5. Move the motors in one axis to obtain 2 points on that axis.  This is done by injecting G Code into a cheap GRBL controller to the motors, described in my previous post

6. The hard part was calculating where to go now, but thanks to this math solution by Paul Bourke it's an easy calculation to find the point closest to the image center and go there with your motors.

7. Repeat for the other axis, and repeat the process until you are close enough. 

I was able to reliably get within 60 pixels of the center in just 8 iterations.

Here is the current code:


#!/usr/bin/env python3
####################
# Import Libraries #
####################

from library.PySkyX_ks import *

import time
import os

import numpy as np
import math
import cv2
from astropy.io import fits

import serial.tools.list_ports
import serial

# Exposure
exposure = 2
# Gaussian blur radius
radius = 33
# Acceptable distance to origin to end run
ok_dist_to_origin = 100
# Acceptable distance to tangent point to move to next axis
ok_dist_to_tangent = 100
# Number of iterations to end the program
max_iterations = 90
# max motor move units
max_move = 30
# Backlash detection, if moved less than this number of pixels
backlash_pix_move = 30
# Motion initial moves and hints
travel = {}
expected_resolution = {}
travel["vertical"] = 8
travel["horizontal"] = 30
# These are system specific, and need to be measured
expected_resolution["vertical"] = 51
expected_resolution["horizontal"] = 22


def command(ser, command):
  command += "\r\n"
  #print ("Injecting " + command)
  ser.write(str.encode(command))
  time.sleep(2)
  while True:
    line = ser.readline()
    #print(line)
    if line == b'ok\r\n':
      time.sleep(5)
      return


def move_motors(port,direction, dist):
    #start_gcode = "G1G21G91"
    if dist > max_move:
        print ("Reducing distance to max units: " + max_move)
        dist = max_move
    start_gcode = "G21G91"
    feed_rate = 1
    if direction == "left":
        cstr = f"{start_gcode}X{dist}Y-{dist}F{feed_rate}"
    elif direction == "right":
        cstr = f"{start_gcode}X-{dist}Y{dist}F{feed_rate}"
    elif direction == "up":
        cstr = f"{start_gcode}Z{dist}F{feed_rate}"
    elif direction == "down":
        cstr = f"{start_gcode}Z-{dist}F{feed_rate}"
    print("Moving " +  direction + " " + str(dist) + " units")
    command(port,cstr)

def swap_direction(axis):
    if axis == "vertical":
        direction[axis] = "up" if (direction[axis] == "down") else "down"
    else:
        direction[axis] = "left" if (direction[axis] == "right") else "right"
    print ("Reversing direction to: " + direction[axis])


def take_image():
    TSXSend("ccdsoftCamera.filterWheelConnect()")  
    TSXSend("ccdsoftCamera.FilterIndexZeroBased = 0") # Luminance    
    timeStamp("Taking image: " + str(exposure) + "s")
    TSXSend("ccdsoftCamera.Asynchronous = false")
    TSXSend("ccdsoftCamera.AutoSaveOn = true")
    TSXSend("ccdsoftCamera.Frame = 1")
    TSXSend("ccdsoftCamera.Subframe = false")
    TSXSend("ccdsoftCamera.ExposureTime = " + str(exposure))
    TSXSend("ccdsoftCamera.Delay = 1")
    TSXSend("ccdsoftCamera.TakeImage()")
    TSXSend("ccdsoftCameraImage.AttachToActiveImager()")
    cameraImagePath =  TSXSend("ccdsoftCameraImage.Path").split("/")[-1]
    return cameraImagePath

# Find the motor controller
ports = serial.tools.list_ports.comports(include_links=False)
for port in ports :
    # These are specific to my Arduino, can be found with dumping serial port data
    if port.vid == 6790 and port.pid == 29987:
        GDEV = port.device
# GDEV = "COM7" # override if needed
ser = serial.Serial(GDEV, 115200)
time.sleep(2)
command(ser,"G1 G54 G17 G21 G90 G94 M5 M9 T0 F0 S0")
time.sleep(2)

# Set the desired autosave directory
ASPath = "C:/Users/Alex/Pictures/cal/apa"
# Make this dir or ignore if it already exists
os.makedirs(ASPath, exist_ok=True)
# Set the autopath root, TSX will append a date folder to it
TSXSend("ccdsoftCamera.AutoSavePath = '" + ASPath + "'")

starcoords = []
ctrdists = []
resolution = {}
direction = {}
backlash = {}

while True:
    for axis in ("vertical","horizontal"):
        axis_runs = 0
        while True:
            imgfile = take_image()
            imgData = fits.getdata(imgfile, ext=0)
            os.remove(imgfile) # clean up
            (imgY,imgX) = (imgData.shape) # size of image in pixels
           
            # find the (x, y) coordinates of the area of the image with the largest intensity value
            imgData = cv2.GaussianBlur(imgData,(radius,radius), 0)
            (minVal, maxVal, minLoc, maxLoc) = cv2.minMaxLoc(imgData)

            #print ("min " + str(minVal) + " max " + str(maxVal) + " minloc " + str(minLoc) + " maxloc " + str(maxLoc))

            # Coordinates of the star
            (starX, starY) = maxLoc
            starcoords.append(maxLoc)
            iteration = len(starcoords)
            iter_index = iteration - 1
            print("\r\nIteration: " + str(iteration))

            # Coordinates of center (crosshairs) of image
            midX = int(imgX/2)
            midY = int(imgY/2)

            print (f"Star at {starX},{starY} - Center at {midX},{midY}")

            # Calculate the Euclidean distance to origin
            DistToOrig = int(math.dist([starX, starY], [midX, midY]))
            ctrdists.append(DistToOrig)
            print("Distance to center: " + str(DistToOrig))

            # Are we close enough to finish up?
            if DistToOrig <= ok_dist_to_origin:
                print("We are close enough, exiting.")
                ser.close()
                exit()

                # Have we reach maximum iterations
            if iteration > max_iterations:
                print("Reached maximum moves, exiting.")
                ser.close()
                exit()

            # Ask if we want to proceed or exit
            #abortYN = input("Abort further tries? (YN): ").upper()
            #if abortYN == "Y":
            #    ser.close()
            #    exit()

            # take a guess as to direction
            if axis_runs == 0 and iteration == 1:
                direction["horizontal"] = "left" if starX < midX else "right"
                direction["vertical"] = "down" if starY < midY else "up"

            # Run analysis when we have at least 2 points of the line
            if axis_runs > 1:
                pix_traveled = int(math.dist(starcoords[iter_index], starcoords[iter_index - 1]))
                resolution[axis] = pix_traveled/travel[axis]
                # Sanity check for after backlash travel
                if resolution[axis] < (expected_resolution[axis] / 2):
                    resolution[axis] = expected_resolution[axis]

                # We traveled too little, ignore for small moves
                if pix_traveled < backlash_pix_move and travel[axis] > 7:
                    backlash_travel = int(travel[axis] - pix_traveled / resolution[axis])
                    print (f"Moved only {pix_traveled} pixels, possible backlash,compensating {backlash_travel} units")
                    move_motors(ser,direction[axis],backlash_travel)
                    continue

                print (f"Traveled {direction[axis]} for {pix_traveled} pixels at {resolution[axis]} pixels/unit")

                # Where is the tangent point with the middle?
                (X1, Y1) = starcoords[iter_index]
                (X2, Y2) = starcoords[iter_index - 1]
                X3 = midX
                Y3 = midY

                # Coordinates of the tangent intersect point
                if axis_runs == 2 or travel[axis] > 5:
                    # per http://paulbourke.net/geometry/pointlineplane/
                    p2p1 = math.dist([X1, Y1], [X2, Y2])
                    u = ((X3-X1)*(X2-X1) + (Y3-Y1)*(Y2-Y1)) / (p2p1 * p2p1)
                    X = int(X1 + u*(X2-X1))
                    Y = int(Y1 + u*(Y2-Y1))

                pixels_to_tangent = int(math.dist([X1, Y1], [X, Y]))
                print (f"Tangent point X={X} Y={Y} distance from current position: {pixels_to_tangent}")
               
                # Set new distance to travel so we reach the tangent point
                # Undershoot a little, because reversing reduces precision
                travel[axis] = int(pixels_to_tangent * 0.95 / resolution[axis])
                if travel[axis] == 0:
                    print ("Increasing distance from 0 to 3")
                    travel[axis] = 3
                elif travel[axis] > max_move:
                    print (f"Reducing distance from {travel[axis]} to max {max_move} units")
                    travel[axis] = max_move

                if pixels_to_tangent <= ok_dist_to_tangent:
                    print(f"Close enough to tangent, ending this {axis} axis run")
                    break
               
                # Have we overshot the tangent?
                (XP, YP) = starcoords[iter_index -1]
                prev_pix_to_tangent = int(math.dist([XP, YP], [X, Y]))

                # Reverse either if we overshot tangent, or getting away from center
                if (pix_traveled > prev_pix_to_tangent) or (ctrdists[iter_index] > ctrdists[iter_index-1]):
                    swap_direction(axis)
                                       
            # Move
            move_motors(ser,direction[axis],travel[axis])

            # Increment per axis counter
            axis_runs += 1

No comments:

Post a Comment

Robotic Paramount MyT Polar Alignment Update

This is an all-in-one combined post about the Robotic Polar Alignment project I initially published on June 6, 2022.   All the pieces are wo...