From 2cfc530fd89f185bc1e59d025fd87cf027cff907 Mon Sep 17 00:00:00 2001 From: Joydip Bhattacharyya Date: Fri, 3 Oct 2025 13:46:29 +0530 Subject: [PATCH 01/11] :sparkles: Skeletonization of image using Zhang-Suen Algorithm --- .../skeletonization_operation.py | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 digital_image_processing/morphological_operations/skeletonization_operation.py diff --git a/digital_image_processing/morphological_operations/skeletonization_operation.py b/digital_image_processing/morphological_operations/skeletonization_operation.py new file mode 100644 index 000000000000..eaef17cf6f3a --- /dev/null +++ b/digital_image_processing/morphological_operations/skeletonization_operation.py @@ -0,0 +1,153 @@ +# @Author: @joydipb01 +# @File: skeletonization_operation.py +# @Time: 2025-10-03 13:45 IST + +import numpy as np +from PIL import Image +from pathlib import Path + + +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(1, 1, np.array([[True, True, False], [True, False, False], [False, True, False]])) + [np.True_, np.False_, np.False_, np.False_, np.True_, np.False_, np.True_, np.True_] + >>> neighbours(1, 2, np.array([[True, True, False, True], [True, False, False, True], [False, True, False, True]])) + [np.False_, np.True_, np.True_, np.True_, np.False_, np.True_, np.False_, np.True_] + """ + img = image + return [ + img[x-1][y], img[x-1][y+1], img[x][y+1], img[x+1][y+1], + img[x+1][y], img[x+1][y-1], img[x][y-1], img[x-1][y-1] + ] + + +def transitions(neighbors: list) -> int: + """ + Count 0->1 transitions in the neighborhood + + >>> transitions([np.False_, np.True_, np.True_, np.False_, np.True_, np.False_, np.False_, np.False_]) + 2 + >>> transitions([np.True_, np.True_, np.True_, np.True_, np.True_, np.True_, np.True_, np.True_]) + 0 + >>> transitions([np.False_, np.False_, np.False_, np.False_, np.False_, np.False_, np.False_, np.False_]) + 0 + >>> transitions([np.False_, np.True_, np.False_, np.True_, np.False_, np.True_, np.False_, np.True_]) + 4 + >>> transitions([np.True_, np.False_, np.True_, np.False_, np.True_, np.False_, np.True_, np.False_]) + 4 + """ + n = neighbors + [neighbors[0]] + return int(sum((n1 == 0 and n2 == 1) for n1, n2 in zip(n, n[1:]))) + + +def skeletonize_image(image: np.ndarray) -> np.ndarray: + """ + Apply Zhang-Suen thinning to binary image for skeletonization. + Source: https://rstudio-pubs-static.s3.amazonaws.com/302782_e337cfbc5ad24922bae96ca5977f4da8.html + + >>> skeletonize_image(np.array([[np.False_, np.True_, np.False_], + ... [np.True_, np.True_, np.True_], + ... [np.False_, np.True_, np.False_]])) + array([[False, True, False], + [ True, True, True], + [False, True, False]]) + >>> skeletonize_image(np.array([[np.False_, np.False_, np.False_], + ... [np.False_, np.True_, np.False_], + ... [np.False_, np.False_, np.False_]])) + array([[False, False, False], + [False, True, False], + [False, False, False]]) + """ + img = image.copy() + changing1 = changing2 = True + + while changing1 or changing2: + + # Step 1: Points to be removed in the first sub-iteration + changing1 = [] + rows, cols = img.shape + for x in range(1, rows - 1): + for y in range(1, cols - 1): + P = img[x][y] + if P != 1: + continue + neighbours_list = neighbours(img, x, y) + total_transitions = transitions(neighbours_list) + N = sum(neighbours_list) + if (2 <= N <= 6 and + total_transitions == 1 and + neighbours_list[0] * neighbours_list[2] * neighbours_list[4] == 0 and + neighbours_list[2] * neighbours_list[4] * neighbours_list[6] == 0): + changing1.append((x, y)) + for x, y in changing1: + img[x][y] = 0 + + # Step 2: Points to be removed in the second sub-iteration + changing2 = [] + for x in range(1, rows - 1): + for y in range(1, cols - 1): + P = img[x][y] + if P != 1: + continue + neighbours_list = neighbours(img, x, y) + total_transitions = transitions(neighbours_list) + N = sum(neighbours_list) + if (2 <= N <= 6 and + total_transitions == 1 and + neighbours_list[0] * neighbours_list[2] * neighbours_list[6] == 0 and + neighbours_list[0] * neighbours_list[4] * neighbours_list[6] == 0): + changing2.append((x, y)) + for x, y in changing2: + img[x][y] = 0 + + return img + + +if __name__ == "__main__": + # Read original image + lena_path = Path(__file__).resolve().parent.parent / "image_data" / "lena.jpg" + lena = np.array(Image.open(lena_path)) + + # Apply skeletonization operation to a binary image + output = skeletonize_image(gray_to_binary(rgb_to_gray(lena))) + + # Save the output image + pil_img = Image.fromarray(output).convert("RGB") + pil_img.save("result_skeleton.png") \ No newline at end of file From 2a4b9be7c9ce8ce14c906c73be2128670eeb6932 Mon Sep 17 00:00:00 2001 From: joydipb01 Date: Fri, 3 Oct 2025 09:18:56 +0000 Subject: [PATCH 02/11] updating DIRECTORY.md --- DIRECTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 36acb3b97f1e..340367cb499c 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -336,6 +336,7 @@ * Morphological Operations * [Dilation Operation](digital_image_processing/morphological_operations/dilation_operation.py) * [Erosion Operation](digital_image_processing/morphological_operations/erosion_operation.py) + * [Skeletonization Operation](digital_image_processing/morphological_operations/skeletonization_operation.py) * Resize * [Resize](digital_image_processing/resize/resize.py) * Rotation From ccd99a7ef4704e5bcb03e37b8366bf8b0cfb86ab Mon Sep 17 00:00:00 2001 From: Joydip Bhattacharyya Date: Fri, 3 Oct 2025 15:37:43 +0530 Subject: [PATCH 03/11] :art: Improved formatting and is compatible with black, ruff and mypy --- .../skeletonization_operation.py | 117 +++++++++++++----- 1 file changed, 87 insertions(+), 30 deletions(-) diff --git a/digital_image_processing/morphological_operations/skeletonization_operation.py b/digital_image_processing/morphological_operations/skeletonization_operation.py index eaef17cf6f3a..92184dfffc1d 100644 --- a/digital_image_processing/morphological_operations/skeletonization_operation.py +++ b/digital_image_processing/morphological_operations/skeletonization_operation.py @@ -2,9 +2,11 @@ # @File: skeletonization_operation.py # @Time: 2025-10-03 13:45 IST +from itertools import pairwise +from pathlib import Path + import numpy as np from PIL import Image -from pathlib import Path def rgb_to_gray(rgb: np.ndarray) -> np.ndarray: @@ -46,42 +48,89 @@ def neighbours(image: np.ndarray, x: int, y: int) -> list: """ Return 8-neighbours of point (x, y), in clockwise order - >>> neighbours(1, 1, np.array([[True, True, False], [True, False, False], [False, True, False]])) + >>> 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(1, 2, np.array([[True, True, False, True], [True, False, False, True], [False, True, False, 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 return [ - img[x-1][y], img[x-1][y+1], img[x][y+1], img[x+1][y+1], - img[x+1][y], img[x+1][y-1], img[x][y-1], img[x-1][y-1] + img[x - 1][y], + img[x - 1][y + 1], + img[x][y + 1], + img[x + 1][y + 1], + img[x + 1][y], + img[x + 1][y - 1], + img[x][y - 1], + img[x - 1][y - 1], ] def transitions(neighbors: list) -> int: """ Count 0->1 transitions in the neighborhood - - >>> transitions([np.False_, np.True_, np.True_, np.False_, np.True_, np.False_, np.False_, np.False_]) + + >>> transitions( + ... [ + ... np.False_, np.True_, np.True_, np.False_, + ... np.True_, np.False_, np.False_, np.False_ + ... ] + ... ) 2 - >>> transitions([np.True_, np.True_, np.True_, np.True_, np.True_, np.True_, np.True_, np.True_]) + >>> transitions( + ... [ + ... np.True_, np.True_, np.True_, np.True_, + ... np.True_, np.True_, np.True_, np.True_ + ... ] + ... ) 0 - >>> transitions([np.False_, np.False_, np.False_, np.False_, np.False_, np.False_, np.False_, np.False_]) + >>> transitions( + ... [ + ... np.False_, np.False_, np.False_, np.False_, + ... np.False_, np.False_, np.False_, np.False_ + ... ] + ... ) 0 - >>> transitions([np.False_, np.True_, np.False_, np.True_, np.False_, np.True_, np.False_, np.True_]) + >>> transitions( + ... [ + ... np.False_, np.True_, np.False_, np.True_, + ... np.False_, np.True_, np.False_, np.True_ + ... ] + ... ) 4 - >>> transitions([np.True_, np.False_, np.True_, np.False_, np.True_, np.False_, np.True_, np.False_]) + >>> transitions( + ... [ + ... np.True_, np.False_, np.True_, np.False_, + ... np.True_, np.False_, np.True_, np.False_ + ... ] + ... ) 4 """ - n = neighbors + [neighbors[0]] - return int(sum((n1 == 0 and n2 == 1) for n1, n2 in zip(n, n[1:]))) + n = [*neighbors, neighbors[0]] + return int(sum((n1 == 0 and n2 == 1) for n1, n2 in pairwise(n))) def skeletonize_image(image: np.ndarray) -> np.ndarray: """ Apply Zhang-Suen thinning to binary image for skeletonization. Source: https://rstudio-pubs-static.s3.amazonaws.com/302782_e337cfbc5ad24922bae96ca5977f4da8.html - + >>> skeletonize_image(np.array([[np.False_, np.True_, np.False_], ... [np.True_, np.True_, np.True_], ... [np.False_, np.True_, np.False_]])) @@ -96,7 +145,7 @@ def skeletonize_image(image: np.ndarray) -> np.ndarray: [False, False, False]]) """ img = image.copy() - changing1 = changing2 = True + changing1 = changing2 = [(-1, -1)] while changing1 or changing2: @@ -105,16 +154,20 @@ def skeletonize_image(image: np.ndarray) -> np.ndarray: rows, cols = img.shape for x in range(1, rows - 1): for y in range(1, cols - 1): - P = img[x][y] - if P != 1: + pixel = img[x][y] + if pixel != 1: continue neighbours_list = neighbours(img, x, y) total_transitions = transitions(neighbours_list) - N = sum(neighbours_list) - if (2 <= N <= 6 and - total_transitions == 1 and - neighbours_list[0] * neighbours_list[2] * neighbours_list[4] == 0 and - neighbours_list[2] * neighbours_list[4] * neighbours_list[6] == 0): + n = sum(neighbours_list) + if ( + 2 <= n <= 6 + and total_transitions == 1 + and neighbours_list[0] * neighbours_list[2] * neighbours_list[4] + == 0 + and neighbours_list[2] * neighbours_list[4] * neighbours_list[6] + == 0 + ): changing1.append((x, y)) for x, y in changing1: img[x][y] = 0 @@ -123,16 +176,20 @@ def skeletonize_image(image: np.ndarray) -> np.ndarray: changing2 = [] for x in range(1, rows - 1): for y in range(1, cols - 1): - P = img[x][y] - if P != 1: + pixel = img[x][y] + if pixel != 1: continue neighbours_list = neighbours(img, x, y) total_transitions = transitions(neighbours_list) - N = sum(neighbours_list) - if (2 <= N <= 6 and - total_transitions == 1 and - neighbours_list[0] * neighbours_list[2] * neighbours_list[6] == 0 and - neighbours_list[0] * neighbours_list[4] * neighbours_list[6] == 0): + n = sum(neighbours_list) + if ( + 2 <= n <= 6 + and total_transitions == 1 + and neighbours_list[0] * neighbours_list[2] * neighbours_list[6] + == 0 + and neighbours_list[0] * neighbours_list[4] * neighbours_list[6] + == 0 + ): changing2.append((x, y)) for x, y in changing2: img[x][y] = 0 @@ -150,4 +207,4 @@ def skeletonize_image(image: np.ndarray) -> np.ndarray: # Save the output image pil_img = Image.fromarray(output).convert("RGB") - pil_img.save("result_skeleton.png") \ No newline at end of file + pil_img.save("result_skeleton.png") From cb6464a2618c3cd000601c1b4f888cc69844d241 Mon Sep 17 00:00:00 2001 From: Joydip Bhattacharyya Date: Fri, 3 Oct 2025 19:26:17 +0530 Subject: [PATCH 04/11] :memo: Health Warning --- .../morphological_operations/skeletonization_operation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/digital_image_processing/morphological_operations/skeletonization_operation.py b/digital_image_processing/morphological_operations/skeletonization_operation.py index 92184dfffc1d..872e651014ce 100644 --- a/digital_image_processing/morphological_operations/skeletonization_operation.py +++ b/digital_image_processing/morphological_operations/skeletonization_operation.py @@ -203,6 +203,7 @@ def skeletonize_image(image: np.ndarray) -> np.ndarray: lena = np.array(Image.open(lena_path)) # Apply skeletonization operation to a binary image + # Caution: Takes at least 20 seconds to execute output = skeletonize_image(gray_to_binary(rgb_to_gray(lena))) # Save the output image From 16cc7f845e1019bada0fbdba13ccf5096e9d41f9 Mon Sep 17 00:00:00 2001 From: Joydip Bhattacharyya Date: Fri, 3 Oct 2025 20:50:12 +0530 Subject: [PATCH 05/11] :sparkles: Introduced pruning morphological operation --- .../pruning_operation.py | 188 ++++++++++++++++++ .../skeletonization_operation.py | 2 +- 2 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 digital_image_processing/morphological_operations/pruning_operation.py diff --git a/digital_image_processing/morphological_operations/pruning_operation.py b/digital_image_processing/morphological_operations/pruning_operation.py new file mode 100644 index 000000000000..3c6024a5dd1e --- /dev/null +++ b/digital_image_processing/morphological_operations/pruning_operation.py @@ -0,0 +1,188 @@ +# @Author: @joydipb01 +# @File: pruning_operation.py +# @Time: 2025-10-03 19:45 IST + +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 + + >>> 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") diff --git a/digital_image_processing/morphological_operations/skeletonization_operation.py b/digital_image_processing/morphological_operations/skeletonization_operation.py index 872e651014ce..bab5f748d32f 100644 --- a/digital_image_processing/morphological_operations/skeletonization_operation.py +++ b/digital_image_processing/morphological_operations/skeletonization_operation.py @@ -203,7 +203,7 @@ def skeletonize_image(image: np.ndarray) -> np.ndarray: lena = np.array(Image.open(lena_path)) # Apply skeletonization operation to a binary image - # Caution: Takes at least 20 seconds to execute + # Caution: Takes at least 30 seconds to execute output = skeletonize_image(gray_to_binary(rgb_to_gray(lena))) # Save the output image From 95765a69245e568ff11c629943078bdf17e807c0 Mon Sep 17 00:00:00 2001 From: joydipb01 Date: Fri, 3 Oct 2025 15:20:27 +0000 Subject: [PATCH 06/11] updating DIRECTORY.md --- DIRECTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 340367cb499c..6d731e7bff8c 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -336,6 +336,7 @@ * Morphological Operations * [Dilation Operation](digital_image_processing/morphological_operations/dilation_operation.py) * [Erosion Operation](digital_image_processing/morphological_operations/erosion_operation.py) + * [Pruning Operation](digital_image_processing/morphological_operations/pruning_operation.py) * [Skeletonization Operation](digital_image_processing/morphological_operations/skeletonization_operation.py) * Resize * [Resize](digital_image_processing/resize/resize.py) From 266ce4fbfafc5e9790d83bb39e177c926b3963a3 Mon Sep 17 00:00:00 2001 From: Joydip Bhattacharyya Date: Fri, 3 Oct 2025 20:50:55 +0530 Subject: [PATCH 07/11] :sparkles: Added skeleton image as data for pruning --- .../image_data/skeleton_lena.png | Bin 0 -> 19905 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 digital_image_processing/image_data/skeleton_lena.png diff --git a/digital_image_processing/image_data/skeleton_lena.png b/digital_image_processing/image_data/skeleton_lena.png new file mode 100644 index 0000000000000000000000000000000000000000..b4ad2d45b4561bd20e7b4bb313f0bd87204f0618 GIT binary patch literal 19905 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

