From 08a8a1ddc3301cfb19f0ce211359286f0dcb04ab Mon Sep 17 00:00:00 2001 From: Mitko Haralanov Date: Fri, 17 Nov 2023 09:42:04 -0800 Subject: [PATCH] bed_mesh: Implement adaptive bed mesh Adaptive bed mesh allows the bed mesh algorithm to probe only the area of the bed that is being used by the current print. It uses [exclude_objects] to get a list of the printed objects and their area on the bed. It, then, modifies the bed mesh parameters so only the area used by the objects is measured. Adaptive bed mesh works on both cartesian and delta kinematics printers. On Delta printers, the algorithm, adjusts the origin point and radius in order to translate the area of the bed being probe. Signed-off-by: Mitko Haralanov Signed off-by: Kyle Hansen --- docs/Bed_Mesh.md | 46 +++++++++- docs/img/adaptive_bed_mesh.svg | 4 + docs/img/adaptive_bed_mesh_margin.svg | 4 + klippy/extras/bed_mesh.py | 121 +++++++++++++++++++++++++- 4 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 docs/img/adaptive_bed_mesh.svg create mode 100644 docs/img/adaptive_bed_mesh_margin.svg diff --git a/docs/Bed_Mesh.md b/docs/Bed_Mesh.md index d2a417dd40ec..9516ef58677f 100644 --- a/docs/Bed_Mesh.md +++ b/docs/Bed_Mesh.md @@ -370,14 +370,54 @@ are identified in green. ![bedmesh_interpolated](img/bedmesh_faulty_regions.svg) +### Adaptive Meshes + +Adaptive bed meshing is a way to speed up the bed mesh generation by only probing +the area of the bed used by the objects being printed. When used, the method will +automatically adjust the mesh parameters based on the area occupied by the defined +print objects. + +The adapted mesh area will be computed from the area defined by the boundaries of all +the defined print object so it covers every object, including any margins defined in +the configuration. After the area is computed, the number of probe points will be +scaled down based on the ratio of the default mesh area and the adapted mesh area. To +illustrate this consider the following example: + +For a 150mmx150mm bed with `mesh_min` set to `25,25` and `mesh_max` set to `125,125`, +the default mesh area is a 100mmx100mm square. An adapted mesh area of `50,50` +means a ratio of `0.5x0.5` between the adapted area and default mesh area. + +If the `bed_mesh` configuration specified `probe_count` as `7x7`, the adapted bed +mesh will use 4x4 probe points (roundUp(7 * 0.5)). + +![adaptive_bedmesh](img/adaptive_bed_mesh.svg) +``` +[bed_mesh] +speed: 120 +horizontal_move_z: 5 +mesh_min: 35, 6 +mesh_max: 240, 198 +probe_count: 5, 3 +mesh_margin: 5 +``` + +- `mesh_margin` \ + _Default Value: 0_ \ + Margin (in mm) to add around the area of the bed used by the defined objects. The diagram + below shows the adapted bed mesh area with a `mesh_margin` set to 5mm. The adapted mesh area + (area in green) is computed as the used bed area (area in blue) plus the defined margin. + + ![adaptive_bedmesh_margin](img/adaptive_bed_mesh_margin.svg) ## Bed Mesh Gcodes ### Calibration `BED_MESH_CALIBRATE PROFILE= METHOD=[manual | automatic] [=] - [=]`\ + [=] [ADAPTIVE=[0|1] [ADAPTIVE_MARGIN=]`\ _Default Profile: default_\ -_Default Method: automatic if a probe is detected, otherwise manual_ +_Default Method: automatic if a probe is detected, otherwise manual_ \ +_Default Adaptive: 0_ \ +_Default Adaptive Margin: 0_ Initiates the probing procedure for Bed Mesh Calibration. @@ -399,6 +439,8 @@ following parameters are available: - `ROUND_PROBE_COUNT` - All beds: - `ALGORITHM` + - `ADAPTIVE` + - `ADAPTIVE_MARGIN` See the configuration documentation above for details on how each parameter applies to the mesh. diff --git a/docs/img/adaptive_bed_mesh.svg b/docs/img/adaptive_bed_mesh.svg new file mode 100644 index 000000000000..954ca0b322c7 --- /dev/null +++ b/docs/img/adaptive_bed_mesh.svg @@ -0,0 +1,4 @@ + + + +
Origin
(0,0)
Origin...

