Skip to content

Commit

Permalink
Merge pull request #5 from TypQxQ/dev
Browse files Browse the repository at this point in the history
From Dev
  • Loading branch information
TypQxQ authored Nov 30, 2023
2 parents ed36ee8 + ad8e1c8 commit 34dc2cd
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 55 deletions.
2 changes: 2 additions & 0 deletions TODO.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
TODO:
- Important
- Add variable for wait after movement, add as pause in server before doing calibration
- Rewrite Readme wit Copilot and spellcheck.
- Logo
- Move inmemory log to file on srv. maybe
- Less important
- Trivial
- Put timers on operations like calibrate camera and get possition.

79 changes: 37 additions & 42 deletions extension/ktamv.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ def __init__(self, config):
# Load config values
self.camera_url = config.get("nozzle_cam_url")
self.server_url = config.get("server_url")
self.speed = config.getfloat("move_speed", 50.0, above=10.0)
self.speed = config.getfloat("move_speed", 1800.0, above=10.0)
self.calib_iterations = config.getint(
"calib_iterations", 1, minval=1, maxval=25
)
self.calib_value = config.getfloat("calib_value", 1.0, above=0.25)
self.send_frame_to_cloud = config.getboolean("send_frame_to_cloud", False)
self.detection_tolerance = config.getint("detection_tolerance", 0, minval=0, maxval=5)

# Initialize variables
self.mpp = None # Average mm per pixel
self.is_calibrated = False # Is the camera calibrated
self.last_nozzle_center_successful = False # Was the last calibration successful

# List of space coordinates for each calibration point
self.space_coordinates = []
Expand Down Expand Up @@ -61,11 +63,6 @@ def handle_ready(self):
self.gcode.register_command(
"KTAMV_GET_OFFSET", self.cmd_GET_OFFSET, desc=self.cmd_GET_OFFSET_help
)
self.gcode.register_command(
"KTAMV_MOVE_TO_ORIGIN",
self.cmd_MOVE_TO_ORIGIN,
desc=self.cmd_MOVE_TO_ORIGIN_help,
)
self.gcode.register_command(
"KTAMV_SIMPLE_NOZZLE_POSITION",
self.cmd_SIMPLE_NOZZLE_POSITION,
Expand Down Expand Up @@ -126,6 +123,7 @@ def cmd_SEND_SERVER_CFG(self, gcmd):
"/set_server_cfg",
camera_url=_camera_url,
send_frame_to_cloud=self.send_frame_to_cloud,
detection_tolerance=self.detection_tolerance,
)
# gcmd.respond_info("Sent server configuration to server")
gcmd.respond_info("kTAMV Server response: %s" % str(rr))
Expand All @@ -139,21 +137,11 @@ def cmd_SEND_SERVER_CFG(self, gcmd):

def cmd_SET_CENTER(self, gcmd):
self.cp = self.pm.get_raw_position()
self.cp = (float(self.cp[0]), float(self.cp[1]))
self.cp = (round(float(self.cp[0]),3), round(float(self.cp[1]),3))
self.gcode.respond_info(
"Center position set to X:%3f Y:%3f" % (self.cp[0], self.cp[1])
)

cmd_MOVE_TO_ORIGIN_help = ("Sets the center position for offset"
+ "calculations based on the current toolhead position")

def cmd_MOVE_TO_ORIGIN(self, gcmd):
self.cp = self.pm.get_raw_position()
self.cp = (float(self.cp[0]), float(self.cp[1]))
self.gcode.respond_info(
"Center position set to X:%3f Y:%3f" % self.cp[0], self.cp[1]
)

cmd_GET_OFFSET_help = (
"Get offset from the current position to the configured center position"
)
Expand All @@ -167,8 +155,8 @@ def cmd_GET_OFFSET(self, gcmd):
return
_pos = self.pm.get_raw_position()
self.last_calculated_offset = (
float(_pos[0]) - self.cp[0],
float(_pos[1]) - self.cp[1],
round((float(_pos[0]) - self.cp[0]),3),
round((float(_pos[1]) - self.cp[1]),3),
)

