From ce2b30eef3bb268e3cda803d51e3afa462f085e7 Mon Sep 17 00:00:00 2001 From: TypQxQ Date: Fri, 1 Dec 2023 00:11:08 +0100 Subject: [PATCH 1/2] Updates.. --- TODO.txt | 2 + extension/ktamv.py | 79 ++++++++++++++++----------------- install.sh | 4 +- ktamv_automation_example.cfg | 85 ++++++++++++++++++++++++++++++++++++ readme.md | 9 +++- server/ktamv_server.py | 28 +++++++----- 6 files changed, 152 insertions(+), 55 deletions(-) create mode 100644 ktamv_automation_example.cfg diff --git a/TODO.txt b/TODO.txt index 9e89aa8..62df325 100644 --- a/TODO.txt +++ b/TODO.txt @@ -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. + diff --git a/extension/ktamv.py b/extension/ktamv.py index 13653ea..14ae4e4 100644 --- a/extension/ktamv.py +++ b/extension/ktamv.py @@ -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 = [] @@ -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, @@ -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)) @@ -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" ) @@ -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( @@ -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 = ( @@ -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: @@ -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 @@ -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: @@ -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 diff --git a/install.sh b/install.sh index 3db1881..085b70a 100644 --- a/install.sh +++ b/install.sh @@ -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" diff --git a/ktamv_automation_example.cfg b/ktamv_automation_example.cfg new file mode 100644 index 0000000..aba54e1 --- /dev/null +++ b/ktamv_automation_example.cfg @@ -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 %} \ No newline at end of file diff --git a/readme.md b/readme.md index 0d00a43..4bb38de 100644 --- a/readme.md +++ b/readme.md @@ -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: @@ -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` number of pixels to have as tolerance when detecting the nozzle. + ## Setting up the server image in Mainsail Add a webcam with and configure like in the image: - Any name you like @@ -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/` diff --git a/server/ktamv_server.py b/server/ktamv_server.py index 7d4cd6b..5a7556e 100644 --- a/server/ktamv_server.py +++ b/server/ktamv_server.py @@ -16,8 +16,6 @@ __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 @@ -25,7 +23,11 @@ # FPS to use when running the preview __PREVIEW_FPS = 2 +# If the nozzle position is within this tolerance, it's considered a match. 1.0 would be 1 pixel. 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 @@ -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 @@ -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 @@ -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 @@ -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}) ***
") - 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.") @@ -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)) From ad8e1c8b114b05a333833b8d83dc6834977b685a Mon Sep 17 00:00:00 2001 From: TypQxQ Date: Fri, 1 Dec 2023 00:19:53 +0100 Subject: [PATCH 2/2] Rephrase --- readme.md | 2 +- server/ktamv_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 4bb38de..6f9dc08 100644 --- a/readme.md +++ b/readme.md @@ -56,7 +56,7 @@ 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` number of pixels to have as tolerance when detecting the nozzle. +`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: diff --git a/server/ktamv_server.py b/server/ktamv_server.py index 5a7556e..f0ebd43 100644 --- a/server/ktamv_server.py +++ b/server/ktamv_server.py @@ -23,7 +23,7 @@ # FPS to use when running the preview __PREVIEW_FPS = 2 -# If the nozzle position is within this tolerance, it's considered a match. 1.0 would be 1 pixel. Only whole numbers are supported. +# 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