{"id":22823131,"url":"https://github.com/jonclayden/mmand","last_synced_at":"2025-04-23T03:48:39.300Z","repository":{"id":3052380,"uuid":"4074016","full_name":"jonclayden/mmand","owner":"jonclayden","description":"Mathematical Morphology in Any Number of Dimensions","archived":false,"fork":false,"pushed_at":"2024-03-18T09:56:32.000Z","size":5527,"stargazers_count":39,"open_issues_count":0,"forks_count":8,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-04-23T03:48:33.341Z","etag":null,"topics":["image-processing","morphology","r","resampling"],"latest_commit_sha":null,"homepage":null,"language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jonclayden.png","metadata":{"files":{"readme":"README.Rmd","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2012-04-19T10:23:56.000Z","updated_at":"2025-04-18T16:22:05.000Z","dependencies_parsed_at":"2023-02-16T09:01:09.827Z","dependency_job_id":null,"html_url":"https://github.com/jonclayden/mmand","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonclayden%2Fmmand","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonclayden%2Fmmand/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonclayden%2Fmmand/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jonclayden%2Fmmand/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jonclayden","download_url":"https://codeload.github.com/jonclayden/mmand/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250366682,"owners_count":21418768,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["image-processing","morphology","r","resampling"],"created_at":"2024-12-12T16:14:20.199Z","updated_at":"2025-04-23T03:48:39.271Z","avatar_url":"https://github.com/jonclayden.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"```{r, echo=FALSE}\noptions(mmand.display.newDevice=FALSE)\nknitr::opts_chunk$set(collapse=TRUE, fig.path=\"tools/figures/\", fig.dim=c(4,4), dpi=128)\n```\n\n[![CRAN version](http://www.r-pkg.org/badges/version/mmand)](https://cran.r-project.org/package=mmand) [![CI](https://github.com/jonclayden/mmand/actions/workflows/ci.yaml/badge.svg)](https://github.com/jonclayden/mmand/actions/workflows/ci.yaml) [![Coverage Status](https://coveralls.io/repos/github/jonclayden/mmand/badge.svg?branch=master)](https://coveralls.io/github/jonclayden/mmand?branch=master) [![status](https://tinyverse.netlify.com/badge/mmand)](https://tinyverse.netlify.app)\n\n# Mathematical Morphology in Any Number of Dimensions\n\nThe `mmand` R package provides tools for performing mathematical morphology\noperations, such as erosion and dilation, or finding connected components, on\narrays of arbitrary dimensionality. It can also smooth and resample arrays,\nobtaining values between pixel centres or scaling the image up or down\nwholesale.\n\nAll of these operations are underpinned by three powerful functions, which\nperform different types of kernel-based operations: `morph()`, `components()`\nand `resample()`.\n\nAn additional function provides a multidimensional\n[distance transform](#the-distance-transform) operation.\n\n## Contents\n\n- [Test image](#test-image)\n- [Mathematical morphology](#mathematical-morphology)\n- [Greyscale morphology](#greyscale-morphology)\n- [Skeletonisation](#skeletonisation)\n- [Smoothing](#smoothing)\n- [Connected components](#connected-components)\n- [The distance transform](#the-distance-transform)\n- [Resampling](#resampling)\n\n## Test image\n\nA test image of a jet engine fan is available within the package, and will be\nused for demonstration below. It can be read in and displayed using the code\n\n```{r, fan}\nlibrary(mmand)\nlibrary(loder)\n\nfan \u003c- readPng(system.file(\"images\", \"fan.png\", package=\"mmand\"))\ndisplay(fan)\n```\n\nHere we are using the [loder](https://github.com/jonclayden/loder) package to\nread the PNG file.\n\n## Mathematical morphology\n\n[Mathematical morphology](https://en.wikipedia.org/wiki/Mathematical_morphology) \nis an image processing technique that can be used to emphasise or remove \ncertain types of features from binary or greyscale images. It is classically \nperformed on two-dimensional images, but can be useful in three or more \ndimensions, as, for example, in medical image analysis. The `mmand` package can \nwork on R arrays of any dimensionality, including one-dimensional vectors.\n\nThe basic operations in mathematical morphology are *erosion* and *dilation*. A \nsimple one-dimensional example serves to illustrate their effects:\n\n```{r}\nx \u003c- c(0,0,1,0,0,0,1,1,1,0,0)\nk \u003c- c(1,1,1)\nerode(x,k)\ndilate(x,k)\n```\n\nThe `erode()` function \"thins out\" areas in the input vector, `x`, which were \n\"on\" (i.e. set to 1), to the point where the first of these areas \"disappears\" \nentirely. Conversely, the `dilate()` function expands these regions into \nneighbouring pixels.\n\nThe vector `k` here is called the *kernel* or *structuring element*. It \neffectively controls the region of influence of the operation when it is \napplied to each value.\n\nDerived from these basic operations are the *opening* and *closing* functions. \nThese apply both basic operations, using the same kernel, but in different \norders: an opening is an erosion followed by a dilation, whereas a closing is a \ndilation followed by an opening.\n\n```{r}\nopening(x,k)\nclosing(x,k)\n```\n\nNotice that, in this case, the closing gets us back to where we started, \nwhereas the opening does not. This is because the initial erosion operation \nremoves the first \"on\" block entirely, so it cannot be recovered by the \nsubsequent dilation. Hence, the effect is to remove small features that are\nnarrower than the kernel.\n\n## Greyscale morphology\n\nMathematical morphology is not limited to binary data. When generalised to \ngreyscale images, erosion replaces each nonzero pixel with the minimum value \nwithin the kernel when it is centred at that pixel, and dilation uses the \nmaximum. For example,\n\n```{r}\nx \u003c- c(0,0,0.5,0,0,0,0.2,0.5,0.3,0,0)\nerode(x,k)\n```\n\nNotice that the remaining nonzero value is now reduced from 0.5 to 0.2, the \nminimum value across the original pixel and its neighbours on either side. With \na wider kernel, its final value would have dropped to zero.\n\nThe effect is more intuitively demonstrated on a real two-dimensional image:\n\n```{r, fan-eroded}\nk \u003c- shapeKernel(c(3,3), type=\"diamond\")\ndisplay(erode(fan, k))\n```\n\nNotice that darker areas appear enlarged. In this case the kernel is itself a \n2D array (or matrix), and unlike the 1D case there is a choice of plausible \nshapes for a particular width. The `shapeKernel()` function will create box, \ndisc and diamond shaped kernels, and their higher-dimensional equivalents.\n\nUsing a wider kernel exaggerates the effect:\n\n```{r, fan-eroded77}\nk \u003c- shapeKernel(c(7,7), type=\"diamond\")\ndisplay(erode(fan, k))\n```\n\nIn the case above, the diamond shape of the kernel is obvious in areas where \nthe kernel is larger than the eroded features.\n\nNote that the kernel may also be anisotropic, i.e. it may have a different \nwidth in each dimension:\n\n```{r, fan-eroded73}\nk \u003c- shapeKernel(c(7,3), type=\"diamond\")\ndisplay(erode(fan, k))\n```\n\nThe effect of dilation is complementary, shrinking dark regions and enlarging \nbright ones:\n\n```{r, fan-dilated}\nk \u003c- shapeKernel(c(3,3), type=\"diamond\")\ndisplay(dilate(fan, k))\n```\n\nIn this case the low-intensity and narrow handwriting towards the middle of the \nfan has all but disappeared.\n\nThe basic operations can be combined together for other useful purposes. For \nexample, the difference between the dilated and eroded versions of an image, \nknown as the *morphological gradient*, can be used to show up edges between \nareas of light and dark.\n\n```{r, fan-gradient}\nk \u003c- shapeKernel(c(3,3), type=\"diamond\")\ndisplay(dilate(fan,k) - erode(fan,k))\n```\n\nThe [Sobel filter](https://en.wikipedia.org/wiki/Sobel_operator) has a similar\neffect.\n\n```{r, fan-sobel}\ndisplay(sobelFilter(fan))\n```\n\n## Skeletonisation\n\n[Topological skeletonisation](https://en.wikipedia.org/wiki/Topological_skeleton)\nis the process of thinning a shape to a medial line or surface representing the\napproximate path of the original. It can be thought of as the result of\nrepeated erosion, up to the point where only a \"core\" of the shape exists. It\nis usually applied to binary data.\n\nAs of version 1.5.0, the `mmand` package offers three different skeletonisation\nalgorithms, with different advantages and limitations. (Please see the\ndocumentation at `?skeletonise` for details.) Below we see the results of\napplying each of them in turn to the outline of a capital letter B.\n\n```{r, skeletonisation, fig.keep=\"none\"}\nlibrary(loder)\nB \u003c- readPng(system.file(\"images\", \"B.png\", package=\"mmand\"))\nk \u003c- shapeKernel(c(3,3), type=\"diamond\")\n\ndisplay(B)\ndisplay(skeletonise(B,k,method=\"lantuejoul\"), col=\"red\", add=TRUE)\n```\n\n![Skeletonised capital B](tools/figures/skeletonisation.png)\n\nThe B is shown alone first, and then with the three skeletons overlaid in red.\n(Only the code for generating the first is shown.) Notice that all three\nskeletons are reasonable medial paths, but the centre one (using Beucher's\nformula) is a little thicker than the others in most places, and only the right\none (using the hit-or-miss transform) is fully self-connected.\n\n## Smoothing\n\nA loosely related operation is [kernel-based \nsmoothing](https://en.wikipedia.org/wiki/Kernel_smoother), often used to\nameliorate noise. In this case the kernel is used as a set of coefficients, \nwhich are multiplied by data within the neighbourhood of each pixel and added \ntogether. For these purposes the kernel should usually be normalised, so that \nits values add up to one.\n\nGaussian smoothing is a typical example, wherein our coefficients are given by \nthe probability densities of a Gaussian distribution centred at the middle of \nthe kernel. The `mmand` package provides the `gaussianSmooth()` function for \nperforming this operation. Below we can see an example in one dimension, where \nwe create some noisy data and then approximately recover the underlying cosine \nfunction by applying smoothing.\n\n```{r, cos-smoothed}\nx \u003c- seq(0, 4*pi, pi/64)\ny \u003c- cos(x) + runif(length(x),-0.2,0.2)\ny_smoothed \u003c- gaussianSmooth(y, 6)\n\nplot(x, y, pch=19, col=\"grey50\", xaxt=\"n\")\naxis(1, (0:4)*pi, expression(0,pi,2*pi,3*pi,4*pi))\nlines(x, y_smoothed, lwd=2)\n```\n\nIt should be borne in mind that the second argument to the `gaussianSmooth()` \nfunction is the *standard deviation* of the smoothing Gaussian kernel in each \ndimension, rather than the kernel size.\n\nOn our two-dimensional test image, which contains no appreciable noise, the \neffect is to blur the picture. Indeed, this operation is sometimes called \n[Gaussian blurring](https://en.wikipedia.org/wiki/Gaussian_blur).\n\n```{r, fan-smoothed}\ndisplay(gaussianSmooth(fan, c(3,3)))\n```\n\nAn alternative approach to noise reduction is\n[median filtering](https://en.wikipedia.org/wiki/Median_filter), and `mmand`\nprovides another function for this purpose:\n\n```{r, fan-median-filtered}\nk \u003c- shapeKernel(c(3,3), type=\"box\")\ndisplay(medianFilter(fan, k))\n```\n\nThis method is typically better at preserving edges in the image, which can be \ndesirable in some applications.\n\n## Connected components\n\nEvery operation described so far has been based on `mmand`'s flexible `morph()` \nfunction, which uses a kernel represented by an array to select pixels of \ninterest in morphing the image, optionally using the kernel's elements to \nadjust their values, and then applying a merge operation of some sort (sum, \nminimum, maximum, median, etc.) to produce the pixel value in the final image. \nIn every case the result has the same size as the original data array.\n\nIn the next section we will examine operations that change the array's\ndimensions, but first we consider another useful operation: finding connected\ncomponents. This is the task of assigning a label to each contiguous subregion\nof an array.\n\nTo demonstrate, we start by first thresholding the fan image using *k*-means\nclustering (with *k*=2). The package's `threshold()` function can be used for\nthis:\n\n```{r, fan-thresholded}\nfan_thresholded \u003c- threshold(fan, method=\"kmeans\")\ndisplay(fan_thresholded)\n```\n\nWe can then find the connected components. In this case the kernel determines\nwhich pixels are deemed to be neighbours. For example,\n\n```{r}\nk \u003c- shapeKernel(c(3,3), type=\"box\")\nfan_components \u003c- components(fan_thresholded, k)\n```\n\nNow we can visualise the result by assigning a colour to each component.\n\n```{r, fan-components}\ndisplay(fan_components, col=rainbow(max(fan_components,na.rm=TRUE)))\n```\n\nAs we might expect, the largest components—which label only the \"on\" areas of\nthe image—correspond to (most of) the ring of fan blades, and the bright part\nof the central hub.\n\nThis is can be a useful tool for \"segmentation\", or dividing an image into\ncoherent areas.\n\n## The distance transform\n\nA useful operation in certain contexts is the distance transform, which\ncalculates the distance from each pixel to a region of interest. There are\nsigned an unsigned variants, with the former also calculating the distance\nto the boundary within the region of interest itself. We can use the\nthresholded image from above to illustrate the point:\n\n```{r, fan-distance}\ndisplay(distanceTransform(fan_thresholded))\ndisplay(abs(distanceTransform(fan_thresholded, signed=TRUE)))\n```\n\nWe take the absolute value of the signed transform here for ease of visual\ninterpretation. Notice how, in both cases, bright \"ridges\" in the transformed\nimage correspond to midlines at maximal distance from the boundary between\nforeground and background. Philip Rideout\n[provides a detailed explanation](https://prideout.net/blog/distance_fields/)\nof the algorithm and its uses.\n\n## Resampling\n\nThe final category of problems that `mmand` can solve uses a different type of\nkernel to resample an image at arbitrary points, or on a new grid. This allows \nimages to be resized, or arrays to be indexed using non-integer indices. The \nkernels in these cases are functions, which provide coefficients for using data \nat any distance from the new pixel location to determine its value.\n\nLet's use an example to illustrate this. Consider the simple vector\n\n```{r}\nx \u003c- c(0,0,1,0,0)\n```\n\nIts second element is 0, and its third element is 1, but what is its value at \nindex 2.5? One answer is that it simply doesn't have one, but if these were \nsamples from a fundamentally continuous source, then there is conceptually a \nvalue everywhere. We just didn't capture it. Our best guess would have to be \nthat it is either 0 or 1, or something in between. If we try to use 2.5 as an \nindex we get the value 0:\n\n```{r}\nx[2.5]\n```\n\n(R simply truncates 2.5 to 2 and returns element 2.) The `resample()` function \nprovides a set of alternatives:\n\n```{r}\nresample(x, 2.5, triangleKernel())\n```\n\nNow we obtain the value 0.5, which does not appear anywhere in the original, \nbut it is the average of the values at locations 2 and 3. In this case, \ntherefore, `resample()` is performing [linear \ninterpolation](https://en.wikipedia.org/wiki/Linear_interpolation).\n\nThe triangle kernel is just one possible function for interpolating the data. \nAnother option is the box kernel, generated by `boxKernel()`, which simply \nreturns the \"nearest\" value in the original data. Yet another option is \nprovided by `mitchellNetravaliKernel()`, or `mnKernel()` for short, which \nprovides a family of cubic spline kernels [proposed by Mitchell and \nNetravali](https://dl.acm.org/doi/10.1145/378456.378514). We can see the profile\nof any of these kernels by plotting them:\n\n```{r, mn-profile}\nplot(mitchellNetravaliKernel(1/3, 1/3))\n```\n\nIn higher dimensions, the resampled point locations can be passed to \n`resample()` either as a matrix giving the points to sample at, one per row, or \nas a list giving the locations on each axis, which will be made into a grid.\n\nA common use for resampling is to scale an image up or down. The `rescale()` \nfunction is a convenience wrapper around `resample()` for scaling an array by a \ngiven scale factor. Here, we can use it to scale a smaller version of the fan \nimage up to the size of the larger version:\n\n```{r, fan-scaled}\nlibrary(loder)\n\nfan_small \u003c- readPng(system.file(\"images\", \"fan-small.png\", package=\"mmand\"))\ndim(fan_small)\n\ndisplay(rescale(drop(fan_small), 4, mnKernel()))\n```\n\nThe scaled-up image of course has less detail than the original 512x512 pixel \nversion, since it is based on 16 times fewer data points, but the general \nfeatures of the fan are perfectly discernible.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonclayden%2Fmmand","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjonclayden%2Fmmand","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjonclayden%2Fmmand/lists"}