Skip to main content

Portrait hair re-colouring with a SegFormer ∪ BiSeNet UNION mask, region growing, and warm-shift LAB Reinhard transfer

Project description

hairtone

PyPI Python License CI CodeQL Downloads

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-parsing checkpoint 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 the HairtoneBackend protocol. The hairtone code is Apache 2.0.

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), hairtone shifts 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. HairtoneBackend implementations run as normal Python code in your process. Only load backends you wrote or trust. See KNOWN_LIMITATIONS.md.

Presets

Run hairtone --list-presets for the full list with hex references. The 19 presets cover:

  • blondesblonde, honey, strawberry
  • pinks & pastelspastel_pink, pink, coral, lavender, mint
  • primariesred, orange, blue, green, purple
  • jewel toneshotpink, cyan, teal, turquoise
  • metallicssilver, 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 — writes photo_blue.jpg next 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. 2001Color Transfer between Images. IEEE Computer Graphics and Applications.
  • Xie et al. 2021SegFormer: Simple and Efficient Design for Semantic Segmentation with Transformers. arXiv:2105.15203.
  • Yu et al. 2018BiSeNet: Bilateral Segmentation Network for Real-time Semantic Segmentation. arXiv:1808.00897.
  • Lee et al. 2019MaskGAN: Towards Diverse and Interactive Facial Image Manipulation (CelebAMask-HQ dataset). CVPR 2020.

License

Full third-party notices: NOTICE.md. Responsible-disclosure contact: SECURITY.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hairtone-0.1.2.tar.gz (35.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hairtone-0.1.2-py3-none-any.whl (35.5 kB view details)

Uploaded Python 3

File details

Details for the file hairtone-0.1.2.tar.gz.

File metadata

  • Download URL: hairtone-0.1.2.tar.gz
  • Upload date:
  • Size: 35.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for hairtone-0.1.2.tar.gz
Algorithm Hash digest
SHA256 54950a73349f1875c2f2ca255f79b126bf4d08ef67d274ca46816c84a494ccdd
MD5 c616da7c812fa3a91a3a659ce19d04aa
BLAKE2b-256 e886ed048b48787f73b5e7ac8e8bc4cea321c324ccecd1d78ff06bf5c1135f36

See more details on using hashes here.

Provenance

The following attestation bundles were made for hairtone-0.1.2.tar.gz:

Publisher: release.yml on hinanohart/hairtone

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hairtone-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: hairtone-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 35.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for hairtone-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f0ccc41f1441f4c7dd7d989bdee3984d015073925cf3f49e40cf4e4cc354c332
MD5 8f5d87fdf577ec48a3b8325590836099
BLAKE2b-256 347d779f841edd0b55f5929105c02663939a43e69481beb004c739cda4f05f72

See more details on using hashes here.

Provenance

The following attestation bundles were made for hairtone-0.1.2-py3-none-any.whl:

Publisher: release.yml on hinanohart/hairtone

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page