diff --git a/glue_jupyter/bqplot/image/layer_artist.py b/glue_jupyter/bqplot/image/layer_artist.py
index dffff1a4..7a546872 100644
--- a/glue_jupyter/bqplot/image/layer_artist.py
+++ b/glue_jupyter/bqplot/image/layer_artist.py
@@ -4,6 +4,7 @@
 from glue.viewers.image.layer_artist import BaseImageLayerArtist, ImageLayerArtist, ImageSubsetArray
 from glue.viewers.image.state import ImageSubsetLayerState
 from glue.core.fixed_resolution_buffer import ARRAY_CACHE, PIXEL_CACHE
+from glue.core.units import UnitConverter
 from ...link import link
 
 from bqplot_image_gl import Contour
@@ -88,6 +89,17 @@ def _update_contour_lines(self):
             self.contour_artist.contour_lines = []
             return
 
+        # As the levels may be specified in a different unit we should convert
+        # the data to match the units of the levels (we do it this way around
+        # so that the labels are shown in the new units)
+
+        converter = UnitConverter()
+
+        contour_data = converter.to_unit(self.state.layer,
+                                         self.state.attribute,
+                                         contour_data,
+                                         self.state.attribute_display_unit)
+
         for level in self.state.levels:
             if level not in self._contour_line_cache:
                 contour_line_set = skimage.measure.find_contours(contour_data.T, level)
diff --git a/glue_jupyter/bqplot/image/state.py b/glue_jupyter/bqplot/image/state.py
index f65f4950..f045f4ed 100644
--- a/glue_jupyter/bqplot/image/state.py
+++ b/glue_jupyter/bqplot/image/state.py
@@ -1,11 +1,12 @@
 import numpy as np
 
-from echo import CallbackProperty
+from echo import CallbackProperty, delay_callback
 from glue.viewers.matplotlib.state import (DeferredDrawCallbackProperty as DDCProperty,
                                            DeferredDrawSelectionCallbackProperty as DDSCProperty)
 
 from glue.viewers.image.state import ImageViewerState, ImageLayerState
 from glue.core.state_objects import StateAttributeLimitsHelper
+from glue.core.units import UnitConverter
 
 
 class BqplotImageViewerState(ImageViewerState):
@@ -31,6 +32,7 @@ class BqplotImageLayerState(ImageLayerState):
     contour_visible = CallbackProperty(False, 'whether to show the image as contours')
 
     def __init__(self, *args, **kwargs):
+
         super(BqplotImageLayerState, self).__init__(*args, **kwargs)
 
         BqplotImageLayerState.level_mode.set_choices(self, ['Linear', 'Custom'])
@@ -53,6 +55,8 @@ def __init__(self, *args, **kwargs):
         self.add_callback('c_max', self._update_levels)
         self.add_callback('level_mode', self._update_levels)
         self.add_callback('levels', self._update_labels)
+        self.add_callback('attribute_display_unit', self._convert_units_c_limits, echo_old=True)
+
         self._update_levels()
 
     def _update_priority(self, name):
@@ -66,9 +70,37 @@ def _update_priority(self, name):
 
     def _update_levels(self, ignore=None):
         if self.level_mode == "Linear":
-            # TODO: this is exclusive begin/end point, is that a good choise?
-            self.levels = np.linspace(self.c_min, self.c_max, self.n_levels+2)[1:-1].tolist()
+            self.levels = np.linspace(self.c_min, self.c_max, self.n_levels).tolist()
 
     def _update_labels(self, ignore=None):
         # TODO: we may want to have ways to configure this in the future
         self.labels = ["{0:.4g}".format(level) for level in self.levels]
+
+    def _convert_units_c_limits(self, old_unit, new_unit):
+
+        if (
+            getattr(self, '_previous_attribute', None) is self.attribute and
+            old_unit != new_unit and
+            self.layer is not None
+        ):
+
+            limits = np.hstack([self.c_min, self.c_max, self.levels])
+
+            converter = UnitConverter()
+
+            limits_native = converter.to_native(self.layer,
+                                                self.attribute, limits,
+                                                old_unit)
+
+            limits_new = converter.to_unit(self.layer,
+                                           self.attribute, limits_native,
+                                           new_unit)
+
+            with delay_callback(self, 'c_min', 'c_max', 'levels'):
+                self.c_min, self.c_max = sorted(limits_new[:2])
+                self.levels = tuple(limits_new[2:])
+
+        # Make sure that we keep track of what attribute the limits
+        # are for - if the attribute changes, we should not try and
+        # update the limits.
+        self._previous_attribute = self.attribute
diff --git a/glue_jupyter/bqplot/image/tests/test_viewer.py b/glue_jupyter/bqplot/image/tests/test_viewer.py
index 7365f06e..fb083a75 100644
--- a/glue_jupyter/bqplot/image/tests/test_viewer.py
+++ b/glue_jupyter/bqplot/image/tests/test_viewer.py
@@ -40,7 +40,7 @@ def test_contour_levels(app, data_image, data_volume):
     layer.state.c_min = 0
     layer.state.c_max = 10
     layer.state.n_levels = 3
