{"id":45744327,"url":"https://github.com/neutrinoceros/ahe","last_synced_at":"2026-02-25T15:47:02.255Z","repository":{"id":333348349,"uuid":"1136895351","full_name":"neutrinoceros/ahe","owner":"neutrinoceros","description":"(Adaptive) Histogram Equalization Python library, written in Rust","archived":false,"fork":false,"pushed_at":"2026-01-25T17:45:50.000Z","size":214,"stargazers_count":1,"open_issues_count":14,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-26T06:48:37.292Z","etag":null,"topics":["histogram-equalization","image-processing","vizualisation"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/neutrinoceros.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-18T14:59:03.000Z","updated_at":"2026-01-25T16:00:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/neutrinoceros/ahe","commit_stats":null,"previous_names":["neutrinoceros/ahe"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/neutrinoceros/ahe","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neutrinoceros%2Fahe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neutrinoceros%2Fahe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neutrinoceros%2Fahe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neutrinoceros%2Fahe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/neutrinoceros","download_url":"https://codeload.github.com/neutrinoceros/ahe/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/neutrinoceros%2Fahe/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29829154,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-25T15:41:19.027Z","status":"ssl_error","status_checked_at":"2026-02-25T15:40:47.150Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["histogram-equalization","image-processing","vizualisation"],"created_at":"2026-02-25T15:47:01.473Z","updated_at":"2026-02-25T15:47:02.245Z","avatar_url":"https://github.com/neutrinoceros.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `ahe`\n[![PyPI](https://img.shields.io/pypi/v/ahe.svg?logo=pypi\u0026logoColor=white\u0026label=PyPI)](https://pypi.org/project/ahe/)\n[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)\n\n\nA minimalist Python library for (Contrast Limited) (Adaptive) Histogram Equalization,\ncombining the expressiveness of a user-friendly Python interface with the raw power of\na low-level implementation.\n\n\u003c!-- TOC --\u003e\n\n- [Development status](#development-status)\n- [Installation](#installation)\n- [Usage](#usage)\n    - [Simple Histogram Equalization](#simple-histogram-equalization)\n    - [Adaptive Histogram equalization AHE](#adaptive-histogram-equalization-ahe)\n        - [Prioritizing accuracy: sliding-tile](#prioritizing-accuracy-sliding-tile)\n        - [Prioritizing performance: tile-interpolation](#prioritizing-performance-tile-interpolation)\n    - [General rules for tiling schemes](#general-rules-for-tiling-schemes)\n- [Migrating from scikit-image](#migrating-from-scikit-image)\n    - [TL;DR](#tldr)\n    - [Disclaimer: missing features](#disclaimer-missing-features)\n    - [Dependency Minimalism](#dependency-minimalism)\n    - [Better performance](#better-performance)\n    - [Interface Consistency](#interface-consistency)\n    - [Conservation of transformation invariants](#conservation-of-transformation-invariants)\n    - [Additional features](#additional-features)\n    - [Migration Guide](#migration-guide)\n        - [HE](#he)\n        - [AHE with implicit kernel size](#ahe-with-implicit-kernel-size)\n        - [CLAHE with explicit kernel size](#clahe-with-explicit-kernel-size)\n- [References](#references)\n\n\u003c!-- /TOC --\u003e\n\n## Development status\n\n`ahe` is currently in alpha. Fundational features are available, and expected to be\nstable, but the software as a whole may not be feature complete yet.\n\n## Installation\n\n```\n$ python -m pip install ahe\n```\n\n## Usage\n\n### Simple Histogram Equalization\n\nWe'll start by defining an image composed of noise.\n\n```python\nimport ahe\nimport numpy as np\n\nimage_shape = (128, 128)\nprng = np.random.default_rng(0)\nimage = np.clip(\n    prng.normal(\n        loc=0.5,\n        scale=0.25,\n        size=np.prod(image_shape),\n    ).reshape(image_shape),\n    a_min=0.0,\n    a_max=1.0,\n)\n```\n\nNon-adaptive histogram equalization is performed as follow\n```python\nimage_eq = ahe.equalize_histogram(image)\n```\nThis method is least expensive in terms of strain put on hardware resources.\nHowever, as a single histogram is computed and adjusted over the entire image,\nthis technique is known to amplify noise in near-uniform regions.\n\n### Adaptive Histogram equalization (AHE)\nAdaptive Histogram Equalization (AHE) was designed to overcome this limitation by\ninstead computing more localized (and numerous) histograms, improving the *local*\ncontrast in all regions, at the cost of a reduced efficiency.\nAs illustrated in the following, there are two main variants of AHE, sliding-tile and\ntile-interpolation. As the names suggest, both methods rely on the use of of tiles,\nalso known as *contextual regions*, that define sub-domains in which different histograms\nare computed and applied.\n\n#### Prioritizing accuracy: sliding-tile\n\nTrue AHE is intrinsically an expensive operation to perform, as it requires computing\na different histogram *per pixel*. One efficient (although still costly) way to\naccomplish this, originally proposed by [Pizer et al. (1987)](#references), reduces the\nredundancy in intermediate computations and is known as the sliding-tile variant of AHE.\nHere's how to use it in `ahe`\n\n```python\nimage_eq = ahe.equalize_histogram(\n    image,\n    adaptive_strategy={\n        'kind': 'sliding-tile',\n        'tile-size': 15,\n    },\n)\n```\n\n\u003e [!NOTE]\n\u003e This strategy requires odd-sized tile shapes, but supports\n\u003e image shapes with any parity.\n\nWhile an exact implementation of AHE, this option remains resource-demanding and is\nnot recommended for production.\n\n#### Prioritizing performance: tile-interpolation\n\nAlternatively, very similar results can be obtained at a fraction of the cost using\nan approximative method known as the tile-interpolation variant of AHE, also\nintroduced by [Pizer et al. (1987)](#references).\nIn this method, an image is split into equal-sized sub domains (tiles), which may be\nspecified either from a tile size\n\n```python\n\nimage_eq = ahe.equalize_histogram(\n    image,\n    adaptive_strategy={\n        'kind': 'tile-interpolation',\n        'tile-size': 16,\n    },\n)\n```\n\nor as a number of tiles to split the domain into (in each direction)\n```python\nimage_eq = equalize_histogram(\n    image,\n    adaptive_strategy={\n        'kind': 'tile-interpolation',\n        'tile-into': 8,\n    },\n)\n```\n\n\u003e [!NOTE]\n\u003e This strategy requires even-sized tile and image shapes.\n\n### General rules for tiling schemes\n\nIn all AHE strategies, all tiles created will be the exact same size, regardless of the\npixel's relative position in the image. The whole domain is generally padded internally\nin order to respect this rule. The exact method used for padding is controlled by the\n`boundaries` keyword argument.\n\nBoth `'tile-size'` and `'tile-into'` will accept either a shape as a pair of integers\n`(n, m)`, or a single integer `n`, which is a shorthand for `(n, n)`, as illustrated\nabove.\n\n\n## Migrating from `scikit-image`\n\n### TL;DR\nPut simply, if all your project needs from `scikit-image` is\n`skimage.exposure.equalize_(adapt)hist`, `ahe` provides a faster, more lightweight and\nportable replacement. The following contains a much more detailed explanation of the\ndifferences. You can also jump to the final [Migration Guide](#migration-guide).\n\n### Disclaimer: missing features\nThe following features from `skimage.exposure.equalize_(adapt)hist` are currently\nmissing from `ahe`:\n- [masking](https://github.com/neutrinoceros/ahe/issues/51)\n- multi-channel images objects (from `PIL`) or arrays. The expectation is that this\n  should be easy to re-implement on the user side.\n- higher dimensionality: `ahe` currently only supports 2D input and outputs. The\n  algorithms can be generalized to work in any dimensionality. Please open an issue\n\nSome, though not all of the above are already planned. Don't hesitate to request\nthe others (or anything else that could be in scope) by opening an issue.\n\n### Dependency Minimalism\n`ahe` has no runtime dependencies beyond `numpy`. Additionally, its binaries are\norders of magnitude lighter than `scikit-image`'s, as well as future-compatible\nwith yet-unreleased versions of Python.\n\n\u003c!-- Generated with `uv run scripts/doc_graphs.py` --\u003e\n\u003cp align=\"center\"\u003e\n  \u003cpicture align=\"center\"\u003e\n    \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/neutrinoceros/ahe/main/assets/wheel-size-dark.svg\" width=\"900\"\u003e\n    \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/neutrinoceros/ahe/main/assets/wheel-size-light.svg\" width=\"900\"\u003e\n    \u003cimg alt=\"Shows a bar chart comparing wheel sizes\" src=\"https://raw.githubusercontent.com/neutrinoceros/ahe/main/assets/wheel-size-light.svg\" width=\"900\"\u003e\n  \u003c/picture\u003e\n\u003c/p\u003e\n\n(*: `numpy` itself, as the common dependency to `ahe` and `scikit-image`, is\nexcluded from this graph)\n\n### Better performance\n\u003c!-- Generated with `uv run scripts/doc_graphs.py` --\u003e\n\u003cp align=\"center\"\u003e\n  \u003cpicture align=\"center\"\u003e\n    \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/neutrinoceros/ahe/main/assets/benchmark-dark.svg\" width=\"900\"\u003e\n    \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/neutrinoceros/ahe/main/assets/benchmark-light.svg\" width=\"900\"\u003e\n    \u003cimg alt=\"Shows a bar chart comparing wheel sizes\" src=\"https://raw.githubusercontent.com/neutrinoceros/ahe/main/assets/benchmark-light.svg\" width=\"900\"\u003e\n  \u003c/picture\u003e\n\u003c/p\u003e\n\n\n### Interface Consistency\n`scikit-image`'s implementation of histogram equalization methods are exposed as two\ndifferent functions: `skimage.exposure.equalize_hist` and\n`skimage.exposure.equalize_adapthist`. Only the former supports masking, and only the\nlatter supports clipping. `ahe.equalize_histogram` provides a consistent feature set,\nindependent of the adaptive strategy selected, or lack thereof.\n\nFurthermore, implicit, default behavior can be hard to reproduce explicitly. For\ninstance, `equalize_adapthist` will, by default, create tiles by dividing the image in 8\nalong each direction, but only exposes a `kernel_size` argument to override this; as a\nresult, one needs to re-implement division logic if they need something very similar to\nthe default, but with any other value than 8.\n\nIn stark contrast, `ahe.equalize_histogram`'s `adaptive_strategy` argument supports all\nthese applications:\n- `adaptive_strategy=None` (default) corresponds to `skimage.exposure.equalize_hist`\n- `adaptive_strategy={'kind': 'tile-interpolation', 'tile-into': 8}` corresponds to\n  `skimage.exposure.equalize_adapthist`'s default, but the exact divisor(s) used can\n   easily be adjusted\n- `adaptive_strategy={'kind': 'tile-interpolation', 'tile-size': 64}` is akin to using\n  `skimage.exposure.equalize_adapthist`'s `kernel_size` argument.\n\nLast but not least, `equalize_hist` does not support contrast limitation, while\n`equalize_adapthist` enables it by default (`clip_limit` defaults to `0.01`), and\nthings get messy when you actually want to *disable* it:\n- `clip_limit`'s name is not descriptive and only makes sense if you already know what\n  it does under the hood. `ahe`'s equivalent parameter is named\n  `max_normalized_bincount`, which is more verbose, but also more explicit about what\n  the number represents.\n- `clip_limit` (a.k.a `max_normalized_bincount`) is effectively a fraction; only values\n  within the open interval `]0.0, 1.0]` are meaningful, *but* `clip_limit=0.0` is\n  *allowed*, and effectively disable all clipping, which means it's equivalent to `1.0`,\n  further mystifying the underlying behavior and meaning of the parameter. Another\n  way to phrase this is that the results change discontinuously at `0.0`, which is very\n  close to the default value *and* should be easy to reason about.\n- results for `clip_limit=1.0` (*or* `0.0`, as we just saw), are actually *incorrect* at\n  a level that is visible to the naked eye (aliasing may be prominent). In comparison,\n  `ahe` does not enable contrast limitation by default: such flagrant defects would be\n  immediately visible in tests.\n\n### Conservation of transformation invariants\n`ahe.equalize_histogram` also provides stricter guarantees regarding the\ntransformation's geometric invariants.\nOutputs are guaranteed to be invariant (to machine precision) to left/right and top/\nbottom symmetries. In contrast, `skimage.exposure.equalize_adapthist`'s outputs are\nsubject to biases on, because it does not enforce symmetry in its internal tiling scheme\n(as of `scikit-image` `v0.26.0`). This improved tiling scheme comes at the cost of\nstricter requirements in `ahe.equalize_histogram`: the tile-interpolation strategy only\nsupports tiles and images with even sizes in both directions.\n\n\n### Additional features\n`ahe.equalize_histogram` supports more tiling scheme than\n`skimage.exposure.equalize_hist` and `skimage.exposure.equalize_adapthist` combined,\n within a consistent interface and a unified feature set.\nIn particular, it offers an exact implementation of Adaptive Histogram Equalization\nimplemented as a sliding-tile, while `skimage.exposure.equalize_adapthist` only\nsupports tile-interpolation (*also* available in `ahe`), which is generally faster,\nbut also a less accurate approximation of a true AHE.\n\n`ahe.equalize_histogram` also supports periodic boundary conditions, which can be\nspecified as `boundaries='periodic'`.\n\n\n### Migration Guide\nThis section provides some practical examples of `scikit-image` applications and their\nequivalent in `ahe`.\n\nNotes\n- in `ahe`, the default `nbins` is *generally* aligned with `scikit-image`'s (256),\n  except for kernels (or tiles) spanning less than 256 pixels. For maximu compatibility,\n  specifying an explicit value is recommended.\n- `ahe.equalize_histogram`'s `max_normalized_bincount` represents the same parameter as\n  `skimage.exposure.equalize_adapthist`'s `clip_limit`, but their default values differ:\n  `max_normalized_bincount` defaults to `1.0` (no contrast limitation), while\n  `scikit-image`'s `clip_limit` default to `0.01`\n- \"kernel\", \"tile\" and \"contextual region\" are different names for the same concept.\n\n#### HE\n```python\nfrom skimage.exposure import equalize_hist\n\nresult = equalize_hist(array)\n\n# becomes\nimport ahe\n\nresult = ahe.equalize_histogram(array, nbins=256)\n```\n\n#### AHE with implicit kernel size\n```python\nfrom skimage.exposure import equalize_adapthist\n\nresult = equalize_adapthist(array, clip_limit=1.0)\n\n# becomes\nimport ahe\n\nresult = ahe.equalize_histogram(\n    array,\n    nbins=256,\n    adaptive_strategy={\n      \"kind\": \"tile-interpolation\",\n      \"tile-into\": 8, # or (8, 8)\n    },\n)\n```\n\n#### CLAHE with explicit kernel size\n```python\nfrom skimage.exposure import equalize_adapthist\n\nresult = equalize_adapthist(array, kernel_size=64)\n\n# becomes\nimport ahe\n\nresult = ahe.equalize_histogram(\n    array,\n    nbins=256,\n    adaptive_strategy={\n      \"kind\": \"tile-interpolation\",\n      \"tile-size\": 64, # or (64, 64)\n    },\n    max_normalized_bincount=0.01,\n)\n```\n\n## References\n\n1. Pizer, Stephen M. et al. (1987). Adaptive Histogram Equalization and Its Variations.\n  *Compute Vizion, Graphics, and Image Processing*, 39, 355-368\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fneutrinoceros%2Fahe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fneutrinoceros%2Fahe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fneutrinoceros%2Fahe/lists"}