-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 literal 0 HcmV?d00001 From 3659f2749debabcf6d8df8c606cd9215da757e74 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:28:17 +0000 Subject: [PATCH 08/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../morphological_operations/skeletonization_operation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/digital_image_processing/morphological_operations/skeletonization_operation.py b/digital_image_processing/morphological_operations/skeletonization_operation.py index bab5f748d32f..46871eb331bb 100644 --- a/digital_image_processing/morphological_operations/skeletonization_operation.py +++ b/digital_image_processing/morphological_operations/skeletonization_operation.py @@ -148,7 +148,6 @@ def skeletonize_image(image: np.ndarray) -> np.ndarray: changing1 = changing2 = [(-1, -1)] while changing1 or changing2: - # Step 1: Points to be removed in the first sub-iteration changing1 = [] rows, cols = img.shape From e10cf9be6335c1669302a13b15f44dd3d8cc12d7 Mon Sep 17 00:00:00 2001 From: Joydip Bhattacharyya Date: Fri, 3 Oct 2025 21:03:02 +0530 Subject: [PATCH 09/11] :bug: Fixed precommit errors --- .../morphological_operations/pruning_operation.py | 3 ++- .../morphological_operations/skeletonization_operation.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/digital_image_processing/morphological_operations/pruning_operation.py b/digital_image_processing/morphological_operations/pruning_operation.py index 3c6024a5dd1e..58724bb89e79 100644 --- a/digital_image_processing/morphological_operations/pruning_operation.py +++ b/digital_image_processing/morphological_operations/pruning_operation.py @@ -1,6 +1,6 @@ # @Author: @joydipb01 # @File: pruning_operation.py -# @Time: 2025-10-03 19:45 IST +# @Time: 2025-10-03 19:45 from pathlib import Path @@ -129,6 +129,7 @@ def prune_skeletonized_image( ) -> 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], diff --git a/digital_image_processing/morphological_operations/skeletonization_operation.py b/digital_image_processing/morphological_operations/skeletonization_operation.py index bab5f748d32f..2efbd74e2f66 100644 --- a/digital_image_processing/morphological_operations/skeletonization_operation.py +++ b/digital_image_processing/morphological_operations/skeletonization_operation.py @@ -1,6 +1,6 @@ # @Author: @joydipb01 # @File: skeletonization_operation.py -# @Time: 2025-10-03 13:45 IST +# @Time: 2025-10-03 13:45 from itertools import pairwise from pathlib import Path @@ -148,7 +148,6 @@ def skeletonize_image(image: np.ndarray) -> np.ndarray: changing1 = changing2 = [(-1, -1)] while changing1 or changing2: - # Step 1: Points to be removed in the first sub-iteration changing1 = [] rows, cols = img.shape From ebb698f2c11112eeb352106328049eae5aa76345 Mon Sep 17 00:00:00 2001 From: Joydip Bhattacharyya Date: Fri, 3 Oct 2025 21:11:15 +0530 Subject: [PATCH 10/11] :memo: Added descriptive variable names to function parameters --- .../pruning_operation.py | 12 +++++------ .../skeletonization_operation.py | 20 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/digital_image_processing/morphological_operations/pruning_operation.py b/digital_image_processing/morphological_operations/pruning_operation.py index 58724bb89e79..324b2db9018f 100644 --- a/digital_image_processing/morphological_operations/pruning_operation.py +++ b/digital_image_processing/morphological_operations/pruning_operation.py @@ -43,9 +43,9 @@ def gray_to_binary(gray: np.ndarray) -> np.ndarray: return (gray > 127) & (gray <= 255) -def neighbours(image: np.ndarray, x: int, y: int) -> list: +def neighbours(image: np.ndarray, x_coord: int, y_coord: int) -> list: """ - Return 8-neighbours of point (x, y), in clockwise order + Return 8-neighbours of point (x_coord, y_coord), in clockwise order >>> neighbours( ... np.array( @@ -84,15 +84,15 @@ def neighbours(image: np.ndarray, x: int, y: int) -> list: 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]) + if 0 <= x_coord + dx < img.shape[0] and 0 <= y_coord + dy < img.shape[1]: + neighbour_points.append(img[x_coord + dx][y_coord + dy]) else: neighbour_points.append(False) return neighbour_points -def is_endpoint(image: np.ndarray, x: int, y: int) -> bool: +def is_endpoint(image: np.ndarray, x_coord: int, y_coord: int) -> bool: """ Check if a pixel is an endpoint based on its 8-neighbors. @@ -121,7 +121,7 @@ def is_endpoint(image: np.ndarray, x: int, y: int) -> bool: True """ img = image - return int(sum(neighbours(img, x, y))) == 1 + return int(sum(neighbours(img, x_coord, y_coord))) == 1 def prune_skeletonized_image( diff --git a/digital_image_processing/morphological_operations/skeletonization_operation.py b/digital_image_processing/morphological_operations/skeletonization_operation.py index 2efbd74e2f66..1b46112c2f66 100644 --- a/digital_image_processing/morphological_operations/skeletonization_operation.py +++ b/digital_image_processing/morphological_operations/skeletonization_operation.py @@ -44,9 +44,9 @@ def gray_to_binary(gray: np.ndarray) -> np.ndarray: return (gray > 127) & (gray <= 255) -def neighbours(image: np.ndarray, x: int, y: int) -> list: +def neighbours(image: np.ndarray, x_coord: int, y_coord: int) -> list: """ - Return 8-neighbours of point (x, y), in clockwise order + Return 8-neighbours of point (x_coord, y_coord), in clockwise order >>> neighbours( ... np.array( @@ -71,14 +71,14 @@ def neighbours(image: np.ndarray, x: int, y: int) -> list: """ img = image return [ - img[x - 1][y], - img[x - 1][y + 1], - img[x][y + 1], - img[x + 1][y + 1], - img[x + 1][y], - img[x + 1][y - 1], - img[x][y - 1], - img[x - 1][y - 1], + img[x_coord - 1][y_coord], + img[x_coord - 1][y_coord + 1], + img[x_coord][y_coord + 1], + img[x_coord + 1][y_coord + 1], + img[x_coord + 1][y_coord], + img[x_coord + 1][y_coord - 1], + img[x_coord][y_coord - 1], + img[x_coord - 1][y_coord - 1], ] From 1f9c818c30149dc454b2894a6675298c4bd27f90 Mon Sep 17 00:00:00 2001 From: joydipb01 Date: Wed, 8 Oct 2025 17:59:54 +0000 Subject: [PATCH 11/11] updating DIRECTORY.md --- DIRECTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 6d731e7bff8c..033a1bef8c5e 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -195,6 +195,7 @@ * [Permutations](data_structures/arrays/permutations.py) * [Prefix Sum](data_structures/arrays/prefix_sum.py) * [Product Sum](data_structures/arrays/product_sum.py) + * [Rotate Array](data_structures/arrays/rotate_array.py) * [Sparse Table](data_structures/arrays/sparse_table.py) * [Sudoku Solver](data_structures/arrays/sudoku_solver.py) * Binary Tree