hairtone
Portrait hair re-colouring with a UNION face-parsing mask and LAB colour transfer.
hairtone photo.jpg blue --out blue.jpg
hairtone photo.jpg all --out out_dir/
photo.jpg ──► SegFormer + BiSeNet ──► 3-zone soft mask ──► LAB Reinhard
(confident hair) (core/mid/edge) (+ warm-shift)
│
▼
photo_blue.jpg
⚠️ License of the default SegFormer weights: non-commercial research and educational use only. Downloading the default
jonathandinu/face-parsingcheckpoint from HuggingFace binds you to that restriction (the model was fine-tuned on CelebAMask-HQ, which is research-only). Commercial users must either retrain on a permissive dataset or swap in another backend via theHairtoneBackendprotocol. The hairtone code is MIT.
What makes it different
Most hair-recolour scripts either (a) paint a flat colour inside a hard
segmentation mask, producing visible edges on fly-aways and skin spill,
or (b) run a full diffusion model and burn 10 s / 12 GB of VRAM per image.
hairtone sits in the middle:
- UNION mask. SegFormer (
jonathandinu/face-parsing) and a BiSeNet trained on CelebAMask-HQ are combined with a per-pixel max. When one network misses the top-left hair strand, the other usually catches it. - Three-zone soft mask. Confident core pixels are trusted, mid pixels are gently filtered by colour distance, edge pixels are only kept if their LAB distance to the core hair colour is small. This turns the binary segmentation into a matte that handles fly-aways.
- Region growing. Pixels near the confident seed that match the core hair chroma get added back — covers strands the segmentation networks cut off.
- Warm-shift colour transfer. Instead of fighting mask spill onto skin
by eroding the mask (which ruins the hairline),
hairtoneshifts the effective target colour toward warm skin tones on skin-like pixels. Blue hair on a skin spill ends up as "shadow" rather than "dyed cheek". - CPU friendly. The critical path is OpenCV + NumPy. A GPU is only useful for the SegFormer / BiSeNet forward passes, and SegFormer alone runs in seconds on modern CPUs.
The algorithm is fully deterministic — same image + same preset + same weights ⇒ same output.
Install
pip install hairtone # CLI + numpy + OpenCV + Pillow only
pip install "hairtone[torch]" # + torch + transformers (recommended)
Model weights
hairtone does not bundle model weights. On first run, HuggingFace
transformers downloads the SegFormer weights (~340 MB) and caches them
under $HF_HOME (default ~/.cache/huggingface).
The optional BiSeNet pass uses the vendored architecture
(hairtone._vendor.bisenet, MIT from
zllrunning/face-parsing.PyTorch).
Download the CelebAMask-HQ .pth checkpoint from the same upstream and
point --bisenet-weights at it:
hairtone photo.jpg blue \
--out photo_blue.jpg \
--bisenet-weights ./79999_iter.pth
ℹ️ Only
torch.save(net.state_dict(), PATH)-style checkpoints are accepted — hairtone refuses pickled Python objects to prevent arbitrary code execution during loading (torch.load(..., weights_only=True)).
Without --bisenet-weights the pipeline still works — it just falls back
to SegFormer-only segmentation, which is slightly less aggressive on
fly-away hair strands.
Python API
import cv2
from hairtone import recolor_image, TorchSegFormerBiSeNetBackend
backend = TorchSegFormerBiSeNetBackend(bisenet_weights=None) # SegFormer only
bgr = cv2.imread("photo.jpg")
out = recolor_image(bgr, "blue", backend=backend, strength=0.85)
cv2.imwrite("photo_blue.jpg", out)
Custom backend? Implement the HairtoneBackend protocol:
from hairtone.backend import HairtoneBackend, SegmentationResult
class MyBackend:
def segment(self, bgr) -> SegmentationResult:
...
and pass it to recolor_image(..., backend=MyBackend()). The colour
transfer, mask builder, and CLI work unchanged.
⚠️ Trust model.
HairtoneBackendimplementations run as normal Python code in your process. Only load backends you wrote or trust. SeeKNOWN_LIMITATIONS.md.
Presets
Run hairtone --list-presets for the full list with hex references. The
19 presets cover:
- blondes —
blonde,honey,strawberry - pinks & pastels —
pastel_pink,pink,coral,lavender,mint - primaries —
red,orange,blue,green,purple - jewel tones —
hotpink,cyan,teal,turquoise - metallics —
silver,ash
Each preset is a LAB tuple; you can define your own:
from hairtone.presets import Preset
emerald = Preset("emerald", "Emerald", lab=(155, 96, 165), hex_reference="#2d8a55")
CLI
hairtone [-h] [--out OUT] [--strength STRENGTH] [--jpeg-quality JPEG_QUALITY]
[--bisenet-weights BISENET_WEIGHTS] [--bisenet-module BISENET_MODULE]
[--list-presets] [--quiet] [--version]
src PRESET
hairtone photo.jpg blue— writesphoto_blue.jpgnext to the source.hairtone photo.jpg all --out out_dir/— writes every preset.hairtone photo.jpg blue --out blue.png --strength 0.7— lower blend.hairtone --list-presets— prints every preset key, name, and hex.
Project layout
src/hairtone/
__init__.py # public API
backend.py # HairtoneBackend Protocol + SegmentationResult
torch_backend.py # SegFormer + (optional) BiSeNet reference impl
masks.py # 3-zone mask + region grow + skin suppression
recolor.py # LAB Reinhard with warm-shift
pipeline.py # recolor_image / recolor_file entry points
presets.py # 19 named LAB presets
cli.py # hairtone console script
_vendor/bisenet/ # zllrunning BiSeNet architecture (MIT, vendored)
licenses/ # third-party license notices
tests/ # stub-backend tests, no weights needed
Development
git clone https://github.com/hinanohart/hairtone
cd hairtone
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,torch]"
pytest
ruff check .
mypy src
Tests use a stub backend and do not download any weights, so CI is fast and offline-safe.
Citations
If you use hairtone in academic work, please cite the underlying methods:
- Reinhard et al. 2001 — Color Transfer between Images. IEEE Computer Graphics and Applications.
- Xie et al. 2021 — SegFormer: Simple and Efficient Design for Semantic Segmentation with Transformers. arXiv:2105.15203.
- Yu et al. 2018 — BiSeNet: Bilateral Segmentation Network for Real-time Semantic Segmentation. arXiv:1808.00897.
- Lee et al. 2019 — MaskGAN: Towards Diverse and Interactive Facial Image Manipulation (CelebAMask-HQ dataset). CVPR 2020.
Verification (sigstore)
Releases from v_next_ (released after 2026-05-16) include a sigstore keyless signature bundle
(.sigstore per artifact) attached to the GitHub Release.
Verify a PyPI install
pip download <pkg-name>==<version> --no-deps -d ./verify
python -m sigstore verify github \
--cert-identity 'https://github.com/hinanohart/hairtone/.github/workflows/release.yml@refs/tags/v<version>' \
--cert-oidc-issuer 'https://token.actions.githubusercontent.com' \
./verify/*.whl ./verify/*.tar.gz
The corresponding .sigstore bundles can be downloaded from the GitHub Release page.
Historic releases (pre-2026-05-16)
Earlier releases were published without sigstore bundles. Re-installing those versions provides no cryptographic provenance — pin to a current release if assurance matters.
License
- Code: MIT (see LICENSE).
- Vendored BiSeNet: MIT (see
licenses/zllrunning-MIT.txt). - SegFormer checkpoint
jonathandinu/face-parsing: non-commercial research and educational use only (CelebAMask-HQ lineage). - BiSeNet CelebAMask-HQ checkpoint (user-supplied): MIT from zllrunning/face-parsing.PyTorch.
Full third-party notices: NOTICE.md. Responsible-disclosure
contact: SECURITY.md.
Metadata
Release files for hairtone 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| hairtone-0.1.4.tar.gz | 30.8 kB | With provenance | Details for hairtone-0.1.4.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| hairtone-0.1.4-py3-none-any.whl | Python 3 | none | any | Details for hairtone-0.1.4-py3-none-any.whl |
Total release size: 58.5 kB
Release files / hairtone-0.1.4.tar.gz
| Download URL | hairtone-0.1.4.tar.gz |
|---|---|
| Size | 30.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
446581e3c24d4d38ba7c48933dc330632264c3addb1e3c7445c2be4e8cf2c0b2
|
|
BLAKE2b-256 checksum How to use checksums |
0e11e02fc0cd7bb7db5ecc1f2051f059bda4e69db479ca6d940b722b59861059
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 19, 2026.
Transparency logRelease files / hairtone-0.1.4-py3-none-any.whl
| Download URL | hairtone-0.1.4-py3-none-any.whl |
|---|---|
| Size | 27.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ed6ce4b6a58d6f5c9eed182cebd6f603dabcec0b0f4a0f290fbb8e990c58f680
|
|
BLAKE2b-256 checksum How to use checksums |
4a810cea5e8a14a999ca17241bed4d2657aaeecb9c55e620fd92ef209a6a3b66
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.13
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 19, 2026.
Transparency logSecurity for hairtone 0.1.4
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for hairtone-0.1.4.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.