{"id":20346665,"url":"https://github.com/arthurfdlr/image_boundaries_analysis","last_synced_at":"2026-06-01T06:31:18.892Z","repository":{"id":112736527,"uuid":"351845567","full_name":"ArthurFDLR/Image_Boundaries_Analysis","owner":"ArthurFDLR","description":null,"archived":false,"fork":false,"pushed_at":"2021-03-29T03:58:26.000Z","size":1638,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-04T15:48:30.686Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Jupyter Notebook","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ArthurFDLR.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-26T16:30:18.000Z","updated_at":"2021-03-29T03:58:28.000Z","dependencies_parsed_at":"2023-09-13T12:47:50.564Z","dependency_job_id":null,"html_url":"https://github.com/ArthurFDLR/Image_Boundaries_Analysis","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ArthurFDLR/Image_Boundaries_Analysis","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurFDLR%2FImage_Boundaries_Analysis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurFDLR%2FImage_Boundaries_Analysis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurFDLR%2FImage_Boundaries_Analysis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurFDLR%2FImage_Boundaries_Analysis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ArthurFDLR","download_url":"https://codeload.github.com/ArthurFDLR/Image_Boundaries_Analysis/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ArthurFDLR%2FImage_Boundaries_Analysis/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33763648,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T22:13:46.438Z","updated_at":"2026-06-01T06:31:18.870Z","avatar_url":"https://github.com/ArthurFDLR.png","language":"Jupyter Notebook","funding_links":[],"categories":[],"sub_categories":[],"readme":"# \u003ch1 align = \"center\"\u003e Image Boundaries Analysis \u003c/h1\u003e\n\n\u003ca href=\"https://colab.research.google.com/github/ArthurFDLR/Image_Boundaries_Analysis/blob/main/Image_Boundaries_Analysis.ipynb\" target=\"_parent\"\u003e\u003cimg src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/\u003e\u003c/a\u003e\n\n\n```python\nimport numpy as np\nfrom cv2 import filter2D\nfrom matplotlib import pyplot as plt\nfrom skimage import io\n```\n\n\n```python\ndef draw_border(border:np.ndarray, img_shape):\n    img = np.zeros(img_shape)\n    if border.dtype == np.complex_:\n        for c in border:\n            img[int(c.imag), int(c.real)] = 1.\n    else:\n        for c in border:\n            img[int(c[0]), int(c[1])] = 1.\n    return img\n```\n\n# Global thresholding\n\nThe following algorithm describes a basic global thresholding method for an image $f$:\n\n1. Select an initial estimate for the global threshold $T$.\n\n2. Segment the image using the function $g$. This will produce two groups of pixels $G_+$ and $G_-$.\n    \n    $$ g(x,y) = \\left\\{\n    \\begin{array}{ll}\n        1, \\ f(x,y) \u003e T \\\\\n        0, \\ f(x,y) \\leq T\n    \\end{array}\n    \\right. $$\n\n3. Compute the average (mean) intensity values $m_+$ and $m_-$ for the pixels in $G_+$ and $G_-$ respectively.\n\n4. Compute a new threshold value:\n\n    $$ T = \\frac{m_+ + m_-}{2} $$\n\n5. Repeat Steps 2 through 4 until the difference between values of $T$ in successive iterations is smaller than a predefined parameter $\\Delta_T$.\n\nOur program is a direct implementation of the above algorithm in Python with an initial threshold value of half the maximum intensity in the image and $\\Delta_T=1$. $T$ being an integer, the algorithm stops when T no longer evolve.\n\n\n```python\ndef basic_global_tresholding(img):\n    delta_T = 1\n    T = np.max(img) // 2 # Step 1\n    T_old = T + delta_T\n    while abs(T - T_old) \u003e= delta_T: # Step 5\n        img_treshold = (img \u003e T) # Step 2\n        m1, m2 = img[~img_treshold].mean(), img[img_treshold].mean() # Step 3\n        T_old, T = T, (m1 + m2)//2 # Step 4\n    return (img \u003e= T)*1, T\n```\n\n\n```python\n# Image import\nimg = io.imread('https://github.com/ArthurFDLR/Image_Boundaries_Analysis/blob/main/test_images/noisy_fingerprint.tif?raw=True')\n\n# Computation\nimg_tresholded, treshold = basic_global_tresholding(img)\n\n# Visualization\nfig, axs = plt.subplots(1, 3, figsize=(10,4),dpi=60)\nhist,bins = np.histogram(img.flatten(),256,[0,256])\naxs[0].set_title(\"Input image\")\naxs[0].imshow(img, cmap='gray'), axs[0].set_xticks([]), axs[0].set_yticks([])\naxs[1].set_title(\"Normalized histogram\")\naxs[1].plot(hist/np.prod(img.shape), c='k', label=r'$p_r$')\naxs[1].legend()\naxs[1].set_xlabel(r\"$r_i$\")\naxs[1].axvline(x=treshold, label='Treshold', c='r', ls='dashed')\naxs[1].set_xticks(list(range(0,255,50)) + [int(treshold)])\naxs[2].set_title(\"Global thresholding\")\naxs[2].imshow(img_tresholded, cmap='gray'), axs[2].set_xticks([]), axs[2].set_yticks([])\nfig.tight_layout()\n```\n\n\n    \n![png](./.github/images/Image_Boundaries_Analysis_6_0.png)\n    \n\n\n# Otsu’s thresholding\n\nWhile the basic global thresholding successfully finds a good thresholding value for histogram presenting well-defined peeks, it performs poorly otherwise.\nThe critical characteristic of good thresholding is the optimization of separation between classes according to their intensity values. This separation can be mathematically formalized as and is the between-class variance $\\sigma_N^2$. Otsu's thresholding algorithm revolves around this observation to find an optimal threshold:\n\n1. Compute the normalized histogram $\\left\\{p_i, \\forall i \\in [\\![0, L-1]\\!]\\right\\}$ of the input image.\n\n2. Compute the cumulative sums, $\\forall k \\in [\\![0, L-1]\\!]$\n    $$ P(k) = \\sum_{i=0}^k p_i $$\n\n3. Compute the cumulative means, $\\forall k \\in [\\![0, L-1]\\!]$\n    $$ m(k) = \\sum_{i=0}^k i p_i $$\n    Note that the global intensity mean $m_g=m(L-1)$.\n\n4. Compute the between-class variance, $\\forall k \\in [\\![0, L-1]\\!]$\n\n5. Obtain the Otsu threshold, $k^*$, as the value of $k$ for which $\\sigma_B^2(k)$ is maximum. If the maximum is not unique, obtain $k^*$ by averaging the values of corresponding to the various maxima detected.\n    $$ k^* = \u003c argmax_{\\ k \\in [\\![0, L-1]\\!]} \\left( \\sigma_B^2(k)\\right) \u003e $$\n\n\n```python\ndef otsu_tresholding(img):\n    hist,bins = np.histogram(img.flatten(),256,[0,256]) # Step 1\n    hist = hist/np.prod(img.shape)\n    cdf = hist.cumsum() # Step 2\n    cmf = np.cumsum([p*i for (i, p) in enumerate(hist)]) # Step 3\n    bcv = np.array(\n        [(cmf[-1] * P - m)**2 / (P * (1. - P)) if (P * (1. - P)) != 0. else 0. \\\n         for P, m in zip(cdf, cmf)]\n    ) # Step 4\n    T = bcv.argmax().mean() # Step 5\n    return 1*(img \u003e T), T\n```\n\n\n```python\n# Image import\nimg = io.imread('https://github.com/ArthurFDLR/Image_Boundaries_Analysis/blob/main/test_images/polymersomes.tif?raw=True')\n\n# Computation\nimg_tresholded_otsu, treshold_otsu = otsu_tresholding(img)\nimg_tresholded, treshold = basic_global_tresholding(img)\n\n# Visualization\nfig, axs = plt.subplots(2, 2, figsize=(8,7), dpi=60)\naxs = [ax for sub_ax in axs for ax in sub_ax]\n\nhist,bins = np.histogram(img.flatten(),256,[0,256])\naxs[0].set_title(\"Input image\")\naxs[0].imshow(img, cmap='gray'), axs[0].set_xticks([]), axs[0].set_yticks([])\naxs[1].set_title(\"Normalized histogram\")\naxs[1].plot(hist/np.prod(img.shape), c='k', label=r'$p_r$')\naxs[1].legend()\naxs[1].set_xlabel(r\"$r_i$\")\naxs[1].axvline(x=treshold, c='r', ls='dashed')\naxs[1].axvline(x=treshold_otsu, c='b', ls='dashed')\naxs[1].set_xticks(list(range(0,255,50)) + [int(treshold), int(treshold_otsu)])\naxs[1].set_xlim(125, 225)\naxs[2].set_title(\"Basic global thresholding\")\naxs[2].imshow(img_tresholded, cmap='gray'), axs[2].set_xticks([]), axs[2].set_yticks([])\naxs[3].set_title(\"Otsu's global thresholding\")\naxs[3].imshow(img_tresholded_otsu, cmap='gray'), axs[3].set_xticks([]), axs[3].set_yticks([])\n\nfig.tight_layout()\n```\n\n\n    \n![png](./.github/images/Image_Boundaries_Analysis_10_0.png)\n    \n\n\nThe input image's histogram does not present distinctive peeks, allowing to telling apart the foreground from the background. As expected, the basic global thresholding method yields poor results. The background is partially detected as a zone of interest on the left side of the image. Otsu's algorithm fixes this issue.\n\n# Chain codes\n\nThis problem's end goal is to generate the integer of the minimum magnitude of the Freeman chain code of an object in an image. We can decompose this problem into two primary steps. First, we have to find the boundary of the shape. The shape must present clean and sharp edges. We can remove the noises and sharpen the image using a combination of a $9 \\times 9$ averaging filter and Otsu's algorithm. The boundary following algorithm is then ideally suited to our application:\n\n1. Let the starting point, $b_0$, be the uppermost-leftmost point in the image. Store its coordinates. Denote by $c_0$ the west neighbor of $b_0$. Examine the 8-neighbors of $b_0$ starting at $c_0$ and proceeding in a clockwise direction. Let $b_1$ denote the first neighbor encountered whose value is 1, and let $c_1$ be the (background) point immediately preceding $b_1$ in the sequence.\n\n2. Initiate a couple of variables, $b=b_1$ and $c=c_1$.\n\n3. Let the N-neighbors (N being 4 or 8) of $b$, starting at $c$ and proceeding in a clockwise direction, be denoted by $\\{n_i, \\forall i \\in [\\![0, N-1]\\!]\\}$. Find the first $n_k$ labeled 1.\n\n4. Let $b=n_k$ and $c=n_{k-1}$.\n\n5. Repeat Steps 3 and 4 until $b=b_0$ and the next boundary point found is The sequence of points found when the algorithm stops constitutes the set of ordered boundary points\n\n![Illustration of the first few steps in the boundary-following algorithm (Digital Image Processing by Gonzalez \u0026 Woods)](https://github.com/ArthurFDLR/Image_Boundaries_Analysis/blob/main/.github/bound_follow.PNG?raw=True)\n\nThe Freeman chain code is generally used as a characteristic to compare or detect shapes across images. Only the global figure is interesting. Fine details can thus be considered as noise regarding the overall shape. To avoid this issue, we can down-sample the boundary.\n\nWe are now all set to compute the chain code. The difference between coordinates of consecutive boundary pixel can be mapped to the directions shown bellow. The index of the following lists corresponds to the direction number of the relative coordinate (e.g. $(0,1) \\longrightarrow 0$ and $(-1,-1) \\longrightarrow 3$ for 8-directional encoding).\n\n$$ x_n = [0, -1, -1, -1,  0,  1, 1, 1] $$\n$$ y_n = [1,  1,  0, -1, -1, -1, 0, 1] $$\n\n![Direction numbers for 4-directional and 8-directional chain code (Digital Image Processing by Gonzalez \u0026 Woods)](https://github.com/ArthurFDLR/Image_Boundaries_Analysis/blob/main/.github/direction.PNG?raw=True)\n\n\n```python\ndef find_upper_left(img):\n    for i in range(1,img.shape[0]+1):\n        for j in range(i):\n            if img[i-j-1, j]:\n                return (i-j-1, j)\n    return None\n\ndef resample(img, grid_width, presence_threshold=0):\n    img_sub = np.zeros(np.array(img.shape)//grid_width)\n    img_sub_viz = np.zeros(img.shape)\n    gw_half = grid_width // 2\n    for x_sub, x in enumerate(range((img.shape[0]%grid_width)//2 + gw_half, img.shape[0], grid_width)):\n        for y_sub, y in enumerate(range((img.shape[1]%grid_width)//2 + gw_half, img.shape[1], grid_width)):\n            if img[x-gw_half:x+gw_half, y-gw_half:y+gw_half].sum() \u003e presence_threshold:\n                img_sub[x_sub,y_sub] = 1\n                img_sub_viz[x-1:x+2, y-1:y+2] = np.ones((3,3))\n    return img_sub, img_sub_viz\n\ndef boundary_following(border, stay_out=True, allow_diag=True):\n    \n    x_n = [0, -1, -1, -1,  0,  1, 1, 1] if allow_diag else [0, -1,  0, 1]\n    y_n = [1,  1,  0, -1, -1, -1, 0, 1] if allow_diag else [1,  0, -1, 0]\n    nbr_neighbore = len(x_n)\n    angle = nbr_neighbore//2\n    \n    def get_neighbor(p,a):\n        x, y = p[0] + x_n[a], p[1] + y_n[a]\n        if (0 \u003c= x \u003c border.shape[0]) and (0 \u003c= y \u003c border.shape[1]):\n            return border[x, y]\n        else: return None\n\n    b = find_upper_left(border)\n    b_init = False\n    coord_border = [b]\n    chain_code = []\n\n    while True:\n        # Revolve around b until hit border\n        while not get_neighbor(b, angle):\n            angle = (angle - 1) if angle else (nbr_neighbore - 1)\n        # Prefer direct neighbore\n        if (not stay_out) and allow_diag and (angle%2 == 1) \\\n            and get_neighbor(b, (angle - 1) if angle else 7):\n            angle = (angle - 1) if angle else (nbr_neighbore - 1)\n        # Update b \u003c- n(k)\n        b = (b[0] + x_n[angle], b[1] + y_n[angle])\n        # End condition: two successive boundary pixels already visited\n        if b_init:\n            if b == coord_border[1]: break\n            else: b_init = False\n        if b == coord_border[0]: b_init = True\n        # Store new border pixel\n        chain_code.append(angle)\n        coord_border.append(b)\n        # Reset angle, c \u003c- n(k−1)\n        angle = (angle+angle%2+2)%8 if allow_diag else (angle+1)%4\n    return np.array(coord_border), chain_code\n```\n\n\n```python\n# Image import\nimg = io.imread('https://github.com/ArthurFDLR/Image_Boundaries_Analysis/blob/main/test_images/circular_stroke.tif?raw=True')\n\n# Computation\nkernel_smoothing = np.ones((9,9),np.float32)/(9*9)\nimg_g = filter2D(img,-1,kernel_smoothing)\nimg_gB, _ = otsu_tresholding(img_g)\ngB_border, _ = boundary_following(img_gB)\nimg_gB_border = draw_border(gB_border, img.shape)\nimg_gB_border_resampled_4, sampled_viz_4 = resample(img_gB_border, 50, 0)\nimg_gB_border_resampled_8, sampled_viz_8 = resample(img_gB_border, 50, 25)\n\n# Visualization\nfig, axs = plt.subplots(2, 3, figsize=(11,8), dpi=60)\naxs = [ax for sub_ax in axs for ax in sub_ax]\nfor ax in axs:\n    ax.set_xticks([])\n    ax.set_yticks([])\n\naxs[0].set_title('Input image')\naxs[0].imshow(img, cmap='gray')\naxs[1].set_title('Smoothed image')\naxs[1].imshow(img_g, cmap='gray')\naxs[2].set_title('Thresholded image')\naxs[2].imshow(img_gB, cmap='gray')\naxs[3].set_title('Outer boundary')\naxs[3].imshow(img_gB_border, cmap='gray')\naxs[4].set_title('Subsampled 4-directional boundary')\naxs[4].imshow(sampled_viz_4, cmap='gray')\naxs[5].set_title('Subsampled 8-directional boundary')\naxs[5].imshow(sampled_viz_8, cmap='gray')\n\nfig.tight_layout()\n```\n\n\n    \n![png](./.github/images/Image_Boundaries_Analysis_18_0.png)\n    \n\n\n\n```python\ndef get_first_diff(chain_code):\n    nbr_direction = max(chain_code)+1\n    out = [(nbr_direction+chain_code[0]-chain_code[-1])%nbr_direction]\n    for i in range(len(chain_code)-1):\n        out.append((nbr_direction+chain_code[i+1]-chain_code[i])%nbr_direction)\n    return out \n\ndef get_magnitude(l):\n    out = 0\n    for i in l:\n        out = out * 10 + i\n    return out\n\ndef get_min_mag(chain_code):\n    mini = get_magnitude(chain_code)\n    mini_chain = chain_code\n    for i in range(1,len(chain_code)):\n        c = chain_code[i:] + chain_code[:i]\n        mag_c = get_magnitude(c)\n        if mag_c\u003cmini:\n            mini = mag_c\n            mini_chain = c\n    return mini_chain\n```\n\n\n```python\nprint('4-directional:')\n_, chain_code_4 = boundary_following(img_gB_border_resampled_4, stay_out=False, allow_diag=False)\nprint(\"Chain code:\\t\\t\", chain_code_4)\n\nchain_code_diff_4 = get_first_diff(chain_code_4)\nprint(\"First difference:\\t\", chain_code_diff_4)\n\nchain_code_diff_min_4 = get_min_mag(chain_code_diff_4)\nprint(\"Minimum magnitude:\\t\",chain_code_diff_min_4)\n\nprint('\\n8-directional:')\n_, chain_code_8 = boundary_following(img_gB_border_resampled_8, stay_out=False)\nprint(\"Chain code:\\t\\t\", chain_code_8)\n\nchain_code_diff_8 = get_first_diff(chain_code_8)\nprint(\"First difference:\\t\", chain_code_diff_8)\n\nchain_code_diff_min_8 = get_min_mag(chain_code_diff_8)\nprint(\"Minimum magnitude:\\t\",chain_code_diff_min_8)\n```\n\n    4-directional:\n    Chain code:\t\t [0, 1, 0, 1, 0, 0, 0, 3, 0, 3, 0, 3, 3, 3, 3, 3, 3, 2, 3, 3, 2, 2, 2, 2, 1, 2, 2, 1, 2, 1, 1, 1, 0, 1, 1, 1]\n    First difference:\t [3, 1, 3, 1, 3, 0, 0, 3, 1, 3, 1, 3, 0, 0, 0, 0, 0, 3, 1, 0, 3, 0, 0, 0, 3, 1, 0, 3, 1, 3, 0, 0, 3, 1, 0, 0]\n    Minimum magnitude:\t [0, 0, 0, 0, 0, 3, 1, 0, 3, 0, 0, 0, 3, 1, 0, 3, 1, 3, 0, 0, 3, 1, 0, 0, 3, 1, 3, 1, 3, 0, 0, 3, 1, 3, 1, 3]\n    \n    8-directional:\n    Chain code:\t\t [1, 1, 0, 0, 0, 7, 6, 0, 6, 6, 6, 6, 6, 6, 5, 5, 4, 4, 4, 3, 3, 4, 2, 2, 2, 1, 2, 2]\n    First difference:\t [7, 0, 7, 0, 0, 7, 7, 2, 6, 0, 0, 0, 0, 0, 7, 0, 7, 0, 0, 7, 0, 1, 6, 0, 0, 7, 1, 0]\n    Minimum magnitude:\t [0, 0, 0, 0, 0, 7, 0, 7, 0, 0, 7, 0, 1, 6, 0, 0, 7, 1, 0, 7, 0, 7, 0, 0, 7, 7, 2, 6]\n    \n\n# Fourier Descriptors\n\nThe Fourier Transform (FT) has proven to be extremely useful in computer vision and signal processing in general. Once again, we can take advantage of the frequency representation of the shape's boundary series. There is a couple of significant application associated to different intensity of low-pass frequency filter. First, we can reduce the quantity of data used to describe a boundary with virtually no resolution loss. Secondly, a strong low-pass filter can smooth the boundary to ease comparison.\n\nAt first sight, one can think about using a 2D FT to generate the frequency description of a boundary due to its visual nature. However, boundaries really are one-dimensional. Each coordinate pair can be treated as a complex number. Consider a boundary $\\left\\{\\left(x_i, y_i\\right), \\forall i \\in [\\![0, K-1]\\!] \\right\\}$,\n\n$$ s(k) = y(k) + j \\cdot x(k) $$\n\nThe classical one-dimensional Fourier Transform can be applied to this complex representation to obtain the Fourier descriptors $a(u)$ of the boundary. $\\forall u \\in [\\![0, K-1]\\!]$\n\n$$ a(u) = \\sum_{k=0}^{K-1} s(k) \\cdot e^{-j 2 \\pi \\frac{uk}{K}} $$\n\nThen, we can reconstruct the border using only a fraction of the available Fourier descriptors. Consider $P \u003c K$ descriptors, $\\forall p \\in [\\![0, P-1]\\!]$\n\n$$ \\widehat{s}(p) = \\frac{1}{K} \\sum_{u=0}^{P-1} a(u) \\cdot e^{j 2 \\pi \\frac{up}{K}} $$\n\n\n```python\ndef fourierdescp(s):\n    return np.fft.fft(s, len(s))\n\ndef ifourierdescp(a, P):\n    P = min(len(a), P)\n    mid = len(a) // 2\n    low  = mid - P // 2\n    high = mid + P // 2\n    a_trunc = np.zeros(a.shape, np.complex_)\n    a_trunc[low:high] = np.fft.fftshift(a)[low:high]\n    return np.fft.ifft(np.fft.ifftshift(a_trunc))\n```\n\n\n```python\n# Image import\nimg = io.imread('https://github.com/ArthurFDLR/Image_Boundaries_Analysis/blob/main/test_images/chromosome.tif?raw=True')\n\n# Computation\nborder_coord, _ = boundary_following(img)\nimg_border = draw_border(border_coord, img.shape)\nborder_complex = np.array([c[1] + 1j * c[0] for c in border_coord])\nfourier_descriptors = fourierdescp(border_complex)\nFD_size = fourier_descriptors.shape[0]\n\n# Vizualisation\nfig, axs = plt.subplots(2, 3, figsize=(10,8), dpi=60)\naxs_flat = [ax for sub_ax in axs for ax in sub_ax]\nfor ax in axs_flat:\n    ax.set_xticks([])\n    ax.set_yticks([])\n\naxs_flat[0].set_title('Input image')\naxs_flat[0].imshow(img, cmap='gray')\nfor i, compression in enumerate([1., .5, .1, .01, .005]):\n    border_complex_smoothed = ifourierdescp(fourier_descriptors, int(FD_size*compression))\n    axs_flat[i+1].set_title(r'$\\gamma = ${:.1%}'.format(compression))\n    axs_flat[i+1].imshow(draw_border(border_complex_smoothed, img.shape), cmap='gray')\n\nfig.tight_layout()\n```\n\n\n    \n![png](./.github/images/Image_Boundaries_Analysis_24_0.png)\n    \n\n\n\n```python\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farthurfdlr%2Fimage_boundaries_analysis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farthurfdlr%2Fimage_boundaries_analysis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farthurfdlr%2Fimage_boundaries_analysis/lists"}