Skin Lesion Segmentation with OpenCV
Segment skin lesions from dermoscopy images using HSV colour thresholding.
Introduction
30 Second Summary
Dermatologists identify suspicious skin lesions by looking at colour differences between the lesion and surrounding skin. Computers can do the same thing using colour-based image processing.
In this project, you will build a Python script that segments a skin lesion from a real dermoscopy image using OpenCV's colour-space processing and automatic thresholding.
What You'll Build
A complete colour-based segmentation pipeline that downloads a dermoscopy image from the ISIC Archive, converts it to HSV colour space, automatically segments the dark lesion region, cleans the mask with morphological operations, and draws a green boundary around the detected lesion.
By the end of this project, you'll have:
- A working Python script that segments a skin lesion from a dermoscopy image using HSV colour-space thresholding.
- A multi-panel visualization showing the original image, binary mask, and detected lesion boundary saved as PNG files.
- A practical understanding of why HSV Saturation beats grayscale for medical image segmentation.
- Secret Mission: Add a semi-transparent colour overlay highlighting the lesion area and calculate its size as a percentage of the image.
Are there any prerequisites?
None. This guide walks you through installing Python, VS Code, and all required libraries from scratch.
Set Up Your Environment
Dermatologists examine skin lesions every day, but spotting the exact boundary of a lesion against surrounding skin is tricky even for trained eyes. Computer vision can help by automatically isolating the lesion region from the rest of the image.
In this project, you will build a Python script that segments a skin lesion from a real dermoscopy image. The key insight is that converting images to the HSV colour space reveals differences between lesion and skin that are invisible in plain grayscale. Specifically, the Saturation channel makes pigmented lesions glow bright while healthy skin stays dark.
In this step, get ready to:
- Install Python 3.13 and verify it runs from the command line.
- Install VS Code with the Python extension for editing your script.
- Create a project folder and install the OpenCV, NumPy, and matplotlib libraries.
Download and install Python 3.13
Python is the programming language you will use to write the segmentation script. You need version 3.13 because it is compatible with the latest OpenCV library.
- Open your browser and go to https://www.python.org/downloads/.
- Click the download button for Python 3.13 (the latest 3.13 release).
- Run the installer. On the very first screen, check the box labelled Add python.exe to PATH before clicking anything else.
- Click Install Now and wait for the installation to finish.
Why check Add python.exe to PATH?
Checking this box tells Windows where to find the python command. Without it, your terminal would not recognize python and you would need to type the full installation path every time.
- Open a new Command Prompt by pressing the Windows key, typing cmd, and pressing Enter.
- Verify Python is installed by running this command:
python --version
✔️ I see Python 3.13 or higher
Python is ready to go. Move on to the next substep.
ⓧ I see an older version
Your system has an older version of Python installed. You need to install 3.13 alongside it or replace it.
- Go back to https://www.python.org/downloads/ and download the latest Python 3.13 installer.
- Run the installer, check Add python.exe to PATH, and click Install Now.
- Close your current Command Prompt and open a new one.
- Run python --version again to confirm it now shows 3.13.
Still showing the old version?
The old Python directory may appear earlier in your PATH. Open Edit environment variables for your account from the Start menu, find the Path variable, and move the Python 3.13 entry above any older Python entries.
ⓧ Command not found
Python is not installed on your machine yet.
- Go to https://www.python.org/downloads/ and download the latest Python 3.13 installer for Windows.
- Run the installer, check Add python.exe to PATH, and click Install Now.
- Close your current Command Prompt and open a new one.
- Run python --version to confirm it prints Python 3.13 followed by a patch number.
Install VS Code and the Python extension
VS Code is a free code editor that gives you syntax highlighting, an integrated terminal, and extension support for Python development.
- Open your browser and go to https://code.visualstudio.com/.
- Click the download button for Windows and run the installer.
- Accept the default settings and complete the installation.
- Open VS Code by pressing the Windows key, typing VS Code, and pressing Enter.
- Click the Extensions icon in the left sidebar (it looks like four small squares).
- Type Python in the search bar and install the extension published by Microsoft.
What does the Python extension do?
The Python extension adds code completion, error highlighting, and the ability to run Python files directly from the editor. It also connects VS Code to your installed Python interpreter so you can execute scripts with one click.
Create your project folder and install libraries
You need three libraries for this project: OpenCV for image processing, NumPy for array operations, and matplotlib for creating multi-panel visualizations.
- Move to your Desktop by running this command in your Command Prompt:
cd %USERPROFILE%\Desktop
- Create your project folder and move into it by running these commands:
mkdir skin-lesion-segmentation
cd skin-lesion-segmentation
- Confirm the folder exists by running:
dir
You should see an empty directory listing for skin-lesion-segmentation.
- Open this folder in VS Code by running:
code .
VS Code did not open?
If the code command is not recognized, open VS Code manually, click File in the top menu bar, click Open Folder, and select the skin-lesion-segmentation folder on your Desktop.
- Open the VS Code integrated terminal by pressing Ctrl+` (the backtick key, above Tab).
- Install the three libraries by running this command:
pip install opencv-python numpy matplotlib
What are these libraries?
- opencv-python provides image loading, colour conversion, thresholding, morphological operations, and contour detection.
- numpy handles the pixel arrays that OpenCV operates on.
- matplotlib creates the multi-panel comparison figures you will save as PNG files.
- Verify the installation by running:
python -c "import cv2; print(cv2.__version__)"
You should see a version number printed (such as 4.13.0.92) with no errors.
Seeing an error on import?
- If you see No module named cv2, the pip that ran may be tied to a different Python version. Try running python -m pip install opencv-python numpy matplotlib instead to make sure packages install for the same Python you are using.
- If you see a permissions error, try adding --user to the end of the pip install command.
help me fix my OpenCV import error on Windows
Your environment is ready. Next up, you will download a real skin lesion image and explore how the HSV colour space reveals hidden structure in the image.
Download the Image and Explore HSV Channels
Your development environment is ready with Python, VS Code, and all three libraries installed. Now it's time to work with a real medical image.
In this step, you'll download a dermoscopy image from the ISIC Archive (a public skin lesion research dataset) and explore how the HSV colour space breaks the image into three separate channels. This will reveal why the Saturation channel is the key to isolating the lesion from surrounding skin.
In this step, get ready to:
- Download a dermoscopy image from the ISIC Archive and load it with OpenCV.
- Convert the image to HSV and visualize the Hue, Saturation, and Value channels.
Create the script and download the image
The ISIC Archive hosts over 500,000 public skin lesion images used by researchers worldwide. You'll download one specific dermoscopy image directly from their S3 bucket using Python's built-in urllib library.
- In VS Code's Explorer sidebar on the left, hover over your project folder name.
- Click the New File icon that appears (a page with a plus sign).
- Type segment_lesion.py and press Enter to create the file.
- Add the following code to segment_lesion.py to import your libraries, download the image, and load it into memory:
import urllib.request
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Download a dermoscopy image from the ISIC Archive
url = "https://isic-archive.s3.amazonaws.com/images/ISIC_0024306.jpg"
filename = "ISIC_0024306.jpg"
print(f"Downloading {filename} from ISIC Archive...")
urllib.request.urlretrieve(url, filename)
print("Download complete!")
# Load the image
image = cv2.imread(filename)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
What does this code do?
- urllib.request.urlretrieve downloads the image file directly from the ISIC Archive's public S3 bucket. No account or authentication is needed.
- cv2.imread loads the downloaded JPEG into a NumPy array. OpenCV loads images in BGR format (Blue, Green, Red) by default.
- cv2.cvtColor(image, cv2.COLOR_BGR2RGB) converts from BGR to RGB so that matplotlib displays the colours correctly.
- Save segment_lesion.py by pressing Ctrl+S (Windows) or Cmd+S (macOS).
- Run the script in your VS Code terminal by entering:
python segment_lesion.py
- You should see Downloading ISIC_0024306.jpg from ISIC Archive... followed by Download complete! in the terminal output.
Seeing a connection error?
- Make sure you have an active internet connection. The script downloads the image from a remote server.
- If you see a certificate error, check that your Python installation is up to date. Python 3.13 includes updated root certificates.
- If nothing appears to happen, the download may be slow on your connection. Wait a moment for the 600x450 pixel image to finish downloading.
Help me debug a connection error when downloading from the ISIC Archive
Convert to HSV and visualize the channels
Colour images are typically stored as RGB (Red, Green, Blue), but this format mixes brightness and colour information together. The HSV colour space separates an image into Hue (the colour itself), Saturation (how vivid the colour is), and Value (how bright the pixel is).
Why HSV instead of grayscale?
Grayscale thresholding is degraded by illumination artifacts that are common in dermoscopy images. The Saturation channel is less affected by these lighting variations.
Skin lesions tend to be more saturated (more vivid colour) than the surrounding skin, making Saturation the ideal channel for automatic segmentation. Research on 300 ISIC images achieved a median Jaccard index of 0.84 using this approach.
- Add the following code below your existing code in segment_lesion.py to convert the image to HSV, split it into channels, and create a 4-panel visualization:
# Convert to HSV colour space
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Split into individual channels
h, s, v = cv2.split(hsv)
# Visualize the HSV channels
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
axes[0].imshow(image_rgb)
axes[0].set_title("Original")
axes[0].axis("off")
axes[1].imshow(h, cmap="gray")
axes[1].set_title("Hue (H)")
axes[1].axis("off")
axes[2].imshow(s, cmap="gray")
axes[2].set_title("Saturation (S)")
axes[2].axis("off")
axes[3].imshow(v, cmap="gray")
axes[3].set_title("Value (V)")
axes[3].axis("off")
plt.suptitle("HSV Colour Space Channels", fontsize=14)
plt.tight_layout()
plt.savefig("hsv_channels.png", dpi=150, bbox_inches="tight")
plt.show()
What does this code do?
- cv2.cvtColor(image, cv2.COLOR_BGR2HSV) converts the image from BGR colour space to HSV. OpenCV uses a Hue range of 0-179, Saturation of 0-255, and Value of 0-255.
- cv2.split(hsv) separates the 3-channel HSV image into three individual grayscale arrays: one for each channel.
- plt.subplots(1, 4) creates a figure with 4 panels side by side. Each channel is displayed in grayscale so you can see intensity differences.
- plt.savefig("hsv_channels.png") saves the figure as a PNG file in your project folder before plt.show() displays it on screen.
- Save segment_lesion.py and run it again in the terminal:
python segment_lesion.py
- A matplotlib window appears showing four panels side by side. In the Saturation panel, the lesion appears as a bright (white) region against the darker surrounding skin.
- Close the matplotlib window. Confirm that hsv_channels.png has been saved to your project folder by running:
ls
- You should see ISIC_0024306.jpg, hsv_channels.png, and segment_lesion.py listed in the output.
Not seeing the matplotlib window?
- If the script hangs without displaying a window, matplotlib may not have a GUI backend configured. Close the terminal, reopen it, and try running the script again.
- Check that you typed plt.show() as the very last line. If plt.savefig comes after plt.show(), the saved file will be blank.
- Even if the window doesn't appear, the hsv_channels.png file is still saved. Open it from your project folder to verify.
Help me troubleshoot matplotlib not displaying the figure window
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here's the complete segment_lesion.py so far:
import urllib.request
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Download a dermoscopy image from the ISIC Archive
url = "https://isic-archive.s3.amazonaws.com/images/ISIC_0024306.jpg"
filename = "ISIC_0024306.jpg"
print(f"Downloading {filename} from ISIC Archive...")
urllib.request.urlretrieve(url, filename)
print("Download complete!")
# Load the image
image = cv2.imread(filename)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Convert to HSV colour space
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Split into individual channels
h, s, v = cv2.split(hsv)
# Visualize the HSV channels
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
axes[0].imshow(image_rgb)
axes[0].set_title("Original")
axes[0].axis("off")
axes[1].imshow(h, cmap="gray")
axes[1].set_title("Hue (H)")
axes[1].axis("off")
axes[2].imshow(s, cmap="gray")
axes[2].set_title("Saturation (S)")
axes[2].axis("off")
axes[3].imshow(v, cmap="gray")
axes[3].set_title("Value (V)")
axes[3].axis("off")
plt.suptitle("HSV Colour Space Channels", fontsize=14)
plt.tight_layout()
plt.savefig("hsv_channels.png", dpi=150, bbox_inches="tight")
plt.show()
You can now see exactly why the Saturation channel is the best candidate for automatic segmentation. Next up, you'll use Otsu's thresholding to turn that Saturation image into a clean binary mask that separates lesion from skin.
Threshold and Clean the Mask
In the previous step, you explored the HSV channels and saw that the Saturation channel clearly highlights the lesion as a bright region against darker surrounding skin.
Now you need to convert that visual difference into a binary mask that a computer can work with. You'll use Otsu's thresholding to automatically find the best cutoff value, then clean the result with morphological operations to remove noise.
In this step, get ready to:
- Apply Otsu's automatic thresholding to create a binary mask from the Saturation channel.
- Clean the mask with morphological closing and opening operations.
- Visualize the thresholding pipeline and save it as a PNG.
Apply Otsu's threshold to the Saturation channel
Otsu's method automatically calculates the optimal threshold value by analysing the pixel intensity distribution. It finds the cutoff that best separates the two main groups of pixels (lesion vs. skin) without you needing to guess a number.
- In segment_lesion.py, scroll to the bottom of the file and find the line that says plt.show().
- Add the following code below it to apply Otsu's threshold:
# Apply Otsu's threshold on the Saturation channel
thresh_val, binary_mask = cv2.threshold(s, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"Otsu automatically selected threshold: {thresh_val}")
What does this code do?
cv2.threshold(s, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) takes the Saturation channel and automatically picks the best threshold value. Pixels above the threshold become white (255) and pixels below become black (0).
The function returns two values: thresh_val (the number Otsu picked) and binary_mask (the resulting black-and-white image). The 0 in the call is ignored because Otsu calculates its own value.
- Save segment_lesion.py.
- Run the script in your terminal by running:
python segment_lesion.py
- Close the HSV channels figure when it appears. In the terminal, you should see a line like Otsu automatically selected threshold: followed by a number.
Not seeing the threshold print?
- Make sure you close the matplotlib figure window that pops up. The script pauses at each plt.show() call until you close it.
- Check that your new code is below the first plt.show() line, not inside the HSV visualization block.
Why isn't my Otsu threshold printing after closing the figure?
Clean the binary mask with morphological operations
The raw threshold result has imperfections. Small holes appear inside the lesion, and specks of noise appear outside it. Morphological operations reshape the mask by applying a small kernel (like a stamp) across the image.
- In segment_lesion.py, add the following code below the print line you just added:
# Morphological operations to clean the mask
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
mask_closed = cv2.morphologyEx(binary_mask, cv2.MORPH_CLOSE, kernel, iterations=2)
mask_clean = cv2.morphologyEx(mask_closed, cv2.MORPH_OPEN, kernel, iterations=2)
What does this code do?
- cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) creates a 7x7 elliptical kernel. An ellipse matches the smooth, curved shape of skin lesion boundaries better than a square.
- MORPH_CLOSE (closing) fills small holes inside the lesion region. It runs first so that gaps get connected before anything is removed.
- MORPH_OPEN (opening) removes small noise blobs outside the lesion. Running 2 iterations of each makes the effect more thorough.
- Save segment_lesion.py.
You won't see a visible change yet. The next substep visualizes the cleaned mask so you can confirm it worked.
Why close before open?
The order matters. If you opened first, you might remove small fragments of the lesion before they get connected. Closing first fills internal gaps so the lesion becomes a solid region. Then opening safely removes only the isolated noise specks that remain outside.
Visualize the thresholding pipeline
- Add the following code below the morphological operations in segment_lesion.py to create a 3-panel figure and save it:
# Visualize the thresholding pipeline
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(s, cmap="gray")
axes[0].set_title("Saturation Channel")
axes[0].axis("off")
axes[1].imshow(binary_mask, cmap="gray")
axes[1].set_title(f"Otsu Threshold (val={int(thresh_val)})")
axes[1].axis("off")
axes[2].imshow(mask_clean, cmap="gray")
axes[2].set_title("After Morphological Cleanup")
axes[2].axis("off")
plt.suptitle("Thresholding Pipeline", fontsize=14)
plt.tight_layout()
plt.savefig("threshold_steps.png", dpi=150, bbox_inches="tight")
plt.show()
What does this code do?
This creates a side-by-side comparison of the three stages: the raw Saturation channel, the noisy binary mask from Otsu's threshold, and the cleaned mask after morphological operations. The title of the middle panel includes the threshold value Otsu selected.
plt.savefig("threshold_steps.png") writes the figure to disk so you have a permanent record of the pipeline.
- Save segment_lesion.py and run the script:
python segment_lesion.py
- Close the HSV channels figure when it appears. A second figure showing the thresholding pipeline should appear with three panels: Saturation Channel, Otsu Threshold, and After Morphological Cleanup.
- Close the second figure. Confirm that threshold_steps.png now exists in your project folder by running:
ls
- You should see threshold_steps.png listed alongside hsv_channels.png, ISIC_0024306.jpg, and segment_lesion.py.
Cleaned mask still looks noisy?
- Check that you apply MORPH_CLOSE before MORPH_OPEN. Reversing the order removes small regions before filling gaps, leaving more noise.
- Make sure mask_clean uses mask_closed as input (not binary_mask). The operations chain together.
- Verify the kernel size is (7, 7) and iterations is 2 for both operations.
Why does my cleaned mask still have noise after morphological operations?
✔️ Awesome, I've got everything!
Great. Double check you've saved your file.
ⓧ I'd like to double check the full code
Here is the full segment_lesion.py up to this point:
import urllib.request
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Download a dermoscopy image from the ISIC Archive
url = "https://isic-archive.s3.amazonaws.com/images/ISIC_0024306.jpg"
filename = "ISIC_0024306.jpg"
print(f"Downloading {filename} from ISIC Archive...")
urllib.request.urlretrieve(url, filename)
print("Download complete!")
# Load the image
image = cv2.imread(filename)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Convert to HSV colour space
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Split into individual channels
h, s, v = cv2.split(hsv)
# Visualize the HSV channels
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
axes[0].imshow(image_rgb)
axes[0].set_title("Original")
axes[0].axis("off")
axes[1].imshow(h, cmap="gray")
axes[1].set_title("Hue (H)")
axes[1].axis("off")
axes[2].imshow(s, cmap="gray")
axes[2].set_title("Saturation (S)")
axes[2].axis("off")
axes[3].imshow(v, cmap="gray")
axes[3].set_title("Value (V)")
axes[3].axis("off")
plt.suptitle("HSV Colour Space Channels", fontsize=14)
plt.tight_layout()
plt.savefig("hsv_channels.png", dpi=150, bbox_inches="tight")
plt.show()
# Apply Otsu's threshold on the Saturation channel
thresh_val, binary_mask = cv2.threshold(s, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"Otsu automatically selected threshold: {thresh_val}")
# Morphological operations to clean the mask
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
mask_closed = cv2.morphologyEx(binary_mask, cv2.MORPH_CLOSE, kernel, iterations=2)
mask_clean = cv2.morphologyEx(mask_closed, cv2.MORPH_OPEN, kernel, iterations=2)
# Visualize the thresholding pipeline
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(s, cmap="gray")
axes[0].set_title("Saturation Channel")
axes[0].axis("off")
axes[1].imshow(binary_mask, cmap="gray")
axes[1].set_title(f"Otsu Threshold (val={int(thresh_val)})")
axes[1].axis("off")
axes[2].imshow(mask_clean, cmap="gray")
axes[2].set_title("After Morphological Cleanup")
axes[2].axis("off")
plt.suptitle("Thresholding Pipeline", fontsize=14)
plt.tight_layout()
plt.savefig("threshold_steps.png", dpi=150, bbox_inches="tight")
plt.show()
Your binary mask is clean and ready. Next up, you'll detect the lesion boundary as a contour and draw it directly on the original dermoscopy image.
Detect the Lesion Boundary and Visualize Results
Your cleaned binary mask now has a solid white region where the lesion is and black everywhere else. The next challenge is turning that white blob into a precise boundary line drawn on the original image.
OpenCV's contour detection traces the edges of white regions in a binary mask. In this step, you'll use it to find the lesion boundary and produce a final side-by-side comparison showing the original image, the mask, and the detected boundary.
In this step, get ready to:
- Find contours in the cleaned mask and select the largest one as the lesion.
- Draw a green boundary on the original image and save the final comparison figure.
Find contours in the cleaned mask
A contour is a curve joining all continuous points along a boundary that share the same colour or intensity. On a binary mask, contours trace the edges between white and black regions.
- In segment_lesion.py, scroll to the bottom of the file (after the thresholding pipeline visualization code) and add the following contour detection code:
# Find contours in the clean mask
contours, _ = cv2.findContours(mask_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f"Found {len(contours)} contour(s)")
# Select the largest contour (the lesion)
largest_contour = max(contours, key=cv2.contourArea)
What does this code do?
cv2.findContours scans the binary mask and returns a list of all contours it finds. cv2.RETR_EXTERNAL tells it to only retrieve the outermost boundaries, ignoring any holes inside a region.
cv2.CHAIN_APPROX_SIMPLE compresses the contour by removing redundant points along straight lines, saving memory. The max(..., key=cv2.contourArea) call picks the contour with the largest area, which is the lesion in a dermoscopy image where the lesion dominates the foreground.
- Save segment_lesion.py.
- Run the script in the VS Code terminal by running:
python segment_lesion.py
- Close any matplotlib windows that pop up (from the earlier HSV and thresholding visualizations). After the figures close, you should see Found 1 contour(s) (or a small number) printed in the terminal.
Seeing zero contours or an error?
If you see Found 0 contour(s), your mask_clean may be entirely black. Check that your morphological operations code from the previous step matches exactly. The kernel size should be (7, 7) with iterations=2.
Help me debug why cv2.findContours is returning zero contours on my mask_clean variable.
Draw the boundary and save the final figure
With the lesion contour identified, you can draw it directly onto the original image. This produces the visual proof that your segmentation pipeline works.
- In segment_lesion.py, add the following code below the largest_contour line:
# Draw the lesion boundary on the original image
result = image_rgb.copy()
cv2.drawContours(result, [largest_contour], -1, (0, 255, 0), 2)
# Final side-by-side visualization
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
axes[0].imshow(image_rgb)
axes[0].set_title("Original Dermoscopy Image")
axes[0].axis("off")
axes[1].imshow(mask_clean, cmap="gray")
axes[1].set_title("Segmentation Mask")
axes[1].axis("off")
axes[2].imshow(result)
axes[2].set_title("Detected Lesion Boundary")
axes[2].axis("off")
plt.suptitle("Skin Lesion Colour Segmentation Result", fontsize=14)
plt.tight_layout()
plt.savefig("lesion_segmentation_result.png", dpi=150, bbox_inches="tight")
plt.show()
print("Segmentation complete! Results saved as PNG files.")
What does this code do?
image_rgb.copy() creates a duplicate of the original image so you can draw on it without modifying the original. cv2.drawContours draws the contour with a green colour (0, 255, 0) and a line thickness of 2 pixels.
The 3-panel figure places the original, the binary mask, and the boundary result side by side. This lets you visually confirm that the green outline matches the lesion shape. The figure is saved as lesion_segmentation_result.png at 150 dpi.
- Save segment_lesion.py and run it again:
python segment_lesion.py
- Close the HSV and thresholding figures as they appear. The third figure shows three panels: the original dermoscopy image, the white-on-black segmentation mask, and the original image with a green outline tracing the lesion boundary.
- After closing all figures, confirm that lesion_segmentation_result.png appears in your project folder by running:
ls
- You should see lesion_segmentation_result.png listed alongside hsv_channels.png, threshold_steps.png, and your other project files.
Green outline doesn't match the lesion?
If the green line appears to outline a random region instead of the dark lesion, your thresholding from the previous step may be inverted. Double check that your threshold_steps.png shows the lesion area as white (not black) in the cleaned mask panel.
If you see multiple outlines, make sure you're using cv2.RETR_EXTERNAL (not cv2.RETR_TREE) and selecting only the largest_contour to draw.
Help me fix my lesion boundary detection so the green outline matches the lesion shape.
Why select only the largest contour?
In dermoscopy images, the lesion is typically the largest foreground object. Even after morphological cleanup, a few small noise blobs might remain. Selecting the contour with the greatest area filters these out automatically.
✔️ Awesome, I've got everything!
Great. Double check you've saved segment_lesion.py and confirmed that lesion_segmentation_result.png exists in your project folder.
ⓧ I'd like to double check the full code
Here is the complete segment_lesion.py file as it should look at the end of this step:
import urllib.request
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Download a dermoscopy image from the ISIC Archive
url = "https://isic-archive.s3.amazonaws.com/images/ISIC_0024306.jpg"
filename = "ISIC_0024306.jpg"
print(f"Downloading {filename} from ISIC Archive...")
urllib.request.urlretrieve(url, filename)
print("Download complete!")
# Load the image
image = cv2.imread(filename)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Convert to HSV colour space
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Split into individual channels
h, s, v = cv2.split(hsv)
# Visualize the HSV channels
fig, axes = plt.subplots(1, 4, figsize=(16, 4))
axes[0].imshow(image_rgb)
axes[0].set_title("Original")
axes[0].axis("off")
axes[1].imshow(h, cmap="gray")
axes[1].set_title("Hue (H)")
axes[1].axis("off")
axes[2].imshow(s, cmap="gray")
axes[2].set_title("Saturation (S)")
axes[2].axis("off")
axes[3].imshow(v, cmap="gray")
axes[3].set_title("Value (V)")
axes[3].axis("off")
plt.suptitle("HSV Colour Space Channels", fontsize=14)
plt.tight_layout()
plt.savefig("hsv_channels.png", dpi=150, bbox_inches="tight")
plt.show()
# Apply Otsu's threshold on the Saturation channel
thresh_val, binary_mask = cv2.threshold(s, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"Otsu automatically selected threshold: {thresh_val}")
# Morphological operations to clean the mask
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
mask_closed = cv2.morphologyEx(binary_mask, cv2.MORPH_CLOSE, kernel, iterations=2)
mask_clean = cv2.morphologyEx(mask_closed, cv2.MORPH_OPEN, kernel, iterations=2)
# Visualize the thresholding pipeline
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
axes[0].imshow(s, cmap="gray")
axes[0].set_title("Saturation Channel")
axes[0].axis("off")
axes[1].imshow(binary_mask, cmap="gray")
axes[1].set_title(f"Otsu Threshold (val={int(thresh_val)})")
axes[1].axis("off")
axes[2].imshow(mask_clean, cmap="gray")
axes[2].set_title("After Morphological Cleanup")
axes[2].axis("off")
plt.suptitle("Thresholding Pipeline", fontsize=14)
plt.tight_layout()
plt.savefig("threshold_steps.png", dpi=150, bbox_inches="tight")
plt.show()
# Find contours in the clean mask
contours, _ = cv2.findContours(mask_clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(f"Found {len(contours)} contour(s)")
# Select the largest contour (the lesion)
largest_contour = max(contours, key=cv2.contourArea)
# Draw the lesion boundary on the original image
result = image_rgb.copy()
cv2.drawContours(result, [largest_contour], -1, (0, 255, 0), 2)
# Final side-by-side visualization
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
axes[0].imshow(image_rgb)
axes[0].set_title("Original Dermoscopy Image")
axes[0].axis("off")
axes[1].imshow(mask_clean, cmap="gray")
axes[1].set_title("Segmentation Mask")
axes[1].axis("off")
axes[2].imshow(result)
axes[2].set_title("Detected Lesion Boundary")
axes[2].axis("off")
plt.suptitle("Skin Lesion Colour Segmentation Result", fontsize=14)
plt.tight_layout()
plt.savefig("lesion_segmentation_result.png", dpi=150, bbox_inches="tight")
plt.show()
print("Segmentation complete! Results saved as PNG files.")
Secret mission
Add a Colour Overlay and Measure Lesion Size
Your segmentation pipeline detects where the lesion is, but a green outline only tells part of the story. In real dermatology research, clinicians want to see the affected area highlighted and know exactly how much skin it covers. Can you add a semi-transparent colour overlay and calculate the lesion's size as a percentage of the image?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- Project folder containing segment_lesion.py, ISIC_0024306.jpg, and generated PNG output files.
- pip packages: opencv-python, numpy, and matplotlib.
- Python 3.13 installation.
- VS Code with the Python extension.
Keep everything running
No action needed. Choose this if you plan to continue experimenting with OpenCV or building more computer vision projects.
- Your project folder, Python installation, VS Code, and pip packages all remain on your machine ready for future use.
- There are no running services or ongoing costs to worry about.
Pause - I'll come back to this later
Since this project runs entirely locally with no background processes, there is nothing to pause. Your files will be waiting for you whenever you return.
- Close VS Code to free up memory.
- Your project folder and all installed packages remain on your machine.
Delete - I don't want to use this again
Remove all project resources and installed packages. Follow each step below to clean up completely.
- Close VS Code if it is still open.
- Delete the project folder. Open a new Command Prompt and run the following command, replacing the path with the actual location of your project folder:
rmdir /s /q [[PROJECT_PATH="C:\Users\YourName\Desktop\your-project-folder"]]
- Uninstall the pip packages by running:
pip uninstall opencv-python numpy matplotlib -y
- Uninstall Python 3.13. Press the Windows key, type Add or remove programs, and press Enter. Search for Python 3.13 in the list, click it, and select Uninstall.
- Uninstall VS Code. In the same Add or remove programs panel, search for Visual Studio Code, click it, and select Uninstall.
Nice Work!
Nice Work!
You just built a complete colour-based segmentation pipeline that isolates a skin lesion from a real dermoscopy image. This is a foundational technique used in dermatology AI research, and you did it all with Python and OpenCV.
You've learned how to:
- Download a real dermoscopy image from the ISIC Archive and segment it using HSV Saturation-based thresholding, which outperforms grayscale for medical image segmentation.
- Apply Otsu's automatic thresholding and morphological operations (closing and opening) to produce a clean binary mask without manually tuning any parameters.
- Detect the lesion boundary using contour detection and produce multi-panel visualizations of the entire segmentation pipeline.
- Secret Mission: Added a semi-transparent colour overlay and calculated the lesion's area as a percentage of the total image.
Ready to quiz yourself?