-    assert layer.state.levels == [2.5, 5, 7.5]
+    assert layer.state.levels == [0, 5, 10]
     # since we start invisible, we don't compute the contour lines
     assert len(layer.contour_artist.contour_lines) == 0
     # make the visible, so we trigger a compute
@@ -48,9 +48,9 @@ def test_contour_levels(app, data_image, data_volume):
     assert len(layer.contour_artist.contour_lines) == 3
     layer.state.level_mode = 'Custom'
     layer.state.n_levels = 1
-    assert layer.state.levels == [2.5, 5, 7.5]
+    assert layer.state.levels == [0, 5, 10]
     layer.state.level_mode = 'Linear'
-    assert layer.state.levels == [5]
+    assert layer.state.levels == [0]
     assert len(layer.contour_artist.contour_lines) == 1
 
     # test the visual attributes
@@ -81,7 +81,7 @@ def test_contour_state(app, data_image):
         {'level_mode': 'Linear', 'levels': [2, 3]}
     )
     # Without priority of levels, this gets set to [2, 3]
-    assert layer.state.levels == [2.5, 5, 7.5]
+    assert layer.state.levels == [0, 5, 10]
 
 
 def test_add_markers_zoom(app, data_image, data_volume, dataxyz):
diff --git a/glue_jupyter/bqplot/image/tests/test_visual.py b/glue_jupyter/bqplot/image/tests/test_visual.py
new file mode 100644
index 00000000..3ef57110
--- /dev/null
+++ b/glue_jupyter/bqplot/image/tests/test_visual.py
@@ -0,0 +1,39 @@
+import numpy as np
+from numpy.testing import assert_allclose
+
+from glue_jupyter import jglue
+from glue_jupyter.tests.helpers import visual_widget_test
+
+
+@visual_widget_test
+def test_contour_units(
+    tmp_path,
+    page_session,
+    solara_test,
+):
+
+    x = np.linspace(-7, 7, 88)
+    y = np.linspace(-6, 6, 69)
+    X, Y = np.meshgrid(x, y)
+    Z = np.exp(-(X * X + Y * Y) / 4)
+
+    app = jglue()
+    data = app.add_data(data={"x": X, "y": Y, "z": Z})[0]
+    data.get_component("z").units = 'km'
+    image = app.imshow(show=False)
+    image.state.layers[0].attribute = data.id['z']
+    image.state.layers[0].contour_visible = True
+    image.state.layers[0].c_min = 0.1
+    image.state.layers[0].c_max = 0.9
+    image.state.layers[0].n_levels = 5
+
+    assert_allclose(image.state.layers[0].levels, [0.1, 0.3, 0.5, 0.7, 0.9])
+
+    image.state.layers[0].attribute_display_unit = 'm'
+
+    assert_allclose(image.state.layers[0].levels, [100, 300, 500, 700, 900])
+    assert image.state.layers[0].labels == ['100', '300', '500', '700', '900']
+
+    figure = image.figure_widget
+    figure.layout = {"width": "400px", "height": "250px"}
+    return figure
diff --git a/glue_jupyter/tests/images/py311-test-visual.json b/glue_jupyter/tests/images/py311-test-visual.json
index ffafa5e4..9e0891fa 100644
--- a/glue_jupyter/tests/images/py311-test-visual.json
+++ b/glue_jupyter/tests/images/py311-test-visual.json
@@ -1,4 +1,5 @@
 {
+  "glue_jupyter.bqplot.image.tests.test_visual.test_contour_units[chromium]": "fa4f68c5c62e1437c1666c656ba02376396f6c75b6f7956f712c760569a2045b",
   "glue_jupyter.bqplot.scatter.tests.test_visual.test_visual_scatter2d[chromium]": "fbdd9fe2649a0d72813c03e77af6233909df64207cb834f28da479f50b9e7a1d",
   "glue_jupyter.bqplot.scatter.tests.test_visual.test_visual_scatter2d_density[chromium]": "d843a816a91e37cb0212c7caae913d7563f6c2eb42b49fa18345a5952e093b2f"
 }
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 74f4df05..8ec43920 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -14,7 +14,7 @@ python_requires = >=3.8
 setup_requires =
   setuptools_scm
 install_requires =
-    glue-core>=1.17.1
+    glue-core>=1.20.0
     glue-vispy-viewers>=1.0
     notebook>=4.0
     ipympl>=0.3.0
@@ -33,7 +33,6 @@ test =
     pytest
     pytest-cov
     nbconvert>=6.4.5
-    glue-core!=1.2.4; python_version == '3.10'
 visualtest =
     playwright
     pytest-playwright