self.gcode.respond_info(
Expand All @@ -183,6 +171,7 @@ def cmd_FIND_NOZZLE_CENTER(self, gcmd):
##############################
# Calibration of the tool
##############################
self.last_nozzle_center_successful = False
self._calibrate_nozzle(gcmd)

cmd_SIMPLE_NOZZLE_POSITION_help = (
Expand Down Expand Up @@ -262,6 +251,7 @@ def _calibrate_px_mm(self, gcmd):
_xy = self.pm.get_gcode_position()

for i in range(len(calibration_coordinates)):
_rr = _xy = None
# Move to calibration location and get the nozzle position
# If it is not found, skip this calibration point
try:
Expand Down Expand Up @@ -311,33 +301,35 @@ def _calibrate_px_mm(self, gcmd):
#
# Finish the calibration loop
#
gcmd.respond_info("Found %s calibration points" % str(len(self.mm_per_pixels)))

# Move back to the center and get coordinates for the center
gcmd.respond_info("Moving back to starting position")
_olduv = _uv # Last position to get center from inverted move
try:
_rr, _xy = self.move_relative_and_get_nozzle_position(
-calibration_coordinates[i][0],
-calibration_coordinates[i][1],
gcmd,
)
except NozzleNotFoundException as e:
_rr = None

# If we did not get a response, indicate it by setting _uv to None
if _rr is None:
_uv = None
else:
# Save the new nozzle position as UV 2D coordinates
_uv = json.loads(_rr["data"])
if _rr is not None:
try:
_rr, _xy = self.move_relative_and_get_nozzle_position(
-calibration_coordinates[i][0],
-calibration_coordinates[i][1],
gcmd,
)
except NozzleNotFoundException as e:
_rr = None

# Calculate mm per pixel and save it to a list
mpp = self.getMMperPixel(calibration_coordinates[i], _olduv, _uv)
# Save the 3D space coordinates, 2D camera coordinates and mm/px
self._save_coordinates_for_matrix(_xy, _uv, mpp)
self.gcode.respond_info(
"Calibrated camera center: mm/pixel found: %.4f" % (mpp)
)
# If we did not get a response, indicate it by setting _uv to None
if _rr is None:
_uv = _olduv = None
else:
_olduv = _uv # Last position to get center from inverted move
# Save the new nozzle position as UV 2D coordinates
_uv = json.loads(_rr["data"])

# Calculate mm per pixel and save it to a list
mpp = self.getMMperPixel(calibration_coordinates[i], _olduv, _uv)
# Save the 3D space coordinates, 2D camera coordinates and mm/px
self._save_coordinates_for_matrix(_xy, _uv, mpp)
self.gcode.respond_info(
"Calibrated camera center: mm/pixel found: %.4f" % (mpp)
)

#
# All calibration points are done, calculate the average mm per pixel
Expand Down Expand Up @@ -528,6 +520,7 @@ def _calibrate_nozzle(self, gcmd, retries=30):
# finally, we're aligned to the center
elif _offsets[0] == 0.0 and _offsets[1] == 0.0:
self.gcode.respond_info("Calibration to nozzle center complete")
self.last_nozzle_center_successful = True
return

except Exception as e:
Expand Down Expand Up @@ -653,6 +646,8 @@ def get_status(self, eventtime=None):
"is_calibrated": self.is_calibrated,
"send_frame_to_cloud": self.send_frame_to_cloud,
"camera_center_coordinates": self.cp,
"travel_speed": self.speed,
"last_nozzle_center_successful": self.last_nozzle_center_successful,
}
return status

Expand Down
4 changes: 2 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ install_klipper_config() {
echo -e "nozzle_cam_url: http://localhost/webcam/snapshot?max_delay=0" >> "${dest}" # Add the address of the webcam stream that will be accessed by the server
echo -e "server_url: http://localhost:${PORT}" >> "${dest}" # Add the address of the kTAMV server that will be accessed Klipper
echo -e "move_speed: 1800" >> "${dest}" # Add the speed at which the toolhead moves when aligning
echo -e "send_frame_to_cloud: ${SEND_IMAGES}" >> "${dest}" # Add the speed at which the toolhead moves when aligning

echo -e "send_frame_to_cloud: ${SEND_IMAGES}" >> "${dest}" # If true, the images of the nozzle will be sent to the developer
echo -e "detection_tolerance: 0" >> "${dest}" # number of pixels to have as tolerance when detecting the nozzle.

log_info "Added kTAMV configuration to printer.cfg"
log_important "Please check the configuration in printer.cfg and adjust it as needed"
Expand Down
85 changes: 85 additions & 0 deletions ktamv_automation_example.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
[gcode_macro tool_align_macro]
description = Aligns all tools

# This macro will align all tools using the KTAMV plugin
gcode:
# Constants to be customized:
{% set VAR_CAMERA_CENTER_COORDINATES = [ 10.0, 10.0] %}# Where you have your camera, X,Y
{% set VAR_TOOL_TO_USE_FOR_CALIBRATION = 0 %}# Tool to use for calibrating the camera
{% set VAR_TOOL_TO_USE_AS_REFERENCE = 0 %}# Tool to use as reference for the offsets
{% set VAR_TOOLS_TO_ALIGN = [ 1, 2, 3] %}# Tools to align and get offset to the reference tool


# Check if printer is homed, mm/px is calculated and we have set a origin for our tools
{% if 'xy' not in printer.toolhead.homed_axes%}
RESPOND PREFIX="info" MSG="Printer not homed."
# If camera is not calibrated or origin position not set, do both.
{% elif printer.ktamv.camera_center_coordinates[0]|default("none")|lower =="none" or printer.ktamv.mm_per_pixels|default("none")|lower == "none"%}
# Set up the server
KTAMV_SEND_SERVER_CFG
# Chose the tool to calibrate with.
T{VAR_TOOL_TO_USE_FOR_CALIBRATION}
# Move to the camera center
G0 X{VAR_CAMERA_CENTER_COORDINATES[0]} Y{VAR_CAMERA_CENTER_COORDINATES[1]} F{printer.ktamv.travel_speed}
# Wait for tool to finish moving
M400
# Calibrate the camera
KTAMV_CALIB_CAMERA
# Load the tool to use as reference for our offsets
T{VAR_TOOL_TO_USE_AS_REFERENCE}
# Move to the camera center
G0 X{VAR_CAMERA_CENTER_COORDINATES[0]} Y{VAR_CAMERA_CENTER_COORDINATES[1]} F{printer.ktamv.travel_speed}
# Find the nozzle center
KTAMV_FIND_NOZZLE_CENTER
# Set the origin to calibrate from if the above calibration was successful
_tool_align_macro_set_origin_if_successful
{% endif %}

# Align all tools, checking everything is ok now inside the next macro because in this the variables have not updated yet.
{% for tool in VAR_TOOLS_TO_ALIGN %}
RESPOND PREFIX="info" MSG="-Aligning tool T{tool}"
_tool_align_macro_align_tool TOOL={tool}
{% endfor %}

# Set Origin if last nozzle center was successful.
# Needs to be in separate macro to be calculated after the previous command is finished.
[gcode_macro _tool_align_macro_set_origin_if_successful]
gcode:
{% if printer.ktamv.last_nozzle_center_successful|default("false")|lower =="true" %}
KTAMV_SET_ORIGIN
{% endif %}

# Align tool macro
[gcode_macro _tool_align_macro_align_tool]
gcode:
# Get tool from params
{% set TOOL = params.TOOL %}

# Check if printer is homed, mm/px is calculated and we have set a origin for our tools
# If everything above is true, we can start the tool alignment
{% if printer.ktamv.camera_center_coordinates[0]|default("none")|lower !="none" and printer.ktamv.mm_per_pixels|default("none")|lower != "none" and 'xy' in printer.toolhead.homed_axes%}
RESPOND PREFIX="info" MSG="Aligning tool T{TOOL}"
# Load tool
T{TOOL}
# Move to camera center
G0 X{printer.ktamv.camera_center_coordinates[0]} Y{printer.ktamv.camera_center_coordinates[1]} F{printer.ktamv.travel_speed}
# Get nozzle center
KTAMV_FIND_NOZZLE_CENTER
# Get offset
KTAMV_GET_OFFSET
# Save offset using the next macro
_tool_align_macro_save_offset TOOL={TOOL}
{% else %}
RESPOND PREFIX="info" MSG="Camera not calibrated or origin not set. Please run the macro again."
{% endif %}

# Macro to save offset
[gcode_macro _tool_align_macro_save_offset]
gcode:
# Get tool from params
{% set TOOL = params.TOOL %}
# If offset is calculated, we can save it
{% if printer.ktamv.last_nozzle_center_successful|default("false")|lower =="true" %}
# Add your code here to save the offset
{action_respond_info("Macro example saving offset X:"~printer.ktamv.last_calculated_offset[0]~" Y:"~printer.ktamv.last_calculated_offset[1]~" for tool T"~TOOL)}
{% endif %}
9 changes: 8 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ nozzle_cam_url: http://localhost/webcam2/snapshot?max_delay=0
server_url: http://localhost:8085
move_speed: 1800
send_frame_to_cloud: false
detection_tolerance: 0
```
If your nozzle webcamera is on another stream, change that. You can find out what the stream is called in the Mainsail camera configuration. For example, here this is webcam2, so my configuration would be:
Expand All @@ -55,6 +56,8 @@ Change the `server_url` if you run on another machine or port.

`send_frame_to_cloud` indicates if you want to contribute to possible future development of AI based detection.

`detection_tolerance` If the nozzle position is within this many pixels when comparing frames, it's considered a match. Only whole numbers are supported.

## Setting up the server image in Mainsail
Add a webcam with and configure like in the image:
- Any name you like
Expand All @@ -73,12 +76,16 @@ Use the printer IP and not localhost or Mainsail will try to connect to the comp
2. Home the printer and move the endstop or nozzle over the camera so that it is aproximatley in the middle of the image. You can run the `KTAMV_START_PREVIEW` command to help you orientate.
2. Run the `KTAMV_CALIB_CAMERA` command to detect the nozzle or endstop. Note that it can have problems with endstops and it's easier to calibrate using a nozzle.
3. If successfull, run the `KTAMV_FIND_NOZZLE_CENTER` command to center the nozzle or endstop.
4. Run the `KTAMV_SET_ORIGIN` command to set this as the origin for all other offsets.
4. Run the `KTAMV_SET_ORIGIN` command to set this as the origin for all other offsets. If a tool is selected, this should not have any XY offsets applied.
5. Change to another tool and move the nozzle over the camera so that it is aproximatley in the middle of the image. You can run the `KTAMV_START_PREVIEW` command to help you orientate.
6. Run the `KTAMV_FIND_NOZZLE_CENTER` command to center the nozzle.
7. Run the `KTAMV_GET_OFFSET` to get the offset from when the first tool or nozzle was in the middle of the image.
8. Run step 5 - 7 for every tool to get their offset.

## Can it be automated?
Of course! And here is a macro you can use as a start point:
[ktamv_automation_example.cfg](ktamv_automation_example.cfg)

## Debug logs
The kTAMV server logs in memory and everything can be displayed on it's root path.
`http://my_printer_ip_address:8085/`
Expand Down
28 changes: 18 additions & 10 deletions server/ktamv_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@
__CV_TIMEOUT = 20
# Minimum amount of matches to confirm toolhead position after a move
__CV_MIN_MATCHES = 3
# If the nozzle position is within this tolerance, it's considered a match. 1.0 would be 1 pixel. Only whole numbers are supported.
__CV_UV_TOLERANCE = 1
# Size of frame to use
_FRAME_WIDTH = 640
_FRAME_HEIGHT = 480

# FPS to use when running the preview
__PREVIEW_FPS = 2

# If the nozzle position is within this many pixels when comparing frames, it's considered a match. Only whole numbers are supported.
__detection_tolerance = 0
# Wheather to update the image at next request
__update_static_image = True
# Error message to show on the image
__error_message_to_image = ""

# Indicates if preview is running
Expand Down Expand Up @@ -56,9 +58,10 @@
# Define a global variable to store the camera path.
_camera_url = None
# Whether to send the frame to the cloud
_send_frame_to_cloud = False
__send_frame_to_cloud = False
# Define a global variable to store a key-value pair of the request id and the result
request_results = dict()
# The transform matrix calculated from the calibration points
_transformMatrix = None


Expand Down Expand Up @@ -135,7 +138,7 @@ def set_server_cfg():
response = ""

# Stoping preview if running
global __preview_running
global __preview_running, __detection_tolerance, __send_frame_to_cloud
__preview_running = False

# Get the camera path from the JSON object
Expand All @@ -153,14 +156,19 @@ def set_server_cfg():
pass

if send_frame_to_cloud is not None:
global _send_frame_to_cloud
if send_frame_to_cloud == True:
_send_frame_to_cloud = True
__send_frame_to_cloud = True
response += "send_frame_to_cloud set to True\n"
else:
_send_frame_to_cloud = False
__send_frame_to_cloud = False
response += "send_frame_to_cloud set to False\n"

try:
data = json.loads(request.data)
__detection_tolerance = data.get("detection_tolerance")
except:
pass

if camera_url is None:
show_error_message_to_image("Error: Could not set camera URL.")
return "Camera path not found in JSON", 400
Expand All @@ -172,7 +180,7 @@ def set_server_cfg():
_camera_url = camera_url
# Return code 200 to web browser
log(f"*** end of set_server_cfg (set to {_camera_url}) ***<br>")
show_error_message_to_image("Camera url set. Cloud upload: " + str(_send_frame_to_cloud))
show_error_message_to_image("Camera url set.")
return response + "Camera path set to " + _camera_url, 200
else:
show_error_message_to_image("Error: Invalid nozzle_cam_url.")
Expand Down Expand Up @@ -278,11 +286,11 @@ def getNozzlePosition():
def do_work():
log("*** calling do_work ***")
detection_manager = dm(
log, _camera_url, __CLOUD_URL, _send_frame_to_cloud
log, _camera_url, __CLOUD_URL, __send_frame_to_cloud
)

position = detection_manager.recursively_find_nozzle_position(
put_frame, __CV_MIN_MATCHES, __CV_TIMEOUT, __CV_UV_TOLERANCE
put_frame, __CV_MIN_MATCHES, __CV_TIMEOUT, __detection_tolerance
)

log("position: " + str(position))
Expand Down

0 comments on commit 34dc2cd

Please sign in to comment.