bed_mesh: Implement adaptive bed mesh (#6461)

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 <voidtrance@gmail.com>
Signed-off-by: Kyle Hansen <kyleisah@gmail.com>
Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
This commit is contained in:
voidtrance 2024-01-26 14:50:01 -08:00 committed by GitHub
parent 5e433fff06
commit 5e3daa6f21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 181 additions and 4 deletions

View File

@ -370,14 +370,68 @@ are identified in green.
![bedmesh_interpolated](img/bedmesh_faulty_regions.svg) ![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 objects 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 (7 * 0.5 rounded up).
![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
adaptive_margin: 5
```
- `adaptive_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 an `adaptive_margin` of 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)
By nature, adaptive bed meshes use the objects defined by the Gcode file being printed.
Therefore, it is expected that each Gcode file will generate a mesh that probes a different
area of the print bed. Therefore, adapted bed meshes should not be re-used. The expectation
is that a new mesh will be generated for each print if adaptive meshing is used.
It is also important to consider that adaptive bed meshing is best used on machines that can
normally probe the entire bed and achieve a maximum variance less than or equal to 1 layer
height. Machines with mechanical issues that a full bed mesh normally compensates for may
have undesirable results when attempting print moves **outside** of the probed area. If a
full bed mesh has a variance greater than 1 layer height, caution must be taken when using
adaptive bed meshes and attempting print moves outside of the meshed area.
## Bed Mesh Gcodes ## Bed Mesh Gcodes
### Calibration ### Calibration
`BED_MESH_CALIBRATE PROFILE=<name> METHOD=[manual | automatic] [<probe_parameter>=<value>] `BED_MESH_CALIBRATE PROFILE=<name> METHOD=[manual | automatic] [<probe_parameter>=<value>]
[<mesh_parameter>=<value>]`\ [<mesh_parameter>=<value>] [ADAPTIVE=[0|1] [ADAPTIVE_MARGIN=<value>]`\
_Default Profile: default_\ _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. Initiates the probing procedure for Bed Mesh Calibration.
@ -399,6 +453,8 @@ following parameters are available:
- `ROUND_PROBE_COUNT` - `ROUND_PROBE_COUNT`
- All beds: - All beds:
- `ALGORITHM` - `ALGORITHM`
- `ADAPTIVE`
- `ADAPTIVE_MARGIN`
See the configuration documentation above for details on how each parameter See the configuration documentation above for details on how each parameter
applies to the mesh. applies to the mesh.

View File

@ -991,6 +991,9 @@ Visual Examples:
# Optional points that define a faulty region. See docs/Bed_Mesh.md # Optional points that define a faulty region. See docs/Bed_Mesh.md
# for details on faulty regions. Up to 99 faulty regions may be added. # for details on faulty regions. Up to 99 faulty regions may be added.
# By default no faulty regions are set. # By default no faulty regions are set.
#adaptive_margin:
# An optional margin (in mm) to be added around the bed area used by
# the defined print objects when generating an adaptive mesh.
``` ```
### [bed_tilt] ### [bed_tilt]

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,6 +1,5 @@
# Mesh Bed Leveling # Mesh Bed Leveling
# #
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018-2019 Eric Callahan <arksine.code@gmail.com> # Copyright (C) 2018-2019 Eric Callahan <arksine.code@gmail.com>
# #
# This file may be distributed under the terms of the GNU GPLv3 license. # This file may be distributed under the terms of the GNU GPLv3 license.
@ -291,6 +290,7 @@ class BedMeshCalibrate:
self.orig_config = {'radius': None, 'origin': None} self.orig_config = {'radius': None, 'origin': None}
self.radius = self.origin = None self.radius = self.origin = None
self.mesh_min = self.mesh_max = (0., 0.) self.mesh_min = self.mesh_max = (0., 0.)
self.adaptive_margin = config.getfloat('adaptive_margin', 0.0)
self.zero_ref_pos = config.getfloatlist( self.zero_ref_pos = config.getfloatlist(
"zero_reference_position", None, count=2 "zero_reference_position", None, count=2
) )
@ -573,6 +573,113 @@ class BedMeshCalibrate:
"interpolation. Configured Probe Count: %d, %d" % "interpolation. Configured Probe Count: %d, %d" %
(self.mesh_config['x_count'], self.mesh_config['y_count'])) (self.mesh_config['x_count'], self.mesh_config['y_count']))
params['algo'] = 'lagrange' params['algo'] = 'lagrange'
def set_adaptive_mesh(self, gcmd):
if not gcmd.get_int('ADAPTIVE', 0):
return False
exclude_objects = self.printer.lookup_object("exclude_object", None)
if exclude_objects is None:
gcmd.respond_info("Exclude objects not enabled. Using full mesh...")
return False
objects = exclude_objects.get_status().get("objects", [])
if not objects:
return False
margin = gcmd.get_float('ADAPTIVE_MARGIN', self.adaptive_margin)
# 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(objects)))
for obj in 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 - margin for x in mesh_min]
adjusted_mesh_max = [x + 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
self._profile_name = None
return True
def update_config(self, gcmd): def update_config(self, gcmd):
# reset default configuration # reset default configuration
self.radius = self.orig_config['radius'] self.radius = self.orig_config['radius']
@ -616,6 +723,8 @@ class BedMeshCalibrate:
self.mesh_config['algo'] = gcmd.get('ALGORITHM').strip().lower() self.mesh_config['algo'] = gcmd.get('ALGORITHM').strip().lower()
need_cfg_update = True need_cfg_update = True
need_cfg_update |= self.set_adaptive_mesh(gcmd)
if need_cfg_update: if need_cfg_update:
self._verify_algorithm(gcmd.error) self._verify_algorithm(gcmd.error)
self._generate_points(gcmd.error) self._generate_points(gcmd.error)
@ -781,7 +890,8 @@ class BedMeshCalibrate:
z_mesh.set_zero_reference(*self.zero_ref_pos) z_mesh.set_zero_reference(*self.zero_ref_pos)
self.bedmesh.set_mesh(z_mesh) self.bedmesh.set_mesh(z_mesh)
self.gcode.respond_info("Mesh Bed Leveling Complete") self.gcode.respond_info("Mesh Bed Leveling Complete")
self.bedmesh.save_profile(self._profile_name) if self._profile_name is not None:
self.bedmesh.save_profile(self._profile_name)
def _dump_points(self, probed_pts, corrected_pts, offsets): def _dump_points(self, probed_pts, corrected_pts, offsets):
# logs generated points with offset applied, points received # logs generated points with offset applied, points received
# from the finalize callback, and the list of corrected points # from the finalize callback, and the list of corrected points