-
- Notifications
You must be signed in to change notification settings - Fork 48.7k
Added Skeletonizing and Pruning Operations - DIP #13141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
joydipb01 wants to merge 15 commits into TheAlgorithms:master Choose a base branch from joydipb01:pruning_morph_op_hacktoberfest_joydipb01
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline, and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
15 commits Select commit Hold shift + click to select a range
2cfc530
:sparkles: Skeletonization of image using Zhang-Suen Algorithm
joydipb01 2a4b9be
updating DIRECTORY.md
joydipb01 ccd99a7
:art: Improved formatting and is compatible with black, ruff and mypy
joydipb01 814f92c
Merge branch 'pruning_morph_op_hacktoberfest_joydipb01' of https://gi…
joydipb01 cb6464a
:memo: Health Warning
joydipb01 16cc7f8
:sparkles: Introduced pruning morphological operation
joydipb01 95765a6
updating DIRECTORY.md
joydipb01 266ce4f
:sparkles: Added skeleton image as data for pruning
joydipb01 b461e85
Merge branch 'pruning_morph_op_hacktoberfest_joydipb01' of https://gi…
joydipb01 3659f27
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] e10cf9b
:bug: Fixed precommit errors
joydipb01 4a3f2a4
Merge branch 'pruning_morph_op_hacktoberfest_joydipb01' of https://gi…
joydipb01 ebb698f
:memo: Added descriptive variable names to function parameters
joydipb01 2722cfd
Merge branch 'master' into pruning_morph_op_hacktoberfest_joydipb01
joydipb01 1f9c818
updating DIRECTORY.md
joydipb01 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
189 changes: 189 additions & 0 deletions 189 digital_image_processing/morphological_operations/pruning_operation.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
# @Author: @joydipb01 | ||
# @File: pruning_operation.py | ||
# @Time: 2025-10-03 19:45 | ||
| ||
from pathlib import Path | ||
| ||
import numpy as np | ||
from PIL import Image | ||
| ||
| ||
def rgb_to_gray(rgb: np.ndarray) -> np.ndarray: | ||
""" | ||
Return gray image from rgb image | ||
| ||
>>> rgb_to_gray(np.array([[[127, 255, 0]]])) | ||
array([[187.6453]]) | ||
>>> rgb_to_gray(np.array([[[0, 0, 0]]])) | ||
array([[0.]]) | ||
>>> rgb_to_gray(np.array([[[2, 4, 1]]])) | ||
array([[3.0598]]) | ||
>>> rgb_to_gray(np.array([[[26, 255, 14], [5, 147, 20], [1, 200, 0]]])) | ||
array([[159.0524, 90.0635, 117.6989]]) | ||
""" | ||
r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2] | ||
return 0.2989 * r + 0.5870 * g + 0.1140 * b | ||
| ||
| ||
def gray_to_binary(gray: np.ndarray) -> np.ndarray: | ||
""" | ||
Return binary image from gray image | ||
| ||
>>> gray_to_binary(np.array([[127, 255, 0]])) | ||
array([[False, True, False]]) | ||
>>> gray_to_binary(np.array([[0]])) | ||
array([[False]]) | ||
>>> gray_to_binary(np.array([[26.2409, 4.9315, 1.4729]])) | ||
array([[False, False, False]]) | ||
>>> gray_to_binary(np.array([[26, 255, 14], [5, 147, 20], [1, 200, 0]])) | ||
array([[False, True, False], | ||
[False, True, False], | ||
[False, True, False]]) | ||
""" | ||
return (gray > 127) & (gray <= 255) | ||
| ||
| ||
def neighbours(image: np.ndarray, x: int, y: int) -> list: | ||
""" | ||
Return 8-neighbours of point (x, y), in clockwise order | ||
| ||
>>> neighbours( | ||
... np.array( | ||
... [ | ||
... [True, True, False], | ||
... [True, False, False], | ||
... [False, True, False] | ||
... ] | ||
... ), 1, 1 | ||
... ) | ||
[np.True_, np.False_, np.False_, np.False_, np.True_, np.False_, np.True_, np.True_] | ||
>>> neighbours( | ||
... np.array( | ||
... [ | ||
... [True, True, False, True], | ||
... [True, False, False, True], | ||
... [False, True, False, True] | ||
... ] | ||
... ), 1, 2 | ||
... ) | ||
[np.False_, np.True_, np.True_, np.True_, np.False_, np.True_, np.False_, np.True_] | ||
""" | ||
img = image | ||
| ||
neighborhood = [ | ||
(-1, 0), | ||
(-1, 1), | ||
(0, 1), | ||
(1, 1), | ||
(1, 0), | ||
(1, -1), | ||
(0, -1), | ||
(-1, -1), | ||
] | ||
| ||
neighbour_points = [] | ||
| ||
for dx, dy in neighborhood: | ||
if 0 <= x + dx < img.shape[0] and 0 <= y + dy < img.shape[1]: | ||
neighbour_points.append(img[x + dx][y + dy]) | ||
else: | ||
neighbour_points.append(False) | ||
| ||
return neighbour_points | ||
| ||
| ||
def is_endpoint(image: np.ndarray, x: int, y: int) -> bool: | ||
| ||
""" | ||
Check if a pixel is an endpoint based on its 8-neighbors. | ||
| ||
An endpoint is defined as a pixel that has exactly one neighboring pixel | ||
that is part of the foreground (True). | ||
| ||
>>> is_endpoint( | ||
... np.array( | ||
... [ | ||
... [True, True, False], | ||
... [True, False, False], | ||
... [False, True, False] | ||
... ] | ||
... ), 1, 1 | ||
... ) | ||
False | ||
>>> is_endpoint( | ||
... np.array( | ||
... [ | ||
... [True, True, False, True], | ||
... [True, False, False, True], | ||
... [False, True, False, True] | ||
... ] | ||
... ), 2, 3 | ||
... ) | ||
True | ||
""" | ||
img = image | ||
return int(sum(neighbours(img, x, y))) == 1 | ||
| ||
| ||
def prune_skeletonized_image( | ||
image: np.ndarray, spur_branch_length: int = 50 | ||
) -> np.ndarray: | ||
""" | ||
Return pruned image by removing spurious branches of specified length | ||
Source: https://www.scribd.com/doc/15792184/042805-04 | ||
| ||
>>> arr = np.array([ | ||
... [False, True, False], | ||
... [False, True, False], | ||
... [False, True, True] | ||
... ]) | ||
>>> prune_skeletonized_image(arr, spur_branch_length=1) | ||
array([[False, True, False], | ||
[False, True, False], | ||
[False, True, True]]) | ||
>>> arr2 = np.array([ | ||
... [False, False, False, False], | ||
... [False, True, True, False], | ||
... [False, False, False, False] | ||
... ]) | ||
>>> prune_skeletonized_image(arr2, spur_branch_length=1) | ||
array([[False, False, False, False], | ||
[False, False, False, False], | ||
[False, False, False, False]]) | ||
>>> arr3 = np.array([ | ||
... [False, True, False], | ||
... [False, True, False], | ||
... [False, True, False] | ||
... ]) | ||
>>> prune_skeletonized_image(arr3, spur_branch_length=2) | ||
array([[False, True, False], | ||
[False, True, False], | ||
[False, True, False]]) | ||
""" | ||
img = image.copy() | ||
rows, cols = img.shape | ||
| ||
for _ in range(spur_branch_length): | ||
endpoints = [] | ||
| ||
for i in range(1, rows - 1): | ||
for j in range(1, cols - 1): | ||
if img[i][j] and is_endpoint(img, i, j): | ||
endpoints.append((i, j)) | ||
for x, y in endpoints: | ||
img[x][y] = False | ||
return img | ||
| ||
| ||
if __name__ == "__main__": | ||
# Read original (skeletonized) image | ||
skeleton_lena_path = ( | ||
Path(__file__).resolve().parent.parent / "image_data" / "skeleton_lena.png" | ||
) | ||
skeleton_lena = np.array(Image.open(skeleton_lena_path)) | ||
| ||
# Apply pruning operation to a skeletonized image | ||
output = prune_skeletonized_image(gray_to_binary(rgb_to_gray(skeleton_lena))) | ||
| ||
# Save the output image | ||
pil_img = Image.fromarray(output).convert("RGB") | ||
pil_img.save("result_pruned.png") |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please provide descriptive name for the parameter:
x
Please provide descriptive name for the parameter:
y