Legend


Legend
Object Polygon
Object Polygon
Used Bed Area
Used Bed Area
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/img/adaptive_bed_mesh_margin.svg b/docs/img/adaptive_bed_mesh_margin.svg new file mode 100644 index 000000000000..6c6216d041aa --- /dev/null +++ b/docs/img/adaptive_bed_mesh_margin.svg @@ -0,0 +1,4 @@ + + + +
Origin
(0,0)
Origin...

Legend


Legend
Object Polygon
Object Polygon
Used Bed Area
Used Bed Area
Adapted Bed Mesh Area
Adapted Bed Mesh Area
(90,75)
(90,75)
(130,140)
(130,140)
(125,135)
(125,1...
(95,120)
(95,12...
(60,90)
(60,90)
(90,130)
(90,13...
(95,80)
(95,80)
(125,110)
(125,1...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/klippy/extras/bed_mesh.py b/klippy/extras/bed_mesh.py index 6c714304fb8a..8f7106ff0554 100644 --- a/klippy/extras/bed_mesh.py +++ b/klippy/extras/bed_mesh.py @@ -2,10 +2,14 @@ # # Copyright (C) 2018 Kevin O'Connor # Copyright (C) 2018-2019 Eric Callahan +# Copyright (C) 2023 Kyle Hansen (Kyleisah) +# Copyright (C) 2023 Brandon Nance +# Copyright (C) 2023 Mitko Haralanov # # This file may be distributed under the terms of the GNU GPLv3 license. import logging, math, json, collections from . import probe +from configparser import Error as config_error PROFILE_VERSION = 1 PROFILE_OPTIONS = { @@ -103,6 +107,7 @@ def __init__(self, config): self.log_fade_complete = False self.base_fade_target = config.getfloat('fade_target', None) self.fade_target = 0. + self.margin = config.getfloat('margin', 0.0) self.gcode = self.printer.lookup_object('gcode') self.splitter = MoveSplitter(config, self.gcode) # setup persistent storage @@ -573,7 +578,8 @@ def _verify_algorithm(self, error): "interpolation. Configured Probe Count: %d, %d" % (self.mesh_config['x_count'], self.mesh_config['y_count'])) params['algo'] = 'lagrange' - def update_config(self, gcmd): + def update_config(self, gcmd, do_adaptive, exclude_object=None, + mesh_margin=0): # reset default configuration self.radius = self.orig_config['radius'] self.origin = self.orig_config['origin'] @@ -584,6 +590,107 @@ def update_config(self, gcmd): params = gcmd.get_command_parameters() need_cfg_update = False + + # If adaptive meshing is enabled and there are defined objects, + # generate adaptive mesh + if do_adaptive and exclude_object is not None and \ + len(exclude_object.objects) != 0: + # List all exclude_object points by axis and iterate over + # all polygon points, and pick the min and max or each axis + list_of_xs = [] + list_of_ys = [] + gcmd.respond_info("Found %s objects" % \ + (len(exclude_object.objects))) + for obj in exclude_object.objects: + for point in obj["polygon"]: + list_of_xs.append(point[0]) + list_of_ys.append(point[1]) + + # Define bounds of adaptive mesh area + mesh_min = [min(list_of_xs), min(list_of_ys)] + mesh_max = [max(list_of_xs), max(list_of_ys)] + adjusted_mesh_min = [x - mesh_margin for x in mesh_min] + adjusted_mesh_max = [x + mesh_margin for x in mesh_max] + + # Force margin to respect original mesh bounds + adjusted_mesh_min[0] = max(adjusted_mesh_min[0], + self.orig_config["mesh_min"][0]) + adjusted_mesh_min[1] = max(adjusted_mesh_min[1], + self.orig_config["mesh_min"][1]) + adjusted_mesh_max[0] = min(adjusted_mesh_max[0], + self.orig_config["mesh_max"][0]) + adjusted_mesh_max[1] = min(adjusted_mesh_max[1], + self.orig_config["mesh_max"][1]) + + adjusted_mesh_size = (adjusted_mesh_max[0] - adjusted_mesh_min[0], + adjusted_mesh_max[1] - adjusted_mesh_min[1]) + + # Compute a ratio between the adapted and original sizes + ratio = (adjusted_mesh_size[0] / + (self.orig_config["mesh_max"][0] - + self.orig_config["mesh_min"][0]), + adjusted_mesh_size[1] / + (self.orig_config["mesh_max"][1] - + self.orig_config["mesh_min"][1])) + + gcmd.respond_info("Original mesh bounds: (%s,%s)" % + (self.orig_config["mesh_min"], + self.orig_config["mesh_max"])) + gcmd.respond_info("Original probe count: (%s,%s)" % + (self.mesh_config["x_count"], + self.mesh_config["y_count"])) + gcmd.respond_info("Adapted mesh bounds: (%s,%s)" % + (adjusted_mesh_min, adjusted_mesh_max)) + gcmd.respond_info("Ratio: (%s, %s)" % ratio) + + new_x_probe_count = int( + math.ceil(self.mesh_config["x_count"] * ratio[0])) + new_y_probe_count = int( + math.ceil(self.mesh_config["y_count"] * ratio[1])) + + # There is one case, where we may have to adjust the probe counts: + # axis0 < 4 and axis1 > 6 (see _verify_algorithm). + min_num_of_probes = 3 + if max(new_x_probe_count, new_y_probe_count) > 6 and \ + min(new_x_probe_count, new_y_probe_count) < 4: + min_num_of_probes = 4 + + new_x_probe_count = max(min_num_of_probes, new_x_probe_count) + new_y_probe_count = max(min_num_of_probes, new_y_probe_count) + + gcmd.respond_info("Adapted probe count: (%s,%s)" % + (new_x_probe_count, new_y_probe_count)) + + # If the adapted mesh size is too small, adjust it to something + # useful. + adjusted_mesh_size = (max(adjusted_mesh_size[0], new_x_probe_count), + max(adjusted_mesh_size[1], new_y_probe_count)) + + if self.radius is not None: + adapted_radius = math.sqrt((adjusted_mesh_size[0] ** 2) + + (adjusted_mesh_size[1] ** 2)) / 2 + adapted_origin = (adjusted_mesh_min[0] + + (adjusted_mesh_size[0] / 2), + adjusted_mesh_min[1] + + (adjusted_mesh_size[1] / 2)) + to_adapted_origin = math.sqrt(adapted_origin[0]**2 + + adapted_origin[1]**2) + # If the adapted mesh size is smaller than the default/full + # mesh, adjust the parameters. Otherwise, just do the full mesh. + if adapted_radius + to_adapted_origin < self.radius: + self.radius = adapted_radius + self.origin = adapted_origin + self.mesh_min = (-self.radius, -self.radius) + self.mesh_max = (self.radius, self.radius) + self.mesh_config["x_count"] = \ + self.mesh_config["y_count"] = \ + max(new_x_probe_count, new_y_probe_count) + else: + self.mesh_min = adjusted_mesh_min + self.mesh_max = adjusted_mesh_max + self.mesh_config["x_count"] = new_x_probe_count + self.mesh_config["y_count"] = new_y_probe_count + need_cfg_update = True if self.radius is not None: if "MESH_RADIUS" in params: self.radius = gcmd.get_float("MESH_RADIUS") @@ -653,8 +760,18 @@ def cmd_BED_MESH_CALIBRATE(self, gcmd): self._profile_name = gcmd.get('PROFILE', "default") if not self._profile_name.strip(): raise gcmd.error("Value for parameter 'PROFILE' must be specified") + exclude_object = None + adaptive_mesh = bool(gcmd.get_int('ADAPTIVE', 0)) + margin = gcmd.get_float('ADAPTIVE_MARGIN', self.bedmesh.margin) + if adaptive_mesh: + try: + exclude_object = self.printer.lookup_object("exclude_object") + except config_error: + gcmd.respond_info("Exclude objects not enabled." + " Using full mesh...") + exclude_object = None self.bedmesh.set_mesh(None) - self.update_config(gcmd) + self.update_config(gcmd, adaptive_mesh, exclude_object, margin) self.probe_helper.start_probe(gcmd) def probe_finalize(self, offsets, positions): x_offset, y_offset, z_offset = offsets