---

# Introduction : https://ansel.photos/en/contribute/introduction/

## Context

Digital photography has widely spread in the 2000's as it allowed faster workflows and made possible to get immediate results, compared to the traditional film & darkroom workflow. But this introduced many new problems.

Firstly, analogue photographers were not necessarily accomplished lab and printing technicians, but could rely on local photo labs to get their developments and prints done. Digital photography added the burden of processing the "digital negatives" (raw files) on the shoulders of the photographers, by means of software. But those photographers didn't usually got the appropriate training, both in digital color management and in general computer use. That pushed many of them in the hands of over-simplified software, verging on the toy side, which defined the mainstream expectation of what a digital photo editing software should be. Video editing software took another approach, being used by heavily-trained professionals in a billion-dollars industry.

Secondly, the analogue printing is a hands-on process, where the lab tech interacted directly with the finished product : the print. Digital processing makes a virtual master edit, because the monitor used to preview the result will generally not have the same visual properties as the printing medium or as the consumer display. This introduces complexity in the imaging pipeline, because what you are interacting with is neither what you will see, nor what you actually manipulate. For example, when you push an Lch slider, that should make a change to HDR pipeline RGB, but what you will see is the effect on SDR display RGB after gamut and tone mapping. The proper way to handle this is through the [model-view-controller paradigm](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller), but many software did not get it right[^1]. Display profiling workflows, like the ICC framework, have tried to automatize and hide most of the complexity of color management, but they mostly managed to confuse users even more by abstracting too much things that can actually be written as a couple of equations using only domestic-accounting-level maths (sums and products).

[^1]: And most software still use broken [alpha compositing](https://hg2dc.com/2020/08/26/question-25/) in sRGB, as well as unassociated alpha, so even the color models are butchered.

Thirdly, the analogue lab tech could directly (physically) access the paper and the negative to perform all kinds of adjustments and even distortions (dodging & burning, masking, split-toning, cross-processing, bleach bypass, solarization, etc.). This direct physical access enabled artists to hack the printing process in ways sometimes unplanned by vendors, to match their targeted visual outcome. Digital imaging has cast away the techs and artists alike from the medium, and they are confined to the functionality provided in the GUI of their software. The actual image lives in the computer as pure data, and only engineers know how to access it and what happens to it.

Finally, digital imaging has been driven by computer scientists rather than by color scientists, let alone by actual photographers or photo lab technicians. They developed a digital language incompatible with the legacy of analogue photography and collections of editing tools making use of anonymous unit-less parameters that didn't care for optics and exposure values, thus confusing the analogue-trained photographers (used to drive brightness through light exposure or chemical developing times…). Their constant reliance on "barely working" color spaces (HSL, non-linearly encoded RGB) to handle color changes impregnated many an user with bad habits which soon became expectations, then requirements. The color pipelines developed by the coding monkeys later needed a completely different workflow to handle HDR images, because their hacky logic didn't scale with dynamic range and "worked" only as long as camera sensors and computer displays had mostly the same dynamic range as paper prints, with no future-proofing intended.

## High-level goal of Ansel

Ansel aims at being an instrument of visual expression letting artists develop their own interpretation of the raw material by allowing a large expressive range of color effects, much like a musical instrument lets musicians interpret the score by allowing a large expressive range of sound effects. This is achieved by putting back the retoucher in the center of the image processing, by allowing a transparent and as-direct-as-possible access to the image data, and by providing color manipulations making sense either on an optical level or on a psycho-perceptual level.

Although film and the analogue legacy is often used as an inspiration and as a starting point/first base, making digital image processing a 1:1 virtual translation of analogue printing is not the goal. Let us not forget that film was engineered within the technical limitations of dyes and chemicals, and that many of its beloved properties are actually limitations of its technology (which is not to say that don't have expressive merits, but let's not fall into backward-looking for the sake of it), and were not actually desired in the first place.

However, much like music, it is expected from users to complete at least a basic theoretical and practical training to be able to use this imaging instrument, and Ansel will not give up visual quality for the sake of smoothing the learning curve.

## User-level goal of Ansel

1. Allow to efficiently cull the photographs coming out of the camera/memory card, in order to pick only the ones worthy of being fully post-processed,
2. Allow to edit/retouch the culled photographs in the most direct way, with a minimal number of steps, using unit image controls that affect only one perceptual or optical property at a time,
3. Allow indexing and later retrieval of the processed photographs for archival purposes.

## Supported workflows

Any tool can only be made optimal for a single and definite use case. Supporting too many uses cases will prevent optimization at all. This is where upstream darktable miserably failed. The workflows presented below are the intended uses for Ansel. Any use that deviates from these may be possible but is not recommended and nothing will be made to actively support it.


### Culling workflow

1. The user will mount the file system of the camera or the memory card using the OS tools (MTP/PTP protocols, FAT/exFAT file systems),
2. The user will import the pictures in individual directories named like `YEAR-MONTH-DAY-Job name`. The pictures will use unique names like `Job name-ID number.extension`. Those directories will be used in Ansel as the parent path for "filmrolls" and opened in lighttable once pictures are imported. Filmrolls may or may not contain the whole content of their associated directory.
3. The user will proceed to the culling part. Images are imported with a rating of 0 star. Obviously bad pictures will be rejected (shortcut : <kbd>R</kbd>). 2 methods can then be used :
   1. negative : assign an high star-rating to all pictures, and gradually downgrade bad pictures, so only the keepers keep their high rating,
   2. positive : assign a 1-star-rating to all the seemingly-good pictures, then assign a 2-star rating only to the best of the 1-star-rated, and carry-on until reaching 5 stars or any rating where an appropriate amount of keepers is left,
4. From there, the highest-rated images are considered keepers and may be edited. [Batch-editing features](https://docs.darktable.org/usermanual/4.0/en/guides-tutorials/batch-editing/) that may speed-up this step for series are presented on upstream darktable's manual.
5. Tracking the state of pictures in the workflow can be made through the darktable internal tags `darktable|changed`, `darktable|exported`, `darktable|printed`, which are set automatically when these operations are done. Users needing more states, like those who edit in several steps (one step of basic editing, for better preview during culling, another step of full editing), may add more children tags to the `darktable` parent tag, like `darktable|edited` for pictures that are finished and ready to export.
6. The user may set color labels, tags and metadata in light table, after editing and before exporting, for better archiving.
7. The user may export and print the pictures. It is advised to keep a 16 bits TIFF export in Adobe RGB or ProPhotoRGB color spaces for archival purposes as soon as the edit is finished, to prevent any data loss that may incur with future versions of Ansel or upstream darktable (although everything is done to ensure future compatibility of the software with old edits, mistakes happened and will happen again).

### Notes and Comments

The proposed culling and storing method is the best as it allows to efficiently search and access the pictures from any external software, including other cataloguing software and file browsers. Directories containing dates and job names can be easily searched from any file browser, and each directory is a consistent collection on its own that can be re-imported as a whole, with no further internal sorting.

Users are advised against ditching all pictures in a yearly directory and relying too heavily on Ansel/darktable's filtering features to browse them. If you need to send pictures to a website or to print via internet (through a web browser calling the file browser), this method is clearly inefficient. It is probably the origin of the filtering over-engineered GUI introduced in darktable 4.0.

Users are warned against the "mad librarian syndrom", which consists in hugely overdoing the tagging and sorting. Photo stack agencies may heavily rely on tags to structure their image database of images because they need to deliver pictures of matching content for illustrative purposes quickly, but individuals should not spend more time tagging than taking pictures. Tags are meant to bind similar pictures together, if you find yourself using tags applied to only 1 or 2 pictures, you are defeating their binding purpose, your tagging method is too restrictive and you may want to use image title instead (which can also be searched later). Restrictive and specialized tags should be made hierarchical, so the parent tag can be fetched instead of cooking complex queries fetching multiple children to get your images.

Although color labels may be used as implicit status tag, the recommended way of documenting the status of an image is through the `darktable` tag children, which is explicit. Such status tags may get a dedicated GUI at some point.

In darktable, the tags have been customarily misunderstood as mere keywords, but they are actually hierarchical taxonomies that can be used to declare categories or arbitrary collections. If your pictures are properly stored in directories containing meaningful names, then those names can be used directly in searches, in place of tags. Many users, including myself, manage databases of more then 30.000 images through only directories (filmroll) names.

In any case, if you find yourself needing a complex GUI to sort and access your pictures, your method needs to be simplified. A good software allows to achieve simple tasks simply, and I refuse to make the simple tasks complicated just to account for insane workflows.

## Processing workflow

The scene-referred workflow is the standard in Ansel, as it proved to be faster and more reliable for users who allocated some time to understand it, and allows an unified treatment for HDR and SDR scenes alike. It relies on manipulating the image in a framework where pixel RGB is treated as a light emission for as long as possible, allowing accurate (de-)blurring, (de-)noising, illuminant correction and color-preserving brightening/darkening based on exposure compensations. When the last optically-bound image filter is applied, it then shifts to a perceptual framework where the pixel RGB is converted and handled as a 3D color object (hue, chroma or saturation, lightness or brightness) using color appearance models.

1. The user will setup the global exposure as to match the overall picture brightness to the desired level, in exposure module. This will usually consist into matching the average brightness or the brightness of the picture's subject to the brightness of the GUI background color (middle-grey by design),
2. The user will ensure the bounds of the scene dynamic range are properly remapped to the bounds of the display dynamic range, in filmic module. For typical SDR monitors, the default display settings will not need to be changed, but the scene white and black relative exposures will need to be adjusted for each image, either automatically (use the "Auto tune levels" button) or manually. Then, adjust the contrast (in "look" tab) to taste.
3. The user will ensure the white balance is neutral (reminder : it's not an artistic choice) by setting the "CAT" tab properly, in color calibration module. The user may calibrate the colors directly from a color checker test shot too.
4. Any artistic color deviation, in hue or in saturation, may be applied in color balance module.
5. Any other artistic change can then be made.
---

# Import images : https://ansel.photos/en/doc/getting-started/import/

## Basics

Ansel backbone is a library of images, saved as an SQLite database into `~/.config/ansel/library.db`. Since Ansel is a non-destructive photo editor, editings are saved as histories of settings, along with image metadata, as text[^1] into the library database. The application of editing histories over raw images, as to produce output raster images, is the export step.

The library is also used to keep track of image collections (by tags, folders, EXIF, editing time, etc.). Because of this, we need to make the library aware of new images : it is the import step. This will initialize a new database entry with metadata and a virgin history for each imported picture.

[^1]: or as binary blobs, but still : we store the recipes, not the results.

The import modal window can be accessed from the global menu _File_ 🡒 _Import..._.

## Two kinds of imports

The _file handling_ setting allows to enforce 2 different strategies :

Import in library
: This simply makes the library aware of pictures that already exist somewhere on a local or distant storage.

[^2]: though there are manual way to update file pathes in the library, if you ever move entire folders.

Copy to disk
: The typical use case for this strategy is when you want to empty your camera memory cards and save the photos to a permanent storage. It adds a copying step, with bulk files and folders renaming, prior to the same insertion to library as in the previous strategy.

### Importing without copy

You will need to ensure that those images stay available on the same storage in the future, at the exact same address, because Ansel needs to access them prior to applying any history or generating exports and thumbnails. Ansel does not keep track of file moves on the filesystem,[^2] meaning displaced images will appear lost in the interface (displayed as skull thumbnails).

### Importing with copy

When emptying your memory card to a permanent storage, that is when you copy the files before importing to the library, you can rename files in batch and split them automatically into subfolders. This is done through the _project directory naming pattern_ and _file naming pattern_ fields, using [variables](..//variables.md). The final path of each image imported with copy will be `Base directory / Project directory / Filename`, where `Base directory` will be selected directly from the filesystem (usually the user's `Pictures` default folder), without variables.

Some particular variable's values can be set from the import window :

Project Date
: By default, it is (explicitly) set to today's date, and the time defaults to 00:00:00 UTC+0 if not explicitly set. Date and time can be changed to any past of future date, either in plain-text (using ISO 8601 format), or using the calendar widget. This date and time will be used by the variables `$(YEAR)`, `$(MONTH)`, `$(DAY)`, `$(HOUR)`, `$(MINUTE)`, `$(SECOND)`, `$(MSEC)`. If you plan on using time (hour, minute, second and below), you should manually set it in plain-text in the entry.

Jobcode
: This is the project name, like the subject of your photo session. It is retrieved in patterns with the `$(JOBCODE)` variable.

Those values are constant among images for a whole import session.

EXIF variables can also be used in file and directory names, like `$(EXIF.YEAR)`, `$(EXIF.MONTH)`, `$(EXIF.DAY)`, etc. for the date and time of image capture. These variables are a property of each image, which means that, if you use them into directory names, images may be split into different directories and later to different _filmrolls_ (see below). EXIF variables are fetched from reading the actual files, which triggers I/O from the filesystem holding them, and can significantly slow-down importation for files hosted on remote storages or USB 2.0 cameras.

To fast-track per-picture date/time retrieval, you can also use the variables `$(FILE.YEAR)`, `$(FILE.MONTH)`, `$(FILE.DAY)`, `$(FILE.HOUR)`, `$(FILE.MINUTE)` and `$(FILE.SECOND)`. These variables read the file modification date from the filesystem, without reading the actual files, which reduces I/O and brings substantial speed-up when accessing memory cards from within cameras. Provided no third-party application tampered with raw files, the date/time should be the same as EXIF, at least down to the second. Issues may arise if pictures were opened and saved from other applications, and their time of modification changed.

{{< warning >}}
Dumping all new images into a (few) single, massive directories is discouraged as it doesn't promote sane culling workflows and might need insane filtering options in lighttable to be workable. (See Darktable's 4.0 new collections/filtering monstruosity…).
{{< / warning >}}

While Ansel allows you to perform advanced SQL queries into the library database, to find images using many properties (including date/time, EXIF, rating, editing history, etc.), you should not rely on it to customarily access your images. Opening images from other applications, or uploading them to websites (for example : to send print orders) will use your native filesystem interface. Even within Ansel, accessing large collections of images at once will be slower.

With this in mind, you should try to keep a tidy file structure, organizing photos into per-project or per-session directories, containing no more than a few hundreds pictures at once. Directories and file names should be self-explanatory, to allow text search into filenames from any file browser. Those are old and low technologies, supported on any platform, and relying on those makes for efficient and simple workflows.


## Importing from cameras

__It is highly recommended to import from memory cards using a card reader__. Mounting the memory card through the camera, using an USB link and PTP/MTP drivers, is possible in most cases but discouraged :

- Read/write performance is bad (in any case, worse),
- There are many possible camera MTP/PTP drivers issues on Linux, possibly involving the `udev` stack,
- Recent cameras may not be supported at all for a while,
- Several applications (and the OS file browser) may compete for access to PTP/MTP cameras and have auto-mount enabled (only one can lock it at a time),
- The camera will need to be powered on during the whole process, draining batteries for no reason.

If you still want to proceed, here is some important information to do so.

### All OS

Ansel uses the [Gio](https://docs.gtk.org/gio/index.html) library to handle files. This library essentially communicates with the operating system to handle local and distant storages, which means the proper drivers need to be installed. Depending on the generation of your cameras, you might look into 3 drivers :

- [USB Mass Storage Device Class (UMS or MSDC)](https://en.wikipedia.org/wiki/USB_mass_storage_device_class), used by card readers, by some cameras and by USB sticks. This is the most reliable and most widely supported, as the drivers already are in all OS since the early 2000's. Prefer this whenever possible,
- [Picture Transfer Protocol (PTP)](https://en.wikipedia.org/wiki/Picture_Transfer_Protocol), used by cameras, especially DSLR,
- [Media Transfer Protocol (MTP)](https://en.wikipedia.org/wiki/Media_Transfer_Protocol), used by some cameras and by smartphones.

Some cameras will let you choose the USB driver to load, in their menu. When that is the case, choose preferably mass storage or MTP. In any case, check that your camera is not in PictBridge mode (used only for direct connexion between a camera and a printer).

### Windows

Mass Storage devices should work out-of-the-box, however PTP and possibly MTP devices will require an USB driver compatible with `libusb` (like [Zadig](https://zadig.akeo.ie/)) to work with Gio. Once the driver is installed and the camera plugged-in, you need to go to the configuration panel and enable the Zadig driver for the relevant USB port. This will prevent the camera to work properly with other Windows applications and with the rest of the OS (file browser and such). You will need to deinstall the USB port and reinstall it with native drivers to regain access to your camera from Windows.

### Linux

Gio relies on [GVFs](https://en.wikipedia.org/wiki/GVfs) and [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace) to mount external (and possibly remote) filesystems using various protocols (FTP, sFTP, Samba, DAV, PTP, MTP, etc.). You will need the following packages installed on your system (_actual package names may vary depending on your distribution_) :

- For all cameras and card readers : `gvfs`, `gvfs-fuse`,
- For PTP cameras : `gvfs-gphoto2`
- For MTP cameras : `gvfs-mtp`.

Note that only one application can lock a PTP camera at a time. If your file browser or any part of your OS is automatically mounting the PTP device when hotplugging, it might not be available to Ansel.

There might also be interactions with the `udev` stack that you might need to debug.

{{< note >}}
Buying a 20 $ memory card reader will save you a lot of headaches and give you much faster I/O. Connection issues with USB cameras are out of the scope of Ansel and will not be debugged.
{{< /note >}}

### Mac OS

No idea.

## What next ?

Photos are imported into _filmrolls_, which are the primary collection type in Ansel. Filmrolls are related to filesystem folders, but they are not equivalent :

- A filmroll contains images from a single folder ; the folder's name is the identifier (unique name) of the filmroll,
- A filmroll does not necessarily contain __all__ images from its sibling folder, for example JPEG files from the folder may have been discarded from the filmroll,
- Filmrolls are not recursive or hierarchical, but it is possible to list all filmrolls attached to the sub-folders of an high-level folder,
- If 2 sets of images are imported, at different times, from the same folder, they will be added to the same filmroll,
- Filmrolls will not be automatically updated when new images are added to the filesystem folders.

In other words, filmrolls are _views_ of filesystem folders that exist only in Ansel's database, and containing only images that Ansel is aware of (because they were imported).

When the import completes, the import window will close and the [lighttable](../views/lighttable/_index.md) view will be updated to the last filmroll created during import.
---

# Editing images : https://ansel.photos/en/doc/getting-started/editing/

## Understanding non-destructive editing

Before diving deeper into how to use the software, we should stop here to explain its general paradigm : non-destructive editing.

Once you [imported](./import.md) images to Ansel, what you really did is only to create an entry for them into the [library](../install/configuration.md) database. The modifications you make on these images are never written to the original images, which are actually never changed. This is a strong design requirement aiming at protecting your input RAW images from any kind of corruption.[^1] Ansel will not even let users change the EXIF date and time in the original files.

[^1]: Except the natural aging of your storage media that may lead to randomly-corrupted bits in the future.

Instead, Ansel writes your image-processing and metadata changes to its library database, and possibly duplicates them to [sidecar XMP files](../views/lighttable/digital-asset-management/sidecar.md). These changes are stored as a list of the [modules](../views/darkroom/modules/_index.md) used and the parameters defined for each module. __Said otherwise, a non-destructive editing is stored only as an history stack of changes, which represents the recipe for building the final image.__

{{< note >}}
For some non-destructive RAW editors, an editing is only a snapshot of the parameters set in all the image-processing filters ([modules](../views/darkroom/modules/_index.md)) used, so you have one entry per filter/module. Ansel keeps the whole history as long as you don't compress it (using global menu 🡒 _Edit_ 🡒 _Compress history_), so you can have many entries per filter/module, one entry per user change.
{{< /note >}}

To actually build the final image, you will need to [export](../views/toolboxes/export.md) it to a target file on a target filesystem : this will apply the history stack to the original, input RAW, to create a final file that can be opened in any image viewer.

There are many benefits to working in a non-destructive fashion :

1. The input image stays as safe as possible,
2. You can apply the same history stack to many images (through copy-pasting or creating [styles](../views/toolboxes/styles.md)), therefore editing in batch,
3. You can go back at any point in your history and revert selective changes (same as <kbd>Ctrl</kbd>+<kbd>Z</kbd>, but persisting between reboots), even in some years,
4. You can start from an existing history, duplicate it, and work on a variant while keeping the original untouched,
5. You can share your histories (as XMP files or as styles) with other users.


Because we keep all those history steps, they are automatically saved once applied. You will find no "save" button in Ansel. Here is the summary of the workflow in Ansel:

{{< table >}}

| Step | Action from user perspective | Action from software perspective |
| ---- | ---------------------------- | -------------------------------- |
| 1. Import images | Select input images from the filesystem | Create virgin entries for images in the database |
| 2. Edit images | Apply corrections to an image             | Add editing history entries for this image into the database |
| 3. Export images | Build a final JPG/TIFF/etc. image       | Run a pixel pipeline applying the history onto the input image |

{{< /table >}}

## Editing, retouching, correcting your image

After [import](./import.md), the series of images you imported will be opened in the [lighttable](../views/lighttable/_index.md) view, as a grid of thumbnails. You only have to double-click on any image you want to edit, which will then open the [darkroom](../views/darkroom/_index.md). This view will provide you with image processing [modules](../views/darkroom/modules/_index.md) allowing to apply changes and correction to the appearance of your image.
---

# Ansel target audience : https://ansel.photos/en/contribute/audience/

As knowing the audience is the first step of design, it is useful to define it.

[The Darktable survey](https://eng.aurelienpierre.com/2023/01/who-are-the-darktable-users/) of 2020 showed an abnormal bias among users, favouring highly-educated men coming from technical and scientific background, and with higher programming skills than the average population. This is problematic because it does not overlap with the sociology of photographers at large (regarding computer skills as well as men/women ratio), but is a filtered subset of that social group.

Ansel expects its users to have

- an intermediate to advanced knowledge of __color theory__ (brightness vs. lightness vs. luminance, chroma vs. saturation, additive models, delta E, etc.), [^1]
- an intermediate to advanced knowledge of __cameras__ (ISO, dynamic range, exposure bias, etc.),[^2]
- an intermediate to advanced knowledge of __stage lighting__ (illuminant, CRI, cast shadows, inverse square law of lighting, etc.)

[^1]: See [Chris Brejon's website](https://chrisbrejon.com/) for the scope of this knowledge,

[^2]: See [DxO Mark website](https://dxomark.com) for the scope of this knowledge,

As such, Ansel does not target beginners and will be more (photography-wise) technical than proprietary competitors, which are frustrating to use for advanced users. That increased technical rootedness is meant to unlock more accurate and fine-grained control over image (especially color) manipulations, it is by no mean technics for the sake of it.

However, Ansel expects average computer-savvy users, with no scripting or programming skills. As such, tasks achieved through the command line interface or direct editing of configuration files should remain exceptionnal, alternative to GUI ways, and limited to advanced tasks.

Ansel does not aim at competing against commercial alternatives like Adobe Lightroom or Capture One. First of all, it would be pointless to compete against million-dollars companies with current resources. But then, those have made technical choices to be appealing to masses, only to ensure a large market share : they are actually quite limited, when you know what you are doing. Which is fine, because the users who are the most willing to pay high price don't.

## Values

### Users should not have to read the manual

_(Some restrictions apply)_

Image processing is hard. It uses notions of optics and color "science". No matter if you shoot digital or analog, _illuminant_, _dynamic range_, _gamut_ and _chroma_ will affect your process, in ways you may not have foreseen, and it might be a good idea to understand what they mean and where they come at play. Digital has its own lot of issues, from _color spaces & management_ to _alpha compositing_. Not much we can do here, except providing documentation : you need the skills. But that is, at least, the core basics of everything we do, no matter the software used.

Managing files and navigating in a graphical interface are things computer users have been doing for decades, using well-known paradigms that converged to pretty unified semantics. Users should not have to read a manual to discover why mouse scrolling is blocked, for example, or how to increase the opacity of a mask, or even what all those silly custom-drawn icons mean.

Users should not have to read the manual because, anyway, they won't. Instead, they will annoy developers with questions already answered somewhere on the extensive docs, which are too long to read because they have to explain why too much standard stuff is not handled in a standard way.

Acknowleging that, bad design loses the time of both users and developers, and it's time to cut the losses, for everybody's sake.

Now, trying to follow typical desktop GUI paradigms is fine for ubiquitous tasks, until you get into the core specifics of your particular application. There, trying to blindly follow paradigms enforced by industry-leading software working on different assumptions and in a different context is just a [cargo cult](https://en.wikipedia.org/wiki/Cargo_cult#As_a_metaphor) that will get in the way of actual productivity. This is no license to stupidly reproduce existing design without understanding the assumptions on which it is grounded.

### Simplifying is not making things easier

A violin is a simple instrument : 4 strings, some wood and a design that has barely changed since the 18th century. A piano is a complicated instrument and got recent innovations : 88 keys, 230 strings, a cast-iron frame on top of a wood table, wool felt everywhere, 3 pedals. Now, your first years of learning the piano will be much easier than your first decade of learning how to play a violin. Until you get to the point where Bach's fugues have 4 voices to play at the same time, or Chopin's studies will have you play chords over 3 octaves at 120 BPM, and you still have only 2 hands and one keyboard, but anyway…

The point is : the apparent simplicity of your instrument (aka the number of organs to interact with) has little to do with the actual easiness of playing it. And a picture-editing software is very much like a music instrument : it lets you manipulate technically a "material" medium for artistic purposes. Emphasis on the dichotomy between using technics to achieve art on a stupid medium that constantly gets in the way of your intents and abilities.

So, simplifying without stupidifying aims at reducing the number of steps needed to achieve a predetermined task. Or the depth of the clutter putting you in cognitive overload during said task. It has nothing to do with allowing you to skip class and use the instrument without prior knowledge.

Allowing users to use the instrument without knowledge nor training is turning a tool into a toy: remove everything possibly harmful or frightening, reduce features to the bare minimum, and put playfulness above the ability to solve problems. You do that when you want to sell many copies of your software, by enlarging your market shares to everybody willing to pose as a photographer without having the slightest idea of what they are doing. ~~Later on, you will force them to host their pictures on the cloud, only to fetch them to train your AI automagic, and sell them canned editings under the label "professional results"~~.

### The ability to learn is more important than being beginner-friendly

Technical names, such as methods or algorithms, are kept as they are published by their authors in the technical and scientific litterature. This allows to use any search engine to learn more about those methods, for example their strong and weak points, or the other competing methods available. It also allows to learn image editing tricks from books written on other software, provided those software abide by the same rule of using original names.

This is an unpopular design choice, as Adobe Lightroom has deliberately chosen to hide technical names, hereby locking users from further understanding… but also preventing any reverse-engineering attempt. Advanced Lightroom users will often try to infer what controls and settings actually do, often incorrectly from the visual feedback and limited theoritical knowledge they have, only to teach, preach and diffuse wrong knowledge.

I believe that keeping doors open to deepen user's understanding of image processing matters is a virtuous and empowering circle, even though it has a cognitive price to pay. Therefore, technics will not be hidden or renamed in GUI for the sake of being less frightening.
---

# Designing Ansel : https://ansel.photos/en/contribute/design/

Ansel is __designed__, not hacked. Hackers may enjoy "working" on accelerating the demise of Darktable by increasing its [technical debt](https://en.wikipedia.org/wiki/Technical_debt).

## What is design ?

Design is a process by which you unroll a methodology to bring a technical solution to an human's problem. The design process is meant to converge to the most suitable solution, while fighting the natural urge to rush into the first or the most comfortable idea.

{{< quote author="Louis Srygley" >}}
Without requirements and design, programming is the art of adding bugs to an empty text file.
{{< /quote >}}

1. __Design starts with an usecase__ : a defined task to achieve (on a picture), by a defined user, into a defined timeframe. If no usecase, then no problem to solve, then keep away from your code editor.
2. __Design requires to know the target user__ : education/training, level of craftmanship/mastership, etc.
3. __Design requires to understand the needs__ : in the context of Ansel, that will often need some art history and darkroom photography knowledge,
4. Once the problem and the user are understood, design requires to specify :
    - the expected functionnalities of the solution,
    - the scope of the solution (where in the lifecycle of an image does the solution stand ?),
    - the constraints and requirements of the solution (supporting some standard, allowing to process _n_ images per time unit, etc.),
    - a series of tests to complete that would validate the quality of the solution, as to limit unproductive opinions, biases and subjectivity in the validation process.
5. __Only then__ can the mockups and brainstorming begin, followed be prototypes.


## What design is not ?

Design does not deal with :

- vague requirements of an undefined, future, or fantasized user,
- "it would be cool if…" (that's how you create inconsistent plugins collections),
- what people like (for everything someone likes, you will find someone to hate it),
- what people think they want (it's often not what they need),
- magic tech buzzwords that "are the future" and, as such, need to be plugged everywhere, regardless of their relevance or feasibility (yes, I'm talking about AI, NFT, blockchain, etc.)

## What is good design

Good design is :

- minimalistic,
- robust,
- future-proof,
- generic and generalized,
- maintainable with limited resources,
- informed by science,
- compatible/interoperable with industry standards.

Since Ansel is a workflow-based application, good design also minds the workflow as a whole, and where the problem/solution fit in it.

## How is good design done ?

To help the design process, communication should remain concise, on-focus, and the people taking part to that process should ensure they have a proper understanding of the theory and technical background involved in the problem/solution scope.

It should be stressed that, although the project is software-driven, not all solutions involve coding. Sometimes (often ?), better education or better documentation is all that is needed.

The purpose of a sane design process is to avoid biasing the solutions too early with one's pet design/tech and to avoid getting lost in the technicalities, but to always come back to the core basics and principles of what we are doing: post-processing possibly large batches of raw images for all kinds of output media.

This is backed up by the fact that users rarely know their own needs, or rather, the needs they express are rarely the root of what they actually want. The difficult task of designing is to cut through the branches to go to the root, because solving the root problem usually ends up in more elegant, generic and minimalistic solutions.

### Problems come first

The first step of Ansel design process is to submit a feature request, on the [Community](https://community.ansel.photos/discussions-category?category=6). Feature requests have been moved out of Github because this platform is unwelcoming to non-programmers and non-English-speakers (although the Community supports only French and English).

This feature request will focus on the problem to solve and refrain from proposing any solution. The problem will be defined in terms of tasks to achieve in a photographer's workflow or expected visual outcome of the processed image, aka in terms of the end goal to achieve, not in terms of tooling or technicalities thought to be needed. This may lead to a discussion to dig into the roots of the problem, which are usually [well hidden beneath what the user thinks their problem is](https://eng.aurelienpierre.com/2020/04/the-designer-and-the-drilling-machine/).

No solution proposal is accepted at this stage.

### Solutions come second

When the definition and scope of the problem is agreed upon between the people involved in the discussion, solutions may be proposed. Further discussion may be necessary to evaluate the drawbacks and benefits of each solution, leading to the best solution being adopted on principle. Solutions are defined by their functionnalities (aka what they should do), not by their technology or means (how they should do it).

No prototype proposal is accepted at this stage.

Adopted solutions will lead to a new issue getting triaged in the [project management Kanban board](https://github.com/users/aurelienpierre/projects/1/views/1).

They might be conditional to researching theoretical and technical aspects to assess their feasibility, in which case they will be triaged into the _To research_ column of the Kanban board. The research findings will be added to the original issue until the feasibility of the solution is proven. When it is, the issue will be moved in the "To do" column of the Kanban board.

Adopted solutions might get directly triaged to the _To do_ column if they require only well-known tools and techs.

Ideally, the points to test and the testing procedure to validate the prototype should be written even before having a working prototype. At very least, the tests should ensure no regression happened in related features and tools.

### Prototypes come third

Only the issues triaged in the "To do" column of the [project management Kanban board](https://github.com/users/aurelienpierre/projects/1/views/1) will be worked on, by myself or by anyone willing to tackle them.

The prototype of the solution will be proposed in a pull request of a topic branch linking the original issue. Topic branches need to be rebased on the `master` branch e.g. `git rebase ustream master` or, if you update your branch locally with new master commits, do `git pull upstream master --rebase` or [globally set up git](https://git-scm.com/docs/git-pull#Documentation/git-pull.txt---rebasefalsetruemergesinteractive) to pull through `rebase` rather than `merge`. This ensures your branch history is kept clean with minimal effort, and keeps the `master` history clean too when your PR gets merged.

When the prototype pull request is reviewed and if it fits the [code quality standards](./coding-style.md) (see below) while fitting the specifications of the adopted solution, it gets approved and automatically triaged to the "To test/validating" column of the [project management Kanban board](https://github.com/users/aurelienpierre/projects/1/views/1).

### Validation comes fourth

Approved pull requests will be merged early in the `candidate` or `dev` branch for testing, depending whether they may break image editing histories (by adding new module parameters or changing database scheme). This branch will always be the master branch with all pull requests pending validation on top. This is meant to help testing from people who are not necessarily up-to-speed with manual git branches merging. Unlike the `dev` branch, `candidate` should not break your edits.

If no bug or breakage is reported after some time and the prototype fulfils its initial purpose correctly, it will get merged in `master` and the related issue will be closed and moved to the "Done" column of the [project management Kanban board](https://github.com/users/aurelienpierre/projects/1/views/1).

If the prototype proves itself unsatisfactory, it may be rejected and another one will need to be worked out.

## Pro tips from a seasoned designer

### Not all _software_ problems are _coding_ problems

Many problems don't require more tools (or toys), and more code. More code is always bad anyway, and should be avoided whenever possible. Very often, user's problem is they can't see how to bend existing features to fulfill their needs. This is solved by education, aka better documentation and more tutorials, and sometimes by better UI.

### Listen but don't listen to users

Users express what they want and what they like, never what they need. And you don't need to listen to them to know what it will be:

1. they will want the same thing as their neighbour just got,
2. they will like what they are used to. 

And then, for everything one likes, you will find another one to dislike it. So the Darktable way of solving conflict is to not solve conflict, but give everyone an option, a mode, a preference to enable that special thing they like, how they like it. This means more `case` in your `switch`, more nested `if`, more codepaths you will need to test now, debug, and maintain in the future, and then more preferences hiding the others in the pref window. Before you know it, the code is a tumor that nobody understands anymore, and fixing it only makes it more complicated.

When you scratch beneath the surface, you find than what people actually need is much closer to other people's needs than what they say they want. So you can reconcile the needs much easier than the desires, and without compromising. But then you have to trace the root needs below the will, and that takes abstraction skills and psychology.

### UI designers are dangerous idiots

Everybody who only sees, focuses and cares about the UI is a dangerous idiot. If your GUI is complicated, it means a lot more than just a "complicated GUI" : it means that __the complexity of your backend has reached your frontend__. I have found the hard way that GUI complexity is never separate, and can't be solved separately, from backend complexity and overall application architecture. GUI is not parallel to backend architecture, it's the termination of it.

The problem of UI designers is they typically don't code, or if they do, they suck at low-level programming and software architecture. So they focus on what little they see and understand (typical [streetlight effect](https://en.wikipedia.org/wiki/Streetlight_effect)), and they only produce non-actionnable designs that conflicts with what the software actually needs to work. Because that GUI is only connecting user input to the backend, and if we need that many widgets, it's because the architecture needs that many inputs. You can't escape it : to remove widgets, you need to remove inputs, which means your architecture will have to work with fewer degrees of freedom __first__. That starts with simplifying the backend, which means stinky refactoring of dusty old code nobody understands anymore.

You don't solve GUI issues with drawings and mockups, you solve GUI issues with solving backend issues. But then you need guys who understand both levels, and they may be too expensive for you.

### Ask yourself 36 times per day what was the problem you were trying to solve

It's super easy to get lost into technicalities when programming in a low-level language and fighting third-party libs or APIs, but sometimes the solution is simple and elegant and you got carried away too far into pointers and thread locks. Always go back to the initial problem at hand, that's your lifeline to simplicity.

What is the problem ? Who faces it ? When ? How often ? Doing what ? 

The best path is the simplest path towards your solution : low techs, little code, few layers.

### Document your shitty design

Many times, I completely redid a design while documenting it, because it's when you try to explain it that your realize it's too complicated to explain, which means it's too complicated to understand. If you can't explain your design in a couple of paragraphs, or your documentation has too many "if this, then that", there are usually two reasons :

1. your GUI doesn't expose the relevant info where user needs it, so you have to link half the documentation in your explanation to redirect users to everything they need to know or check before using the one thing you were documenting. The solution is to bring back relevant info where it's needed.
2. your GUI has too many trays, collapsible stuff, contextual behaviours, use cases or hidden preferences, and covering all bases makes you write a novel. The solution is to linearize the workflow, maybe remove options or split features.

GUI is how users control the backend, but it's also where they learn about existing features and what they do. The documentation should provide context, guidelines and references regarding how we do stuff, but the GUI should explain what it does itself.

Of course, there is an obvious limitation to that : in a photography application, users need to understand photography and its language, which involves things like _dynamic range_, _color gamut_, _tone mapping_, etc. The GUI should be self-explanatory on _how it's supposed to be used_, not remove the need to learn the trade (what should be done and how).

### Design is an iterative process

An application is a virtual world in which one small change can reorder how the rest of the ecosystem adapts around it. Therefore any design change can trigger the need to change other things around (refactor tools, move widgets, prune features). Which then might trigger the need to correct the initial change again. It's a step by step process in which it is foolish to even try to get everything right at each step, what matters it that each steps improves the environment from the previous.

Sometimes, (re)design can't be done by small steps but by large leaps : that's when you redo the architecture. These leaps will break many things around them, which is ok if the newer architecture is simpler and more robust overall, and if you give it some time to recover before taking the sledgehammer again. But that will create a transient state in which the new design will appear worse than the previous. This is telling us that how the design is percieved is not a valid input : design quality has to be assessed against its goals and evaluated with objective metrics, not with feelings and quick tests.

And sometimes, some steps are mistakes and should be reverted. The [sunk cost fallacy](https://en.wikipedia.org/wiki/Sunk_cost) should not be used to justify that some redesign should be kept because it was a lot of work to achieve. It's expected that all research & development doesn't make it into production.

### Whack-a-mole sessions mean your architecture has run its course

Whether you keep creating new bugs while fixing old ones, or you keep creating edge cases by extending some feature, it all points in the same direction : your architecture can't be bent any more because it has outgrown its design requirements. It might be that the backend has grown too convoluted or it might be that the existing architecture was really not planned for what you are trying to make it do, but both ways, you will have to redo the architecture and stop hacking in-place. Otherwise, you are only adding technical debt.

But then, the development cost changes scale and that saturday-afternoon project might become a month-long project.

### Best-practices are guidelines, not rules

Best-practices help developing sane habits and clean code, unless you don't understand the problem they tried to solve and use them out of their scope of validity. In that case, they become cargo cult : trying to mimic the effects in the hope that it will magically fix the causes too.

The first that comes to mind is code reuse/code sharing. If reusing the same code for (seemingly similar) features leads to too much internal branching (nested `if`, `switch`/`case`, etc.), to handle all possible paths, what you win on code volume is lost on cyclomatic complexity, and, by the way, your features are not as similar as you thought. 

Also, duplicating code might be a starting point to locally optimize the duplicate later: once you have the complete procedure in front of you, you may spot steps that can be cached or factorized. Whereas if the procedure is only opaque, high-level, reusable API methods, then you loose the ability to spot and remove redundant computations. So, there is a principle of __data reuse/sharing__ (aka caching computed data that will be used later with no change, to spare CPU cycles) that can be made impossible by __code reuse/sharing__, because it obfuscates and abstracts data lifecycle.

This becomes critical on pixel loops: you want to collapse all pixel-wise operations into the same loop, to pay the memory I/O price only once. Which means that you will have to re-implement the same affine correction ($y = a * x + b$) into each loop using it, rather than having a reusable method that does just that in its own loop.

### Don't brace yourself

If you find yourself overwhelmed by some cryptic and random bugs that keep coming and that you can't make sense of, don't keep fighting blindly and take a step back. Then instrument debug helpers, or higher-level managers that keep track of internal states and give you a map of the software data values at any relevant point in its lifecycle. This is especially critical in asynchronous setups, where several threads create, access or compute stuff in parallel on different timelines, and the actual ordering sequence depends on runtime context.---

# Library and collections : https://ansel.photos/en/doc/getting-started/library/

Ansel keeps a database of all images it knows, that is all images that were [imported](import.md), until you actively remove them from the library (global menu : _File_ 🡒 _Remove from library_). The library stores the filesystem path of the images, their metadata and their editing history.

{{< warning >}}
Ansel does not listen to the content of filesystem folders, meaning it will not be notified if imported images are (re)moved on the filesystem. You will need to manually update the pathes if that is the case, or you can move those files directly with Ansel (under the global menu _File_).
{{< /warning >}}

Ansel will then perform database extractions based on arbitrary user criteria : those are called _collections_. They are built in [lighttable](../views/lighttable/_index.md) from the _library_ toolbox and from the _include_ toolbar. Ansel has no concept of gallery or virtual folder that you __create__ first, then to which you __add__ arbitrary images. Instead, in Ansel, you __attach__ tags to images, then __collect__ all images having a certain tag (or any kind of metadata, base folder, etc.), possibly mixing filters and search criteria to narrow-down the search : this is what makes a collection.

The first two tabs of the _library_ toolbox, _folders_ and _collections_, contain simple interfaces to the most-used querying criteria : base folder of the images (called _filmroll_), and tags attached to images. The third tab, _queries_, offers a more complete and somewhat cumbersome interface to perform any kind of advanced query, possibly mixing several criteria with boolean operators (OR/AND/AND NOT), among filmrolls, tags and many more image metadata.

Once you have built a collection by inputing querying criteria, you can save them into a preset into the _library_ toolbox, by clicking on the menu in its header, then _store a new preset_.

Once the first stage of querying is performed in the _library_ toolbox, the _include_ toolbar, in the second row of the header, will apply workflow-related refinements (see [lighttable](../views/lighttable/_index.md) for more details).

A collection can be defined only from the lighttable view, and is then globally shared within the whole software. It defines the content of the lighttable thumbnail grid and of the [filmstrip](../views/toolboxes/filmstrip.md) in all other views.
---

# Project organization : https://ansel.photos/en/contribute/organization/

The project is run by Aurélien Pierre, who tries to balance his photography work (mostly unexistent since 2019), developing & maintaining the software, and handling user education in individual training sessions. This calls for low-overhead project management strategies, relying on cloud-based collaborative tools.


## Departments

### Software development

Development is done on [Github](https://github.com/aurelienpierreeng/ansel),

Feature requests are not taken from users at this point. Users are consulted by the developer regarding their needs when a (re)design project is started. This is to prevent disruptive inputs at random times that would only slow-down the opened projects.

The time planning of issues being currently worked is available on the [Kanban board](https://github.com/orgs/aurelienpierreeng/projects/1). Developers can pick issues in the _To Do_ column. New changes to watch and test are in the _Done_ column. Pull requests that don't follow the [design protocol](./design.md) will be refused.

Developers that need help, introduction to the code base, or code reviews can [book an appointment with Aurélien Pierre](https://cal.com/aurelien-pierre/developer-mentorship) to do it by videoconferencing (possibly using [Visual Studio Live Share](https://visualstudio.microsoft.com/fr/services/live-share/)).

News about development projects closed or milestones are published on the [blog](/news). A dedicated [Matrix chat](https://matrix.to/#/#ansel-dev:matrix.org) centralizes all updates and notifications from new commits, Github issues, new blog posts and new community forum posts.

### Nightly builds

Installable packages for Windows (`.exe`) and Linux (`.AppImage`) are built automatically on Github around 1:00 am UTC, if new commits were pushed the day before. The direct download links are published to a dedicated [Matrix chat](https://matrix.to/#/#ansel-builds:matrix.org), so you can get see the notifications pop and get the new builds, all in one place.

Nightly builds are meant to promote early testing from users who can't or don't want to build themselves from source. They can be instable.

### Bugs

Bugs (as in, stuff that breaks the software), are handled on [Github](https://github.com/aurelienpierreeng/ansel/issues) when they are confirmed.

They can be discussed on [the community forum](https://community.ansel.photos) or the [Matrix chats](https://app.element.io/#/room/#ansel:matrix.org), especially to confirm that they are actually bugs (and not design changes).

Opening issues on Github is important to include them to the project management and track them from a single place.

{{< note >}}
More details : read [a culture of problem solving in open-source software](https://community.ansel.photos/view-discussion/a-culture-of-problem-solving-in).
{{</ note >}}

### Website

The website is generated using [Hugo static website builder](https://gohugo.io/), which is a fairly low-overhead way of writing technical websites using Markdown syntax.

The source code of the website is on [Github](https://github.com/aurelienpierreeng/ansel-website/). You can correct typos or help translating directly by editing the source files on Github UI. Otherwise, you can install Hugo on your computer, then the `Readme` file on Github explains how to build a preview website locally, using a test server on your computer, to better preview (and debug) your changes.

Changes to the website have to use the typical Git + Pull Request (on Github) workflow, which can deter non-programmers, but it's the least shitty way of remote-collaborating on somethng text-based, while ensuring reversible versionning and backups.

### Documentation

The documentation is not included in the website repository for licensing reasons (GPL v3), so it is imported as an external Hugo module. The source code is on [Github](https://github.com/aurelienpierreeng/ansel-doc), and everything else applies the same as for the website. The `Readme` presents the available shortcodes that you can use to format the content, in Markdown files.

There is a caveat, though, if you want to build the documentation locally, because it imports the theme from the main Ansel website, so the easiest way is actually to build the main website while linking locally the documentation as a module. The procedure is detailed on the `Readme` of the main website.

The documentation is currently undergoing structural changes, along with software design changes, so don't hesitate to ask on [Matrix](https://matrix.to/#/#ansel-en:matrix.org) if you have a particular project in mind, before you commit to something on the verge of being removed.


### Teaching and user education

As many "bugs" report show, insufficiently-trained users have wrong expectations, and if you take their feature requests too seriously, you end up with crippled software duplicating features and CPU load. Those need to be solved at the root : with teaching.

1. The community forum has a place to post links to [video tutorials](https://community.ansel.photos/videos-home),
1. The community forum has a place for users to write educational [blog posts](https://community.ansel.photos/posts-home),
1. The [documentation](../doc) is meant to provide usage information closely tied to the software GUI, so users could learn about the features in linear order of GUI appearance.
1. The main website [workflow](../workflows/) section is meant to provide usage information tied to a specific task to achieve, so users could learn "how to".
1. The main website [resources](../resources/) section is meant to provide background theoritical information to help building a deeper understanding of color and photography, and empower users to troubleshoot retouching issues themselves.
1. Users can [book 1-on-1 training sessions](https://cal.com/aurelien-pierre/darktable-ansel-editing-class-en) (classes) with Aurélien Pierre, for faster and more focused training.


## Management

This is mostly a one-guy operation, so things have to be efficient and low-overhead. Which requires some discipline.

### Programming management

There are usually 2 open programming projects at the same time, that are chosen because independent from each other. This allows to switch to project #2 while waiting for user feedback on changes made in #1, in a way that still allows to identify which one created regressions and new bugs. Think of it as alternate single focus.

In the middle of a project, the developer will typically not deal with, care about nor listen to issues related to anything but that project, because brainpower is a precious resource, faster spent than recovered. In particular, feature requests on other parts of the software will be disregarded.

The day-to-day focus is subjected to change unexpectedly, depending on the shit uncovered while fixing other shit, thanks to Darktable crappy legacy of semi-broken non-modular madness-inducing spaghetti code, which often requires partial or full rewrites (in any case, cleaning up) before attempting to fix anything (in a way that doesn't induce more future problems, that is).

### Communication

We live in a World where the volume of information and communication has become overwhelming and humans don't have the bandwidth to process all of it. Endless threads and unregulated conversations are actively harming communication by diluting important information and exhausting the reader. **Discussion is solely meant to reach an understanding and proceed to actionable decisions**. There is a subtle trade-off between completeness and conciseness to find.

For chat or general questions, please use the [Matrix space](https://app.element.io/#/room/#ansel:matrix.org). But even there, conciseness is key.

In pull requests and issues, whether on Github or on the [Community forum](https://community.ansel.photos) please try to stay concise and on-point :

* Technical details (like OS, use of OpenCL, screen size, etc.) should make use of bullet-point lists.
* Screenshots and drawings can go a long way.
* If you are replying to a particular point or person, quote the section of text you are replying to.
* Break your text into paragraphs of roughly 4 to 8 lines, but avoid sending each sentence to a new paragraph.
* Keep in mind everybody speaks English but very few people are native speakers, so try to stick to basic [Globish](https://en.wikipedia.org/wiki/Globish_(Nerri%C3%A8re)).

Good principles on issues/tickets interactions can be found [here](https://www.yegor256.com/2014/11/24/principles-of-bug-tracking.html).

## Updates and notifications

Many centralized and automated ways are offered to keep track of what's new in the project :

- The main website has a central RSS feed, where new and updated page goes : [global RSS](../index.xml),
- For more granularity, each section of the website (News, Doc, Workflows, etc.) has its own RSS feed too. The <i class="fas fa-rss-square"></i> icon you find on section index pages and on every page links to that RSS feed in the current language.
- The Community website has a [central public RSS feed](https://community.ansel.photos/m/timeline/rss/public/) too (truncated to the 25 most recent events).
- Code changes can be tracked from commits index on [Github](https://github.com/aurelienpierreeng/ansel/commits/master/), or using the [Github Atom feed](https://github.com/aurelienpierreeng/ansel/commits/master.atom) (truncated to the 20 most recent commits). Commit messages are usually quite verbose and should explain well enough what was changed and why.
- New commits, Github issues updates (created, edited, closed), new community posts and new website pages are all posted to a [dedicated Matrix chat](https://matrix.to/#/#ansel-dev:matrix.org).
- Nightly builds packages are posted to a [dedicated Matrix chat](https://matrix.to/#/#ansel-builds:matrix.org). They are also listed on the [Github pre-release page](https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0).
- Open and closed project/issues can be seen on the Github [Kanban board](https://github.com/orgs/aurelienpierreeng/projects/1).

{{< note >}}
The website RSS feeds are untruncated (all items since forever are kept), and have the `pubDate` and `updated` tags properly set. On each page content update, the `guid` tag is changed to force RSS readers to bump updated pages on top.
{{< /note >}}
---

# Image selection : https://ansel.photos/en/doc/getting-started/selection/

Ansel allows batch-applying editing histories (through copy-pasting, or through styles) or metadata. The target images for those batch-operations are _selected_ images. But even single-image operations (like opening in [darkroom](../views/darkroom/_index.md) to edit, or editing metadata, copy/pasting, etc.) use selections.

Selections are particular objects in Ansel, because :

1. They are remembered between reboots and across ateliers (views) changes,
1. They are global in the sofware and used to pass on lists of images __between__ lighttable and other ateliers,
1. They are disabled and inactive __within__ an atelier (except for the lighttable), which use active images instead (contextual selection),
2. They are necessarily a subset of the current filtered collection (as displayed in the [lighttable](../views/lighttable/)),
3. They can be set only from the [lighttable](../views/lighttable/) view (no selection can be performed or used from the [filmstrip](../views/toolboxes/filmstrip.md)),
4. They can be turned into a temporary collection themselves, by using the _Restrict to selection_ button in the include/filter toolbar of the [lighttable](../views/lighttable/).

Selected images will appear highlighted in the lighttable. Because selections are used to perform (possibly harmful) _write_ operations, Ansel forces "hard" interactions (pressing on an hardware button) on thumbnails to define them. These allowed interactions are :

- <kbd>Left click</kbd> or <kbd>Space</kbd> : to select a single image,
- <kbd>Ctrl</kbd>+<kbd>Left click</kbd> or <kbd>Ctrl</kbd>+<kbd>Space</kbd> : to add/remove an image to the current selection (toggle),
- <kbd>Shift</kbd>+<kbd>Left click</kbd> or <kbd>Shift</kbd>+<kbd>Space</kbd> : to extend the selection range from the nearest-selected image to the current image,
- Global menu 🡒 _Selection_ : to select all/none images from the current collection, or invert the current selection. Associated keyboard shortcuts will be shown in the menu entries,
- <kbd>Double left click</kbd> or <kbd>Enter</kbd> will open the hovered image in [darkroom](../views/darkroom/_index.md), but will also create a singleton selection, because the darkroom can only handle one image at a time. This is a particular behaviour.

A selection will be needed to perform any property editing, even on a single image, among the following :

- Applying star ratings, or rejecting,
- Applying color labels,
- Applying tags, title, copyright or any metadata,
- Changing GPS coordinates or time zone,
- Copying/pasting image histories, deleting them,
- Removing images from library, or from the disk,
- Etc.

Unlike Darktable, Ansel doesn't implicitely treats the hovered image as a part of any selection, which prevents unpredictable and often unwanted changes on random images when the mouse cursor is left dangling on the thumbnail grid.

If you scrolled far away from the selection and lost it, the global menu _Selection_ 🡒 _Scroll back to selection_ will realign the view on the thumbnail grid to the start of your selection.

A message sitting on the right of the top-most toolbar will tell how many images are selected, among how many in the current collection, and, if there is only one selected, will tell the image index after the `#` symbol. This index will match the `#` numbers in the background of the lighttable thumbnails, and is referred to the current collection.

In ateliers (views) other than the lighttable, interactions with the filmstrip will define active images, which are contextual selections relevant only to the current atelier. We don't call them selection here is to distinguish with the application-wide, global selection. Selections are inactive in views other than the lighttable, and even disabled in darkroom, which forces digital assets management actions from the global menu to always target the main image.
---

# Coding style : https://ansel.photos/en/contribute/coding-style/

## Values

### If it ain't broken, don't fix it

Too much of Darktable "design" has started with "it would be cool if we could ...". I'll tell you what's cool : hanging good pictures of yours on your walls ASAP. Visual arts are not performing art (like music or theater), so only the result matters. Everything that comes before is overhead, and you typically want to keep it minimal. That's not to say that the process can't be enjoyed in itself. However, to enjoy the process, you need to master your tools and to bend them to __your__ will, otherwise you only fight them and the whole process amounts to frustration. Problem is, Darktable "design" puts too much effort into being different for the sake of it.

In this process of adding "cool new stuff", Darktable has broken keyboard shortcuts and a lot of basic GUI behaviours, replacing clean code with spaghetti and adding more GUI clutter without ever pruning stuff.

__Ansel__ has an [explicit](./design.md) design process that mandatorily starts with defined problems met by defined users. Turns out the quantity of code to write is inversely proportionnal to the amount of thinking you have done on your solution, typically to spot the root problem out of what users tell you, and find the simplest path to solution (which is often not even a software solution...).

But bugs don't wait for you in the thinking, they wait only in the code you wrote. So, the more you think, the less you code, the less maintainance burden you create for yourself in the future. But of course... you need to have enough time to think things through. Essentially, that means bye bye to Saturday-afternoon, amateur-driven hacking !

### Don't extend it if you can't simplify it first

A lot of Darktable hacking has been done by copy-pasting code, from other parts of the software, or even from other projects, mostly because contributors don't have time nor skills to undertake large rewrites. This triggers code duplication and increases the length of functions, adding internal branching and introducing `if` and `switch case` nested sometimes on more than 4 levels, making the structure and logic more difficult to grasp and bugs more difficult (and frustrating) to chase, while being more likely to happen.

In any case, when the code responsible for existing features is only growing (sometimes by a factor 10 over 4 years), it raises serious questions regarding future maintainablity, in a context where contributors stick around for no more than a couple of years, and developers have a limited time to invest. It's simply irresponsible, as it sacrifices long-term maintainability for shiny new things.

Simplifying and generalizing code, through clean APIs, before adding new features is a must and Ansel only accepts code I personaly understand and have the skills to maintain. KISS.

## Basic coding logic

Pull requests that don't match the minimum code quality requirements will not be accepted. These requirements aim at ensuring long-term maintainability and stability by enforcing clear, legible code structured with a simple logic.

1. Procedures need to be broken into unit, reusable functions, whenever possible. Exception to this are specialized linear procedures (no branching) doing tasks too specific to be reused anywhere, but in this case use comments to break down the procedures in "chapters" or steps that can be easily spotted and understood.
2. Functions should achieve only one task at a time. For example, GUI code should not be mixed with SQL or pixel-processing code. Getters and setters should be different functions.
3. Functions should have only one entry and one exit point (`return`). The only exceptions accepted are an early return if the memory buffer on which the function is supposed to operate is not initialized or if a thread mutex lock is already captured.
4. Functions should have legible, explicit names and arguments name that advertise their purpose. Programs are meant to be read by humans, if you code for the machine, do it in binary.
5. Functions may only nest up to 2 `if` conditional structures. If more than 2 nested `if` are needed, the structure of your code needs to be reevaluated and probably broken down into more granular functions.
6. `if` should only test uniform cases like the state or the value of ideally one (but maybe more) variable(s) of the same type. If non-uniform cases need to be tested (like `IF user param IS value AND picture buffer IS initialized AND picture IS raw AND picture HAS embedded color profile AND color profile coeff[0] IS NOT NaN`), they should be deferred to a checking function returning a `gboolean` `TRUE` or `FALSE` and named properly so fellow developers understand the purpose of the check without ambiguity on cursory code reading, like `color_matrix_should_apply()`. The branching code will then be `if(color_matrix_should_apply()) pix_out = dot_product(pix_in, matrix);`
7. Comments should mention why you did what you did, like your base assumptions, your reasons and any academic or doc reference you used as a base (DOI and URLs should be there). Your code should tell what you did explicitly. If you find yourself having to explain what your code is doing in comments, usually it's a sign that your code is badly structured, variables and functions are ill-named, etc.
8. Quick workarounds that hide issues instead of tackling them at their root will not be accepted. If you are interested in those, you might consider contributing to upstream darktable instead. The only exceptions will be if the issues are blocking (make the soft crash) and no better solution has been found after some decent amount of time spent researching.
9. Always remember that the best code is the most simple. KISS. To achieve this goal, it's usually better to write code from scratch rather than to try mix-and-matching bits of existing code through heavy copy-pasting.

In an ideal world, any PR would follow [design patterns best practices](https://en.wikipedia.org/wiki/Software_design_pattern).

Some random pieces of wisdom from the internet :

{{< quote author="Brian W. Kernighan" class="full-width ps-0 ms-0 my-2" >}}
Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?
{{< / quote >}}

{{< quote author="Martin Fowler, Refactoring: Improving the Design of Existing Code" class="full-width ps-0 ms-0 my-2" >}}
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
{{< /quote >}}

{{< quote author="John Woods" class="full-width ps-0 ms-0 my-2" >}}
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.
{{< /quote >}}

{{< quote author="Martin Fowler, Refactoring: Improving the Design of Existing Code" class="full-width ps-0 ms-0 my-2" >}}
Whenever I have to think to understand what the code is doing, I ask myself if I can refactor the code to make that understanding more immediately apparent.
{{< /quote >}}

{{< quote author="[Rich Skrenta](http://www.skrenta.com/2007/05/code_is_our_enemy.html)"  class="full-width ps-0 ms-0 my-2" >}}
<div class="mb-3">
Code is bad. It rots. It requires periodic maintenance. It has bugs that need to be found. New features mean old code has to be adapted. The more code you have, the more places there are for bugs to hide. The longer checkouts or compiles take. The longer it takes a new employee to make sense of your system. If you have to refactor there’s more stuff to move around.
</div>
<div>
Code is produced by engineers. To make more code requires more engineers. Engineers have n^2 communication costs, and all that code they add to the system, while expanding its capability, also increases a whole basket of costs. You should do whatever possible to increase the productivity of individual programmers in terms of the expressive power of the code they write. Less code to do the same thing (and possibly better). Less programmers to hire. Less organizational communication costs.
</div>
{{< /quote >}}

{{< quote author="[John Byrd](https://www.quora.com/profile/John-Byrd-2)" class="full-width ps-0 ms-0 my-2" >}}
Good programmers write good code. Great programmers write no code. Zen programmers delete code.
{{< /quote >}}

<div class="clearfix"></div>

## Specific C coding logic

Ansel as well as darktable are written in C. This language is meant for advanced programmers to write fast bugs in OS and system-level applications. It gives too much freedom to do harmful things and can't be debugged before running the program, or writing your own tests (which can be bugged themselves, or can bias the kind of bugs they let through, and anyway, nobody writes tests). Yet most contributors are not trained for C, many of them are not even professional programmers (though professional C programmers should probably not be let anywhere nead end-user applications), so C is a dangerous language for any open source app.

C will let you write in buffers that have not been allocated (resulting in `segfault` error) and will let you free them more than once, but will not free buffers when they are not needed anymore (resulting in memory leaks if you forgot to do it manually). Problem is, since buffer alloc/free may be far away (in the program lifetime as in the source code) from where you use them, it's easy to mess that up. C will also let you cast any pointer to any data type, which enables many programmer mistakes and data corruption. The native string handling methods are not safe _(for reasons I never bothered to understand)_, so we have to use the GLib ones to prevent security exploits.

Basically, C makes you your own and worst enemy, and it's on you to observe safety rules which wisdom will become clear only once you break them. Much like the bugs in a C program. Consider that you write your code to be read by dummies who never programmed in C before.

You also need to keep in mind that the compiler will do most optimizations for you, but will be super conservative about them. The rule of thumb is, if your code is easily understandable by an human (simple logic), it will be properly understood by the compiler, which will take the appropriate optimization measures. The other way around, manual optimizations in the code, that yield cryptic code assumed to be faster on single-threaded systems, usually backfires and yields slower programs after compilation.

### Patterns and structures

1. `for` loops are reserved for iterating over arrays of size known beforehand, so the number of looping steps is known. Stretching that logic, they can also be used to iterate over `GList *` items (which have no size property since they are dynamically allocated), although this checks if each item `(GList *)->next` is not `NULL`. `for` loops should generally not use `break` or `return` statements inside their control flow, unless the loop is looking for a specific item inside the array and returns is as soon as it is found. If your loop has a stopping condition, use `while`.
2. C is not an object-oriented language, but you can and should use OO logic when relevant in C by using structures to store data and pointers to methods, then uniform [getters and setters](https://en.wikibooks.org/wiki/Object_Oriented_Programming/Getters_and_Setters) to define and access the data.
2. structures like `while`, `for`, `if`, or `switch` should not be nested over more than 3 (and preferably 2) levels. Use functions if that happens :
    ```C
    // Bad
    void stuff(float *array, char *output)
    {
      if(condition)
      {
        for(int i = 0; i < 5; i++)
        {
          if(array[i] > 1.f)
            array[i] = ...
        }
        output = "true";
      }
      else
      {
        ...
      }
    }

    // Good
    char *_process(float *array)
    {
      for(int i = 0; i < 5; i++)
      {
        if(array[i] > 1.f)
          array[i] = ...
      }
      return "true";
    }
    void stuff(float *array, char *output)
    {
      if(condition)
      {
        output = _process(array);
      }
      else
      {
        output = _something_else(array);
      }
    }

    ```
3. Long sequequences of checks should be put in function returning `gboolean` clearly stating what we are checking, so in procedures, we get:
    ```C
    gboolean _is_raw(dt_image_t *image)
    {
      return (image->flag & DT_RAW == DT_RAW) &&
             (image->buffer != NULL) &&
             strcmp(image->ext, "dng");
    }

    void stuff(dt_image_t *image)
    {
      if(_is_raw(image))
        ...
      else if(_is_raster(image))
        ...
    }
    ```
    instead of
    ```C
    if((image->flag & DT_RAW == DT_RAW) && (image->buffer != NULL) && strcmp(image->ext, "dng"))
      ...
    else if(...)
      ...
    ```

3. Always access data from buffers using the array-like syntax, from their base pointer, instead of using non-constant pointers on which you perform arithmetic. For example, do:
    ```C
    float *const buffer = malloc(64 * sizeof(float));
    for(int i = 0; i < 64; i++)
    {
      buffer[i] = ...
    }
    ```
    Do not do:
    ```C
    float *buffer = malloc(64 * sizeof(float));
    for(int i = 0; i < 64; i++)
    {
      *buffer++ = ...
    }
    ```
    The latter version is not only less clear to read, but will prevent parallelization and compiler optimizations because the value of the pointer depends on the loop iteration and would need to be shared between threads if any. The former version leads to a memory access logic independent from the loop iteration and can be safely parallelized.
4. The use of inline variable increments (see a [nightmare example here](https://www.youtube.com/watch?v=_7Wok3JoOcE)) is strictly forbidden, unless it's the only operation of the line. These are a mess making for many programming errors. This is permitted :
    ```C
    uint32_t counter;
    for(int i = 0; i < 64; i++)
    {
      if(array[i] > threshold)
        counter++;
    }
    ```
5. The `case` statements in the `switch` structure should not be additive. Do not do:
    ```C
    int tmp = 0;
    switch(var)
    {
      case VALUE1:
      case VALUE2:
        tmp += 1;
      case VALUE3:
        do_something(tmp);
        break;
      case VALUE4:
        do_something_else();
        break;
    }
    ```
    On cursory reading, it will not be immediately clear that the `VALUE3` case inherits the clauses defined by the previous cases, especially in situations where there are more cases. Do:
    ```C
    int tmp = 0;
    switch(var)
    {
      case VALUE1:
      case VALUE2:
        do_something(tmp + 1);
        break;
      case VALUE3:
        do_something(tmp);
        break;
      case VALUE4:
        do_something_else();
        break;
    }
    ```
    Each case is self-enclosed and the outcome does not depends on the order of declaration of the cases.

7. Sort and store your variables into structures that you pass as function arguments instead of using function with more than 8 arguments. Do not do:
    ```C
    void function(float value, gboolean is_green, gboolean is_big, gboolean has_hair, int width, int height, ...)
    {
      ...
    }

    void main()
    {
      if(condition1)
        function(3.f, TRUE, FALSE, TRUE, 80, 90, ...);
      else if(condition2)
        function(3.f, FALSE, TRUE, TRUE, 80, 90, ...);
      else
        function(3.f, FALSE, FALSE, FALSE, 110, 90, ...);
    }
    ```
    Do:
    ```C
    typedef struct params_t
    {
      gboolean is_green;
      gboolean is_big;
      gboolean has_hair;
      int width;
      int height;
    } params_t;

    void function(float value, params_t p)
    {
      ...
    }

    void main()
    {
      params_t p = { .is_green = (condition1),
                    .is_big = (condition2),
                    .has_hair = (condition1 || condition2),
                    .width =  (condition1 || condition2) ? 80 : 110,}
                    .height = 90 };
      function(3.0f, p);
    }
    ```
    The former example is taken from [darktable](https://github.com/darktable-org/darktable/blob/master/src/bauhaus/bauhaus.c#L2210-L2251). The copy-pasting of the function calls is unnecessary and the multiplication of positional arguments makes it impossible to remember which is which. It also doesn't show what arguments are constant over the different branches, which will make refactoring difficult. The latter example is not more concise, however the structure not only makes the function easier to call, but the structure declaration allows to explicitly set each argument, with inline checks if needed. The dependence of the input arguments upon the external conditions is also made immediately clear, and the boolean arguments are directly set from the conditions, which will make the program easier to extend in the future and less prone to programming error due to misunderstandings in the variables dependence.

### OpenMP optimisations

Pixels are essentially 4D RGBA vectors. Since 2004, processors have special abilities to process vectors and apply Single Instructions on Multiple Data (SIMD). This allows us to speed-up the computations by processing an entire pixel (SSE2) up to 4 pixels (AVX-512) at the same time, saving a lot of CPU cycles.

Modern compilers have auto-vectorization options that can optimize pure C, and the OpenMP library allows to provide hints to improve that, provided the code is written in a vectorizable way and uses some pragmas are used.

Write vectorizable code : https://info.ornl.gov/sites/publications/files/Pub69214.pdf

Best practices for auto-vectorization:

* avoid branches in loops that change the control flow. Use inline statements like `absolute = (x > 0) ? x : -x;` so they can be converted to bytes masks in SIMD,
* pixels should only be referenced from the base pointer of their array and the indices of the loops, such that you can predict what memory address is accessed only from the loop index,
* avoid carrying `struct` arguments in functions called in OpenMP loops, and unpack the `struct` members before the loop. Vectorization can't be performed on structures, but only on `float` and `int` scalars and arrays. For example:
    ```lang-C
    typedef struct iop_data_t
    {
      float[4] pixel;
      float factor;
    } iop_data_t;

    float foo(float x, struct iop_data_t *bar)
    {
      return bar->factor * (x + bar->pixel[0] + bar->pixel[1] + bar->pixel[2] + bar->pixel[3]);
    }

    void loop(const float *in, float *out, const size_t width, const size_t height, const struct iop_data_t bar)
    {
      for(size_t k = 0; k < height * width; ++k)
      {
        out[k] = foo(in[k], bar);
        // the non-vectorized function will be called at each iteration (expensive)
      }
    }
    ```
    should be written:
    ```lang-C
    typedef struct iop_data_t
    {
      float[4] pixel DT_ALIGNED_PIXEL; // align on 16-bits addresses
      float factor;
    } iop_data_t;

    #ifdef _OPENMP
    #pragma declare simd
    #endif
    /* declare the function vectorizable and inline it to avoid calls from within the loop */
    inline float foo(const float x, const float pixel[4], const float factor)
    {
      float sum = x;

      /* use a SIMD reduction to vectorize the sum */
      #ifdef _OPENMP
      #pragma omp simd aligned(pixel:16) reduction(+:sum)
      #endif
      for(size_t k = 0; k < 4; ++k)
        sum += pixel[k];

      return factor * sum;
    }

    void loop(const float *const restrict in,
              float *const restrict out,
              const size_t width, const size_t height,
              const struct iop_data_t bar)
    {
      /* unpack the struct members */
      const float *const restrict pixel = bar->pixel;
      const float factor = bar-> factor;

      #ifdef _OPENMP
      #pragma omp parallel for simd default(none) \
      dt_omp_firstprivate(in, out, pixel, factor, width, height) \
      schedule(simd:static) aligned(in, out:64)
      #endif
      for(size_t k = 0; k < height * width; ++k)
      {
        out[k] = foo(in[k], pixel, factor);
      }
    }
    ```
* if you use nested loops (e.g. loop on the width and height of the array), declare the pixel pointers in the innermost loop and use `collapse(2)` in the OpenMP pragma so the compiler will be able to optimize the cache/memory use and split the loop more evenly between the different threads,
* use flat indexing of arrays whenever possible (`for(size_t k = 0 ; k < ch * width * height ; k += ch)`) instead of nested width/height/channels loops,
* use the `restrict` keyword on image/pixels pointers to avoid aliasing and avoid inplace operations on pixels (`*out` must always be different from `*in`) so you don't trigger variable dependencies between threads
* align arrays on 64 bytes and pixels on 16 bytes blocks so the memory is contiguous and the CPU can load full cache lines (and avoid segfaults),
* write small functions and optimize locally (one loop/function), using OpenMP and/or compiler pragmas,
* keep your code stupid simple, systematic and avoid smart-ass pointer arithmetic because it will only lead the compiler to detect variable dependencies and pointer aliasing where there are none,
* avoid types casts in loop,
* declare input/output pointers as `*const` and variables as `const` to avoid false-sharing in parallel loops (using  `shared(variable)` OpenMP pragma).

### Code formatting

- Use spaces instead of tabs,
- Indentation uses 2 spaces,
- Remove trailing spaces,
- `{` and `}` go to their own line,

## Guidelines

1. **Do things you master** : yes, it's nice to learn new things, but Ansel is not a sandbox, it's a production software, and it's not the right place to get your training.
2. **KISS and be lazy** : Ansel doesn't have 50 devs full-time on deck, being minimalistic both in features and in volume of code is reasonable and sane for current management, but also for future maintenance. *(KISS: keep it stupid simple)*.
3. **Do like the rest of the world** : sure, if everybody is jumping out of the window, you have a right to not follow them, but most issues about software UI/UX have already been solved somewhere and in most cases, it makes sense to simply reuse those solutions, because most users will be familiar with them already.
5. **Programming is not the goal** : programming is a mean to an end, the end is to be able to process large volume of pictures in a short amount of time while reaching the desired look on each picture. Programming tasks are to be considered overhead and should be kept minimal, and the volume of code is a liability for any project.
---

# Translating : https://ansel.photos/en/contribute/translating/

Ansel uses [Gettext](https://www.gnu.org/software/gettext/) to translate all parts of the project:

- The software application (written in C),
- The website (Hugo templates and Markdown content),
- The documentation/user manual, inserted into the website as a module (Hugo templates and Markdown content too).

This ensures the same workflow can be used to translate all files, but also that some translated strings can be shared (for example, the translated GUI controls from the applications can be inserted directly into the documentation).

## Organization of translation files

The source code of the website, documentation and software all contain an immediate `po/` subfolder, containing:

- one `.pot` file that holds all the available, translatable strings in their original language (in English),
- many `.po` files matching translatable strings in their original language with their translation (in one language per file).

Translations files are all named following the convention `language-code.po`. For example:

- for German,
  - the software translation is `de.po`,
  - the website translation is `content.de.po`,
  - the documentation translation is `content.de.po`,
- for Brazilian Portuguese,
  - the software translation is `pt_BR.po`,
  - the website translation is `content.pt_br.po`,
  - the documentation translation is `content.pt_br.po`.

## Translating when you can't use CLI/Git

You will need to locate the relevant `.po` file for your language for the part of the project you want to translate:

- the software : <https://github.com/aurelienpierreeng/ansel/tree/master/po>
- the website : <https://github.com/aurelienpierreeng/ansel-website/tree/master/po>
- the documentation : <https://github.com/aurelienpierreeng/ansel-doc/tree/master/po>

1. Download this file and open it with [Poedit](https://poedit.net/),
2. Make the corrections and editions you need,
3. Add your name in a comment for the strings you translate if you want to be credited on the page :
    - Automatically-translated strings will have `TRANSLATOR ChatGPT` there, once you verify those strings, please remove this line,
    - Then add a comment containing `TRANSLATOR Your Name` on a new line. Keep other (non-ChatGPT) contributors there, if any.
4. Save the file and:
    - __Alternative 1__ _(easier for contributor, more steps for maintainer)_: drop it [on my private cloud](https://cloud.apmlt.net/s/YAdfYajPkE5nLyW),
    - __Aternative 2__ _(more steps for contributor, easier for maintainer)_: commit it with Git and open a pull request against the proper Github repository.

## Translating for power users

This will update the `.pot` file using the source code of the project. You will need to have `Git` installed, and [Hugo 0.146](https://github.com/gohugoio/hugo/releases/tag/v0.146.7) installed on your computer.

1. Clone the source code of the relevant project:
    - the software :
      ```bash
      $ git clone --depth 1 \
        https://github.com/aurelienpierreeng/ansel.git
      $ cd ansel
      ```
    - the website :
      ```bash
      $ git clone --depth 1 \
        https://github.com/aurelienpierreeng/ansel-website.git
      $ cd ansel-website
      ```
    - the documentation :
      ```bash
      $ git clone --depth 1 \
        https://github.com/aurelienpierreeng/ansel-doc.git
      $ cd ansel-doc
      ```
    Later, you will update the repository using :
    ```bash
    $ git pull
    ```
2. Update the `.pot` and all `.po` files from the source code (this step works the same for all 3 projects):
    ```bash
    $ sh tools/update-translations.sh
    ```
3. Translate the relevant `.po` file using Poedit or directly in a text editor (see [_Translating when you can't use CLI/Git_](#translating-when-you-cant-use-cligit)),
4. Test & review your translation :
    - For the software, you will need to build Ansel on your OS. Please see [the documentation](../doc/install/_index.md).
    - For the website and the documentation, you can run :
      ```bash
      sh build-modules.sh
      hugo
      ```
    Watch out for any critical error from po4a, especially for mismatching `\n` characters, and errors from Hugo, especially regarding shortcodes syntax.
5. For the website and documentation, cleanup the translated Markdown files (automatically-generated by po4a using the `.po` file) before commiting, using :
    ```bash
    sh tools/build-translations.sh --remove
    ```
6. Commit all `.pot` and `.po` files and open a pull request against the relevant Github repository. Never commit translated `.md` (Markdown) files.


## Translating images

_The following applies to the website and documentation only._

Images can be translated too, for example application screenshots. Images are stored in the `assets/` folder if they are re-used on several pages (global assets), otherwise they are stored in the same folder as the Mardown file using them (local assets). Whether global or local, the translation process is the same, only the base folder changes.

If, for example, you want to translate the `assets/screenshot.jpg` for the language `LANG` (which is the ISO code of the language, like `de`, `nl`, `pt_br`, `zn_cn`, etc.):

1. add and commit a new `assets/screenshot.LANG.jpg` image file to the documentation or website Git repository,
2. in the `content.LANG.po`, locate the entry containing the Markdown tag for the original image, which will be something like `![alt text](screenshot.jpg)`,
3. translate the Markdown tag by replacing the URL of the image, like `![translated alt text](screenshot.LANG.jpg`,
4. save and commit the `content.LANG.po` file,
5. create a pull request against Ansel website or Ansel docs repository.

## Auto-tools and helper scripts

### Init documentation translation with software one

Because the documentation and the software share the same strings for the GUI controls, you can lazily init the documentation strings from the software ones if they exactly match (including casing). This needs a Python interpeter and the `regex` package (install with `pip install -U regex`). From the source code of the documentation, you can call :

```bash
$ python tools/merge-translations.py path/to/software path/to/doc
```

### Auto-translate documentation and website with ChatGPT

ChatGPT-4o does a very fair job at translating Markdown-formatted text from English, although not in every language. You will need a private API key to store in the folder of the documentation or website in a `.chatgpt.api_key` file. Then, calls to the ChatGPT API are not free, and the minimal payment of 5 US$ will roughly get you the website fully translated into 4 languages.

The all-in-one script can be called using :

```bash
$ sh auto-translate.sh LANG
```

where `LANG` is the target language code (de, fr, pt_br, etc.). This will process the translation in batches of 90 to 120 strings to comply with ChatGPT API limitations and thresholding. This will :

- parse the original `po/content.LANG.po` file and export the batch to translate into a temporary `po/content.LANG.txt` file,
- send `po/content.LANG.txt` file to ChatGPT and get the response in `po/content.LANG.generated.txt`
- fix most common formatting inconsistencies that ChatGPT can introduce and inject the translations back into `po/content.LANG.po`,
- build translated Markdown files (following the `page.LANG.md` naming convention),
- build the website with Hugo.

If all these steps complete without error, then you are good to run the script again to process the next batch until completion. If errors are shown, you will need to fix them. We run only one batch at each call to let user the opportunity to find errors while there are not too many changes to inspect.

__Common errors__:

- nothing will be translated : check ChatGPT answer within `po/content.LANG.generated.txt`, sometimes it is unable to understand its mission. You can try again, sometimes it works on the 3rd call. But often, there is nothing to do and some languages/strings can't be translated at all.
- when building the website with Hugo, some shortcode can't be found. This is because shortcodes are declared like so : `{{</* shortcode_name */>}}`. Sometimes, ChatGPT will try to translate `shortcode_name` and the shortcode won't work again. The solution is to bring back the English name for the shortcode and its attributes,
- same with [Mermaid](https://mermaid.js.org/) graphs, ChatGPT can try to translate commands and properties that shouldn't be translated,
- original strings end with the newline character `\n` and the translated strings don't (or the other way around). The script tries to sanitize that, but some corner cases are not handled. Original strings `msgid` and their translation `msgstr` in the `.po` file should have the same number of `\n` characters at the same place,
- improperly escaped double quotes : the `msgid` and `msgstr` Gettext strings should be delimited by unescaped double quotes `"` at each extremity of the string. Any other double quote, within the Gettext string, should be escaped using `\"`.

The best way to fix errors is to open the relevant `.po` file in a text editor. If you can't find the error and solve it, you can try opening the file into Poedit, but when saving, it will usually completly erase the faulty strings without fixing them, so translation will need to be started again from scratch.

### Build translated Markdown files

For the website and documentation, [Hugo](https://gohugo.io/) handles the translations of any given page `new_page.md` using the naming convention `new_page.LANG.md`, where `LANG` is the language code. Hugo natively supports manually writing these translated files in the same folder as their original, however, here we generate them using the `.po` translation file and the program `po4a`. The `build-modules.sh` and `tools/auto-translate.sh` scripts handle this internally, but you may want to generate those file manually :

1. Update the `.pot` and `.po` files with the source code :
    ```bash
    $ sh tools/update-translations.sh
    ```
2. Create the translated `.md` files :
    ```bash
    $ sh tools/build-translations.sh --add
    ```
2. Cleanup the translated `.md` files :
    ```bash
    $ sh tools/build-translations.sh --remove
    ```

It is important to never commit the translated `.md` files with Git, as they are regenerated only from that script when building the website. This is only for repositiory hygiene, there is no technical drawback. Cleaning up the translated `.md` before committing ensures no mistakes.

## Lost in translation ?

If you have issues or questions, feel free to ask on the dedicated [Matrix translators channel](https://matrix.to/#/#ansel-translators:matrix.org).

## Notes to translators

### Policy on capitals

The darktable project made a priority to put everything in lower case, which makes the GUI difficult to read, especially for tooltips having several sentences. Capitals anchor visually the beginning of sentences and other important text, like buttons, controls etc. It's no accident if all languages converged to using them (though German has its particular way of putting them everywhere), they help legibility whether you like their aesthetic or not.

Ansel source code reuses most of darktable's labels and adds an initial capital in most places where they are needed (module headers, buttons). This is done by a bit of code using the C function `g_unichar_toupper()` from Gtk Glib, such that the original English text stays in lower case to keep compatibility with translations.

This programmatic fix works for non-accented characters, no matter the language used (default strings in English, or translations). However, it does not work for initial accented characters, which will not be capitalized. In this case, translators are asked to force their translation to use initial capitalized accented characters whenever they are grammatically correct in their language.

New labels or old labels recently changed (which would break translations anyway) will get initial capitals from now on, in the source code (English version), so this should progressively be fixed.

### Translating technical terms

Technical terms related to color theory and colorimetry need to be translated exactly from English, with extra care because these terms can exist also in common language (aka non-technical) but with a different meaning. The [International Electrotechnical Commission](https://www.electropedia.org/iev/iev.nsf/welcome) provides a search-engine where you can search for the English technical terms and get the accurate translations in different languages, including the main European ones as well as Arabian and Chinese.

### Notes aux traducteurs francophones

La traduction de darktable comporte des bizarreries incompréhensibles pour quiconque utilise un ordinateur de bureau depuis plus de 10 ans. Voici une liste rapide des erreurs à corriger:

* "set" est traduit "positionné" mais sa traduction correcte est "réglé". C'est illogique car "settings" est correctement traduit "réglages". Dans Ansel, on ne positionne que des masques (ou leurs nœuds de contrôle) dans le plan 2D. Le reste, ce sont des réglages.
* "reset" est traduit "repositionné" mais sa traduction correcte est "réinitialiser".
* En anglais, un grand nombre de verbes ont la même graphie pour leur infinitif et leur participe-passé, voire même existent comme substantif ("set", dans l'exemple ci-dessus, peut être traduit "réglé" ou "régler" ou comme "ensemble" sous sa forme substantivée). Si une action (pas encore effectuée) est requise, l'infinitif doit être utilisé en français. Si une action est déjà effectuée, c'est le participe-passé qui doit être employé. Les choses se corsent pour les substantifs car l'anglais ne requiert pas toujours de déterminant devant, il faut donc le déduire du contexte. À surveiller : "click" (cliquer ou clic), "type" (type ou entrer/taper), etc.
---

# Triaging issues : https://ansel.photos/en/contribute/triaging/

This page is written for people helping triaging issues on the issue Github tracker.

## Preamble

- Any project has limited resources, the difference between projects will be the threshold.
- Any project should have clear goals. For Ansel, it is to manage, edit and export collections of RAW images on a desktop computer by an end-user who is not a CLI user but puts visual image quality above all else.
- Any project has overhead, that is actions requested to meet the goals, although they are not directly the goal and therefore should stay minimal. For Ansel, it is the maintenance of the website, documentation, servers, nightly-built packages, code cleanups, debugging, regression tests, cross-OS support, issues triaging, etc.
- Goals and overhead should be expressed in terms of __tasks__ to perform in order to __solve problems__ (issues). If no problem to solve, then no work to do: status quo is great too, don't create work for the sake of it.
- because of resource limitation, tasks have to be ordered depending on their priority.

The following document aims at defining this priority.

Ansel was forked on Darktable because Darktable has no clear goal, no priority management, and the overhead is increasing every year, which is the trademark of burn-out factories and is unsustainable mid-term.

## Defining good issues

Project management works better with SMART tasks. S.M.A.R.T. stands for:

- Specific _(ex: finding URI of pictures Ansel exported on the filesystem and open them)_
- Measurable _(ex: number of clicks/steps required, CPU time to perform the task on some target platform)_
- Actionnable/Achievable _(ex: can be integrated on current code base with only minor rewrites, needs only a few hundreds of lines of code)_
- Relevant/Reasonable _(ex: is part of a fairly-general photography workflow, would be used by a signicative part of users)_
- Time-bound _(ex: requires at most 70 man-hours)_.

__A good issue is one that leads to a SMART task__. For Ansel, that means issues focusing on a clearly-defined problem affecting a clearly-defined step of the picture editing workflow ("I have problems doing X because Y and I would like Z").

Questions and general discussions should happen on <https://community.ansel.photos>.

Bad issues are:

- too broad _("automatize workflow", "improve UX")_,
- focusing on the means _("use neural network", "extend tone curve")_ instead of the goal _("mask the sky out", "control saturation selectively")_,
- out of scope _("port to Android", "switch to Qt", "switch to Vulkan")_
- affecting third-party libraries/projects _(Rawspeed, Libraw, Exiv2, Lensfun, GPhoto2, Gtk, etc.)_,
- too subjective _("please do things like that other software I used in the past and really like")_. What user A likes will be disliked by user B, we can't work with that.

Side note: some issues may sound like things in need for more code, whereas they actually need better documentation of current features, or slight GUI touch-ups (renaming labels, reorganizing widgets), so that's something to keep in mind before jumping on the guns.

## Defining priorities

In an ideal world, tasks (aka good issues) would be added to the to-do list in a linear fashion, as milestones are reached, and their code product would be tested for a couple of weeks while the code is otherwise frozen, until it is proven that everything holds, in which case we would unfreeze the code and move on to the next task in the to-do list.

Problem is this implies everybody on deck for the testing phase, so it doesn't freeze the code for too long. Because that doesn't happen (people take vacations, have kids, move homes, change jobs, have a life…), we have to parallelize testing the product of previous tasks while we are working on the next, to be efficient.

This is to say the to-do list is not linear and some important things might get added or removed dynamically, depending on what happens. The problem is then to determine what constitutes something important enough as to disrupt the schedule.

There is no definitive rule here, so you will have to use your best judgment, but there are some rules of thumbs:

- something recently broken (a regression) is easier to spot and fix sooner than later, so in the grand scheme of things, it might be less work overall to do it sooner,
- something that prevents the software from working at all (crash, corrupted output files, loss of data) is critical enough to take precedence over improvements and more cosmetic fixes,
- something that impacts a large number of users and where no work-around can be found will take precedence too.

On the contrary, anything impacting a small number of users, or niche/secondary features, or minor annoyances that have work-arounds, are not critical enough to justify disrupting the schedule. Those will be added on the queue in a first-in/first-out way.

Ansel has 4 levels of priorities, set as issue tags:

- `priority: critical`: Affects basic and core functionnalities of the software in a way that prevents it to work at all,
- `priority: high`: Affects basic and core functionnalities of the software in a way that severly degrades usability,
- `priority: medium`: Affects basic and core functionnalities of the software in a way that mildly degrades usability (work-arounds available),
- `priority: low`: Affects optional and niche functionnalities

## Defining milestones

Modules parameters are saved as binary blobs. We deal with these by handling their bit size. When a new parameter is added, we need to write code to handle the conversion, aka the different bit size of the parameter blob, and we increase the internal version of modules parameters. No code is written for backward compatibility, so pictures edited with newer modules can't be opened in older modules. Modules parameters are used in styles, in presets and in XMP files too.

For these reasons, any issue that would lead to adding parameters in modules (either the image processing modules in darkroom, or the lighttable modules dealing with export & metadata), would break backwards compatibility and needs to be planned for the next major version of the software (1.0, 2.0, 3.0, etc.). This means that only GUI & behaviour changes are allowed within the same major version, and can be planned for the next minor version (0.1, 0.2, 0.3, then 1.1, 1.2, 1.3, etc.).

Ansel has only 2 milestones at all time: the next minor version and the next major version.

## Defining difficulty

Difficulty is directly related to the amount of work required by a task, that is:

- the number of lines of code to write,
- the number of files to change,
- the probability of breaking existing features, leading to extra testing work,
- the existence of similar features or written theory to achieve the task,
- the overhead of making changes and features work reliable across operating systems.

Estimating difficulty in an accurate fashion is something only an experienced developer can do.

## Defining nature

The nature of the issues is handled with labels. We have:

- regressions (stuff that use to work but was broken by recent-ish changes),
- bugs (stuff that has never worked in the past years),
- enhancements (stuff that needs to be improved or added),
- wontfix (not a bug, but a feature or design choice or necessity imposed by third-party dependencies),
- question (should not be on Github, but on <https://community.ansel.photos>),
- duplicate (issue already reported),
- unclear (issue can't be understood),
- invalid (issue is "bad" according to the above definition of a good issue).

## Priority are relative

See <https://www.youtube.com/watch?v=8fnfeuoh4s8>. Having to repair the car in order to change the light-bulb is a great metaphor of doing code on a 12 years-old software having hundreds of thousands of lines of code written by people who didn't talk to each other and didn't document their changes. Fixing the car is not top-priority until it becomes the pre-requisite to fix the high-priority light.

## Your triager job

Ultimately, only an experienced developer will be able to accurately triage issues. But reason requires that an experienced developer would be employed to do things that only an experienced developer can do: write simple code to efficiently solve technical problems requiring some amount of theory and design.

The trade-off is to have triagers help prioritize obvious issues so developers only have to deal with the least-obvious issues and focus on coding.

1. label only issues you understand and you feel comfortable triaging. You don't have to do them all, it's fine if you don't know.
2. for issues you understand:
   - assign a priority label if you can,
   - assign a nature label if you can,
   - assign a milestone if you can,
   - immediately close the issue if it is invalid or duplicate.
3. for issues you don't understand:
   - try asking more questions to the author,
   - ensure authors fill all relevant info (OS, hardware, reproducing steps for bugs),
4. it's better if you do nothing than if you do it wrong: issues without labels are easier to spot than issues with wrong labels.
5. don't hesitate to call-in video meetings: better sit together for 30 min to have a productive chat and adjust decisions than exchange endless threads of useless messages.

Thank you !
---

# Documenting Ansel : https://ansel.photos/en/contribute/documenting/

## Introduction 

There are different ways to access information :

1. __(chrono)logical__, like reading page by page, line by line, until you reach the end of the publication,
2. __thematic__, like getting to the table of contents and jumping straight to the part you are interested at, __provided the content is divided into meaningful units of content__,
3. __transversal__, like following a "related posts" section based on content similarity (defined manually, with tags & keywords, or learned by AI topic analysis), or explicit cross-references. For example, most websites have archives listing all pages that have a certain tag/keyword, books have glossaries.
4. __hint-based__, like presenting a bibliography of more in-depth publications or a "more info" section at the end of the content, or anticipating on later content,
5. __source-based__, following references (typically footnotes or marginnotes) to publications from where the info is extracted, mostly for verification purposes,
6. __information retrieval__, aka search engine.

You have to support all of them at once because they are complimentary and the best in context depends on the initial knowledge and needs of the reader. Not one of those ways is superior to the others. This means there is a fair deal of "keywords stuffing" to do into your writing, as to ensure that keyword-based content analysis and information retrieval by keywords will work as expected.

A manual/documentation is not a course, but sticking to a dry list of features/GUI controls and their definition is… too dry. You need to create links between content (which is not merely HTML links). In Ansel, [workflows](../workflows/) start with a goal, and unroll the tooling to achieve it. [Documentation](../doc/) starts with the tooling and presents how and where it can be used. But those are the two ends of the spectrum, and the reality is always a bit in-between.

Knowledge is a [network graph](https://en.wikipedia.org/wiki/Knowledge_graph) anyway. You just have to mind the links between the nodes. They are at least as important as the content.

## Practical implementation in Ansel

Ansel uses [Hugo](https://gohugo.dev) as its CMS, both for the documentation and the rest of the [website](./website/index.md). The practical implementation of the principles enunciated above will have to deal with Hugo's core features.

### (Chrono)logical and thematic access

Hugo content is organized into [sections](https://gohugo.io/content-management/sections/) that are essentially sub-folders of the main `/content` folder. Sub-folders can be infinitely nested. The theme of the website presents the treeview of all sections in the left sidebar, on wide screens (desktop). Sections and sub-section top levels can be (un)collapsed upon user request. This treeview provides the top-level table of contents which acts as __thematic access__.

Within sections, the relative order of pages can be manually defined using the [`weight` parameter](https://gohugo.io/methods/page/weight/#article) in Markdown headers, like so :

```yaml
---
title: Documenting Ansel
date: 2025-10-13
weight: 9
---

My content here
```

The `weight` parameter is optional. If not used, the pages listings will typically use the `date` to order content, but could also use alphabetical ordering on page title. This ordering provides the __(chrono)logical access__.

Whithin pages, if there are more than two sections in the content (defined by second-level titles, e.g. `<h2>` in HTML or `##` in Markdown), internal table of content will be automatically added by Hugo into the right sidebar, on wide screens (desktop).

### Transversal access

Tags can be defined on the website using the `tags` parameter in the Markdown header, like so :

```yaml
---
title: This page title
date: 2022-12-04
tags:
    - color science
    - pipeline
---

Your content
```

Tags are optional and are displayed as clickable links in several places in the website theme. Clicking on one tag opens its archive, listing all pages having this tag. This provides __transversal__ access.

Writers are also encouraged to add cross-links in their content, from website pages to other website pages, to promote transversal access. Concepts that have an entry on the website should be turned into links to the page describing each concept.

### Hint-based

Writers are free to add a _Bibliography_ or _More information_ section at the end of their pages, with a list of publications and links. Those publications can be internal or external to Ansel project. They can be peripheral to the topic treated in the content.

It is also possible to finish pages with an opening on the next logical step, when writing on workflows or modules.

### Source-based

Hugo supports extended Markdown, which [supports footnotes](https://www.markdownguide.org/extended-syntax/#footnotes). These are recommended to reference sources, like so :

```markdown
The typical observer has Just Noticeable Difference (Delta E) of 2.3[^1]

[^1]: Some Author, Some Publisher, _A real-world, large-sampled, study of vision parameters for white, rich, educated, American students of the Rochester Institute of Technology_, (some year). [URL](https://doi.org/xxxxx)
```

Ansel has not settled for any particular academic formatting of source citations at this point, although the [IEEE citation style](https://ieee-dataport.org/sites/default/files/analysis/27/IEEE%20Citation%20Guidelines.pdf) seems the best-suited to the footnote approach with numerical index.

Ensure to include the [DOI](https://www.doi.org/) of the publication, or at least some long-term URL at which it can be retrieved now and in the future.

### Information retrieval

For now, [Chantal](https://chantal.aurelienpierre.com) handles that part. The web index is manually and periodically updated.

## Guidelines

Writing, even technical, is an art that is difficult to reduce to a set of definite guidelines or best practices, because this varies upon the context. You should be careful of not getting more catholic than the Pope. A good rule of thumb is to write to solve problems, which means start by asking yourself why and from where the reader landed on the page you are writing :

1. what kind of knowledge the reader is supposed/assumed to already have ?
    - the reader should ideally be aware of those prerequisites, so maybe start with a list of links,
    - anything not in this list should be defined and explained on your page,
2. what kind of task the reader is attempting to complete that led them to this page ?
    - do they want a quick cheatsheet, or a detailed how-to, or a theoritical background ? You may have to choose one arbitrary.
    - this will decide what hints you may add in-text to improve the knowledge network,
    - this should probably bias the whole vantage point of your content and its length/depth.


A good way of assessing documentation quality is by looking at the frequently asked questions (or least understood topics) on forums. If the topic is already covered but questions keep arising, it can be because the documentation is not clear or the relevant pages is buried into the network and not discoverable enough.---

# Shortcuts and keyboard interaction : https://ansel.photos/en/doc/getting-started/keyboard/

Ansel interface is designed to support two mutually-exclusive modes of interaction and navigation: through pointing devices (mouse or graphic tablet) and through the keyboard. Compared to Darktable, many interactions that required combinations of both have been removed, which makes the workflows more efficient.

## Shortcuts and available actions

Ansel exposes app-wise actions and GUI controls to keyboard bindings through pathes like `Darkroom/Modules/Exposure/Black point`, where the left-most member of the path is the view and the right-most member is the actual control being bound. A such path defines an human-readable unique identifier that also says where the control is located in the application GUI.

Ansel has two kinds of objects that users can map to keyboard binding through those pathes:

- __GUI controls/widgets__ : buttons, sliders, comboboxes, etc. Shortcuts attached to those will give them the focus,
- __Actions__ : most of them are attached to the global menu, but some are contextual, depending on which widget has the focus. Shortcuts attached to those will trigger the actions.

The focus is a GUI state in which a widget will capture all keyboard events sent to the application. For text entries, that obviously means recording keystrokes to type text. For other widgets, like sliders, that may be capturing arrow keys events to increase/decrease the value. Only one widget in the whole application can grab the focus at any given time.

Clicking on a widget will automatically give it the focus, but that makes for a terrible workflow where you may have to constantly switch between mouse and keyboard. To allow a full-keyboard workflow, Ansel provides shortcuts to switch the focus to any desired target widget. From there, interacting with widgets (to change their value or their state) is done through generic (non-user-configurable) key bindings.

This means that :

1. Widgets shortcuts are only meant to __select a target widget__ (visible or not) to which further keyboard interactions will be dispatched,
2. There is __only one shortcut__ to configure (and remember) per widget,
3. All widgets of the same type (button, slider, combobox) will use the __same keyboard interactions__ once focused (easier to remember).

Focused control widgets will appear in a bolder font that the others. Focused modules will be highlighted.

Actions (other than widget focusing ones) don't target widgets at all, and calling their shortcut directly triggers them, possibly outside the GUI.

### Finding default shortcuts, setting new ones

{{< note >}}
__A popup window__ showing all available actions and their defined keys is available from the __global menu__, under _Edit_ 🡒 _Keyboard shortcuts…_. This window is meant to be at the same time a cheatsheet and a controller to edit shortcuts.
{{< /note >}}

{{< figure src="/shortcuts-popup.jpg" caption="The shortcuts window" />}}

The shortcut pathes are broken into a hierarchical tree in this window. The first level of the hierarchy defines the shortcuts groups:

- There is one _global group_ (tied to the global menu),
- There are several _local groups_ (tied to each view : darkroom, lighttable, etc.).

Inside each group, a combination of keys can be used at most once. If you try to re-use a key combination already in use somewhere else in the same group, you will get an error message saying what path is already using it.

Several shortcuts groups can include the same combination of keys. The global group is loaded all the time in all views of the application, the local groups are loaded only when entering their respecting view and unloaded when exiting it (only one is loaded at anytime). If the global and a local group have each a shortcut using the same keys, as long as the local group is loaded, its shortcut will take precedence over the global shortcut.

Read-only shortcuts
: They are signaled with a lock icon, meaning they can't be edited by users. They are described as _contextual interaction on focus_, because they will need a widget of the right type focused before to have any effect. You will find them under :
    - `Darkroom/Controls` (for sliders and comboboxes interactions),
    - `Lighttable/Thumbtable` (for navigation into the thumbnails grid).
: They are given here in a "cheatsheet" spirit and are guaranteed to be up-to-date with what the code defines. The values found in the popup supercede any documentation regarding shortcuts defaults. The keys used by the read-only shortcuts will be unusable in user-parametrable shortcuts from the same group.

User-parametrable shortcuts
: They are either focusing actions, targeting a control, a module or a toolbox, or direct action triggers, like any action from the global menu.
    - __Edit a shortcut__ : double click on the shortcut label, or on the _disabled_ label, in the _keys_ column, then type the keys you want to define as a shortcut. To stop recording keystrokes, you can either hit <kbd>Escape</kbd> or click outside the _keys_ column (which may be more reliable).
    - __Clear a shortcut__ : double click on the trashbin icon, on the right of the shortcut. There is no way to clear a shortcut once in editing mode.
: After a shortcut has been edited, it will be used without having to restart the application. If the shortcut applies to a menu entry, it should be updated in the menu too.

The shortcuts popup window has __a double search-engine__:

- One __for features__, that performs a case-insensitive search into action pathes,
- One __for keys__, allowing to search for key modifiers, or keys, or combinations of both:
    - If only a key modifier or key is given, results will be returned for partial matches,
    - If both a key and a modifier are given, results will be returned for full matches,
    - The syntax for key combinations is `<Mod1><Mod2>Key`, where all elements are optional. Auto-completion is provided for modifiers when you start typing `<`.
- __features and key searches can be combined__ to efficiently narrow results,
- As long as any search entry is not empty, the shortcut treeview is expanded. To automatically expand all items while not searching anything in particular, you can input `/` into the feature search entry.

### Vimkey-like, global action search

All the available actions can be searched and triggered from anywhere in the software, using a vimkey-like command with auto-completion. The feature is available from the global menu, under _Help_ 🡒 _Search actions_ (default shortcut : <kbd>Ctrl</kbd>+<kbd>P</kbd>).

{{< figure src="/actions-search.jpg" caption="The action search" />}}

This opens a modal popup with a search entry. Type in there a path (partial or complete) or simply the name of the action you are looking for. The auto-completion will return the list of all available actions matching your search from the currently-active shortcut groups, along with a description and their defined shortcut (if any). From there, by selecting one of the results (with mouse click, or navigating with arrow keys then hitting <kbd>Enter</kbd>), you will trigger the action. To cancel, hit <kbd>Escape</kbd>.

In this context, the action pathes become commands that can be triggered from the search popup without having to define shortcuts, which helps if you know __what__ action you want to trigger but don't remember __where__ its controller is in the GUI, or simply if you can't be bothered remembering shortcuts.

In darkroom, if you select in the global action search the path to a control (slider or combobox) within a module, the focusing action will open the parent module, scroll it such that it gets visible, make sure the control is visible (like changing the notebook page if the control is within a notebook/tabs) and focus the control. From there, you can keep interacting with the control through arrow keys to change its value.

This feature has also replaced the module search bar used since Darktable 3.6, because triggering the focus action on a module achieves the same result as restricting the list of modules to those matching a name query (making this module appear in GUI).

## Keyboard workflow

### Global menu

All menues have mnemonics that can be shown by hitting <kbd>Alt</kbd> : they will then appear underlined. Hitting the underlined letter along with <kbd>Alt</kbd> will open the target menu. Once one menu is opened, you can navigate through all menues using arrow keys.

### Lighttable

#### Toolboxes

You can collapse/expand the toolboxes, in the left sidebar, by assigning them shortcuts or using the global action search on their focusing action. The effect of the focusing action is a toggle, so expanded toolboxes will be collapsed on the next action trigger.

Once a control has gained focus in a toolbox (which, so far, can only happen with a click), focusing on the next control can be done with <kbd>Tab</kbd> or using the arrow keys (in some situations). Otherwise, the focus is automatically given to the thumbtable (grid of thumbnails).

#### Thumbtable

Navigation within thumbnails can be done with arrow keys. <kbd>PageUp</kbd>/<kbd>PageDown</kbd> scroll the view to the previous/next page. <kbd>Home</kbd>/<kbd>End</kbd> move all the way to the start/end of the grid.

Navigation within thumbnails only hovers. To commmit the hovered thumbnail to a selection, press <kbd>Space</kbd> for a singleton selection. To add/remove the hovered thumbnail to/from the current set of selected images, press <kbd>Ctrl</kbd>+<kbd>Space</kbd>. To extend the current selection up to the hovered thumbnail (range selection), press <kbd>Shift</kbd>+<bkd>Space</kbd>.

A selection is absolutely mandatory to be able to modify image metadata, such as ratings (stars), color labels, etc. or to apply any batch operations. Ansel is designed such that selections can only be made by "hard" interactions (pressing an hardware button), as to prevent user errors. Batch selection options, independent from the thumbtable, can be found in the global menu, under _Selection_.

Applying ratings uses the numeric keys (0-5) by default, applying color labels uses the F1-F6 keys.

Opening a picture in darkroom is done with <kbd>Enter</kbd>.

#### Filters

You can filter the current collection by text using the search entry, which will search into all text metadata of the image. To enter the search entry, the default shortcut is <kbd>Ctrl</kbd>+<kbd>F</kbd>.

### Darkroom

#### Modules

To collapse/expand and focus a module, you can:

- Hit <kbd>Alt</kbd> and see the mnemonic keys that some module have in their name, then type the mnemonic. This works only for visible modules.
- Use the action search or define a shortcut on the module focusing action. This works also for hidden modules.

The module focusing action will automatically :

1. Open the native module tab of this module,
2. Expand and focus the module,
3. Scroll the right sidebar such that at least the header of the module (if not the whole module) is visible,
4. Collapse and fade out all other modules,

The user-defined set of favourite modules, that used to be displayed in a separate tab, is replaced by the ability to target a specific module through its focusing action.

To expand+focus the previous/next module in the current module tab, use <kbd>Page Up</kbd>/<kbd>Page Down</kbd>.

To switch to the next/previous module tab, use <kbd>Ctrl</kbd>+<kbd>Tab</kbd> and <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Tab</kbd>. Once you reached one end of the modules tabs, it will cycle back to the other end.

#### Modules controls

To go directly to a control (slider or combobox), use the action search or define a shortcut to its focusing action. This will automatically:

1. Open the native module tab of this module,
2. Expand and focus the module,
3. Scroll the right sidebar such that at least the header of the module (if not the whole module) is visible,
4. Collapse and fade out all other modules,
4. Open the right notebook (tab) page if the control is insterted into a notebook,
5. Focus the control.

Once the control is focused:

- On sliders:
    - Arrow keys <kbd>Left</kbd>/<kbd>Right</kbd> will increase/decrease the value by a normal step,
    - <kbd>Ctrl</kbd>+ arrows change the value using a fine step,
    - <kbd>Shift</kbd>+ arrows change the value using a coarse step,
    - <kbd>Insert</kbd> triggers the attached color-picker if any.
- On comboboxes:
    - Arrow keys <kbd>Up</kbd>/<kbd>Down</kbd> will cycle through the options,
    - <kbd>Enter</kbd> validates the selected option and closes the popup,
    - <kbd>Escape</kbd> exits the popup without validating the selected option.

Once a control is focused, you can focus to the previous/next with <kbd>Up</kbd>/<kbd>Down</kbd> keys, or <kbd>Tab</kbd>. To focus the previous/next control outside a notebook tab, when you reached the end of the current notebook page, use <kbd>Ctrl</kbd>+<kbd>Up</kbd>/<kbd>Down</kbd>.

Once a shortcut is attached to a control (slider or combobox) focusing action, as long as the shortcut keys are kept pressed, all mouse scrolling events are sent to this control even when the mouse is not over the widget (as it would normally require to record scrolling events). This allows a 2-hands mixed workflow where one hand, on the keyboard, selects the target control, while the other scrolls on the mouse to adjust the value. It replaces MIDI support that achieves the same through mapping exactly one potentiometer to exactly one control, which will quickly run out of knobs to control Ansel.

#### Module instances

When several instances of modules are in use, shortcuts are always attached to the first-added instance, which may not necessarily be the first in the pixel pipeline: the first instance is always the unnamed, base instance, until it gets deleted (then the next replaces it).[^1]

[^1]: The recommended workflow always uses the first instance as an average global setting, most likely to be accessed from keyboard for quick adjustments. Later instances are supposed to be local, often using masks, for which keyboard setting will not be enough if possible at all.

All module instances that have an instance name set will get a private focusing action attached to the module (but none attached to its controls), using a path prefixed with `Modules/Instances/` and followed by `Module name/Instance name`. Since instance names are a property of each editing history, and not constant during the lifecycle of the whole application, this focusing action cannot be assigned a key shortcut and is accessible only from the global action search.

When renaming the base instance of a module, it gets its own `Modules/Instances/` focusing action, but this is only a duplicate of the default one from `Modules/`, and it doesn't change the behaviour of the default actions (focusing the module and its controls).

## System integration

Certain shortcuts are already reserved by your operating system and desktop environment. It may be a good idea to let the OS listen and decode those before they are passed on to Ansel (in which case, they are consumed and never passed on). To do so, go into the [Preferences](../preferences-settings/miscellaneous.md) popup, to _miscellaneous_, and check the box _let the OS mask Ansel shortcuts_.

## Key variants and alternatives

The numpad key alternatives (digits, enter, delete, home, end, insert, page up/down, arrows) are decoded as the non-numpad alternatives by all shortcuts, which means numbers will be seen as the same object no matter where they are typed. They respond to numlock as they should (numlock on enables digits and arithmetic operators, numlock off enables navigation).

Ansel allows single-key shortcuts, without modifier, which is non-standard and may cause issues with some keys. For this reason, shortcuts are temporarily disabled when a text entry is active. If that causes issues on your system, consider using typical double-key shortcuts (with a modifier).
---

# Color theory readings : https://ansel.photos/en/resources/readings/

What resources can be found online and in libraries to help understand _some_ color theory ?

<!--more-->

## Preamble

### What is color theory ?

Color theory has 2 aspects :

* a __scientific__ one, historically linked to light physics, but now related more closely to psychology, trying to link physical signals with perceptual stimuli,
* an __artistic__ one, studying color semantics (which are mostly cultural) and how they play in creating believable shadows in paintings or moody ambiance into photographs and movies.

It is worth noting that the scientific side has been studied by artists too (like [Munsell](https://en.wikipedia.org/wiki/Munsell_color_system)). The field mixes notions of :

* physics (light spectrum, where everything begins),
* biology and medicine (retina cells, optical nerve and brain, that is the human sensor),
* psychology (color memory and afterimages),
* art history (color harmonies),
* ethnology (cultural meaning of color).

Every author being typically an expert in only one of those fields, but still needing the others, it is often useful to know beforehand from what vantage point he will treat the subject, and at which point he might not be at home anymore.

### Pitfalls

Most color talk focuses on tri-dimensional models of color appearance : the hue-chroma-lightness, or the hue-saturation-brightness. That is basically color split into 3 absolute properties that would ideally be completely independent from each other, because it would be mathematically practical for real-world applications.

Problem is, vision is not absolute but subjected to the interference of the background and surrounding. The same color patch displayed on different backgrounds (varying color and illumination) will appear differently. But vision is also very sensitive to local contrast and patterns, and scientific evidence suggests that it is actually much more about color opponency (that is, subtractive stimuli) than about intensities (additive stimuli).[^2]

[^2]: To convince yourself, visit [this website](https://www.echalk.co.uk/amusements/OpticalIllusions/colourPerception/colourPerception.html), showcasing optical illusions where grey patches put in a blue surround appear yellow, and vice versa.

When we talk about the hue-chroma-lightness model, for instance, we imply that colors are evaluated against a white background, as in the [Munsell book](https://fr.wikipedia.org/wiki/Nuancier_de_Munsell#/media/Fichier:Munsell_Books.jpg). This model becomes fairly wrong in any other context, in particular because chroma contributes to the perception of brightness ([Helmholtz-Kohlrausch effect](https://en.wikipedia.org/wiki/Helmholtz%E2%80%93Kohlrausch_effect)), which is discarded by the lightness.

This is why, in the printing and media industries, color assessment is realized in standardized conditions : D50 or D65 lighting, middle-grey background and surround, 100 to 300 Cd/m² of incident light luminance. The problem is this still doesn't account for the effect of the image content itself, because the same red dress may appear quite different against a blue sky, a grey wall, or a green foliage, even when you have discarded the possible effects of the surround lighting.

As the color appearance models try to discard many parameters to try to find truly independent color dimensions, we need to keep in mind that their base assumptions are rarely found in real settings, and that their real-life useability is limited. In any case, models are mathematical reductions of much more complex realities for the sake of being computable. And the 3 color dimensions are actually not fully independent.

### How useful is color theory ?

Image retouchers, digital artists and other pixels pushers deal with images encoded as RGB signals. Those are additive signals that make sense in imaging pipeline starting with sensors gathering photons and ending with LED panels emitting photons. But none of that is directly linked to the actual mechanics of human color vision : it is actually much closer to basic light physics.

For most of art history, painters have worked with pigments, which are subtractive in nature. Centuries before Newton observed light diffraction (rainbow colors) through prisms, they were able to mix pigments as to render human skin and flesh with incredible mastership, without nearly as much knowledge as we have now, and using a color mixing scheme that has nothing to do with actual vision.

Color theory is thus not a requirement to make art, let alone to make good art. But…

Color theory provides names to put on phenomena we experience daily and that are for the most part deeply counter-intuitive. If you have ever met people who suffer great pain for months and years while the medical staff is unable to diagnose precisely what disease they have, you know how important it is to them to simply have a name to put on that disease, no matter if there is a cure or not.

Image retouchers suffer from using color manipulation tools that simply don't behave according to color vision. You can add lights, you can add pigments, but you can't add hues because hues are entirely a product of the human cognitive system. Mixing lights and pigments of a certain original hue will not produce an easily-predictable hue at the end. Color theory provides concepts to understand those deviations and to better handle them, that is, to make sense of what you see beyond what you do when pushing GUI sliders.

### Colorimetry or color sciences ?

Colorimetry is a special branch of color sciences, which aims at _measuring_ color, and this is short from being a contradiction in terms, because color is a perception and we can't really measure perceptions without violating the ethical rules of research on living patients.

We call [colorimeter](https://en.wikipedia.org/wiki/Tristimulus_colorimeter) a sensor that satisfies the Maxwell-Luther-Ives condition, meaning its "primaries" allow to express any light spectrum as a linear combination. I will spare you the maths details, just remember that any random photographic sensor is not automatically a colorimeter, and therefore introduces measuring errors called [metamerism](https://en.wikipedia.org/wiki/Metamerism_(color)#Metameric_failure), which will be very annoying when trying to accurately reproduce colors. In practice, only sensors used for calibration of the graphic chain will be close to the Maxwell-Luther-Ives condition.[^1]

[^1]: From the paper [_What is the space of spectral sensitivity functions for digital cameras_](http://www.gujinwei.org/research/camspec/camspec.pdf), by Jun Jiang, Dengyu Liu, Jinwei Gu and Sabine Süsstrunk (2013), the Canon cameras are the closest from the Maxwell-Luther-Ives condition (twice as good as the Hasselblad), but a cross-search on [DXOMark](https://www.dxomark.fr/Cameras/) indicates that this improved colorimetry is done at the expense of low-light noise. The current ISO beasts have most likely sacrificed a lot of color accuracy to win on sensitivity.

The final goal of colorimetry is not to study human vision, nor to understand its behaviour, but to provide optical and mathematical tools, as well as error metrics (like the delta E) allowing to profile and to correct color rendering on display and image reproduction systems, but also to perform quality control and management. Colorimetry is therefore an engineering field, with all the practical compromises it implies, not a science.

The _colorimetry_ term is improperly used by all sorts of scammers to designate unrelated things, because it sounds scientific while still containing the artsy-connotated "color" keyword.

## References

### Color science

_Fundamentals, concepts and terminology of colo(u)r._

Color Appearance Models, 3rd Edition. Mark D. Fairchild. 2013.
: Mark Fairchild is professor at the Rochester Institute of Technology (closely tied to Munsell legacy and located in the neighbourhood of the Eastman Kodak company). The  Chapters 1 to 9 list the different aspects of vision and adaptation, along with the parameters affecting it and the color terminology. The 9 central chapters detail the typical industry-ready color appearance models, with implementation details that will only interest engineers. The last 3 chapters treat matters such as color management and color reproduction that may interest any graphic artist. [Publisher website](https://www.wiley.com/en-us/Color+Appearance+Models%2C+3rd+Edition-p-9781119967033)

Colour : sense and measurement. Richard Kirk. 2022.
: Richard Kirk holds a PhD in physics and has worked at Filmlight UK research and development since the 1980's. Filmlight is best known for its film digitization workflow (software and hardware) and its Baselight color grading software, used by most Hollywood movie productions to fine-tune the color look. Kirk is the co-author of the color-grading "tRGB" space used in Ansel [color balance](/doc/views/darkroom/modules/color-balance-rgb/) module and presented in the book (p. 79). The book itself is made available free of charge, as a PDF, so I will not expand on its content here : have a look for yourself. Just know that it is fairly accessible to non-technical people, well illustrated, and covers both film and digital imaging, with their relationships. [Download the PDF](https://www.filmlight.ltd.uk/support/documents/colourbook/colourbook.php).

The dimensions of color. David Briggs. [Website 1](http://www.huevaluechroma.com/). [Website 2](https://sites.google.com/site/djcbriggs/life-drawings-2).
: David Briggs is a member of the Colour Society of Australia and teacher at the National Art School and University of Technology in Sydney. As a drawer and painter, his publications give an useful insight on the interconnections between color theory and pigments mixing practice.

### Color pipelines

_How digital images are handled in your computer from start to finish_

The Hitchhiker's Guide to Digital colour. Troy Sobotka. [Website](https://hg2dc.com/)
: I have worked with Troy for years — he has basically helped most open-source software projects to unfuck their color pipelines in the past decade (at least the ones who accepted they had a problem whether or not they saw it) — and he is the original author of Filmic for Blender. We share the same passion for calling bullshit bullshit and idiots idiots. The HG2DC website is a step-by-step walk through computer graphics with lots of pictures and video animations explaining where, why and what happens to your RGB pixels.

The Computer Graphics Cinematography Book. Chris Brejon. [Website](https://chrisbrejon.com/cg-cinematography/)
: Chris has worked at 5 of the most prominent movie studios in the world, over the past 13 years, as a lighting and compositing artist. Although the book focuses on cinematography, the chapters on color management, composition, lighting and color theory apply directly to photography as well (though the workflow changes a bit).

### 2D painting and 3D rendering

_Constructing images from scratch_

Marco Bucci's YouTube channel. [Website](https://www.youtube.com/channel/UCsDxB-CSMQ0Vu_hTag7-2UQ)
: Marco Bucci is a painter and shows how he constructs his paintings, most importantly how he shades subjects to give depth to 2D paintings. This is highly interesting because photographers just capture what is there, and can afford to never bother about the "true", "desired" and "believable" colors of a shadow. Since painters (and 3D artists) create everything from scratch, they have to ask themselves what color it should be. Give a good binge to his channel, I promise you will never look at a drop shadow the same way. _(You may need to discard some of his color theory explanations though, they are often inaccurate)_.

### Visual illusions

_Witnessing the gullibility of our own perceptual system is key to anticipating problems in real-world applications_

The illusory staircase Gelb effect. [Website](http://www.psy.ritsumei.ac.jp/~akitaoka/illgelbe.html)
: Repeating the same grey or colored patch over a gradient changes the percieved color of the patch, in addition of the typical Mach banding effect (see below).

Color and Contrast. Nate Baldwin. [Website](https://colorandcontrast.com/)
: Many visual effects and illusions demonstrated, involving local contrast illusions. The website focuses on user interface design and aims at demonstrating the origin of best practices, but the demonstrations are relevant for any audience.

Optical Illusions. R. Beau Lotto. [Website](https://www.echalk.co.uk/amusements/OpticalIllusions/colourPerception/colourPerception.html)
: Witness the impact of different surround brightness and hue on color patches that are exactly similar.


### Color-grading

_Correcting or introducing color shifts to ensure consistent look between pictures and create some ambiance_

Color Correction Handbook (vol. 1) / Color Correction Look Book (vol 2). Alexis Van Hurkman. 2013. [Google Play vol. 1](https://books.google.fr/books?id=kDcdAgAAQBAJ). [Google Play vol. 2](https://books.google.fr/books?id=Hm9VAgAAQBAJ)
: These books have a special place in Darktable/Ansel history because I used them as a reference to redesign the features of the [color balance](/doc/views/darkroom/modules/color-balance-rgb/) module. While they focus on color grading for movies, they are software-agnostic (showing how to get things done in different applications), and make use of color tools like RGB curves, color balance, channel mixer and 3D LUTs that are all available in Ansel. They contain valuable insight on how to (and why) create color looks in images, to put the form at the service of the content. I recommend you get digital copies of them because they have lots of illustrations that will be rendered on screen better than what printed paper allows.

### News and insights

_Staying up-to-date with color stuff, technical analyses and other myths debunking about color_

Colour science library, blog. [Website](https://www.colour-science.org/blog/)
: The colour-science project provides a Python library with many color models and other numeric tools for engineers and researchers working on color and vision. I use it extensively, in particular to create color sweeps and other graphs on this website. While this will not interest end-users, the blog of the project contains well-documented and useful insights on matters like color calibration and lighting.
---

# Common misconceptions : https://ansel.photos/en/resources/misconceptions/

This page addresses most of the mistakes and misconceptions about Ansel that can be found online.

<!--more-->

## I need to be an expert / engineer to use Ansel

My [videos](https://www.youtube.com/channel/UCmsSn3fujI81EKEr4NLxrcg) and my posts typically contain both the "how to" and the "why/how" part. The "why/how" is typically technical or even theoritical, and is there to justify the "how to". There are several reasons for which I give both :

1. The accurate technical explanations are really difficult to find on the internet, and I'm pretty much the only one to link theory and practice on video. On the other hand, it's easy to find wrong information in photography, from people slightly above the average who try to help, but actually mislead others.
2. I personaly hate gurus that drop instructions without bothering to justify them. Rules always have a reason and need to be broken as soon as this reason stops being valid. You see a lot of people continuing to follow old rules because "the elders knew what they were doing" — but don't remember why they did — while circumstances have changed.
3. Understanding how tools behave allows you to predict when they will fail (because they will all fail at some point), which enables you to solve problems even before they appear, and to be ready with a plan B when it happens,
4. Most pieces of advice I give are contextual to the desired result and the type of image being worked. Removing context would make them simply wrong in general.

Because of that, many people have conceived the idea that they need to understand 100 % of the technical content before being able to use the software. That is simply not true. Ultimately, Ansel is just a software with a GUI, you can push cursors or use factory presets until the picture looks good. Whatever you don't understand can be disregarded for now, and perhaps tried again later.

On the other hand, if you start mixing media, like printing pictures on paper and releasing digital pictures from the same edit, or inputing your Ansel exports into another software for further manipulation, having at least a basic understanding of how an imaging pipeline works is going to help you tremendously.

Like any technological object, the more you understand it and the better you control it, the least you fight it. But Ansel comes shipped with a pack of default presets and a pre-configured pipeline that should give you a proper editing base in most cases.

It is true, however, that the image processing controls in the GUI tend to be more grounded into color science and optics than in other applications. The reason is processing HDR without artifacts needs more accurate color models, that take more input parameters to adapt to the dynamic range of images. The reason most applications can afford to look more simple is their color models are less performant and rely on approximations that don't really scale with dynamic range. Everything has a cost…

## Ansel processes my raw pictures in a way that makes them darker and duller

The reality is actually the other way around.

Raw photographs typically have a JPEG file embedded as a low-resolution thumbnail. This thumbnail is what you see in Ansel lighttable as well as on your camera back screen. You will never see a raw photograph without any kind of correction, it's simply not displayable.

This thumbnail has been processed and enhanced by the camera firmware, in a way that usually brightens it a lot, adds contrast, saturation, and very often tints it for a warmer look.

What you see when opening the picture in Ansel darkroom is a much less processed picture than the JPEG, closer to the raw and more neutral, meant to be a base for your personal editing.

But remember that the default look when opening the darkroom is just that : a base look, a starting point. Ultimately, even the default settings can be adjusted to your liking, which is the whole point of the software.

## Deprecated modules don't work anymore

Ansel is based on darktable 4.0. darktable 4.0 has deprecated many modules. Ansel has deprecated even more. Modules get deprecated when they get a better alternative introduced.

But "deprecated" is a strong word to say "the widget of the module is hidden from the GUI". Both the GUI and the pixel code of the module are still in the software, and will still run for old edits using this module. For these edits, the module will show in the GUI.

For new edits, the module will only be hidden from the GUI. Deprecation is only a display clean-up to limit modules proliferation.
---

# Resources : https://ansel.photos/en/resources/
---

# Transitioning from darktable to Ansel : https://ansel.photos/en/doc/from-darktable/

This page was first written in January 2023, right after the fork, and listed what had already diverged from darktable 3.8/4.0. A lot has happened on both sides since. The sections below summarize the major changes; the gory technical details live in the [news](https://ansel.photos/en/news/) section of the website.

## What changed in Ansel since 2023

### Performance and reliability

The pixel pipeline has been [completely overhauled](https://ansel.photos/en/news/complete-pipeline-overhaul/): modules run about 1.8× faster on CPU and 1.6× faster on GPU than darktable, and — more importantly — the pipeline cache was rewritten so that disabling a module, using a color picker or re-exporting an already-exported image does not recompute the whole thing. That makes Ansel 4 to 100× faster than darktable to refresh the image. Memory management was rewritten too: Ansel knows exactly how much RAM it uses and stays within the budget you set, instead of darktable's hand-wavy "performance profiles" that have no predictability.

### Lighttable, thumbnails and selection

The whole [lighttable, thumbnail and mipmap-cache stack was rewritten](https://ansel.photos/en/news/redesigning-lighttable-and-mipmap-cache/). Concretely: starting the app and drawing a full collection is roughly 3.5× faster, switching between lighttable and darkroom is near-instant, and scrolling a large collection no longer stutters. The right sidebar was removed (its content moved to the global menu and the left panel), culling became the _Restrict to selection_ filter, the filtering toolbar was rebuilt as plain inclusive toggle buttons, the zoomable lighttable came back (Columns + Zoom with barycentric auto-pan), and selection became explicit and _What You See Is What You Get_ — no more metadata loss from a dangling mouse cursor.

### GUI controls

The home-made Bauhaus sliders and comboboxes were [largely rewritten](https://ansel.photos/en/news/undarktable-ing-gui-controls/): clicking the combobox chevron now works (imagine that), values are dispatched to the pipeline only when you stop dragging instead of on every intermediate step, and widgets redraw immediately for instant feedback even on slow machines.

### Color tools

The color-grading tools were [reworked so their color models are actually correct](https://ansel.photos/en/news/color-controls-finally-correct/), rather than relying on broken assumptions papered over with magic numbers.

### Masking

Drawn, parametric and raster masks are now independent, combinable toggles instead of a single exclusive mode. In particular, a raster mask can be combined _on top_ of a drawn and/or parametric mask — see [combining masks](views/darkroom/masking-and-blending/_index.md#combining-masks).

### Global menu and keyboard

The global menu and the from-scratch rewrite of the keyboard shortcuts handler both shipped since this page was written; they are described in the relevant sections below.

## What darktable added that Ansel did not follow

darktable kept piling on features through versions 4.2 to 5.4. Ansel deliberately did not adopt most of them, either because they duplicate existing tools or because they don't fit the design:

- **Sigmoid** and **AgX** tone mappers — Ansel keeps a single, well-understood tone mapper (filmic). Bolting on two more display transforms that do the same job differently is exactly the kind of redundant clutter the fork exists to avoid.
- **Enlarge canvas** and **Overlay** compositing modules — extending the canvas is handled by the _framing_ module, and compositing several images is a job for a compositor, not a RAW developer.
- **Camera-specific "JPEG-matching" styles** for 500+ camera models — chasing the out-of-camera JPEG per body is the opposite of a camera-agnostic, scene-referred workflow.
- **Multiple workspaces** with separate databases — one library, kept fast, is enough.
- **The startup splash screen** — a loading screen is a confession that startup is slow, not a feature. Ansel made startup fast instead.
- **The swap-sidebars preference, drag-and-drop utility-module headers, and the right-click "which modules to show" menu** — Ansel removed the whole problem by moving button-only toolboxes into the global menu, so there is nothing left to rearrange.

Ansel does have its own _color equalizer_ and _color primaries_ modules, so those are not in this list.

## What was removed from mainstream darktable

### Features

[The tethering view](https://docs.darktable.org/usermanual/3.8/en/tethering/)
: It relies on the lib GPhoto2 which is super brittle and buggy. The tethering view is merely a GUI over Gphoto2, much like [Entangle](https://entangle-photo.org/). The problem is GPhoto2 is known to break something different at each new release, so it's simply not worth the trouble to maintain a GUI over a Schrödinger cat. Use Entangle if you need to.

The easter egg video game
: Not funny if you happen to be with a client on April 1st and your working app opens on a silly game that can't even be closed with <kbd>Ctrl+Q</kbd>. (Yes, it happened to me).

The histogram overlay control for [exposure and black level](https://docs.darktable.org/usermanual/3.8/en/module-reference/utility-modules/shared/scopes/#exposure-adjustment)
: It's too easy to inadvertently mess up exposure when cycling through scopes (especially with a Wacom tablet) and it adds unnecessary GUI motion events and redraws. In cases of multiple instances of exposure module, it is automatically linked to the controls of the last instance, which is generally wrong because it is typically masked.

The new filtering option
: A beautiful shit made of 6300 lines of bloat and still counting, that needs a screen-reader to understand WTF those icons mean, supposed to fix a problem nobody had with something that adds bugs and trouble. That thing is the poster case of everything that's wrong in Geektable 4.0. It should be called a regression. The same feature is achieved in Ansel with about 580 lines of code. And to say that the new filtering started as a refactoring project… Never let the darktable idiots refactor anything, it's too dangerous.

The [stars rating](https://docs.darktable.org/usermanual/3.8/en/lighttable/digital-asset-management/star-color/#star-ratings) toolbar in lighttable
: It's redundant with overlays and requires to travel too far with the cursor to set it. Use overlays with cursor or key shortcuts to set stars.

The [timeline](https://docs.darktable.org/usermanual/3.8/en/module-reference/utility-modules/lighttable/timeline/)
: terrible code and a huge loss of CPU cycles for a niche feature that essentially serves no purpose. This was responsible for way too many SQL requests and removing it is responsible for most of the GUI responsiveness improvement.

The main window borders with collapsing arrows / silly cycling through headers
: The borders marked with arrows allow to collapse or expand the GUI panels. But they waste space, are typically too narrow for many displays, and tend to reappear when you don't want them because their state is defined per view. Collapsing panels is done with <kbd>Ctrl + Shift + L(eft)</kbd>, <kbd>Ctrl + Shift + R(ight)</kbd>, <kbd>Ctrl + Shift + T(op)</kbd> for the header, and <kbd>Ctrl + Shift + F(ilmstrip)</kbd> for the bottom panel. <kbd>Shift + F11</kbd> toggles all panels at once, <kbd>F11</kbd> toggles fullscreen. And the global menu (you know, like in any app since 1990) now exists and takes care of all that from its _Display_ menu, along with everything else darktable buried inside cryptic icon-buttons.
: The silly cycling through all the possible combinations of headers and bottoms that happened when hitting these shortcuts in darktable has been completly removed: each shortcut toggles exactly one panel and nothing else.
: Removing the borders allowed to increase the width of the sidebars scrolling sliders for better interaction.

The [second darkroom window](https://docs.darktable.org/usermanual/3.8/en/darkroom/darkroom-view-layout/)
: darktable is slow enough with 2 pipelines (thumbnail & preview), a third one is not realistic. This should have been the main preview redirected to another window but the code has been hastily hacked by copy-pasting, it's terrible. Also there is no traceability on which display profile will be used, good luck with that.

The block and timeout overlay mode in lighttable
: terrible code for another niche feature that nobody tests until bug appear on the issue tracker that will be answered with "it works on my computer".

The extended overlays mode for thumbnails
: each of the EXIF metadata display triggered its own SQL request per-thumbnail, because no cache was used, because the guy who coded that doesn't learn or doesn't care, but such a loss of CPU cycles is inacceptable.

The non-dynamic culling mode and the zoomable lighttable view
: Both were ripped out in 2023 because they were a pile of spaghetti code duplicating what the lighttable already did, in a worse way. Both came back later, done right: culling is now simply the _Restrict to selection_ filter button (no separate view, no static/dynamic nonsense nobody understood), and the zoomable lighttable returned as a proper _Columns_ + _Zoom_ pair, with barycentric auto-panning so faces roughly line up across a series. Turns out the features were fine; darktable's implementation was the problem.

The dark and icon themes
: dark themes trigger all sorts of unwanted visual illusions that make color assessment impossible. Their use is discouraged everywhere on darktable's doc but they are still there.

The ability to assign keyboard shortcuts to Gtk notebook tabs, as well as switching tabs by mouse scroll.
: Just because you can doesn't mean you should, and capturing mouse scroll on inner tabs messes up with sidebar scrolling (you know, the regular scolling over content that doesn't fit in the viewport).

The VIM-style shortcuts
: I bet you never knew, but if you start typing `:` in darktable, followed by a command, you can trigger stuff. This feature has never documented and the available features are really limited, but that still listens to all keystrokes to check if it should capture them. In Ansel, you can trigger commands by typing <kbd>Ctrl</kbd>+<kbd>P</kbd>, and the whole UI is mapped to it, so it's basically a shortcuts search engine that doubles with a command palette.

The histogram profile
: That thing is coded by copy-pasting the display code, instead of writing reusable functions. It wouldn't be that bad if it wasn't completely misleading and actually broke the over-exposure alert (because, yes, it's used there too). Because the histogram is captured at the far end of the pipeline, in display color space, it might be already clipped to display gamut. Say you use Rec2020 space as the histogram profile, because it's the working space, what you see is RGB clipped to display space then rescaled to Rec2020, which means your clipping peak will not be at the far end of the graph, where you expect it. The result is highly misleading and voids the whole principle of a scope. That's just a whole pile of crap coded by people who have no idea of what a graphic pipeline is and hack small bits here and there because the concept of "modules" fooled them. They might be modular, but they still apply in sequence in the pipeline, so better check your inputs.

Warning messages in modules
: I'm responsible for that stupidity. The warning messages were supposed to tell users they are doing things not really intended by the designer (for example, using 2 different steps of white balance correction) and to ask them to double-check if that's really what they wanted. That backfired badly because users don't get the difference between an error and an alert, so even when instructed to use 2 white balances (for example, a custom-corrected raw WB), they would get afraid by the "error" and annoy me with questions. Given that this feature is clearly not helping anybody, and did not trigger the expected behaviour (that is, RTFM), there is no point in keeping it. The option to hide them is removed as well.

Favourite presets button
: There was this favourite presets button at the left of the bottom panel in darkroom, and I found nobody who ever used it. In fact, most people who I asked did not ever know it even existed. So it's gone.

Importing from camera
: darktable has a way to mount the camera file system (SD or CF memory cards) through USB, using the library GPhoto2. But the desktop environment is also be able to mount the camera as a local hard drive using PTP and/or MTP protocol, possibly also through Gphoto2. In that case, darktable and the OS may compete for access to the USB media, so you need to unmount it first from the OS to be able to open it from darktable. On Windows, you even need to install custom USB drivers to make it work with GPhoto2, but these drivers will prevent the camera to work with the OS until you completly remove and reset them. So this feature is completely removed, and Ansel loads memory cards as USB media through the OS mounts. To enable this, you may have to install Linux packages such as `libmtp`, `linuxptp`, `libptp`, `libgphoto2` and possibly `gphoto2`. For your desktop environnment, you may install `kio-mtp` and `kio-extras` on KDE or `gvfs-mtp` and `gvfs-gphoto2` on Gnome. In any case, you are advised to simply use an SD of CF card reader, it's cheap and prevents most issues that can arise from udev and cameras connected through USB.

Overall, 5 to 6 SQL requests per thumbnail per "mouse over" event are now spared, due to the features removed (which still lets 3 requests…, aka 2 more than what should be).

### Preferences

* The option to display a mask button in module's header is removed and the button is always shown,
* The option to display the scroll bars in lighttable and darkroom is removed, scrollbars will always appear in lighttable and will never appear in darkroom,
* The option to favour speed at the expense of quality is removed (process downscaled thumbnails), quality is favoured. This will produce previews consistent with the main preview and will prevent much confusion among users, at the expense of speed.
* The option to expand modules on "enable" event and collapse on "disable" event is removed, collapsed state is independent from enabled state (I was the author of that stupidity and even I grew weary of this).
* The option to scroll to the expanded module is removed, expanded modules will always trigger a scroll to be fully visible in viewport.
* The option to open only one module at a time is removed. Modules will never be auto-collapsed when a new module is expanded. You may use <kbd>Shift + Click</kbd> on a module header to open this one while collapsing all the others.
* The option allowing to immediately rename a new instance of a module has been removed and users will always be prompted to rename a new instance right after adding it. This simply encourages sane workflows because everybody adding unnamed instances has always hated themselves shortly after. Be nice to your future self people (no, you will not remember).
* The option to disable the silly behaviour removing the star rating if you hit 1 star twice has been removed because WTF ? To remove the star rating, hit 0 and be done with it. 1 means 1.
* The option allowing to compute a downscaled preview pipeline in darkroom is removed, and preview pipelines will always be computed at their correct size. This is a can of worms since neighbourhood filters (local contrast, contrast equalizer, diffuse and sharpen, blur, etc.) are accurate only when zoomed-in at 100%. If you zoom-out at screen size, as to fit the image within the viewport, you already introduce a visual inconsistency that produces misunderstanding among users and lots of false-positive bugs reports. But if you even downscale that, you are really pushing your luck and looking for trouble. You don't pay for that software, at least buy decent hardware to run it properly.
* The option to use single click to open images in darkroom from thumbnail has been removed. That's just not how file managers work and it creates too many opportunities to accidentally open images.

## Modules deprecated

All the remaining Lab modules that I have warned against for years are deprecated, namely:

* Bloom, *(replaced by diffuse or sharpen, preset blooming)*
* Raw chromatic aberrations, *(replaced by RGB chromatic aberrations)*
* Contrast, lightness, saturation, *(replaced by filmic and color balance)*
* Colorize, *(replaced by color balance)*
* Color contrast, *(replaced by color balance)*
* Color correction, *(replaced by color balance)*
* Color mapping, *(replaced by color mapping features in exposure and color calibration)*
* Color reconstruction, *(replaced by filmic color reconstruction, "high quality reconstruction" method)*
* High-pass, *(replaced by diffuse or sharpen, various deblurring presets)*
* Levels, *(replaced by filmic, white and black relative exposures settings)*
* Low light, *(replaced by color balance)*
* Low-pass, *(replaced by blurs)*
* Monochrome, *(replaced by color calibration, "grey" tab, and various film-emulation presets)*
* Raw denoise, *(replaced by denoise profiled)*
* Shadows and highlights, *(replaced by tone equalizer and local contrast)*
* Sharpen, *(replaced by diffuse or sharpen, various deblurring presets)*
* Soften, *(replaced by diffuse or sharpen, blooming preset)*
* Split-toning, *(replaced by color balance)*
* Tone curve, *(replaced by tone equalizer, filmic and color balance)*
* Velvia. *(replaced by color balance)*

The display-referred RGB modules have been deprecated too:

* Base curve, *(replaced by filmic)*
* RGB levels, *(replaced by filmic, white and black relative exposures settings)*

The scene-referred and display-referred processing workflows have been removed too, the scene-referred workflow is the only one and default. The chromatic adaptation workflows are removed too, the modern workflow will always be used.

One of the most common questions, since the big scene-referred change, was "what modules should I avoid". Now, you don't need to ask. Despite what some people say, "deprecated" means the modules can't be found in UI for new edits, but they are still very much in the program and ready to serve for old edits that use them. Deprecating is just GUI cleaning.

The *output color profile* module has been removed from the darkroom GUI. It is **still** applied in the pipeline, so only the GUI widget is hidden, but since the output profile is set either from the display options (for the thumbnails and previews) or in the export module (for file exports), the profile defined in the darkroom module was overwritten anyway, and the duplicated setting confused some users. In the export module, the former "same as image" profile option (that used the options set in the output color profile module) has been replaced by "same as original", which will reuse the embedded color profile if any or fall back to sRGB otherwise (noticeably for raw files). All in all, this only changes **where** the output profile is set in the GUI and de-duplicate this setting, it does not affect the pipeline.

## What was changed from mainstream darktable

### Behaviours

Mouse scroll
: The mouse scroll will scroll the content of the container (sidebars) and will not be captured by sliders, unless sliders got clicked over first (or used through keyboard shortcuts). This restores the intuitive and common behaviour of a mouse scroll and protects from unwanted setting changes. See [#11](https://github.com/aurelienpierreeng/ansel/issues/11). The preference to decide how mouse scroll should be treated is consequently removed.

"Mouse over" event
: In lighttable, the "mouse over" event now does not select images for writing and possibly harmful operation (writing metadata, copying history stack, deleting/moving files, applying styles, rating, labelling, tagging etc.). These "mouse over" events trigger only safe read-only events  in the metadata display module.

Modules listening to "mouse over"
: Similarly, the modules like tagging, geotagging, etc. that reacted to the "mouse over" event (by updating their content) now only react to hard selection (click or key stroke). This saves many SQL requests to read metadata when moving the mouse in lighttable and improves responsiveness.

Copy all
: The "copy all" history stack now copies and pastes white balance too. Meaning it does what it says. That used to be the case, but another hasty hack got that changed for madness 2 years ago.

CSS animations
: Animations are basically videos that need to be computed at 30 FPS while Gtk is only single-threaded on CPU, that's just a stupid loss of CPU cycles.

Filmstrip thumbnails
: The thumbnails interaction have been synchronized with the lighttable :
    * Opening a new image will need double-click
    * Copy/pasting editing histories in filmstrip view over arbitrary selections will need a interaction with a knobby interface (mouse over event will be disregarded),
    * The weird selection modes relying on key modifiers are removed. Selection is done with regular single click, as in lighttable.
    * Rating/rejecting on overlays in filmstrip is prevented because the controls are too small to prevent misclicking accidents.

Keyboard shortcuts
: The great MIDI input thingy of 2021 broke basic functionality and user expectations : the numeric pad events did not listen to the state of `numlock` and were always recorded as `Home`, `End`, `PageDown` etc. Also, on AZERTY and BÉPO keyboards, the regular number keys are accessed through `Shift`. This broke the picture ratings because numbers from the numeric pad were not recorded as numbers, and the regular numbers did nothing. When reconfiguring the rating shortcuts with regular numbers, you would then see things like `Shift + &` on AZERTY or `Shift + "` on BÉPO, instead of the expected `1`.
: Then, to get the numpad numbers to work, you had to manually add them as alternative shortcuts. That's a backstep of more than 25 years regarding UI, all because the MIDI developer thought it was clever to disregard OS key mapping and handle it internally. This has been fixed in a way that gets the OS-decoded key (including modifiers like `Shift` and `numlock` when needed), and then remaps all numpad key strokes to regular key strokes, so the `Enter`, `*`, `/`, `Delete` and all the numeric keys will be treated equally no matter where they are input, without having to duplicate shortcuts for the numpad. It is crazy that this should be announced as a new feature in 2023 and is another proof that darktable development went south thanks to die-hard geeks.
: Triple-click has also been removed from shortcuts because promoting repetitive stress injury by design is totally stupid.
: Since then, the whole shortcuts handler — the infamous "Great MIDI turducken" of 2021 — has been [rewritten from scratch](getting-started/keyboard.md), in a fifth of the code and a sixth of the complexity. MIDI and gamepad support are gone (2% of users, for a tertiary feature), and in their place the interface is fully navigable from the keyboard without memorizing a single shortcut, with a global action search (<kbd>Ctrl + P</kbd>) that finds and triggers any command by name. The full story is in the [news](https://ansel.photos/en/news/).

### Views

The less-used views like _Map_, _Print_ and _Slideshow_ are hidden by default, but can be re-enabled from the _Preferences_ -> _Other views_. This prevents GUI bloat since the `.so` objects of the views are simply not loaded at startup (they are not hidden from the GUI, they completely don't exist). This has the drawback of deleting user-defined shortcuts if any, for these views. But the issue is Gtk initializes and redraws all GUI widgets, whether they are visible or not, so even hidden and unused views will drain your CPU.

Users who want to preserve their shortcuts for the 3 removed views may edit the `~./config/ansel/anselrc` file by adding the following lines __before__ starting the software :

```ini
/views/map/enable=TRUE
/views/print/enable=TRUE
/views/slideshow/enable=TRUE
```

### Widgets

Capitals
: Modules names, buttons and section labels have their initial letter capitalized programmatically as grammar and typography rules suggests for better visual cues and for consistency with the rest of the world. This does not break translations but may trigger weird behaviours with accented characters. This is a work in progress and not all places have capitals yet. It uses sentence capitalization because the code to do so is 4 lines, while using title capitalization would require tokenization and is far less robust to achieve across a variety of languages.

Export
: The "export" module is moved to the left sidebar in lighttable, for consistency with import, collect, etc.

DAM modules removed from darkroom
: The metadata, tags and export modules are removed from darkroom, since they are DAM features. Exporting an image cannot be done from the darkroom to prevent running 3 concurrent pipelines, which can produce crashes and loss of data if the available RAM is exceeded (the RAM allocation code was still broken back then, especially on GPU; it has since been rewritten from scratch, so Ansel now knows exactly how much memory it uses and never exceeds the user-defined budget). This also frees some space for the image processing widgets in darkroom.


Modules reorganized
: Darktable's "modules" lumped two different things behind the same look: image-processing filters (which live in the pipeline) and arbitrary toolboxes (which are just panels of buttons). The toolboxes have been renamed to plain functional names and sorted by where they belong.
: In the lighttable, a single **left** sidebar holds the file and metadata tools — "Library" (the old "collect"/"collections"), the metadata editor, tagging, "Datetime and GPS" (geotagging) and image information. The whole **right** sidebar of the lighttable has been removed: once the toolboxes-that-are-really-menus moved out, there was nothing worth keeping there, and the freed space went to the thumbnails.
: In the darkroom, the **right** sidebar is reserved for the image-processing modules, and the **left** sidebar holds the editing helpers (scopes, snapshots, history of changes, duplicates, mask manager…).

Toolboxes moved to the global menu
: The "modules" that took no user input and only displayed buttons — selection helpers, image actions, history copy/paste, styles management, the maintenance scripts — have been moved into the global menu, where hierarchical lists of buttons have belonged since graphic designers invented drop-down menues in the 1980's. Another thing darktable failed at, for years, while shipping grids of cryptic icons instead.

## Presets

Exposure module
: The exposure boost has been changed from +0.5 to +0.7 EV,

Filmic
: In filmic module, the contrast is changed to 1, latitude is set to 0.01% (which practically disables it — doesn't matter for v6), and the interpolation spline is set to back to "hard" (fourth order polynomial with $C^2$ connectivity condition).

Color balance RGB
: This module is now enabled by default with the "standard colorfulness" preset, as to give a base rendition closer to the infamous out-of-camera JPEG.

Lens correction
: This module is now enabled by default because I couldn't think of a photographer __not__ wanting to correct lenses.

Dithering
: This module is now enabled by default because it does no harm but protects JPG and other low-bit-depth exports from posterization.
---

# Install : https://ansel.photos/en/doc/install/

## Dependencies for Source Builds

If you plan to build from source, the Ansel repository ships per-platform dependency installer scripts in `packaging/`. Use the script matching your OS (for example `./packaging/install-deps-ubuntu.sh`, `./packaging/install-deps-macos.sh`, or `./packaging/install-deps-windows-msys2.sh`).

## Understanding Ansel's releases

### Ansel channels

Ansel development and release path follows 3 channels :

1. A __production channel__ :
	1. Linked to the Git [master branch](https://github.com/aurelienpierreeng/ansel),
	2. Contains tested code supposed to work reliably for daily use,
	3. Contains features found under the _Done_ column, in the [project management board](https://github.com/orgs/aurelienpierreeng/projects/1).
2. A __pre-release channel__ :
	1. Backward-compatible with the production channel _(so you can switch from one to another without breaking your image edits)_,
	2. Linked to the Git [candidate branch](https://github.com/aurelienpierreeng/ansel/tree/candidate),
	3. Contains code to test before integration in production channel but still relatively safe to use,
	4. Contains features found under the _Testing_ column, in the [project management board](https://github.com/orgs/aurelienpierreeng/projects/1).
3. An __experimental channel__ :
	1. __non-backward-compatible__ with either the production or pre-release channels,
	2. Linked to the Git [dev branch](https://github.com/aurelienpierreeng/ansel/tree/dev),
	3. Contains features found under the _In progress_ column, in the [project management board](https://github.com/orgs/aurelienpierreeng/projects/1),
	4. This channel needs to be used aside of the production install since it contains prototypes of new modules, new versions of old modules, and other features that will upgrade the version of the [Ansel database](../preferences-settings/storage#database), making it incompatible with previous versions. It can also contain prototypes that will never make it in production.

At all time, the  [project management Kanban board](https://github.com/orgs/aurelienpierreeng/projects/1) will show what changes are being currently worked on in the non-production channels, with links to their pull request and issue, to help traceability of changes and bug reports.

### Ansel versioning

The __production channel__ is versioned as follow :

1. All versions having the same major version number (`0.x`, `1.x`, `2.x`) produce editing histories that are compatible with each other and use the same internal [database](../preferences-settings/storage#database) version. Major version numbers are upgraded every time changes are introduced that break this property, such as new modules, database structure changes, or new versions of old modules.
2. Minor version numbers (`x.0`, `x.1`, `x.2`) are upgraded every time new changes are made that don't break compatibility within the major version, like GUI refactorings and bug fixes.
3. The major version `0.x` is compatible with darktable 4.0 and 4.0.1 image editing histories, which means it is also compatible with edits made with any darktable version earlier than 4.0. Ansel will not maintain compatibility with upstream darktable for future versions after `0.x`, since darktable 4.2 will introduce nonsensical changes that will stay the burden of the darktable team only.
4. Versions are tested snapshots of the __production channel__. This channel will keep getting changes in-between versions, notably from the __pre-release channel__, supposed to be safe between versions.

### Ansel testing

Tests are important because I develop and use Ansel on Linux Fedora, and I have no way of knowing how it works on other OS. A [continuous integration](https://github.com/aurelienpierreeng/ansel/actions/workflows/ci.yml) bot automatically tests building and basic execution (processing a test picture through the [CLI variant](../cli/ansel-cli) of the software), on Windows 10, Linux Ubuntu 20.04 and 22.04 and Mac OS 11.7 and 12.6. On each OS, the CI bot builds using GCC 9 to GCC 12 and Clang 12 to 14.

But those continuous integration tests are not enough as they don't encompass a real desktop use with a graphical interface and will not show runtime errors. The different channels are meant to gradually push changes to production.

```mermaid
graph TD;
	master[/Stable/];
	candidate[/Pre-release/];
	dev[/Experimental/];
	master2[/Stable/];
	master3[/Stable/];

	master --> m1((Merge));
	master --> m2((Merge));

	master --> dev;
	dev --> c3[fa:fa-code Make unsafe changes];
	c3 --> t3[fa:fa-clock Testing];
	t3 --> r3{Bugs ?};
	r3 -- no ---> m2;
	r3 -- yes --> f3[Fix];
	f3 --> t3;
	m2 --> p3([fa:fa-tag Tag new major version]);
	p3 --> master2;

	master --> candidate;
	candidate --> c1[fa:fa-code Make safe changes];
	c1 --> t1[fa:fa-clock Testing];
	t1 --> r1{Bugs ?};
	r1 -- no -----> m1;
	r1 -- yes --> f1[Fix];
	m1 --> p1([fa:fa-tag Tag new minor version]);
	f1 ---> t1;
	p1 --> master2;

	master2 --> t2[fa:fa-clock Testing];
	t2 --> r2{Bugs ?};
	r2 -- no ---> p2([fa:fa-tag Tag new maintenance version])
	r2 -- yes --> f2[Fix];
	f2 --> t2;
	p2 --> master3;

```

This graph shows how tests are dispatched in the general development workflow. Pre-built "nightly" packages are provided for all 3 channels (stable, pre-release and experimental) for all the platforms (Linux, Windows, Mac OS) and can be tested directly by anyone.

After some time without bug reports, the pre-release and experimental channels are successively merged into the stable channel, letting some time between both merges.

After some time with no bug report in the stable channel, a new maintenance release gets tagged. This means that the `x.0.0` major versions are less tested than the `x.0.1` and `x.1.0`, and should be avoided in environments where stability is the priority.

### Release schedule

The __production channel__ is updated in a rolling-release fashion. Each change triggers automatic builds on Linux, Windows and Mac OS, producing "nightly" packages that can be directly downloaded and installed on each platform. You can subscribe to these nightly builds to update Ansel in a rolling-release fashion.

Major, minor and maintenance versions are released when they are ready.

## Ugrading between major releases

Major releases are defined by the fact that the [Ansel database](../preferences-settings/storage#database) structure will be upgraded and therefore will not be usable by previous major releases. Before the ugrading process starts, Ansel automatically backs up the database in the configuration folder, appending the new version number to the database filename.

The configuration folder of Ansel is located by default at :
* `$HOME/.config/ansel` for Mac OS and Linux
* `C:\%LOCALAPPDATA%\ansel` on Windows.

In this configuration folder, the image editing histories are stored in the file `library.db` and the presets, styles, and other preferences are stored in the file `data.db`.

If you upgrade, for example, from version 2.6 to 3.0, both files are backed up under the respective names `library.db-pre-3.0`  and `data.db-pre-3.0`. If, for some reason, you want to go back to the previous 2.6 version after having upgraded to 3.0, you just need to remove the `pre-3.0` suffixes from the file names to restore your back-ups. Any editing you may have done with the 3.0 version will be lost without a chance of being recovered in that case, unless you manually back up `library.db` and `data.db` for a future where you will use the 3.0 version.

{{< warning >}}
There is no backup mechanism in place for the [XMP sidecar files](../views/lighttable/digital-asset-management/sidecar.md), but they can be restored from the `library` database, which means you need to keep it clean and up-to-date.
{{</ warning >}}
---

# Install on Linux : https://ansel.photos/en/doc/install/linux/

## Prerequisites

Install your GPU OpenCL drivers if you have a GPU :
- For __Nvidia__ GPUs, OpenCL support is part of the CUDA proprietary driver and is not available with the open-source _Nouveau_ driver:
	- [add Nvidia's repository to your package manager](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#package-manager-installation),
	- [install the CUDA packages](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#driver-installation),
	- Reboot.
	- __It is highly discouraged to manually install drivers through [runfiles (.run)](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#runfile-installation) as it is riddled with problems you will need to manually fix everytime the Linux kernel is updated__.
- For __AMD__ GPUs, the [AMDGPU-Pro](https://www.amd.com/en/support/kb/faq/amdgpu-installation) driver is recommended:
	- [download the relevant driver](https://www.amd.com/en/support),
	- [extract the archive](https://www.amd.com/en/support/kb/faq/amdgpu-installation#faq-Prerequisites),
	- Run the installation script:
		- If you want only the OpenCL driver, without video drivers and OpenGL (assuming your system already has some of those working): `./amdgpu-pro-install -y --opencl=pal,legacy --headless`,
		- If you want the complete GPU stack (OpenCL/OpenGL, Vulkan, video drivers): `./amdgpu-pro-install -y --opencl=pal,legacy`è
	- Reboot.
- For __Intel__ embedded GPUs, the _Neo_ driver provides support for OpenCL 3.0 on relatevly recent
	- [official driver releases](https://github.com/intel/compute-runtime/releases/latest) contain the Ubuntu `.deb` packages, you need to install `intel-opencl-icd_xxxx.deb`,
	- The safest way is to locate the `intel-opencl-icd` package in your distribution's repository, and install it. It can be part of optional or third-party repositories for distributions leaning a bit too hard toward open-sourcery (Debian/Fedora).


If you don't have a GPU, a manual build is recommended to get the best performance possible (see below).

For a better GUI, install [Roboto fonts](https://fonts.google.com/specimen/Roboto). It is available as a package in most distributions. It has been chosen because it is clean, designed for software GUI and supports 311 languages, including extended Latin, Cyrillic and Greek alphabets.

## AppImage package (recommended)

The Ansel project provides an official AppImage package, built for the stable channel, every night at 00:00 UTC. This is the recommended way of installing Ansel, since it is fresh from the repository, always up-to-date, contains all necessary libraries and dependencies at their proper version, and ships updated lens databases for Lensfun.

The AppImage is build on an Ubuntu 18.04 machine, which is old enough to support most currently-running Linux distributions.

### Prerequisites

- Install [Fuse2](https://docs.appimage.org/user-guide/troubleshooting/fuse.html),
- Ensure your Linux distribution runs at least `libc`/`glibc` version 2.27, that is :
	- Ubuntu ≥ 18.04,
	- Fedora ≥ 28,
	- Debian ≥ Buster,
	- OpenSuse Leap ≥ 15.3.

In practice, most Linux distributions released after 2018 and all distributions released after 2021 are good to go.

### Downloads

- [Get the latest AppImage](https://nightly.link/aurelienpierreeng/ansel/workflows/lin-nightly/master/ansel.stable.AppImage.zip),
- [Find earlier AppImage packages](https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0).

This will most likely put the AppImage package into your `~/Downloads` folder, you may want to move it to a dedicated folder like `~/bin`, into your personal directory.[^1]

[^1]: The `~/bin` folder is natively supported by [appimaged](https://github.com/AppImageCommunity/appimaged).

### Run the AppImage

- [Give it execution permission](https://discourse.appimage.org/t/how-to-run-an-appimage/80/4).
- Either double-click on the AppImage file from your file browser or launch `./Ansel-xxxx-x86_64.AppImage` in terminal.
- Command-line arguments are forwarded to the packaged binaries, so `./Ansel-xxxx-x86_64.AppImage --version`, `./Ansel-xxxx-x86_64.AppImage -d dev -d perf -d opencl -d verbose` and `./Ansel-xxxx-x86_64.AppImage /path/to/image.raw` work as expected.
- Auxiliary command-line tools can be called through the AppImage too, by passing the tool name as the first argument, for example:
```bash
./Ansel-xxxx-x86_64.AppImage ansel-cli input.raw output.tif
./Ansel-xxxx-x86_64.AppImage ansel-cltest
./Ansel-xxxx-x86_64.AppImage ansel-cmstest
./Ansel-xxxx-x86_64.AppImage ansel-generate-cache
./Ansel-xxxx-x86_64.AppImage ansel-noiseprofile
./Ansel-xxxx-x86_64.AppImage ansel-gen-noiseprofile
```

To create a system shortcut and have Ansel accessible from your dash/app menu, you can use [AppImageLauncher](https://assassinate-you.net/posts/2020/09/appimagelauncher-2.2.0-released/), available as `.deb`, `.rpm` and `.AppImage` packages ([download](https://github.com/TheAssassin/AppImageLauncher/releases/tag/v2.2.0)). It will integrate the AppImage with your desktop environment with or without running extra services in background, upon request, and can update the Ansel AppImage.

### Update the AppImage

Automatic updates are not available at this point, you may use [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) to update the Ansel AppImage. The benefit of this method is to allow incremental updates, avoiding to re-download a ~95 MB file every time.

### Caveats

Though the Lensfun database of lens profiles is stored and up-to-date in the AppImage package, if you installed a custom Lensfun database at some point in your home directory (usually, by running the command `lensfun‑update-data`), this database can take precedence over the one shipped in the AppImage.

If you note that a relatively-new lens supposed to be supported by Lensfun doesn't appear to be supported in the [lens correction module](../views/darkroom/modules/lens-correction.md), either run the `lensfun-update-data` command again, or simply delete the local database, usually located in `~/.local/share/lensfun/updates`.

The AppImage intentionally uses the host OpenMP runtime (`libgomp`) instead of bundling its own copy. This avoids mixing a packaged OpenMP runtime with the host OpenCL and GPU drivers, which has been observed to trigger stability issues on some systems.

## Packages from Linux distributions repositories

Pre-built packages are provided by third-party maintainers, may not be up-to-date and may be compiled with some optional features disabled and other distro-centric customizations. They are outside of the scope of the Ansel project and no support is offered. They are mentionned here for what they are worth.

 - [Arch Linux Repository (AUR)](https://aur.archlinux.org/packages/ansel-git).

## Building from source code manually

Building manually ensures that the software uses all available optimizations for your particular hardware, while using the pre-built package uses generic optimizations that fit all modern 64 bits architectures. Depending on the modules you use and on your CPU, you may experience a 25-30 % speed-up by using a manual build taylored for your hardware, when not using OpenCL.

When using OpenCL, the GPU code is compiled for your particular hardware whether you use a pre-built package, the AppImage or a manual build, so it will make no difference for OpenCL-ready modules, but not all modules have an OpenCL variant and many parts of the software run on CPU (like the picture codecs).

### Prerequisites

- Install `git` through your package manager,
- In a terminal, run :
```bash
$ git clone --depth 1 https://github.com/aurelienpierreeng/ansel.git
$ cd ansel
$ git submodule init
$ git submodule update
```
- Install the dependency packages (see below).

### Dependencies

The repository includes per-distro dependency installers in `packaging/`. From the repo root, run one of:

```bash
./packaging/install-deps-ubuntu.sh
./packaging/install-deps-debian.sh
./packaging/install-deps-fedora.sh
./packaging/install-deps-arch.sh
./packaging/install-deps-suse.sh
./packaging/install-deps-nix.sh
```

These scripts install the build/runtime dependencies used by CI and packaging.

Note that optional dependencies don't make the script abort when they are not found, but will disable the corresponding features. You need to read the build script output to find out if everything works as you expect it.

{{< warning >}}
Fedora (and possibly other distributions) builds the Exiv2 library (required by Ansel to read image metadata) without the ISOBMFF support. Since Canon CR3 raw files are ISOBMFF containers, this makes Ansel built on Fedora unable to open .CR3 files. You will need to build Exiv2 yourself too (see below).
{{</ warning >}}

### Compile the application

#### Easy way

From the source code directory, you need to invoke the build script as follow:

```bash
$ sh build.sh --install --sudo --clean-all
```

This will:

- Cleanup any remnant of a previous Ansel build and installation (avoiding weird corner-cases),
- Build the software with the most aggressive level of optimizations for your particular hardware (the produced binary will not be portable to another hardware),
- Install the software in `/opt/ansel`
- Install a system-wide command `ansel` that can be invoked in a terminal,
- Install a system-wide desktop launcher,
- Update Lensfun database of lenses profiles.

To enable less-aggressive optimizations (if you discover picture artifacts) and install the software somewhere else, use:

```bash
$ sh build.sh --prefix /opt/YOUR/PATH --build-type RelWithDebInfo --install --sudo --clean-all
```

Invoke `sh build.sh --help` for a complete overview of available options. In most cases, you can skip `--clean-all` for a faster build (so it does not rebuild from scratch), but that can cause inconsistencies sometimes, and spotting those is a lot more time-consuming than simply rebuilding from scratch.

#### Custom way

If you don't have a good reason to do that, don't, and use the previous method.

```bash
$ cd ansel
$ mkdir build
$ cd build
$ export CXXFLAGS="-O3 -fno-strict-aliasing"
$ export CFLAGS="$CXXFLAGS"
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -G Ninja -DCMAKE_BUILD_TYPE=Release -DBINARY_PACKAGE_BUILD=ON -DCMAKE_INSTALL_LIBDIR=lib64
$ sudo cmake --build . --target install -- -j 8
```

### Run the self-built application

You can run `ansel` or `/opt/ansel/bin/ansel` from the terminal, or use your applications menu to locate Ansel.

### Update the self-built application

You can run the build script above with the extra argument `--update` like:

```bash
$ sh build.sh --install --sudo --clean-all --update
```

The caveat is this will update the source code except the build script itself, which may cause issues if the build script was modified (but that rarely happens). To overcome this, you can execute the script twice, or update manually with:

```bash
$ cd ansel
$ git pull --recurse-submodules
```

Then run the building script above, same as when you installed.

### Caveats

If you need to build Exiv2 yourself, either because the version provided by your distribution is too old, or it is built without ISOBMFF support and therefore does not support Canon CR3, here is the sequence of commands to input in terminal:

```bash
$ git clone https://github.com/Exiv2/exiv2.git
$ cd exiv2
$ git checkout 0.27-maintenance
$ cmake -B build -G Ninja -DEXIV2_ENABLE_XMP=ON -DEXIV2_ENABLE_BMFF=ON
$ ninja -C build
$ sudo ninja -C build install
```

It appears that most Linux distributions provide outdated Lensfun databases (even though the binary is up-to-date), so you may want to update it: `sudo lensfun-update-data`

## Containers and sandboxes (Docker, Kubernetes, Windows Subsystem for Linux, etc.)

Ansel is a front-end desktop application that needs a CMS (_Color Management System_) having direct access to the GPU (graphic processor).

Indeed, even if you don't use OpenCL hardware acceleration, display color profiles (`.icc` and `.icm`) often contain an "hardware" calibration tagged VCGT (_Video Card Gamma Table_) that typically calibrates the gamma and white point of the screen. This VCGT is directly pushed to a special memory onto the GPU, used to color-correct the whole screen framebuffer, whether or not individual applications are actually color-managed internally.

Internally color-managed applications may then apply a second round of color correction, aiming at converting standard RGB spaces to the display RGB space (typically through a matrix conversion). The correctness of this second round is subjected to the correctness of the first round: the VCGT.

This design is of course terrible, because different parts of the OS (operating system) can compete for access to the VCGT (most obvious example: the redshift apps adjusting the color temperature of your screen depending of the time of day, as to reduce blue light at night and help sleep), and overwrite it at different times, making the VCGT unreliable and unpredictable.

When using containers and sandboxed environments like Docker, or WSL2, there is no way of knowing how the framebuffer will be color-corrected, that is no way of knowing what part of the pipeline (host OS or container) will get the last say over what happens in the VCGT. These systems have been designed to run text-based and server-side applications, image production applications have never been a goal.

__It is highly discouraged to try and run Ansel in Docker/Kubernetes containers or in the Windows Subsystem for Linux as these have no way of ensuring color traceability.__

## 32 bits platforms, Raspberry Pi and non-conventional platforms

Though the support for 32 bits platforms was officially dropped as of Darktable 2.2, some people have reported to be able to install later Darktable versions (so possibly Ansel) on them. Similarly, people have reported successful installations of Darktable on Raspberry Pi which, despite being a 64 bits platform, doesn't nearly have sufficient power to process 24 to 52 Mpx images.

Users are hereby discouraged from trying to install Ansel on anything else than a typical x86_64 desktop computer. Ansel uses heavy image processing and being able to install and start the software on some weak hardware doesn't imply the runtimes will be bearable in a production environment.
---

# Ansel : https://ansel.photos/en/doc/cli/ansel/

The `Ansel` binary starts Ansel with its GUI and full functionality. This is the standard way to use Ansel.

`Ansel` can called with the following command line parameters:

```
Ansel [-d {all,act_on,cache,camctl,camsupport,control,demosaic,
               dev,fswatch,imageio,input,ioporder,lighttable,lua,
               masks,memory,nan,opencl,params,perf,print,pwstorage,
               signal,sql,tiling,undo,verbose}]
          [<input file>|<image folder>]
          [--version]
          [--disable-opencl]
          [--library <library file>]
          [--datadir <data directory>]
          [--moduledir <module directory>]
          [--tmpdir <tmp directory>]
          [--configdir <user config directory>]
          [--cachedir <user cache directory>]
          [--localedir <locale directory>]
          [--luacmd <lua command>]
          [--noiseprofiles <noiseprofiles json file>]
          [--d-signal <signal>]
          [--d-signal-act <all,raise,connect,disconnect,print-trace>]
          [--conf <key>=<value>]
          [-t <num openmp threads>]
```

All parameters are optional. In most cases Ansel should be started without any additional parameters, in which case Ansel uses suitable defaults.

`-d {all,act_on,cache,camctl,camsupport,control,demosaic,dev,fswatch,imageio,input,ioporder,lighttable,lua,masks,memory,nan,opencl,params,perf,print,pwstorage,signal,sql,tiling,undo,verbose}`
: Enable debug output to the terminal. There are several subsystems of Ansel and each of them can be debugged separately. You can use this option multiple times if you want to debug more than one subsystem (e.g. `Ansel -d opencl -d camctl`) or debug all of them at once (with `-d all`). Some debug options (like `-d opencl`) can also provide more verbose output, which can be invoked with the additional option `-d verbose`. The verbose option must be explicitly provided, even when using `-d all`.

`--d-signal <signal>`
: If `-d signal` or `-d all` is specified, specify the signal to debug using this option. Specify `ALL` to debug all signals or specify signal using it's full name. Can be used multiple times.

`--d-signal-act <all,raise,connect,disconnect,print-trace>`
: If `-d signal` or `-d all` is specified, specify the signal action to debug using this option.

`<input file>|<image folder>`
: Optionally supply the name of an image file or folder. If a filename is given Ansel starts in darkroom view with that file opened. If a folder is given Ansel starts in lighttable view with the content of that folder as the current collection.

`--version`
: Print the Ansel version number, a copyright notice, some other useful information, and then terminate.

`--disable-opencl`
: Prevent Ansel from initializing the OpenCL subsystem. Use this option if Ansel crashes at startup due to a defective OpenCL implementation.

`--library <library file>`
: Ansel keeps image information in an sqlite database for fast access. The default location of that database file is `$HOME/.config/Ansel/library.db`. Use this option to provide an alternative location (e.g. if you want to do some experiments without compromising your original `library.db`). If the database file does not exist, Ansel creates it for you. You may also provide `:memory:` as the library file, in which case the database is kept in system memory -- all changes are discarded when Ansel terminates.

: Whenever Ansel starts, it will lock the library to the current user. It does this by writing the current process identifier (PID) into a lock file `<library file>.lock` next to the library specified. If Ansel finds an existing lock file for the library, it will terminate immediately.

`--datadir <data directory>`
: Define the directory where Ansel finds its runtime data. The default location depends on your installation. Typical locations are `/opt/Ansel/share/Ansel/` and `/usr/share/Ansel/`.

`--moduledir <module directory>`
: Ansel has a modular structure and organizes its modules as shared libraries for loading at runtime. This option tells Ansel where to look for its shared libraries. The default location depends on your installation. Typical locations are `/opt/Ansel/lib64/Ansel/` and `/usr/lib64/Ansel/`.

`--tmpdir <tmp directory>`
: Define where Ansel should store its temporary files. If this option is not supplied Ansel uses the system default.

`--configdir <config directory>`
: Define the directory where Ansel stores user-specific configuration. The default location is `$HOME/.config/Ansel/`.

`--cachedir <cache directory>`
: Ansel keeps a cache of image thumbnails for fast image preview and precompiled OpenCL binaries for fast startup. By default the cache is located in `$HOME/.cache/Ansel/`. Multiple thumbnail caches may exist in parallel -- one for each library file.

`--localedir <locale directory>`
: Define where Ansel can find its language-specific text strings. The default location depends on your installation. Typical locations are `/opt/Ansel/share/locale/` and `/usr/share/locale/`.

`--luacmd <lua command>`
: A string containing lua commands to execute after lua initialization. These commands will be run after your “luarc” file.
: If lua is not compiled-in, this option will be accepted but won't do anything.

`--noiseprofiles <noiseprofiles json file>`
: Provide a json file that contains camera-specific noise profiles. The default location depends on your installation. Typical locations are `/opt/Ansel/share/Ansel/noiseprofile.json` and `/usr/share/Ansel/noiseprofile.json`.

`--conf <key>=<value>`
: Ansel supports a rich set of configuration parameters defined by the user in `$HOME/.config/Ansel/Anselrc`. You may temporarily overwrite individual settings on the command line with this option -- these settings will not be stored in `Anselrc` on exit.

`-t <num openmp threads>`
: limit number of openmp threads to use in openmp parallel sections
---

# Sidecar files : https://ansel.photos/en/doc/views/lighttable/digital-asset-management/sidecar/

Ansel is a non-destructive image editor and opens all images in read-only mode. Any data created within Ansel (metadata, tags, and image processing steps) is stored in separate `.XMP` _sidecar_ files. These files are stored alongside the original Raw files and allow Ansel to store information about the images as well as the full editing history without touching the original raw files. When you import an image into Ansel for the first time, an XMP file is automatically generated. The generation of XMP files can be disabled in [preferences > storage > xmp](../../../preferences-settings/storage.md#xmp) but this is not recommended in normal use.

For a given source image, multiple editing versions, called _duplicates_, can co-exist, sharing the same input image data but each having their own metadata, tags and processing steps. Each duplicate of a given image (named `<basename>.<extension`) is represented by a separate XMP sidecar file (with a filename constructed in the form `<basename>_nn.<extension>.xmp`, where `nn` represents the version number of that edit). Information for the initial edit -- the “duplicate” with version number zero -- is stored in the sidecar file named `<basename>.<extension>.xmp`. The version number of each duplicate is displayed in the [image information](../../toolboxes/image-information.md) module in each of Ansel's views.

Your work is automatically synchronised to the sidecar files without the need to press a “save” button. When backing up your data, make sure that you also retain copies of the XMP files, as these are required to fully reconstruct your work in case of a disaster.

In addition to the sidecar files, Ansel keeps all image-related data in its library database for fast access. An image can only be viewed and edited from within Ansel if its data has first been loaded into the library database. This happens automatically when you first [import](../../../getting-started/import.md) an image. If an image is subsequently re-imported, the database will be updated from the contents of its XMP file.

Once an image has been imported into Ansel, the database entries take precedence over the XMP file. Subsequent changes to the XMP file by any other software are not visible to Ansel -- such changes will be overwritten the next time Ansel synchronizes the file. On request, Ansel can be configured to search for updated XMP files at startup, offering a choice to update the database or overwrite the XMP file where changes are identified. This configuration can be changed in [preferences > storage > xmp](../../../preferences-settings/storage.md#xmp).
---

# The anatomy of a processing module : https://ansel.photos/en/doc/views/darkroom/pixelpipe/the-anatomy-of-a-module/

The basic element of image processing in Ansel is the [processing module](../modules/_index.md). In order to process a raw image a number of such modules act on the input image in sequence, each performing a different _operation_ on the image data. For those familiar with Adobe Photoshop, the concept of a _processing module_ in Ansel is analogous to that of an _adjustment layer_ in that both make an incremental adjustment to the image, building on top of the adjustments that came before.

[Utility modules](../../toolboxes/_index.md) are also provided by Ansel, however these are not directly involved in image processing, instead providing a GUI that allows you to manage your images, tag them, export them etc.

Every processing module acts independently of the others, but all modules perform their processing in a similar manner:

```mermaid
flowchart TD
    IN([module input]) --> OP[1 · operation]
    OP --> PO([processed output])
    IN --> BL[2 · blend operator]
    PO --> BL
    BL --> BO([blended output])
    MK[3 · mask:<br/>per-pixel opacity] --> MIX
    IN --> MIX[4 · mix by mask]
    BO --> MIX
    MIX --> FO([final output → next module])
```

1. Receive the _module input_ from the last executed module and perform an _operation_ on it to produce the _processed output_. This _operation_ is different for every [module](../modules/_index.md).

2. Combine the _module input_ and _processed output_ using a [blending operator](../masking-and-blending/blend-modes.md) to produce the _blended output_. If no blending is performed, the output of this step is the same as the _processed output_.

3. Generate a _mask_, which defines an _opacity_ for each pixel in the image. The _opacity_ is later used to control how strongly the module's operation is applied to each part of the image.

   You may define your own mask by drawing shapes over the image or by using pixel properties from the _module input_ or _processed output_ (see [masks](../masking-and-blending/masks/_index.md) for details). This mask may be further modified with a global opacity setting, which affects every pixel equally.

   If no drawn/parametric mask is used, the output of this step is a mask where every pixel has the same opacity (governed by the global opacity setting). If no opacity is defined (no blending is performed) a global opacity of 1.0 (or 100%) is assumed.

4. Combine the _module input_ and _blended output_ pixel-by-pixel using the _mask_ as a mixing operator, to produce the _final output_. Writing $o$ for the effective per-pixel opacity (the global opacity multiplied by the mask value at that pixel), the final output of each pixel is:

   ```math
   \text{final} = (1 - o)\,\text{input} + o\,\text{blended}
   ```

   Where the mask opacity is 100% ($o = 1$), the _final output_ is the _blended output_ for that pixel; where the mask opacity is 0 ($o = 0$), it is the _module input_; an intermediate opacity combines the two proportionally. The _final output_ is passed to the next module for further processing.

Steps 2 and 3 are optional and not supported by all modules. For example, the [demosaic](../modules/demosaic.md) module must be applied to the entire raw file in order to produce a legible image so it does not make sense to mask or blend its output.

Each of the above steps is defined in more detail in subsequent sections.
---

# The background : https://ansel.photos/en/doc/preferences-settings/performance/opencl/background/

Processing high resolution images is a demanding task requiring a modern computer. In terms of both memory and CPU power, getting the best out of a typical 15, 20 or 25 Megapixel image can quickly take your computer to its limits.

Ansel's requirements are no exception. All calculations are performed on 4 x 32bit floating point numbers. This is slower than “ordinary” 8 or 16 bit integer algebra, but eliminates all problems of tonal breaks or loss of information.

A great deal of optimization has been undertaken to make Ansel as fast as possible. If you run a current version of Ansel on a modern computer, you might not notice any “slowness”. However, there are conditions and certain modules where you will feel (or hear from the howling of your CPU fan) how much your poor multi-core processor has to struggle.

That's where OpenCL comes in. OpenCL allows Ansel to take advantage of the enormous power of modern graphics cards. Gamers' demand for highly detailed 3D worlds in modern shooters (as well as cryptocurrency mining) has fostered rapid GPU development. AMD, NVIDIA and Co had to put enormous processing power into their GPUs to meet these demands. The result is modern graphics cards with highly parallelized GPUs that can quickly calculate surfaces and textures at high frame rates.

You are not a gamer and you don't take advantage of that power? Well, then you should at least use it in Ansel! For the task of highly parallel floating point calculations modern GPUs are much faster than CPUs. This is especially true when you want to repeat the same few processing steps millions of times. Typical use case: processing high megapixel images.
---

# News : https://ansel.photos/en/news/
---

# Global menu : https://ansel.photos/en/doc/views/global-menu/

The global menu bar sits at the top-left of the [header](_index.md#header-top-panel) and is available in every view. It gathers the application-wide commands, image operations, history and style management, maintenance tasks, display settings, view switching and help. It is the central reference point of the interface: most commands that are not a per-module setting live here.

{{< note >}}
Coming from Darktable: many commands that were previously buried inside side-panel modules, cryptic icon buttons or undocumented keyboard shortcuts have been collected into this menu. "Modules" that were really just grids of buttons (image actions, view toolbox, selection helpers…) became menu entries.
{{< /note >}}

## Using the menu

- Click a menu title to open it, or press <kbd>Alt</kbd> to reveal the underlined **mnemonic** letters and press one to open the matching menu. Once a menu is open, navigate all menus with the arrow keys and trigger an entry with <kbd>Enter</kbd>.
- Entries that have a default keyboard shortcut show it on their right. All these shortcuts are **defaults that you can change** from _Edit → Keyboard shortcuts…_ (see [shortcuts and keyboard interaction](../getting-started/keyboard.md)).
- Many entries act on the **active images** — that is, the images currently [selected](../getting-started/keyboard.md#thumbtable) in the lighttable or filmstrip. When nothing is selected, those entries are greyed out.

The menus are described below in their bar order.

## File

Image-file and collection operations.

Import… (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd>)
: Open the [import](../getting-started/import.md) window to add images to the library.

Export… (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd>)
: Open the [export](toolboxes/export.md) settings to render the selected images to files.

Recent collections
: A submenu listing the collections you have recently browsed; pick one to restore it.

Copy files on disk… / Move files on disk…
: Physically copy or move the selected image files to another folder, updating the library accordingly.

Create a blended HDR
: Merge the selected bracketed exposures into a single high-dynamic-range DNG.

Copy distant images locally / Resynchronize distant images
: Manage [local copies](lighttable/digital-asset-management/local-copies.md) of images stored on removable or network drives.

Remove from library (<kbd>Delete</kbd>)
: Remove the selected images from Ansel's library **without** deleting the files on disk. Available in the lighttable.

Delete from disk (<kbd>Shift</kbd>+<kbd>Delete</kbd>)
: Remove the selected images from the library **and** delete (or trash) the files on disk. Available in the lighttable.

Quit (<kbd>Ctrl</kbd>+<kbd>Q</kbd>)
: Close Ansel.

## Edit

Editing-history management, undo/redo and application settings. See [history copy/paste](lighttable/history-copy-paste.md) and [undo/redo](lighttable/undo-redo.md).

Undo (<kbd>Ctrl</kbd>+<kbd>Z</kbd>) / Redo (<kbd>Ctrl</kbd>+<kbd>Y</kbd>)
: Step backwards/forwards through your most recent actions in the current view.

Copy history (all) (<kbd>Ctrl</kbd>+<kbd>C</kbd>) / Copy history (parts)… (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>C</kbd>)
: Copy the whole editing history of the selected image, or pick which modules to copy.

Paste history (all) (<kbd>Ctrl</kbd>+<kbd>V</kbd>) / Paste history (parts)… (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>V</kbd>)
: Paste the copied history onto the selected images, either entirely or selectively.

History pasting mode
: How a pasted history combines with the target's existing history:
    - **Prepend** — the copied history is applied _before_ the current one; current edits win conflicts.
    - **Append** — the copied history is applied _after_ the current one; copied edits win conflicts.
    - **Replace** — the current history is discarded and replaced by the copied one.

Nodes pasting mode → Copy module order
: When enabled, pasting also transfers the module (pixelpipe node) order along with the settings.

Ask merge settings before paste
: When enabled, a dialog asks how to merge the histories each time you paste, instead of using the modes above silently.

Load history from XMP…
: Apply an editing history read from an external XMP sidecar file to the selected images.

Create new history (<kbd>Ctrl</kbd>+<kbd>N</kbd>)
: Create a fresh, empty duplicate of the selected image to start a new edit.

Duplicate existing history (<kbd>Ctrl</kbd>+<kbd>D</kbd>)
: Create a duplicate of the selected image that carries over its current edit.

Compress history
: Collapse the history stack to the minimum set of states producing the current result.

Delete history
: Discard the editing history of the selected images, resetting them.

Preferences…
: Open the [preferences & settings](../preferences-settings/_index.md) window.

Keyboard shortcuts…
: Open the [shortcuts](../getting-started/keyboard.md#finding-default-shortcuts-setting-new-ones) window, which both lists and lets you edit every shortcut.

## Selection

Batch selection helpers that work on the current collection. Selecting from the thumbnail grid is described in [shortcuts and keyboard interaction](../getting-started/keyboard.md#thumbtable).

Select all (<kbd>Ctrl</kbd>+<kbd>A</kbd>)
: Select every image in the current collection.

Clear selection (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>A</kbd>)
: Deselect everything.

Invert selection (<kbd>Ctrl</kbd>+<kbd>I</kbd>)
: Select the images that are not currently selected, and vice-versa.

Scroll back to selection
: Scroll the grid to bring the selected image(s) into view.

## Image

Per-image metadata and grouping actions, applied to the selected images.

Rotate
: **90° counter-clockwise**, **90° clockwise**, or **Reset rotation**. This sets the image orientation flag.

Color labels
: Toggle a color label: **Red** (<kbd>F1</kbd>), **Yellow** (<kbd>F2</kbd>), **Green** (<kbd>F3</kbd>), **Blue** (<kbd>F4</kbd>), **Purple** (<kbd>F5</kbd>), or **Clear labels** (<kbd>F6</kbd>).

Ratings
: Set a star rating: **Reject** (<kbd>R</kbd>), **★** (<kbd>1</kbd>) … **★★★★★** (<kbd>5</kbd>), or **Clear rating** (<kbd>0</kbd>).

Reload EXIF from file
: Re-read the EXIF metadata from the original files, discarding any cached values.

Group images (<kbd>Ctrl</kbd>+<kbd>G</kbd>) / Ungroup images (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd>)
: Combine the selected images into a [group](lighttable/digital-asset-management/grouping.md), or break the group apart.

## Styles

Apply and manage [styles](toolboxes/styles.md) — named, reusable sets of module settings.

(list of styles)
: Each available style is listed; click it to apply it to the selected images. Hovering a style name previews it on the current image.

History pasting mode / Nodes pasting mode / Ask merge settings before apply
: Control how a style merges with the target's existing history, exactly like the equivalent options in the _Edit_ menu.

Create new style…
: Create a style from the editing history of the selected image.

Manage styles…
: Open the style manager to rename, edit, import, export or delete styles.

## Run

Background and maintenance tasks.

Clear darkroom pipeline caches
: Free the cached intermediate pipeline results, forcing a full recompute on the next edit.

Preload selected thumbnails in cache
: Pre-render the thumbnails of the selected images into the on-disk cache, up to a chosen resolution (from _360×225 px_ up to _8K_, or _for current grid size_). Runs as a background task.

Purge selected thumbnails from cache
: Delete the cached thumbnails of the selected images from disk, so they are regenerated next time.

Defragment the library
: Optimize the library database file.

Backup the library
: Make a backup copy of the library database.

Resynchronize library and XMP
: Scan for XMP sidecars that changed outside Ansel and reconcile them with the library.

Save selected developments to XMP
: Write the editing history of the selected images to their XMP sidecar files.

Resynchronize database with distant XMP for local copies
: Reconcile the library with the XMP sidecars of images held as local copies.

{{< note >}}
Coming from Darktable: preloading and purging the thumbnail cache used to be possible only through hidden shell scripts. They are now first-class menu commands, and the choice between processing the RAW or using an embedded JPEG (in the _Display_ menu) can be changed at runtime instead of only in the preferences.
{{< /note >}}

## Display

View-level display settings. These are detailed in the [views overview](_index.md#display-menu).

Monitor color profile / Monitor color intent
: The display [color-management](../color-management/_index.md) profile and rendering intent (_Perceptual_, _Relative colorimetric_, _Absolute colorimetric_).

Panels
: Individually toggle the **Top** (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd>), **Left** (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>L</kbd>), **Right** (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>R</kbd>) and **Filmstrip** (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd>) panels.

Thumbnail overlays
: When the badges drawn over thumbnails are shown: **Always hide** (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd>), **Show on hover**, or **Always show** (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>O</kbd>).

Thumbnail source
: How thumbnails are generated: **Always process the RAW**, **Use embedded JPG if unedited**, or **Always use embedded JPG**.

Collapse grouped images / Show group borders (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>)
: Control the display of [grouped images](lighttable/digital-asset-management/grouping.md).

Full screen (<kbd>F11</kbd>)
: Toggle fullscreen mode. (To toggle all panels at once, use <kbd>Shift</kbd>+<kbd>F11</kbd>.)

## Ateliers

The view switcher. It lists every available view and switches to the one you pick (the current view is greyed out):

- **Lighttable** (<kbd>Escape</kbd>)
- **Darkroom** (<kbd>Enter</kbd> — opens the selected image)
- **Studio capture** — to survey and import new image automatically to see them directly into darktable
- **Map**, **Print**, **Slideshow** (when enabled in the [preferences](../preferences-settings/other-views.md))

## Help

Documentation and support links.

Online documentation
: Open this documentation website.

Ask a question
: Open the Ansel search engine.

Join the support chat / Join the support forum
: Open the community chat and forum.

Open contextual help
: Turn the cursor into a help probe; click a control to open the relevant documentation page.

Search actions… (<kbd>Ctrl</kbd>+<kbd>P</kbd>)
: Open the [global action search](../getting-started/keyboard.md#vimkey-like-global-action-search).

About
: Show the version and credits dialog.
---

# The mathematics of diffuse or sharpen : https://ansel.photos/en/resources/diffuse-or-sharpen-math/

This article documents the mathematics currently implemented in Ansel's `diffuse or sharpen` module, as found in `src/iop/diffuse.c`, `src/common/bspline.h`, `data/kernels/diffuse.cl`, and `data/kernels/bspline.cl`. It is not a usage guide. It is a reconstruction of the numerical model from the source code, with the scientific claims traced back to the references cited in the code comments.[^impl]

<!--more-->

## Abstract

The module operates on a full-resolution a-trous multiscale decomposition of the image built from cardinal B-spline blurs. At each scale, Ansel computes four anisotropic second-order operators on the low-frequency and high-frequency bands separately, regularizes their action by a scale-normalized local high-frequency band energy, optionally restricts the update to a binary inpainting mask, and reconstructs the image from coarse to fine.[^qin] The discrete spatial operators are 3x3 central-difference stencils sampled on the sparse a-trous sub-lattice of the current scale, so the larger physical support comes from the stride $2^s$ of the wavelet ladder rather than from a larger PDE kernel.[^witkin83][^impl]

## Motivation

The goal of the diffuse module was to simulate watercolour-like pigment escaping from an image into its borders like that :

{{% figure src="./alchemyrefiner_alchemymagic_0_07044b2e-293d-433e-974b-8c7399f28d47_0_01.jpg" %}}
Image generated by AI using [Stable Diffusion](https://en.wikipedia.org/wiki/Stable_Diffusion) model through [Leonardo AI](https://leonardo.ai/).
{{% /figure %}}

It turned out that merely changing the sign of the diffusion partial derivative equation update could actually sharpen the image. I had worked on blind deconvolution for years when I started working on a diffusion model, and never managed to make even the _state-of-the-art_ method work outside of the nice poster cases (low ISO, low saturation, uniform blur). But this of course needed a regularization parameter to avoid diverging. 

This is how the _diffuse or sharpen_ module was born, as a generic framework for all sorts of diffusive or counter-diffusive phenomenon simulation.

{{< note >}}
In the following, we will use the vocabulary of Fourier harmonic analysis (especially _frequency_) even though we are not strictly in a such framework, but the wavelets scheme is very close in principle from a signal decomposition into Fourier harmonic series.
{{< /note >}}

## Continuous model

At heart, the module implements a family of anisotropic diffusion equations of the form

$$
\partial_t u = \nabla \cdot (\mathbf{A} \nabla u),
$$

where $u$ is the image and $\mathbf{A}$ is a symmetric positive diffusion tensor that steers the smoothing either along the isophotes, along the gradient, or isotropically.[^qin][^witkin91]

The original inspiration in the code is the inpainting model of Qin et al., who couple image structure and texture restoration through anisotropic heat transfer.[^qin] Ansel keeps that spirit, but applies the PDE in a wavelet domain, separately on low-frequency and high-frequency bands, and exposes four user-controlled transport coefficients.

## Discrete gradients and anisotropy

For each pixel and each channel, the module extracts a 3x3 neighborhood and evaluates centered first derivatives

$$
\begin{align}
g_x &= \frac{u(i+1,j) - u(i-1,j)}{2} \\\
g_y &= \frac{u(i,j+1) - u(i,j-1)}{2}
\end{align}
$$

This is the standard central-difference gradient used in discrete scale-space and diffusion methods.[^witkin83]

The local gradient orientation is then :

$$
\begin{align}
\cos\theta =
\frac{g_x}{\sqrt{g_x^2 + g_y^2}} \\\
\sin\theta =
\frac{g_y}{\sqrt{g_x^2 + g_y^2}}
\end{align}
$$

with the usual fallback to $(1,0)$ when the gradient magnitude vanishes.[^impl] Note that we will avoid computing the expensive $\theta$ angle as the $\arctan2$ function of the $(x, y)$ components, since it will never be used directly in the following.

The anisotropy strength is converted from the user parameter $a$ into a positive coefficient

$$
\alpha = a^2,
$$

while the sign of $a$ selects the mode:

* $a = 0$: isotropic diffusion,
* $a > 0$: diffusion aligned with the isophote direction,
* $a < 0$: diffusion aligned with the gradient direction.[^impl]

The damping term is then

$$
c^2 = \exp(-\alpha \lVert \nabla u \rVert),
$$

which matches the anisotropic diffusion of Qin et al. for inpainting.[^qin]

For isophote-aligned diffusion, the tensor written in the local image basis is :

$$
\mathbf{A}_{\perp} =
\begin{bmatrix}
\cos^2\theta + c^2 \sin^2\theta & (c^2 - 1)\cos\theta\sin\theta \\\
(c^2 - 1)\cos\theta\sin\theta & c^2 \cos^2\theta + \sin^2\theta
\end{bmatrix}.
$$

For gradient-aligned diffusion, Ansel uses the inverted form :

$$
\mathbf{A}_{\parallel} =
\begin{bmatrix}
c^2 \cos^2\theta + \sin^2\theta & (1 - c^2)\cos\theta\sin\theta \\\
(1 - c^2)\cos\theta\sin\theta & \cos^2\theta + c^2 \sin^2\theta
\end{bmatrix}.
$$

## The 3x3 diffusion stencils

Depending on user parameters, we will evaluate for each pixel the $\mathbf{K(A)}$ stencil using one of the $\mathbf{A}$ tensors above.

Given the symmetric tensor $\mathbf{A}$ :

$$
\mathbf{A} =
\begin{bmatrix}
a_{11} & a_{12} \\\
a_{12} & a_{22}
\end{bmatrix},
$$

we build the discrete, rotated, anisotropic Laplacian kernel $\nabla \cdot (\mathbf{A}\nabla u)$ as the 3x3 stencil :

$$
\mathbf{K}(\mathbf{A}) =
\begin{bmatrix}
\frac{a_{12}}{2} & a_{22} & -\frac{a_{12}}{2} \\\
a_{11} & -2(a_{11}+a_{22}) & a_{11} \\\
-\frac{a_{12}}{2} & a_{22} & \frac{a_{12}}{2}
\end{bmatrix},
$$

up to the sign convention mentioned in the source code for the off-diagonal terms.[^qin][^witkin91][^impl]

In the isotropic case expands to a constant :

$$
\mathbf{K}_{\text{iso}} =
\begin{bmatrix}
\tfrac14 & \tfrac12 & \tfrac14 \\\
\tfrac12 & -3 & \tfrac12 \\\
\tfrac14 & \tfrac12 & \tfrac14
\end{bmatrix},
$$

which is the classical Oono-Puri style isotropic Laplacian. Its interest is rotational behavior: compared with the 5-point Laplacian, the angular error is reduced, which matters when diffusion must not privilege the image axes.[^oono][^patra] This angular error was evaluated against similar kernels in [_Rotation-invariant Laplacian for 2D grids_](https://eng.aurelienpierre.com/2021/03/rotation-invariant-laplacian-for-2d-grids/).

To understand the effect of direction in diffusion, let's start with a noisy disc and diffuse it with Ansel :

{{< figure src="./bruit.jpg" />}}

{{< gallery cols="3" >}}
{{< figure src="./bruit-isotrope.jpg" >}}
50 iterations of 12 px __isotrope__ diffusion : it's equivalent to a good old gaussian blur.
{{< /figure >}}
{{< figure src="./bruit-gradient.jpg" >}}
50 iterations of 12 px __gradient-parallel__ diffusion : note the feathers (or "sun rays") close to north/south/east/west positions.
{{< /figure >}}
{{< figure src="./bruit-isophote.jpg" >}}
50 iterations of 12 px __gradient-perpendicular (isophote)__ diffusion : it's an edge-avoiding surface blur, especially close to north/south/east/west positions.
{{< /figure >}}
{{< /gallery >}}


A couple of things are worth mentionning here :

- Only the isotrope blurring yields an achromatic disc, the other retain some large-scaled chroma noise, because the chroma noise creates local gradient variations.
- The anisotrope diffusion is perfect near the north/east/south/west positions of the circle, that is when gradient angle is perfectly aligned on the pixel grid (0° or 90°). In-between, we see discrepancies regarding how direction is dealt with, due to the numerical limitations of a rotated 3×3 square stencil.

## The à-trous B-spline pyramid

The multiscale analysis is built from the separable 5-tap cardinal B-spline filter

$$
h_0 = \frac{1}{16}[1,4,6,4,1].
$$

This filter is a compact Gaussian approximation, which is why it appears naturally in spline-based scale-space methods.[^unser] We re-use here the framework introduced by Johannes Hanika for the _contrast equalizer_ module.[^dreggn]

[^dreggn]: Holger Dammertz, Daniel Sewtz, Johannes Hanika, Hendrik P.A. Lensch, "Edge-Avoiding À-Trous Wavelet Transform for fast Global
Illumination Filtering", Ulm University, Germany, 2010. [URL](https://jo.dreggn.org/home/2010_atrous.pdf)

The current implementation uses the historical undecimated a-trous ladder:

1. The first full-resolution low-pass is

   $$
   G_0 = h_0 * u,
   $$

2. The finest detail band is

   $$
   H_0 = u - G_0.
   $$

3. Coarser levels keep the same image resolution and only enlarge the blur stride by $2^s$:

   $$
   G_s = h_{2^s} * G_{s-1}, \qquad s > 0.
   $$

   meaning at $s=1$, $h_{2}$ taps are evaluated every next pixel, as $s=2$, $h_4$ taps are evaluated every 4 pixels, and so on…

4. The band stored at scale $s$ is the difference between two successive low-passes:

   $$
   H_s = G_{s-1} - G_s, \qquad s > 0.
   $$

So every band lives on the original image grid (no decimation), and we solve the PDE fine-to-coarse in parallel, by accumulating the solution scale by scale into the output buffer until we add the final residual.[^impl] This scheme avoids stacking up the fine scales correction on top of the coarse scales, as is customary with Gaussian pyramids or multi grid solvers, and has shown empirically better stability in the inverse problem of sharpness reconstruction. This is equivalent to managing the energy of each band separately, in a way that resembles HiFi audio equalizers, but within a 2D spatial framework.

## Kernel scale, band scale, and the GUI scale envelope

To study the wavelet scheme properties, we will use the property of the cardinal B-spline of being a Gaussian filter approximation. The Gaussian parameter $\sigma$ controls how blur kernels compose:

$$
G(\sigma_1) * G(\sigma_2) = G\left({\sqrt{\sigma_1^2 + \sigma_2^2}}\right)
$$

so Gaussian *kernel* variances $\sigma^2$ add under convolution.[^witkin83][^unser] By contrast, the variance of a filtered signal $X$ :

$$
\operatorname{Var}[g_\sigma * X],
$$

depends on the spectrum of $X$ and is generally not equal to $\sigma^2$. To disambiguate both _variances_, in the rest of this article, $\sigma$ refers only to the parameter of the Gaussian kernel, never to the square root of signal variance.

To avoid ambiguity, it also helps to separate the low-pass levels from the levels bands:

* $G_0 = h_1 * u$ is the first full-resolution low-pass over the input image $u$,
* $G_s = h_{2^s} * G_{s-1}$ for $s \ge 1$,
* $H_0 = u - G_0$,
* $H_s = G_{s-1} - G_s$ for $s \ge 1$.

So:

* the low-pass index $s$ refers to the blur level $G_s$,
* the band index $s$ refers to the detail band $H_s$ sitting between $G_{s-1}$ and $G_s$,
* every level remains sampled on the original image grid.[^impl]

The B-spline filter approximates best the equivalent Gaussian kernel of parameter :

$$
\sigma_B \approx 1.055365.
$$

Because Gaussian-kernel variances add under convolution, the effective blur radius of $G_s$ follows directly from the a-trous analysis ladder. Therefore the cumulative low-pass sequence is

$$
G_0\; G_1\; G_2\; G_3\; \dots
\quad \Longleftrightarrow \quad
\sigma_{G_0}\; \sigma_{G_1}\; \sigma_{G_2}\; \sigma_{G_3}\; \dots
$$

with

$$
\sigma_{G,0} = \sigma_B,\qquad
\sigma_{G,1} = \sqrt{5}\sigma_B,\qquad
\sigma_{G,2} = \sqrt{21}\sigma_B,\dots
$$

and, in general,

$$
\sigma_{G,s}^2 =
\sum_{k=0}^{s} \sigma_B^2 4^k
= \sigma_B^2 \frac{4^{s+1} - 1}{3}.
$$

Equivalently,

$$
\sigma_{G,s} =
\sigma_B \sqrt{\frac{4^{s+1} - 1}{3}},
$$

The code uses $\sigma_{G_s}$ to decide how many scales are needed to match the user-requested `radius` parameter and to weight each band around the user-selected central radius:

$$
w_s = \exp\left( - \frac{(z\sigma_{G,s} - r_c)^2}{r_w^2} \right),
$$

where $z$ is the darkroom zoom level (compared to full-resolution raw), $r_c$ is `radius_center` in the GUI, and $r_w$ is `radius`.[^impl] By setting those, users define the decay of a sort of discretized "bandwidth" filter centered on an arbitrary frequency. In that sense, $r_w$ is the width of a Gaussian envelope in scale space. It is not the radius of the PDE stencil, nor the radius of a single blur. It is the spread of the gain profile applied to the discrete wavelet bands.

The weighting scheme is stable no matter the zoom level : radii are taken in raw-image (full-resolution) space. When previewing images downscaled in darkroom, the highest frequencies are truncated by the downscaling, so we only apply the wavelets decomposition starting from the highest frequency available and reweigh them according to their equivalent full-resolution radius. This allows a fairly accurate downscaled preview, even though it may hide noisy artifacts appearing at the highest levels.

Bands whose equivalent blur radius $\sigma_{G_s}$ lies close to $r_c / z$ receive the largest gain, while bands farther away are progressively attenuated. A small $r_w$ gives a narrow selection of scales; a large $r_w$ gives a broader and flatter response over neighboring bands.

So the pair (`radius_center`, `radius`) should be read as a center and width in scale space. The module does not directly define a Gaussian in Fourier frequency; instead, it defines a Gaussian envelope over the available multiscale bands.[^impl]

## Generic multiscale PDE update

Let:

* $H_s$ be the stored detail band at band index $s$,
* $G_s$ be the *current low-frequency reconstruction* used when solving band $H_s$ during synthesis.

At a given band $s$, the code builds four diffusion responses:

$$
\begin{align}
D_{1,s} &= p_1 \\, K_{1,s}(a_1, G_s) * G_s,\\\
D_{2,s} &= p_2 \\, K_{2,s}(a_2, H_s) * G_s, \\\
D_{3,s} &= p_3 \\, K_{3,s}(a_3, G_s) * H_s, \\\
D_{4,s} &= p_4 \\, K_{4,s}(a_4, H_s) * H_s,
\end{align}
$$

where:

* $K_{1,s}$ and $K_{2,s}$ are 3x3 anisotropic diffusion stencils applied to the current low-frequency reconstruction $G_s$,
* $K_{3,s}$ and $K_{4,s}$ are the analogous stencils applied to the detail band $H_s$,
* $a_1$ to $a_4$ are 4 user-defined anisotropy damping coefficients (see above),
* $p_1$ to $p_4$ are 4 user-defined PDE update (transport) coefficients (see below),
* the convolution is channel-wise on the sparse a-trous sub-lattice of stride $2^s$.[^impl]

All the Laplacian $K$ kernels are applied with the same stride distance as the B-spline blur having been used to produce the wavelet scale $s$ over which they are applied, that is $2^s$. Given that the blur kernel is 5×5 and the Laplacian kernels are 3×3, that means the Laplacian covers a quarter of the blur surface.

All those $K$ are an effort to make the _diffuse or sharpen_ a generic multiscale PDE framework, as they will cross information between $G_s$ and $H_s$ :

{{< table >}}
| Laplacian evaluated on \ Gradient evaluated on | $G_s$ | $H_s$ |
| ---------------------------------------------- | ----- | ----- |
| $G_s$                                          | $K_{1,s}$ | $K_{2,s}$ |
| $H_s$                                          | $K_{3,s}$ | $K_{4,s}$ |
{{< /table >}}

The gradients evaluated on $G_s$ are more likely to point toward legitimate image details, and can be used in a sharpening setting to ignore noise. The gradients evaluated on $H_s$ are more sensitive to noise and can be used in a diffusive setting to soften noise. We separate the layer on which we infer image structure (gradient direction) from the layer where we compute the PDE update (Laplacian) and thus the diffusion.

For lack of better term, those $K_i$ responses are linked to GUI parameters called "orders" from first to fourth :

- `speed`, which is the PDE update coefficient,
- `anisotropy`, which is the $a$ damping coefficient from the anisotropy tensor above.

We have therefore 4 `speed` parameters, one for each order :

$$
(p_1, p_2, p_3, p_4) = (\texttt{first}, \texttt{second}, \texttt{third}, \texttt{fourth}),
$$

And similarly, 4 `anisotropy` parameters :
$$
(a_1, a_2, a_3, a_4) = (\texttt{first}, \texttt{second}, \texttt{third}, \texttt{fourth})
$$

The meaning of all that can be translated in layman's term like so :

1. we diffuse structure in the direction of structure ($D_1$, _first order_),
2. we diffuse structure in the direction of texture ($D_2$, _second order_),
3. we diffuse texture in the direction of structure ($D_3$, _third order_),
4. we diffuse texture in the direction of texture ($D_4$, _fourth order_).

Any $p$ coefficient set to 0 cancels diffusion, any $a$ coefficient set to 0 cancels anisotropy.


The PDE update at scale $s$ is therefore :

$$
U_s = G_s + H_s + \frac{\kappa \\, w_s}{\nu_s} \sum_{i=1}^{4} D_{i,s},
$$

where :

- $\kappa$ is the discretization factor, that is $\frac14$ for finite centered differences,
- $\nu_s$ is the regularization parameter that we will see next section,
- $w_s$ is the scale weighting defined at the previous section.

And the final resynthesis is simply :

$$
u' = \sum_{s=0}^{n} U_s
$$

So, if we summarize the whole algorithm, given $u$ the initial image, $u'$ the final one,
$n$ the final number of scales :

$$
\begin{align*}
s &\in [0, n] \\\
G_{-1} &= u \\\
G_s &= h_{2^s} * G_{s-1},  \\\
H_s &= G_{s-1} - G_s, s\in[0,n] \\\
w_s &= \exp\left( - \frac{(z\sigma_{G,s} - r_c)^2}{r_w^2} \right) \\\
D_{1,s} &= p_1 \\, K_{1,s}(a_1, G_s) * G_s \\\
D_{2,s} &= p_2 \\, K_{2,s}(a_2, H_s) * G_s \\\
D_{3,s} &= p_3 \\, K_{3,s}(a_3, G_s) * H_s \\\
D_{4,s} &= p_4 \\, K_{4,s}(a_4, H_s) * H_s \\\
u' &= \sum_{s=0}^{n} \left[ G_s + H_s + \frac14 \frac{w_s}{\nu_s} \sum_{i=1}^{4} D_{i,s}\right]
\end{align*}
$$

Some notes:

- $n$ is not an user parameter but is determined with regard to the target final $\sigma$ requested by user radius parameters. This adjusts to zoom level as a byproduct.
- The Gaussian blur is itself the 2D isotrope solution to the heat equation : it is already diffusion.
- Increasing the blur radius is equivalent to letting the diffusion run for a longer time : the signal spreads farther.
- For $s > 0$, $H_s$ actually becomes a difference of Gaussians. At some scaling correction coefficient, the difference of Gaussians is an approximation of the Laplacian of a Gaussian, which itself is an estimation of the Laplacian at a scaling coefficient $\sigma$.
- Applying a (possibly anisotrope) Laplacian over $H_s$ again is equivalent to a 4-th order partial derivative (bi-Laplacian).

## Structure of the correction factor

The term that controls the PDE transport is

$$
\frac{\kappa \\, w_s}{\nu_s},
$$

In this section, we will define the $\nu_s$ factor.

### Defining well-behaved frequency filters for photographs

Photographs are digital reproductions of a latent image throuh an exposure apparatus (diaphragm aperture, sensor ISO sensitivity, shutterspeed) and a discretization (or spatial sampling) apparatus (color filter array, pixel grid). Those are artifacts of the technology used to capture the latent image and do not concern the actual image.

Unfortunately, the capture heuristics regarding exposure and sampling affect how we process the digital image. I have shown on discs at the _3×3 diffusion stencils_ section, how diagonal edges behave differently from grid-aligned edges (vertical/horizontal), even though I chose the most rotationally-invariant kernels : image content rotation (compared to pixel grid) will change how discrete gradients are evaluated. The concrete implication here is : rotating the image before or after _diffuse or sharpen_ will not yield the same outcome.

But it doesn't stop there : exposure changes the signal variance too. Given a white signal $X$, its local variance $V_1$ over a sampling window $\mathcal{N}$ is expressed :

$$
\begin{align}
V_{1, \mathcal{N}} &= \frac{1}{|\mathcal{N}|} \sum_{i\in\mathcal{N}} (\bar{X} - X_i)^2 \\\
& = \frac{1}{|\mathcal{N}|} \sum_{i\in\mathcal{N}} \left(\left(\sum_{i\in\mathcal{N}} X_i \right) - X_i\right)^2
\end{align}
$$

If, instead of capturing $X$, we over-exposed the same image by a factor $l$, then the variance of $lX$ would become $V_2$:

$$
\begin{align}
V_{2, \mathcal{N}} &= \frac{1}{|\mathcal{N}|} \sum_{i\in\mathcal{N}} (l\bar{X} - lX_i)^2 \\\
& = \frac{l^2}{|\mathcal{N}|} \sum_{i\in\mathcal{N}} (\bar{X} - X_i)^2 \\\
& = l^2 \\, V_{1, \mathcal{N}}
\end{align}
$$

So the variance of the signal increases with the square of the exposure factor. This matters to us here for two reasons, that can be summarized right now by this : all we do here is changing the scale-wise variance of the image.

First, our B-spline blurring step is a local weighted average, and $H_s$ evaluated at the pixel of coordinates $(x, y)$ can actually be written :

$$
\begin{align}
H_s(x, y) 
&= X(x, y) - G_s(x,y)\\\
&= X(x, y) -  h_s * X(x,y)\\\
&= -\left(\frac{1}{16^2} \sum_{i = -2}^{+2} \sum_{j = -2}^{+2} h_{i + 2} \\, h_{j + 2} \\, X(x + i, y + j) \right) + X(x, y)
\end{align}
$$

with $h_{i, j}$ the coefficients of the 5-taps, 2D, B-spline kernel, and $X$ the blurred signal of the previous scale (or the initial image for the first step). We can similarly show that $H_s$ is linearly dependent to the exposure factor. The above equation shows how $H_s$ can be seen as a modulation around a local average : the magnitude of that modulation is not independent from the magnitude of the signal. It means that any smoothing of $H_s$ (carried out as a diffusive process) will have a different weight and a different impact on details whether the image is over-exposed or under-exposed, even though the content is the same. 

Said otherwise, smoothing (or conversly, sharpening) then underexposing, or underexposing then smoothing will not have the same effect on details, even though the final overall magnitude (average) of the signal will be the same. This is not what we expect from a well-behaved image filter : the data-representation of the content should not affect how we process the content itself. In the diffusive setting, it is not that damaging, but in the sharpening setting, details in shadows get really oversharpened compared to details in highlights, without any kind of normalization.

Second, the B-spline blur (or its Gaussian best approximation) will change the variance of the signal too. If we express the discrete signal $X$ as a local modulation around the global average $\mu$, we get $X_n = \mu + \epsilon_n$. Then, the B-spline blur applied to $X$ becomes :

$$
\begin{align}
G_{0}[n] &= \sum_{k} h_{k} (\mu + \epsilon_{n-k}) \\\
&= \sum_{k} h_{k} \\, \mu + \sum_{k} h_{k} \\, \epsilon_{n-k} \\\
&= \mu \sum_{k} h_{k}  + \sum_{k} h_{k} \\, \epsilon_{n-k}
\end{align}
$$

Because the coefficients of the $h_{k}$ kernel are normalized and $\mu$ is, by definition, constant over the window of length $k$, we get $\mu \sum_{k} h_{k} = \mu$, meaning that the blur doesn't change the average value. The variance is then expressed :

$$
\begin{align}
\operatorname{Var}(G_0) &= \frac{1}{\mathcal{N}} \sum_{n \in \mathcal{N}} \left(\mu - \left(\mu + \sum_{k} h_{k} \\, \epsilon_{n-k}\right) \right)^2 \\\
&= \frac{1}{\mathcal{N}} \sum_{n \in \mathcal{N}} \left(\sum_{k} h_{k} \\, \epsilon_{n-k} \right)^2 \\\
&= \operatorname{Var}\left(\sum_{k} h_{k} \\, \epsilon_{n-k}\right)
\end{align}
$$

From there, we can show that, for white, uncorrelated signals, $\operatorname{Var}(G_0) = \operatorname{Var}(X) \sum_k h_k^2$, and more generally :

$$
\operatorname{Var}(G_s) = \operatorname{Var}(G_{s-1}) \sum_k h_k^2
$$

where $\sum_k h_k^2 = (35 / 128)^2$ for the 5-taps cardinal B-spline 2D filter.

And here again, we have a problem : the same object sampled at some resolution, or at 4 times that resolution, would appear at the same "frequency" one step of wavelets decomposition later, which would make its surface variance $(35 / 128)^2$ lower. But this is purely a sampling artifact. The variance of the object itself can be conceptualized, outside of the image, in a continuous setting, as the local color modulations around its average surface color. While this idealized variance cannot be recovered by any imaging apparatus, the way we handle the signal should at least be stable in variance so we can posit image variance represents the object idealized variance to some constant scaling factor.

This might seem like philosophical concerns until we hit a practical problem of any imaging software : what happens when we preview the effect zoomed in/out ? How do we scale the effect so the downscaled preview is still faithful to the full-resolution result ?

So, all these sampling discrepancies need to be normalized to achieve an image filter that tries to manipulate content regardless of its data representation.

### Defining a regularization metric

We have seen above how the signal variance is a relevant metric for what we are doing here : we can trace it through the blurring steps, link it to signal magnitude, and it represents the signal modulation around the average value.

Unfortunately, we don't have access to a metric of variance once we enter the wavelets decomposition scheme. However, we have seen above that $H_s$ was pretty close, conceptually, to the $(\bar{X} - X_i)$ term of the variance :

- instead of an arithmetic mean, we use a weighted average using B-spline coefficients,
- instead of a global average, we use a local one,
- the B-spline radial nature makes it more rotationally-invariant than any square patch-wise average.

So we will use the $H_s$ band energy, evaluated at the same pixel coordinates as the Laplacian stencil, defined as :

$$
Q_s = \sum_{q \in \mathcal{N}_{3\times 3}} H_s(q)^2.
$$

For a slowly-varying, white, uncorrelated signal, $\overline{Q_s} = Q_s / |\mathcal{N}_{3\times 3}|$ becomes close to the scale-wise and patch-wise variance.

The regularization is meant for the sharpening problem, which is ill-defined : in this setting, we increase the energy of each layer $H_s$ and we need a parameter to dock it at bay at some point. This is a common procedure in inverse problems such as denoising and deblurring, for which Total Variation has been used as a regularization scheme for quite some time.

The regularization model we will use is :

$$
\nu_s = \tau + \lambda \\, \dfrac{1}{9} \sum_{q \in \mathcal{N}_{3\times 3}} \left(\frac{H_s(q)}{L_s(q)} \right)^2.
$$

with the user parameters :

$$
\lambda = 10^{\texttt{regularization}} - 1,
\qquad
\tau = 10^{\texttt{variance\_threshold}}.
$$

We have shown above how the signal variance varies with the square of the exposure scaling, and how $H_s$ varies linearly with the exposure scaling. $G_s$ carries the same linear dependency through its property of being a local weighted average.

So, the ratio $H_s / G_s$ is exposure-invariant. By identification, $L_s(q) = G_s(q)$ in the regularization equation, so we use the exposure-invariant band energy :

$$
Q_s' = \sum_{q \in \mathcal{N}_{3\times 3}} \left(\frac{H_s(q)}{G_s(q)} \right)^2
$$

and its local average :

$$
\overline{Q_s'} = \frac{1}{9} \sum_{q \in \mathcal{N}_{3\times 3}} \left(\frac{H_s(q)}{G_s(q)} \right)^2
$$

### Normalizing scale and spatial coverage

The 3×3 Laplacian stencil expands by a $2^s$ stride for each scale $s$, just like the B-spline kernel does : this is the basis of the "à-trous" scheme. The physical space covered by that kernel increases with wavelets scales.

We can show that a 2D Gaussian filter of parameter $\sigma$ changes the original signal variance like :

$$
\operatorname{Var}(g_\sigma * X) = \dfrac{\operatorname{Var}(X)}{4 \pi \sigma^2}
$$

Given that $4 \pi \sigma^2$ is the effective disc area covered by the Gaussian filter, this matches a very simple intuition : the Gaussian filter spreads the original modulation (expressed as variance) over a larger surface. That's diffusion in a nutshell. We derive from that :

$$
\operatorname{Var}(X) \propto \sigma^2 \operatorname{Var}(g_\sigma * X)
$$

Between 2 Gaussian blur steps, the variance parameter _of the equivalent Gaussian filter_ varies by :

$$
\begin{align}
\Delta \sigma_s^2 &= \sigma_s^2 - \sigma_{s-1}^2 \\\
&= \sigma_B^2 \\, 4^s
\end{align}
$$

And from one scale to the other, this radius grows by $\Delta \sigma = \sigma_B 2^s \approx 2^s$, recalling that $\sigma_B \approx 1.05\dots$.

So the evaluation of the Laplacian spatially expands at the same rate (same stride at each scale) as the B-spline or Gaussian filters : the Laplacian implicitly follows the variance spreading across scales, and no additional scale normalization is needed here. 

The initial implementation of _diffuse or sharpen_ had a $\sigma^2_s$ boost applied to the $\lambda$ regularization parameter : in practice, that prevents coarse scales from having any visible impact, and limit them to being the support for high-frequency work. 

Another attempt was made to normalize $\lambda$ accounting for the fact that $Q_s$ has a decreasing band energy as $s$ increases, so a scale-invariant energy metric led to :

$$
E_s = \frac{4}{(\Delta\sigma_s^2)^2}Q_s
$$

Even combined with the $\sigma^2_s$ factor above, that yielded far too much weight on the coarse scales, making it difficult to sharpen at high frequencies while the low ones were already overshooting badly (giving a smudgy look). Both these attempts have been abandonned.

## Result

{{< compare after="./DSC_0176-dehaze.jpg" before="./DSC_0176-non-dehaze.jpg" >}}
© Nicolas Tissot (if I recall correctly…)
{{< /compare >}}

The picture above will look overcooked to most but it's beyond the point : I have had my share of broken sharpening operators that worked well enough as long as you didn't push the strength above 2%. Such image filters have zero interest. You learn about algorithms much more by looking at how they fail than at how they succeed in their sweet spot.

This image is the ultimate trap for every sharpening algorithms :

- the foreground is closer and less hazy than the background, so it's begging to be oversharpened,
- the close foreground is also much darker, so again, easy to push sharpness up the sky there,
- we have a very contrasted mountain ridge begging to produce halos around edges,
- the sun disc in a cloudy sky will be sharpened by most algorithms with dark edges,
- the amount of dehazing required would make the noise explode (this was taken in 2017 with a Nikon D90 at 200 ISO, we are far from current sensors).

So, here we perform a joint denoising & deblurring at large radii. The deblurring uses isotropic counter-diffusion (which best describes atmospheric hazing), on high-frequency and low frequency alike. The denoising uses isophote diffusion on high-frequency, following the gradient sampled in high-frequency. All that was done with no masking, in a single _diffuse or sharpen_ instance.

Overall, we see no edge overshooting and no halos. The dark foreground was mostly ignored, as it should, but we got the details down in the valley back. We got no color shift or chromatic aberrations. I won't claim this picture is artifact-free, because, when looking up close, we get color streaks and new details that might be actual reconstruction or plain hallucinations of the diffusive model. But the point remain that these artificats, if any, look organic enough to go unnoticed if we don't have the original image.

This validates the relevance of the multi-scale diffusive scheme, along with the regularization strategy.

{{< compare after="./2022-06-17__DSC00078-dehaze.jpg" before="./2022-06-17__DSC00078-no-dehaze.jpg" >}}
© Radu Dumitrescu [source](https://discuss.pixls.us/t/backlit-hardcore-flaring-vintage-lens-fun/31270)
{{< /compare >}}
{{< compare after="./2022-06-17__DSC00078-detail-dehaze.jpg" before="./2022-06-17__DSC00078-detail-no-dehaze.jpg" >}}
© Radu Dumitrescu [source](https://discuss.pixls.us/t/backlit-hardcore-flaring-vintage-lens-fun/31270)
{{< /compare >}}

This one was taken with a Sony Alpha 7R Mk 2 but a Konika Hexanon 40 mm f/1.8. This is a pancake lens from the 1970's, and so the picture is 45 Mpx of beautiful blur. Portraits are less forgiving than landscapes because skin needs to stay healthy when bringing back sharpness. Here we also have all the lensflare and the sun rays that can quickly degenerate.

The detail is especially impressive here because we get back the eyebrowes and eyelashes without oversharpening the backlit hair. Overall, there is no ringing, fringing or haloing. Unfortunately, the skin quality has degraded and it's as far as we can go without masking skin to exclude it. From there, it's only about bringing back global contrast selectively with a tone curve : 

{{< compare after="./2022-06-17__DSC00078-face-dehaze.jpg" before="./2022-06-17__DSC00078-face-no-dehaze.jpg" >}}
Slightly different edit with 2 instances of _diffuse or sharpen_ : one for global deblooming, one for pixel-level sharpness. Global deblooming was softened compared to versions above to be more flattering on the skin, and the black point was corrected for more contrast. __This is not an endorsement of exaggerated sharpening as a photographic style__ but only a stress-test of the algorithm.
{{< /compare >}}


## What is specific to Ansel

The scientific references explain the building blocks:

* anisotropic diffusion and heat-transfer inpainting,[^qin]
* scale-space and discrete diffusion,[^witkin83][^witkin91]
* isotropic 9-point Laplacians,[^oono][^patra]
* B-spline Gaussian approximation.[^unser]

What is specific to Ansel is the way they are assembled:

* a full-resolution a-trous B-spline pyramid,
* four independently parameterized diffusion operators, split between low and high frequencies,
* an HF band-energy regularizer additionally normalized by the local LF energy,
* zoom-aware scale selection in darkroom preview,
* identical CPU and OpenCL mathematics.[^impl]

So the module should be understood as an engineering synthesis of several numerical ideas, not as a literal implementation of a single paper.

## Perspectives

The multiscale wavelets scheme of anisotropic diffusion PDE with regularization produces usable photographic results, beyond the mere proof of concept. It allows to leverage joint sharpening and denoising, along with regular oriented diffusion. It also allows to rejuvenate, through software, old lenses that were deemed unfit for high-definition digital photography. The module itself provides a generic PDE playground that can be used in many different ways.

The problem though is that the nature of the settings is grounded into (at least) bachelor-level mathematics and is cryptic to most photographers. Explaining the effect of the parameters is difficult without diving into what they mean mathematically. Trying to rename them by their function instead of their nature is doomed to fail because their function depends on how they are combined together, and whether they are used in the positive or negative value range.

The diffusion setup is pretty straightforward and doesn't require regularization. It doesn't need the 4 orders altogether, but only the first and third order settings are enough. In the isotropic setting, it is fully equivalent to a Gaussian blur, which will be less expensive to compute (because non-iterative).

The sharpening setup, along with joint denoising, is more complicated. The only way to make it more user-friendly is to train it as a machine-learning algorithm :

- shoot pairs of clean and blurry/noisy/hazy pictures of the same scene (motion blur, lens defocus blur, soft lenses),
- perform a brute-force parameters sweep of _diffuse or sharpen_ module performing reconstruction of the dirty images, and record the $L_2$ norm of the error between the clean reference images and the attempted reconstructions,
- manually classify the images in binary categories (blurry, noisy, hazy) or by intensity (noise can be measured as PSNR, RMS, … ; radius will probably need to be there too),
- the machine-learning problem becomes : in the 14D space of _diffuse or sharpen_ input parameters, what are the 4 main directions (associated with denoising, deblurring, dehazing, radius) that minimize the error $E = ||\text{clean} - \text{reconstructed}||_2$ ? We are looking for the eigenvectors of those main directions.
- solve that by weighted PLS (partial least-squares) : $Y = X' B + c$ for $n$ pairs of clean/reconstructed images, where :
   - $Y$ is the $4 × n$ vector of categories for each sample,
   - $X'$ is the $14 × n$ standardized vector (standardized component-wise : $X_j' = \frac{X_r - \mu}{\sigma}$) of _diffuse or sharpen_ parameters,
   - $B$ is the $14 × 4$ matrix mapping our 14 cryptic _D or S_ parameters to 4 user-friendly parameters (it's the unknown here),
   - $c$ is the residual (scalar or vector depending what fits),
   - the $L_2$ norm of the error is used as the PLS weighting (probably injected into an exponential),
- once the matrix $B$ is known (mapping 14D -> 4D), invert it to get the 4D -> 14 D model,
- add an alternative GUI mode exposing the 4 user-friendly parameters, and a GUI <-> parameters layer converting those 4 to the 14 input arguments of _D or S_ (meaning : write the matrix product).

Any other attempt at "simplifying" _diffuse or sharpen_ will only be a silly relabelling job obfuscating the real meaning of the parameters and preventing anybody with the proper mathematical background from understanding it. It would be a shame to make sure that the only few people able to understand it would actually be deterred of even trying. Right now, _D or S_ is difficult to understand, but at least it can be explained. Relabelling controls will not make it easier to understand, but will only add an extra layer of semantic translation between math and GUI, most likely inaccurate and misleading anyway, which will only make it more cognitively demanding to explain and to grasp.

[^impl]: Current implementation in the Ansel source tree: `src/iop/diffuse.c`, `src/common/bspline.h`, `data/kernels/diffuse.cl`, `data/kernels/bspline.cl`.

[^qin]: Chuan Qin, Shuozhong Wang, and Xinpeng Zhang, “Simultaneous inpainting for image structure and texture using anisotropic heat transfer model,” *Multimedia Tools and Applications*, 56(3), 469-483, 2012. DOI: [10.1007/s11042-010-0601-4](https://doi.org/10.1007/s11042-010-0601-4). Metadata: [DBLP](https://dblp.org/rec/journals/mta/QinWZ12.html).

[^witkin83]: Andrew P. Witkin, “Scale-Space Filtering,” *Proceedings of the 8th International Joint Conference on Artificial Intelligence (IJCAI)*, 1983, pp. 1019-1022. Open PDF: [IJCAI proceedings](https://www.ijcai.org/Proceedings/83-2/Papers/091.pdf). Metadata: [DBLP](https://dblp.org/rec/conf/ijcai/Witkin83.html).

[^witkin91]: Andrew P. Witkin and Michael Kass, “Reaction-Diffusion Textures,” *Proceedings of SIGGRAPH 1991*, pp. 299-308. Canonical DOI: [10.1145/122718.122750](https://doi.org/10.1145/122718.122750). Open-access copy: [Carnegie Mellon Robotics Institute](https://www.ri.cmu.edu/publications/reaction-diffusion-textures/). The implementation comments currently point to a nearby DOI variant.

[^oono]: Y. Oono and S. Puri, “Computationally efficient modeling of ordering of quenched phases,” *Physical Review Letters*, 58(8), 836-839, 1987. DOI: [10.1103/PhysRevLett.58.836](https://doi.org/10.1103/PhysRevLett.58.836).

[^patra]: M. Patra and M. Karttunen, “Stencils with isotropic discretization error for differential operators,” *Numerical Methods for Partial Differential Equations*, 22(4), 936-953, 2006. DOI: [10.1002/num.20129](https://doi.org/10.1002/num.20129).

[^unser]: Michael Unser, “Splines: A Perfect Fit for Signal and Image Processing,” *IEEE Signal Processing Magazine*, 16(6), 22-38, 1999. DOI: [10.1109/79.799930](https://doi.org/10.1109/79.799930). Open metadata and reprint links: [EPFL](https://bigwww.epfl.ch/publications/unser9902.html).
---

# The mathematics of highlight reconstruction by guided laplacians : https://ansel.photos/en/resources/guided-laplacian-highlights/

This article presents the mathematics of Ansel's `guided laplacians` highlight reconstruction — the
method behind the `_highlight reconstruction` module (`src/iop/highlights.c`,
`data/kernels/basic.cl`, `src/common/bspline.h`). It starts from first principles, derives the
reconstruction and its optimization objective, and validates it numerically on synthetic images.
The scientific claims trace back to the references cited in the code and to the original design
report on the *pixls.us* forum.[^forum] It describes the method as it is *meant* to work; a
magnitude-recovery bug in the current implementation — found during the numerical validation — is
documented in the closing section.[^impl]

<!--more-->

## Abstract

When a camera sensor saturates, the three color channels do not clip at the same time, so a blown
highlight drifts in color — usually toward magenta. The `guided laplacians` method reconstructs the
missing data *before demosaicing*, treating the raw mosaic as a collection of gradients rather than
colorimetric data. Its principle is the cross-channel **colour-line**: inside a small neighborhood
the color channels are affinely related, so a channel that clipped can be rebuilt from the channels
that survived by fitting a local affine model — Kaiming He's **guided filter**. Applied across
scales *on the full signal*, this recovers both the texture **and the magnitude** of a clipped
channel. How far to trust that borrow is decided per pixel by the local **inter-channel
correlation** — a strong colour-line follows the guide, a weak one extends the channel's own
gradient instead. Where every channel clips and no guide survives, the fine detail is lost, but a
single shared **luminance dome** — biharmonic (gradient-continuing) inpainting of the summed
brightness — restores the magnitude while the **hue** is diffused inward from the rim, so a blown
neutral highlight is rebuilt as a neutral dome rather than an off-colour disk. A final
confidence-weighted biharmonic pass irons out the seams left where these operators meet — since, as we
show, no local statistic cleanly tells a colour-line from noise, it is cheaper to regularize the
output than to perfect the metric. We derive the model from first principles, state it as the
minimization of four coupled energies — cross-channel affine consistency, magnitude curvature, chroma
smoothness, and uncertainty-aware output smoothness — and validate it on synthetic clipped
gradients.[^he][^qin]

## The problem : why clipped highlights turn magenta

A digital sensor is an array of photosites, each covered by one colored filter of a **color filter
array** (CFA) — the Bayer pattern (2×2 of R, G, G, B) or Fuji's X-Trans (6×6). Each photosite is a
potential well that fills with photo-electrons during the exposure and saturates at a fixed
capacity. Because the well capacity is a property of the silicon, **all three colors saturate at
roughly the same raw code value**.

The trap is white balance. A neutral grey subject does not produce equal raw signals in the three
channels: the CFA transmissions, the sensor's spectral sensitivity, and the scene illuminant all
differ per channel. To render such a subject as neutral, the raw developer multiplies each channel
by a **white-balance coefficient** — typically the green channel is left near $1$ while red and
blue are multiplied up by $1.5$ to $2$.

Now follow a neutral highlight as it gets brighter. The channel with the largest raw response (here
green) reaches the well's ceiling first and stops climbing. The other two keep rising until they
saturate in turn. Past the first saturation the recorded ratios are no longer neutral: after white
balance, red and blue overshoot green, and the "white" highlight reads as **magenta**.

{{< plotly title="A neutral highlight clips channel by channel. Once green saturates, the white-balanced red and blue overshoot it, and the recorded color drifts to magenta — even though the subject is grey." src="magenta-clipping.json" caption="true" >}}

{{< note >}}
This is a purely mechanical, colorimetric artifact of the capture apparatus. There is no magenta in
the scene. Any reconstruction that works in a color space *after* demosaicing is already fighting a
hue error that a demosaicing algorithm will have smeared across neighboring pixels. Reconstructing
*before* demosaicing, while the data is still a clean per-channel mosaic, is the whole point of the
guided-laplacian method.
{{< /note >}}

The value at which a channel is declared clipped is not the numeric maximum but a per-channel
threshold derived from the raw white point:

```math
\text{clip}_c = 0.995 \times \texttt{clip} \times \text{white}_c,
```

where $\text{white}_c$ is the module's `processed_maximum` for channel $c$ (the per-channel raw
white level surviving the earlier pipeline stages) and `clip` is a user safety factor around $1$.
The $0.995$ margin keeps almost-saturated photosites — whose response has already gone nonlinear
near the top of the well — out of the "valid" set.[^impl]

## The landscape of simpler fixes

Ansel's module offers three cheaper reconstruction modes before the guided-laplacian one, and they
are worth stating because they frame what the expensive method buys:

* **Clip** simply crushes every channel to the common threshold $\min_c \text{clip}_c$. No magenta,
  but every clipped region becomes a flat, textureless white blob.
* **Reconstruct in LCh** converts each Bayer block to a luminance/chroma/hue triple, rescales the
  chroma of clipped blocks to match the unclipped luminance, and converts back. It removes the hue
  drift but cannot invent texture.[^impl]
* **Reconstruct color (inpaint)** propagates color *ratios* from neighboring unclipped pixels along
  rows and columns, using the exponential-decay ratio update of Magic Lantern's algorithm. It is
  fast and directional but one-dimensional and easily fooled by complex edges.[^impl]

The `guided laplacians` mode is the only one that restores both the **texture and the magnitude** of
a clipped region, by borrowing from the channels that did survive along the local colour-line.

## First principles : the building blocks

The method is an assembly of four ideas. Two of them — discrete Laplacians and the à-trous
B-spline pyramid — are shared verbatim with [*diffuse or sharpen*](/resources/diffuse-or-sharpen-math/) and
are only summarized here. The other two — the guided filter and chroma diffusion — carry the
reconstruction and are derived in full.

### Gradients and Laplacians

For a discrete image $u(i,j)$, the **gradient** measures the local slope,

```math
\nabla u = \left( \frac{u(i+1,j) - u(i-1,j)}{2}, \; \frac{u(i,j+1) - u(i,j-1)}{2} \right),
```

and the **Laplacian** measures the local curvature — how much a pixel departs from the average of
its neighbors,

```math
\Delta u = \frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2}.
```

The Laplacian is the workhorse here because it isolates *texture* as oscillation around a local
average — it is zero on flat regions and responds only to local contrast — and because it is
**linear**: over- or under-exposing the image simply rescales it (a property we lean on below).
Isolating texture this way is what lets us transplant it between channels without dragging along the
guide's absolute brightness; the difference in overall magnitude between a clipped channel and its
guide is absorbed by the guided filter's slope, not by the Laplacian itself. Ansel uses the
rotationally-symmetric 9-point stencil

```math
\mathbf{K}_{\text{iso}} =
\begin{bmatrix}
\tfrac14 & \tfrac12 & \tfrac14 \\
\tfrac12 & -3       & \tfrac12 \\
\tfrac14 & \tfrac12 & \tfrac14
\end{bmatrix},
```

whose angular error is much smaller than the naive 5-point cross, so diffusion does not privilege
the pixel-grid axes.[^oono][^patra][^ripl]

### The à-trous B-spline pyramid

To act on structures of many sizes, the image is split into frequency bands by repeatedly blurring
it with the separable cardinal B-spline kernel

```math
h_0 = \frac{1}{16}[1,4,6,4,1],
```

a compact approximation of a Gaussian of parameter $\sigma_B \approx 1.0554$.[^unser] At scale $s$
the taps are spread apart by a stride of $2^s$ pixels ("à-trous" = "with holes"), so the same tiny
kernel reaches ever farther without ever growing in cost. Writing $G_s$ for the successive
low-pass images and $H_s$ for the **detail bands**,

```math
G_{-1} = u, \qquad G_s = h_{2^s} * G_{s-1}, \qquad H_s = G_{s-1} - G_s,
```

the image is exactly the sum of its bands, $u = \sum_{s=0}^{n-1} H_s + G_{n-1}$. A detail band $H_s$ is a
difference of Gaussians, which is itself a scaled approximation of a Laplacian-of-Gaussian — so
"filtering the band $H_s$" and "applying a Laplacian at scale $s$" are two views of the same
operation. The full derivation, including how the equivalent Gaussian radius grows as

```math
\sigma_{G,s} = \sigma_B \sqrt{\frac{4^{s+1}-1}{3}},
```

is given in the [companion article on *diffuse or sharpen*](/resources/diffuse-or-sharpen-math/#the-à-trous-b-spline-pyramid).[^impl][^dreggn]

{{< note >}}
Two products of this pyramid are used below. The **low-pass** $G_s$ is a local weighted mean at
scale $s$ — exactly the local average the guided filter needs, at a whole ladder of window sizes.
The **detail band** $H_s$ is a scaled Laplacian, the operator the chroma-diffusion fallback
integrates. The reconstruction proper works on the $G_s$ (which carry the color's magnitude); the
fallback works on the $H_s$.
{{< /note >}}

### The guided filter, from first principles

The **guided filter** of He, Sun and Tang is the engine that borrows texture from a good channel
into a clipped one.[^he] Suppose we want to produce an output image $q$ that stays faithful to some
target $p$ but wears the *edges and texture* of a **guide** $I$. Assume that, inside any small
window $\omega_k$ around pixel $k$, the output is an **affine function of the guide**:

```math
q_i = a_k \, I_i + b_k, \qquad \forall i \in \omega_k.
```

This single assumption — a *local color line* — is the whole model. It says that within a small
patch the channel we are rebuilding is just a scaled, shifted copy of the guide. It is the same
prior that underlies cross-channel demosaicing, dehazing, image matting and colorization: natural
surfaces trace *color lines* — locally, their channels are affinely related — because most edges are
changes in *reflectance* that scale all channels together.[^colorline] Under an affine map,
$\nabla q = a_k \nabla I$, so $q$ inherits every edge of $I$, merely rescaled by $a_k$.

We fit $(a_k, b_k)$ by least squares, keeping $a_k$ small to avoid amplifying noise (a ridge term
$\varepsilon a_k^2$):

```math
E(a_k, b_k) = \sum_{i \in \omega_k} \Big[ (a_k I_i + b_k - p_i)^2 + \varepsilon \, a_k^2 \Big].
```

Setting the derivatives to zero gives the closed form that appears, almost verbatim, in the code:

```math
\begin{aligned}
a_k &= \frac{\operatorname{cov}_{\omega_k}(I, p)}{\operatorname{var}_{\omega_k}(I) + \varepsilon}, \\
b_k &= \bar{p}_{\omega_k} - a_k \, \bar{I}_{\omega_k}.
\end{aligned}
```

The covariance in the numerator is the key: where guide and target move together, $a_k \to 1$ and
the guide's texture is copied through; where the guide is flat ($\operatorname{var} \to 0$), $a_k
\to 0$ and the output falls back to the local mean $\bar p$. The ridge parameter $\varepsilon$ sets
the scale below which variations are treated as noise and smoothed rather than transferred.

{{< note >}}
He's guided filter has a second step — averaging the per-window coefficients $(a_k, b_k)$ over all
windows covering a pixel — that keeps the output from looking blocky. Ansel gets the same smoothing
for free by computing the window statistics with a **smooth** kernel (a Gaussian, or the à-trous
B-spline low-pass) rather than a hard box: overlapping smooth windows make $a$ and $b$ smoothly
varying fields, so $a\,I + b$ is already artifact-free without a separate averaging pass. This is not
a cosmetic detail — using a hard box window here leaves visible **blocky/diamond artifacts** in the
reconstructed highlights; the smooth window is what removes them.[^impl]
{{< /note >}}

### Diffusion as color inpainting

Filling the *color* of a hole is a different problem from filling its *texture*. A hole's color
should vary **smoothly** and match its rim; it should not carry high-frequency detail of its own.
The natural formalism is the **Dirichlet energy**

```math
E[u] = \int_\Omega \lVert \nabla u \rVert^2 \, \mathrm{d}x,
```

whose minimizer over the hole $\Omega$, with the surrounding pixels as boundary condition, is the
**harmonic** function satisfying $\Delta u = 0$. The gradient descent of this energy is precisely
the **heat equation**

```math
\partial_t u = \Delta u,
```

i.e. isotropic diffusion. Running it spreads the boundary color inward until the hole is filled by
a smooth, curvature-free surface. This is the same anisotropic-heat-transfer inpainting model of
Qin et al. that Ansel already uses for *diffuse or sharpen*, restricted here to its isotropic
case.[^qin] We will apply it not to the pixels but to the **color ratios**, so that only chroma is
smoothed while the reconstructed luminance is left alone.

### Biharmonic inpainting: continuing gradients, not flattening

Harmonic inpainting is the right tool for a signal that *should* go flat inside the hole — a smooth
chroma. It is the wrong tool for one that was still *rising* when the sensor clipped it: a blown
highlight's magnitude kept climbing, and filling it flat ($\Delta u = 0$) leaves a matte disk where a
bright dome belongs. To carry the surrounding **slope** inward instead of erasing it, penalize the
signal's *bending* rather than its gradient — minimize the thin-plate (biharmonic) energy

```math
E_{\text{bihar}}[u] = \int_\Omega \lVert \Delta u \rVert^2 \, \mathrm{d}x
\qquad \Longrightarrow \qquad
\Delta^2 u = 0 \ \text{ on } \Omega, \quad u\big|_{\partial\Omega} = u_{\text{valid}} ,
```

whose Euler–Lagrange equation is the **biharmonic** equation $\Delta^2 u = 0$. Where the harmonic
solution forces $\Delta u = 0$ (a flat minimal surface), the biharmonic solution makes $\Delta u$
*itself* harmonic — the curvature at the rim is carried into the interior, so the boundary's rising
gradient is extrapolated into a dome (a thin-plate spline). It is the higher-order,
gradient-*extending* counterpart of diffusion, in the same spirit that *diffuse or sharpen* flips the
sign of the Laplacian to sharpen rather than smooth. We solve it as a direct sparse linear system on
the hole, and use it below to rebuild a clipped channel's magnitude from its own valid rim wherever
no correlated channel survives to guide it.[^ripl]

## The algorithm, end to end

Everything happens on the raw mosaic, in linear scene-referred RGB, before demosaicing, and at full
resolution. The method rebuilds each clipped channel from the channels that *survived* — along the
local colour-line, trusting it in proportion to how well it actually holds — and where **no** channel
survived it rebuilds one joint luminance dome and carries the surrounding hue inward.

```mermaid
flowchart TD
  A["raw CFA mosaic"] --> B["per-channel clip thresholds<br/>clip_c = 0.995 · clip · white_c"]
  B --> C["bilinear demosaic → temporary RGB<br/>+ per-channel clip mask"]
  C --> D["local channel normalization"]
  D --> E["feather mask → opacity α<br/>(full resolution, no downsampling)"]
  E --> F["for each clipped channel, coarse → fine:<br/>guided-filter fit on the FULL signal (LF + HF)<br/>vs the best VALID guide → colour-line a·g + b,<br/>and its reliability R² = corr(channel, guide)²"]
  F --> G{"a valid guide<br/>at this pixel?"}
  G -->|"yes — 1–2 channels clipped"| H["blend by squared confidence Wₑ = (R²)²:<br/>Wₑ·(colour-line a·g + b)<br/>+ (1−Wₑ)·(own-rim biharmonic dome)"]
  G -->|"no — all channels clipped"| I["one joint biharmonic <b>luminance</b> dome<br/>from the true valid rim<br/>× chromaticity diffused inward"]
  H --> P["iron out seams:<br/>uncertainty-aware biharmonic solve<br/>(diag(R⁴) + λΔ²)u = diag(R⁴)·û<br/>flattens seam curvature where R² is low"]
  I --> P
  P --> J["remosaic + composite by α<br/>out = α·reconstructed + (1−α)·raw"]
  J --> K["reconstructed CFA mosaic"]
```

Three preparatory stages deserve a note.

**Bilinear demosaic.** The mosaic is bilinearly demosaiced to a temporary RGB image — a *throwaway*
demosaic whose only job is to give every channel a value everywhere for the per-channel guided fit;
the final output is remosaiced back to a single channel. For the all-clipped cores a scalar
**magnitude** $L$ (the summed luminance $R+G+B$) is split from the **chromaticity** $\text{RGB}/L$, so
the two can be reconstructed by different means (a dome for the magnitude, diffusion for the hue).
A per-channel **clip mask**, together with its logical OR (feathered into the compositing opacity
$\alpha$), records which channels saturated.[^impl]

**Local channel normalization.** Each color is divided by the *average value of that color in the
current tile*, a crude local white balance computed on the spot. This equalizes channel magnitudes
so the guide-selection variance comparison is not biased toward whichever channel carries the
largest raw numbers; it deliberately does not reuse the white balance declared upstream.[^impl]

**Feathering the mask.** The binary mask is smoothed by a small $5\times5$ box average into an
opacity $\alpha \in [0,1]$, used as the final compositing weight. Feathering softens the seam where
the reconstruction meets untouched pixels; in the [numerical tests](#numerical-validation) it
lowered boundary error rather than raising it.

## Reconstructing the clipped channels

The core step applies the guided filter derived above at a ladder of scales, coarse to fine. Two
choices make it recover *magnitude*, not merely texture.

### Choosing the surviving guide

At each pixel and scale, among the channels that are **not clipped there**, pick the one with the
largest local variance — the channel that still carries genuine structure:

```math
g(x) = \arg\max_{c \,\in\, \text{valid}(x)} \operatorname{var}_\omega(u_c).
```

A clipped channel is rebuilt **only where such a valid guide exists**. Where a pixel keeps at least
one surviving channel (one- or two-channel clipping — the common case), that survivor guides the
reconstruction. Where *all three* channels clip, there is no guide and the pixel is handed to the
joint core below.

Selecting a single guide (rather than blending both) keeps the correlated case maximally sharp: where
a colour-line actually holds, the two candidate guides *agree*, so which one wins does not matter and
the estimate is smooth regardless. Where no colour-line holds the hard choice would **jump** as the
winner flips — but there the cross-channel estimate is untrustworthy anyway, and the correlation
weighting of the [next section](#trusting-the-guide--correlation-weighting) has already routed those
pixels to the smooth per-channel fallback, so the jump never reaches the output.

### Fitting the colour-line on the full signal

For a clipped channel $c$, fit the affine colour-line against the guide over a window $\omega$ of
radius $r_s$, using **only the samples where both channels are valid**, and — the crucial point —
on the **full pixel values**, not the mean-subtracted detail:

```math
a_c = \frac{\operatorname{cov}_\omega(u_g, u_c)}{\operatorname{var}_\omega(u_g) + \varepsilon},
\qquad
b_c = \bar u_{c,\omega} - a_c\, \bar u_{g,\omega},
```

then reconstruct the clipped pixels by

```math
u_c \leftarrow a_c\, u_g + b_c .
```

Because the fit is on values, the intercept $b_c$ carries the **DC level** of the color: a channel
clipped at the white point is *extrapolated back above it* along the colour-line the surviving
channels define. This is precisely what lets the method recover a highlight's true magnitude, not
just re-texture it at the clip level. The slope is clamped non-negative (color-lines do not invert),
and $\varepsilon$ / a variance floor guard the division where the guide is flat.

{{< note >}}
Fitting from the **valid samples only** is essential. The clipped samples are pinned at the white
point; including them in the fit would flatten the observed slope, and the model would reproduce the
clipped value instead of extrapolating past it. This is the single subtlety that separates a working
reconstruction from one that merely re-paints the clip.
{{< /note >}}

### Trusting the guide : correlation weighting

The colour-line borrow is only as good as the assumption behind it — that the clipped channel and
its guide are, locally, affinely related. On a natural surface they are; on pathological content —
independent per-channel gradients, or a boundary between two differently-colored materials — they
are not, and forcing $u_c = a_c u_g + b_c$ would paint in structure that does not belong. The fit
itself reports how far to trust it: the coefficient of determination

```math
R^2 = \frac{\operatorname{cov}_\omega(u_g, u_c)^2}{\operatorname{var}_\omega(u_g)\,\operatorname{var}_\omega(u_c)} \in [0,1],
```

the squared correlation of guide and channel over the valid samples. $R^2 \to 1$ where the
colour-line explains the data (trust the cross-channel estimate); $R^2 \to 0$ where it does not
(the borrow is meaningless).

So the method does not *switch* between cross-channel and single-channel filling — it **blends them
by confidence**. Where the colour-line is weak, it falls back to extending the clipped channel's
*own* gradient inward: per-channel **biharmonic** inpainting ([above](#biharmonic-inpainting-continuing-gradients-not-flattening)),
anchored to that channel's *true* valid rim.
For a clipped channel that still has a surviving guide at the pixel,

```math
u_c \leftarrow W_e \underbrace{\,(a_c u_g + b_c)\,}_{\text{cross-channel guide}}
            + (1 - W_e)\, \underbrace{u_c^{\text{self}}}_{\text{own gradient}} ,
\qquad
W_e = (R^2)^2 .
```

The weight is the **squared** confidence, not $R^2$ itself. Measured over the clipped pixels, $R^2$
separates the two regimes but only with a soft margin: on natural-like content it averages
$\approx 0.9$, on decorrelated content $\approx 0.65$ — and the distributions overlap by $\sim\!15\%$.
(No cheaper statistic does better: cross-scale slope stability and inter-guide agreement both
separate *worse*, so $R^2$ is the honest ceiling.) Used linearly, the decorrelated case would still
take $\sim\!65\%$ of a cross-channel estimate that is both wrong and — because the hard guide
selection flips there — *discontinuous*. Squaring pulls the regimes apart: $0.9^2 \approx 0.81$ barely
moves the trustworthy case, while $0.65^2 \approx 0.42$ halves the untrustworthy one, so decorrelated
pixels lean on the **smooth** own-gradient fill. This improves the decorrelated reconstruction (it now
leans on the more accurate self-fill) and leaves the correlated case essentially untouched. Because
the margin is soft, though, a *faint* seam still survives on the most adversarial content — squaring
cannot fully hide a discontinuity in the quantity it weights. That residue is removed not by chasing a
sharper metric but by regularizing the finished output, [below](#ironing-out-the-seams--uncertainty-aware-regularization).

Where **no** channel survives — the all-clipped core of a blown highlight — there is no guide and no
$R^2$ to weigh, so the reconstruction hands off to the joint core of the [next
section](#filling-holes-with-no-survivor). The blend is thus gated by **guide-validity** and weighted
by squared correlation, not by a hard count of clipped channels: one mechanism spans the whole range
from a one-channel clip (mostly cross-channel) through a two-channel clip (the surviving channel
guides) to a fully-blown core (the joint dome), and in every case it is the reconstructed/true edges
that drive the central fill — which is what rebuilds a large all-clipped centre instead of leaving it
flat.

This is what makes the method degrade gracefully as inter-channel correlation falls. On the
natural-like test images $R^2$ averages about $0.9$ and the cross-channel term dominates; on the
adversarial random-gradient images it averages far lower and the self-extension takes over, so the
reconstruction never fabricates a colour-line that is not there. It is the *correlation*, not the
number of survivors, that decides whether the guide can be trusted — a two-survivor pixel on
decorrelated content is no better guided than a one-survivor pixel on a clean colour-line.

### Coarse to fine

One window size cannot serve both a one-pixel specular fringe and a hundred-pixel blown cloud. The
à-trous B-spline pyramid supplies the local means the guided filter needs at every scale — the
low-passes $G_s$, at window radii $r_s = 2^s$ — and we sweep them **coarse to fine**. Coarse windows
reach across a large hole to its valid rim and fill the bulk; successive finer windows refine the
detail near the edges, where valid samples are close. Each scale updates only the clipped pixels for
which its window can actually see a valid guide, so the estimate sharpens inward without ever
inventing a guide that is not there.

### Adding grain

As a final touch, and only where the mask is open, the reconstruction receives **Poissonian** noise
of amplitude proportional to the local value ($\sigma_c = \texttt{noise\_level}\cdot u_c$), folded to
be strictly brightening and composited by $\alpha$. Photon shot noise is signal-dependent, so a
perfectly smooth patch dropped into a high-ISO frame looks plastic; matching the grain lets the
repair disappear.[^impl]

## Filling holes with no survivor

Where *every* channel clips, no colour-line can be fit: the highlight's fine detail and its exact
peak are genuinely lost. But two things can still be salvaged from the surroundings — the hole's
low-frequency **magnitude** curvature and its **hue** — provided they are reconstructed *jointly*.

The naive move is to dome each channel independently: run the per-channel biharmonic self-inpaint of
the confidence fallback, now unweighted, on all three channels. It fails in an instructive way.
Because the channels clip at **different radii** (each has its own white level, and a near-neutral
highlight crosses them in turn), each channel is domed from a rim at a *different* distance from the
centre, and the three domes reach *different* heights. The last channel to clip saturates flat right
at its rim, so it carries almost no inward slope and its dome barely rises — while the first channel
to clip is domed across a wide radius and rises steeply. The core therefore drifts **off-hue**: a
neutral sun blown out over an orange sky reconstructs as a **yellow** disk, because blue (the last to
clip here) collapses while red over-recovers. Three correct-looking 1-D domes, one wrong colour.

The fix is to separate what is shared from what is not. **Magnitude** is common to all three channels
— they are all bright because the same light overflowed them — so it is reconstructed **once**, as a
single dome. **Hue** varies smoothly and is carried inward from the rim. Concretely, split the
summed luminance $L = R+G+B$ from the chromaticity $\text{RGB}/L$, reconstruct each by its proper
operator, and recombine $\text{core}_c = L_{\text{dome}}\cdot (\text{RGB}/L)_c$.

### One shared magnitude dome

The luminance $L$ is domed over the all-clipped core by the [biharmonic
inpainting](#biharmonic-inpainting-continuing-gradients-not-flattening) introduced in first
principles — the solve $\Delta^2 L = 0$, anchored to the **true valid rim** of the core (the
already-reconstructed partial-clip annulus that surrounds it). Filling the magnitude by plain
diffusion would flatten it into a matte disk; the biharmonic solve instead continues the rim's slope,
so a large all-clipped core **rises into a dome** rather than collapsing to a plateau. Because all
three channels ride on this one dome, none can collapse relative to the others — the failure above is
structurally impossible. It cannot reach the exact peak — the steepest part of the gradient was
clipped away, and the true summit is simply unobserved — but it recovers roughly $75$–$80\%$ of the
clipped rise, as a smooth dome of the correct width. Solving a *single* scalar field rather than three
also makes the biharmonic system a third the size.

### Hue, by diffusion

The chromaticity $r = \text{RGB}/L$ is diffused inward from the core rim — harmonic inpainting of the
hue, $\Delta r = 0$ with $r$ fixed on the rim. Crucially the rim here is the **reconstructed annulus**
immediately around the core, not the distant sky: as the surviving channels were guided back toward
neutral approaching the highlight, the annulus is already near-neutral, so the diffused core comes out
**near-neutral too** — the blown sun is repainted white, not the sky's orange. (This is the essential
difference from the shipped code, whose chroma diffusion pulled from too far out and yellowed the
core.) The recombined $\text{core}_c = L_{\text{dome}}\cdot r_c$ is then a correctly-coloured dome.

Ansel's shipped diffusion realises this $\Delta r = 0$ solve not as one linear system but as an
iterated à-trous relaxation: decomposing the ratios into wavelet bands, each detail band takes one
explicit Euler step of

```math
r \leftarrow r + \kappa \, \big( \Delta r - \lambda \, r \big),
\qquad
\kappa = \frac{1}{\texttt{B\_SPLINE\_TO\_LAPLACIAN}} = \frac{\sigma_B^2}{2\sqrt{\pi}} \approx 0.31 ,
```

with $\Delta$ the isotropic 9-point Laplacian $\mathbf{K}_{\text{iso}}$. The constant $\kappa$
rescales the à-trous detail band (a difference of B-splines) into a correctly-scaled continuous
Laplacian and sets the effective diffusion time step.[^ripl]

Two things are worth reading off this update:

* With $\lambda = 0$ it is the pure **heat equation** $\partial_t r = \kappa \Delta r$ on the color
  ratios — harmonic inpainting that fills the hole's hue smoothly from its rim, the Dirichlet
  minimizer derived above (equivalent to the direct $\Delta r = 0$ solve, iterated).
* The term $-\lambda\, r$, controlled by the `solid_color` parameter, is a **first-order reaction**
  damping the ratio detail toward zero. Its Euler–Lagrange equation is the screened-Poisson /
  modified-Helmholtz equation $\Delta r - \lambda\, r = 0$; larger $\lambda$ pulls the interior hue
  toward a flatter, more uniform "solid color" fill.

The diffusion is purely **isotropic**: texture should follow structure, but hue should fill without
regard to direction, so a rotationally-symmetric smoother is what is wanted. The whole joint fill is
**gated to the all-clipped pixels**, so it never disturbs a channel the guided step already recovered.

{{< figure src="./magenta-sun.png" >}}
The desperate all-clipped case: a bright sun blown past every channel's (per-channel) white point,
over an orange sky. The sensor records a flat **magenta** disk; the current implementation returns a
dim, over-sized **yellow** disk with a hard edge (its per-channel fills drift off-hue and never dome);
the corrected method rebuilds a **neutral** dome of the right size from one shared luminance dome plus
rim-diffused hue. Over the clipped core, RMSE $0.87 \to 0.27$ and SSIM $0.61 \to 0.95$; the fill's
chroma moves from magenta $(0.37, 0.27, 0.36)$ to a near-neutral $(0.39, 0.33, 0.28)$, essentially the
ground truth's $(0.36, 0.33, 0.31)$ (the current implementation lands at a yellow $(0.51, 0.34, 0.15)$).
{{< /figure >}}

## Ironing out the seams : uncertainty-aware regularization

Everything so far recovers *magnitude* well, but it stitches together operators of different
character — the cross-channel colour-line, the own-gradient self-fill, the joint core — and the
reconstruction leaves faint **seams** where it crosses from one to another: the hard guide selection
is discontinuous where it flips (decorrelated content), and the core/annulus handoff meets with a
slope break. The natural instinct is to find a better *confidence metric* that switches between
operators without a jump. That instinct fails: as [measured above](#trusting-the-guide--correlation-weighting),
no cheap local statistic ($R^2$, cross-scale slope stability, inter-guide agreement) separates the
correlated and decorrelated regimes cleanly — they overlap — so any metric-driven switch will
mislabel some pixels and seam there. A discontinuity cannot be hidden by *weighting* it.

So do not hide it — **remove it**, after the fact, with a confidence-weighted smoothness solve on the
finished reconstruction $\hat u$:

```math
u_c = \arg\min_{u}\; \sum_{\Omega} W_d\,(u - \hat u_c)^2 \;+\; \lambda \int_\Omega \lVert \Delta u \rVert^2 ,
\qquad
(\operatorname{diag}(W_d) + \lambda\,\Delta^2)\,u_c = \operatorname{diag}(W_d)\,\hat u_c .
```

Two choices make this behave. First, the data term pulls toward the **reconstruction itself**, not the
(collapsed) guided target — so magnitude is preserved: the already-smooth all-clipped dome is left
essentially unchanged, and a global re-solve that would flatten it (which we tried, and which drove
the sun's centre back to the clip value) is avoided. Second, the regularizer is **biharmonic**
($\Delta^2$, penalizing curvature) rather than harmonic ($\Delta$, penalizing gradient): a seam is a
*curvature* spike, so the biharmonic term flattens it while leaving smooth domes and gradients — which
have low curvature — intact; a harmonic term would sand down the highlights themselves. The fidelity
weight $W_d = (R^2)^2$ preserves the reconstruction where it is trustworthy and lets the smoothness
prior act where it is not (decorrelated annulus, seams, and the all-clipped core, where $W_d \to 0$).

Crucially this needs **no** clean segregation of the two regimes: it is a soft prior that flattens
curvature in proportion to uncertainty, wherever a seam happens to be. On the decorrelated test scene
it roughly **halves** the reconstruction's residual curvature (the measurable signature of the seam)
and lifts SSIM, while the correlated scene — which has no seam — is barely touched. It is the one
place the method regularizes the *output* rather than the model, precisely because the model cannot
know, from local evidence alone, whether a given clipped pixel lives on a colour-line or not.

## The optimization objective

Neither step assembles a global energy and calls a named solver, but each *is* the local update rule
of a variational problem, and together they make the objective explicit. On the clipped region
$\Omega$ the reconstruction minimizes four coupled energies (three on the model, one on the output).

**1. Cross-channel affine consistency.** Where a valid guide $g$ exists, each clipped channel should
be an affine function of the guide — the guided filter is the exact minimizer of

```math
E_{\text{affine}} = \sum_{\omega}\sum_{c}\sum_{i \in \omega}
w_i \Big[ \big( a_{c} \, u_g(i) + b_{c} - u_c(i) \big)^2 + \varepsilon\, a_{c}^2 \Big],
\qquad w_i = [\,i \text{ valid}\,].
```

Fitting on the full values $u$ (not the detail) is what makes the minimizer carry the DC, so
$E_{\text{affine}}$ recovers **magnitude and texture together**.[^he][^colorline] This term is
trusted in proportion to the fit's **squared** correlation $(R^2)^2$; where $R^2 \to 0$ the channel is
instead held to its own second-order smoothness — a per-channel biharmonic term $\int_\Omega \lVert
\Delta u_c \rVert^2$ — so the objective never rewards a colour-line the data do not support. The
squaring sharpens that trade so a middling correlation leans on the smooth fallback rather than a
discontinuous cross-channel guess.

**2. Magnitude curvature (fallback).** Where no guide survives, fine detail is lost but the
low-frequency shape is not: it should *continue* the surrounding curvature rather than flatten. For a
partially-clipped channel this is the per-channel biharmonic term $\int_\Omega \lVert \Delta u_c
\rVert^2$ already invoked above (the $(1-R^2)$ leg of term 1). For the all-clipped core it is applied
**once** to the summed luminance $L = R+G+B$ — one shared dome instead of three divergent ones:

```math
E_{\text{bihar}} = \int_\Omega \lVert \Delta L \rVert^2\,\mathrm{d}x,
\qquad \Delta^2 L = 0,
\qquad L\big|_{\partial \Omega} = L_{\text{valid}} ,
```

anchored to the core's true valid rim; its Euler–Lagrange equation $\Delta^2 L = 0$ is the domed fill
above.

**3. Chroma smoothness (fallback).** Where no guide survives, the reconstructed ratios
$r = \text{RGB}/L$ should be smooth and match the rim, optionally biased toward flatness:

```math
E_{\text{chroma}} = \int_\Omega \Big( \lVert \nabla r \rVert^2 + \lambda \, \lVert r \rVert^2 \Big)\, \mathrm{d}x,
\qquad
r\big|_{\partial \Omega} = r_{\text{valid}} .
```

Its Euler–Lagrange equation is the screened-Poisson equation the diffusion integrates,[^qin] and the
recombination $\text{RGB} = L\cdot r$ closes the joint fill.

**4. Uncertainty-aware smoothness (post-fit).** Finally, one energy acts not on the model but on the
*output* $\hat u$, to remove the seams the operator switches leave behind:

```math
E_{\text{reg}} = \sum_\Omega (R^2)^2 (u_c - \hat u_c)^2 + \lambda \int_\Omega \lVert \Delta u_c \rVert^2 ,
```

a confidence-weighted fidelity to the reconstruction plus a biharmonic smoothness prior. It preserves
$\hat u$ where the fit is trustworthy and flattens the residual seam curvature where it is not —
[above](#ironing-out-the-seams--uncertainty-aware-regularization).

The energies act on **complementary pixels** — the affine term where a channel survives (weighted by
its correlation $R^2$), the per-channel biharmonic term where it does not, the shared luminance dome
plus chroma diffusion only in the all-clipped cores, and the uncertainty-aware smoothness everywhere
as a light finishing prior — and the à-trous ladder solves each coarse to fine.
This is the same
engineering philosophy as *diffuse or sharpen*: a stack of local, physically-motivated update rules
whose combined fixed point is the reconstruction, rather than one monolithic inverse problem.

## Numerical validation

To check the model — and to measure it — the reconstruction was re-implemented in pure NumPy (a
faithful port of the à-trous pyramid, the guided filter, and the diffusion) and exercised on
synthetic images with known ground truth. The scripts are in this page's bundle:
`reconstruct_highlights.py` (the faithful port of the shipped method), `fix_prototype.py` (the
corrected reconstruction, the scene generators, and the metrics), and `make_figures.py` (every figure
here). All numbers reproduce with `python3.12 fix_prototype.py`, all figures with
`python3.12 make_figures.py`.[^impl]

Two families of test images are used, spanning the range from adversarial to natural:

* **Random RGB gradients** — each channel an independent multiscale gradient. There is *no*
  colour-line, so this is the hardest case for a cross-channel method. This case reproduces the 
  situations met in events & concert photography, with narrow-band colored stage lights (LED blue).
* **Correlated gradients** — a multiscale **luminance** field times a smooth random **chromaticity**,
  i.e. $\text{RGB} = L(x)\cdot \mathbf{c}(x)$. This is how natural images actually behave (luminance
  carries the structure, hue varies slowly) and the colour-line holds locally by construction.

Onto each, bright multiplicative highlights of varying size are added and the image is clipped at the
white point, producing regions where one, two, or all three channels saturate. Reconstructing and
comparing to ground truth (RMSE over the clipped cores, mean of several seeds):

{{< table >}}
| scene | clipped channels | RMSE  (clip · current · **fixed**) | SSIM  (clip · current · **fixed**) |
|-------|:----------------:|:----------------------------------:|:----------------------------------:|
| correlated (natural-like) | 1 | 0.049 · 0.073 · **0.015** | 0.945 · 0.906 · **0.992** |
| correlated | 2 | 0.123 · 0.117 · **0.036** | 0.897 · 0.909 · **0.981** |
| correlated | 3 (all) | 0.221 · 0.215 · **0.065** | 0.744 · 0.775 · **0.959** |
| random | 1 | 0.080 · 0.124 · **0.043** | 0.915 · 0.855 · **0.964** |
| random | 2 | 0.132 · 0.167 · **0.078** | 0.874 · 0.819 · **0.940** |
| random | 3 (all) | 0.184 · 0.193 · **0.076** | 0.741 · 0.755 · **0.918** |
{{< /table >}}

The pattern confirms the theory. When **one or two** channels clip, a surviving guide lets the
colour-line recover the missing channel accurately — on natural-like images RMSE falls by 2–3× and
SSIM approaches $1$. When **all three** clip there is no guide, yet the joint core still helps markedly:
one shared biharmonic luminance dome restores the magnitude and rim diffusion its hue, so RMSE falls
by more than $3\times$ and SSIM jumps ($0.74 \to 0.96$ on natural-like scenes) — only the exact peak
and the finest detail are beyond reach. Throughout, recovery is stronger on correlated (natural-like) scenes than on
random ones, exactly because the colour-line is the operative assumption. On the random scenes the
[uncertainty-aware regularizer](#ironing-out-the-seams--uncertainty-aware-regularization) is what
lifts SSIM (e.g. $0.911 \to 0.918$ at three-channel clip) — it flattens the residual seam curvature
that RMSE, a pointwise average, barely registers but the eye and SSIM do. The current shipping code,
by contrast, is often *worse than doing nothing* on one-channel clips (RMSE $0.049 \to 0.073$) — the
[bug](#the-current-implementation-a-magnitude-recovery-bug) documented below.

{{% figure src="./synthetic-bug-fix.png" %}}
A synthetic scene: three saturated single-channel highlights (red, green, blue disks) and a bright
near-neutral one. **Ground truth** is the unclipped scene, **clipped** what the sensor records,
**current implementation** the shipping module, **corrected method** the reconstruction described
above. In the single-channel disks the corrected result climbs back toward the true color (e.g. the
red disk recovers $\approx 1.7$ vs. true $\approx 1.9$, with green and blue matched exactly) where the
current implementation stays *below* the clip ($\approx 0.9$). The all-clipped neutral disk — flat at
the clip in both the sensor and the current implementation — is re-domed by the shared luminance fill
(center $\approx 1.5$ vs. true $\approx 1.6$) and stays near-neutral, its hue carried by the
rim diffusion.
{{% /figure %}}

The two scene families behind the table, each as ground truth → sensor clip → current
implementation → corrected method:

{{% figure src="./correlated-validation.png" %}}
**Correlated (natural-like)** — a luminance field times a smooth chromaticity, with highlights
clipping one, two, or three channels. Over the clipped cores of this scene: RMSE $0.127 \to 0.053$,
SSIM $0.835 \to 0.954$.
{{% /figure %}}

{{% figure src="./random-validation.png" %}}
**Random** — three *independent* per-channel gradients: the adversarial case, with no colour-line to
exploit. RMSE $0.124 \to 0.054$, SSIM $0.875 \to 0.953$ — the reconstruction still helps even here.
Because the channels are uncorrelated there is little to borrow across them; the correlation
weighting detects the low $R^2$ and falls back to extending each channel's own gradient, which is the
only sound thing to do. In the largest all-clipped cores the joint luminance dome — anchored to the
*true* valid rim — fills the centre with a proper bulge rather than the flat plateau a naïve fill
leaves: peak-height recovery roughly doubles (from $\sim35\%$ to $\sim60\%$ of the clipped rise) so the
flat core is domed, not merely shrunk. It still undershoots the exact peak — the steepest part was
clipped away and, without a correlated channel, is genuinely unrecoverable — but the centre reads as a
dome, not a disk. A real photograph is never this
decorrelated; it lives nearer the correlated case above, where the colour-line extrapolates the peak
almost exactly.
{{% /figure %}}

One further result from the same harness: the reconstruction is markedly better run at **full
resolution**. The shipped module solves on a quarter-resolution copy for speed, but that
downsampling blurs the reconstruction and, blended back against the sharp original, is the dominant
source of edge error — running full-res lowered boundary RMSE by about 30%. Downsampling is a
speed/quality trade, not a free lunch.

## Parameters, mapped to the math

{{< table >}}
| GUI control | symbol | role in the model |
|-------------|--------|-------------------|
| `clipping threshold` | `clip` | scales the per-channel saturation thresholds $\text{clip}_c$ that define the hole $\Omega$ |
| `iterations` | $N$ | number of coarse-to-fine sweeps; raise to fill larger holes |
| `diameter of reconstruction` | — | coarsest window radius → number of à-trous scales (2 px … 4096 px) |
| `noise level` | `noise_level` | amplitude of the signal-dependent Poisson grain added to the fill |
| `inpaint a flat color` | $\lambda$ | strength of the first-order reaction term in the chroma fallback; pulls the fill toward a uniform solid color |
{{< /table >}}

Note that the number of scales adapts to the darkroom **zoom level**: radii are defined in
full-resolution raw space, and a downscaled preview simply starts the wavelet ladder from the
finest scale still available, so the preview stays faithful to the full-resolution result. This is
the same zoom-aware scale selection described for *diffuse or sharpen*.[^impl]

{{< advice >}}
The clipping threshold has a mask-visualization toggle (the icon next to the slider). When active,
the module renders a **false-color** overlay — valid pixels dimmed, clipped pixels flagged — so you
can see exactly which region $\Omega$ the reconstruction will act on before committing to it. You
should almost never need to move the threshold itself.
{{< /advice >}}

## What is specific to Ansel

The scientific references supply the pieces:

* the guided filter and its local affine model,[^he]
* the cross-channel "color-line" affine prior behind it,[^colorline]
* anisotropic heat-transfer inpainting,[^qin]
* the à-trous B-spline wavelet transform,[^dreggn][^unser]
* rotationally-invariant discrete Laplacians.[^oono][^patra][^ripl]

What is specific to Ansel is the assembly:

* reconstruction is done **before demosaicing**, on the raw mosaic, where the per-channel clipping
  is still clean;
* the guide is selected **per pixel and per scale** as the highest-variance *surviving* channel, so
  a clipped channel is always rebuilt from a channel that is valid there, with no fixed assumption
  about which channel that is;
* the colour-line is fit and applied **across the à-trous scale ladder, coarse to fine**, so holes
  of every size are filled by the smallest window that reaches valid data;
* the cross-channel borrow is **weighted by its own reliability** — the fit's correlation $R^2$ — so
  decorrelated content falls back to per-channel gradient extension instead of inventing a
  colour-line;
* the energies act on **complementary pixels** — guided recovery wherever a channel survives, and a
  joint luminance dome plus chroma diffusion only where none does — so they never fight;
* the whole thing is **zoom-aware** and shares its CPU and OpenCL mathematics.[^impl]

## Perspectives and limitations

The method recovers a clipped channel's magnitude *and* texture wherever a channel survives to guide
it, and it fails gracefully — to a smooth hue fill — only where none does. Its limits follow
directly from that structure:

* **All-channel clipping thins the method out, but is not a total wall.** With no surviving channel
  there is no cross-channel guide, so the fine detail and the exact peak are gone. What survives is
  the *low-frequency* structure: one shared biharmonic luminance dome extends the surrounding
  curvature into a plausible bulge and the diffusion supplies a plausible hue — the
  [validation](#numerical-validation) shows this recovers most of the error even there (SSIM
  $0.74 \to 0.96$ on natural-like scenes). Only the highest frequencies are truly unrecoverable.
* **Downsampling is a speed/quality trade.** Solving on a reduced-resolution copy is much cheaper,
  but it blurs the reconstruction and is the dominant source of edge error; full-resolution
  reconstruction is measurably sharper and more accurate.
* **It does not repair downstream demosaic artifacts.** Residual fringing or chromatic aberration
  around a clipped edge still wants a dedicated correction later in the pipeline.
* **It is expensive** — a multiscale guided fit swept coarse-to-fine and iterated — which is why the
  reconstruction diameter and iteration count carry "slow" warnings.

The broader lesson mirrors *diffuse or sharpen*: a reconstruction built as a stack of transparent,
physically-grounded local rules — a guided colour-line fit plus a screened-Poisson fallback in a
multiscale frame — can outperform monolithic, heuristic highlight recovery, and it fails gracefully
when it fails. The price is a set of controls whose meaning is mathematical rather than photographic;
this article is an attempt to make that meaning legible.

## The current implementation: a magnitude-recovery bug

The numerical validation above surfaced a discrepancy between the method as designed and the code as
written. It is documented here because the fix is not yet in the shipping module.

The colour-line recovery hinges on fitting and applying the guided filter to the **full signal**, so
the intercept $b_c$ carries the color's DC and a clipped channel is extrapolated *back above* the
white point. The current implementation instead runs the guided filter on the à-trous **detail
bands** $H_s$ (the mean-subtracted high frequencies). A detail band has zero local mean, so the
fitted intercept is $\approx 0$: only the *texture* (the slope term $a_c H_g$) transfers, never the
DC. The clipped channel's low-pass residual — pinned near the white point — is then added back
unchanged. Both the CPU (`guide_laplacians`) and OpenCL (`basic.cl`) paths do this identically, so
it is a design bug, not a porting slip.[^impl]

The consequence is visible in the figure's **stock** panel: a single-channel clip is not lifted
toward its true value; it is left at — or, after the chroma pass pulls it toward the neutral
surround, slightly *below* — the clip. In the validation harness the stock reconstruction is often
*worse than doing nothing* on one-channel clips (RMSE 0.073 vs. 0.049 for a natural-like scene),
whereas the corrected method reaches 0.015.

The correction changes the *signal*, not the mathematics. Run the guided filter on the **whole
wavelet scale** — the low-pass and its detail together, i.e. the pixel values themselves — instead of
on the mean-subtracted detail band alone. It is the *same* guided-filter fit derived
[above](#the-guided-filter-from-first-principles), $u_c \leftarrow a_c\, u_g + b_c$, only now the
intercept $b_c$ carries the DC and the clipped channel is lifted back above the white point.
Everything else in this article follows unchanged — the per-pixel guide selection, the correlation
weighting, and the coarse-to-fine sweep.
Two practical riders: the fit must **exclude clipped samples** (or their pinned values flatten the
slope), and the reconstruction is better run at **full resolution** (the stock 4× downsampling is the
dominant edge-error source).

The all-clipped cores need two further changes beyond running on the full signal. First, the shipped
code's chroma diffusion pulls hue from too far out and yellows a blown neutral core; replacing it with
a **rim-anchored** harmonic diffusion (hue carried from the reconstructed annulus that immediately
surrounds the core) keeps it neutral. Second, and the one genuinely different operator, is a
**biharmonic** magnitude dome — and even that is a revival, not an invention. An early design already
tried gradient-extension as a reverse-sign Laplacian, exactly the trick *diffuse or sharpen* uses to
sharpen instead of smooth; but iterating a reverse diffusion is unstable and it was dropped as
artifact-prone. The fix brings the idea back in its stable, direct form — but applied **once to the
summed luminance**, $\Delta^2 L = 0$
([above](#biharmonic-inpainting-continuing-gradients-not-flattening)), rather than independently per
channel. Three per-channel domes reach different heights and tint the core; one shared dome cannot.
It extends the rim gradient into a bulge without the runaway of an iterated reverse diffusion.

A NumPy prototype of the corrected method (`fix_prototype.py` in this bundle) produces the RMSE/SSIM
and figures reported above; porting it to the CPU and OpenCL kernels is the remaining work.

## Conclusion

The guided Laplacian method is one of the very few clipped signal reconstructions that can salvage
gradients instead of inpainting a flat, solid color. It exploits both inter-channel signal correlation
to inpaste rescaled gradients from valid channels onto clipped ones, but also propagates gradients
and color from neighbouring regions when all channels are clipped. As such, it deals with signal
magnitude and high frequencies, which makes it fairly immune to white balance discrepancies.

Its main limitation is that blown blue sky seen through green tree leaves will be recovered
green, by propagating the neighbouring color. It is not a bug, but a situation for which
the method was not designed. Indeed, the guided Laplacian was designed for the 
_unavoidable_ clipping situations where the image contains a primary light source or specular reflections.
The sun or a flame usually create a glowing halo around them, which contains enough color and gradient
information to infer the original content of the clipped area, and this is what we take advantage of
with this gradient propagation method. Other situations will still require the photographer to underexpose
the shot as to protect highlights, then increase exposure back in post-production,
which modern sensors SNR comfortably allow.

The corrected version of 2026 was derived during an attempt to formally document the maths of the initial
version of 2021. The synthetic test cases designed in 2026 from my experience and the methodical verification
of the hypotheses and strategies, coupled with the use of objective reconstruction error metrics (RMSE and SSIM) 
led to not only fix the issues, but also greatly improved the results, 
pushing further the initial mathematical intuition.

This is a lesson learned: the guided Laplacian was the last image processing method that I started 
programming before I had all the maths and theory written down, and it was tested empirically on natural images.
This hacky scheme produces quick results but doesn't allow for methodical studying of the properties
and shortcomings of the method. 

Unfortunately, having research retreats is not compatible with the workflow of an
[unmanaged open-source project](https://darktable.org) where agitation is mistaken for actual work
and communication is completely deregulated. So, when I crawled from under my rock after having
spent months on developing the [Darktable UCS 22](https://eng.aurelienpierre.com/2022/02/color-saturation-control-for-the-21th-century/),
it was only to discover that the "team" had butchered yet another previously-working part of the GUI :
I was told I was too late to oppose to the massive regression introduced behind my back,
and the sunk costs fallacy was used to justify why the crippled product of so much work
would have to make it into production no matter what, otherwise the author(s) of the damages
would cry over their misplaced desire to make a difference in what seemed like a rough mid-life crisis.

So open-source is forever condemned to mediocre hasty hacks, because the very few developers willing
to do their homework before coding anything will be unable to keep up with the devious work
of mindless hackers with too much free time, and their polished changes against an already-obsolete
codebase will be incompatible with the core of the software by the time they are ready to ship.

[^impl]: Current implementation in the Ansel source tree: `src/iop/highlights.c` (functions
`_interpolate_and_mask`, `_compute_laplacian_normalization`, `guide_laplacians`,
`heat_PDE_diffusion`, `wavelets_process`, `process_laplacian_bayer` / `_xtrans`),
`data/kernels/basic.cl` (kernels `guide_laplacians`, `diffuse_color`, `highlights_false_color`),
and `src/common/bspline.h` (`B_SPLINE_SIGMA`, `B_SPLINE_TO_LAPLACIAN`, `equivalent_sigma_at_step`).

[^forum]: Aurélien Pierre, "Guiding Laplacians to restore clipped highlights," design report and
discussion, *pixls.us* community forum, 2021.
[URL](https://discuss.pixls.us/t/guiding-laplacians-to-restore-clipped-highlights/28493).

[^he]: Kaiming He, Jian Sun, and Xiaoou Tang, "Guided Image Filtering," *IEEE Transactions on
Pattern Analysis and Machine Intelligence*, 35(6), 1397-1409, 2013. DOI:
[10.1109/TPAMI.2012.213](https://doi.org/10.1109/TPAMI.2012.213). Originally in *ECCV 2010*.

[^colorline]: The local-affine "color-line" prior — within a small patch, a surface's color channels
are affinely related. Canonical sources: Ido Omer and Michael Werman, "Color Lines: Image Specific
Color Representation," *CVPR 2004*; and the matting Laplacian of Anat Levin, Dani Lischinski, and
Yair Weiss, "A Closed-Form Solution to Natural Image Matting," *IEEE TPAMI* 30(2), 228-242, 2008,
DOI [10.1109/TPAMI.2007.1177](https://doi.org/10.1109/TPAMI.2007.1177). The related "Colorization
using Optimization" (SIGGRAPH 2004, DOI
[10.1145/1015706.1015780](https://doi.org/10.1145/1015706.1015780)) uses a softer intensity-affinity
prior.

[^qin]: Chuan Qin, Shuozhong Wang, and Xinpeng Zhang, "Simultaneous inpainting for image structure
and texture using anisotropic heat transfer model," *Multimedia Tools and Applications*, 56(3),
469-483, 2012. DOI: [10.1007/s11042-010-0601-4](https://doi.org/10.1007/s11042-010-0601-4).

[^dreggn]: Holger Dammertz, Daniel Sewtz, Johannes Hanika, and Hendrik P.A. Lensch,
"Edge-Avoiding À-Trous Wavelet Transform for fast Global Illumination Filtering," Ulm University,
2010. [PDF](https://jo.dreggn.org/home/2010_atrous.pdf).

[^unser]: Michael Unser, "Splines: A Perfect Fit for Signal and Image Processing," *IEEE Signal
Processing Magazine*, 16(6), 22-38, 1999. DOI:
[10.1109/79.799930](https://doi.org/10.1109/79.799930).

[^oono]: Y. Oono and S. Puri, "Computationally efficient modeling of ordering of quenched phases,"
*Physical Review Letters*, 58(8), 836-839, 1987. DOI:
[10.1103/PhysRevLett.58.836](https://doi.org/10.1103/PhysRevLett.58.836).

[^patra]: M. Patra and M. Karttunen, "Stencils with isotropic discretization error for differential
operators," *Numerical Methods for Partial Differential Equations*, 22(4), 936-953, 2006. DOI:
[10.1002/num.20129](https://doi.org/10.1002/num.20129).

[^ripl]: Aurélien Pierre, "Rotation-invariant Laplacian for 2D grids," 2021, which derives the
9-point stencil, the B-spline Gaussian-equivalent $\sigma_B$, and the difference-of-Gaussians to
Laplacian normalization constant used as `B_SPLINE_TO_LAPLACIAN`.
[URL](https://eng.aurelienpierre.com/2021/03/rotation-invariant-laplacian-for-2d-grids/).
---

# Studio Capture : https://ansel.photos/en/doc/views/studio-capture/

Studio Capture is a dedicated view for tethered shooting sessions. Instead of
switching back to Lighttable every time a new photo lands on disk, this view
watches a folder, imports new captures automatically, optionally applies a
set of styles to them, and always shows you the latest shot full-size — with
the filmstrip below to review the rest of the session.

{{ warning }}
Studio Capture doesn't support yet control and loading images directly from a camera. You need to use a third party program such as Digikam or Entangle to take and download images  in a local folder.
{{ /warning }}

It sits alongside Lighttable, Darkroom, Map and Print in the global menu "Ateliers".

## Quick start

1. Open **Studio Capture** in the global menu "Ateliers".
2. In **Auto import**, choose the folder your camera or tethering software writes images into.

{{ warning }}
Do not choose the on-camera folder as you either will not be able to take pictures or Ansel will not be able to refresh the folder correctly.
{{ /warning }}

3. Optionally, build a list of styles in **Auto style** to auto-apply to every incoming shot (e.g. a base look, a black & white conversion...).
4. Press **Start the session**.
5. Shoot. Each new image appears automatically in the center, with any configured styles already applied, and gets added to the filmstrip.

You can leave Studio Capture and come back — monitoring keeps running in the background as long as Ansel is open, and Ansel offers to resume it if it was still running when you last closed the application.

## Auto import

This panel controls what folder is being watched and what happens to each image that shows up in it.

### Session controls

At the top of the panel:

- **Project date** — free-text date used by the `$(YEAR)`, `$(MONTH)`, `$(DAY)` naming variables (see below). Must be typed as `YYYY-MM-DD`, optionally followed by `HH:MM:SS.mmm` — you can type just the leading part you care about (e.g. `2026` or `2026-07`) and the rest is filled in for you (`2026` becomes January 1st, 2026 at midnight). Leave empty to use the current date at scan time.
- **Jobcode** — a free-text label available as the `$(JOBCODE)` naming variable, handy for tagging a session by client or shoot name.
- **Scan frequency** — how often (in seconds) the folder is checked for new images. Changes apply the next time you start a session, not to one already running.
- **Status line** — shows whether a session is running, whether it's ready to start, or (in orange) what's missing before it can start.

### Source tab

- **Folder to survey** — the folder that receives your camera's images.
- **Delete original file** — only relevant when copying to another location (see Destination below): removes the source file once the copy has been verified byte-for-byte.

{{ warning }}
Do not choose the on-camera folder as you either will not be able to take pictures or Ansel will not be able to refresh the folder correctly.
{{ /warning }}

{{< note >}}
- Deleting image on-camera is not possible as it could corrupt the memory card.
- The source folder and scan frequency are locked while a session is running, since the engine compares each scan against a baseline recorded when monitoring started.
- The source folder cannot contain the destination folder.
{{< /note >}}

### Destination tab

- **File handling** — *Add to library* imports images where they already are; *Copy to disk* additionally copies each file to an organized location before importing it. Choosing *Copy to disk* reveals the rest of this tab:
  - **On conflict** — what to do if the computed destination file already exists:
    - *Skip*: keep the existing destination file and import that instead of copying over it.
    - *Overwrite*: replace it with the incoming source.
    - *Create unique filename* (default): copy the source under a numbered suffix instead of colliding. This is the safest default for tethering, since naming patterns often don't vary from shot to shot.
  - **Base directory**, **Project directory pattern**, **File naming pattern** — where copies land and how they're named. Both patterns accept `$(...)` variables — start typing `$(` in either field to see the list via auto-completion (things like `$(YEAR)`, `$(JOBCODE)`, `$(FILE_NAME)`...).
  - A live preview below shows the destination path your current settings would produce, so you can check it before starting.

{{< note >}}
- The destination folder cannot be contained in the source folder.
{{< /note >}}

### Starting and stopping

Press **Start the session** once your configuration is valid — the button stays greyed out otherwise, with the status line explaining what's missing (an unreadable source folder, an invalid project date, a base directory that doesn't exist or sits inside the surveyed folder, an empty naming pattern...).

Every time you press **Start**, Ansel checks whether the surveyed folder already holds images — a folder never surveyed before, new files that appeared while Ansel was closed, or files added since the last time you stopped this session — and if so asks whether to import them right away instead of silently absorbing them into the baseline. Declining just records them as already-known, so a later scan won't pick them up either.

A new file is only imported once its size and modification time stop changing for one full scan interval — this avoids importing a file while your camera or tethering software is still writing it.

## Auto style

This panel manages an ordered list of styles ("the pool") that gets applied, in order, to every image Studio Capture imports.

### Pool and Styles

- **Pool** — the styles that are actually applied on import, in the order they're applied. Each row has:
  - `↑` / `↓` to move it earlier/later in the order.
  - `-` to remove it from the pool.
- **Styles** — every style available in your library. Click the **+** icon next to a style to add it to the pool. A style already in the pool is greyed out here so you can see at a glance what's already queued.

Styles in the pool all apply in `append` pasting mode on top of each other, in list order, the same way applying several styles manually would. Reorder or remove entries at any time — it takes effect on the next imported image; already-imported photos are not retroactively changed and you would have to apply them manually by clicking the button.

### Applying styles manually

The **Apply to the displayed image** button re-applies the whole pool, in order, to whichever capture is currently shown in the center, on top of its current history (with the `append` pasting mode) — useful if you change your style pool mid-session and want to bring an earlier shot in line with the rest, without leaving Studio Capture.

## Viewing captures

The center always shows one capture, full-size, with any styles already baked in. Use the filmstrip at the bottom to look back through the session.

- **Zoom**: <kbd>double-click</kbd>, <kbd>middle-click</kbd>, or <kbd>scroll</kbd> the image to toggle between fit-to-window and 100%.
- **Pan** at 100%: <kbd>drag</kbd>, or use the <kbd>arrow keys</kbd>.
- **Filmstrip**: <kbd>double-click</kbd> a thumbnail to preview that capture in the
  center.
- **Open in Darkroom**: press <kbd>Return</kbd> at any time to open the currently displayed capture for full editing.

### Color picker

The Scopes module's color picker (point or box sample) works directly on the image shown here, the same way it does in Darkroom.

## Display toolbox

The bottom-left toolbox buttons — shared with Darkroom — affect how the
image is *displayed*:

- **Raw overexposed** / **Overexposed (clipping)** — highlight clipped highlights, with the same options (threshold, mode, colors) as in
  Darkroom.
- **Soft proof** / **Gamut check** — preview how the image will look under a target output profile, or flag out-of-gamut colors.
- **ISO 12646** — frame the image on a neutral grey background with a white margin, for more accurate visual judgment of exposure/color.
- **Picture display** — background brightness and picture margins for the center view.
- **Guides** — overlay a grid (rule of thirds, etc.) on top of the displayed image; right-click the button for guide options.

{{< note >}}
These only affect the picture while it's shown at fit-to-window zoom; they have no effect at 100%.
{{< /note >}}

## Session resume

If Ansel is closed while a session is still monitoring, it offers to resume that session the next time it starts — accepting switches straight to Studio Capture and restarts monitoring on the same folder. Declining stops asking for that session. Stopping a session manually also clears the prompt.

## Tips

- Keep the scan frequency low (a few seconds) for a responsive tethering workflow; there's little cost to scanning often.
- The base directory for copies can never be inside the folder being surveyed, to avoid the copies themselves being picked up as new input.
- The picker position is remembered between images, so it makes easier to check the value of a particular area. 
---

# Getting started : https://ansel.photos/en/doc/getting-started/

Ansel is a **non-destructive RAW photo editor**. It never modifies your original files: every change is stored as a recipe (an editing _history_) in a [library](library.md) database, and only written out to a real image file when you [export](../views/toolboxes/export.md). This means your originals stay safe, edits are reversible forever, and the same recipe can be applied to many images at once.

This section is a quick bootstrap. Each step links to a more detailed page and to the relevant [view](../views/_index.md).

## The four-step workflow

```mermaid
flowchart LR
    A[Import] --> B[Cull &amp; organize] --> C[Edit] --> D[Export]
```

### 1. Import

Tell Ansel about your images. From the [global menu](../views/global-menu.md), choose _File → Import…_ (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd>). You can either reference images where they already are, or copy them off a memory card into a tidy folder structure as you import.

More details: [Import images](import.md)

### 2. Cull and organize

Imported images appear as thumbnails in the [lighttable](../views/lighttable/_index.md). This is where you sort the keepers from the rest: assign **star ratings** (<kbd>0</kbd>–<kbd>5</kbd>), **reject** (<kbd>R</kbd>) or **color labels** (<kbd>F1</kbd>–<kbd>F6</kbd>), add tags and metadata, and narrow the view down to exactly the images you want with the [collection filters](library.md) and the _Library_ tool.

To act on images — rate them, tag them, copy an edit onto them — you first **select** them with a click or a keystroke. Selection is deliberate and explicit, so nothing changes by accident.

More details: [Library and collections](library.md) · [Image selection](selection.md)

### 3. Edit

Double-click a thumbnail (or select it and press <kbd>Enter</kbd>) to open it in the [darkroom](../views/darkroom/_index.md). The image-processing [modules](../views/darkroom/modules/_index.md) sit in the right panel, grouped into workflow tabs that follow the [pixelpipe](../views/darkroom/pixelpipe/_index.md). Editing left-to-right across the tabs, and bottom-to-top within them, walks you through a sound order of operations.

There is **no save button**: every change is recorded automatically into the history. You can step back at any time with <kbd>Ctrl</kbd>+<kbd>Z</kbd>, or revisit any past state from the [history of changes](../views/toolboxes/history-stack.md).

More details: [Editing images](editing.md)

### 4. Export

Apply the history to the original to produce a final file. Select the images and choose _File → Export…_ (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd>) to render them to JPEG, TIFF and other formats. You can also reuse an edit across a whole shoot by copying its history or saving it as a [style](../views/toolboxes/styles.md).

More details: [Export](../views/toolboxes/export.md)

## Finding your way around

A few things worth knowing before you start:

- **Views.** Ansel is split into [views](../views/_index.md) — _lighttable_ for browsing, _darkroom_ for editing, plus optional map, print and slideshow. Switch views from the **Ateliers** menu; return to the lighttable from anywhere with <kbd>Escape</kbd>.
- **The menu bar is the backbone.** Most application-wide commands live in the [global menu](../views/global-menu.md) at the top of the window. Press <kbd>Alt</kbd> to reveal each menu's keyboard mnemonic.
- **Keyboard or mouse, your choice.** The whole interface can be driven from either. The [global action search](keyboard.md#vimkey-like-global-action-search) (<kbd>Ctrl</kbd>+<kbd>P</kbd>) finds and triggers any command by name.

More details: [Shortcuts and keyboard interaction](keyboard.md)

## Coming from another editor?

If you are migrating from Darktable, the [from Darktable](../from-darktable.md) page summarizes what changed. The short version: selection is explicit (hovering never changes anything), and the GUI is organized around the menu bar and pipeline-ordered module tabs rather than customizable icon groups.
---

# Install on Windows : https://ansel.photos/en/doc/install/windows/

## Prerequisites

Install your OpenCL GPU drivers if you have a GPU.

On Windows 11, it seems that the system OpenCL drivers for Intel embedded GPU cause issues (black images), especially with newer generation CPU. You may want to [remove Windows drivers](https://community.intel.com/t5/OpenCL-for-CPU/uninstall-Intel-OpenCL/m-p/1134032#M5756) and [install Intel ones](https://www.intel.com/content/www/us/en/developer/articles/tool/opencl-drivers.html#proc-graph-section).

See the [caveats section below](#caveats) for more details.

## EXE package (recommended)

The Ansel project provides an official `.exe` package, built for the stable, pre-release and experimental channels. This is the recommended way of installing Ansel, since it is fresh from the repository, always up-to-date and ships updated lens databases for Lensfun.

### Downloads

- [Get the latest EXE package](https://nightly.link/aurelienpierreeng/ansel/workflows/win-nightly/master/ansel.stable.win64.zip)
- [Find earlier EXE packages](https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0).

## Building from source code manually

- Step 1:  Install MSYS2 (instructions and prerequisites can be found on the official website: https://www.msys2.org)
- Step 2:  Using the MSYS terminal - Update the base system until no further updates are available by repeating:
  ```bash
  $ pacman -Syu
  ```
- Step 3:  Using the UCRT64 terminal - Clone the Ansel git repository (in this example into `~/ansel`):
  ```bash
  $ cd ~
  $ git clone --depth 1 https://github.com/aurelienpierreeng/ansel.git
  $ cd ansel
  $ git submodule init
  $ git submodule update
  ```
- Step 4:  Using the UCRT64 terminal - Install all build dependencies using the script from the repository:
  ```bash
  $ ./packaging/install-deps-windows-msys2.sh
  ```
- Step 5:  Using the UCRT64 terminal - Update your lensfun database:
  ```bash
  $ lensfun-update-data
  ```

{{< note >}}
MSYS will initialize a personal Unix-like `/home` folder, by default located in `C:\\msys64\home\USERNAME` where `USERNAME` is your current Windows username. If your username contains non-latin characters, like accentuated letters, the clashes between Windows encoding and Linux encoding will make the compilation fail on directory pathes issues. In that case, create a new directory in `C:\\msys64\home\USERNAME_WITHOUT_ACCENTS`, and in the MSYS terminal, do `cd /home/USERNAME_WITHOUT_ACCENT`.
{{</ note >}}

- Step 6:  Using a text editor, eg. MS Notepad - Modify the `.bash_profile` file in your `$HOME` directory and add the following lines:
  ```bash
  # Added as per http://wiki.gimp.org/wiki/Hacking:Building/Windows
  export PREFIX="/ucrt64"
  export LD_LIBRARY_PATH="$PREFIX/lib:$LD_LIBRARY_PATH"
  export PATH="$PREFIX/bin:$PATH"
  ```
- Step 7:  By default CMake will only use one core during the build process. To speed things up, using a text editor, eg. MS Notepad, you might wish to add a line like:
  ```bash
  export CMAKE_BUILD_PARALLEL_LEVEL="8"
  ```
  to your `~/.bash_profile` file. This would use 8 cores.
  
- Step 8:  Using the UCRT64 terminal - Execute the following command to activate profile changes:
  ```bash
  $ . .bash_profile
  ```
- Step 9:  Using the UCRT64 terminal - Build and install Ansel:
  - Variant 1: __with all contextual optimizations enabled for your hardware__:
    ```bash
    $ mkdir build
    $ cd build
    $ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DBINARY_PACKAGE_BUILD=OFF -DCMAKE_INSTALL_PREFIX=/opt/ansel ../.
    $ cmake --build .
    $ cmake --install .
    ```
    After this, Ansel will be installed in `/opt/ansel` directory and can be started by typing `/opt/ansel/bin/ansel.exe` in MSYS2 UCRT64 terminal.
  - Variant 2: __with only generic optimizations and producing an installable EXE package__:
    ```bash
    $ mkdir build
    $ cd build
    $ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DBINARY_PACKAGE_BUILD=ON -DCMAKE_INSTALL_PREFIX=/opt/ansel ../.
    $ cmake --build . --target package
    $ cmake --install .
    ```
    After this, you will need to double-click and install the `ansel.exe` found in the `build/bin` folder. This package will be portable and be installable on other platforms.

## VS Code setup

To make debugging easier with VS Code, you can use the UCRT64 terminal within the program, which is better suited than the standard Windows terminal. For example, you can use GDB and simply `Ctrl + click` on relevant lines in the debugger output to jump directly to the corresponding line in the code editor.

- To add UCRT64 terminal:
  - Open settings.json
  - Insert this code in the line before the last `}` :

    {{< warning >}} The preceding item must end with a `,` {{</ warning >}}

    ```json 
    "terminal.integrated.profiles.windows": {
      "UCRT64": {
        "path": "C:\\MSYS2\\usr\\bin\\bash.exe",
        "env": {
            "MSYSTEM": "UCRT64",
            "CHERE_INVOKING": "1"},
        "args": [ "--login", "-i"],
      }
    }
    ```

- To make it the default terminal in VS Code:
  - Insert this code in the line before the last `}` :

  {{< warning >}} The preceding item must end with a `,` {{</ warning >}}

  ```json
  "terminal.integrated.defaultProfile.windows": "UCRT64"
  ```

## Caveats

### Starting in command line (with arguments)

In some situations, you will need to start Ansel in command line, with arguments modifying its default behaviour, to test or debug issues:

1. If you built yourself, start the MSYS2 MINGW64 terminal (from the applications menu), and execute `/opt/ansel/bin/ansel.exe`,
2. If you installed from the EXE package, open the Windows terminal (`cmd.exe`) and execute `"C:\Programs Files\ansel\bin\ansel"` (assuming you installed Ansel in the default directory, suggested by the installer).

For example, if you note issues with OpenCL, you could start Ansel with OpenCL entirely disabled using `"C:\Programs Files\ansel\bin\ansel" --disable-opencl`.

The caveat though is the debugging commands (`-d OPTION`) don't output messages to the terminal, as they do on Unix, because of issues with Windows. Instead, the output will be written in an `ansel-log.txt` text file, in your cache directory. Start the help of the software ( runing `"C:\Programs Files\ansel\bin\ansel" -h`), and the last line should tell you where the file will be located after the line `note: debug log and output will be written to this file: PATH`. Typically, it should be `C:\Users\USERNAME\AppData\Local\Microsoft\Windows\INetCache\ansel` on Windows 10.

### OpenCL

Darktable blacklists all Intel OpenCL drivers to prevent issues, because of an history of bad drivers. In practice, since Intel Neo, things are better and reasonably-old Intel platforms are well supported, so the blacklist is removed on Ansel. OpenCL issues (typically: black images) are still regularly reported with brand-new hardware.

If you find yourself in this situation, you have several mitigation options:

0. Try to install a newer or an older version of your GPU driver.
1. If you have a discrete GPU (Nvidia or AMD), you can disable the Intel embedded GPU:
    1. By entirely removing the Intel OpenCL driver (use your software manager to locate it), so Ansel uses only your discrete driver,
    2. By entirely disabling the Intel GPU in Ansel config:
        - Locate the `anselrc` text file on your system (typically in `C:\Users\USERNAME\AppData\Local\ansel`),
        - Open it and locate the line `cldevice_v4_YOUR_DEVICE=0 250 0 16 16 128 0 0 0.053989` where `YOUR_DEVICE` is the name of your GPU (possibly associated with a driver version),
        - On that line, change the 8th (penultimate) digit from `0` to `1`, so you get `cldevice_v4_YOUR_DEVICE=0 250 0 16 16 128 0 1 0.053989`,
2. Try to change the build options for OpenCL kernels:
    - Locate the `anselrc` text file on your system (typically in `C:\Users\USERNAME\AppData\Local\ansel`),
    - Open it and locate the line `cldevice_v4_YOUR_DEVICE_building=-cl-fast-relaxed-math` where `YOUR_DEVICE` is the name of your GPU (possibly associated with a driver version), and the options after `=` may be different from this example,
    - Clear the building options, so you get `cldevice_v4_YOUR_DEVICE_building=` (nothing after `=`)
3. If you don't have a discrete GPU and the Intel one is your only one:
    1. Start the application with OpenCL disabled at all with `COMMAND --disable-opencl` (see the previous section for the actual system command to run, depending on your installation)
    2. Disable the Intel GPU in Ansel config (see point 1.2. above)


### Memory allocation

Ansel uses virtual memory pre-allocation for fast memory access. Virtual memory needs to be enabled on your system and the pagefile size required by Ansel should be available on your system. If, at startup, you see errors like :

```bash
couldn't alloc map (VirtualAlloc error 1455)
ERROR: can't init pixelpipe cache, aborting.
```

it means the system refused to allocate the requested memory. This can happen on gamers PC where performance optimizations have been made.

#### Diagnose 

Open Task Manager → Performance → Memory and check `Committed: X / Limit`. `X` is the currently-used virtual memory. If it is close to `Limit` you will have to close applications using it.

#### Fix 

The first approach is to enable or increase the virtual memory on your system. Go to System Properties → Advanced → Performance → Virtual Memory and set pagefile to system managed or several GB.

If this is not enough, the second approach is to reduce Ansel memory consumption. Open `C:\%LOCALAPPDATA%\ansel\anselrc` with a text editor and locate the lines :

```
host_memory_limit=-1
...
memory_os_headroom=25000
```

When set to `-1`, `host_memory_limit` detects all the RAM available on your system and will allocate to Ansel `host_memory_limit - memory_os_headroom`. Values are in MiB. To manually control this, you can set `host_memory_limit=4000` (4 GiB) and `memory_os_headroom=0` as a starting point. If that works, you can try allocating more.---

# Ansel-cli : https://ansel.photos/en/doc/cli/ansel-cli/

The `ansel-cli` binary starts the command line interface variant of Ansel which allows images to be exported.

This variant does not open any display -- it works in pure console mode without launching a GUI. This mode is particularly useful for servers running background jobs.

`ansel-cli` can be called with the following command line parameters:

```
ansel-cli [<input file or folder>]
              [<xmp file>]
              <output file or folder>
              [--width <max width>]
              [--height <max height>]
              [--hq <0|1|true|false>]
              [--upscale <0|1|true|false>]
              [--style <style name>]
              [--style-overwrite]
              [--apply-custom-presets <0|1|false|true>]
              [--out-ext <extension>]
              [--import <file or dir>]
              [--icc-type <type>]
              [--icc-file <file>]
              [--icc-intent <intent>]
              [--verbose]
              [--help [option]]
              [--core <Ansel options>]
```

The user must supply an input filename and an output filename. All other parameters are optional.

`<input file or folder>`
: The name of the input file or folder (containing images) to be exported. If you wish to process multiple images or multiple folders use the `--import` option instead.

`<xmp file>`
: The optional name of an XMP sidecar file containing the history stack data to be applied during export. If this option is not provided Ansel will search for an XMP file that belongs to the given input file(s).

`<output file or folder>`
: The name of the output file or destination folder. The export file format is derived from the file extension or from the `--out-ext` option. You can also use a number of [variables](..//variables.md) in the output filename. For obvious reasons this parameter is mandatory if you use the program on an image folder containing multiple images. If you specify output folder it is recommended that you also specify the file format with `--out-ext`.

`--width <max width>`
: Limit the width of the exported image to the given number of pixels.

`--height <max height>`
: Limit the height of the exported image to the given number of pixels.

`--hq <0|1|true|false>`
: Define whether to use high quality resampling during export (see the [export](../views/toolboxes/export.md) module reference for more details). Defaults to true.

`--upscale <0|1|true|false>`
: Define whether allow upscaling during export. Defaults to false.

`--style <style name>`
: Specify the name of a style to be applied during export. If a style is specified, the path to the Ansel configuration directory must also be specified (i.e. `--core --configdir ~/.config/Ansel`). By default no style is applied.

`--style-overwrite`
: The specified style overwrites the history stack instead of being appended to it.

`--apply-custom-presets <0|1|false|true>`
: Whether to load `data.db` which contains presets and styles. Disabling this option allows you to run multiple instances of `Ansel-cli` at the cost of being unable to use the `--style` option. Defaults to true.

`--out-ext <extension>`
: Set the output extension to use. If specified takes precedence over `<output file>`. By default this is extracted from `<output file>`. Defaults to `jpg` if `<output folder>` is specified.

`--import <file or dir>`
: Specify input file or folder, can be used multiple times. This option cannot be combined with `<input file or folder>`.

`--icc-type <type>`
: Specify the ICC profile type, which is the same as specifying the "output profile" in the [_output color profile_](../views/darkroom/modules/output-color-profile.md) module. Defaults to "image specified". Use `--help icc-type` to obtain a list of the supported types. See the _output color profile_ module reference for a more detailed description of the available options.

`--icc-file <file>`
: Specify the ICC profile filename. Defaults to an empty filename.

`--icc-intent <intent>`
: Specify the rendering intent. Defaults to "image specified". Use `--help icc-intent` to obtain a list of the supported intents. See [rendering intent](../    color-management/rendering-intent) for a more detailed description of the available options.

`--verbose`
: Enables verbose output.

`--help [option]`
: Prints usage and exits. If `option` is specified, additionally prints usage for the given option.

`--core <Ansel options>`
: All command line parameters following `--core` are passed to the Ansel core and handled as standard parameters. See the [`Ansel binary`](./Ansel.md) section for a detailed description.

## Export options

Export options for Ansel are defined as configuration items, set from within the [export module](../views/toolboxes/export.md). There are two ways to alter this configuration when using `Ansel-cli`, as described below.

### Use the export module

The `Ansel-cli` command will use the last format configuration used in the export module, when run in interactive (gui) mode. You may therefore manually set your desired format options in the Ansel gui and then run `Ansel-cli` to export your files.

### Pass options on the command-line

You can set any export format configuration option using the following syntax:

```
    --core --conf plugins/imageio/format/<FORMAT>/<OPTION>=<VALUE>
```

where `<FORMAT>` is the name of the desired output format and `<OPTION>` is any configuration option for that format.

An option set in this way will not be permanently stored but will be used just for this run of `Ansel-cli`.

The following sections describe the configuration options/values that are available for each export format:

#### Jpeg

`quality`
: The compression quality (`5` - `100`)

#### J2k (jpg2000)

`format`
: The format of the output
:  - `0`: J2K
:  - `1`: jp2

`quality`
: The compression quality (`5` - `100`)

`preset`
: The DCP mode
: - `0`: Cinema2K, 24 FPS
: - `1`: Cinema2K, 48 FPS
: - `2`: Cinema4K, 24 FPS

#### Exr (OpenEXR)

`bpp`
: The bit depth (`16` or `32`)

`compression`
: The compression type
:  - `0`: uncompressed
:  - `1`: RLE
:  - `2`: ZIPS
:  - `3`: ZIP
:  - `4`: PIZ
:  - `5`: PXR24
:  - `6`: B44
:  - `7`: DWAA
:  - `8`: DWAB

#### Pdf

`title`
: The title of the pdf (any character)

`size`
: The size of the pdf (`a4`, `a3`, `letter`, `legal`)

`orientation`
: the paper orientation of the pdf
:  - `0`: portrait
:  - `1`: landscape

`border`
: The empty space around the pdf; format: size (a number) + unit; examples: 10 mm, 1 inch

`dpi`
: The resolution in dots per inch inside the pdf (`1` - `5000`)

`rotate`
: Whether to rotate the pdf (`0` or `1`)

`icc`
: Whether to embed an icc profile (`0` or `1`)

`bpp`
: The bit depth (`8` or `16`)

`compression`
: Whether to compress the pdf (`0` or `1`)

`mode`
: The mode to put the images in the pdf
:  - `0`: normal: just put the images into the pdf
:  - `1`: draft: images are replaced with boxes
:  - `2`: debug: only show the outlines and bounding boxen

#### Pfm

No options provided.

#### Png

`bpp`
: The bit depth (`8` or `16`)

`compression`
: The compression level (`0` - `9`)

#### Ppm

No options provided.

#### Tiff

`bpp`
: The bit depth (`8`, `16`, `32`)

`compress`
: The compression type
:  - `0`: uncompressed
:  - `1`: deflate
:  - `2`: deflate with predictor

`compresslevel`
: The compression level (`0` - `9`)

`shortfile`
: B&W or color image
:  - `0`: write rgb colors
:  - `1`: write grayscale

#### Webp

`comp_type`
: The compression type
:  - `0`: lossy
:  - `1`: lossless

`quality`
: the compression quality (`5` - `100`)

`hint`
: The preferred way to manage the compression
:  - `0`: default
:  - `1`: picture: digital picture, like portrait, inner shot
:  - `2`: photo: outdoor photograph, with natural lighting
:  - `3`: graphic: discrete tone image (graph, map-tile etc)

#### Copy

No options provided.

#### Xcf

`bpp`
: The bit depth (`8`, `16`, `32`)
---

# Blend modes : https://ansel.photos/en/doc/views/darkroom/masking-and-blending/blend-modes/

Blend modes define how the input and output of a module are combined (blended) together before the module's final output is passed to the next module in the pixelpipe.

Classic blending modes, designed for display-referred RGB (constrained to 0-100%), implicitly define a fulcrum at 50% (gray) or 100% (white) in their algorithms, depending on the blend mode. Because scene-referred is not subject to these restrictions, this fulcrum needs to be explicitly defined by the user when performing blending operations in the "RGB (scene)" color space. The additional _blend fulcrum_ parameter will be presented to the user when using one of these blend modes in this color space. The effect depends on the operator used. For example, values above the fulcrum might be brightened and values below darkened, or vice versa.

The final output of a module is computed 'per-pixel' as follows:

```math
final_output = (1.0 - opacity) * module_input + opacity * blended_output
```

where the `blended_output` is a combination of the input and output images, depending on the blend mode (below), and the `opacity` is defined 'per-pixel' by a combination of the mask and global opacity parameter. An opacity of 0% outputs an image that is identical to the input image of the module.

The _reverse_ button (the polarity toggle next to the blend-mode selector) effectively reverses the roles of the input and output images in the 'per-pixel' computation:

```math
final_output = (1.0 - opacity) * module_output + opacity * blended_input
```

where the `blended_input` is a combination of the output and input images, depending on the blend mode below where _output_ and _input_ image references are reversed. In "reversed" blend modes, an opacity of 0% outputs an image that is identical to the _output_ image of the module.

## Normal modes

normal
: The most commonly used blend mode, "normal" simply mixes input and output to an extent determined by the opacity parameter. This mode is commonly used to reduce the strength of a module's effect by reducing the opacity. This is also usually the blend mode of choice when applying a module's effect selectively with masks. This mode is also known as the "over" Porter-Duff alpha blending operator (see [alpha compositing](https://en.wikipedia.org/wiki/Alpha_compositing) for more details).

normal bounded
: _not available in the "RGB (scene)" color space_
: This blend mode is the same as “normal”, except that the input and output data are clamped to a particular min/max value range. Out-of-range values are effectively blocked and are not passed to subsequent modules. Sometimes this helps to prevent artifacts. However, in most cases (e.g. highly color-saturated extreme highlights) it is better to let unbound values travel through the pixelpipe to be properly handled later. The “normal” blend mode is therefore usually preferred.

## Arithmetic modes

addition
: Add together the pixel values of the input and output images, lightening the output. When blending in the "RGB (scene)" color space, the pixel values of the output image are multiplied by a value proportional to the "blend fulcrum".

subtract
: Subtract the pixel value of the _output_ from the _input_. When blending in the "RGB (scene)" color space, the pixel values of the output image are multiplied by a value proportional to the "blend fulcrum". Pixel values less than 0 are set to 0.

multiply
: Multiply the pixel values of the input and output together. When blending in display-referred color spaces, pixel values are between 0 and 1.0, the final output will be clamped and will always be darker. When blending in the "RGB (scene)" color space, this value is further multiplied by a value proportional to the "blend fulcrum". In this case, values may be greater than 1.0 and therefore brighten the base image. This may have other side-effects, such as updating the white point in the filmic module.

: Multiply blending simulates an optical variable density filter, where the density is defined by the output of the module. It has many applications, from blooming and local contrast enhancements (when used with a blur or low-pass filter) to dodging/burning and global contrast enhancements (when used with exposure). The fulcrum sets the output intensity threshold between darkening and brightening (any RGB value below fulcrum will darken).

divide
: Divide the pixel values of the input by the output. When blending in the "RGB (scene)" color space, the pixel values of the output image are multiplied by a value proportional to the "blend fulcrum".

: Since this is the inverse of the multiply mode, it will darken where multiply brightens and vice versa. Everything else works in essentially the same way.

screen
: _not available in the "RGB (scene)" color space_
: Invert the input and output pixel values, multiply those values together and invert the result. This yields approximately the opposite effect to "multiply" mode -- the resulting image is usually brighter, and sometimes “washed out” in appearance.

average
: Return the arithmetic mean of the input and output pixel values.

difference
: Return the absolute difference between the input and output pixel values.

geometric mean
: Return the square root of the product of the input and output pixel values.

harmonic mean
: Return the product of the input and output pixel values, multiplied by 2 and divided by their sum.

## Contrast enhancing modes

The following modes are not available in the "RGB (scene)" blending color space as they rely on an assumption of "50% mid gray" which only applies to display-referred and non-linear color spaces.

overlay
: This mode combines the "multiply" and "screen" blend modes: The parts of the input where the output is brighter, become brighter; The parts of the image where the output is darker, become darker; Mid-gray is unaffected.

softlight
: This mode is similar to "overlay", except the results are softer and less bright.

hardlight
: This mode is not related to "softlight" in anything but name. Like overlay mode it is a combination of "multiply" and "screen" modes and has a different effect above and below mid-gray. The results with hardlight blend mode tend to be quite intense and usually need to be combined with a reduced opacity.

vividlight
: This mode is an extreme version of overlay/softlight. Values darker than mid-gray are darkened; Values brighter than mid-gray are brightened. You will probably need to tone down its effect by reducing the opacity

linearlight
: This mode is similar to the effect of "vividlight".

pinlight
: This mode performs a darken and lighten blending simultaneously, removing mid-tones. It can result in artifacts such as patches and blotches.

## Color channel modes

### Lab channels

The following are available for blending in the Lab color space only

Lab lightness
: Mix the lightness from the input and output images, while taking the color channels (a and b) unaltered from the input image. In contrast to “lightness” this blend mode does not involve any color space conversion and does not clamp any data. In some cases this blend mode is less prone to artifacts than “lightness”.

Lab a-channel
: Mix the Lab "a" color channel from the input and output images, while taking the other channels unaltered from the input image.

Lab b-channel
: Mix the Lab "b" color channel from the input and output images, while taking the other channels unaltered from the input image.

Lab color
: Mix the Lab color channels (a and b) from the input and output images, while taking the lightness unaltered from the input image. In contrast to “color” this blend mode does not involve any color space conversion and does not clamp any data. In some cases this blend mode is less prone to artifacts than “color”.

### RGB channels

The following are available when blending in RGB color spaces only.

RGB red channel
: Mix the "red" channel from the input and output images, while taking the other channels unaltered from the input image. When blending in the "RGB (scene)" color space, the "red" channel from the output image is multiplied by a value proportional to the "blend fulcrum".

RGB green channel
: Mix the "green" channel from the input and output images, while taking the other channels unaltered from the input image. When blending in the "RGB (scene)" color space, the "green" channel from the output image is multiplied by a value proportional to the "blend fulcrum".

RGB blue channel
: Mix the "blue" channel from the input and output images, while taking the other channels unaltered from the input image. When blending in the "RGB (scene)" color space, the "blue" channel from the output image is multiplied by a value proportional to the "blend fulcrum".

## HSV channels

The following are available when blending in the "RGB (display)" color space only.

HSV value
: Mix the lightness from the input and output images, while taking color unaltered from the input image. In contrast to “lightness” this blend mode does not involve clamping.

HSV color
: Mix the color from the input and output images, while taking lightness unaltered from the input image. In contrast to “color” this blend mode does not involve clamping.

## Others

lightness
: Mix lightness from the input and output images, while taking color (chromaticity and hue) unaltered from the input image.

chromaticity
: Mix chromaticity from the input and output images, while taking lightness and hue unaltered from the input image. This blend mode uses RGB ratios, divided by a Euclidean norm.

lighten
: _not available in the "RGB (scene)" color space_
: Compare the pixel values of the input and output images, and output the lighter value.

darken
: _not available in the "RGB (scene)" color space_
: Compare the pixel values of the input and output images, and output the darker value.

hue
: _not available in the "RGB (scene)" color space_
: Mix hue (color tint) from the input and output images, while taking lightness and chroma unaltered from the input image.

color
: _not available in the "RGB (scene)" color space_
: Mix color (chroma and hue) from the input and output images while taking lightness unaltered from the input image.

: _Caution: When modules drastically modify hue (e.g. when generating complementary colors) this blend mode can result in strong color noise._

coloradjustment
: _not available in the "RGB (scene)" color space_
: Some modules act predominantly on the tonal values of an image but also perform some color saturation adjustments. This blend mode takes the lightness from the module's output and mixes colors from input and output, enabling control over the module's color adjustments.
---

# Darkroom view layout : https://ansel.photos/en/doc/views/darkroom/darkroom-view-layout/

The darkroom surrounds the center image with a left panel of peripheral tools, a right panel of image-processing modules, and a bottom toolbar of visual-assessment overlays. An optional [filmstrip](../../views/toolboxes/filmstrip.md) (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd>) can be shown along the bottom for quick navigation through the current collection.

## Left panel

Shown and hidden with <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>L</kbd>. It holds the peripheral tools that support editing (but do not themselves process pixels):

[Navigation](../../views/toolboxes/navigation.md)
: Navigate and zoom the center view via a thumbnail overview. Sits at the top of the panel.

[Scopes](../../views/toolboxes/scopes.md)
: A graphical depiction of the image's tones and colors (histogram, waveform, vectorscope…). The [global color picker](../../views/toolboxes/global-color-picker.md), used to sample colors from the image, is part of this module.

[Snapshots](../../views/toolboxes/snapshots.md)
: Take and compare snapshots against the current edit.

[Duplicate manager](../../views/toolboxes/duplicate-manager.md)
: View and manage the duplicates (versions) of the current image.

[Mask manager](../../views/toolboxes/mask-manager.md)
: View and edit the [drawn shapes](masking-and-blending/masks/drawn.md) used by masks.

[History of changes](../../views/toolboxes/history-stack.md)
: The editing history of the current image, where you can step back to any previous state, compress or reset the history.

[Image information](../../views/toolboxes/image-information.md)
: Display EXIF and IPTC information about the current image.

[Notes](../../views/toolboxes/notes.md)
: Free-text notes attached to the image.

{{< note >}}
Coming from Darktable: the _metadata_ and _tags_ editors are no longer duplicated in the darkroom. Metadata handling now belongs to the [lighttable](../lighttable/_index.md); the darkroom is dedicated to editing.
{{< /note >}}

## Right panel

Shown and hidden with <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>R</kbd>. It is dedicated to the [image-processing modules](../../views/darkroom/modules/_index.md): the workflow tabs sit at the top, and the modules themselves fill the rest of the panel.

To inspect or rearrange the order in which modules are applied, see [the pixelpipe and module order](pixelpipe/the-pixelpipe-and-module-order.md).

## Module workflow tabs

The processing modules are grouped into tabs that follow the pixelpipe, so that editing from left to right (across tabs) and bottom to top (within a tab) walks you through a sound workflow. Hover a tab label for its description.

Pipeline
: Lists every module that is currently **enabled**, in the reverse order of application in the pixelpipe (last-applied on top). This is your overview of what the image actually does.

Basic
: Modules that adjust brightness, contrast and dynamic range, work with film scans, and perform color-grading.

Repair
: Modules that repair and reconstruct noisy or missing pixels.

Sharpness
: Modules that manipulate local contrast, sharpness and blur.

Effects
: Modules applying special effects.

Technics
: Technical modules that can be ignored in most situations.

All
: Every module available in the software.

{{< note >}}
Coming from Darktable: the icon-labelled module groups and the user-customizable group editor are gone. Tabs are named in words, fixed, and ordered to match the pipeline. The "favourite modules" tab is replaced by the ability to jump directly to any module through a shortcut or the [action search](../../getting-started/keyboard.md#vimkey-like-global-action-search).
{{< /note >}}

### Navigating tabs and modules

With the mouse
: Click a visible tab; click the arrows to reveal hidden tabs; right-click a tab for the full list; or scroll over the tab bar to cycle. Scroll the panel to move through modules; unfolding a module scrolls it into view.

With the keyboard
: <kbd>Ctrl</kbd>+<kbd>Tab</kbd> / <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Tab</kbd> move to the next/previous tab (cycling at the ends). <kbd>Page Down</kbd> / <kbd>Page Up</kbd> expand and focus the next/previous module in the current tab. <kbd>Ctrl</kbd>+<kbd>Down</kbd> / <kbd>Ctrl</kbd>+<kbd>Up</kbd> move focus between a module's controls.

The complete keyboard model — focusing a module or control, then editing it — is in [shortcuts and keyboard interaction](../../getting-started/keyboard.md#darkroom).

### Reordering modules

Hold <kbd>Ctrl</kbd>+<kbd>Shift</kbd> and drag a module by its header to move it. Be aware that this changes the **order of the modules in the pixelpipe**, not just their position on screen — it is best done from the _Pipeline_ or _All_ tab, where you can see the whole pipeline. See [the pixelpipe and module order](pixelpipe/the-pixelpipe-and-module-order.md).

## Bottom panel

A toolbar of visual-assessment overlays. From left to right:

[Styles](../../views/toolboxes/styles.md)
: Quick-access styles menu. Hover a style name to preview it on the current image.

[Color assessment](../../views/toolboxes/color-assessment.md)
: Toggle the ISO 12646 color-assessment view (neutral surround and reference frame).

[Raw overexposed](../../views/toolboxes/raw-overexposed.md)
: Toggle raw-overexposure indicators (right-click for options).

[Clipping](../../views/toolboxes/clipping.md)
: Toggle output clipping warnings (right-click for options).

[Soft proofing](../../views/toolboxes/soft-proof.md)
: Toggle soft-proofing (right-click for options).

[Gamut check](../../views/toolboxes/gamut.md)
: Toggle gamut checking (right-click for options).

[Guides & overlays](../../views/toolboxes/guides-overlays.md)
: Left-click to toggle the guide overlays; right-click to change the guide settings, including the color of all on-image drawing (masks, crop guides, etc.).

Pipeline node graph
: Open the [module-order graph](pixelpipe/the-pixelpipe-and-module-order.md#changing-module-order) — a left-to-right view of the pixelpipe where you can inspect and rearrange the order in which modules are applied.

Autoset
: Run [autoset](_index.md#autoset-auto-developing-an-image) on the chosen modules: each capable module computes its own settings from the image content. Right-click to pick which modules participate.
---

# Display profile : https://ansel.photos/en/doc/color-management/display-profile/

 For darktable to faithfully render colors on screen it needs to find the correct display profile for your monitor. In general this requires your monitor to be properly calibrated and profiled, and it needs the profile to be correctly installed on your system. darktable queries your X display server's xatom as well as the system service colord (if available) for the right profile. If required you can enforce a specific method in [preferences > miscellaneous](../preferences-settings/miscellaneous.md).

To investigate your display profile configuration you can run the [`ansel-cmstest`](../cli/ansel-cmstest.md) binary (Linux only) which prints out useful information (e.g. profile name per-monitor) and tells you if the system is correctly configured.

In rare cases you may need to manually select the display profile. This is possible from within the [soft proof](../views/toolboxes/soft-proof.md) and [gamut check](../views/toolboxes/gamut.md) option dialogs in the darkroom view and the display profile dialog in the lighttable view.

Bear in mind that high-tier consumer-grade screens usually don't need a user-made display profile unless you need to perform soft-proofing with professional expectations, since they are properly calibrated to sRGB in the factory.

A poorly-made display profile will be more harmful than sticking to the default sRGB profile, since the default might be slightly inaccurate but will at least be reliable. Advanced and professional users are advised to proceed with the production of custom display profiles only if they have the training to assess the quality of the resulting profile and understanding of the profiling options.
---

# Drawn masks : https://ansel.photos/en/doc/views/darkroom/masking-and-blending/masks/drawn/

With the drawn mask feature you can construct a mask by drawing shapes directly onto the image canvas. Shapes can be used alone or in combination. Once a shape has been drawn on an image it can be adjusted, removed, or reused in other modules.

Shapes are stored internally as vectors and are rendered with the required resolution during pixelpipe processing. Shapes are expressed in the coordinate system of the original image and are transformed along with the rest of the image by any active distorting modules in the pipe ([_lens correction_](../../modules/lens-correction.md), [_rotate and perspective_](../../modules/rotate-perspective.md) for example). This means that a shape will always work on the same image area regardless of any modifications that may be subsequently applied.

The controls required to create and alter drawn masks may be enabled by selecting either the "drawn mask" or "drawn & parametric mask" icon at the bottom of a module. You can also create and edit shapes using the [mask manager](../../../toolboxes/mask-manager.md) module.

## Creating shapes

Choose a shape by clicking on the appropriate shape icon (from left to right: circle, ellipse, path, brush, gradient).

This will take you into the creation mode for that shape. Once you have finished drawing your shape you will automatically be taken into edit mode.

Ctrl+click on the shape icon to continuously draw multiple shapes of the same type -- each time a shape is completed, you will re-enter creation mode for a new instance of that shape. While in continuous creation mode, right-click on the image to stop drawing shapes and enter edit mode.

For all drawn shapes, you can hold Shift while scrolling with the mouse wheel to change the extent of the shape's feathering (the blur at the edge of the shape) and use Ctrl+scroll to change the shape's opacity (how transparent it is). These operations are available in both creation and edit modes (as long as your mouse is over the shape in question).

By default, scrolling your mouse up increases the value of the relevant shape parameters. This behavior can be changed in [preferences > darkroom > scroll down to increase mask parameters](../../../../preferences-settings/darkroom.md).

---

**Note:** When used in shape creation mode, the preceding scroll operations will also cause the _default_ feathering or opacity to be changed. The new default values will be used the next time you create a new shape.

---

## Editing shapes

Click the 'show and edit mask elements' icon to display the current shapes on the canvas and edit them. <kbd>Ctrl</kbd>+click the same icon to enter _restricted_ edit mode, in which the overall position and size of a shape are locked and only its individual nodes and segments can be adjusted. This is particularly useful to avoid costly mistakes when editing path and brush shapes.

Click and drag a shape to move it around the image canvas. Clicking on a shape will also select that shape in the mask manager.

## Removing shapes

While in edit mode right-click on a shape to remove it.

## Reusing shapes

You can reuse shapes that you have drawn in other modules. Click on the shapes drop-down (next to the 'show and edit mask elements' button) to choose previously-drawn shapes individually or to use the same group of shapes as used by another module. The following options are available for selection:

add existing shape
: Choose either an individual shape or a group of shapes that you've drawn previously (either within the [mask manager](../../../toolboxes/mask-manager.md) or from within the drawn mask of another module). If that shape or group is used elsewhere, any changes you make will be reflected everywhere the shape or group is used.

use same shapes as
: Add a list of shapes used in another module to the current module's mask. This differs from the previous option in that it creates a new group of shapes, allowing shapes to be added to or removed from the group independently of the module from which they were copied. All shapes that are common to both groups remain linked.

## Combining and managing shapes

The [mask manager](../../../toolboxes/mask-manager.md) module can be used to manage your drawn shapes. This module also allows you to group and combine drawn masks using set operators (union, intersection, difference, exclusion).

## Shape distortions

In order to ensure a consistent co-ordinate system, when you place a shape on the image, it is actually drawn on the original RAW file. This shape then passes up through the pixelpipe before finally being used by the module and drawn on the screen. This means that, if you have any enabled any distorting modules (such as lens correction), drawn shapes may appear distorted on the screen and in the final image. This can lead, for example, to circles being rendered as ellipses and gradient lines becoming curved. If you need to create a more accurate shape (to overcome these distortions) it is recommended that you avoid using the simple shapes (circles / ellipses) in favor of the path shape (which can be drawn using more points, reducing distortions). You can adjust the curve on gradient lines to overcome the simple distortions introduced by lens correction.

## Available shapes

circle
: Click on the image canvas to place the circle. Scroll while hovering over the circle to change its diameter. Scroll while hovering over the circle's border to change the width of the feathering (the same effect as holding Shift while scrolling with the mouse wheel within the main shape).

ellipse
: The general principle is the same as for the circle shape. In addition, four nodes are shown on the ellipse line. Click and drag the nodes to adjust the ellipse's eccentricity. Ctrl+click and drag the nodes or use Shift+Ctrl+scroll (with the mouse wheel) to rotate the ellipse. Shift+click within the shape to toggle the gradual decay between equidistant and proportional mode.

path
: Click on the image canvas to place three or more nodes and generate a free-format enclosed shape. Terminate the path by right-clicking after having set the last point. By default, nodes are connected with smooth lines. If you want a node to instead define a sharp corner, you can do so by creating it with Ctrl+click.

: In edit mode Ctrl+click on an existing node to convert it from smooth to sharp corners and vice versa. Ctrl+click on one of the line segments to insert an additional node. Right-click on a node to delete it. Take care to ensure that the mouse pointer is over the desired node and the node is highlighted, to avoid accidentally removing the whole path.

: The size of the completed shape can be modified by scrolling. The same holds true for the width of the border (the area with a gradual opacity decay), which can also be changed with Shift+scroll (with the mouse wheel) from anywhere within the shape. Single nodes as well as path segments can be moved by dragging them with the mouse. If a node is selected by clicking on it, a further control point appears which allows you to modify the curvature of the line (reset to default by right-clicking). Dragging one of the control points on the border adjusts the border width just in that part of the shape.

: Consider fine-tuning paths in restricted edit mode (enabled by Ctrl+clicking on the 'show and edit mask elements' icon). This allows you to adjust single nodes and segments without the risk of accidentally shifting or resizing the whole shape.

brush
: Start drawing a brush stroke by left-clicking on the image canvas and moving the mouse while keeping the button pressed. The brush stroke is finalized once you release the mouse button. Scroll the mouse to change the shape size and Shift+scroll to change the feathering (hardness), either before you start drawing or at any time during the operation. Likewise you can use the "`{`" and "`}`" keys to decrease/increase hardness, and the "`<`" and "`>`" keys to decrease/increase opacity.

: If you have a graphics tablet with pen pressure sensitivity, Ansel can apply the recorded pen pressure to certain attributes of the brush stroke. This operation can be controlled in [preferences > darkroom > pen pressure control for brush masks](../../../../preferences-settings/darkroom.md).

: On lifting the tablet pen or releasing the left mouse button the brush stroke is converted into a number of connected nodes, which define the final shape. A configuration option ([preferences > darkroom > smoothing of brush strokes](../../../../preferences-settings/darkroom.md)) controls how much smoothing is applied. A higher level of smoothing leads to fewer nodes being created – this eases subsequent editing at the expense of lower accuracy.

: Nodes and segments of a brush stroke can be modified individually. See the documentation on path shapes (above) for more details. Change the size or hardness of a node by scrolling and Shift+scrolling over a node, respectively.

---

**Note:** Rendering a complex brush shape can consume a significant number of CPU cycles. Consider using the circle, ellipse or path shapes instead where possible.

---

gradient
: The gradient shape is a linear gradient which extends from a given point to the edge of the image.

: Click on the image canvas to define the position of the line that defines 50% opacity. Dotted lines indicate the distance beyond which the opacity is 100% and 0%. Between these dotted lines the opacity changes linearly.

: The line has two anchor nodes which you can drag to change the rotation of the gradient. You can also set the rotation angle when placing the gradient shape by clicking and dragging to place the shape.

: Gradient lines can also be curved by scrolling with your mouse while hovering close to the center line. This can be useful to counteract the distortion caused by the [_lens correction_](../../modules/lens-correction.md) module.

: Depending on the module and the underlying image, using a gradient shape might provoke banding artifacts. You should consider activating the [_dithering_](../../modules/dithering.md) module to alleviate this.

## Reversing the polarity of a drawn mask

Click on the "`+/-`" button to reverse the polarity of the entire drawn mask. For example, a circular mask will, by default, cause the module to be applied only to the area inside the drawn circle. Reversing its polarity will cause the module to apply to the whole image, _except for_ that circle.

## Panning and zooming the image

While creating or editing a shape, mouse actions are applied to the current shape.  If you need to move or zoom the portion of the image shown in the center view, hold down the 'a' key while dragging the mouse or using the scroll wheel.  While the key is held down, the mouse actions will apply to the entire image rather than the current shape.
---

# General : https://ansel.photos/en/doc/preferences-settings/general/

Control the overall look and feel of Ansel.

interface language
: Set the language of the user interface. The system default is marked with an * (needs a restart)

theme
: Set the theme for the user interface. Aside from any aesthetic considerations, the recommended interface color for color evaluation is middle gray. Visual perception is affected by ambient brightness, and a low user interface brightness causes all kinds of illusions. Using a dark interface to retouch photos can therefore lead to excessive retouching (abuse of contrast and saturation) and to a photo that is too dark when printed. It is therefore highly recommended that you use one of the "grey" themes for retouching work as these are designed so that the user interface approximates middle gray (default "Ansel").

use system font size
: Select this option to use the font size defined by your system. If unchecked, you may enter a custom font size in the box below (default on).

font size in points
: If the "use system font size" option is switched off, enter a font size (in points) for Ansel to use. The font size will be changed immediately.

GUI controls and text DPI
: Adjust the global GUI resolution to rescale controls, buttons, labels, etc. Increase for a magnified GUI, decrease to fit more content in the window. Set to -1 to use the system-defined global resolution. The default is 96 DPI on most systems. (needs a restart)

## CSS theme modifications

In addition to selecting a pre-built theme you can also apply additional CSS customisations of your own to tweak the look-and-feel of Ansel.

Two different methods are provided for this:

create a custom theme
: If you wish to make a large number of changes to Ansel's UI you may wish to create your own theme (in a `.css` file) and place it in `$HOME/.config/ansel/themes` (or `C:\%LOCALAPPDATA%\ansel\themes` on Windows). Your new theme will automatically appear in the _theme_ selection list the next time you restart Ansel.

: Please note that the structure of Ansel's internal CSS changes frequently and you may need to make significant changes to your own themes when new versions of Ansel are released. For this reason (among others) we do not recommend creating complex custom themes unless you are willing to devote a lot of time to ongoing maintenance. If your theme loads any of Ansel's pre-built themes using the `@import url` directive, note that your CSS theme file may not be portable between installations (`@import url` uses relative paths and the location of the pre-built themes is system-dependent).

create theme tweaks
: A text box is provided at the bottom of the general tab within which you can enter your own CSS tweaks. When using this option, Ansel will first load your selected theme (the "base" theme, chosen in the _theme_ drop-down) and then apply your custom CSS on top. This means that you can easily make minor alterations to the look-and-feel, while still keeping mostly up-to-date with core theme changes when a new version of Ansel is released. It also means that you can usually change your base theme without affecting your custom CSS tweaks.

: When you have finished entering your CSS, click the "save CSS and apply" button. This will save your CSS to `$HOME/.config/Ansel/user.css` (or `C:\%LOCALAPPDATA%\Ansel\user.css` on Windows) and immediately apply it to the current Ansel session.

: If you notice any issues after applying your CSS, you can uncheck the "modify selected theme with CSS tweaks below" box to revert. This will immediately restore the base theme but will leave your tweaks in the editor so that you can re-edit them and try again. Simply press "save CSS and apply" again when you are ready to retry. This will automatically re-check the "modify selected theme with CSS tweaks below" checkbox and apply the new CSS.

---

**Note**: If you have any issues while using custom CSS tweaks please retry with the "modify selected theme with CSS tweaks below" option unchecked to be certain that they were not caused by any of your alterations.

---

### Understanding Ansel's themes

All of Ansel's pre-built themes are provided as CSS files in `$Ansel/share/Ansel/themes/` (where `$Ansel` is Ansel's installation directory). The default theme is `Ansel.css` and this contains the bulk of the code used to control the look-and-feel of Ansel.

If you choose to create your own custom theme file you are advised import Ansel's existing theme files using `@import url` (this directive expects relative paths) and then apply your own customisations on top. You do not need to do this when using the CSS text box in the preferences dialog -- attempting to use `@import url` in the CSS tweaks text box will not work correctly.

Themes use the same basic CSS principles as in html browsers (with some minor exceptions -- see the [Gtk documentation](https://developer-old.gnome.org/gtk3/stable/chap-css-overview.html) for details):

- The majority of the style properties are assigned to broad groups of UI elements, for example, Gtk buttons and text entry fields
- Next, related groups of Ansel-specific UI elements are given _class names_ allowing them to be styled as a group
- Finally, some unique UI elements are assigned a CSS _id_ so that they can be styled independently

You are encouraged to explore the existing theme and to make use of the [Gtk Inspector](https://wiki.gnome.org/Projects/GTK/Inspector) tool to figure out how to select the specific UI element (or class of elements) you wish to modify. Some experimentation will be required.

Please note that Ansel themes are grayscale by default in order that users not be distracted by strong colors while editing images. You are advised to retain this practice in your own themes and to keep the average shade as close to middle-gray as possible. In addition you are advised to review your custom CSS each time Ansel is updated, to ensure that changes to the application have not adversely affected your tweaks.
---

# How opencl works : https://ansel.photos/en/doc/preferences-settings/performance/opencl/how-opencl-works/

As you can imagine, the hardware architecture of GPUs can vary significantly. There are different manufacturers, and even different generations of GPUs from the same manufacturer may not be comparable. At the same time GPU manufacturers don't normally disclose all the hardware details of their products to the public. One of the consequences of this is the need to use proprietary drivers under Linux, if you want to take full advantage of your graphics card.

Fortunately an industry consortium lead by The Khronos Group has developed an open, standardized interface called OpenCL, which allows your GPU to be used as a numerical processing device. OpenCL offers a C99-like programming language with a strong focus on parallel computing. An application that wants to use OpenCL will need OpenCL source code that it hands over to a hardware-specific compiler at run-time. This way applications can use OpenCL on different GPU architectures (even at the same time). All of the hardware “secrets” are hidden in this compiler and are normally not visible to the user (or the application). The compiled OpenCL code is loaded onto your GPU and -- with certain API calls -- it is ready to perform calculations for you.
---

# Importing sidecar files generated by other applications : https://ansel.photos/en/doc/views/lighttable/digital-asset-management/sidecar-import/

 When importing an image, Ansel automatically checks if it is accompanied by a sidecar file. As well as looking for files named `<basename>.<extension>.xmp` and `<basename>_nn.<extension>.xmp` (Ansel's XMP file naming formats) Ansel also checks for the presence of a file in the form `<basename>.xmp` (the naming format for Lightroom's XMP sidecar files). Files with the latter naming format will be read by Ansel but will not be written to. Once the image has been imported, Ansel will generate an additional XMP file using its own naming convention.

At present, Ansel is able to load the following metadata from Lightroom-generated sidecar files during the import process:

- Tags (including hierarchical tags)
- Color labels
- Ratings
- GPS information

In addition, Ansel has been designed to help migrate some image operations from other specific applications. The aim here is not to make Ansel a drop-in replacement for any other software, but rather to help you to recover part of the work you have already invested into your image. It is important to understand that the import process will never give identical results to other software. The underlying processing engines are very different from application to application, and depend a lot on the individual image. In some cases, the results may be similar but often, further adjustment will be required in Ansel.

This migration happens automatically when entering the darkroom view, provided that a corresponding XMP sidecar is found.

At present, Ansel is able to handle the following development steps from Lightroom-generated XMP files (with the corresponding Ansel module in parentheses):

- Black level ([_exposure_](../../darkroom/modules/exposure.md))
- Exposure ([_exposure_](../../darkroom/modules/exposure.md))
- Vignette ([_vignetting_](../../darkroom/modules/vignetting.md))
- Clarity ([_local contrast_](../../darkroom/modules/local-contrast.md))
- HSL ([_color zones_](../../darkroom/modules/color-zones.md))
- Grain ([_grain_](../../darkroom/modules/grain.md))
---

# Lighttable : https://ansel.photos/en/doc/views/lighttable/


The lighttable view is the default view loaded when Ansel is started, and allows you to view and manage your image collection. It will noticeably allow you to:

- Cull your images, assign then ratings and ultimately decide which images are worth keeping and editing,
- Filter your images based on ratings, base folder, or any metadata supported by the software (EXIF/IPTC/XMP),
- Apply metadata, like title, description, tags, GPS coordinates, or correct EXIF date/time,
- Copy-paste editing histories between images or apply styles, individually or in batches,
- Export images editing histories to final files.

## Lighttable content

All the images known to Ansel are stored in a database, the [library](../../install/configuration.md). The lighttable shows thumbnails of a subset of this library, called a _collection_. A collection is created by filtering the images from the library using criteria, first from the [library toolbox](../toolboxes/collections.md), then from filtering/include toolbar.

Library
: The library toolbox, in the left sidebar, provides the primary querying criteria to extract a subset of images from the library database. The main criteria are :
- __Folders__ : the base folder in which the images are stored on your filesystem. They can be shown as a flat list or as a tree. Note that Ansel does not directly read the folder's content, but shows its [imported](../../getting-started/import.md) images (from its database) whose base folder matches this criterion. As such, the actual filesystem folder may contain more images than what Ansel will show, if they have not been imported.
- __Collections__ : allows to query all images that have a certain tag assigned. This is similar to what other software call "collections" or "virtual folders", except that, in Ansel, you don't add an image to a virtual folder, you assign a tag to an image, and then query all images having that tag.
- __Queries__ : enables more advanced querying rules, using metadata and boolean combinations of criteria (or/and/and not). Note that _folders_ and _collections_ (aka _tags_) only provide a simplified interface for the most common fields that can be found in the _queries_ tab. Available metadata to filter can be:
    - __gear__ : lens name, camera, focal length,
    - __exposure settings__ : shutterspeed, ISO, aperture,
    - __date and time__ : of the shot, of last modification, last export, last print, etc.
    - __user set data__ : title, description, copyright, creator, publisher, etc.
    - __internal technical tags__ : file format, local copies, etc.

Include
: The _include_ toolbar, in the second top row, allows to quickly filter in and out images based on their (_in order of appearance_):
- __rating__ :
    - Unrated (0 star) : the barred star icon,
    - _rejected_ state : the circled cross icon (same as on the thumbnails),
    - 1 to 5 stars : the next 5 star icons. Those are positional, meaning :
        - Selecting the third starred-button will filter in all images having 3 stars,
        - To select images rated at least 3 stars, you will need to select the third, fourth and fifth stars, which can be quickly achieved with <kbd>Ctrl</kbd>+<kbd>left click</kbd> on the third star.
- __color labels__ :
    - Unlabled images : the white disk,
    - Color-labeled images : colored disks,
- __edited state__ :
    - Images having no editing history : the horizontally-barred circle,
    - Images having an editing history : the yin-yang +/- circle (same as on the thumbnails),
- __selection__ : restricting the collection to any arbitrary user-defined selection. This replaces Darktable's _culling_ view.
- __text search__ : restricting the collection to all images having at least one textual property matching the search query (filename, folder, title, etc.).
: The controls from the _include_ toolbar are toggle buttons, meaning when they are selected, images matching those criteria will be shown (filtered in/included), and when they are unselected, images matching those criteria will be hidden (filtered out/excluded). To show everything (_include everything_ or _exclude nothing_), you will therefore need to have all buttons checked, which can be quickly achieved by doing a right click on any of the buttons, then selecting _Select all filters_ in the context menu. If no button is toggled, no image is included and the lighttable grid stays empty.

To summarize, the _library_ toolbox helps filtering images based on their intrinsic properties, while the _include_ toolbar, allows to target workflow-based properties that you can set yourself to keep track of your work in progress :

- Star ratings help culling and narrowing down the set of images you will actually edit. For example :
    - 5 stars means "to edit absolutely",
    - 4 stars means "to edit if time permits",
    - 3 stars means "backup plan",
- Color labels help tracking where you are in your workflow. For example :
    - Red label means "basic/batch editing applied",
    - Yellow label means "fine-tuned/individual editing applied",
    - Green label means "exported",
    - Blue label means "printed"
    - Purple label means "sent to client".

Those are, of course, examples that you can adapt to your needs and workflow. Note that you can also use text tags to keep track of all that, but they will not show over thumbnails in the lighttable.

## Selecting and acting on images

The lighttable enforces a simple, safe interaction model:

- Any action that **writes** metadata (ratings, color labels, tags, history…) applies only to **explicitly selected** images.
- A selection is only ever made by a "hard" interaction: a **mouse click** or a **keyboard keystroke**. Hovering an image never changes a selection and never writes anything — hover events are strictly read-only.

This rules out the accidental metadata changes that a hover-driven workflow can cause. Selection is also _What You See Is What You Get_: you can only select images that are actually visible on screen.

Selecting with the mouse:

- **Click** a thumbnail to select it alone.
- **<kbd>Ctrl</kbd>+click** to add/remove a thumbnail to/from the selection.
- **<kbd>Shift</kbd>+click** to extend the selection up to the clicked thumbnail (range selection).

Selecting with the keyboard is described in [shortcuts and keyboard interaction](../../getting-started/keyboard.md#thumbtable); batch helpers (select all, invert, clear) live in the [_Selection_ menu](../global-menu.md#selection).

Double-click a thumbnail, or select it and press <kbd>Enter</kbd>, to open it in the [darkroom](../darkroom/_index.md).

## Lighttable display

The display options live in the [second top row](../_index.md#second-top-row), next to the collection filters, and in the **Display** menu. They control the size of the grid, in-image magnification and the overlays drawn over thumbnails.

### Columns (grid size)

The **Columns** spinner sets how many images are shown per row (1 to 12). Fewer columns means larger thumbnails. You can also:

- **<kbd>Ctrl</kbd>+scroll** over the grid to change the number of columns,
- press <kbd>Ctrl</kbd>+<kbd>+</kbd> / <kbd>Ctrl</kbd>+<kbd>-</kbd> to add/remove a column.

{{< note >}}
Coming from Darktable: "zoom" used to mean both the number of images per row and the magnification inside a thumbnail. These are now two distinct controls — **Columns** for the grid size, **Zoom** for the magnification.
{{< /note >}}

### Zoom (magnification)

The **Zoom** control magnifies the content _inside_ each thumbnail frame: **Fit**, **50 %**, **100 %** or **200 %**. When you zoom in:

- **drag** inside a thumbnail to pan it,
- **<kbd>Shift</kbd>+drag** to pan across **all** zoomed thumbnails at once,
- Ansel automatically pans each image toward the barycenter of its details, so faces and subjects roughly line up across pictures with different framing and aspect ratios.

This makes the lighttable a practical tool for comparing sharpness and framing across a series, replacing the dedicated culling/preview views.

### Overlays

Thumbnails can carry overlays: star rating, reject mark, color labels, group borders and metadata. From the **Display → Thumbnail overlays** menu you choose whether overlays are **always hidden** (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd>), **shown on hover**, or **always shown** (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>O</kbd>).

Two analysis overlays are available from the display toolbar:

Overlay focus zones
: Shade each thumbnail to indicate the regions in focus.

Overlay focus peaking
: Highlight the sharpest edges of each thumbnail, to quickly judge focus across a series. See [focus peaking](../toolboxes/focus-peaking.md).

### Thumbnail source and group display

Thumbnail generation and group display are also set from the **Display** menu:

- **Thumbnail source** — process the RAW, use the embedded JPEG only when the image is unedited, or always use the embedded JPEG. Changing this takes effect at runtime.
- **Collapse grouped images** and **Show group borders** (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>) — see [grouping](digital-asset-management/grouping.md).

## Navigation

Scroll the grid with the mouse wheel. From the keyboard, navigate thumbnails with the arrow keys, jump a page with <kbd>Page Up</kbd>/<kbd>Page Down</kbd>, and go to the start/end with <kbd>Home</kbd>/<kbd>End</kbd>. The full keyboard model is documented in [shortcuts and keyboard interaction](../../getting-started/keyboard.md#thumbtable).

---

# Print view layout : https://ansel.photos/en/doc/views/print/print-view-layout/

The central area displays the image layout on the paper (the white area). Some gray borders may be displayed around the image to represent the printable area (the page minus the borders) not filled by the image.

The [filmstrip](../toolboxes/filmstrip.md) below the image allows you to select more images.

### Overlays

When the mouse is over the bounding box of an image, its width and height are shown along its top and left, respectively. Margins between the bounding box and the page edge are notated next to the dotted lines extending out from each side of the bounding box. All measurements are shown in the units as chosen in the print settings module.

Images are inset along one dimension of their bounding box when they do not match the aspect ratio of the box. The overlaid margin measurements should therefore only be used to understand the layout bounds, not the actual printed size of the image.

## Left panel

[collections](../toolboxes/collections.md)
: Filter the list of images displayed in the lighttable.

[image information](../toolboxes/image-information.md)
: Display image information

## Right panel

[print settings](../toolboxes/print-settings.md)
: Adjust print settings and initiate printing.
---

# The pixelpipe & module order : https://ansel.photos/en/doc/views/darkroom/pixelpipe/the-pixelpipe-and-module-order/

The ordered sequence of [processing modules](../modules/_index.md) operating on an input file to generate an output image is known as the "pixelpipe".

The order of the pixelpipe is represented graphically by the order in which modules are presented in the user interface -- the pixelpipe starts with a RAW image at the bottom of the module list, and applies the processing modules one by one, piling up layer upon layer of processing from the bottom up, until it reaches the top of the list, where it outputs the fully processed image.

---

**Note:** The order in which processing modules are executed exactly matches the order in which the modules appear in Ansel's user interface. **Changing the order of the modules in the user interface changes how your image is processed.**

---

## Module order and workflows

The order in which modules are executed within the pixelpipe has been carefully chosen to give the best output quality. In previous versions of Ansel it was not possible to change the module order. However, there are a number of very specific use cases where the movement of some modules within the pixelpipe is advised.

The _scene-referred_ workflow attempts to perform as many operations as possible in a linear RGB color space, only compressing the tones to fit the output medium (with a non-linear tone mapping) at the end of the pixelpipe. This has the advantage of being a more physically-realistic space to do transformations than the traditional _display-referred_ workflow, which attempts to perform operations in a non-linear _perceptual_ color space. Honoring the physical realism (rather than the perceptual realism) makes it much easier to produce predictable processing algorithms with a minimum of artifacts.

The following diagram should help you to understand the difference between these workflows. The early, _scene-referred_ part of the pipe works on linear light; a non-linear tone mapping then compresses the dynamic range; the remaining, _display-referred_ part finishes the image for the screen or print:

```mermaid
flowchart LR
    A([RAW]) --> B[exposure] --> C[color calibration] --> D[color balance RGB]
    D --> TM{{"tone mapping<br/>(filmic RGB)"}}
    TM --> E[sharpening] --> F[output profile] --> G([display / export])
    classDef scene fill:#cfe,stroke:#284,color:#000;
    classDef display fill:#fed,stroke:#a52,color:#000;
    class A,B,C,D scene;
    class E,F,G display;
```

1. _Scene-referred_ modules (green) process linear data that is proportional to the amount of light collected by the camera at the scene. The dynamic range of an image in the scene-referred section of the pixelpipe is often larger than that of the display medium.

2. At some point in the pixelpipe, these pixel values are compressed by a non-linear tone mapping operation into a smaller dynamic range more suitable for display on a monitor or a print.

3. The remaining modules operate in the non-linear _display-referred_ section of the pixelpipe to produce the final output image.

## Changing module order

It remains highly recommended that users not change the order within the pixelpipe for a number of reasons:

- The sequence of modules has been selected with great care in order to give highest output quality. Changes to the sequence often worsen the result rather than improving it.
- Some processing modules simply don't make sense if they are shifted in the pixelpipe. For example, [_highlight reconstruction_](../modules/highlight-reconstruction.md) needs to be performed on raw data before [_demosaic_](../modules/demosaic.md), which itself needs to be performed before any [_input color profile_](../modules/input-color-profile.md) can be applied. For this reason it is still not possible to move some of the modules that are placed early in the pixelpipe.
- Most processing modules are designed to work within a specific color space (see the [color management](../../../color-management/_index.md) section for more details). Full flexibility would require modules to support different parallel algorithms depending on the color space they are working in, which would drastically increase complexity.

Despite the general recommendation to leave the pixelpipe order alone, it is possible to move modules within the pixelpipe from the darkroom module-order graph. Open it from the node-graph button in the darkroom toolbar, then drag and drop modules directly in the graph to a new location. This should only be done by experienced users who understand the impact this will have on the image.

The graph popup provides a scrollable left-to-right view of the pipeline, from the base image to the screen output. It shows:

- The current execution order of visible modules,
- The active color-space lifecycle across the pipeline,
- The input/output runtime descriptors for each module,
- Raster-mask dependencies between producer and consumer modules,
- The module-order presets toolbar, used to add presets, reset the order, or apply an existing preset.

Modules that cannot cross ordering fences are constrained by the same rules as the processing pipeline itself, so impossible moves are prevented in the graph.

The module order can be manually changed back to either the _v3.0_ or _legacy_ versions using the module order popup, which can also be used to define your own custom module order presets.
---

# Usage : https://ansel.photos/en/doc/views/slideshow/usage/

The slideshow view is still in an early stage of development with only a basic set of features.


```

spacebar             start and stop auto-advance mode which automatically switches
                     to the next images every five seconds by default.

ESC                  leave slideshow mode and return to lighttable view.


+ or                 increase delay between each image.
up arrow

- Or                 decrease delay between each image.
down arrow

left-click or
right arrow or       switch to the next image of the collection.
right shift-key

right-click or
left arrow or        switch to the previous image of the collection.
left shift-key

```

---

**Hint:** To take full advantage of your screen size, put Ansel into fullscreen mode by pressing F11 and hide the border-controls by pressing the B key.

---
---

# Views : https://ansel.photos/en/doc/views/

Ansel separates its functionality into several _views_, each dedicated to one task:

[Lighttable](lighttable/_index.md)
: Manage, sort, cull and tag your image collection. This is the default view, loaded at startup, and you can return to it from anywhere by pressing <kbd>Escape</kbd>.

[Darkroom](darkroom/_index.md)
: Develop a single image. Open it from the lighttable by double-clicking a thumbnail, or by selecting a picture and pressing <kbd>Enter</kbd>.

[Map](map/_index.md)
: Show geo-tagged images on a map and geo-tag new images manually. Hidden by default; enable it in the [preferences](../preferences-settings/other-views.md).

[Print](print/_index.md)
: Send images to a printer. Hidden by default; enable it in the [preferences](../preferences-settings/other-views.md). Not available on Windows, as it relies on [CUPS](http://www.cups.org/).

[Studio Capture](studio-capture/_index.md)
: Dedicated view for tethered shooting sessions.

[Slideshow](slideshow/_index.md)
: Display images full-screen as a slideshow, processing them on the fly. Hidden by default; enable it in the [preferences](../preferences-settings/other-views.md).

You switch views from the **Ateliers** menu in the top menu bar, which lists every available view and the shortcut to reach it (the current view is greyed out). The darkroom is the exception: because it needs an image to open, you enter it from a lighttable or filmstrip thumbnail, not from the menu.

## Window layout

Every view shares the same window frame: a central work area surrounded by panels. The visibility and size of each panel are remembered independently for each view.

### Header (top panel)

The header runs along the top of the window and is common to all views. From left to right it contains:

The [global menu](global-menu.md) bar
: **File · Edit · Selection · Image · Styles · Run · Display · Ateliers · Help**. This menu bar is the backbone of the interface: it gathers application-wide commands, image operations, view switching and help. Each menu has a keyboard mnemonic — press <kbd>Alt</kbd> to underline the mnemonic letters, then press one to open that menu, and navigate the open menus with the arrow keys.

_Search actions…_ button
: Opens the [global action search](../getting-started/keyboard.md#vimkey-like-global-action-search) (default <kbd>Ctrl</kbd>+<kbd>P</kbd>), from which any action can be found and triggered by name, whether or not it has a shortcut.

Message area
: On the right, a text area where some modules display hints and status messages.

Window buttons
: A _Go back to lighttable_ button (home icon), plus the minimize and close buttons for the window.

The header can be shown and hidden with <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd>.

{{< note >}}
Coming from Darktable: the view switcher, the preferences button, the context-help button and the shortcut-mapping button that used to live in the top toolbar are now reached from the global menu (_Ateliers_, _Edit → Preferences…_, _Help → Open contextual help_ and _Edit → Keyboard shortcuts…_ respectively). The separate top toolbar no longer exists.
{{< /note >}}

### Second top row

Directly below the header, the lighttable shows a single toolbar that combines the [collection filters](toolboxes/collections.md) (rating, color label, edited status, text search, _restrict to selection_) on one side and the [display options](lighttable/_index.md) (columns, zoom, overlays) on the other. Other views leave this row empty.

### Left panel

Shown and hidden with <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>L</kbd>. It hosts the view's [toolboxes](toolboxes/_index.md):

- **In lighttable**: the _Library_ collection builder and import/export tools.
- **In darkroom**: peripheral tools about the picture being edited — navigation, snapshots, color pickers, image information, mask manager, etc.

### Right panel

Shown and hidden with <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>R</kbd>. Used only in the **darkroom** (and map/print), where it holds the image-processing modules and the scopes. The lighttable has no right panel.

### Bottom panel (filmstrip)

Shown and hidden with <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd>. Available in the **darkroom**, **map** and **print** views, where it displays the [filmstrip](toolboxes/filmstrip.md): a horizontal strip of the current collection's thumbnails for quick navigation without leaving the view. The lighttable has no filmstrip, since it already shows the whole collection.

### Resizing and global layout

- Drag the inner border of the left, right or filmstrip panels to resize them.
- <kbd>Shift</kbd>+<kbd>F11</kbd> toggles the visibility of all panels at once, expanding the work area to fill the window.
- <kbd>F11</kbd> toggles fullscreen mode.

All these layout commands are also available from the **Display** menu, under _Panels_ and _Full screen_.

## Display menu

The **Display** menu collects the view-level display settings that previously lived in scattered toolbars:

- _Panels_ — individually toggle the top, left, right and filmstrip panels.
- _Thumbnail overlays_ — choose whether thumbnail badges (rating, labels, metadata) are _always hidden_, _shown on hover_ or _always shown_.
- _Thumbnail source_ — choose how thumbnails are generated: _always process the RAW_, _use the embedded JPEG if the image is unedited_, or _always use the embedded JPEG_. This can be changed at runtime.
- _Collapse grouped images_ and _Show group borders_ — control how [grouped images](lighttable/digital-asset-management/grouping.md) are displayed.
- _Monitor color profile_ / _Monitor color intent_ — the display [color management](../color-management/_index.md) settings.
- _Full screen_.

See the [global menu reference](global-menu.md) for the complete list of menus and their entries.
---

# Workflows : https://ansel.photos/en/workflows/
---

# Install on Mac OS : https://ansel.photos/en/doc/install/macos/

## Build from source (Homebrew, recommended)

This method is derived from the repository scripts in `packaging/macosx/`.

### Prerequisites

- Install Xcode Command Line Tools:
  ```bash
  xcode-select --install
  ```
- Install Homebrew from https://brew.sh/ (use the default prefix: `/opt/homebrew` on Apple Silicon, `/usr/local` on Intel).

### Steps

1. Clone the repository and submodules:
   ```bash
   git clone --depth 1 https://github.com/aurelienpierreeng/ansel.git
   cd ansel
   git submodule init
   git submodule update
   ```
2. Install build dependencies:
   ```bash
   ./packaging/macosx/1_install_hb_dependencies.sh
   ```
   This is the canonical dependency list used by CI and packaging.
3. Build and install into the local `install/` directory:
   ```bash
   ./packaging/macosx/2_build_hb_ansel_custom.sh
   ```
   Edit `packaging/macosx/2_build_hb_ansel_custom.sh` if you need custom CMake options.
4. (Optional) Create a macOS application bundle (can be signed if you have a Developer ID):
   ```bash
   export CODECERT="your.developer@apple.id"   # optional
   ./packaging/macosx/3_make_hb_ansel_package.sh
   ```
   The bundle is generated at `install/package/Ansel.app`.
5. (Optional) Generate a DMG image from the bundle:
   ```bash
   ./packaging/macosx/4_make_hb_ansel_dmg.sh
   ```
   The output is `Ansel-<version>-<arch>.dmg` in the `install/` directory.

### Running the build

- Development install (after step 3):
  ```bash
  ./install/bin/ansel
  ```
- From the app bundle (after step 4):
  ```bash
  ./install/package/Ansel.app/Contents/MacOS/ansel --configdir ~/.config/ansel --cachedir ~/.cache/ansel/
  ```

### Notes and limitations

- The DMG is not notarized. If Gatekeeper blocks it, remove the quarantine attribute:
  ```bash
  xattr -d com.apple.quarantine Ansel-*.dmg
  ```
- The DMG is only guaranteed to run on the same macOS version it was built on.
- Homebrew package versions define the library versions bundled into the app.

## MacPorts method (legacy, Intel-only)

A legacy Intel-only method using MacPorts exists in `packaging/macosx/BUILD.txt`. It includes custom patches for `exiv2`, `gnutls`, and `gtk-osx-application`, and uses `gtk-mac-bundler` to produce a DMG. Use this only if you specifically need a MacPorts-based build.
---

# Activating opencl in Ansel : https://ansel.photos/en/doc/preferences-settings/performance/opencl/activate-opencl/

Using OpenCL in Ansel requires that your PC is equipped with a suitable graphics card and that it has the required libraries in place. Most modern graphics cards from NVIDIA and AMD come with full OpenCL support. The OpenCL compiler is normally shipped as part of the proprietary graphics driver and is used as a dynamic library called `libOpenCL.so`. This library must be in a folder where it can be found by your system's dynamic linker.

When Ansel starts, it will first try to find and load `libOpenCL.so` and, on success, check if the available graphics card comes with OpenCL support. A sufficient amount of graphics memory (1GB+) needs to be available for Ansel to take advantage of the GPU. If that check passes, Ansel tries to setup its OpenCL environment: a processing context needs to be initialized, a calculation pipeline to be started, OpenCL source code files (extension is `.cl`) needs to be read and compiled and the included routines (OpenCL kernels) need to be prepared for Ansel's modules. If all of that completes successfully, the preparation is complete.

By default, OpenCL support is activated in Ansel if all the above steps were successful. If you want to de-activate it you can do so in [Preferences > Processing > CPU, GPU, Memory](../../processing#cpu-gpu-memory). This configuration parameter is grayed out if the OpenCL initialization failed.

You can switch OpenCL support off and on at any time without requiring a restart. Depending on the type of modules you are using, you will notice the effect as a general speed-up during interactive work and export. Most modules in Ansel can take advantage of OpenCL but not all modules are demanding enough to make a noticeable difference. In order to feel a real difference, use modules like [_diffuse or sharpen_](../../../views/darkroom/modules/diffuse.md), and [_denoise (profiled)_](../../../views/darkroom/modules/denoise-profiled.md).

If you are interested in profiling statistics, you can start Ansel with command line parameters `-d opencl -d perf`. After each run of the pixelpipe you will be shown details of processing time for each module plus an even more fine-grained profile for all used OpenCL kernels.

Apart from the speed-up you should not see any difference in the results between CPU and GPU processing. Except for some rounding errors, the results are designed to be identical. If, for some reason, Ansel fails to properly finish a GPU calculation, it will normally detect the failure and automatically (and transparently) fall back to CPU processing.
---

# Ansel-generate-cache : https://ansel.photos/en/doc/cli/ansel-generate-cache/

The `ansel-generate-cache` binary updates Ansel's thumbnail cache. Invoke this program to generate all missing thumbnails in the background when your computer is idle.

`ansel-generate-cache` can be called with the following command line parameters:

```
ansel-generate-cache
              [-h, --help]
              [--version]
              [--min-mip <0-8>] [-m, --max-mip <0-8>]
              [--min-imgid <N>] [--max-imgid <N>]
              [--core <Ansel options>]
```

All parameters are optional. If started without parameters `ansel-generate-cache` uses reasonable defaults.

`-h, --help`
: Display usage information and terminate.

`--version`
: Display copyright and version information and terminate.

`--min-mip <0-8>, -m, --max-mip <0-8>`
: Ansel can store thumbnails with up to eight different resolution steps for each image. These parameters define the maximum resolution to be generated (defaults to a range of 0-2). There is normally no need to generate all possible resolutions here -- missing ones will be automatically generated by Ansel the moment they are needed. When asked to generate multiple resolutions at once, the lower-resolution images are quickly downsampled from the highest-resolution image.

`--min-imgid <N>, --max-imgid <N>`
: Specifies the range of internal image IDs from the database to work on. If no range is given, `ansel-generate-cache` will process all images.

`--core <Ansel options>`
: All command line parameters following `--core` are passed to the Ansel core and handled as standard parameters. See the [`Ansel binary`](./Ansel.md) section for a detailed description.
---

# Darkroom : https://ansel.photos/en/doc/views/darkroom/

The darkroom view is where you develop a single image. The center area shows the picture currently being edited; the [left panel](darkroom-view-layout.md#left-panel) holds peripheral tools (navigation, scopes, snapshots, history…), and the [right panel](darkroom-view-layout.md#right-panel) holds the image-processing modules.

Open the darkroom from the [lighttable](../lighttable/_index.md) by double-clicking a thumbnail, or by selecting an image and pressing <kbd>Enter</kbd>. Return to the lighttable with <kbd>Escape</kbd> (or the home button in the header). You can switch to another image without leaving the darkroom by enabling the [filmstrip](../toolboxes/filmstrip.md) (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd>) and clicking a thumbnail in it.

How an image is processed is the subject of the [pixelpipe](pixelpipe/_index.md) and [modules](modules/_index.md) sections; this page and the [darkroom layout](darkroom-view-layout.md) page cover the view itself.

## Zoom and pan

Middle-click the center area to cycle between **fit to screen**, **1:1** and **2:1**.

Scroll with the mouse wheel to zoom between fit-to-screen and 1:1. Hold <kbd>Ctrl</kbd> while scrolling to extend the range from 2:1 up to 1:10. When zoomed in past the window, drag the image to pan.

## Working with modules

The image-processing modules in the right panel are organized into [workflow tabs](darkroom-view-layout.md#module-workflow-tabs) that follow the order of the pixelpipe. The recommended way to edit is to move through the tabs from left to right, and through each module stack from bottom to top.

Modules and their controls are fully navigable from the keyboard, and any module or control can be reached directly through the [global action search](../../getting-started/keyboard.md#vimkey-like-global-action-search) (<kbd>Ctrl</kbd>+<kbd>P</kbd>) or an [assigned shortcut](../../getting-started/keyboard.md#darkroom). The [anatomy of a module](pixelpipe/the-anatomy-of-a-module.md) page explains the common module controls (enable, reset, presets, multiple instances, masking & blending).

## Autoset: auto-developing an image

Some modules can compute their own settings from the content of the image instead of relying on a fixed default. The **autoset** button in the [bottom toolbar](darkroom-view-layout.md#bottom-panel) runs this automatic computation on several modules at once, giving you a sensible starting point that you then refine by hand.

**Right-click** the autoset button to open a list of the modules that support it, and tick the ones you want it to act on. Capable modules include:

- [Raw black/white point](modules/raw-black-white-point.md) and [exposure](modules/exposure.md) — set the working range and overall brightness;
- [Highlight reconstruction](modules/highlight-reconstruction.md) — adapt to the clipped channels;
- [Color calibration](modules/color-calibration.md) — automatic white balance / illuminant detection;
- [Filmic RGB](modules/filmic.md) — fit the tone mapping to the image's dynamic range;
- [Tone equalizer](modules/tone-equalizer.md), [color balance RGB](modules/color-balance.md), [color equalizer](modules/color-equalizer.md), [color primaries](modules/color-primaries.md) and [denoise (profiled)](modules/denoise-profiled.md).

```mermaid
flowchart LR
    A([Click autoset]) --> B{For each<br/>ticked module}
    B --> C[Module analyzes<br/>the image]
    C --> D[Module writes<br/>its own settings]
    D --> E([Refine by hand])
```

Autoset runs in the background and processes the modules one after another (the button shows a busy state while it works). Each module's result is recorded in the [history](pixelpipe/history-stack.md) like any other change, so you can undo it or tweak it afterwards.

## On-image overlays and assessment

The [bottom toolbar](darkroom-view-layout.md#bottom-panel) gives quick access to the visual assessment overlays: ISO 12646 [color assessment](../toolboxes/color-assessment.md), [raw overexposed](../toolboxes/raw-overexposed.md) and [clipping](../toolboxes/clipping.md) warnings, [soft-proofing](../toolboxes/soft-proof.md), [gamut checking](../toolboxes/gamut.md), and [guides & overlays](../toolboxes/guides-overlays.md). The quick-access [styles](../toolboxes/styles.md) menu is also there.
---

# Documentation : https://ansel.photos/en/doc/


This documentation is derived from [Darktable 4.0 user manual](https://github.com/darktable-org/dtdocs.git) and further developed under the [Ansel project](https://github.com/aurelienpierreeng/ansel-doc), where comments, suggestions and corrections can be made. It is released under the [GPL 3.0](https://www.gnu.org/licenses/gpl-3.0.en.html) license.

[Darkroom modules](./views/darkroom/_index.md) that have been deprecated in either Ansel or darktable 4.0 are not documented here and can be found on [darktable user manual](https://docs.darktable.org).

{{< note >}}
"Deprecated module" means the GUI widget of the module is hidden from the software interface as to decourage users from using it in new edits. The pixel processing filters for deprecated modules are __still__ in the software and the GUI will show the widgets of the deprecated modules for any old edit using them. This ensures compatibility of old edits since Darktable 1.0 at least.
{{</ note >}}

The goal of this documentation is to explain the effect and use cases of the features and settings existing in the software. The opposite approach, explaining what features are useful to achieve a certain goal, is provided in the [workflows](../workflows/_index.md) section.

Users may subscribe to the [RSS feed of the documentation](./index.xml) to follow new pages and fresh updates of old pages.
---

# Local copies : https://ansel.photos/en/doc/views/lighttable/digital-asset-management/local-copies/

Many users have huge image collections stored on extra hard drives in their desktop computer, or on an external storage medium (RAID NAS, external hard drives etc.).

It is a common requirement to develop a number of images while travelling using a laptop and then later synchronize them back to the original storage medium. However, copying images manually from the main storage to the laptop and back is cumbersome and prone to errors. The “local copies” feature of Ansel has been designed to directly support these use cases.

Create local copies of the selected images from the global menu _File_ 🡒 _Copy distant images locally_. Local copies are always used when present, giving continued access to images even if the external storage is no longer connected. Later, when your primary storage medium has been reconnected, re-synchronize the XMP sidecar files back to it (and delete the local copies) with _File_ 🡒 _Resynchronize distant images_.

For safety reasons, if local copies exist and the external storage is available, the local XMP sidecars are automatically synchronized at start up.

Local copies are stored within the `$HOME/.cache/ansel` directory and named `img-<SIGNATURE>.<EXT>` (where `SIGNATURE` is a hash signature (MD5) of the full pathname, and `EXT` is the original filename extension).

Local copies can be identified in the lighttable view by a white marker on the top right of the thumbnail. In addition, all local copies carry the `darktable|local-copy` tag to allow them to be easily selected.
---

# Masks : https://ansel.photos/en/doc/views/darkroom/masking-and-blending/masks/

Masks allow you to limit the effect of a module so that it only applies to certain parts of the image.

A mask can be regarded as a grayscale image where each pixel has a value between 0 and 1.0 (or between 0% and 100%). This value is the _opacity_ and is used to determine how much a module affects each pixel.

The following sections explain how to construct masks in Ansel.
---

# Opencl : https://ansel.photos/en/doc/preferences-settings/performance/opencl/
---

# Parametric masks : https://ansel.photos/en/doc/views/darkroom/masking-and-blending/masks/parametric/

The parametric mask feature offers fine-grained selective control over how individual pixels are masked. It does this by automatically generating an intermediate blend mask from user-defined parameters. These parameters are color coordinates rather than the geometrical coordinates used in drawn masks.

For each data channel of a module (e.g. Lab, RGB) and several virtual data channels (e.g. hue, saturation) you  can construct a per-channel opacity function. Depending on each pixel's value for a given data channel this function calculates a blending factor between 0 and 1 (100%) for that pixel.

Each pixel of an image thus has different blending factors for each of its data channels. All blending factors are finally multiplied together (pixel-by-pixel), along with the value of the global opacity slider, to form a complete parametric blend mask for the image.

If the blend mask has a value of 0 for a given pixel, the input of the module is left unchanged. If the blend mask has a value of 1 (100%) for a pixel, the module has its full effect.

## Channel tabs

Click on one of the channel tabs to select a data channel to use to build your mask.

Modules acting in (_display-referred_) _Lab_ color space have data channels for L, a, b, C (chroma of LCh) and h (hue of LCh).

Modules acting in _display-referred RGB_ color space have data channels for g (gray), R, G, B, H (hue of HSL), S (saturation of HSL), and L (lightness of HSL).

Modules acting in _scene-referred RGB_ color space have data channels for g (gray), R, G, B, Jz (luminance component of JzCzhz), Cz (chroma, or saturation, of JzCzhz), and hz (hue of JzCzhz). The g (gray) value is calculated as a weighted average of the R, G & B channels, the exact weightings depending on the working color space being used. The JzCzhz color space is a polar representation of the Jzazbz color space, in the same way that LCh is a polar representation of the Lab space. Like the L in Lab color space, the Jz is a representation of the luminosity of a pixel that aligns with how we perceive brightness. However, the Jzazbz color space is much better for high dynamic range images and is less susceptible to hue shifts than Lab space.

_See [Wikipedia](https://en.wikipedia.org/wiki/Color_space) for more details about these color spaces._

Two sliders can be shown for each associated data channel: one that works on the _input data_ that the module receives and one that works on the _output data_ that the module produces prior to blending. The sliders for the output data channels are hidden by default and can be shown using the _show output channels_ option in the blending menu.

The _boost factor_ slider allows the range of values targeted by the parametric mask sliders to be extended. It may be used in scene referred editing, where luminance values may extend beyond 100%, to target highlights. This slider is only available for channels where it is meaningful.

### Inspecting data channels & masks

Press the letter C while hovering over a channel's input/output slider to view the input/output image data for that color channel. The center image changes to display that color channel either in gray-scale values or in false colors depending on the setting in [preferences > darkroom > display of individual color channels](../../../../preferences-settings/darkroom.md).

Press the letter M to see the resulting mask for that slider overlaid on the image.

When the mouse pointer leaves the slider the image returns to normal after a short delay.

### Linear / log mode

Press the letter A while hovering over a slider to change its display to 'log' mode. This provides more fine control in the shadows. Press A again to toggle back to 'linear' mode.

## Channel input/output sliders

With each color channel slider you can construct a trapezoidal opacity function. For this purpose there are four markers per slider. Two filled triangles above the slider mark the range of values where opacity is 1. Two open triangles below the slider mark the range values where opacity is 0. Intermediate points between full and zero are given a proportional opacity.

The filled triangles, or inside markers, indicate the closed (mostly narrower) edge of the trapezoidal function. The open triangles, or outside markers, indicate the open (mostly wider) edge of the trapezoidal function. The sequence of the markers always remains unchanged: they can touch one another but they cannot switch position.

A polarity (+/-) button to the right of each the slider switches between "range select" and "range de-select" modes, with visual confirmation provided by exchanging the upper and lower triangle markers. These two types of trapezoidal functions are represented graphically in the following images.

**range select**
: With the inside (filled) markers at the extremes, the whole range of values is selected (an "all at 100%" mask). Moving the markers inwards gradually excludes more of the image, keeping only the remaining narrow range. The outside (open) markers set how soft the transition is at each edge.

: ![range select slider](blendif_2a.jpg)
: ![range select opacity function](blendif_2b.jpg)

**range deselect**
: Toggling the polarity swaps the markers: by default the whole range is deselected (an "all-zero" mask), and moving the markers inwards gradually includes more of the image around the remaining range.

: ![range deselect slider](blendif_3a.jpg)
: ![range deselect opacity function](blendif_3b.jpg)

## Color pickers

With the left-hand color picker button you can select a point or area probe from your image. The corresponding values for the real and virtual data channels are then displayed within each color channel slider.

With the right-hand color picker button you can automatically set the slider's values based on the selected range. Click and drag to set the parameters for the input slider from the drawn rectangle; Ctrl+click and drag to set the parameters for the output slider.

## Invert

Click the invert button above the sliders to invert the polarity of the entire parametric mask. This differs from the polarity buttons beside the individual sliders which just invert the parameters for the current slider/channel.

## Reset

Click the reset button above the sliders to revert all parametric mask parameters to their default state.
---

# Rendering method : https://ansel.photos/en/doc/color-management/rendering-method/

Ansel can render colors either with its own internal algorithms or by using the external library LittleCMS2. Ansel's internal method is, by an order of magnitude, faster than the external one. The external option gives you a choice of the rendering intent and might offer a slightly higher accuracy in some cases.

You can change the default method in [preferences > processing > always use LittleCMS 2 to apply output color profile](../preferences-settings/processing.md)

---

**Note:** If the given ICC is LUT-based or contains both, a LUT and a matrix, Ansel will use LittleCMS2 to render the colors regardless of the configuration parameter's value.

---
---

# The history stack : https://ansel.photos/en/doc/views/darkroom/pixelpipe/history-stack/

The _history stack_ stores the entire editing history for a given image, in the order in which those edits were applied. It is saved to Ansel's library database and the image's XMP sidecar file and persists between editing sessions.

Each time a processing module is enabled, disabled, moved or amended a new entry is added to the top of the _history stack_.

The history stack can be queried and modified within the [history stack](../../toolboxes/history-stack.md) module in the darkroom.

---

**Note:** The history stack is not a representation of the order in which the modules are **executed** but the order in which they were **amended**. The execution order is represented by the order of the modules in the right-hand panel.

---
---

# The pixelpipe : https://ansel.photos/en/doc/views/darkroom/pixelpipe/

The **pixelpipe** is the ordered sequence of [processing modules](../modules/_index.md) that turns your original RAW file into the image you see and export. Each module takes the output of the previous one as its input, applies one operation, and hands its result to the next module — exactly like a stack of adjustment layers, applied from the bottom up.

```mermaid
flowchart LR
    RAW([RAW file]) --> M1[module 1] --> M2[module 2] --> M3[module n] --> OUT([output image])
```

Nothing is ever written back to your original file: the pixelpipe is re-run on demand from the RAW plus your [history stack](history-stack.md), both for the on-screen preview and for the final [export](../../toolboxes/export.md).

This section explains the concepts behind that process:

- [The anatomy of a processing module](the-anatomy-of-a-module.md) — what a single module is and the controls it shares with every other module.
- [The pixelpipe & module order](the-pixelpipe-and-module-order.md) — why the order matters, the scene-referred workflow, and how to inspect or change the order.
- [The history stack](history-stack.md) — how your edits are recorded.
- [Undo and redo](undo-redo.md).

## Why Ansel stays responsive

You do not need to understand the internals to use the darkroom, but one design choice is worth knowing because it shapes how the darkroom feels: **Ansel caches the output of every module and only recomputes what actually changed.**

Each module's output is stored under a fingerprint of everything it depends on (its parameters, its mask, the image region, the modules before it). When you change a setting, only that module and the ones **after** it in the pipe are recomputed — everything before it is reused from the cache.

```mermaid
flowchart LR
    A[module 1] --> B[module 2] --> C["module 3 ✏️<br/>(changed)"] --> D[module 4] --> E[module 5]
    classDef cached fill:#2d6,stroke:#161,color:#000;
    classDef recomp fill:#f95,stroke:#a40,color:#000;
    class A,B cached;
    class C,D,E recomp;
```

In the diagram above, editing module 3 only recomputes modules 3 to 5 (orange); modules 1 and 2 (green) are served instantly from the cache. The same logic means that toggling a module on/off, using a [color picker](../../toolboxes/scopes.md), or re-exporting an image you already exported does not rebuild the whole pipeline. In practice this makes Ansel several times to several **dozens** of times faster than recomputing everything, which is what makes a heavy edit still feel interactive.
---

# Thumbnails : https://ansel.photos/en/doc/views/lighttable/digital-asset-management/thumbnails/

Each image in the current collection is represented by a thumbnail in the lighttable view and filmstrip module. A cache of the most recently used thumbnails is stored in a file on disk and loaded into memory at startup. The size of this cache can be adjusted in [Preferences > Processing > CPU, GPU, Memory](../../../preferences-settings/processing.md#cpu-gpu-memory).

## Thumbnail creation

A thumbnail is created when an image is imported into Ansel for the first time, after an image has been modified in the darkroom, or when revisiting an image whose thumbnail is no longer available.

When an image is imported for the first time Ansel can either try to extract an embedded thumbnail from the input image (most raw files contain these, usually in JPEG format) or process the raw image itself using default settings. Choose how Ansel obtains its thumbnails from the global menu _Display_ 🡒 _Thumbnail source_ — _always process the RAW_, _use embedded JPG if unedited_, or _always use embedded JPG_. This can be changed at runtime.

Extracting an embedded thumbnail from the input image is usually very fast. However, these thumbnails have been generated by the raw converter of the camera and do not represent Ansel's “view” of that image. You will notice the difference as soon as you open the image in the darkroom mode, at which point Ansel replaces the thumbnail with its own internally processed version.

After import Ansel automatically generates thumbnails for new images as they are needed. When importing a large set of new images, thumbnail generation can slow down navigation in the lighttable view. Alternatively you may terminate Ansel and generate the thumbnail cache separately by running [`ansel-generate-cache`](../../../cli/ansel-generate-cache.md). This program will generate all missing thumbnails in one go.

As the thumbnail cache has a pre-defined maximum size it will eventually get filled up. If new thumbnails are subsequently added, old thumbnails are dropped from the cache. However, Ansel will keep all thumbnails on disk if the corresponding disk backend option is activated in [Preferences > Processing > CPU, GPU, Memory](../../../preferences-settings/processing.md#cpu-gpu-memory). Access to the thumbnails in this secondary cache is slower than the primary cache, but still much faster than reprocessing thumbnails from scratch. The size of the secondary cache is limited only by the available disk space.

You can pre-render or purge cached thumbnails for the selected images at runtime from the global menu _Run_ 🡒 _Preload selected thumbnails in cache_ and _Purge selected thumbnails from cache_.

Thumbnails are never removed automatically from the secondary cache. You can manually clean it by recursively deleting all images in the `$HOME/.cache/ansel/mipmaps-xyz.d` folder (where `xyz` denotes an alphanumeric identifier of the cache). After clearing the secondary cache you can simply let Ansel re-generate thumbnails as needed, or generate them all in one go with [`ansel-generate-cache`](../../../cli/ansel-generate-cache.md).

If you choose not to activate the disk backend and select too small a cache size, Ansel may become unresponsive, you may experience continuous regeneration of thumbnails when you navigate your collection or flickering of thumbnail images. A good choice of cache size is 512MB or higher (see [Memory and performance tuning](../../../preferences-settings/performance/mem-performance.md) for more information).

All thumbnails are fully color managed. Colors are rendered accurately on screen as long as your system is properly set up to hand over the right monitor profile to Ansel. For more information see the [color management](../../../color-management/_index.md) section.

## Skulls

If for some reason Ansel is unable to generate a thumbnail, it displays an image of a skull instead. Don't panic!

![skull](skull.jpg)

There are three main reasons this could happen:

- _Missing image file_: Ansel remembers all images it has ever imported, as long as they have not been removed from your database. If Ansel wants to create a thumbnail but is not able to open the input file, a skull is displayed instead. Users are advised to remove images from the database using the selected images module before physically removing them from disk. Alternatively you may occasionally run the script [`purge_non_existing_images.sh`](../../../cli/purge_non_existing_images_sh.md) from Ansel's toolset to clean-up your database.

- _Invalid image format_: While the extension of an image may seem to be supported by Ansel, its contents could be either an unsupported image format or a corrupt file.

- _Low memory_: If Ansel runs out of memory while generating a thumbnail, it will warn you and display a skull. This can happen if Ansel is run with sub-optimal settings, especially on a 32-bit system. See [memory and performance tuning](../../../preferences-settings/performance/mem-performance.md) for more information.
---

# Undo/redo : https://ansel.photos/en/doc/views/lighttable/undo-redo/

Most changes made within the lighttable are recorded and can be reverted to a previous state. This includes modifications to color labels, ratings, geo-localization, tags, metadata, orientation, [copy/paste of history](./history-copy-paste.md), image duplication, or [application of a style](../toolboxes/styles.md). The undo/redo facility is unlimited in the number of steps while in the lighttable, but it is reset each time you switch to a different view.

Press <kbd>Ctrl</kbd>+<kbd>Z</kbd> to undo the last modification and <kbd>Ctrl</kbd>+<kbd>Y</kbd> to redo the last undone one (if any). These are also available from the global menu, under _Edit_ 🡒 _Undo_ / _Redo_.

The [darkroom](../darkroom/_index.md) has its own, independent undo/redo for image-editing actions; see the [history of changes](../toolboxes/history-stack.md).
---

# Contribute : https://ansel.photos/en/contribute/

This section is dedicated to people willing to contribute to Ansel, whether by designing, fixing or triaging bugs, coding, troubleshooting bugs, writing documentation or helping users.
---

# Memory & performance tuning : https://ansel.photos/en/doc/preferences-settings/performance/mem-performance/

## Memory requirements

Processing a raw image in Ansel requires a great deal of system memory. A simple calculation makes this clear: For a 20 megapixel image, Ansel requires a 4x32-bit floating point cell to store each pixel, meaning that each full image of this size will require approximately 300MB of memory just to store the image data. In order to actually process this image through a given module, Ansel needs at least two buffers (input and output) of this size, with more complex modules potentially requiring several additional buffers for intermediate data. Without further optimization, anything between 600MB and 3GB of memory might be required to store and process image data as the pixelpipe executes. On top of this is Ansel's code segment, the code and data of any dynamically-linked system libraries, as well as further buffers that Ansel uses to store intermediate states (cache) for quick access during interactive work.

All in all, Ansel requires _at least_ 4GB of physical RAM plus 4 to 8GB of additional swap space to run but it will perform better the more memory you have.

As well as executing on your CPU, many Ansel modules also have OpenCL implementations that can take full advantage of the parallel processing offered by your graphics card (GPU). Similarly, the more GPU memory you have, the better Ansel will perform.

### Tiling

If Ansel does not have sufficient memory to process the entire image in one go, modules may choose to use a "tiling strategy", wherein the image is split into smaller parts (tiles) which are processed independently, and then stitched back together at the end. While this allows images to be processed with a much smaller memory footprint, it does also come with some down-sides:

- Tiling is always slower -- sometimes up to 10x slower, though for some modules the difference is negligible,
- Tiling is not technically possible for some modules because of the nature of the underlying algorithms

For most systems, tiling will probably only be used for full-sized image exports, with interactive work in the darkroom being processed more efficiently. For best performance (and avoidance of tiling modes) you should run Ansel alongside as few other applications as possible and configure Ansel to use as much of your system and GPU memory as you can.

## Performance tuning

There are a number of configuration parameters that can help you to fine-tune your system's performance. Some of these parameters are available in [Preferences > Processing > CPU, GPU, Memory](../processing.md#cpu-gpu-memory) and others need to be modified directly in Ansel's configuration file (found in `$HOME/.config/Ansel/Anselrc`).

This section provides some guidance on how to adjust these settings.

### How to test

In order to determine how much your modifications improve (or not) Ansel's performance, you will need one or more sample images to test with, and a method of assessing the speed of the pixelpipe.

For sample images, you are advised to use some of the more intensive modules, such as [diffuse or sharpen](../../views/darkroom/modules/diffuse.md) or [denoise (profiled)](../../views/darkroom/modules/denoise-profiled.md). Exports are likely to have more consistent and comparable timings between pipe runs than interactive work (and will also push your hardware more).

In order to obtain profiling information you need to start Ansel from a terminal with `Ansel -d opencl -d perf`. If you want more information about tiling you should use `Ansel -d opencl -d tiling -d perf`.

Each time the pixelpipe is processed (when you change module parameters, zoom, pan, export etc.) you will see (in your terminal session) the total time spent in the pixelpipe and the time spent in each of the OpenCL kernels. The most reliable value is the total time spent the in pixelpipe and you should use this to assess your changes.

---

**Note**: The timings given for each individual module are unreliable when running the OpenCL pixelpipe asynchronously (see _asyncronous mode_ below).

---

To allow for efficient processing with OpenCL it is essential that the GPU is kept busy. Any interrupts or a stalled data flow will add to the total processing time. This is especially important for the small image buffers used during interactive work, which can be processed quickly by a fast GPU. However, even short-term stalls of the pixelpipe can easily become a bottleneck.

On the other hand Ansel's performance during file exports is more or less only governed by the speed of the algorithms and the horse-power of your GPU. Short-term stalls will not have a noticeable effect on the total time of an export.

### Ansel resources

The "Ansel resources" preference (in [Preferences > Processing > CPU, GPU, Memory](../processing.md#cpu-gpu-memory)) allows you to choose between four different approaches to allocating your system's resources to Ansel. Each of these options controls multiple individual parameters, which are defined independently in `$HOME/.config/Ansel/Anselrc`. You can amend any of these directly within your Anselrc file to tweak values for your selected resource level, though you cannot add your own custom resource level to the preferences drop-down.

Each of the four "Ansel resources" options are defined as follows:

```
resource_default=512 8 128 700
resource_large=700 16 128 900
resource_small=128 4 64 400
resource_unrestricted=16384 1024 128 900
```

More generally, these can be represented as `resource_level=a b c d` where `a` - `d` are defined as follows:

a. system memory for module processing
: The maximum amount of system memory made available for module processing. Lower values force memory-hungry modules to process images with an increasing number of tiles. This number is a fraction of the total amount of system memory, divided by 1024. For example, on a system with 16GB of total system memory the amount assigned by `resource_default` (in GB) is `16 * 512 / 1024`, or 8GB of system RAM.

b. minimum tiling buffer size
: The minimum size of a single tiling buffer, similarly expressed as a fraction of total system memory. For example, on a system with 16GB of total system memory the amount assigned by `resource_default` (in GB) is `16 * 8 / 1024`, or 0.125GB of system RAM. Note that this setting is largely historic and is no longer of much practical use -- you are advised to leave it at its default value.

c. thumbnail cache memory
: The amount of memory to use for the thumbnail cache. Again, this is expressed as a fraction of total system memory and, on a 16GB system, the amount assigned by `resource_default` is `16 * 128 / 1024`, or 2GB of system RAM.

d. OpenCL (GPU) memory
: The maximum amount of GPU memory made available for module processing. As with system memory, lower values will force memory-hungry modules to process images with an increasing number of tiles. Your GPU memory will likely also be used by other applications on your system. However, in contrast to system memory, your GPU is not able to take advantage of swap files and it can be difficult for Ansel to know exactly how much memory is available at a given time. If this parameter is set too high, Ansel could be forced to fall back to CPU processing (which will be significantly slower). For this reason, the GPU memory parameter fraction also includes an extra 400MB of headroom in an attempt to avoid over-allocation of memory. For example, on a GPU with 6GB of memory, Ansel will use approximately `(6 - 0.4) * 700 / 1024`, or 3.8GB of GPU RAM when using the `resource_default` level.

In addition to the resource levels presented in the UI the following options can be set via the command-line (e.g. `Ansel --conf resourcelevel="notebook"`). These modes are designed for debugging tiling issues and testing performance of common systems on larger development machines. The following options are provided:

- "mini" (1GB ram, 2MB single buffer, 128MB thumbnail cache, 200MB OpenCL memory)
- "notebook" (4GB ram, 32MB  single buffer, 512MB thumbnail cache, 1GB OpenCL memory)
- "reference" (8GB ram, 32MB single buffer, 512MB thumbnail cache, 2GB OpenCL memory)

### Tuning GPU memory usage

If you want to make maximal use of your GPU memory for OpenCL, you have three options:

- Choose the "large" resource level. For a 6GB card, this will use approximately 5GB of GPU memory, leaving 1GB for the rest of your system.
- Alter Anselrc to increase the last number (the OpenCL memory fraction) for your selected resource level. For example, increasing the OpenCL memory fraction to 950 would increase the available memory on a 6GB GPU to approximately 5.3GB.
- Set [preferences > processing > cpu / gpu / memory > tune OpenCL performance](../processing.md#cpu-gpu-memory) to "memory size", which will use all of your device's memory, less a 400MB headroom. Please see the [section below](#id-specific-opencl-configuration) for other options related to this setting.

### Device-specific OpenCL configuration

The default Ansel settings should deliver a reasonable GPU performance on most systems. However, if you want to try to optimize things further, this section describes the relevant configuration parameters (all of which are set in your Anselrc file).

Since darktable 4.0 most of the OpenCL-related options are managed with a "per device" strategy. The configuration parameter for each device looks like:

`cldevice_v4_quadrortx4000=0 250 0 16 16 1024 0 0 0.017853`

or, more generally

`cldevice_version_canonicalname=a b c d e f g h i`

An entry will be automatically created in Anselrc for each newly-detected device when you launch Ansel for the first time, with the correct canonical device name and version number. The parameters `a` - `i` are defined as follows and can be manually edited:

a. avoid atomics
: _1 = avoid atomics; 0 = use atomics_
: Atomic operations in OpenCL are a special method of data synchronization and are only used in a few modules. Unfortunately, some old AMD/ATI devices are extremely slow in processing atomics and, on these cards, it is better to process the affected modules on the CPU rather than accepting an ultra-slow GPU codepath. Set this parameter to 1 if you experience slow processing within modules like [_local contrast_](../../views/darkroom/modules/local-contrast.md) or if you get intermittent system freezes. Please note that this should not affect any card manufactured since 2015.

b. micro nap
: _default 250_
: In an ideal case you will keep your GPU busy at 100% when processing the pixelpipe. However, if your GPU is also required to update your screen, and Ansel is using it at 100%, there may not be sufficient time left for this task. This will usually manifest as jerky GUI updates on panning, zooming or when moving sliders. To resolve this issue Ansel can add small pauses into its pixelpipe processing so that the GPU can catch its breath and perform GUI related activities. The "micro nap" parameter controls the duration of these pauses in microseconds. On current systems you are pretty safe with the default value, even for integrated graphics cards. If you are using multiple devices or you are not using your discrete GPU for drawing on your screen, this value can be set to 0 for the non-desktop device.

c. pinned memory
: _0 = use gui to select mode; 1 = enforce pinned transfer; 2 = disable pinned transfer_
: During tiling huge amounts of memory need to be transferred between host and device. On some devices direct memory transfers to and from an arbitrary host memory region may give a large performance penalty. This is especially noticeable when exporting large images on smaller graphics cards or while using newer modules like [_diffuse or sharpen_](../../views/darkroom/modules/diffuse.md) or the _guided laplacians_ mode in the [_highlight reconstruction_](../../views/darkroom/modules/highlight-reconstruction.md) module.

: There is no safe method or general rule to predict whether or not this parameter will provide a performance benefit, so you will have to experiment for yourself. This mode can also be set globally by setting the "tune OpenCL performance" option to "memory transfer" (in [Preferences > Processing > CPU, GPU, Memory](../processing.md#cpu-gpu-memory)), in which case this parameter should be set to 0. Otherwise, you can enable/disable it at a device level using this parameter.

d. clroundup wh / e. clroundup ht
: These parameters should be left at this default value -- testing has not shown any benefit to using other values.

f. number of event handles
: Event handles are used by Ansel to monitor the success/failure of kernels and provide profiling info even if the pixelpipe is executed asynchronously. The number of event handles is a limited resource of your OpenCL driver -- while they can be recycled, there is a limited number that can be used at the same time. Unfortunately, there is no way to find out what the resource limits are for a given device, so Ansel uses a very conservative guess of 128 by default. On most current devices and drivers you can expect a number of up to 1024 to be safe and lead to slightly better OpenCL performance. If your driver runs out of free handles you will experience failing OpenCL kernels with error message `CL_OUT_OF_RESOURCES` or even crashes or system freezes.

: A value of 0 will block Ansel from using any event handles. This will prevent Ansel from properly monitoring the success of your OpenCL kernels but saves some driver overhead leading to a better performance. The consequence is that any failures will likely lead to garbled output without Ansel noticing. This is only recommended if you know for sure that your system runs rock-solid.

g. asynchronous mode
: _1 = use asynchronous mode; 0 = don't use_
: This flag controls how often Ansel blocks the OpenCL pixelpipe to get a status on success/failure of the kernels that have been run. For optimum latency set this to 1, so that Ansel runs the pixelpipe asynchronously and tries to use as few interrupts/events as possible. If you experience OpenCL errors like failing kernels, reset the parameter to 0 (the default). This will cause Ansel to interrupt after each module so that you can more easily isolate any problems. Issues have been reported with some older AMD/ATI cards (like the HD57xx) which can produce garbled output if this parameter is set to 1. If in doubt, leave it at its default of 0.

h. disable device
: _0 = enable device; 1 = disable device_
: If Ansel detects a malfunctioning device it will automatically mark it as such by setting this parameter to 1. If you have a device that reports a lot of errors you can manually disable it by setting this field to 1.

i. benchmark
: When Ansel detects a new device on your system it will do a small benchmark and store the result here. You can change this back to 0 to force Ansel to redo the benchmark but in most cases **you should not edit this setting**.

---

**Note**: if Ansel detects a "buggy" device configuration key it will be rewritten back to default values.

---

### Id-specific OpenCL configuration

A second device-specific configuration key is also provided, which takes into account both the device name **and** the device id (just in case you have two identical devices). In this case, the usual key name `cldevice_version_canonicalname` is followed by `_idX` with X being the device id. For example, if the above example device was referred to as device 0, the second configuration setting would (by default) be `cldevice_v4_quadrortx4000_id0=400`.

This configuration key currently only has a single parameter defined:

forced headroom (default 400)
: The amount of memory (in MB) that will **not** be used by Ansel during OpenCL processing. This setting is only valid if you set [preferences > processing > tune OpenCL performance](../processing#cpu-gpu-memory) to "memory size".

: If you set this parameter to zero (`0`) then, on the first run of a pixelpipe, Ansel will attempt to determine how much GPU memory is actually available and use this (with a safety-margin of 100MB) as the maximum amount of memory that Ansel will use, for the remainder of your session. This is usually safe unless you start other applications (that use a reasonable amount of GPU memory) while Ansel is running. Otherwise, use of this option could lead to out-of-memory errors, which will cause Ansel to fall back to CPU, significantly reducing performance. You may switch this option off and on again to prompt Ansel to perform its memory calculation again (at the start of the next pipe run). Note that there are known issues with memory auto-detection on newer Nvidia drivers so auto-detection should be used with care and is therefore disabled by default.

: If you are certain that no apps (or your OS) make use of the specific device you can set this parameter to 1 for the otherwise-unused device so that Ansel will use all of that device's memory.

: The default of 400MB should be fine for most systems. If you find you run into performance problems due to Ansel falling back to CPU, try changing it to 600 or disabling "tune for memory size".

### Other configuration keys

The following additional configuration keys are also available in Anselrc:

cldevice\_version\_canonicalname\_building
: This option is used when compiling OpenCL kernels and may be provided for performance tuning or to work around bugs. You must remove any existing kernels in order to recompile them with the new options. Provide an empty string to recompile without any options. Remove the setting entirely to recompile with default options, default is `-cl-fast-relaxed-math`

opencl\_synch\_cache
: If set to "true", this parameter will force Ansel to fetch image buffers from your GPU after each module and store them in its pixelpipe cache. This is a resource consuming operation, but can make sense depending on your GPU (including if the GPU is rather slow). In this case Ansel might in fact save some time when module parameters have changed, as it can go back to some cached intermediate state and reprocess only part of the pixelpipe. In many cases this parameter should be set to "active module" (the default), which will only cache the input of the currently-focused module.
---

# Copy and paste history : https://ansel.photos/en/doc/views/lighttable/history-copy-paste/

Transfer the editing recipe from one image to one or many others. The result is a **merge** of the source history into each destination image — not a blind overwrite — which means you have fine-grained control over how conflicts between the two histories are resolved.

Copy-paste is available from the **Edit** menu in any view, and the keyboard shortcuts work in both the lighttable and the darkroom.

{{< warning >}}
The current darkroom image cannot be a paste target: Ansel applies the paste to the live in-memory pipeline, which would cause conflicts with the undo stack. The darkroom image is silently excluded from the destination list when you paste.
{{< /warning >}}

## Copying a history

### Copy all (Ctrl+C)

**Edit → Copy history (all)** records the entire editing recipe of the single selected image into a clipboard. The clipboard persists for the duration of the session.

The source image must be saved to the database before the clipboard is filled. If you are in the darkroom and the image has unsaved in-progress edits, Ansel flushes those to the database first so the clipboard always reflects what you see on screen.

### Selective copy (Ctrl+Shift+C)

**Edit → Copy history (parts)** opens a dialog listing all processing modules that are present in the source image's history. Tick or untick individual modules to choose exactly which parts of the recipe you want to transfer. Only the ticked modules will be pasted.

## Pasting a history

### Paste all (Ctrl+V)

**Edit → Paste history (all)** applies the entire clipboard to all selected images. If **Ask merge settings before paste** is enabled (the default), a dialog appears first so you can choose how the incoming history is integrated. See [Merge settings dialog](#merge-settings-dialog) below.

### Paste parts (Ctrl+Shift+V)

**Edit → Paste history (parts)** opens the same module-selection dialog as selective copy, but this time against the clipboard contents. Tick the modules you want to paste, then click OK. The merge settings dialog appears next (if enabled).

## Merge settings dialog

Because each destination image already has its own editing history, pasting is a **merge** operation. The dialog gives you two independent choices each time you paste (or lets you save a default and skip the dialog).

### Merge position

Controls how the incoming history is placed relative to the destination's existing history. Since the processing pipeline is applied from the bottom up (early modules run first, later modules override), placement determines which side wins when the same module appears in both histories.

```mermaid
graph LR
    subgraph "Below — Prepend"
        direction LR
        A1["Incoming edits (base)"] --> B1["Your existing edits"]
    end
```

```mermaid
graph LR
    subgraph "Above — Append"
        direction LR
        A2["Your existing edits (base)"] --> B2["Incoming edits"]
    end
```

```mermaid
graph LR
    subgraph "Replace"
        direction LR
        A3["Incoming edits only"] --> C3["Output"]
    end
```

Below (Prepend)
: The incoming history is inserted **before** your existing edits in the processing stack. Your current edits run later and therefore **override conflicts**. Use this when you want to apply a baseline recipe while keeping your personal adjustments on top.

Above (Append)
: The incoming history is inserted **after** your existing edits. The incoming edits run later and therefore **override conflicts**. Use this when the source image carries corrections (e.g. a colour calibration) that should take precedence over what is already in the destination.

Replace
: Your existing history is discarded entirely and replaced with the incoming one. No conflict resolution is needed because nothing is kept from the destination.

### Use incoming pipeline order

The _pipeline order_ is the spatial order in which processing modules execute — independent from the temporal history stack. When checked, the module execution order is taken from the source image. When unchecked, your current pipeline order is preserved.

{{< note >}}
The pipeline order matters when you have reordered modules from their defaults (e.g. moved _colour calibration_ before _exposure_). Copying pipeline order from an image with a different arrangement will resequence your pipeline.
{{< /note >}}

### Ask me every time

When checked, this dialog appears every time you paste. When unchecked, the current settings are used silently. You can still change the saved defaults at any time via **Edit → History pasting mode** and **Edit → Nodes pasting mode**.

## Default settings (Edit menu)

The merge settings persisted between sessions are controlled by two submenus in the Edit menu:

**Edit → History pasting mode** sets the default merge position:

- **Prepend** — below mine, my edits win
- **Append** — above mine, incoming wins
- **Replace** — discard destination history

**Edit → Nodes pasting mode → Copy module order** toggles whether the source pipeline order is copied.

**Edit → Ask merge settings before paste** toggles whether the dialog is shown before each paste operation.

These defaults are used silently when **Ask me every time** is disabled, and are pre-filled in the dialog when it is shown.

{{< note >}}
Copy-paste and [styles](../toolboxes/styles.md) each maintain **independent** merge settings. Changing the paste defaults here has no effect on style application, and vice versa.
{{< /note >}}

## Workflow examples

### Propagating a colour calibration to a batch

1. Edit one image: apply _colour calibration_, _exposure_, and _tone equalizer_.
2. **Edit → Copy history (parts)** and tick only _colour calibration_.
3. Select the rest of the batch in the lighttable.
4. **Edit → Paste history (parts)**, choose **Above mine** so the calibration overrides any existing per-image settings.

### Sharing a base recipe across a project

1. Develop one image to the look you want for the whole project.
2. **Edit → Copy history (all)**.
3. Select all images from the same session.
4. **Edit → Paste history (all)**, choose **Below mine** so per-image exposure adjustments you add later will override the shared base.

### Resetting to a clean slate

1. Copy from an image that has only the default pipeline (no edits).
2. **Edit → Paste history (all)**, choose **Replace**.

This is equivalent to **Edit → Delete history** but lets you keep the default module activations from a specific image rather than from Ansel's built-in defaults.

## Keyboard shortcuts

| Action | Shortcut |
|---|---|
| Copy history (all) | Ctrl+C |
| Copy history (parts) | Ctrl+Shift+C |
| Paste history (all) | Ctrl+V |
| Paste history (parts) | Ctrl+Shift+V |
---

# Install OpenCL drivers : https://ansel.photos/en/doc/install/opencl/

OpenCL is a vendor-agnostic and cross-platform way of offloading computations to the GPU. GPUs are designed specifically to handle pixel operations, with an optimized memory layout and a massively multi-core architecture[^2] allowing for fast and energy-efficient image processing.

[^2]:  At least 720 cores for cheap GPUs, to almost 10.000 cores for the beasts

But OpenCL can only be used for background computational (offline rendering) tasks and cannot be used to render the GUI in real-time, which is the realm of OpenGL.

The architecture of Ansel and darktable pixel pipeline is flawed in that regard, because the output of (fast) OpenCL computations has to be sent back to the (slow) Gtk 3 thread, which paints the processed image within the GUI (along with the rest of GUI controls) using only a single CPU core. This is the biggest performance bottleneck in darktable and Ansel, and the reason why it is a rather slow application.

Nevertheless, OpenCL will bring a substantial speed-up to your image processing and exporting.

## Scope of OpenCL

OpenCL is used only by the [image processing modules](../views/darkroom/modules/_index.md) that have an OpenCL kernel available as an alternative to the CPU (plain C) code. This is optional, and not all modules have an OpenCL kernel, depending on the skills, willingness and available hardware of the developer who programmed them. [^1]

[^1]: Plain C code can rarely be translated directly to OpenCL kernels and needs extra effort to adapt it to the different memory layout.

OpenCL kernels are compiled at runtime by [LLVM](https://llvm.org/), when the application is first executed, which may take some time and delay the startup. They are then cached in `~/.config/ansel`, and recomputed every time the OpenCL driver version changes (on upgrades and downgrades).

Apart from some (_most_) image processing modules, the rest of Ansel does not make use of the GPU at all, being through OpenCL or OpenGL.

## Requirements

OpenCL will be available when a GPU and its driver are detected and compatible with OpenCL 1.2 at least. A GPU with at least 2 GB of memory is recommended. LLVM v4 to v15 is needed to build the kernels at runtime (it is included in the official packages – _the drivers are not_).

## Expected benefits

It is impossible to say how much faster a particular GPU will make image rendering, compared to a particular CPU, without actually benchmarking them, especially because image processing is more bound by the speed of I/Os (transfers between hardware memories) than by the raw clock speed of the computing chip (the infamous GHz × number of cores).

It has been found that crazy-expensive CPUs still perform better than cheap GPUs. However, at similar performance, GPUs will typically drain less electric power and produce less heat than CPUs, which will also prevent [thermal throttling](https://pcguide101.com/cpu/what-is-cpu-throttling/) to some extent.

The overall performance of OpenCL depends on :
- The GPU raw performance,
- How well the driver uses the GPU (beware the power-saving modes),
- How well the OS integrates the driver (noticeably, support in the Linux Kernel),
- How many applications use the GPU simultaneously,
- How cleverly the LLVM compiler was able to optimize the OpenCL kernels for your hardware and driver,
- How well your computer cooling system is able to maintain the casing at a low-enough temperature,
- How fast your motherboard bus is able to copy pixels from the RAM to the GPU memory, and the other way around.

Anyone pretending to guess the overall performance by just looking at a GPU specifications sheet is a fraud.

## Vendors

### Intel

Ansel can use the Intel GPU embedded on the CPU chip, provided you use recent [Intel Graphics Compute Runtime drivers](https://www.intel.com/content/www/us/en/developer/articles/tool/opencl-drivers.html) (dubbed [_Intel Neo_](https://github.com/intel/compute-runtime)). These seem to work fairly on Linux and Windows at least.

The previous generation of Intel OpenCL drivers, dubbed _Intel Beignet_, has never worked reliably. Fortunately, it has gradually disappeared from repositories and distributions since 2018.

Using the embedded GPU will be typically slightly faster than using the CPU. If getting the Intel Graphics Compute Runtime on your system is not too much of a hassle, the boost is definitely worth it.

### Optimus

[Optimus](https://en.wikipedia.org/wiki/Nvidia_Optimus) is an Intel + Nvidia hardware setting used on laptops since 2011, which consists in a low-power/low-performance Intel embedded GPU (_eGPU_) used for lightweight tasks, coupled with a discrete high-performance/power-hungry Nvidia GPU (_dGPU_) for games and rendering, but turned on only on demand.

On Linux, this technology has been properly supported only since circa 2020 (_though HDMI still does not work on eGPU for most laptops if the HDMI port is soldered on the dGPU_), but only for the mainstream distributions, and Gnome even allows to start applications explicitly on the eGPU or the dGPU without having to use command line or restart the graphical session.

However, "starting an application on dGPU" only means that the GUI rendering (aka OpenGL) is piped to the dGPU. Since Ansel renders all the GUI on CPU and doesn't use OpenGL at all, it doesn't need to be explicitly started on dGPU. OpenCL will be able to work in background on all detected GPUs no matter which one renders the GUI.

Ansel is able to use both the eGPU and the dGPU at the same time to render image pipelines concurrently.

### AMD

Only the proprietary [amdgpu-pro driver](https://amdgpu-install.readthedocs.io/en/latest/install-installing.html) supports OpenCL.

On Linux, AMD has been known for several years to be very slow to update their drivers when distributions are updated. Don't upgrade your distribution before you know AMD has working drivers for it, and stick to LTS versions if using Ubuntu.

### Nvidia

Only the proprietary driver supports OpenCL.

On Linux, Nvidia OpenCL depends on the CUDA libraries. There are known issues of kernel panic or system crash when the computer goes in hibernate or suspend mode, because the allocated memory buffers are not preserved. If that happens to you, Linux distributions typically have packages called `nvidia-modprobe`, `nvidia-persistenced` and `nvidia-power(save)d` which should take care or that (or worsen it). In any case, avoid letting your computer go in suspend or hibernate mode while any application using OpenCL is running, and save any work in progress before.

## Caveats

OpenCL and graphics drivers have the worst track-records and are known to break every next upgrade, mostly because vendors consider them low-priority, compared to their proprietary equivalent (CUDA for Nvidia, Metal for Apple) or the more recent Vulkan.

### Stay on the production/LTS branches

If you have an AMD card, you may have no choice : the latest driver will generally be the only one working on your system.

For Nvidia and Intel cards, there are production branches that ship the yesteryear driver version. It may not support the brand new hardware nor the latest cool upgrades, but will generally show improved stability and fewer bad surprises.

Keep away from the hot new drivers for as long as you can. Linux distributions typically ship a couple of legacy drivers alongside the most recent.

### Memory use

All applications using the GPU, through CUDA, Vulkan, Metal, OpenCL and OpenGL will share the GPU memory, which is typically much smaller than the system RAM. The GPU memory allocation is not as predictable as the RAM allocation, and is mostly vendor/driver-dependent.

That is to say that running concurrently several applications all using the GPU may result in crashes if they temporarily try to use more memory than available. Try to avoid playing 4K videos in your web browser, playing games or running GPU-accelerated video encoders (Nvenc, VDPAU, etc.) when Ansel is processing 36-52 Mpx pictures.
---

# Combining drawn & parametric masks : https://ansel.photos/en/doc/views/darkroom/masking-and-blending/masks/drawn-and-parametric/

Drawn and parametric masks can be used in combination to form a single mask that can be applied to a module.

There are two main elements which control how individual masks are combined: the _polarity_ setting of each individual mask (defined by the plus or minus buttons) and the setting in the “combine masks” combobox.

The "combine masks" combobox contains the following options, defining how the drawn and parametric masks will be combined:

exclusive
: A straightforward method of combining masks, exclusive mode multiplies together the individual pixel values from each of the component masks.

: For a given pixel, the final mask will have value of 0 if _any_ of the individual masks are 0 at that location and it will only have a value of 1.0 if _all_ masks have a value of 1.0 at that location.

: Any individual mask can _exclude_ a pixel by setting its value to 0, regardless of what the other masks do. Once a pixel is excluded by a mask there is no way for another mask to include it again.

inclusive
: Inclusive mode first inverts each individual mask (subtracts its value from 1.0), multiplies the inverted masks together, and finally inverts the combined mask once again.

: For a given pixel, the final mask will have a value of 1.0 if _any_ of the individual masks are 1.0 at that location and it will only have a value of 0 if _all_ masks have a value of 0 at that location.

: Any individual mask can _include_ a pixel by setting its value to 1.0, regardless of what the other masks do. Once a pixel is fully included by a mask (its value is 1.0) there is no way for another mask to exclude it again.

exclusive and inclusive inverted modes
: Using the above combination methods alone would still be rather limiting. We gain maximum flexibility by allowing an additional inversion step for each individual mask. This is governed by the polarity buttons that you find close to the individual channels.

: Toggling the polarity button of a mask inverts its values (subtracts the original value from 1.0).

: Finally within the “combine masks” combobox you may select the _exclusive & inverted_ or _inclusive & inverted_ options. Each of these options is equivalent to the _exclusive_ and _inclusive_ modes, respectively, but with a final step that inverts the resulting mask.

## Typical use cases

inclusive mode
: For this mode you set the “combine masks” combobox to inclusive mode and make sure that all polarity buttons of all the individual channels and of the drawn mask are set to negative (-). Your starting point is a mask where all pixels have a value of zero (no pixel is selected). You now adjust the parametric mask sliders to bring more and more pixels into the selection or you draw shapes on the canvas to select specific areas of your image.

exclusive mode
: In the opposite case you set the “combine masks” combobox to exclusive mode and make sure that all polarity buttons are set to positive (+). Your starting point is a mask with all values at 1.0 (all pixels are selected). You now adjust the parametric mask sliders to exclude parts of your image as needed or directly draw shapes on the canvas to exclude those areas.

For your convenience the parametric masks GUI provides a toggle button that inverts all channel polarities and toggles between inclusive and exclusive mode in the “combine masks” combobox.

For novice users it is recommended that you stick to the above two use cases. This means that you should decide beforehand how you want to construct your mask.
---

# Lighttable : https://ansel.photos/en/doc/preferences-settings/lighttable/

Control functionality in the [lighttable](../views/lighttable/_index.md) view and modules.

## General

hide built-in presets for utility modules
: If enabled, only user-defined presets will be shown in presets menu for utility modules -- built-in presets will be hidden (default off).

show image time with milliseconds
: Choose whether to include milliseconds when displaying time values (default off). If set, milliseconds are shown in the [image information](../views/toolboxes/image-information.md) module and can also be used in the [geotagging](../views/toolboxes/geotagging.md) module.

## Thumbnails

use raw file instead of embedded JPEG from size
: When generating thumbnails for images that have not yet been processed in the darkroom, if the thumbnail size is greater than this value, generate it by processing the raw image data. If the thumbnail is below this size, use the JPEG preview image embedded in the raw file. Once an image has been processed in the darkroom, thumbnails will always be generated from raw data (you can revert back to the JPEG preview by discarding history). To render thumbnails with the best quality choose "always".

high quality processing from size
: If the thumbnail size is greater than this value and is being generated from raw data, it will be processed using the full quality rendering path, which is better but slower (default 720p). To render thumbnails with the best quality, choose "always".

delimiters for size categories
: Size categories are used to allow different thumbnail overlays to be shown depending on the thumbnail size. A pipe delimited set of values defines at what image sizes the category changes. The default value of "120|400" means that there will be 3 categories of thumbnail: 0-120px, 120-400px and >400px.
---

# Rendering intent : https://ansel.photos/en/doc/color-management/rendering-intent/

If rendering with LittleCMS2 is activated (see [rendering method](./rendering-method.md)) you can define how to handle out-of-gamut colors when converting between color spaces. A selection box in the [export](../views/toolboxes/export.md), [_output color profile_](../views/darkroom/modules/output-color-profile.md), and [soft proof](../views/toolboxes/soft-proof.md) modules gives you a choice of the following rendering intents:

perceptual
: Best suited to photographs as it maintains the relative position of colors. This is usually the best choice.

relative colorimetric
: Out-of-gamut colors are converted to colors having the same lightness, but different saturation. Other colors remain unmodified.

saturation
: Saturation is retained but lightness is slightly changed.

absolute colorimetric
: Keep the white point.
---

# Setting up opencl : https://ansel.photos/en/doc/preferences-settings/performance/opencl/setting-up/

The huge diversity of systems and the marked differences between OpenCL vendors and driver versions makes it impossible to give an comprehensive overview of how to setup OpenCL. We only can give you an example, in this case for NVIDIA driver version 331.89 on Ubuntu 14.04. We hope that this will serve as a basic introduction and will help you to solve any problems specific to your setup.

The principle OpenCL function flow is like this:

`Ansel > libOpenCL.so > libnvidia-opencl.so.1 > kernel driver module(s) > GPU`

- Ansel dynamically loads `libOpenCL.so` -- a system library that must be accessible to the system's dynamic loader (`ld.so`).

- `libOpenCL.so` reads the vendor-specific information file (`/etc/OpenCL/vendors/nvidia.icd`) to find the library that contains the vendor-specific OpenCL implementation.

- The vendor-specific OpenCL implementation comes as a library `libnvidia-opencl.so.1` (which in our case is a symbolic link to `libnvidia-opencl.so.331.89`).

- `libnvidia-opencl.so.1` needs to talk to the vendor-specific kernel modules `nvidia` and `nvidia_uvm` via device special files `/dev/nvidia0`, `/dev/nvidiactl`, and `/dev/nvidia-uvm`.

At system startup the required device special files (`/dev/nvidia*`) need to be created. If this does not happen on your system by default, the easiest way to set them up and make sure all modules are loaded is by installing the `nvidia-modprobe` package.

A user account that needs to make use of OpenCL from within Ansel must have read/write access to NVIDIA's device special files. On some systems these files allow world read-write access by default, which avoids permission issues but might be debatable in terms of system security. Other systems restrict the access to a user group, e.g. “video”. In this case your user account has to be member of that group.

To summarise, the packages that needed to be installed in this specific case were:

```
nvidia-331 (331.89-0ubuntu1~xedgers14.04.2)
nvidia-331-dev (331.89-0ubuntu1~xedgers14.04.2)
nvidia-331-uvm (331.89-0ubuntu1~xedgers14.04.2)
nvidia-libopencl1-331 (331.89-0ubuntu1~xedgers14.04.2)
nvidia-modprobe (340.24-1)
nvidia-opencl-dev:amd64 (5.5.22-3ubuntu1)
nvidia-opencl-icd-331 (331.89-0ubuntu1~xedgers14.04.2)
nvidia-settings (340.24-0ubuntu1~xedgers14.04.1)
nvidia-settings-304 (340.24-0ubuntu1~xedgers14.04.1)
nvidia-libopencl1-331 (331.89-0ubuntu1~xedgers14.04.2)
nvidia-opencl-dev:amd64 (5.5.22-3ubuntu1)
nvidia-opencl-icd-331 (331.89-0ubuntu1~xedgers14.04.2)
opencl-headers (1.2-2013.10.23-1)
```

The list of NVIDIA related kernel modules as reported by lsmod was:

```
nvidia
nvidia_uvm
```

The list of NVIDIA related device special files (`ls -l /dev/nvidia*`) should read like:

```
crw-rw-rw- 1 root root 195,   0 Jul 28 21:13 /dev/nvidia0
crw-rw-rw- 1 root root 195, 255 Jul 28 21:13 /dev/nvidiactl
crw-rw-rw- 1 root root 250,   0 Jul 28 21:13 /dev/nvidia-uvm
```

Beware that the major/minor numbers (e.g. `250/0` for `/dev/nvidia-uvm` in this example) may vary depending on your system.
---

# Star ratings & color labels : https://ansel.photos/en/doc/views/lighttable/digital-asset-management/star-color/

Star ratings and color labels help you to sort and rank images according to your own criteria. An image's star rating and color labels can be displayed over thumbnails in the lighttable view and filmstrip module.

## Star ratings

You can give an image a rating from zero to five stars. Whenever you import images, each image receives a default rating which you can define in the [import](../../../getting-started/import.md) module.  You can also mark an image as “rejected”.

There are several ways to change a rating. While hovering the cursor over an image thumbnail, you can press a number key 0 – 5 to define the number of stars, or press R to “reject” an image. This is probably the fastest way to rate your images on first inspection of a film roll.

You can also click directly on the star icons overlaid on the thumbnails (click the ✕ to reject), or use the global menu _Image_ 🡒 _Ratings_.

As rejecting an image removes the currently-applied star rating, you can undo the rejection by clicking x or pressing R again.


To rate multiple images at once, select those images in the lighttable or filmstrip and then press the appropriate shortcut key.

## Color labels

Color labels are another way to classify images, and can be used as an alternative to star ratings or to work alongside them. Each image can carry any combination of one or more color labels (red, yellow, green, blue, or purple).

You can set the color labels for a single image by hovering your cursor over the thumbnail and pressing the function keys F1 – F5, which correspond with the labels in the order given above. F6 clears all labels.

To set the color labels of one or more images, select the desired images in the lighttable and then press the appropriate function key, or use the global menu _Image_ 🡒 _Color labels_. A color label is added to all selected images if **any** of them do not currently have it; otherwise the label is removed from all of them. To remove all labels (of any color) from the selected images, press F6 (or _Image_ 🡒 _Color labels_ 🡒 _Clear labels_).
---

# Support : https://ansel.photos/en/support/

<div class="lead my-5 ps-3">This page will show you how and where you can learn and get help, in particular on Ansel, but also in general on photography.</div>

## General info

Ansel is mostly a one-person operation. This is good news when it comes to keeping developement in sync with users needs and expectations, not so much when it comes to balancing the workload. So everything that could be automated [was automated](./contribute/workflows.md) for the developer's sanity, and you are asked to use the online resources first everytime it is possible.

{{< warning >}}
When you submit a bug report or book a training session, please upload your attachment files on my [private cloud](https://cloud.apmlt.net/s/YAdfYajPkE5nLyW) instead of using third-party hosting services with an expiration date : there is no guaranty I will download the files in time and it keeps things tidy for me.
{{< /warning >}}


## Online learning resources

There already is a fair deal of information around on Ansel's usage. As Ansel is based on darktable 4.0, most tutorials on darktable 4.x (and 3.x, to a lesser extent) are still relevant, even though the GUI may be slightly different.

{{% row %}}
{{% card title="Videos" icon="chalkboard-teacher" %}}
{{< button url="https://www.youtube.com/channel/UCmsSn3fujI81EKEr4NLxrcg" icon="youtube fab" label="Youtube channel" >}}
{{% /card %}}

{{% card title="Documentation" icon="book" %}}
{{< button url="/en/doc" icon="glasses" label="Read it, seriously" >}}
{{% /card %}}
{{% /row %}}

{{% row %}}
{{% card title="Chantal AI, search engine 🇫🇷 🇬🇧" icon="search" class="border border-info bg-info-subtle" %}}
Chantal is a __bilingual AI language model__ trained specifically for open-source __image processing and photography__, coupled with a search engine that indexes more than __47.000 pages__, including Ansel docs, bug reports and forum threads.

It acts as your virtual librarian and can greatly improve access to information when you are __not sure of the exact terms__ to look for because __it understands synonyms__ and context.

{{< button url="https://chantal.aurelienpierre.com" icon="search" label="Search" >}}
{{% /card %}}

{{% card title="Ansel GPT" icon="search" %}}
Ansel GPT is a custom ChatGPT model trained with all the content of the Ansel website and documentation, plus other color science resources. It can answer many questions and help you build and configure Ansel on your system. It has access to significantly fewer resources than Chantal but can form full explanations.

It is available to _ChatGPT Plus_ subscribers only (as per OpenAI limitations).
{{< button url="https://chatgpt.com/g/g-680d2f861a608191a0f7549eadd40f2e-ansel-gpt" icon="search" label="Search" >}}
{{% /card %}}
{{% /row %}}

## Community support

Free user support is typically assumed by other users.

{{% row %}}
{{% card title="Chat" icon="comment-dots" %}}
{{< button url="https://matrix.to/#/#ansel:matrix.org" icon="envelope" label="Matrix chat" >}}
{{% /card %}}
{{% /row %}}

## Bugs and oddities

{{% row %}}
{{% card title="Bugs" icon="bug" %}}
Everything that leads to the software crashing, producing invalid output or unusable core features is a bug.
{{< button url="https://github.com/aurelienpierreeng/ansel/issues/" icon="github fab" label="Bug tracker" >}}
{{% /card %}}
{{% card title="Questions" icon="question" %}}
If a feature seems to have been removed or to work differently, that might be a redesign, and there is always a reason behind.
{{< button url="https://community.ansel.photos/discussions-category?category=7" icon="question" label="Ask a question" >}}
{{% /card %}}
{{% /row %}}---

# Toolboxes : https://ansel.photos/en/doc/views/toolboxes/

A _toolbox_ is a collapsible tool docked in one of the side panels. Unlike the [darkroom processing modules](../darkroom/modules/_index.md), toolboxes do not modify pixels — they help you browse, organize, inspect, export and annotate images, or assess the picture being edited.

Each view shows only the toolboxes relevant to its task, so the same panel position can hold different tools depending on whether you are in the lighttable, the darkroom, the map or the print view.

{{< note >}}
Coming from Darktable: many tools that used to be toolbox "modules" are now [global menu](../global-menu.md) entries instead, because a grid of buttons is really just a menu in disguise. In particular, _history stack_ copy/paste, _styles_ management, _selection_ helpers, _image_ actions and the maintenance commands now live in the menu bar. The toolboxes below are the ones that remain genuine docked panels.
{{< /note >}}

## Working with toolboxes

- Click a toolbox header to expand or collapse it.
- A toolbox can be expanded and focused directly through the [global action search](../../getting-started/keyboard.md#vimkey-like-global-action-search) (<kbd>Ctrl</kbd>+<kbd>P</kbd>) or an assigned shortcut; triggering its focusing action again collapses it.
- Drag the inner border of a panel to resize it; panel visibility and size are remembered per view.

## Lighttable toolboxes

Left panel:

- [Library](collections.md) — build the current collection from folders, tags or arbitrary queries.
- [Metadata editor](metadata-editor.md) — edit title, description, creator and other text metadata.
- [Tagging](tagging.md) — attach and manage keyword tags.
- [Geotagging](geotagging.md) — attach GPS coordinates, including from a GPX track.
- [Image information](image-information.md) — EXIF/IPTC data of the highlighted image.
- [Notes](notes.md) — free-text notes attached to an image.

Second top row:

- [Collection filters](collections.md) — rating, color label, edited status, text search and _restrict to selection_ (see the [lighttable view](../lighttable/_index.md)).
- Display options — columns, zoom and overlays (see the [lighttable view](../lighttable/_index.md#lighttable-display)).

Reached from the menu bar:

- [Export](export.md) — render the selected images to files (_File → Export…_).

## Darkroom toolboxes

Left panel:

- [Navigation](navigation.md) — pan and zoom the center image from a thumbnail overview.
- [Scopes](scopes.md) — histogram, waveform and vectorscope, including the [global color picker](global-color-picker.md).
- [Snapshots](snapshots.md) — freeze and compare edit states.
- [Duplicate manager](duplicate-manager.md) — manage the versions of the current image.
- [Mask manager](mask-manager.md) — list and edit the [drawn shapes](../darkroom/masking-and-blending/masks/drawn.md) used by masks.
- [History of changes](history-stack.md) — step through, compress or reset the editing history.
- [Image information](image-information.md) and [Notes](notes.md).

Right panel:

- The [image-processing modules](../darkroom/modules/_index.md), in their [workflow tabs](../darkroom/darkroom-view-layout.md#module-workflow-tabs).

Bottom toolbar (visual assessment):

- [Styles](styles.md), [Color assessment](color-assessment.md), [Raw overexposed](raw-overexposed.md), [Clipping](clipping.md), [Soft proofing](soft-proof.md), [Gamut check](gamut.md), [Guides & overlays](guides-overlays.md), [Focus peaking](focus-peaking.md).

## Filmstrip

The [filmstrip](filmstrip.md) (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd>) is an optional bottom panel available in the darkroom, map and print views. It shows the current collection's thumbnails for quick navigation without returning to the lighttable.

## Map toolboxes

- [Find location](find-location.md) and [Locations](locations.md) — search places and manage location tags.
- [Geotagging](geotagging.md) and [Map settings](map-settings.md).

## Print toolboxes

- [Print settings](print-settings.md) — page setup, printer, profile and layout for the [print view](../print/_index.md).

## Other

- [Lua scripts installer](lua-scripts-installer.md) — manage optional Lua extensions.
---

# Undo and redo : https://ansel.photos/en/doc/views/darkroom/pixelpipe/undo-redo/

While you are editing your image, Ansel records all of the modifications you make to that image. This means that it is possible to undo and redo changes to recover a previous editing state. Note that the undo/redo facility is unlimited in the number of steps while editing an image, but is reset each time the darkroom is switched to a new image.

Press Ctrl+Z to undo the last modification and Ctrl+Y to redo the last undone modification (if any).
---

# Configuration files : https://ansel.photos/en/doc/install/configuration/

The configuration files of Ansel are located in :

* `$HOME/.config/ansel` for Mac OS and Linux
* `C:\%LOCALAPPDATA%\ansel` on Windows.

## Default configuration files

`anselrc`
: contains all user preferences except presets and styles. This may be opened in any text editor for manual edit if any preference makes the software crash at startup.

`keyboardrc`
: contains all user-defined keyboard shortcuts.

`data.db`
: contains user presets for modules, styles, tags and locations. This is an SQLite database. It is backed-up before updating to a new release and automatic snapshots can be configured (see below).

`library.db`
: contains all images known to Ansel, along with their directories (dubbed _film rolls_). Like the previous, it is an SQLite database backed-up before updating to new releases and with optional snapshots.

The frequency of snapshots can be defined in the software global _Preferences_ -> tab _Storage_ -> section _Database_ -> entry _Create database snapshots_ and _how many snapshot to keep_. By default, Ansel saves one snapshot every week when closing the software.

## Optional configuration files

`luarc`
: a text file containing enabled Lua modules. Note that Lua doesn't work yet on Ansel v0.0.

`lua` directory
: containing the tree of all available Lua modules. This directory is actually a local Git repository that can updated through `git pull` from the sources. It is added by the automatic Lua installer or can be addde manually.

`user.css`
: a stylesheet containing user-defined CSS tweaks to the default theme,

`watermark` directory,
: to add manually, containing SVG files to be used in the [watermark module](../views/darkroom/modules/watermark.md),

`color` directory
: containing an `in` and an `out` sub-directory in which user-defined ICC v2 or v4 profiles can be dropped for later use respectively in the [input color profile](../views/darkroom/modules/input-color-profile.md) and [output color profile](../views/darkroom/modules/output-color-profile.md) modules. Input profiles will be used to decode images opened in Ansel, output profiles to export images, for softproofing in the darkroom or for display color management.


## XMP sidecars

Ansel allows to [write XMP sidecar files](../views/lighttable/digital-asset-management/sidecar.md) along the raw images. These files duplicate the content of the local `library.db` in a text file hosted on the same storage as the raw pictures (which may be a server, a NAS or an external hard-drive), while the `library.db` stays locally in the home directory of the current computer user.

Ansel always loads the editing histories from the local `library.db` database first, and can optionally [look for updated XMP files on startup_](../preferences-settings/storage#xmp). If the editing version doesn't match between XMP and database, you will prompted with a conflict resolution window allowing to synchronize from XMP to database or from database to XMP, or to keep the most/least recent version. This can be used to synchronize edits between computers, but needs to be used carefully because the content of XMP is not checked, so all computers need to use the same version of Ansel.

XMP sidecars can also interfere with darktable if you use both. [Learn more](./darktable.md#working-with-ansel-alongside-darktable)
---

# Ansel-cltest : https://ansel.photos/en/doc/cli/ansel-cltest/

The `Ansel-cltest` binary checks if there is a usable OpenCL environment on your system that Ansel can use. It emits some debug output that is equivalent to calling `Ansel -d opencl` and then terminates.

`Ansel-cltest` is called without command line parameters.
---

# Ansel's color spaces : https://ansel.photos/en/doc/color-management/color-spaces/

Input images are either RGB files (like JPEGs or TIFFs) or camera RAWs. Both store visual information as a combination of primary colors (e.g. red, green and blue) which together describe a light emission to be recreated by a display.

The following image illustrates this concept.

![Spectral decomposition of a light emission into 3 RGB intensities](spectral-decomposition.jpg)

The left-hand-side of the image depicts a colored light that we need to represent digitally. We can use three ideal color filters to decompose this light into three colored primary lights at different intensities. In order to recreate the original colored light from our ideal decomposition (as illustrated in the center of the image) we simply need to recombine those three primary lights by addition.

It should be possible to reproduce the original colored light by taking a set of white lights at the correct intensities and projecting those lights through appropriately colored filters. This experiment can be performed at home using gels and dimmable white bulbs. This is roughly what old color CRT displays did and is how video projectors still work.

In photography, the initial decomposition step is performed by the color filter array that sits on top of your camera's sensor. This decomposition is not ideal, so it isn't possible to precisely recreate the original emission with simple addition -- some intermediate scaling is required to adjust the three intensities.

On screens, the LED bulbs are dimmed proportionally to each intensity, and the emissions of the three lights are physically added to reconstruct the original emission. Digital images store the intensities of these primary lights as a set of three numbers for each pixel, depicted on the right-hand side of the above image as shades of gray.

While a set of display intensities can be easily combined to recreate an original light on a screen (for example, if we created a synthetic image in-computer) the set of captured intensities from a sensor needs some scaling in order for the on-screen light addition to reasonably reproduce the original light emission. This means that every set of intensities, expressed as an RGB set, must be linked to a set of filters (or primary LED colors) that define a _color space_ -- any RGB set only makes sense with reference to a color space.

Not only do we need to temper the captured intensities to make them summable again, but if we are to recompose the original light on a display that does not have the same colored filters or primaries as the space in which our RGB set belongs, these intensities need to be rescaled to take into account the differing filters on the display. The mechanism for this scaling is described in _color profiles_, usually stored within `.icc` files.

---

**Note**: Color is not a physical property of light -- it exists only in the human brain, as a product of the decomposition of a light emission by the cone cells in the retina, again very similar in principle to the above filtering example. An "RGB" value should be understood as "light emissions encoded on 3 channels connected to 3 primaries", but the primaries themselves may look different from what humans would call "red", "green" or "blue".

---

The filters described here are overlapping band-pass filters. Since they overlap, summing them back together would not preserve the energy of the original spectrum so (long story short) we need to dial them down with regard to the retina cone response

Most of Ansel's actual image processing takes place in a large RGB "working profile" space, with some (mostly older) modules internally working in the CIELab 1976 color space (often just called “Lab”). The final output of the image processing pipeline is once again in an RGB space shaped for either the monitor display or the output file.

This process implies that the pixelpipe has two fixed color conversion steps: [_input color profile_](../views/darkroom/modules/input-color-profile.md) and [_output color profile_](../views/darkroom/modules/output-color-profile.md). In addition there is the [_demosaic_](../views/darkroom/modules/demosaic.md) step for raw images, where the colors of each pixel are reconstructed by interpolation.

Each module has a position in the pixelpipe that tells you which color space the module lives in:

- Up to [_demosaic_](../views/darkroom/modules/demosaic.md)
: The raw image information does not yet constitute an "image" but merely "data" about the light captured by the camera. Each pixel carries a single intensity for one primary color, and camera primaries are very different from primaries used in models of human vision. Bear in mind that some of the modules in this part of the pipe can also act on non-raw input images in RGB format (with full information on all three color channels).

- Between [_demosaic_](../views/darkroom/modules/demosaic.md) and [_input color profile_](../views/darkroom/modules/input-color-profile.md)
: Image is in RGB format within the color space of the specific camera or input file.

- Between [_input color profile_](../views/darkroom/modules/input-color-profile.md) and [_output color profile_](../views/darkroom/modules/output-color-profile.md)
: Image is in the color space defined by the selected working profile (linear Rec2020 RGB by default). As Ansel processes images in 4x32-bit floating point buffers, we can handle large working color spaces without risking banding or tonal breaks.

- After [_output color profile_](../views/darkroom/modules/output-color-profile.md)
: Image is in RGB format as defined by the selected display or output ICC profile.
---

# Darkroom : https://ansel.photos/en/doc/preferences-settings/darkroom/

Control functionality in the [darkroom](../views/darkroom/_index.md) view and associated modules.

## General

pen pressure control for brush masks
: Controls how the pressure reading of a graphics tablet impacts newly generated [drawn mask](../views/darkroom/masking-and-blending/masks/drawn.md) brush strokes. You can control the brush width, hardness and opacity. “Absolute” control means that the pressure reading directly defines the attribute with a value between 0% and 100%. “Relative” means that the pressure reading adjusts the attribute between zero and the pre-defined default value (default off).

smoothing of brush strokes
: Sets the level for smoothing of [drawn mask](../views/darkroom/masking-and-blending/masks/drawn.md) brush strokes. Stronger smoothing leads to fewer nodes and easier editing at the expense of lower accuracy.

scroll down to increase mask parameters
: By default, scrolling your mouse up increases the value of the relevant shape parameters in [drawn masks](../views/darkroom/masking-and-blending/masks/drawn.md). Set this preference to reverse the behavior (default off).

pattern for the image information line
: Set the information to be displayed in the image information line, at the right of the top-most toolbar. You can use any variables in the [variables](..//variables.md) section as well as `$(NL)` for a new line. You can also include [formatting](..//variables.md#formatting) (bold, italic, colors etc).

border around image in darkroom mode
: Display the center image in darkroom mode with an outside border of the given number of pixels (default 20).

show loading screen between images
: Show gray loading screen when navigating between images in the darkroom. Switch this option off to just show a simple toast message and leave the previous image in place until the next image is loaded. Note that switching this option off can be very useful to quickly compare duplicate images, however, there might be issues with long loading times (leading you to think the next image has already loaded) and you may observe visual artifacts while the next image is loading (default on).

## Modules

display of individual color channels
: Control how individual color channels are displayed when activated in the [parametric masks](../views/darkroom/masking-and-blending/masks/parametric.md) feature. You can choose between “false color” and “gray scale” (default "false color").

hide built-in presets for processing modules
: If enabled, only user-defined presets will be shown in the presets menu for processing modules -- built-in presets will be hidden (default off).

show the guides widget in modules UI
: Enable this to show the local [guides & overlays](../views/toolboxes/guides-overlays.md#local-guides) interface directly within the UI of the modules that support it (default on).

show right-side buttons in processing module headers
: Choose whether to show the four buttons (mask indicator, multi-instance menu, reset, presets menu) on the right-hand-side of the module header for processing modules. These buttons will always appear when the mouse is over the module. At other times they will be shown or hidden according to this preference selection:
: - _always_: always show all buttons
: - _active_: only show the buttons when the mouse is over the module
: - _dim_: buttons are dimmed when mouse is not over the module
: - _auto_: hide the buttons when the panel is narrow
: - _fade_: fade out all buttons when the panel narrows
: - _fit_: hide all the buttons if the module name doesn't fit
: - _smooth_: fade out all buttons in one header simultaneously
: - _glide_: gradually hide individual buttons as needed
: (default _always_)

prompt for name on addition of new instance
: If enabled, when creating a new instance of a processing module, a prompt will be immediately displayed allowing you to set a name for the new instance (default on).
---

# Digital asset management : https://ansel.photos/en/doc/views/lighttable/digital-asset-management/

Beyond editing pixels, Ansel keeps track of your images and the work you do on them. This is _digital asset management_ (DAM): organizing, describing, rating, grouping and safeguarding your collection so it stays searchable and recoverable over time.

The pages in this section cover how that information is stored and used:

- [Sidecar files](sidecar.md) — how edits and metadata are saved alongside your originals as XMP files, and [importing sidecars](sidecar-import.md) from other software.
- [Star ratings & color labels](star-color.md) — ranking and classifying images for culling.
- [Metadata and tagging](metadata-tagging.md) — descriptive text and keywords.
- [Image grouping](grouping.md) — stacking related images together.
- [Local copies](local-copies.md) — working off-site and re-synchronizing later.
- [Thumbnails](thumbnails.md) — how previews are generated and cached.
---

# Image grouping : https://ansel.photos/en/doc/views/lighttable/digital-asset-management/grouping/

Grouping images helps to improve the structure and clarity of your image collection when displayed in the lighttable view.

You can combine images into a group by selecting them and using the global menu _Image_ 🡒 _Group images_ (<kbd>Ctrl</kbd>+<kbd>G</kbd>). Likewise, remove selected images from a group with _Image_ 🡒 _Ungroup images_ (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd>).

Duplicated images are automatically grouped together. Similarly, if you import multiple images from the same directory, having the same base name, but different extensions (eg. `IMG_1234.CR2` and `IMG_1234.JPG`), those images automatically form a group.

Images that are members of a group are denoted by a group icon on their thumbnail.

Whether groups are collapsed is controlled from the global menu _Display_ 🡒 _Collapse grouped images_. When this is off, all images are displayed as individual thumbnails. When it is on, each group is represented by a single thumbnail (the group leader). Press the group icon on a group leader's thumbnail to expand that group (press again to collapse); expanding another group collapses the first.

A group can be outlined by a frame around its member thumbnails, enabled with _Display_ 🡒 _Show group borders_ (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>). The border becomes more visible when you hover over one of the group's images.

You can define which image is considered to be the group leader by clicking on the group icon of the desired image while that group is expanded. The group icon is shown only if grouping mode is enabled, so to change the group leader, you need to first enable grouping, expand the appropriate group and finally click the group icon of the desired "group leader" image. The current group leader is shown in a tooltip when you hover over the group icon of an image.

If you collapse an image group and then enter darkroom mode (e.g. by double-clicking on the thumbnail), the group leader image will be opened for developing.

Image groups are also a convenient way to protect an existing history stack against unintentional changes. If you have just finalized an image and want to protect its current version, select the image, duplicate it with _Edit_ 🡒 _Duplicate existing history_ (<kbd>Ctrl</kbd>+<kbd>D</kbd>), and make sure grouping is collapsed. Now, whenever you open the image group again in the darkroom, only the group leader will be altered, and the underlying duplicate remains unchanged.

---

**Note:** “duplicating images” only generates a copy of an image's history stack, stored in another small XMP file. There is still only one raw file.

---
---

# Map : https://ansel.photos/en/doc/views/map/

The Map view is optional and needs to be activated in [Preferences > Other views](../../preferences-settings/other-views.md). It allows you to see where your geo-tagged images were taken, and to add location information to non-geo-tagged images.

The Map view shows a world map with the images in the current [collection](../../getting-started/library.md) pinned to their geo-tagged location (if available). This requires that images are tagged with location information. Some newer cameras, including smartphones, are already equipped with GPS receivers. Other cameras may need additional GPS hardware to do this.

Even if your camera doesn't support this feature, there is an alternative method -- Ansel can match the Exif date/time in your image(s) to a separate GPX data tracking file created by a GPS tracker, that has recorded your movements. GPS trackers can be purchased as standalone handheld devices or you can install a GPS tracker app on your smartphone. Location tagging with GPS tracking data can be done using the [geotagging](../toolboxes/geotagging.md) module, in the lighttable and Map views.

## Center Map view

In the center of the Map view you will see a world map.

Map data is taken from open map sources on the internet. As such, new map data is only available if you are connected to the internet -- Ansel keeps a disk cache of previously loaded map data.

You can navigate within the map using your mouse. Left-click and drag to move the map. Use the scroll-wheel to zoom in and out.

On-screen controls and displays are available to help you find your way. A navigation area is located on top left of the map -- use this as an alternative to mouse-dragging and scrolling. The scale of the map is displayed at the bottom-left. At the bottom-right you can see the geographical coordinates for the center of the map.

Images that already have geo-location attributes in their metadata are displayed as small icons on the map. Images close to each other are grouped and a count of grouped images is displayed on the bottom-left corner.

In order to assign geo coordinates to an image, activate the [filmstrip](../toolboxes/filmstrip.md) on the lower panel (press Ctrl+F). You can assign a geo location to an image by dragging the image icon from the film-strip and positioning it on the map -- Ansel will record the new location (latitude and longitude ) as part of the image metadata. This data will be included in exported images.

In order to remove location data from an image simply drag it from the map and drop it onto the filmstrip.

Images close to each other are grouped under a single image group. You can use the  [map settings](../toolboxes/map-settings.md) module to control the grouping as needed. The number displayed on the bottom left of the thumbnail is the number of images inside the group. A white number means that all images in the group are at exactly the same location, whereas a yellow number means they are not. Use the mouse scroll wheel while hovering over a group of images to scroll through the thumbnails of the images in that group.

Normally, images in the center Map view have black borders. If an image is selected in the filmstrip, then the corresponding image on the map will be highlighted with a white border.

Click+drag to adjust the location of an image. Shift+click to move a complete group of images.

## Undo/Redo

All image movements in the Map view are recorded by Ansel. It is possible to undo or redo such changes to recover a previous state. Note that this undo/redo facility is unlimited while moving images but it is reset each time you leave the Map view.

Press Ctrl+Z to undo the last modification and Ctrl+Y to redo the last undone modification (if any).
---

# Mask contours : https://ansel.photos/en/doc/views/darkroom/masking-and-blending/masks/refinement-controls/

When a parametric or drawn mask is active, several additional sliders are shown which allow the mask to be further refined.

details threshold (_deprecated_)
: This control allows you to alter the opacity of the mask based on the amount of detail in the image. Use this slider to select either areas with lots of detail (positive values) or areas that are flat and lacking in detail (negative values). The default (zero) effectively bypasses details refinement. This is mostly useful to apply sharpening and blurring effects that ignore out-of-focus (bokeh) regions or to sharpen only blurry parts, preventing over-sharpening of in-focus regions.
{{< note >}}
In Darktable, the details mask is available only for RAW images. Ansel generalizes it to any kind of image.
{{< /note >}}

blurring radius
: Blurring the mask creates a softer transition between blended and unblended parts of an image and can be used to avoid artifacts. The blurring radius slider controls the radius of a gaussian blur applied to the final blend mask. The higher the radius, the stronger the blur (set to 0 for an unblurred mask). Mask blur is always applied after feathering if both kinds of mask adjustment are activated. This allows any resulting sharp edges or artifacts to be smoothed.

feathering guide
: Mask feathering smooths a drawn or parametric mask such that the mask's edges automatically align with the edges of features in the image. The smoothing is guided either by the module's input or output (before blending), and may happen before or after the mask blurring, depending on what is selected in the “feathering guide” combobox. Feathering is particularly sensitive to the choice of guide image when used with edge-modifying modules (modules for sharpening or blurring an image).
: - _output before blur_: feathering is guided using the _output_ image of the module and takes place _before_ the mask is blurred
: - _input before blur_: feathering is guided using the _input_ image of the module and takes place _before_ the mask is blurred
: - _output after blur_: feathering is guided using the _output_ image of the module and takes place _after_ the mask has been blurred
: - _input after blur_: feathering is guided using the _input_ image of the module and takes place _after_ the mask has been blurred

feathering radius
: Adjust the strength of the feathering effect. Feathering works best if the mask's edges already approximately match some edges in the guiding image. The larger the “feathering radius” the better the feathering algorithm can align the mask to more distant edges. If this radius is too large, however, the feathered mask may overshoot (cover regions that the user wants to exclude). Feathering is disabled when the feathering radius is set to 0.


feathering mask opacity
: The strength of the module's effect is determined by the mask's local opacity. Feathering and blurring the mask may reduce the opacity of the original mask. The “feathering mask opacity” slider allows you to readjust the mask opacity to compensate. If the mask opacity is decreased (negative slider values) less opaque parts are affected more strongly. Conversely, if the mask opacity is increased (positive slider values) more opaque parts are affected more strongly. As a consequence, completely opaque portions of the mask always remain opaque and completely transparent portions always remain transparent. This is to ensure that regions that have been fully excluded from or included in a module's effect (by setting the mask's opacity to 0% or 100%) remain fully excluded or included.

feathering mask contrast
: This slider increases or decreases the mask contrast. This allows you to adjust the transition between the opaque and transparent parts of the mask.

temporarily switch off mask (eye icon)
: Sometimes it is useful to visualize a module's effect without the mask being active. Click this icon to temporarily deactivate the mask (the selected blend mode and global opacity remain in effect).

display mask (mask icon)
: Click this icon to display the current mask as a yellow overlay over a black-and-white version of the image. Solid yellow indicates an opacity of 100%; a fully visible gray background image (without yellow overlay) indicates an opacity of 0%.

## Example: feathering a drawn mask

![feathering a rough drawn mask onto a subject, step by step](feathering.jpg)

It can be rather tedious to create a drawn mask that precisely covers a particular feature in an image — say, to enhance the color contrast of a foreground subject without affecting the background. Feathering does most of the work:

1. Start from a rough, fuzzy drawn shape that only approximately follows the subject's outline.
2. Raise the **feathering radius** (e.g. to 50) so the mask snaps to the nearest strong edges, and add a small **blurring radius** (e.g. 5) to smooth it.
3. Adjust the **feathering mask opacity** and **feathering mask contrast** (e.g. to 0.3 and 0.5) to tighten the transition.

The effect then stays restricted to the subject. Feathering works best when the subject is well separated from an out-of-focus background, because the distinct edge at its border guides the mask to match its shape.
---

# Masking & blending : https://ansel.photos/en/doc/views/darkroom/masking-and-blending/
Each processing module takes its input from the preceding module in the pixelpipe, performs its operation on the image data, and then hands the output to the next module in the pixelpipe.

A module's output data can optionally be reprocessed (combined) with its input data before being handed to the next module. This additional processing step is called _blending_ -- input and output data is reprocessed using algorithms called blending operators or [blend modes](./blend-modes.md).

Each blend mode is further controlled by the _opacity_ parameter (having a value between 0% and 100%) which defines how much the input and output images contribute to the final result. Typically an opacity of 0% outputs an image which is identical to the input image (the module has no effect) whereas an opacity of 100% delivers the maximum effect of the module.

This opacity can be the same for every pixel (using the global opacity slider), in which case blending acts uniformly over the entire image. Alternatively the opacity values can vary depending on the properties or location of each pixel. This local modification of opacity is called a _mask_. Masks provide the user with fine control over which parts of an image are affected by a module and to what extent. You may activate a [drawn mask](./masks/drawn.md), a [parametric mask](./masks/parametric.md), a [raster mask](./masks/raster.md), or **any combination of the three** (see [combining masks](#combining-masks) below).

Blending and masking functionality is controlled from a set of tabs at the bottom of each applicable module, one per masking method, each with its own _Enable_/_Disable_ toggle. The drawn shapes you create here are the same objects listed in the [mask manager](../../toolboxes/mask-manager.md) (the _Masking & Blending_ tool in the left panel), and can be reused across modules.

The available masking and blending methods are:

off
: Module output is passed to the next module in the pixelpipe without additional reprocessing. No further controls are displayed.

uniformly
: Input and output images are reprocessed uniformly with the chosen blend mode, where the amount of blending is controlled by a single opacity slider. Additional controls are displayed to allow the blend mode and opacity to be selected. The default is a blend mode of “normal” with an opacity of 100%.

[drawn mask](./masks/drawn.md)
: Reprocessing takes place with the chosen blend mode and an opacity based on pixel location as defined by one or more drawn shapes. Additional controls are displayed to allow mask elements to be drawn. If no mask elements are drawn then all pixels have the same opacity, as defined by the opacity slider.

[parametric mask](./masks/parametric.md)
: Reprocessing takes place with the chosen blend mode and an opacity based on the properties of individual pixels. Additional controls are displayed to allow the opacity to be adjusted on a per-pixel basis, determined by pixel values.

[raster mask](./masks/raster.md)
: Reprocessing takes place with the chosen blend mode and an opacity based on a mask that was generated by another module earlier in the pixelpipe.

blending options
: Choose which color space to use when calculating the blending mask, and specify whether or not to allow a mask to be generated based on the module's output channels (normally a parametric mask is generated based on the input channels coming into the module). The following options are available:
: - _reset to default blend colorspace_: Use the default color space for the module to specify the parametric mask.
: - _Lab_: Use the Lab color space (where available) to specify the parametric mask.
: - _RGB (display)_: Use the display-referred RGB/HSL color space to specify the parametric mask.
: - _RGB (scene)_: Use the scene-referred RGB/J<sub>z</sub>C<sub>z</sub>h<sub>z</sub> color space to specify the parametric mask.
: - _show output channels_: Show the [parametric mask](./masks/parametric.md) output channel controls, so that the parametric mask can be defined in terms of the module's output channels.

---

**Note:** Not all of these blending options are available for every module.

---

## Combining masks

Unlike Darktable, where you had to pick a single masking mode, Ansel exposes drawn, parametric and raster masking as **independent toggles that can be combined**. Each method has its own tab, and you enable as many as you need at once:

- A [drawn mask](./masks/drawn.md) and a [parametric mask](./masks/parametric.md) combine according to the _combine masks_ setting — see [combining drawn & parametric masks](./masks/drawn-and-parametric.md).
- A [raster mask](./masks/raster.md) can now be combined **on top of** a drawn and/or parametric mask: the raster mask is used as the base, and the drawn and parametric masks refine it further. In Darktable, a raster mask was mutually exclusive with the other mask types and could not be refined.

The final per-pixel opacity is the product of every active mask, multiplied by the global opacity slider. A pixel excluded by any one mask (opacity 0) stays excluded, so adding a mask on top can only ever _restrict_ the affected area, never extend it.
---

# Possible problems & solutions : https://ansel.photos/en/doc/preferences-settings/performance/opencl/problems-solutions/

Ansel will detect OpenCL run-time errors automatically. On detecting an error, it will then reprocess everything on the CPU. While this will slow down processing it should not affect the end result.

There can be various reasons why OpenCL might fail during the initialization phase. OpenCL depends on hardware requirements and on the presence of certain drivers and libraries. In addition all these have to fit in terms of maker, model and revision number. If anything does not fit (e.g. your graphics driver -- loaded as a kernel module -- does not match the version of your `libOpenCL.so`) OpenCL support will likely not be available.

In this case, the best thing to do is start Ansel from a console with `Ansel -d opencl`.

This will give additional debugging output about the initialization and use of OpenCL. First, if you find a line that starts with `[opencl_init] FINALLY ...` that should tell you whether OpenCL support is available for you or not. If initialization failed, look at the messages above for anything that reads like `could not be detected` or `could not be created`. Check if there is a hint about where it failed.

Here are a few cases that have been observed in the past:

- Ansel states that no OpenCL aware graphics card is detected or that the available memory on your GPU is too low and the device is discarded. In that case you might need to buy a new card if you really want OpenCL support.

- Ansel finds your `libOpenCL.so` but then tell you that it couldn't get a platform. NVIDIA drivers will often give error code -1001 in this case. This happens because `libOpenCL.so` is only a wrapper library. For the real work further vendor-specific libraries need to be loaded. This has failed for some reason. There is a structure of files in `/etc/OpenCL` on your system that `libOpenCL.so` consults to find these libraries. See if you can find something fishy in there and try to fix it. Often the required libraries cannot be found by your system's dynamic loader. Giving full path names might help.

- Ansel states that a context could not be created. This often indicates a version mismatch between the loaded graphics driver and libOpenCL. Check if you have left-over kernel modules or graphics libraries from an older installation and take appropriate action. When in doubt, perform a clean reinstall of your graphics driver. Sometimes, immediately after a driver update, the loaded kernel driver does not match the newly installed libraries. In this case reboot your system before trying again.

- Ansel crashes during startup. This can happen if your OpenCL setup is completely broken or if your driver/library contains a severe bug. If you can't fix it, you can still use Ansel with option `--disable-opencl`, which will skip the entire OpenCL initialization step.

- Ansel fails to compile its OpenCL source files at run-time. In this case you will see a number of error messages looking like typical compiler errors. This could indicate an incompatibility between your OpenCL implementation and Ansel's interpretation of the standard. In that case please raise an issue on [github](https://github.com/darktable-org/darktable/issues/new/choose) and we will try to assist. Please also report if you see significant differences between CPU and GPU processing of an image.

A few on-CPU implementations of OpenCL also exist, coming as drivers provided by INTEL or AMD. We have observed that they do not provide any speed gain versus our hand-optimized CPU code. Therefore Ansel simply discards these devices by default. This behavior can be changed by setting the configuration variable `opencl_use_cpu_devices` (in `$HOME/.config/Anselrc`) to `TRUE`.
---

# Other views : https://ansel.photos/en/doc/preferences-settings/other-views/

Enable optional views:

* [map](../views/map/)
* [slideshow](../views/slideshow/)
* [print](../views/print/)

Once you restart Ansel the enabled view(s) will be available in the *Ateliers* menu.

These views are all hidden by default to spare some computing resources.
---

# Import configuration from darktable : https://ansel.photos/en/doc/install/darktable/

Ansel is based on darktable 4.0 and configuration files of darktable 4.0.1 and prior can be seemlessly imported by Ansel, such that you recover your tags, styles, presets, and library of images.

{{< warning >}}
Importing darktable 4.2 database into Ansel will make you lose the settings of _filmic RGB_, _sigmoid_ and _highlights reconstruction_ modules for all edits using them. _Sigmoid_ is not and will never be supported into Ansel, and the nonsensical changes made to _filmic RGB_ in darktable 4.2 will never be ported to Ansel. All other modules should still work.
{{</ warning >}}

## Importing usual darktable configuration files

{{< note >}}
This section applies if you installed darktable from sources or pre-built packages, on Linux, Windows and Mac. It does __not__ apply if you installed darktable from Flatpak packages (_see the next section in this case_).
{{</ note >}}

The configuration folder of darktable is located by default at :

- `~/.config/darktable` for Mac OS and Linux
- `C:\%LOCALAPPDATA%\darktable` on Windows.

Ansel re-uses the same folder structure, where `darktable` is replaced by `ansel`. Since Ansel is based on darktable 4.0, you can import your darktable 4.0 and earlier configuration files by simply copying into `/ansel` configuration folder.

### Terminal

This works on Mac OS and Linux terminal : 

```bash
$ cp -R ~/.config/darktable ~/.config/ansel
$ mv ~/.config/ansel/darktablerc ~/.config/ansel/anselrc
```

### Graphical

If you are not comfortable with command lines or use Windows, use your file browser to copy-paste the following files from the darktable configuration folder to the Ansel's one :

- `library.db`,
- `data.db`,
- `shortcutsrc`,
- `darktablerc`, which will need to be renamed `anselrc` after copying.
- The `color` folder, if any, especially if some of your edits use custom-made ICC color profiles.

From there, you can continue to work just as before. Ansel can be installed alongside darktable.


## Importing configuration files from Flatpak

Flatpak packages work on recent Linux distributions only and are a hacky way of dealing with outdated packages on distributions repositories, but they create many other problems in the process.

Flatpak run in sandboxes and don't store their configuration files at the usual place but in ` ~/.var/app/org.darktable.Darktable/config/darktable`, where `~/` is the current user home folder.

From this Flatpak configuration folder, you will need to import the files mentionned in the previous section into `~/.config/ansel`. However, the current user will not be the owner of the copied files, which belong to the Flatpak user, and they are copied with their permissions retained. In practice, it means that the copied files will be read-only and Ansel will not be able to write into the database.

To fix the permissions, you have a console and a graphical method.

### Terminal

Run in terminal :

```bash
$ cp -R ~/.var/app/org.darktable.Darktable/config/darktable ~/.config/ansel
$ mv ~/.config/ansel/darktablerc ~/.config/ansel/anselrc
$ chown -R USER:USER ~/.config/ansel # may need sudo
$ chmod -R 644 ~/.config/ansel
```

where `USER` is your OS-wise username.

### Graphical

Use your file browser to navigate to `~/.config/`. Since it is an hidden file (as the `.` prefix suggests), you may need to display hidden files first to find it (the typical shortcut is <kbd>Ctrl + H</kbd>).

{{< note >}}
The following uses a generic logic. The actual wording of the labels depend on the file browser and desktop environment you are using and need to be adjusted.
{{</ note >}}

Once in `~/.config/`, right-click on the `ansel` folder and click on the _Properties_ entry of the contextual menu, then on the _Permissions_ tab. From there, set `read` and `write` permissions for the file owner and its group and check the option _Apply to subfolders and their content_.

Note that the graphical method does not allow you to change the file owner, which needs to be done in terminal with the command `chown -R USER:USER ~/.config/ansel`. If you don't know how to do it, and if you are not the owner of the folder, set `read` and `write` permissions to _Others_ too (which means all users of the system). __This is a security threat if you are not the only user of the computer since it will allow anybody to access and write changes in the folder__. It is ok if you are the only user of the computer or you trust the intents __and__ the skills of the other users (at home).


## Working with Ansel alongside darktable

Darktable and Ansel can store the images editing histories in [XMP sidecar files](../views/lighttable/digital-asset-management/sidecar.md) when the option is enabled. Since they both use the same XMP tags, the last application to have opened the image will overwrite its own history within the XMP. This is not a real issue for darktable 4.0 and Ansel 0.0 since their [pixel pipelines](../views/darkroom/pixelpipe/_index.md) are compatible, but the compatibility is broken between Ansel and darktable 4.2.

However, both applications load the editing histories first from their database. Then, if the _look for updated xmp files on startup_ is [enabled in the preferences](../preferences-settings/storage#xmp), they can crawl the image folders to detect if the XMP edits are more recent than the database ones. In that case, they will prompt a window asking if the files should be synchronized and in which direction. Never synchronize from XMP to database if you use both applications on the same files.
---

# Ansel-cmstest : https://ansel.photos/en/doc/cli/ansel-cmstest/

The `Ansel-cmstest` binary (Linux only) investigates whether the color management subsystem of your computer is correctly configured and displays some useful information about the installed monitor profile(s).

`Ansel-cmstest` is called without command line parameters.
---

# Metadata and tagging : https://ansel.photos/en/doc/views/lighttable/digital-asset-management/metadata-tagging/

Ansel allows you to store additional information about your images to allow them to be more easily searched and grouped. This information is stored in Ansel's database and XMP sidecar files and can also be included within exported images.

## Metadata

Metadata (e.g. title, description) is free-format text that usually differs for each image. You can add metadata to images in the [metadata editor](../../toolboxes/metadata-editor.md) module.

## Tagging

Tags are usually shared between multiple images and are used to categorise and group them. You can add tags to images in the [tagging](../../toolboxes/tagging.md) module.
---

# Raster masks : https://ansel.photos/en/doc/views/darkroom/masking-and-blending/masks/raster/

As described in the previous sections, the final output of a module's mask (the combined effect of any drawn and parameteric masks) is a grayscale raster image representing the extent to which the module's effect should be applied to each pixel. This raster image is stored internally for active modules and can be subsequently reused by other modules in the pixelpipe.

As with any mask, if the opacity value for a pixel in a raster mask is zero the module's input passed through the module unchanged. If the opacity is 1.0 the module has its full effect. For each value between 0 and 1.0 the module's effect is applied proportionally at that location.

Enable the raster mask from its tab and choose a source from the combobox. Raster masks can be identified by the name of the module against which they were originally generated.

## Combining with drawn and parametric masks

In Ansel, a raster mask is no longer a mutually-exclusive mode: it can be **combined with a [drawn](drawn.md) and/or [parametric](parametric.md) mask on top**. When you do, the raster mask serves as the **base**, and the drawn and parametric masks refine it — their opacities are multiplied together pixel-by-pixel. Because masks combine multiplicatively, adding a drawn or parametric mask on top of a raster mask can only restrict its area further, never extend it.

This lets you reuse a mask computed elsewhere in the pipeline (for instance a luminance or edge mask produced by another module) and then trim it to a region with a drawn shape, or restrict it by color with a parametric mask, without having to rebuild it from scratch. In Darktable, selecting a raster mask disabled the other mask types entirely.

---

**Note:** Raster masks are generated as part of a module's internal processing. Once a module's processing is complete its mask then becomes available to subsequent modules in the pixelpipe.

This has two implications:

1. Raster masks cannot be generated by disabled modules since they do not participate in pixelpipe processing. As soon as you disable a module, its mask is no longer available for use.

2. Raster masks are passed _up_ the pixelpipe after module processing -- they can only be used by modules that come later in the pipe than the generating module.

---
---

# Slideshow : https://ansel.photos/en/doc/views/slideshow/

The Slideshow view is optional and needs to be activated in [Preferences > Other views](../../preferences-settings/other-views.md). It allows you to watch a slideshow of your current collection with the associated filtering rules and sort order applied.

To learn more about how to define the collection and filtering rules, see the section on [collections](../../getting-started/library.md).

The next section provides more details on the [usage](usage.md) of the Slideshow view.
---

# Unbounded colors : https://ansel.photos/en/doc/color-management/unbounded-colors/

Screens and most image file formats can only encode RGB intensities confined within a certain range. For example, images encoded on 8 bits can only contain values from 0 to 255, images on 10 bits from 0 to 1023, and so on… Graphic standards postulate that the maximum of that range, no matter its actual value, will always represent the maximum brightness that the display medium is able to render, usually between 100 and 160 Cd/m² (or nits) depending on the actual standard. We generally call this maximum "100 % display-referred". The minimum of the range, encoded 0 no matter the bit-depth used, becomes then "0 % display-referred". 100 % encodes pure white, 0 % encodes pure black.

This is a limitation for image processing applications, because it means that any pixel lying outside of this range will be clipped to the nearest bound, resulting in non-recoverable loss of data (colors and/or textures).

For the longest time, image processing software too was bounded to this limitation for technical reasons, and some still is, but now by design choice. As a result, they would clip RGB intensities at 100 % display-referred between image operations.

Ansel uses floating-point arithmetic inside its color pipeline, which means it can handle any RGB value internally, even those outside the display-referred range, as long as it is positive. Only at the very end of the pipeline, before the image is saved to a file or sent to display, are the RGB values clipped if needed.

Pixels that can take values outside of the display range are said to have “unbounded colors”. One could choose to clamp (i.e. confine) those values to the allowed range at every processing step or choose to carry on with them, and clamp them only at the last step in the pipeline. However, it has been found that processing is less prone to artifacts if the unbounded colors are not clamped but treated just like any other color data.

At the end of the pipeline, modules like [_filmic_](../views/darkroom/modules/filmic.md) can help you to remap RGB values to the display-referred range while maximizing the data preservation and avoiding hard clipping, which is usually not visually pleasing.

However, at all times in the pipeline, you must ensure that you do not create negative RGB values. RGB intensities encode light emissions and negative light does not exist. Those modules that rely on a physical understanding of light to process pixels will fail if they encounter a non-physical light emission. For safety, negative RGB values are still clipped whenever they might make the algorithms fail, but the visual result might look degraded. Negative values can be produced when abusing the _black level_ in [_exposure_](../views/darkroom/modules/exposure.md) or the _offset_ in [_color balance_](../views/darkroom/modules/color-balance.md) and care should be taken when using these modules.
---

# Variables : https://ansel.photos/en/doc/variables/

Ansel supports variable substitution in a number of modules and preference settings. For example:

- Defining file names in the [export](./views/toolboxes/export.md) module and [import tool](./getting-started/import.md),
- Displaying image information in the darkroom's image information line,
- Displaying image information in the lighttable's overlays and tooltips (see [preferences > lighttable](./preferences-settings/lighttable.md))
- Placing text on an image in the [_watermark_](./views/darkroom/modules/watermark.md) processing module.

Most, if not all text entries that use variables implement auto-completion as soon as you start typing `$(`.

## Available variables

Here is an outdated, partial list of all available variables. They may not all be applicable in every context, and the real, up-to-date, list will be found in the auto-completion popup:

```
$(ROLL.NAME)               film roll of the input image
$(FILE.FOLDER)             folder containing the input image
$(FILE.NAME)               basename of the input image
$(FILE.EXTENSION)          extension of the input image
$(FILE.YEAR)               year of modification of the input image (read on the filesystem)
$(FILE.MONTH)              month of modification of the input image (read on the filesystem)
$(FILE.DAY)                day of modification of the input image (read on the filesystem)
$(FILE.HOUR)               hour of modification of the input image (read on the filesystem)
$(FILE.MINUTE)             minute of modification of the input image (read on the filesystem)
$(FILE.SECOND)             second of modification of the input image (read on the filesystem)
$(ID)                      the image id
$(VERSION)                 the duplicate version number
$(VERSION.IF_MULTI)        same as $(VERSION) but null string if only one version exists
$(VERSION.NAME)            version name from metadata
$(DARKTABLE.VERSION)       the version of the running Ansel instance
$(DARKTABLE.NAME)          name of Ansel
$(SEQUENCE)                a sequence number within an export job
$(WIDTH.SENSOR)            width of RAW data in pixels before RAW crop
$(HEIGHT.SENSOR)           height of RAW data in pixels before RAW crop
$(WIDTH.RAW)               width of RAW data in pixels after RAW crop
$(HEIGHT.RAW)              height of RAW data in pixels after RAW crop
$(WIDTH.CROP)              image width in pixels at the end of the pixelpipe, but before export resize
$(HEIGHT.CROP)             image height in pixels at the end of the pixelpipe, but before export resize
$(WIDTH.EXPORT)            image width in pixels at the end of the pixelpipe and after export resize
$(HEIGHT.EXPORT)           image height in pixels at the end of the pixelpipe and after export resize
$(WIDTH.MAX)               maximum width entered in export module
$(HEIGHT.MAX)              maximum height entered in export module
$(YEAR)                    year at date of import/export
$(MONTH)                   month at date of import/export
$(DAY)                     day at date of import/export
$(HOUR)                    hour at time of import/export
$(MINUTE)                  minute at time of import/export
$(SECOND)                  second at time of import/export
$(MSEC)                    millisecond at time of import/export
$(EXIF.YEAR)               Exif year
$(EXIF.MONTH)              Exif month
$(EXIF.DAY)                Exif day
$(EXIF.HOUR)               Exif hour
$(EXIF.MINUTE)             Exif minute
$(EXIF.SECOND)             Exif second
$(EXIF.MSEC)               Exif millisecond
$(EXIF.ISO)                Exif ISO value
$(EXIF.EXPOSURE)           Exif exposure
$(EXIF.EXPOSURE.BIAS)      Exif exposure bias
$(EXIF.APERTURE)           Exif aperture
$(EXIF.FOCAL.LENGTH)       Exif focal length
$(EXIF.FOCUS.DISTANCE)     Exif focus distance
$(LONGITUDE)               longitude
$(LATITUDE)                latitude
$(ELEVATION)               elevation
$(STARS)                   star rating (text only)
$(RATING.ICONS)            star rating (using star characters)
$(LABELS)                  colorlabels (color labels as text)
$(LABELS.ICONS)            colorlabels (color labels as icons)
$(MAKER)                   camera maker
$(MODEL)                   camera model
$(LENS)                    lens
$(TITLE)                   title from metadata
$(DESCRIPTION)             description from metadata
$(CREATOR)                 creator from metadata
$(PUBLISHER)               publisher from metadata
$(RIGHTS)                  rights from metadata
$(TAGS)                    tags list (Xmp.dc.Subject)
$(CATEGORYn(category))     tag name of level n [0,9] of selected category (or tag)
$(SIDECAR_TXT)             content of the text sidecar file (if any)
$(FOLDER.PICTURES)         pictures folder
$(FOLDER.HOME)             home folder
$(FOLDER.DESKTOP)          desktop folder
$(OPENCL.ACTIVATED)        whether OpenCL is activated
$(USERNAME)                user name defined by OS
$(NL)                      newline character
$(JOBCODE)                 internal jobcode of current job
```

## String substitution

All of the variables support basic string substitution inspired by bash though some of the details differ.

All patterns are treated as simple string comparisons. There is no regex support.

The following string replacement functions are provided, where `var` is one of the variables listed above:

```
$(var-default)                   If var is empty, return "default"
                                 It is possible to use another variable as "default", e.g.
                                 $(WIDTH.CROP-$(WIDTH.RAW))

$(var+alt_value)                 If var is set, return "alt_value" else return empty string

$(var:offset)                    Return var starting from offset
                                 If offset is negative count from the end of the string

$(var:offset:length)             Starting from offset, return at most length characters of var
                                 If offset is negative the length is counted from the end of var
                                 If length is negative this indicates the end of the result,
                                  counted from the end of var, and not an actual length

$(var#pattern)                   Remove "pattern" from the start of var

$(var%pattern)                   Remove "pattern" from the end of var

$(var/pattern/replacement)       Replace the first occurrence of "pattern" in var with "replacement"
                                 If "replacement" is empty then "pattern" will be removed

$(var//pattern/replacement)      Replace all occurrences of "pattern" in var with "replacement"
                                 If "replacement" is empty then "pattern" will be removed

$(var/#pattern/replacement)      If var starts with "pattern then "pattern" is replaced with "replacement"

$(var/%pattern/replacement)      If var ends with "pattern" then "pattern" is replaced with "replacement"

$(var^)                          Make the first character of var uppercase

$(var^^)                         Make all characters of var uppercase

$(var,)                          Make the first character of var lowercase

$(var,,)                         Make all characters of var lowercase
```

## Formatting

The image information patterns support [markup](https://docs.gtk.org/Pango/pango_markup.html). For example, adding the following will provide a clear warning (large, red, bold text) when OpenCL has failed to initialise:

`<span alpha='1%'>$(OPENCL_ACTIVATED/no/<span foreground='red' weight='heavy' size='xx-large' alpha='100%'>OPENCL ACTIVATION FAILED</span>$(NL))</span>`
---

# Data privacy : https://ansel.photos/en/doc/data-privacy/

Ansel is a desktop application that runs entirely on your computer. It needs **no account**, and by
default it sends **nothing** anywhere. The only data that ever leaves your machine is the *optional*,
*anonymous* information described on this page — and only after you have explicitly agreed to it.

This page documents, from your point of view, exactly **what** can be collected, **how**, **where**
it goes, **who** processes it and **what for**. The exact source code that collects and sends each
piece of data is linked at the bottom for independent audit.

## The short version

- Everything here is **opt-in**. On first launch Ansel shows a single dialog with one checkbox per
  data flow, both of which you can leave unticked.
- Ansel **never** sends your images, your file names, your folder paths, your metadata, your location,
  your name, your email, your IP-based identity, or anything that could identify you or your photos.
- You can change your mind at any time in **Preferences ▸ Storage ▸ Privacy**.
- If you build Ansel yourself, both features can be compiled out entirely.

{{< note >}}
The information collected is *anonymous and aggregate*: it describes the software and the computer it
runs on, not you and not your work. There is no account and no way to link a report back to a person.
{{< /note >}}

## The consent dialog

The first time you run Ansel with its interface, you are shown one dialog titled **"Help us improve
Ansel"**. It contains:

- a checkbox to enable **crash reports**,
- a checkbox to enable **anonymous usage statistics**,
- a link to this very page,
- a button to confirm your choice.

Whatever you choose is remembered, and the dialog is never shown again. Nothing is sent for any flow
whose box you leave unticked. If Ansel is started without its graphical interface (command-line
processing), no dialog can be shown and nothing is collected until you have made a choice in the
interface.

## The two data flows

Crash reporting and usage analytics are **independent**. They have separate checkboxes, separate
on/off switches in the preferences, go to **different** service providers, and can be enabled one
without the other.

### 1. Crash reports

**What for** — so that when Ansel crashes, the developers automatically receive the technical details
needed to find and fix the bug, instead of relying on you to notice, locate and attach a crash log.

**When** — only if a crash actually happens. A crash report is stored locally at the moment of the
crash and uploaded the **next** time you start Ansel.

**What is sent**

- the *backtrace* — the list of internal program functions that were running when Ansel failed
  (including, on Linux, a detailed `gdb` backtrace attached as a text file);
- your operating system and version;
- your hardware: number of CPU cores, amount of RAM, graphics card / OpenCL device name;
- on Linux: whether you use X11 or Wayland, and your desktop environment (GNOME, KDE, …);
- your screen resolution, the Ansel window size, and display scaling (DPI/PPD);
- the Ansel version, build type, and whether it is an official or self-built binary;
- how long the session ran before crashing, and counts of recent crash-free sessions;
- *context about the moment of the crash*: which views, panels and editing modules were in use during
  that session (by name and count), and the **kind** of image being processed — its file
  **extension** (e.g. `cr3`, `jpg`), whether it is a raw/non-raw/HDR/monochrome file, whether it
  still needs demosaicing, and its pixel dimensions (no filename, no path).

**Who and where** — crash reports are processed by [**Sentry**](https://sentry.io), on their
**European Union** (Germany) infrastructure. Sentry's privacy policy:
<https://sentry.io/privacy/>.

**Turn it off** — untick *Preferences ▸ Storage ▸ Privacy ▸ Send anonymous crash reports*.

### 2. Anonymous usage statistics

**What for** — so the developers can see which features and platforms are actually used, and focus
their limited time where it helps the most. A small open-source project otherwise has almost no
visibility into how its software is used.

**When** — a short message the first time you use a given tool or open a given kind of file during a
session, plus a summary at the start and (on a clean exit) end of the session. It records *that* a
tool was used or a kind of file was opened — never the order, exact timing, or content of what you
did.

**What is sent**

- a *random installation identifier* — a meaningless random number generated on your computer, used
  only to avoid counting the same installation many times. It is **not** derived from your hardware,
  account or network, and cannot identify you;
- your operating system, number of CPU cores, amount of RAM, graphics card, and (on Linux) display
  server and desktop environment;
- your screen resolution and display scaling;
- the Ansel version, build type, and whether it is an official or self-built binary;
- how long the session lasted;
- which views, panels and editing modules you used, **as counts only** (e.g. "exposure module
  enabled 4 times") — never in what order or at what time;
- the **kinds** of files you worked on: file **extensions** and broad type flags (raw vs non-raw,
  needing demosaicing or not) with counts — never file names, paths, contents or metadata.

**Who and where** — usage statistics are processed by [**PostHog**](https://posthog.com) on their
**European Union** infrastructure (`eu.i.posthog.com`). PostHog's privacy policy:
<https://posthog.com/privacy>.

**Turn it off** — untick *Preferences ▸ Storage ▸ Privacy ▸ Share anonymous usage statistics*.

## What is never collected

To be unambiguous, Ansel **never** transmits any of the following, under any setting:

- your photographs or any image data, thumbnails or previews;
- file names, folder names or paths;
- image metadata (EXIF, IPTC, GPS/location, capture dates, camera serial numbers, …);
- your name, email address, or any account information (there is no account);
- your editing history or the parameter values of your edits;
- the contents of your catalog/database, tags, ratings or comments;
- keystrokes, mouse movement, or screen captures.

## Changing or revoking your choice

Open **Preferences ▸ Storage ▸ Privacy**. There you will find the two independent switches:

- **Send anonymous crash reports**
- **Share anonymous usage statistics**

Changes take effect immediately for usage statistics and from the next launch for crash reporting.
Turning a switch off stops all corresponding transmission.

## For developers and auditors

Both features are free software, like the rest of Ansel, and can be reviewed line by line. The
relevant source files in the
[Ansel repository](https://github.com/aurelienpierreeng/ansel) are:

| What | File |
|---|---|
| The consent dialog and on/off logic | [`src/common/privacy_consent.c`](https://github.com/aurelienpierreeng/ansel/blob/master/src/common/privacy_consent.c) |
| Crash reporting: setup, what context is attached, what is sent | [`src/common/sentry.c`](https://github.com/aurelienpierreeng/ansel/blob/master/src/common/sentry.c) |
| Usage statistics: setup, payload, network sending | [`src/common/telemetry.c`](https://github.com/aurelienpierreeng/ansel/blob/master/src/common/telemetry.c) |
| Where view usage is recorded | [`src/views/view.c`](https://github.com/aurelienpierreeng/ansel/blob/master/src/views/view.c) |
| Where panel usage is recorded | [`src/libs/lib.c`](https://github.com/aurelienpierreeng/ansel/blob/master/src/libs/lib.c) |
| Where editing-module usage is recorded | [`src/develop/imageop.c`](https://github.com/aurelienpierreeng/ansel/blob/master/src/develop/imageop.c) |
| Where the processed file type is recorded | [`src/develop/pixelpipe_hb.c`](https://github.com/aurelienpierreeng/ansel/blob/master/src/develop/pixelpipe_hb.c) |

If you distribute your own build of Ansel, you can disable these features entirely at compile time
with the CMake options `-DUSE_SENTRY=OFF` (crash reporting) and `-DUSE_TELEMETRY=OFF` (usage
statistics), in which case the corresponding code is not even included in the binary.
---

# Possible color artifacts : https://ansel.photos/en/doc/color-management/color-artifacts/

There are some infrequent situations that still can lead to problematic results unless the user takes some action. Some modules in Lab color space rely on the fact that the L channels carries all lightness information, with the a and b channels purely representing chroma and hue. Unbounded colors with negative L values are especially problematic to these modules and can lead to black pixel artifacts.

It has been found that highly saturated blue light sources in the image frame are likely candidates for pixels with negative L values. If you are engaged in stage photography you should pay close attention to such lights appearing in images.

In order to mitigate this issue the [_input color profile_](../views/darkroom/modules/input-color-profile.md) module has a gamut clipping option. This option is switched off by default but can be activated if artifacts are observed. Depending on the settings, colors will be confined to one of the available RGB gamuts. In effect black pixel artifacts are prevented at the costs of losing some color dynamics.
---

# Print : https://ansel.photos/en/doc/views/print/

The Print view is optional and needs to be activated in [Preferences > Other views](../../preferences-settings/other-views.md). This view allows you to print your images. Because printing is not easy, there are many technical aspects to be taken into account.

After selecting an image in the [Lighttable view](../lighttable/_index.md) you can enter the [Print settings](../toolboxes/print-settings.md) module to adjust printer settings and initiate printing.

This module supports the printer's ICC profile, which is somewhat mandatory if you want to obtain a high quality print close to the image displayed on the screen.

It is important to note that ICC profiles provided by the paper and/or printer manufacturers cannot be used on GNU/Linux as they are printer-driver dependent. The Ansel print module uses CUPS and there are no ready-to-use ICC profiles available for this driver.
---

# Processing : https://ansel.photos/en/doc/preferences-settings/processing/

Control how images are processed.

## Image processing

always use LittleCMS 2 to apply output color profile
: If this option is activated, Ansel will use the LittleCMS 2 system library to apply the output color profile instead of its own internal routines. This is significantly slower than the default but might give more accurate results in some cases.

: If the given ICC is LUT-based or contains both a LUT and a matrix, Ansel will use LittleCMS 2 to render the colors regardless of this parameter's value (default off).

pixel interpolator (warp)
: The pixel interpolator used for rotation, lens correction, liquify, crop and final scaling.

: Whenever we scale or distort an image we have to choose a pixel interpolation algorithm (see [wikipedia](https://en.wikipedia.org/wiki/Image_scaling) for details). For warping modules, Ansel offers bilinear, bicubic or mitchell. In general, mitchell is better both at preserving sharpness and at preventing halos.

pixel interpolator (scaling)
: The pixel interpolator used for scaling. The same options are provided as for the warp modules.

: lanczos3 can cause pixel overshoots leading to artefacts but sometimes gives a more crisp visual appearance. This option is therefore only provided for transforming (scaling) algorithms and is the default value.

3D lut root folder
: Define the root folder (and sub-folders) containing Lut files used by the [_lut 3D_](../views/darkroom/modules/lut-3D.md) module


## CPU, GPU, Memory

### Settings

CPU cores
: Number of CPU cores (physical and virtual) in use. `-1` will use all detected cores (physical and virtual). Depending on your hardware, you may want to try setting this value equal to the number of physical cores of your CPU, which may speed-up memory I/O, since memory transfer usually is our bottleneck in image processing. This will be irrelevant for most users, aside from debugging issues. This value does not affect modules using OpenCL when OpenCL is enabled.

Background workers
: Number of background threads used to process thumbnail rendering pipelines, import, export, etc. Increasing this value will not increase memory usage proportionnaly, because only one pipeline at a time is allowed to run, but it may help hiding filesystem I/O latencies when loading image files. The maximum value here is defined by your operating system, and is typically 1024 or 2048.

Memory headroom for OS/applications (MiB)
: This is the amount of RAM space that Ansel will __never use__, and leave to the operating system and other applications. To set it, you can reboot your computer and open a system monitoring application, then measure how much RAM space the idle OS is using. If you like to play videos or music in background, while retouching in Ansel, you will need to plan for this too and include the RAM usage of your player into this headroom. __Setting this value too low will result in crashes of the application__ because the OS will kill the Ansel process when it reaches RAM saturation.

Memory reserved for lighttable thumbnail cache (MiB)
: This is the amount of RAM space that Ansel will allocate to rendered thumbnails displayed in the [lighttable](../views/lighttable/_index.md) and [filmstrip](../views/toolboxes/filmstrip.md). You should set this value according to the typical number of images you have in a folder and how large they appear in the lighttable :
    - A 360×225 px image uses 0.35 MiB,
    - A 720×450 px image uses 1.24 MiB,
    - A 1440×900 px image uses 5 MiB,
    - A 6000×4000 px image uses 92 MiB
: The default value of 1000 MiB can hold 2800 small thumbnails, or 805 medium thumbnails, or 200 large thumbnails, or 10 images at 24 Mpx full resolution (for full-resolution preview in lighttable).

Maximum RAW resolution to edit
: Set this value to the resolution of your best camera. Ansel will then reserve at all time a sufficient RAM space to hold 4 internal full-resolution buffers, which will be enough to avoid tiling in most modules using intermediate copies (when not using OpenCL).

{{< warning >}}
Ansel uses the total memory available on the system, subtracts the headroom size, the thumbnail cache size, the size of the 4 full-resolution RAW buffers and uses what remains for its pixelpipe cache. This cache is used to store the intermediate output of modules, as to prevent recomputing when not necessary. It will make Ansel RAM consumption increase a lot, which is no issue. The cache will be automatically shrunk if the application has trouble allocating new buffers. However, the operating system will not shrink it itself, so the _memory headroom_ needs to be properly set.
{{< /warning >}}

{{< note >}}
The pixel pipeline cache can be manually emptied using the global menu _Run_ → _Clear pipeline caches_.
{{< /note >}}

Pipe recompute timeout
: When doing value changes on sliders and comboxes, in darkroom [modules](../views/darkroom/modules/_index.md), the changes are sent to the pixel pipeline to re-render the output once every _N_ milliseconds. _N_ is the _pipe recompute timeout_. It will ensure that intermediate value changes (when dragging a cursor) don't trigger a new (computationnaly-expensive) recomputation, at the expense of making the GUI feel more or less _laggy_ or _responsive_. Powerful hardware can tolerate values as low as 50 ms, but weak hardware (or battery mode) may benefit from values as high as 200 ms to spare useless intermediate recomputations.

Enable disk backend for thumbnail cache
: Saves rendered [lighttable](../views/lighttable/_index.md) thumbnails to the disk cache, in the current user's home folder. This will prevent recomputing them on the next startup, but may create privacy issues if you are editing private pictures on a publicly shared computer.

Activate OpenCL support
: Process darkroom [modules](../views/darkroom/modules/_index.md) that support it on the GPU using OpenCL. This option will be available only if an OpenCL driver and device were found on your system.

GPU vRAM headroom (MiB)
: Amount of video RAM that Ansel will __never use__ on the GPU, and leave to other applications using hardware graphics acceleration (OpenGL, Vulkan, VDPAU, CUDA, etc.).

System library with OpenCL runtime
: Ansel will typically detect automatically the OpenCL drivers on your system, so you can leave this option empty. But if you have several conflicting OpenCL libraries, or your runtime is installed in an unusual place, you can define here the path to the OpenCL runtime to load.

### Testing your configuration

- To diagnose OpenCL issues, start `ansel -d opencl`
- To benchmark performance and test different memory configurations, start `ansel -d perf`,
- To diagnose memory and cache size issues, start `ansel -d memory -d cache -d pipe`

## Libraw

Ansel uses the Rawspeed library by default to decode raw image files. Rawspeed is flawlessly integrated in Ansel, but does not support Canon `.CR3` files yet. For this reason, a basic support of Libraw has been implemented such that owners of recent Canon cameras can still decode their files. Libraw also tends to support new formats faster than Rawspeed.

The options of this section allow users to force the use of Libraw for any picture they want, using rules based on file extension and camera/vendor EXIF metadata. The feature is brittle and unsafe in general because we don't check and sanitize every possible flavour of encoding.

Supported files
:  * Canon `.CR3`

Files that seem to be working
:   * Olympus `.ORF`
    * Hasselblad `.3FR`
    * Nikon `.NEF` __non-compressed__

Files that definitely don't work and make the software crash
:   * Nikon sRAW and compressed `.NEF`
    * Phase One `.IIQ`

{{< warning >}}
The library used to decode files is remembered at the application level, globally, not for each file. Editing pictures decoded by Libraw may not produce the exact same result if you revert to Rawspeed in the future. You should really stick to Rawspeed whenever possible.

You are strongly advised to save any edited picture to 16 bits TIFF at full resolution using Rec2020 linear color space as an archival backup of your work, and not rely on the consistency of the non-destructive editing result in the future.
{{</ warning >}}

Raw file extensions to load through Libraw
: case-insensitive, coma-separated list of the file extensions. Default : `cr3`.

Camera models to load through Libraw
: case-insensitive, coma-separated list of the camera models as they appear in the [_Display metadata_](../views/toolboxes/image-information.md) module, under the _model_ field. You may have to enable this field using the preferences of the module if it does not appear in the widget.

Camera makers to load through Libraw
: case-insensitive, coma-separated list of the camera manufacturers as they appear in the [_Display metadata_](../views/toolboxes/image-information.md) module, under the _maker_ field. You may have to enable this field using the preferences of the module if it does not appear in the widget.

{{< note >}}
For Canon files using the `.CR3` format, the _model_ and _maker_ metadata are not decoded properly and left blank. You have to filter them mandatorily by file extension.
{{</ note >}}

To debug this feature : 

1. Start Ansel in command line using `ansel -d imageio`. For each loaded image, it will tell which library was used to decode it,
1. If the settings you input make the software crash at startup, remove the `libraw/extensions`, `libraw/models`, `libraw/makers` configuration keys in the `anselrc` configuration file, located in `~./config/ansel` folder on Linux and Mac, or `APPDATA\.config\ansel` on Windows.
---

# Purge_non_existing_images.sh : https://ansel.photos/en/doc/cli/purge_non_existing_images_sh/

Find and remove entries from the library database referencing images that no longer exist in the filesystem. You must close Ansel before running this script.

The script can be called with the following command line parameters:

```
purge_non_existing_images.sh [-c|--configdir <path>]
                             [-l|--library <path>]
                             [-p|--purge]
```

Run the script with no options to perform a "dry run", which generates a report of the missing files without committing any changes to the database.

The available options are:

`-c|--configdir <path>`
: Specify the path to the Ansel configuration directory to be used by the script. If this option is not provided, the default config directory location will be used.

`-l|--library <path>`
: Specify the path to the library.db database file to be analysed by the script. If this option is not specified, the default library.db file location will be used.

`-p|--purge`
: Actually delete any entries in the database that refer to non-existent files. If the option is not provided, a report will be printed without committing any changes to the database.

---

**Notes:**

1. The script must be run in a unix shell, and the `sqlite3` client must be available in the command search path. For Linux systems, this will normally not be an issue.

2. For Windows systems, you will normally need the MSYS2 environment to be installed, as described in the [instructions for building Ansel in a Windows environment](../install/windows.md). If you installed Ansel using the standard Windows installer package, the location of the script would normally be something like: `C:\Program Files\Ansel\share\Ansel\tools\purge_non_existing_images.sh`.

3. For macOS systems, the Terminal application provides a shell, and the `sqlite3` client is provided by the operating system by default. If Ansel was installed using an application bundle from a dmg image, then the default location for the script would be `/Applications/Ansel.app/Contents/Resources/share/Ansel/tools/purge_non_existing_images.sh`

4. The delete operation can't be undone. It is therefore strongly recommended that you take a backup of the database before purging any entries.

---
---

# Ansel's color dimensions : https://ansel.photos/en/doc/color-management/color-dimensions/

This section defines the perceptual properties of color, both conceptually and quantitatively, in order to characterize and quantify the creative and corrective adjustments made to color in Ansel.

## Definitions

Color properties like "saturation", "brightness" or "lightness" have passed into common usage but are largely misused and often used to mean different things. In color science, each of these terms has a precise meaning.

There are two frameworks within which color properties may be analyzed and described:

- A scene-linear, physiological, framework, that mostly focuses on the response of the retina cone cells, using color spaces such as CIE XYZ 1931 or CIE LMS 2006,
- A perceptual, psychological, framework that stacks the brain's corrections on top of the retina signal, using color spaces such as CIE Lab 1976, CIE Luv 1976, CIE CAM 2016 and JzAzBz (2017).

These two frameworks provide us with metrics and dimensions to analyze color and allow us to change some of its properties while preserving others.

The following dimensions of color are used by Ansel:

hue
: An attribute of visual perception in which an area appears to be similar to one of the colors red, yellow, green, or blue, or to a combination of adjacent pairs of these colors considered in a closed ring. [^1] Hue is a shared property between the perceptual and scene-linear frameworks.

luminance
: The density of luminous intensity with respect to a projected area in a specified direction at a specified point on a real or imaginary surface. [^2] Luminance is a property of scene-referred frameworks, and is expressed by the Y channel of the CIE XYZ 1931 space.

brightness
: An attribute of visual perception according to which an area appears to emit, transmit or reflect more or less light. [^4]

lightness
: The brightness of an area judged relative to the brightness of a similarly illuminated area that appears to be white or highly transmitting. [^3] Lightness is the perceptual, non-linear homologue of luminance (roughly equal to the cubic root of luminance Y). Lightness is expressed by the L channel in CIE Lab and Luv 1976 and the J channel in JzAzBz.

chroma
: The colorfulness of an area judged as a proportion of the brightness of a similarly illuminated area that appears gray, white or highly transmitting. [^5] _Warning: chroma is not short for chrominance, which is the color part of a video signal (the Cb and Cr channels in YCbCr, for example)._

brilliance
: The brightness of an area judged relative to the brightness of its surroundings. [^6]

saturation
: The colorfulness of an area judged in proportion to its brightness. [^7]

[^1]: CIE definition of _hue_: <https://cie.co.at/eilvterm/17-22-067>
[^2]: CIE definition of _luminance_: <https://cie.co.at/eilvterm/17-21-050>
[^3]: CIE definition of _brightness_: <https://cie.co.at/eilvterm/17-22-063>
[^4]: CIE definition of _lightness_: <https://cie.co.at/eilvterm/17-22-059>
[^5]: CIE definition of _chroma_: <https://cie.co.at/eilvterm/17-22-074>
[^6]: Article about _brilliance_ (paywall): <https://doi.org/10.1002/col.20128>
[^7]: CIE definition of _saturation_: <https://cie.co.at/eilvterm/17-22-073>

Colors can be described in many different color spaces, but no matter the color space, each color needs at least 3 components: some metric of luminance or brightness, and 2 metrics of chromaticity (hue and chroma, or opponent color coordinates).

## Illustrations

While the previous definitions are useful to give a meaning to the words, they don't tell us what we should be looking at. The following charts show luminance, lightness, chroma, brilliance/brightness and saturation varying from a "0" base color and how the resulting colors degrade:

![swatches](swatches.jpg)

(Lightness + Chroma) or (Brilliance/Brightness + Saturation) are two different ways to encode the same reality. They are orthogonal spaces that can be converted from one to another by a simple rotation of the base. This means that chroma evolves at constant lightness, saturation evolves at constant brilliance/brightness, and vice versa:

![lightness and chroma to brilliance and saturation](lightness-chroma-to-brilliance-saturation.jpg)

Lines of equal chroma are vertical (following the patches grid), meaning that chroma has the same direction for all colors in the gamut (see below). However, lines of equal saturation are oblique (drawn dashed on the graph) and all go from black through each color patch, meaning that their directions are particular to each color.

Increasing the chroma will therefore move all colors uniformly away from the central gray axis horizontally, while increasing the saturation will close or open the angle of the oblique dashed lines like a flower.

Similarly, increasing the lightness will move all colors uniformly up from the horizontal axis, while increasing brilliance/brightness will move them along the lines of equal saturation.

On both of the above charts, lightness, chroma, saturation and brilliance are drawn in the JzAzBz color space, which is a perceptual color space suited for an HDR signal, and is used in parametric masks and the color balance RGB module. Luminance is drawn in the CIE XYZ 1931 color space, and represents the effect of an exposure compensation. It shows the same behaviour as brilliance, except that the step size is not perceptually scaled.

---

**Note**: In this section, _brilliance_ and _brightness_ are both used to describe the same dimension. In all rigor, brightness is an absolute metric, whereas brilliance is the brightness of some surface relative to the brightness of its surroundings (that is, how much a surface "pops" out of its surroundings and looks fluorescent). But in image editing, increasing the brightness of some surface will indeed increase its brilliance too, so the term _brilliance_ is preferred in Ansel's user interface for clarity and in reference to its visual effect.

---

## Color dimensions and gamut

The gamut is the volume of colors that a certain color space can encompass and encode. It is important to note that, once converted to perceptual spaces, the gamut of any RGB space is not uniform along hues.

The following examples show the gamut volume of the sRGB space on hue slices containing the primary red, green and blue lights of the sRGB space, over a lightness-chroma plane with a uniform scale:

![sRGB-red](sRGB-red.jpg)
![sRGB-green](sRGB-green.jpg)
![sRGB-blue](sRGB-blue.jpg)

This shows that increasing the chroma (displacement over the horizontal axis) of some quantity can be safe for some hues at some lightness, but can push other hue-lightness coordinates way out of gamut. For example, we have much more margin in green or magenta than in cyan.

Many gamut issues at export are actually user-induced and the result of harsh chroma enlarging. For that reason, using brightness-saturation color models may be safer.

## Color dimensions and complementary colors

Cyan, magenta, yellow (CMY) are complementary colors of red, green, blue (RGB). However, the complementary CMY spaces computed from RGB spaces are not perceptually complementary. To show this, we create a CMY space from sRGB, where cyan has sRGB coordinates (0, 1, 1), magenta (1, 0, 1) and yellow (1, 1, 0), and display it in a lightness-chroma space:

![sRGB-cyan](sRGB-cyan.jpg)
![sRGB-magenta](sRGB-magenta.jpg)
![sRGB-yellow](sRGB-yellow.jpg)

By comparing with the hue slices of the primary colors in the previous section, it is easy to see not only that the gamuts don't have the same shapes, but that the colors do not match.

This is one more reason to avoid using HSL/HSV spaces (derived from RGB spaces) to perform color editing: since these RGB spaces are not perceptually uniform in the first place, the resulting HSV/HSL spaces are not uniform either. While RGB spaces have some merit based on their connection to physical light, any process involving hue should go directly to perceptual spaces.

## Color dimensions and settings

Many applications, including Ansel, call any settings that affect chroma "saturation" (for example, in color balance, "contrast/brightness/saturation"). This is a symptom of software trying to be accessible to non-professionals by using a common language. This is misleading, since saturation does exist and is quite different from chroma. In addition, many video specifications improperly call chroma "saturation". Whenever Ansel reuses such specifications, it uses the incorrect term from the specification rather than the proper color dimension term.
---

# Scheduling profile : https://ansel.photos/en/doc/preferences-settings/performance/opencl/scheduling-profile/

Ansel can use the CPU and one or several OpenCL capable GPUs. Depending on the relative performance of these devices, users can choose among certain scheduling profiles to optimize performance. This is achieved by setting the configuration parameter [Preferences > Processing > CPU, GPU, Memory > OpenCL scheduling profile](../../processing#cpu-gpu-memory), which offers the following choices:

default
: If an OpenCL-capable GPU is found Ansel uses it for processing the center image view while the [navigation preview window](../../../views/toolboxes/navigation.md) is processed on the CPU in parallel. This is the preferred setting for systems with a reasonably fast CPU and a moderately fast GPU. The exact allocation of devices to the various pixelpipe types can be finetuned with the “opencl\_device\_priority” configuration parameter (see [multiple devices](./multiple-devices.md)).

very fast GPU
: With this scheduling profile Ansel processes the center image view and the preview window on the GPU sequentially. This is the preferred setting for systems with a GPU that strongly outperforms the CPU.

multiple GPUs
: This setting addresses systems with multiple GPUs whose relative performance does not differ significantly. Whenever a processing job is started Ansel uses any currently idle GPU but not the CPU. Users of systems with a variety of GPUs will need better control on their relative priority. They would be better off selecting the “default” profile and fine-tuning their system with the “opencl\_device\_priority” configuration parameter (see [multiple devices](./multiple-devices.md)).

On first start-up or after any detected change in the GPU configuration of your system Ansel tries to identify the best suited profile for you. You can change it at any time in [Preferences > Processing > CPU, GPU, Memory](../../processing.md#cpu-gpu-memory).
---

# Security : https://ansel.photos/en/doc/preferences-settings/security/

Control whether warning messages are shown before undertaking certain activities.

## General

ask before removing images from the library
: Always ask before removing image information from Ansel's library database, where the xmp file is retained (default on).

ask before deleting images from disk
: Always ask before deleting an image file (default on).

ask before discarding history stack
: Always ask before discarding the history stack of an image (default on).

try to use trash when deleting images
: Instead of physically deleting images from disk, attempt to put them into the system's trash bin (default on).

ask before moving images from film roll folder
: Always ask before moving an image file (default on).

ask before copying images to new film roll folder
: Always ask before copying an image file to a new location (default on).

ask before removing empty folders
: Always ask before removing any empty folder. This can happen after moving or deleting images (default off).

ask before deleting a tag
: Always ask before deleting a tag from an image (default on).

ask before deleting a style
: Always ask before deleting a style (default on).

ask before deleting a preset
: Always ask before deleting a preset (default on).

ask before exporting in overwrite mode
: Always ask before [exporting](../views/toolboxes/export.md) images in overwrite mode.

## Other

password storage backend to use
: The backend to use for password storage. Options: “auto” (default), “none”, “libsecret”, “kwallet”.

executable for playing audio files
: Define an external program which is used in the lighttable view to play audio files that some cameras record to keep notes for images (default “aplay”).
---

# Multiple devices : https://ansel.photos/en/doc/preferences-settings/performance/opencl/multiple-devices/

The scheduling of OpenCL devices can be optimized on most systems using the “OpenCL scheduling profile” settings. However, if your system is equipped with more than one GPU, you might want to set the relative device priority manually. To do this you need to select the “default” scheduling profile and change the settings in the “opencl\_device\_priority” configuration parameter.

It is important to understand how Ansel uses OpenCL devices. Each processing sequence of an image -- to convert an input to the final output using a history stack -- is run in a pixelpipe. There are four different types of pixelpipe in Ansel. One type is responsible for processing the center image view (or full view) in darkroom mode, another pixelpipe processes the preview image (navigation window). There can be one of each of these two pixelpipes running at any time, with the full and preview pixelpipes running in parallel. In addition there can be multiple parallel pixelpipes performing file exports as well as multiple parallel pixelpipes generating thumbnails. If an OpenCL device is available, Ansel dynamically allocates it to one specific pixelpipe for one run and releases it afterwards.

The computational demand varies significantly depending on the type of pixelpipe being executed. The preview image and thumbnails are low resolution and can be processed quickly, whereas processing the center image view is more demanding. A full export pixelpipe is more demanding still.

The configuration parameter “opencl\_device\_priority” holds a string with the following structure: `a,b,c.../k,l,m.../o,p,q.../x,y,z...`. Each letter represents one specific OpenCL device. There are four fields in the parameter string separated by a slash, each representing one type of pixelpipe. `a,b,c...` defines the devices that are allowed to process the center image (full) pixelpipe. Likewise devices `k,l,m...` can process the preview pixelpipe, devices `o,p,q...` the export pixelpipes and finally devices `x,y,z...` the thumbnail pixelpipes. An empty field means that no OpenCL device may serve this type of pixelpipe.

Ansel has an internal numbering system, whereby the first available OpenCL device receives the number 0. All further devices are numbered consecutively. This number, together with the device name, is displayed when you start Ansel with `Ansel -d opencl`. You can specify a device either by number or by name (upper/lower case and whitespace do not matter). If you have more than one device with the same name you need to use the device numbers in order to differentiate them.

A device specifier can be prefixed with an exclamation mark `!`, in which case the device is excluded from processing a given pixelpipe. You can also use an asterisk `*` as a wildcard, representing all devices not previously explicitly mentioned in that group.

Sequence order within a group matters -- Ansel will read the list from left to right and whenever it tries to allocate an OpenCL device to a pixelpipe it will scan the devices in that order, taking the first free device it finds.

If a pixelpipe process is about to be started and all GPUs in the corresponding group are busy, Ansel automatically processes the image on the CPU by default. You can enforce GPU processing by prefixing the list of allowed GPUs with a plus sign `+`. In this case Ansel will not use the CPU but rather suspend processing until the next permitted OpenCL device is available.

Ansel's default setting for “opencl\_device\_priority” is `*/!0,*/*/*`.

Any detected OpenCL device is permitted to process the center view image. The first OpenCL device (0) is not permitted to process the preview pixelpipe. As a consequence, if there is only one GPU available on your system, the preview pixelpipe will always be processed on the CPU, keeping your single GPU exclusively for the more demanding center image view. This is a reasonable setting for most systems. No such restrictions apply to export and thumbnail pixelpipes.

The default is a good choice if you have only one device. If you have several devices it forms a reasonable starting point. However, as your devices might have quite different levels of processing power, it makes sense to invest some time optimizing your priority list.

Here is an example. Let's assume we have a system with two devices, a fast Radeon HD7950 and an older and slower GeForce GTS450. Ansel (started with `Ansel -d opencl`) will report the following devices:

```
[opencl_init] successfully initialized.
[opencl_init] here are the internal numbers and names of
                          OpenCL devices available to Ansel:
[opencl_init]           0       'GeForce GTS 450'
[opencl_init]           1       'Tahiti'
[opencl_init] FINALLY: opencl is AVAILABLE on this system.
```

Here, the GeForce GTS 450 is detected as the first device and the Radeon HD7950 ('Tahiti') as the second. This order will normally not change unless the hardware or driver configuration is modified, but it's better to use device names rather than numbers to be on the safe side.

As the GTS450 is slower than the HD7950, an optimized "opencl\_device\_priority" could look like: `!GeForce GTS450,*/!Tahiti,*/Tahiti,*/Tahiti,*`.

The GTS450 is explicitly excluded from processing the center image pixelpipe; this is reserved to “all” other devices (i.e. the HD7950/Tahiti). Conversely, for the preview pixelpipe, the Tahiti is excluded, so that only the GTS450 is permitted to do the work.

For file export and thumbnail generation we want all hands on deck. However, Ansel should first check whether the Tahiti device is free, because it's faster. If it is not free, then all other devices -- in fact only the GTS450 -- are checked.
---

# Preferences & Settings : https://ansel.photos/en/doc/preferences-settings/


Ansel comes with a number of user-configurable preferences. These can be adjusted in the preferences dialog, which can be reached via *Edit > Preferences* in the global menu. Preferences are separated into tabs, each of which is described in more detail in the following sections.

Any altered settings (i.e. those that differ from their default state) are highlighted with a bullet beside them. If you change a setting that needs a restart to take effect, a message will appear as a reminder after you exit the preferences dialog.

Double click on a setting's label to reset to its default value.

The preferences dialog can be closed by clicking on the close button in your window manager or pressing the Escape key.
---

# Color management : https://ansel.photos/en/doc/color-management/


Ansel employs a fully color managed workflow:

- Input color specifications are taken from embedded or user-supplied ICC profiles or (in the case of raw files) from a library of camera-specific color matrices.

- Ansel automatically reads the display profile of your monitor (if properly configured) for accurate on-screen color rendition. Multiple-screen setups are fully supported as long as a system service like colord is in place and properly configured to inform Ansel of the correct monitor profile.

- Output files can be encoded in one of Ansel's built-in profiles, including sRGB and Adobe RGB, or into a color space supplied by the user as an ICC profile.
---

# Performance : https://ansel.photos/en/doc/preferences-settings/performance/
---

# Debugging : https://ansel.photos/en/doc/install/debug/


Ansel is written in the C language. While C __pretends__ to be portable (_runs the same on all operating systems_), many little things need to be contextually adapted to each OS using slightly different code. Also, depending on hardware and on the version of the dependencies, even on Linux, some discrepancies can occur between distributions.

That is to say that bugs are almost impossible to anticipate when programming, and the default state of a computer program is buggy. Reporting bugs properly is the only way to help cleaning up the unforseen pathological cases.

## What is needed to debug ?

4 things :

* The sequence of user operations that lead to the bug (doing this, clicking that, etc.),
* The nature of the bug (segmentation fault at line xxx, crash with no warning),
* The hardware and operating system used, OpenCL driver if any, Linux kernel version, etc.
* The exact version of the software used, that is the full name of the `.exe` Windows package or `.AppImage` Linux package, or the full version string that can be found in the popup by clicking the Ansel logo, in the top panel of the software.

## Unexpected crashes

When Ansel crashes unexpectectdly, an `ansel_bt_xxxxxx.txt` file (where `xxxxxx` is an hexadecimal identifier) is usually written in the `/tmp` directory on Linux, or in `$USER\appData\local\temp\` on Windows. When you are lucky, it will contain the exact line of the exact file of the sourcecode responsible for the crash. This needs to be reported to the developer.

If the file is empty, please move on to the next section.

## Forced crash

When you note a reproduceable crash and are lucky enough to be on Linux, you can force it to crash with better feedback by running the program within `gdb` (_it is typically already installed on most distributions, otherwise find the package providing it on yours and install it_). `gdb` will usually provide more information than the backtrace file, and may also record backtraces in situations where the above method produces an empty file.

1. Start a `gdb` session, in terminal :
   * If using a self-built version, run `gdb /opt/ansel/bin/ansel`
   * If using an AppImage, run `gdb /path-to-appimage/Ansel-xxxxx-gxxxx-x86_64.AppImage`
2. Start the software with the `run` command, from the same terminal, and reproduce your faulty sequence of operations,
3. When it crashes, run `bt full > ansel_bt.log` (`bt` is for backtrace), and send the `ansel_bt.log` file just created in your home folder to the developer.

## Typical bugs

* When there is no space left on the hard drive hosting your home folder, Ansel will crash fair and square at startup with a segmentation fault.
* When your system is out-of-memory (RAM) or Ansel tries to reserve more memory than what is still available, the OS will kill Ansel fair and square, without backtrace or error message. This typically happens when processing heavy editings. It can be fixed to some extent by reducing the resources used by Ansel, in the [preferences](../preferences-settings/processing.md).

## Warnings

When starting the program in terminal, you will usually see a list of warnings linked to Gtk, like:

```bash
Gtk-WARNING **: 19:22:37.893: Theme parsing error: gtk.css:123:31: The :insensitive pseudo-class is deprecated. Use :disabled instead.
Gtk-Message: 17:18:35.117: Failed to load module  "canberra-gtk-module"
```

These can be ignored.
---

# Opencl still does not run for me : https://ansel.photos/en/doc/preferences-settings/performance/opencl/still-doesnt-work/

As has been mentioned, OpenCL systems come with a huge variety of setups: different GPU manufacturers and models, varying amounts of GPU memory, different drivers, different distributions etc..

Many of the potential problems will only appear with very specific combinations of these factors. As the Ansel developers only have access to a small fraction of these variations, please understand that we might not be able to fix your specific problem. There is not much we can do if we are unable to reproduce your issue.

If you are having issues with a specific device (and you have other OpenCL devices available) the first thing to do would be to try disabling just that device -- see [memory & performance tuning](../mem-performance.md#device-specific-opencl-configuration) for more information.

If nothing else helps, the best option is probably to start Ansel with

`Ansel --disable-opencl`

In the end there is nothing in Ansel that only runs on GPU. Don't let the lack of OpenCL discourage you -- Ansel's CPU code is also highly optimized for performance!
---

# Storage : https://ansel.photos/en/doc/preferences-settings/storage/


## Database

check for database maintenance
: Indicates when darktable should check for database fragmentation and perform maintenance. Options are "never", "on startup", "on close" and "on both". Each of these is also available with an additional "(don't ask)" option to perform the checks automatically without prompting (default "on close").

database fragmentation ratio threshold
: Fragmentation ratio (in per cent) above which database maintenance should be performed (subject to the selection made in the option above) (default 25).

create database snapshot
: Specifies how often Ansel should create database snapshots. Options are "never", "once a month", "once a week", "once a day" and "on close" (default "once a week")

how many snapshots to keep
: Number of snapshots to keep after creating a new snapshot, not counting database backups taken when moving between Ansel versions. Enter "-1" to store an unlimited number of snapshots. (default 10)

## Xmp

write sidecar file for each image
: XMP files provide a redundant method of saving the changes that you have made to an image, in addition to the changes saved to Ansel's database. This option allows you to choose when to write these files. Choose from:

: - _never_: Don't write XMP files. This can be useful if you are running multiple version of Ansel for development/testing purposes but is not normally recommended
: - _on import_: An XMP file will be written as soon as you add an image to Ansel's library and will be subsequently updated each time you edit it
: - _after edit_: An XMP file will be written the first time you perform an edit on an image and will be updated on each subsequent edit. Importing will not generate an XMP file by default.

: It's strongly recommended that you choose either "on import" or "after edit". XMP files provide a useful fail-safe so that you don't lose data if your database becomes corrupted. Backing up your raw file plus the accompanying XMP file will allow you to fully restore your work at a later date by re-importing your edit history back into Ansel (default "on import").

store xmp tags in compressed format
: Entries in XMP tags can get rather large and may exceed the available space to store the history stack in some output files on export. This option allows binary XMP tags to be compressed in order to save space. Available options are “never”, “always”, and “only large entries” (default).

look for updated xmp files on startup
: Scan all previously-known XMP files on startup and check if any have been updated in the meantime by some other software. If updated XMP files are found, a menu is opened for the user to choose if the XMP content should be used to update the database or the other way around (replacing Ansel's database entries by the XMP file contents). Activating this option also causes Ansel to check for text sidecar files that have been added after import time (default off). This makes the application take more time to load, especially if your files are hosted remotely (on a server or on a NAS).
---

# Miscellaneous : https://ansel.photos/en/doc/preferences-settings/miscellaneous/

## Interface

load default shortcuts at startup
: When launching the application, Ansel loads default shortcuts first, and then loads user-defined shortcuts on top. This allows default shortcuts to be overridden with a new action but prevents them from being deleted (since the deleted shortcut will be automatically reloaded on the next restart). Deactivate this preference to stop loading default shortcuts on startup -- only load the user-defined ones (including any defaults that you have not subsequently deleted or overridden). This makes deletion easier but also means that you will not benefit from new shortcuts added in future versions without first re-enabling this preference (default on).

scale slider step with min/max
: When activated, the default step-size, when altering sliders, will depend on the current min/max values for that slider (default on).

sort built-in presets first
: Choose how the presets menu is sorted. If this option is enabled, built-in presets are shown first. If the option is disabled, user presets are shown first (default on).

always show panels' scrollbars
: Defines whether the panel scrollbars should be always visible or activated only depending on the panel content (default on). (needs a restart)

position of the scopes module
: Choose whether to show the [scopes](../views/toolboxes/scopes.md) module in the left or right panel (default right). (needs a restart)

method to use for getting the display profile
: This option allows the user to force Ansel to use a specific method to obtain the current display profile for [color management](../color-management/_index.md). In the default setting “all”, Ansel will choose to query the X display server's xatom or the colord system service. You can set this option to “xatom” or “colord” to enforce a specific method if the two methods give different results. You can run the [Ansel-cmstest](../cli/Ansel-cmstest.md) binary to examine your color management subsystem.

## Tags

omit hierarchy in simple tag lists
: When exporting images any hierarchical tags are also added as a simple list of non-hierarchical tags to make them visible to some other programs. When this option is checked Ansel will only include the last part of the hierarchy and ignore the rest. So `foo|bar|baz` will only add `baz`.

## Shortcuts with multiple instances

It is possible to create multiple instances of many processing modules. In this scenario it is not always obvious which instance should be controlled by keyboard shortcut operations. The following options control rules that are applied (in order) to decide which module instance keyboard shortcuts should be applied to.

These rules are also used when clicking and dragging on the scopes module to change exposure.

prefer focused instance
: If an instance has focus, apply the shortcut to that instance and ignore any other rules. Note that this option does not impact blending shortcuts, which are always applied to the focused instance (default on).

prefer expanded instances
: If instances of the module are expanded, ignore collapsed instances (default off).

prefer enabled instances
: After applying the above rule, if remaining instances of the module are active, ignore inactive instances (default off).

prefer unmasked instances
: After applying the above rules, if remaining instances of the module are unmasked, ignore masked instances (default off).

selection order
: After applying the above rules, apply the shortcut to the first or last instance remaining (default "last instance").

## Map / geolocalisation view

pretty print the image location
: Show a more readable representation of the geo-location in the [image information](../views/toolboxes/image-information.md) module (default on).

## Slideshow view

waiting time before each picture in slideshow
: The number of seconds before displaying the next image (default 5) in the [slideshow view](../views/slideshow/_index.md).
---

# Shortcuts : https://ansel.photos/en/doc/preferences-settings/shortcuts/
Some actions in Ansel can be performed with a keyboard shortcut. A table of key shortcuts is available in _Help > Table of key shortcuts_.

**Note:** The table shows different key shortcuts for different Views.

## Assigning key shortcuts

For the time being shortcuts can only be defined or changed by editing your `$HOME/.config/Ansel/shortcutsrc` file. There is no support for MIDI-devices in Ansel.

### Deleting default shortcuts

When launching the application, Ansel loads default shortcuts first, and then loads user-defined shortcuts on top. This allows default shortcuts to be overridden with a new action but prevents them from being deleted (since the deleted shortcut will be automatically reloaded on the next restart).

There are two ways to delete default shortcuts:

#### Prevent default shortcuts from being reloaded

Disable [Preferences > Miscellaneous > Interface > Load default shortcuts at startup](./miscellaneous.md) to prevent default shortcuts from being reloaded. While this option is disabled, Ansel will only load user-defined shortcuts and any defaults that you have not subsequently deleted or overridden.

#### Override default shortcut with a no-op action

You can _override_ the action of a default shortcut by assigning an identical shortcut to the "global/no-op" action (which does nothing). You can do this by directly editing your `$HOME/.config/Ansel/shortcutsrc` file (you must exit Ansel first). For example, the following default shortcuts are defined in `shortcutsrc` for switching views in Ansel:

```
Return=global/switch views/darkroom
Escape=global/Ateliers/Lighttable
```

You could disable these shortcuts by changing `shortcutsrc` as follows:

```
Return=global/no-op
Escape=global/no-op
```

## Actions

**Note:** The following two section were used to explain the deprecated shortcut mapping screen and are left here for future reference or development.

Shortcuts are used to initiate _actions_ within Ansel.

An _action_ is usually (but not always) an operation that you might undertake using Ansel's point-and-click user interface. For example:

- Increase, decrease or reset sliders
- Scroll through dropdown lists
- Enable, expand or focus modules
- Click buttons
- Switch between views

Such point-and-click type _actions_ are normally defined as the application of an _effect_ to an _element_ of a _widget_, where these terms are defined as follows:

Widget
: Each visible part of the user interface is known as a _widget_. For example the Ansel application window is a widget, containing side panel widgets, each of which contains module widgets, each of which contains button, slider and dropdown list widgets etc... When assigning a shortcut to an action, you must first decide which widget it is to be applied to.

Element
: An _element_ is the part of a UI widget that is affected by your shortcut. For example, for a slider that has a color picker, you can make a shortcut activate the color picker _button_ element or change the _value_ element of the slider. For a row of tabs (the row is a single widget) you can select which tab element to activate or use your mouse scroll wheel to scroll through the tabs.

Effect
: A shortcut can sometimes have multiple possible _effects_ on a given _element_. For example, a button can be activated as if it was pressed with a plain mouse-click or as if it was pressed with Ctrl+click. A slider's value can be edited, increased/decreased or reset.

## Common actions

The following is a list of some of the actions to which you can assign shortcuts, organized by widget type.

### Global

Actions in the "global" section can be executed from any Ansel view. Most of these actions do not have specific _elements_ as they are used to perform one-off operations.

### Views

Actions in the "views" section can only be executed from the specified Ansel view. As with global actions, most do not have specific _elements_ as they are used to perform one-off operations.

### Buttons

A button is a clickable icon in the Ansel interface. The default action, when assigning shortcut to a _button_, is to activate that button as if clicked with the left mouse button. You can modify this action to activate the button as if clicked while holding a modifier key.

### Toggles

A toggle is a button that has a persistent on/off state. It therefore has additional _effects_ to allow you to toggle it or explicitly set its state. As with a normal button the default action, when assigning a shortcut to a toggle, is to activate the toggle as if clicked with the left mouse button (which toggles the button on/off).

### Utility modules

All utility modules have the following elements:

_Show_
: Acts as a _toggle_ that expands and collapses the module.

_Reset_
: Acts as a _button_ that resets all module parameters when activated. The _ctrl-activate_ action can be used to re-apply any automatic presets for that module.

_Presets_
: Allows you to select actions from the presets menu (e.g. edit, update, previous, next). The default action, when assigning a shortcut to a _preset_ element, is to display a list of the available presets for selection.

The default action, when assigning a shortcut to a utility module, is to _toggle_ the _show_ element (expand/collapse the module).

In addition, shortcuts are available for all of the controls on each module as well as any stored presets (see below).

### Processing modules

Processing modules have the same elements and defaults as utility modules with the following additional elements:

_Enable_
: Acts as a _toggle_ that switches the module on and off.

_Focus_
: Acts as a _toggle_ that focuses or defocuses the module.

_Instance_
: Allows you to select actions from the multiple-instance menu (e.g. move up/down, create new instance). The default action, when assigning a shortcut to the _instance_ element, is to display a list of the available options for selection;

If an action affects a processing module that can have multiple instances, you can choose which instance to adjust with a given shortcut. By default, all actions will affect the "preferred" instance, as defined using the settings in [Preferences > Miscellaneous > Shortcuts with multiple instances](./miscellaneous.md#shortcuts-with-multiple-instances).


### Dropdowns

A dropdown is a multi-selection box and has the following elements available:

_Selection_
: Allows values to be selected from the dropdown list in various ways. The default action, when assigning a shortcut to a dropdown, is to display a popup _edit_ box with a list of the available values for selection.

_Button_
: A standard _button_ element that allows the button to the right of the dropdown (if present) to be activated. For example, the _aspect_ dropdown in the [_Crop_](../views/darkroom/modules/crop.md) module has a button that allows the crop controls to be changed from portrait to landscape and vice versa.

### Sliders

A slider allows you to continuously alter an integer or decimal value, and has the following elements available:

_Value_
: Allows the current value of the slider to be altered. The default action, when assigning a shortcut to a slider, is to display a popup _edit_ box so you can enter a value. Value elements are also used for modifying some on-screen graphs. When modifying the _value_ element with a shortcut you may not exceed the bounds set in the visual slider.

_Force_
: This is the same as the _value_ element described above, but it allows you to exceed the bounds set in the visual slider.

_Zoom_
: Allows you to change the upper and lower bounds of the visual slider without altering the current value.

_Button_
: A standard _button_ element that allows the button to the right of the slider (if present) to be activated. For example, a slider may include a color picker to visually set its value based on selected elements of the image.
---

# Presets : https://ansel.photos/en/doc/preferences-settings/presets/

This menu provides an overview of the presets that are defined for Ansel's modules, and allows you to modify some of their properties.

Pre-defined presets (those that are included by default within Ansel) are shown with a lock symbol. Their properties cannot be changed.

User-defined presets can be imported from exported `.dtpreset` files using the "import" button at the bottom of the screen. You can export _all_ user-defined presets to a single directory using the "export" button.

Delete a user-defined preset by selecting it and pressing the Delete key.

Edit a user-defined preset's properties by selecting it and pressing Enter or double-clicking. This will open a dialog that allows the preset to be edited, exported to an external `.dtpreset` file or deleted.
---

# Lua options : https://ansel.photos/en/doc/preferences-settings/lua-options/

lua scripts installer don't show again
: Check this box to hide the [lua scripts installer](../views/toolboxes/lua-scripts-installer.md) in the lighttable if no lua scripts are installed.
---

# Command Line Interface : https://ansel.photos/en/doc/cli/
---

# Aurélien Pierre : https://ansel.photos/en/authors/aurelien-pierre/
---

# Authors : https://ansel.photos/en/authors/
---

# Color management for Linux users : https://ansel.photos/en/resources/color-management-linux/

Linux is still vastly lagging behind MacOS and even Windows when it comes to ensuring the consistency of colors displayed on the monitor. The widely forced adoption of Wayland graphic server in most Linux distributions has damaged this state even more, since color management has been long refused by Wayland developers. The specification for a CMS in Wayland was finally drafted in 2020 and the code was merged into upstream Wayland in 2025, by external contributors, and after a long battle from the developers of earlier CMS software against the historical developers of Wayland.[^1] Since then, the state of CMS support is very hit-and-miss across desktop environments and distributions. And yet, the preliminary results of Ansel [telemetry](/doc/data-privacy.md) show 67 % of Wayland adoption among Ansel users.

__You should not use Ansel, or any other image editing software, on Wayland.__

{{< note >}}
The Wayland CMS situation varies greatly depending on distribution and desktop environment, and will evolve in the future. Rather than a full (soon outdated) landscape of what works and what doesn't, I will present the checklist that need to be validated to consider using Wayland.
{{< /note >}}

[^1]: Pekka Paalanen, _12 years of incubating Wayland color management_, February 2025. [URL](https://www.collabora.com/news-and-blog/news-and-events/12-years-of-incubating-wayland-color-management.html)

## TL;DR: why not use Wayland on Linux ?

Because the very author of Wayland color management system said it's nowhere near ready for professional photo & video usage.[^1]

For consumer & entertainment use, you can check the state of Wayland CMS support by your desktop compositor on [Wayland documentation](https://wayland.app/protocols/color-management-v1#compositor-support).

## The problem

Most (if not all) laptop screens and many consumer-level monitors have a white point _much_ more blue than the standard 6500 K, anywhere between 6800 K to 7200 K. To correct that, the historical way is to calibrate the screen and write the calibration curves into the [Video Card Gamma Table (VCGT)](https://www.color.org/groups/medical/displays/controllingVCGT.pdf). The VCGT is both:

- [a special data field](https://www.argyllcms.com/doc/iccvcgt.html) inside the ICC profiles,
- a special memory region on your GPU.

When starting your Linux session, some piece of software reads the VCGT from an ICC profile and loads it on the actual video memory. So the whole desktop would have its white point and contrast curve corrected in one lazy step, whether application were "color managed" or not.

Later on, many applets called "night color" or "redshift" hacked the VCGT to shift colors to amber in the evening and at night, because blue(ish) light is known to disturb sleep patterns, and while it is great for your circadian rhythm, it adds a layer of randomness in the color pipeline. Also, the VCGT could be lost without warning when resuming from standby mode, and several applications could race to be the last one overriding it. The rise of dual-GPU systems (discrete + embedded) did nothing to help reliability in the color pipeline, and the proprietary Nvidia driver is still the only one allowing to preview the VCGT.

But the bottom line is: you need some sort of system-wide way of loading a VCGT to normalize the white point,[^2] and Xorg/X11 had `colord` to globally advertise a system display profile that applications could fetch.

[^2]: The contrast/brightness curve is not so much of an issue on LED screens that are, by nature, close to linear.

On top of the VCGT, that holds the _calibration_, comes the _profiling_ which corrects the native color primaries of the display, such that RGB triplets match the light spectrum they are associated with. In short: 

- nailing the display white point at 6500 K and linearizing the brightness response is the first stage (calibration), 
- nailing the display color primaries to reduce hue and saturation deviations is the second stage (profiling). 

The profiling is typically a 3×3 color matrix, sometimes with extra curves or gamma, but it can also be a LUT (not recommended in Ansel).

When a photographer "calibrates" their display, actually both stages are usually silently done and saved into the same ICC profile. There is a way, in particular in Display Cal, to disable the calibration through the VCGT, which then bakes it into the profiling as a pack, but this is generally a very bad idea because then GUI theme colors are excluded from white point adaptation even on Xorg.

{{< note >}}
Contrarily to a common belief, ICC profiles are nothing more than a descriptor (kind of a file of metadata), that actual programs choose to apply (and choose how). They are not a piece of software in themselves.
{{< /note >}}

__At the application level, we can only apply the profiling stage on images__. This has 2 implications :

1. The color validity of the profiling stage relies on the calibration stage being properly performed at the VCGT level. Profiling becomes unpredictable and inaccurate without its twin calibration : it's a pipeline.
2. We cannot color-correct/color-manage GUI colors from the application theme (in particular: background color) declared in CSS stylesheet and passed through to Gtk.

This is a problem in Ansel because we set the whole GUI neutral grey, for color assessment purposes, and we need this grey to be 6500 K, but also consistent with the white point of the image. So we need a full-desktop color management, at least for the calibration, to avoid clashing white points between windows. Which Wayland initial design explicitely makes it none of its business, relying on apps developers to do the right thing (as if…).

And then, last but not least, if you are using several monitors, you need some piece of software mapping an ICC profile to each monitor, such that the CMS can grab the right profile for the monitor where your application window is sitting.[^3] Which is what Wayland explicitely forbids for "security" reasons.

[^3]: And since Xorg as well as Wayland allow application windows to sit on multiple monitors at once, you can only guess which color profile is going to be applied.

## What you need to check if using Wayland

The following assumes that you calibrated and profiled your monitor with a colorimeter, and produced a display ICC profile neutralizing the color deviations of your monitor.

Check that each monitor has its calibration loaded from ICC profile into VCGT when starting a graphic session
: That part seems to be fairly covered as of 2026, at least for the main players Gnome/KDE Plasma. The desktop environment allows to define a system profile that will globally change the white point of all applications.
: If not, the [`dispwin`](https://argyllcms.com/doc/dispwin.html) command of the `argyllcms` software allows to manually load it, like `dispwin -d 1 ~/.local/share/icc/YOUR_DISPLAY_PROFILE.icc`. "Suffice" to script it and have the script started automatically with your session, which excludes any user unfamiliar with scripting.

~~Check that each monitor is tagged with a color profile~~
: This part is explicitely removed from the Wayland protocol, so applications are, by design, forbidden to fetch information about displays. In particular, starting [`ansel-cmstest`](/doc/cli/ansel-cmstest/) in Wayland will return:
    ```bash
    $ /opt/ansel/bin/ansel-cmstest
    ansel-cmstest version 0.0.0+3877~gcfa6648f92
    this executable was built with colord support enabled
    ansel itself was built with colord support enabled

    primary CRTC is at CRTC 0

    eDP-1   the X atom and colord returned the same profile
            X atom: _ICC_PROFILE (0 bytes)
                    description: (none)
            colord: "(none)"
                    description: (file not found)

    Better check your system setup
    - some monitors lacked a profile
    You may experience inconsistent color rendition between color managed applications
    ```
: This practically means that Ansel _system profile (default)_, which is supposed to be automatically detected through `colord` or `xatom` on X11/Xorg, is empty/undefined all the time on Wayland and you will need to:
    1. add your display profile into `~./config/ansel/color/out`,
    2. choose it manually in the global menu __Display__ -> __Monitor color profile__,
    3. manually change it if/when you move the Ansel window to a different monitor.
: Failure to do that will result in Ansel defaulting to sRGB as output colorspace. If your display native gamut is:
    - Adobe RGB, then colors in the green-cyan region will appear more saturated on screen than they are in the file,
    - Display P3, then all colors will appear more saturated on screen than they are in the file, but the orange-green-cyan region will also be the worst offender.
: This problem has no solution within GTK3, that Ansel uses as a graphic toolkit, and even with GTK4, there is still no viable, widely supported solution as of now. Manually handling color profiles in Ansel works until the compositors start actively color-managing application windows themselves : then GTK3 offers no way of tagging a window as "already color-managed", as to prevent the compositor from doing them further harm. It will instead double up on colorspace conversions we already did internally and mess up colors in way that only experts will be able to spot. Luckily for us, as of mid-2026, no compositor is able to color-manage anything.
: In any case, the manual colorspace conversions can be done only on image surfaces that we paint in the Ansel app window. We have zero control over GUI controls colors defined in the theme stylesheet (`ansel.css`), and handled directly by GTK widgets, which are implicitely sRGB (as per CSS standard). Meaning GUI colors will always look oversaturated if your monitor native gamut is larger than sRGB, compared to what they are intended to be.

Check that HDR support is disabled
: Wayland now supports HDR capabilities by tampering with display backlighting. Although Ansel outputs good old 8 bits RGB and should not pull the HDR triggers, HDR is not part of ICC v2 or v4 and there is no telling how this feature would affect the tone response. It may be harmless, but until this is thorougly audited, the safe path is to keep HDR disabled.

Check that you are using Xorg
: Seriously, just don't use Wayland for photography.---

# Styles : https://ansel.photos/en/doc/views/toolboxes/styles/

A **style** is a named, saved editing recipe — a subset of a history stack that can be applied to any number of images. Styles let you reuse looks, technical corrections, or module configurations without having to redo them from scratch.

Styles are accessible from two places:

- The **Styles menu** in the menu bar, which lists all your styles and applies them in one click.
- The **Styles toolbox module**, available as a floating window via **Styles → Manage styles…**

## Applying a style

Click any style name in the **Styles menu** to apply it to all currently active images. If **Ask merge settings before apply** is enabled (the default), a [merge settings dialog](#merge-settings-dialog) appears first. See the [copy-paste history](../lighttable/history-copy-paste.md) page for a full explanation of what merge position and pipeline order mean.

Double-clicking a style name in the **Styles toolbox module** applies it to all selected images using the currently saved settings, without showing the dialog.

{{< note >}}
Style application and [copy-paste](../lighttable/history-copy-paste.md) share the same underlying merge engine but maintain **independent** default settings. Changing the merge mode for styles has no effect on paste operations, and vice versa.
{{< /note >}}

### Merge settings dialog

When **Ask merge settings before apply** is active, clicking a style in the Styles menu opens a dialog with two choices:

Merge position
: how the style's modules are placed relative to the image's existing history:
    - **Below (Prepend)** — the style becomes the base; your existing edits run afterwards and override conflicts. Use this for decorative looks that sit underneath per-image corrections.
    - **Above (Append)** — your existing edits become the base; the style runs afterwards and overrides conflicts. Use this for technical corrections (calibration, profiling) that must take precedence.
    - **Replace** — your existing history is discarded and replaced entirely by the style.

Use incoming pipeline order
: when checked, the module execution order stored in the style replaces yours. This option is **disabled** when the style was saved without a pipeline order; a tooltip explains this when you hover over the greyed-out checkbox.

Ask me every time
: when unchecked, the saved defaults are used silently on future applies. You can re-enable the dialog from **Styles → Ask merge settings before apply**.

### Default settings (Styles menu)

The merge mode and pipeline order defaults for style application are controlled at the bottom of the Styles menu:

**Styles → History pasting mode**
: Choose **Prepend**, **Append**, or **Replace** as the default merge position.

**Styles → Nodes pasting mode → Copy module order**
: Toggle whether the style's pipeline order is applied by default.

**Styles → Ask merge settings before apply**
: Toggle whether the dialog is shown each time a style is applied from the menu.

## Creating a style

### From the history stack module (darkroom)

Open the [history stack](./history-stack.md) panel in the darkroom. Click the **create style** button (the icon to the right of "compress history stack"). A dialog lets you name the style, add an optional description, and choose which modules from the current history to include.

### From the Styles toolbox module

Click **create** in the toolbox. The same dialog appears, drawing from the history stack of the single selected image.

### Hierarchical names

Use the pipe character `|` as a separator in the style name to create sub-categories. For example, a style named `print|tone curve +0.5 EV` appears under a **print** category in both the Styles menu and the toolbox. Categories can be nested.

### Saving pipeline order

When creating a style, the pipeline order (module execution sequence) is saved alongside the module parameters only if you check the appropriate option in the creation dialog. Styles without a saved order are still fully functional — Ansel will keep the destination image's existing order when applying them.

## Managing styles

The **Styles toolbox module** is the primary interface for managing styles. Open it via **Styles → Manage styles…** in the menu bar.

edit
: Open a dialog to include or exclude individual modules from an existing style. Tick **duplicate** to create a new style instead of overwriting the existing one (you will need to supply a new unique name).

remove
: Delete the selected style immediately, without a confirmation prompt.

import
: Load a style from a `.dtstyle` XML file. If a style with the same name already exists you will be asked whether to overwrite it.

export
: Save the selected style to disk as a `.dtstyle` file for backup or sharing.

## Keyboard shortcuts

You can assign a keyboard shortcut to any style in **Edit → Keyboard shortcuts…** and then press that shortcut from anywhere in the lighttable or darkroom to apply the style to all active images using the current saved merge settings (the dialog is not shown for shortcut-triggered applies).
---

# Welcome the new Ansel maintainer : https://ansel.photos/en/news/welcome-the-new-ansel-maintainer/

Jiyoné (Guillaume Stutin) has taken over Ansel maintainance since october 2025. The current goal is to turn Ansel into a [cooperative](https://en.wikipedia.org/wiki/Cooperative) where users can become members and democratically control the project orientations and priorities, but also guarantee a fair retribution and safe working environment for those who commit their workforce to the project. I have layed out the description of the problem and the proposed solution on the [_democracy_](../contribute/democracy.md) page.

Pending a real structure, Guillaume needs donations to pay his bills and you can [support him on Liberapay](https://liberapay.com/jiyone).

Guillaume has been using Darktable since circa 2018 and moved to Ansel since the beginning. As a kitchen chef and culinary photographer, he has co-authored and photographed several cooking books whose pictures were edited with Darktable then with Ansel :

- [À l'école de la fermentation](https://www.editionsalternatives.com/site.php?type=P&id=2368), 2025, Éditions Alternatives.
- [Le miel, guide d'usage](https://www.editionsalternatives.com/site.php?type=P&id=2256), 2022, Éditions Alternatives.
- [Légumes lacto-fermentés](https://www.gallimard.fr/catalogue/legumes-lacto-fermentes/9782072932588), 2021, Éditions Gallimard.
- [Faire son pain et son levain](https://www.editions-larousse.fr/livre/faire-son-pain-et-son-levain-9782036000919/), 2021, Éditions Larousse.

Guillaume also has an extensive experience with analog photography and film scanning, developping his negatives himself at home with C41 & E6 processes, or caffenol. He shoots stereophotography on diapositives. He has created some impressive Arduino-based devices to help with film chemical development and scanning, where he learned how to program in C.

On top of his real-world photography experience, both digital and analog, I trained him myself in software engineering. Needless to say, I have complete trust in him to carry on with my vision of what a good photo editing app should be.

So, please, go support him.---

# Changes in Ansel packages distribution : https://ansel.photos/en/news/changes-in-packages-distribution/

## Target clones

Compiling the software natively on the computer use to improve runtimes on CPU by around 30 %, compared to pre-built packages. The reason is the compiler makes specific optimizations for the target hardware on which it is compiled, while pre-built packages have to stay generic and trigger more conservative optimization for the sake of wide support. Note that OpenCL kernels are, anyway, compiled for your particular GPU using your OpenCL driver, so the story is different there.

In 2016, GCC introduced [target clones](https://maskray.me/blog/2023-02-05-function-multi-versioning), followed in 2023 by CLang, for which I added support in Darktable in 2019 in some parts only (noticeably, the _tone equalizer_). This experiment had never been scaled to the whole software until now.

Target clones essentially allow to build different versions of the code, optimized for different hardware architectures, and the software will choose the right one to execute at runtime. Since this relies on `ifunc`, this is supported only on Linux (and even there, not for all versions of `libc`) and Mac OS Intel, so expect no Windows support. The same feature on Windows would have to be coded manually.

With generalized target clones, Ansel AppImages packages now run significantly faster, and within a 5 % margin compared to native builds (aka compiling yourself). This is a hand streched out toward all the less computer-litterate users, who can't build the software themselves and usually overlap with the crowd that owns weak hardware.

## Linux AppImage

2 months ago, an annoying bug forcing the recompilation of all OpenCL kernels at each AppImage start was fixed. The reason was that AppImage are mounted in a random container (unstable through restarts), while the integrity check performed on kernels (as to recompile only when they changed) hardcoded the path of the binary. That AppImage-only penalty is now solved.

AppImages have been modified to accept command-line arguments and expose other binaries than the main GUI. This allows:

1. to get debug logs through `./Ansel-xxxx-x86_64.AppImage -d dev -d perf -d opencl -d verbose`,
2. to process images without GUI through `./Ansel-xxxx-x86_64.AppImage ansel-cli input.raw output.tif`
3. to call other internal programs (see the [docs](/doc/install/linux/#run-the-appimage)).

Due to dependencies issues on Ubuntu, the AppImage had to be upgraded to Ubuntu 24.04, which makes it now compatible with all Linux systems supporting at least `libc 2.39`. You can check your own `libc` version by running `ldd --version` in a terminal. Ansel AppImage is therefore compatible with :

1. Ubuntu 24.04,
2. Debian 13,
3. Linux Mint 22,
4. Pop!_OS 24.04,
5. Fedora 40,
6. openSUSE Leap 16

Note that the Ansel AppImage doesn't bundle the OpenMP library anymore, since it conflicted with some debug helpers in a weird way. It will now use the system one. If not installed (which would be rare on Linux), it is part of the GCC ecosystem (`libgomp`).

## Docker images

Nightly builds have been added as [Docker images](https://hub.docker.com/r/aurelienpierre/ansel) too. These will allow to run `ansel-cli` on backend servers. Note though that Ansel CLI is not safe to run on servers with public web access, as it doesn't prevent code injection and doesn't sanitize user inputs in any way. Docker images are based on Ubuntu 24.04.

One possible use case of Docker images would be to run rendering farms to offload Ansel image exports to servers with large GPU :

- send the RAW and editing XMP to the server,
- get the resulting JPG.

On reasonably-fast networks, that would prevent users from buying expensive hardware if they only rarely use it to its full potential, while sharing the cost of a 24/7 remote server between users.

## MacOS

MacOS nightly builds have been added last October for the Apple M (Arm64) architecture. In September 2025, Github added runners for MacOS 15 Sonoma on Intel architecture. This allowed to add support of Intel i386 for Ansel nightly builds today. Both architectures are compiled on MacOS 15 Sonoma.

Note that [Apple Intel architecture](https://github.com/actions/runner-images/issues/13045) is planned for deprecation by Github in August 2027 since Apple has discontinued support of this architecture. After this, there will be no easy way of building Ansel for Apple Intel hardware.

## About nightly builds

Nightly builds are auto-generated on Github runners (think of them as virtual servers you can start and stop from scripts to execute things), every morning between 5 and 7 am UTC. The generated packages are added to the [pre-release notes](https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0), which acts as a repositories, and provides 1.5 year of build histories so you get a chance to rollback to a previously-working version if code changes suddently break the application in a way that prevents you from using it.

The nightly builds are entirely dependent:

- on Github for providing the runners, 
- on [Homebrew](https://docs.brew.sh/) for providing the MacOS dependencies ecosystem,
- on [MSYS Pacman](https://www.msys2.org/) for providing the Windows dependencies ecosystem,
- on [Ubuntu](https://ubuntu.com/download/server) for providing the Linux dependencies ecosystem.

That's a lot of third parties to rely on, and nightly builds often break just because Github deprecated a runner or one of MSYS/Ubuntu/Homebrew renamed a package, removed it or upgraded it to a newer version that changes API and breaks with Ansel internals. So, despite the apparent automatization of the whole workflow, regular maintenance is still needed and nightly builds might stay broken for some time.

## Credits

I would like to thank:

- [Alynx Zhou](https://alynx.one/), for having been an huge help with debugging AppImage and Linux nightly builds, on top of other CMake configuration,
- [Jake Langford](https://github.com/jakenvac), [Miguel Moquillon](https://www.moquillon.fr/), [Laurent Perraut](https://www.perraut.net/), [Sidney Markowitz](https://github.com/sidney) for having brought MacOS support and nightly builds,
- [Jiyoné](https://github.com/Jiyone) for having handled the MacOS code reviews and merges.---

# Development : https://ansel.photos/en/tags/development/
---

# Tags : https://ansel.photos/en/tags/
---

# Color controls finally correct : https://ansel.photos/en/news/color-controls-finally-correct/

## Introduction

### Tools vs. Machines, Crafts vs. Industry

In his book, [_The Technological Society_](https://en.wikipedia.org/wiki/The_Technological_Society) (1954), Jacques Ellul presents the difference between the pre- and post-industrial revolution as follows:

The pre-industrial era is the reign of the tool and of craftsmanship. The foremost property of tools is to be generic, versatile, and adaptable. It is up to the craftsman to develop his skills to make the tools follow his intent, so the hand will make up for the limitations of the instrument. This concept is still well known to musicians today: you have to practice, learn, try, fail, retry… there are no shortcuts. Ellul emphasizes the idea of parsimony that comes with tools: resources are limited, so your toolset is pretty much defined by what you can afford, carry, master, and build locally. Trends change slowly and are local, because they use local resources and adapt to local needs, and tools follow the same pattern. Tools are heirlooms passed from master to apprentice, from parent to child. They don't get incompatible or outdated.

The industrial era is the reign of the machine. It comes from a culture of standardization and massification. The machine is much more productive because it is specialized for one task, but it is useless for everything else and can rarely be adapted to another use. When production needs change, the machine is replaced by another because it is good for nothing. This is not a parsimonious scheme anymore, and it is allowed because resources are much cheaper and mass production allows economies of scale. Then, humans become the servants of the machine, and handcrafts become a luxury.

Productions that use to be made at home or in small shops, in a family context, are moved to big factories, where there is enough room for machines. By the way, that had very concrete effects on how we organize our daily life, including when and how many times we eat[^2], as well as sleeping patterns[^1]. So it is not a stretch to say that the industrial revolution also deeply changed the way we think, what we consider normal or evident.

[^1]: Ekirch AR. Segmented Sleep in Preindustrial Societies. Sleep. 2016 Mar 1;39(3):715-6. doi: 10.5665/sleep.5558. PMID: 26888454; PMCID: PMC4763365. [URL](https://pmc.ncbi.nlm.nih.gov/articles/PMC4763365/)

[^2]: Grignon, Claude. La règle, la mode et le travail : la genèse sociale du modèle des repas français contemporain. In Le temps de manger, édité par Maurice Aymard, Claude Grignon, et Françoise Sabban. Paris: Éditions de la Maison des sciences de l’homme, 1993. doi:10.4000/books.editionsmsh.8155. [URL](https://books.openedition.org/editionsmsh/8155)

That spoke to me because it's how I had been developing color-editing tools, intuitively, since I started: trying to build generic tools that relied on the fewest assumptions to stay versatile, while giving fine-grained control over image parameters, and ultimately relying on craftsmen's abilities rather than on half-broken, unvalidated, auto-magical tools that only work in ideal cases.

### Digital photography : automating or enjoying the process ?

I have never understood the passion of many photographers, especially the most technophile, for push-button workflows and automated procedures. I have learned and taught the piano, and I'm used to the _shut up and practice_ paradigm : training to develop and maintain agility, and then repeating the same few bars again and again until the sound is right. Working the quality, the texture and the expressivity of the sound, through the amount of weight you put on the keys, the speed of the attack, the motion of the wrist and forearm. Using your body for fine motion and training it beyond its current abilities, turning very unnatural gestures into reflexes. And finally, learning to hear in your mind the sound you are reaching for, before even moving your hands. All those things that will never be reducible to GUI sliders and buttons, metrics, variables, or algorithms.

The way many photographers approach photo editing sounds like they got punished by digital photography because they now have to use a software to get their photos. So it should be reduced to a soulless procedure where everything should be automated if it can be. We exchange presets and recipes, some even sell them. And now we have AI to emulate one style or another. Where is the joy in industrialising artistic creation ? What is the point of having a hobby that feels like the burden of having to develop your photos ? Art is a process, not a procedure. What sick mind would design a robot that does pottery, engraving or floral arrangements in your place ? What about using software as an opportunity to finish the look of your images according to your taste and sensibility ? Have we lost the raw pleasure of doing things ourselves, even badly ? At what age is this appetite lost ?

Granted, fighting against a photo-editing software is a frustrating experience, but you need to understand what you are fighting against. Is it just the intrinsic difficulty of your craft and your lack of skills, or is it the poor design of the tool ? When you struggle on a 25 k€ piano that just saw the piano technician, you know the instrument is not the problem. But with software, how do you know ? Well, you can't until you fully commit to the _shut up and practice_ paradigm. Good retouchers will get good results out of any software, the difference is in how much time that will take them. And, well, sometimes you have to do a lot of cover up for the broken color models of editing applications.

### Tools in the photo-editing GUI

In terms of UI design, the parsimony of generic, versatile tools is the best way to avoid getting overwhelmed by dozens of features that hide each other, overlap in functionality, and just pollute your visual field. It's software feng shui. But then, the nature of the tools shifts a bit. 

There are two ways of thinking of image processing color controls : from their functional use, and from the way they allow to manipulate color properties. From their functional use :

{{< table >}}
| Function | Methods |
| ----------- | ------ |
| __Denoise__ | Wavelets filtering, non-local means, bilateral filter, guided filter, diffusion, |
| __Deblur__  | Richardson-Lucy deconvolution, Gaussian/Laplacian pyramid, high-pass filter, wavelets filtering, counter-diffusion, unsharp mask |
| __Blur__ | Wavelets filtering, diffusion, bilateral filter, guided filter |
| __Brighten__ | Exposure correction, tone curve, power ("gamma") transfer function, channel mixer |
| __White balance__ | Channel-wise normalisation, chromatic adaptation transform, channel mixer |
| __Color shift__ | Channel mixer, lift-gamma-gain, slope-offset-power, hue rotation, LUT |
| __Colorfulness enhancement__ | Saturation, chroma, vibrance, LUT |
| __Contrast enhancement__ | Tone curve, sigmoidal transfer functions, range normalization ("levels"), LUT |
| __Dynamic range compression__ | Same as contrast, but in "reduce" mode instead of "increase" mode |
{{< /table >}}

Note that color shift could be resplit into two modes: corrective (which calls for light models) and creative (which calls for perceptual models). We won't go that far.

If we approach the same list from the other entry point, the way they allow to manipulate color properties, we get :

{{< table >}}
| Method | Functions |
| ------ | -------- |
| Wavelets filtering | __Denoise__, __Deblur__, __Blur__ |
| Diffusion |  __Denoise__, __Deblur__, __Blur__ |
| Channel mixer | __Brighten__, __Color shift__, __White balance__ |
| LUT | __Contrast enhancement__, __Color shift__, __Colorfulness enhancement__ |
| Tone curve | __Brighten__, __Contrast enhancement__, __Dynamic range compression__ |
{{< /table >}}

I did not redo the whole list but you get the idea : whether you want to classify your list of tools by method or by function, you don't end up with a 1:1 match, except for a few ones. So none of these mappings allows you to systematically factorize your tools into unit GUI elements, as to honour the principle of parsimony.

And then, image processing is not just a set of tools in a box, it's actually a pipeline of pixel filters that need to be inserted as graph nodes in an order that matches the input requirements of the filters. Here you go (simplified):

```mermaid
flowchart TD
    r(Raw input) --> d1[Raw denoise]
    d1 --> demosaic[Demosaic]
    demosaic --> d2[RGB denoise]
    d2 --> wb[White balance]
    wb --> bright1["Brightness enhancement (linear)"]
    bright1 --> blur1[Blur / Deblur]
    blur1 --> color_lin["Color shift (linear)"]
    color_lin --> contrast1["Contrast enhancement / Brightness enhancement (non-linear)"]
    contrast1 --> color_nl["Color shift (non-linear) / Colorfulness enhancement"]
    color_nl --> dr[Dynamic range compression]
    dr --> o(Output)
```

And, last but not least, the problem nobody sees: the math. For example, a guided filter applied to an opacity mask and guided by the RGB image (as in Ansel/Darktable mask refinement) doesn't have the same math as a guided filter applied to a luminance mask and guided by itself (as in the tone equalizer), or a full RGB image guided by itself (as in dehaze). Those need to be implemented several times, with different types of inputs and different kinds of optimizations. The same applies to methods working on non-demosaiced images: we have to account for the fact that we don't have a full RGB signal at each sensel, and XTrans arrays need special math different from Bayer.

So, given that:

1. functionalities have different methods,
2. methods can't be reduced to a single functionality,
3. all of them are inserted as nodes in a pipeline where order matters,
4. methods have implementation variations depending on their input signal, i.e. their position in the pipeline,
5. users want to have fun without having to learn anything,
6. GUI clutter serves nobody and tires everyone,

__…we have a problem: how to split features and organize them as a coherent GUI?__

In a professional setup, where you can expect users to know how to build a pipeline by themselves and understand the ins and outs of its nodes, you can go the DaVinci Resolve way and build a nodal editor. Then you build one node per method, and handle internally the implementation variants by checking input signal type. This is the most minimalistic way, because there is zero GUI duplication: just a bank of nodes, and nodes can be set in parallel rather than forced sequentially. But it requires expert users.

The Lightroom/Capture One way is to completely hide that there is even a pipeline, and provide a flat GUI. Then multi-instantiation of nodes is done through masks: adding new masks lets you assign them a subset of the software controls to selectively edit the masked regions. But that also comes with the fact that none of these applications allow scene-referred linear editing, which makes blurring, deblurring, denoising, and actual mask transitions wonky.

Ansel is on a middle path. First, there is a prototype nodal editor now:

![](node-editor.jpg)
![](node-editor-2.jpg)

This allows us to explicitly show the pipeline order, reorder it manually, and change ordering presets, in a way that also shows inputs and outputs. Previously, we only had the stack of GUI modules in the right sidebar, which sort-of suggested pipeline order if read as a layer stack, but it was often misused by users as a mere GUI toolbox reordering convenience.

And then, the feature splitting is hybrid:

- _Contrast equalizer_, _local contrast_ and _diffuse or sharpen_ modules are intrinsically dual in their nature: they can increase or decrease local contrast and sharpness depending on their parameters. The _contrast equalizer_ even has a denoising method, which makes sense because sharpening increases noise, so the right place to keep it under control is where it gets created. __Those are method-first modules__.
- _White balance_ duplicates the functionality of the _color calibration_ chromatic adaptation part. _White balance_ is a simple channel-wise normalization that happens in sensor RGB, and is needed by some demosaicing methods (especially for XTrans sensors), while _color calibration_ performs a much more perceptually accurate illuminant compensation in CIE CAT16 space. That space is defined from CIE XYZ 1931 space, which we get only after input color profiling (and that requires a demosaiced image to work). There are different reasons and needs for both, and they must sit in different places in the pipeline. __Those are pipeline-first modules__: while they seem functionally similar, their reasons for existence and their internal requirements (and completely different math) make them much more different than you might think. The same applies to the various denoising modules: some work on RAW input, some don't, and some work better on non-linear signals.
- _Color balance_ was designed as a one-stop color module, applying an extended [ASC CDL](https://en.wikipedia.org/wiki/ASC_CDL) (slope-offset-power), and then handling global and luminance-wise saturation and chroma, therefore replacing previous Darktable modules like _contrast-brightness-saturation_, _vibrance_, and _velvia_ (all working in CIE Lab 1976). Taken alone, __it is a functionality-first module__, but it is not the only color-shifting module. The other color-shifting modules use very different methods (matrices, LUTs, or hue-wise controls), that are [3 to 9 times](../complete-pipeline-overhaul/index.md) less computationally expensive to run, and operate in different color spaces.

Ansel inherits its concept of modules from Darktable: modules are at the same time a GUI toolbox widget and a pixel filter inserted somewhere in the pipeline. So, designing very small modules (feature-wise) will spawn a lot of high-level GUI widgets in the right sidebar, while designing very large modules will make fewer high-level widgets but they will be higher or use more internal tabs. The second option might still be preferable because high-level GUI modules act as trays, so fewer trays that are functionally consistent help better understanding the high-level feature split (like a table of contents).

But it doesn't stop there, because again… nodes. A module should be a set of color controls that are consistent as a multi-instantiated masked node in the pipeline. It's no good to pack a module with dozens of features where half of them are meaningful as global settings, and the rest only in the mask locality; they will only consume CPU cycles for nothing once instantiated.

And finally, there is performance. Color controls that need to work in some particular color space will need a back-and-forth color conversion, which will consume CPU cycles, so it's better for them to all run in the same module if they make sense together functionality-wise. Also, any new module will run a new loop over pixels, meaning it will move the image once more into CPU memory cache from/to the RAM. So, collapsing features into the same loop prevents additional I/O bandwidth abuse and helps with performance.

So, if we recap the whole list of constraints, here, it's a really difficult challenge to solve:

1. we should always strive to build the fewest GUI elements because a cluttered GUI makes __everything__ hard to find (and cognitively overwhelming): the best place to hide a tree is in the forest,
2. the clutter we build should be organized from global to local, generic to specific, frequently-used to niche, because it's the easiest way to reason about: thinking of the GUI structure as a "table of contents" is the best I have come up with so far,
3. but making it a rule of systematically splitting features based on functionality or based on method still ends up with duplications in both cases, unless we go in the full-node-editor direction, do everything method-based, and defer the responsibility of building pipelines to the expert user,
4. modules should be designed as __functionality-first__ whenever possible, but a lot of limitations apply :
    1. the input signal type, which determines the position in pipeline, and may force us to prioritize __pipeline-first__ and duplicate tools,
    2. the relevance and consistency of the set of module controls when multi-instantiated and masked: a module should be a consistent unit of editing workflow, not a holdall to squeeze everything. Especially, if it makes sense (workflow-wise) to apply a mask on only a subset of the module features, then this subset should be its own module,
    3. performance issues (color space roundtrip, gamut mapping of the output, I/O bandwidth): we can't think of user ergonomics solely from the UI side; runtime speed is part of ergonomics too,
    4. the mathematical nature of the operations realized : some methods (especially sharpening/local contrast) can have opposite effects depending on the sign of their parameters, some methods are poorly suitable to be grouped together in the same pixel filter,
5. finally, the amount of clicks/scroll steps needed to reach a frequently-used feature needs to be considered, because the generic -> specific axis is not always parallel to the frequently-used -> niche axis. So, while the generic -> specific high-level organization makes sense cognitively, it's not always the most ergonomic one.

All these adversarial requirements need to be arbitrated on a case-by-case basis, by looking at the entirety of the pipeline, otherwise we don't build a workflow application but a plugins registry. They live at the cross-roads between low-level programming, math, retouching workflow and UI ergonomics, and not one of these concepts is more important in general than the others. However, some of these constraints are more easily understood than others, which will make them receive a lot more attention, in a typical [streetlight effect](https://en.wikipedia.org/wiki/Streetlight_effect): keep chasing problems in the layer you understand while completely discarding their true origin because it touches a layer you don't understand/know nothing about. This is why it is a real pain-in-the-ass to work with "UX designers", because they typically don't have the background to look beyond the GUI and often don't care that there is an engine to control underneath the UI. You can't just choose to cancel gravity because you would like to remove the [pitch](https://en.wikipedia.org/wiki/Aircraft_principal_axes#/media/File:Aileron_pitch.gif) controller for simpler UI.

Design is a process of iterative refinements under user feedback. Some of these iterations will redesign macro to micro (global application architecture to module inner controls), and some will redesign micro to macro, until it converges towards unified semantics and coherent feature splitting. It's foolish to think that anybody will get in right in one pass, or to think that only one of the micro/macro levels need fixing. It's a bridge to build from both ends at the same time, and find a way to make them meet in the middle.

### The properties of good color controls

From my 17 years of experience editing pictures, and 8 years of experience studying and experimenting image-processing math, there are a couple of things I have learned about what is a good or a bad color control. By "color control", I mean some way to feed an user intent to a pixel algorithm that will edit a visual property of an image. Good color controls :

1. are orthogonal to each other, meaning one control changes one color dimension at any given time and doesn't force you to compensate on another control,
2. are evenly-scaled from a perceptual perspective, meaning going from "0" to "1" will change the visual result by the same amount as going from "1" to "2". This is unfortunately not always possible, because math and physics hate you.
3. scale gracefully as dynamic range increases, which discards pretty much all color appearance models (CIE Lab 1976, CIECAM 16, dt UCS 22), built for SDR on datasets recorded for painted colorpatches.
4. degrade gracefully as we increase the magnitude of changes, meaning they can get overcooked but can't change the image content and create artifacts,
5. preserve image gradients, meaning they keep the tonal and chromatic local variations as variations, instead of flattening them, making them overshoot (creating halos, fringes, noise, etc.), or reversing them.
6. allow artists to get as close as possible to their intent in the least amount of time.
7. are not duplicated.

From 1., we deduce for example that any kind of RGB tone curve is bad, because it will desaturate highlights and saturate shadows as a byproduct of the amount of (luminance) contrast you added, and you can't control (or decorrelate) the amount of saturation change you will get for the amount of contrast change you asked for, except by hacks like RGB ratios preservation or manually massaging saturation/desaturation through the tone curve application. This is the reason of existence for the _tone equalizer_, which corrects contrast by using a selective exposure adjustment instead of a tone curve.

From 2., we deduce that GUI controls should use color appearance models whenever possible.

From 3., we deduce that the color appearance models used in GUI __should not be used on pixel__ but should be converted so pixels are handled in RGB. This is a typical [model-view-controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) paradigm that is consistently ignored in all image processing applications. Apparently, there is a belief among developers that the pixel color model should match 1:1 the GUI controller colorspace.

From 4., we deduce that controls that are practically usable only on a small subset of their value range are using bad math, bad color models, bad priors. The failure modes are just as important as what happens when using the controls in their sweet spot.

From 5., we just confirm the point 3.

From 6., we raised a new problem : many photographers don't have a retouching/editing intent (as in: a visual endgoal), but only push and pull sliders in the direction that seems to make the image look nicer, and end up with an happy accident they would be unable to reproduce (or ditch the image for being unable to work it). In this case, there is no metric of success for the controls because there is no difference to compute between intent and result, since the intent is undefined. Which is why, when working with users feedback, we need to be critical and investigate who is speaking and from where. That is to say : not every user feedback is good user feedback (and years of experience is not a proof of anything by itself).

And 7. is merely a recall of the previous section : having to duplicate a setting is usually indicative of bad feature splitting, or you need to have a good reason to do so.

All of these are obviously the ideal cases to tend to. In practice, you have to evaluate different candidates and pick the best one, which will rarely fit all the requirements. But to evaluate competing candidates, you first need objective checkpoints and scales. Those are the checkpoints that have become obvious to me over the years.

### The properties of good editing workflows

I recall endless email chains, 9 years ago on the Darktable mailing list (now defunct), regarding how best to reorganize the GUI, in which I was told : "there is no right or wrong workflow, only personal preferences". Absolute relativism is always more dumb than it sounds. That stupidity now carries on on discuss.pixls.us. It's really the same discussions, saying the same things about the same issues, going in loops for 9+ years, and still no tangible solution whatsoever.

First there is the obvious case where you define a parametric mask on, say, _color balance RGB_, and set it to mask-in RGB values between whatever and whatever + epsilon. Then, you are happy with colors, but your image looks a bit dark, so you go raise exposure. Now your parametric mask is invalid and you have to redo it again, because _exposure_ seats earlier in the pipeline than _color balance RGB_, so you shifted your whole range of RGB code values. __This is circular editing__, it is the most preventable loss of time, and if you want to prevent it, there is not much choice : you have to mind your pipeline order. __Circular editing is an objectively bad workflow__. Do I need to justify it ?

It doesn't stop to masks, because many color modules need to normalize HDR white to be able to use color models that can work only with the assumption that white = {1, 1, 1}. Which is why I put those later in the pipeline, after the global brightness and contrast adjustments.

And then, there is the __batch editing case__ : you have a series of images to edit, you want them to look consistent throughout the whole series, but they have minor variations in lighting, contrast, and color temperature. The only way to make that work is to have a first step of image normalization, to handle individual image variations and convert them to a constant state. Once this is done, you can batch-apply the same style or copy-paste the later modules history on top, because that next step will get normalized "constant" inputs and should therefore produce consistent (if not constant) outputs. Failing to achieve that first step of normalization will make the next one completely unreliable and unpredictable, which will defeat the purpose of batch editing. And even with that seemingly-rigid workflow, you will still end up having to do individual fine-tuning if you really want consistent results. __This is not my opinion, it's a simple fact : if you want consistent outputs for a set of image modifications with rigid settings, you need consistent inputs.__

But there is a limitation to that, because when working on HDR signal, you need to setup your HDR -> SDR tonemapping early, so you can actually see what you are doing in your image, without highlights clipping. So there is a first step of taking a view over your content, and then a step of finalizing the bounds of the dynamic range. Which means that you should always setup the global exposure first, looking at midtones (and nothing else), and then setup your working tonemapper for now. So that rule of trying to anchor your workflow 1:1 on pipeline order (itself anchored on input requirements of modules) has some exceptions and can't be applied without discernment.

Which means you should have learned all that. Good workflows come from people who thought about how to adapt to the tools they were using, by looking at them both from the practical and the theoretical vantage point, and not from the happy campers who misconstructed all the habits they took over the years for a workflow. Again, there are things happening beneath the GUI surface, we are not writing letters in a word processing app. For 9+ years, I have been yelling in loops on forums: IT'S A PIPELINE, NOT A DISCONNECTED SET OF RANDOM IMAGE CONTROLS. Order matters. If there is only one thing you understand in this whole blabbering, it should be this one.

And I think Lightroom has done a lot of damage to user's expectations and comprehension by hiding its internal pipeline. But it works for Lightroom because it removes a lot of degrees of freedom to users. Darktable has reused a lot of Lightroom UI semantics while proposing gradually more freedom and direct control over the pipeline nodes (introducing over the years module-wise masking, then module multi-instanciation and relative instances reordering, then raster masks reusable between non-consecutive modules, and finally full module reordering), which has confused old-timers and newcomers alike.

The idea of a pipeline is not even specific to digital photography. Oil painting also has a pipeline of sorts, where you start by preparing your support, coat it with primer to make it less absorbing, then put the outlines, then prepare the background, then layer [your glazing](https://evolveartist.com/blog/how-to-glaze-an-oil-painting/) and finally put the varnish coat. Nobody in his right mind would start with the varnish and end with the primer. Analog photography has obviously a pipeline of chemical bathes, timers, and such. And painting apps like Photoshop, Gimp or Krita have an explicit pipeline, materialized by layers which occlude each other, down to top, with effect layers that are nothing else than parametric pixel filters producing dynamic outputs.

But somehow, raw photo-editing software making it seems disconnected from material realities, it seems ok in digital to work without structure nor method. The "non-destructive" editing workflow might also have been interpreted liberally as [YOLO](https://en.wikipedia.org/wiki/YOLO_(aphorism)) here, which is not helping either. Just because you got <kbd>Ctrl+Z</kbd> doesn't mean everything is suddenly forgiving.

To summarize, good editing workflows are the ones that give you predictable results at each step while avoiding circular editing. They need to be informed by pipeline order, but they can't stick 1:1 to it all the time, and you have to use your judgment to distinguish when to stick to pipeline and when to drift. That leaves very little room for personal preferences.

### The ethics of digital image processing

In analog times, artists worked with physical media, and could perform all sorts of hacks and find new techniques to achieve custom results. It was accessible because you could physically touch the medium you were working with. Artists were free.

Digital art is inherently violent, insofar as it has removed freedom from artists: now they can't find new techniques by themselves, because their art exists as pure information on some computer memory, so they are limited to what the applications propose, and can either beg software editors/developers to consider their needs, or learn how to code and code them themselves (which is quite an entry fee if you have no technical background in applied sciences or computer science). The loss of freedom can be hidden by the fact that anything done in software is reversible, so photographers don't permanently damage negatives, for example. Worse, software can make you more productive, so it's a net win from a business point of view. But it shouldn't hide the fact that artists are put in a more passive consumer position than before, and dominated by those who know how and want to code for them, who get to decide how they will do art from now on.

"Violence", "suppression of freedom", "domination"… You know where this is headed: it's called oppression. A sexy oppression that promises you to achieve professional results in just a few clicks without having to go to photography school. Or, in the context of free software, an oppression that seems benevolent and harmless because nothing to pay and built-in data privacy. But none of that is incompatible with the fact that, _we, the developers_ control what you have the right to do in/to your images, or not, because we control the code. Not because we have been elected by users, not because we are competent for the task at hand (by now, I have demonstrated many times here that the Darktable "team" was nothing but a bunch of clueless idiots with too much freetime), just because we have admin rights on some Github repository. That gives us power, and power comes with accountability.

This has been in the back of my mind for several years now, because it becomes an ethical conundrum too (on top of all the technical ones previously mentioned) to decide what features to add, or to remove, or to refactor. At the same time, trying to please everybody is not possible and would end up overwhelming everybody with lots of niche tools they will never use (again: hiding a tree in the forest). Then, it's also more code to maintain, so more work and a burden for the maintainers in the future, with more threats to the stability of the software. Building a Swiss-army knife that is constantly half-broken is not going to help anybody.

But it remains that removing controls for the sake of simplifying the GUI might just be removing freedom, if we are not sure that similar functionality continues to exist, one way or another, in the software. Artistic freedom is not a commodity, it's a necessary breath of fresh air in societies that are increasingly drifting into (techno-)fascism, once again.

Again, all that has to be arbitrated on a case-by-case basis. Letting image-processing apps go in all directions at once, trying to accommodate all needs in a single app, even the most peculiar, is going to build 5-legged sheep that are impractical to use for anybody. Which circles back to my first section: building generic, versatile and assumption-free tools that can be adapted to many different needs in a factorized way. Industry vs. craftsmanship.

Productivity is achieved through specialization (which, combined with standardization, unlocks automation), specialization is achieved through duplication, duplication creates GUI bloat and clutter, bloat and clutter are the enemies of UX and creativity. The conclusion of all that is: let's choose what is worth specializing, and thus hinder users' freedom. Therefore, productivity is the wrong paradigm: its natural conclusion is oppression.

The opposite of productivity is robustness and versatility. It might be slower (is it though ?), but we can handle a larger variety of cases with the same number of tools, without arbitrarily removing options, thus user's freedom. The concept of robustness has already been developed in the context of social and climate instability by Olivier Hamant, in his book [_Antidote to the cult of performance. Robustness from nature_](https://tracts.gallimard.fr/collections/olivier-hamant/products/tracts-n-50-antidote-to-the-cult-of-performance-robustness-of-nature) (2024). One of his main takeaways is that the cult of performance (or efficiency) only leads to competition, which promotes violence, and in the unstable times to come, we need cooperation. In a society that produces more than enough to cover everybody's needs (but poorly distributes it), performance doesn't win us anything except more profits (again, poorly distributed). But what it surely produces is burn-out, both in people and in ecosystems.

In software, burn-out can come from several things :

1. [technostress](https://en.wikipedia.org/wiki/Technostress) linked to too many applications, tools, standards and paradigms to adapt to,
2. [information overload](https://en.wikipedia.org/wiki/Information_overload), linked to a glut of GUI widgets and tools to navigate,
3. [change fatigue](https://en.wikipedia.org/wiki/Change_fatigue) linked to too frequent updates changing workflows.

To sum up, chasing up performance cascades into a series of drawbacks:

1. from a technical point of view, it means having to build more specialized tools cluttering GUI, which produces information overload,
2. from an UX point of view, keeping clutter and overload reasonable implies arbitrarily choosing whose needs will be covered (probably the majority) and whose needs will be ignored, which is removing artistic freedom to anybody deviating too much from mainstream : this is a first in art history, and no amount of software freedom makes it any less violent,
3. from a marketing point of view, only professionals need productivity, and right now they seem to prefer commercial software in large majority. There is no benefit for open-source to go "win new markets", it should rather strive to cover the left-behind niches. And also, professional photography is slowly dying since the 1980's, so I'm not sure it will stay a substantial market for much longer.

---

_That was a long introduction, but in a World where everybody thinks they can be a designer, it's necessary to completely state the full list of antagonistic design requirements in order to calm down those who think they are the next Leonardo Da Vinci, and give them a sense that the simple idea they got seems bright only because they don't have the full specification note. I have had too many of them in my mailbox and on my issue trackers._

---

## How it is handled in Ansel

_All screenshots have been done on a 15.6'' laptop._

### Global modules presentation

First of all let's recall how the general darkroom presentation [was redone](../modules-groups-redesign/index.md) :

{{< figure src="darkroom.jpg" />}}

- modules tabs got explicit names instead of cryptic icons,
- tabs are ordered (externally) in suggested workflow order, left to right, so users just have to follow the UI as a guide,
- inside tabs, modules are ordered in pipeline order (with a layer "over" logic, bottom to top), which is also the generally advised workflow order. The new "basic" tab is an exception, having modules sections : inside sections, modules are ordered pipeline-wise, but sections themselves are ordered workflow-wise (again, layer "over" logic, so bottom to top),
- the "pipeline" tab or the module order nodal graph disambiguate everything and show a direct view of the pipeline nodes with no intermediate reordering,
- the "favorite" tabs has been entirely removed, as it only pretends to solve bloat by adding more of it.

Meanwhile, central toolbars have been entirely removed, freeing a maximum amount of space for portrait images, on 16:9 and 16:10 screen where vertical real-estate is really more precious than horizontal. The use of a global menu has again allowed to remove many weird icon buttons from the toolbars, and replace them with explicit, textual menu items. So, in full-screen mode, you now loose only the global menu bar height :

{{< figure src="darkroom-2.jpg" />}}

Blending and masking options, being unified between modules, have been moved to the left sidebar. This frees up a lot of vertical space for modules, reducing the need for inner tabs and preventing a lot of clicking around. Note that the blending options are currently being rewritten, after the mask API has been entirely refactored, simplified and extended, so this is only a temporary view.

Ansel views are quite verbose, so to help distinguishing between constant text (labels) and variables (values), a syntax coloring has been introduced :

- constant labels are white,
- variable values are orange.

### Color calibration

In _color calibration_, the 3 tabs _R_, _G_, and _B_ have been merged into the same _mixer_ tab, which now fits entirely vertically. Same for _brightness_, _colorfulness_ and _B&W_ which are now merged into _outputs_.

The _mixer_ tab has now 2 alternative GUI modes, in addition of the typical one (now dubbed _complete_) ;

{{% gallery cols=3 %}}
{{< figure src="color-calibration-complete.jpg" caption="Complete mode" />}}
{{< figure src="color-calibration-simple.jpg" caption="Simple mode" />}}
{{< figure src="color-calibration-primaries.jpg" caption="Primaries" />}}
{{% /gallery %}}

The _primaries_ view has been taken to upstream Darktable, except they made it a stand-alone module, which is stupid : the internal pixel maths are exactly a 3×3 matrix multiplication, which is what _color calibration_ is too, so there was no need for an additional module. This is again a problem solved in the wrong layer : it needed an additional GUI layer in an existing module, they made it a duplicated module.

Instead, Ansel implements it as a GUI layer, meaning the typical channel mixer parameters are converted back and forth to the primary view parameters, and the pixel maths of the module have not changed since 2021. For the sake of a fully invertible transform from the 3×3 matrix to the primaries view, an extra _gain_ parameter had to be added to mathematically close the transform.

The _simple_ view is something that I had on the backburner for several years : re-expressing the not-user-friendly mixer parameters in terms of hue rotation, chroma plane stretching (color contrast) and achromatic resizing. The _U/V stretch_ replace the legacy Darktable module [_color contrast_](https://docs.darktable.org/usermanual/development/en/module-reference/processing-modules/color-contrast/) that works in CIE Lab 1976, and push/pulled the a and b chromaticity. The _simple views_ goes further and lets you define your own uv chromaticity space using the _chroma (uv) axes rotation_ : when set to 0°, u is a green-magenta axis and v is a blue-yellow axis (similar to _color contrast_). But then, you can turn it in whatever you need, for example around -20°, the v axis becomes quite close to a color temperature axis, and the u axis to the orthogonal tint axis. I'll let you with the [documentation](doc/views/darkroom/modules/color-calibration/#simple-mode).

This mode is particularly well-suited to recover overwhelming blue stage lights, much more easily than with the typical channel mixer UI :

{{< compare before="achromatic-coupling-before.jpg" after="achromatic-coupling-after.jpg" >}}
Photo © Reinout Nonhebel, 2018
{{< /compare >}}

1. through compressing the V axis, we compress the gamut heavily on the blue-yellow axis, which is much more gentle on the rest of the gamut than using a global chroma compression,
2. through compressing the U axis, we also compress the gamut on the magenta-green axis, but much gentler,
3. the achromatic coupling hue is set to deep blue. By increasing the amount of coupling, we remap a portion of blue into the achromatic axis, meaning we desaturate and brighten it at once, which helps a lot recovering it in gamut, while still preserving the overall blue feeling. Conversely, the complementary color gets darkened and resaturated, but since that opponent color is yellow, and we compressed the yellow-blue axis through V, then we end up roughly in the same place.

So this _simple_ coordinates rewrite of the channel mixer parameters yields 6 controls instead of 9, and they are much more meaningful and easier to control. This is a UX issue that was tackled purely from the mathematical side, because this is nothing more than re-expressing a 3×3 matrix in a new orthogonal eigenvector basis, rotated on the achromatic RGB axis, meaning it started purely from a linear algebra intuition. Because of the properties of this new eigenvector basis (which are imposed by design), I was able to remove dimensions and turn the remaining ones into more meaningful controls. Which is what I mean when I repeat that you can't solve UX issues by merely looking at the GUI.

### Split-toning RGB

The legacy Darktable [_split-toning module_](https://darktable-org.github.io/dtdocs/en/module-reference/processing-modules/split-toning/) worked in HSL space, which doesn't support RGB code values greater than 1 (so, no scene-referred). Also, mixing colors in HSL is wonky and feels like a toy filters when you start increasing the settings.

At the same time, the current chromatic adaptation scheme in Ansel assumes a single illuminant. However, real scenes always have at least two illuminants :

1. the main light source, which is the primary illuminant, and will mostly weigh on highlights and midtones,
2. colored surfaces bouncing light from the main source, and tinting it, which act as secondary illuminants, and will mostly weigh on shadows up to midtones.

So far, to handle this mixed-illuminants situation, you had to duplicate instances of _color calibration_ and mask them in/out. But trying to cut a binary mask (one region attributed to one illuminant, and the rest to the other illuminant) is brittle because it doesn't account for the light mix happening around midtones.

So the new _split-toning_ module in Ansel proposes two channel mixers and two color temperature corrections, and duplicates the new GUI modes from the _color calibration_. It lets you define the brightness of each illuminant, associates one color matrix to each, and computes a mix of both matrices to apply to each pixel depending on its luminance.

{{< compare before="split-toning-before.jpg" after="split-toning-after.jpg" >}}
© Luc Viatour, 2016
{{< /compare >}}

_At that point, you may think I'm obsessing about concert photography, but it's only because stage lights are the most demanding setup and forgive no mistake in the color pipeline. Those have been unsolved issues for decades; blue lights have been known to break into magenta, and they are the ultimate color benchmark_.

Here, we adjusted highlights temperature for more natural skin tones, while preserving the overall blue surround in shadows. In such a scene, there is no white that you can sample, so targeting natural skin tones is the only guide. Then, you do your best to preserve the spirit of the stage lighting (keep blue blue), while minding the limits of your RGB gamut. For more details, see the [documentation](doc/views/darkroom/modules/split-toning).

### Color primaries

As I mentioned earlier, the Darktable _primaries_ module is a mere duplicate of the channel mixer with a different UI. But there are cases where massaging the whole color space through its primaries is damaging too much the low-saturation range that was perfectly valid. So, there was a need to affect primary and secondary colors (at the vertices of the gamut cube) in a way that excluded the center of the gamut, but still blending the effects smoothly between both regions and preserving image gradients.

This was achieved by building an RGB LUT in new module : _color primaries_.

{{% compare before="primaries-before.jpg" after="primaries-after.jpg" %}}
© Andrea ([source](https://discuss.pixls.us/t/sunset-on-brusvikken-darktable-filmic-vs-sigmoid/))
{{% /compare %}}

This example was designed to make red and orange less overwhelming, but dramatically deepen blues, to showcase how stable the color transform is along edges between diffently-colored surfaces.

Since, internally, the module dynamically builds a LUT by shifting the color of control nodes, users get the possiblity to decide how far from the RGB cube vertices the control nodes sit, through the _gamut coverage_ slider. Then, 3 smoothing parameters allow to blend more or less heavily the color shifts in RGB, and protect more or less neutral colors from the shift. Finally, the module has a 3D LUT viewer:

{{< figure src="primaries-options.jpg" width="350" />}}

The LUT viewer shows color shifts across the whole RGB cube from origin to destination. It can be rotated around the achromatic axis (azimut), or put in a chromaticity plane view (axis tilt = 90°). It can be zoomed, panned and rotated in 3D from mouse events, and sliced to get a better view at a certain depth. Finally, the generated 3D LUT can be exported under a `.cube` cLUT file to be reused in any software supporting those. For more details, see the [documentation](doc/views/darkroom/modules/color-primaries).

### Color equalizer

The Darktable "team" took my [second (unworking) prototype](https://github.com/aurelienpierreeng/ansel/pull/283) for _color equalizer_, didn't understand why it was not working, added post-filtering steps to hide the problems, and released it as if it was their own work, without as much as crediting me. They can keep it : it's shit. And I don't sign my name on shit anyway.

See, the problem of that prototype is that it applied the saturation shift in dt UCS 22 HSB color space, which [I designed in 2021](https://eng.aurelienpierre.com/2022/02/color-saturation-control-for-the-21th-century/). This color space is already used in _color balance RGB_ for saturation, and aimed at finding the right amount of darkening that should be applied to a color when increasing its "saturation" (actually, its chroma, in rigorous color-science terms), as to avoid degrading into unnatural fluo and neon colors, which are the typical pitfalls when adding a lot of "saturation". So, instead of the typical chroma setting that reduces colorfulness at constant luminance or lightness, this saturation formula darkens too. And while that works very well on flat color patches, the issue is images are not flat colored surfaces, but have gradients.

It was reported to me circa 2022 that the _color balance RGB_ dt UCS HSB saturation created a weird achromatic bright fringe between bright, saturated yellow autumn leaves and the deep blue sky behind them. The issue was, at the edge, yellow light from the leaves and blue light from the sky mixed into achromatic (as they should, being complementary colors) due to a slightly soft lens or atmospheric haze. The saturation algorithm darkened yellow and blue on each side of the achromatic fringe, but not the fringe itself, which now stood out brighter. And there is no fix to that; it's not an algorithmic bug: the issue is the color model, that accounts for perception but not for light mixing. So I was yet another author of another broken color space that took me months to develop and 20 hours of computation to numerically fit the parameters of the model.

Trying to implement a color equalizer reusing that same colorspace made these issues even worse because now the effect was driven hue-wise, which meant problems on two dimensions instead of one. To alleviate the problem, I tried to smoothen things out using RGB guided filters. But I couldn't quite find the magic formula to have a proper, robust blend. That's when the Darktable team decided to scavenge the shiny new thing off the shelf, and when I understood that this could only happen in RGB if it was to blend properly and preserve gradients.

{{% compare before="coloreq-before.jpg" after="coloreq-after.jpg" %}}
© baongoc124 ([source](https://discuss.pixls.us/t/sunset-scene-with-darktable-rawtherapee/23139))
{{% /compare %}}

The _color equalizer_ inherits the same interactive cursor as the _tone equalizer_, for direct editing on the image by sampling the hue of the pixel under the cursor, and scrolling will automatically update the graph. Nodes can be freely added anywhere, and from the interactive cursor, adding a node at the current hue is done on right click. 

The _color equalizer_ allows to define a hue-wise color shift for shadows, midtones, and highlights. The color picker allows to see where a region sits between the tonal controls. This allows very fine-grained control which, along with the same 3D smoothing as _color primaries_ provides a very robust way of blending color shifts. The chroma noise that was the main issue, when defining dramatic color shifts, with the legacy Darktable _color zones_ (working in CIE Lab 1976) or with the previous prototype of _color equalizer_ don't show up anymore.

As with _color primaries_, this module dynamically creates a 3D RGB LUT that can be viewed and saved to `.cube` files just the same. For more details, see the [documentation](doc/views/darkroom/modules/color-equalizer).

### Drawing

{{< figure src="drawing.jpg" />}}

Do I need to say more ?

Ansel implements now a prototype drawing module that allows you to draw raster images in a scene-referred pipeline from 32-bit brushes supporting HDR colors (>100%). Brushes support opacity and flow (same as Photoshop), random sprinkles, path smoothing, edge feathering, and can be used in painting, erasing, smudging and blurring modes. Brush size, opacity, flow and hardness can be mapped to Wacom pen pressure and tilt, or to generic cursor acceleration. They use proper premultiplied alpha and save layers into 16-bit float TIFF sidecar files that can be edited in most major drawing apps.

Several layers can be used by instantiating multiple instances of the _drawing_ module and composited to the image using generic Ansel blending & masking options. It can also be used to composite any kind of arbitrary layer coming from any software, as long as it is saved in 16-bit float as a layer in the Ansel TIFF sidecar file. The background image (before the module) can be exported as a background layer if you need a reference image to draw in another software and import the result back.

This was made possible by the new [pipeline architecture](../complete-pipeline-overhaul/index.md) that enabled a real-time mode. Granted, it is still not as fast as Photoshop because, at each brush stroke refresh, there are other modules running after _drawing_ in the pipeline.

That's the ultimate freedom of achieving everything buttons and sliders will never get you, whether it's dodging and burning, fixing damaged parts (clipped highlights, missing areas) or simply mixing photography and painting. For more details, see the [documentation](doc/views/darkroom/modules/drawing).

### Photographic grain

The legacy Darktable _grain_ module was really unsatisfactory, as it allowed only luminance grain and was applied on the lightness channel of CIE Lab 1976 color space. The results were odd, not matching silver halide in any way. I finally implemented the [stochastic grain synthesis](https://eng.aurelienpierre.com/2023/07/stochastic-photographic-grain-synthesis-from-crystallographic-structure-simulation/) that I developed in 2023 in a new module: _photographic grain_. This splits the light field into virtual silver halide crystals and simulates grain sensors stacked on layers. It works for B&W and color grain alike, though I had to take some distances with my initial article to handle color.

{{% compare before="grain-before.jpg" after="grain-after.jpg" %}}
© Alessandro Amato del Monte ([source](https://discuss.pixls.us/t/positano-boring-landscape-using-filmic-dt-3-0/14836))
{{% /compare %}}

{{% compare before="20190424_FUJ2114-nograin.jpg" after="20190424_FUJ2114-grain.jpg" %}}
© Alessandro Amato del Monte ([source](https://discuss.pixls.us/t/positano-boring-landscape-using-filmic-dt-3-0/14836))
{{% /compare %}}

### Filmic

For several years, people have been telling me that the stupid Darktable modules _sigmoid_ and more recent _AgX_ were giving them a bit more control. And none of them has been able to tell me exactly _control over what_. So it took me a long time to figure it out.

_Sigmoid_ and _AgX_ are not revolutions, they are other filmics : 

1. you convert colors using a logarithm or a power shaper, 
2. you slap some S-shaped curve on top, 
3. then you do your best to unfuck what the tone curve has been doing to chroma throughout the process, 
4. and finally you undo the shaper. 

_Sigmoid_ and _AgX_ could have been alternative modes __inside filmic__ : they all go through the same steps with slightly different priorities and strategies. Doing so would have provided one single module for dynamic range compression, whith different modes depending on how much fine-grained control users wanted. Instead of that, they duplicated functionality and added GUI bloat, so users now have to choose between _base curve_ (which was already a duplicated & specialized variant of _tone curve_ itself), _filmic_, _sigmoid_ and _AgX_ to achieve the same task, while none of those actually state the functionality they provide.

The one thing that _filmic_ does best is handling explicitely the boundaries of the dynamic range, which allows to use it for [black point compensation](../../workflows/printing/) when printing. The two others handle them as a byproduct of the contrast setting. Since the developers of the two others don't care about prints, you bet that relieves some constraints on the design, and removes some sliders in the GUI. The one thing that the two others do better is to provide manual color controls to unfuck saturation and hue shift issues through the tone mapping, but I really don't think that level of color granularity belongs in a __tone__ mapping feature : it's a completely flawed workflow and feature split.

Anyway, I finally figured out that the finer contrast control went from the fact that _sigmoid_ directly provides a _toe_ and _shoulder_ nodes control, while _filmic_ uses a global _latitude_ and _offset_ that is cumbersome to use since it links both. This was originally designed to map it to actual film stocks datasheets, because the latitude is a real film thing, in order to maybe emulate real filmstocks one day. That day never came.

So I solved the problem __in the GUI layer__ by adding a conversion between latitude/offset and highlights/shadows settings, and now _filmic RGB_ allows you to manipulate the toe/shoulder nodes position directly : 

{{< figure src="filmic.jpg" width="350" />}}

No pixel math was changed in the process, the module parameters are still the same as before, no new module was created, it's just an intermediate GUI conversion step.

## Conclusion

All of the new modules support GPU offloading through OpenCL. They have been designed for robustness, and I think that goal has been achieved. They have replaced worse previous modules, working in CIE Lab 1976, that have shown their limits and flaws for a long time. You now run a full RGB pipeline in Ansel. Legacy modules are still in the program and will still run for old edits. Also, the colored sliders and hue graphs are color-managed using the display color profile.

The modules I have recently introduced are not shiny new toys to get excited about. They are the conclusion of years of thinking about a consistent, coherent feature splitting that helps the [scene-referred workflow](../../workflows/scene-referred/index.md). That scene-referred workflow is more complex than the previous display-referred one, if only because we need to normalize "white" before going into any LUT or perceptual colorspace, but it's the only way to tackle HDR images, proper alpha blending for masks, and physically-accurate pixel filters that simulate light mixing. Physically-accurate filters are robust and produce organic results, even pushed to dramatic settings. But they are often less accessible to newcomers and disturbing for experienced retouchers used to Lightroom and the likes.

This will be my last post on this website because I retire from development. It has cost me too much : too much stress, too many burn-outs, too many years spent fixing other people's shit and suffering from their bad decisions. I am tired beyond what you can imagine. I hate programming and I hate programmers. I know very few of them who program to build things or to solve problems, most just enjoy too much spending quality time with a computer, and turn into pyromaniac firefighters that you can't trust to design things. I hate technology and so-called "innovation": all of this is just a capitalistic scam designed for infinite growth in a World where resources are limited, and we pretend to solve problems created by technology with even more technology. That's insane. The open-source world inherits the same mindset, including the [techno-solutionism](https://en.wikipedia.org/wiki/Technological_fix), because that's the kind of brainrot 350 years of capitalism produce, even though FLOSS doesn't have the profits to justify it. It is full of lies and full of shit, because _libre_ means freedom only for engineers, and users can go to hell. The problem is, I use to believe in those lies, in those values : they were (and still are) mine. But realizing they are only empty words repeated in toxic "communities" of middle-aged white men with B.Eng, M.Sc and Ph.D was a brutal wake-up call. What do we _actually_ do to empower users who _actually_ need it ? At what point casually requiring to use the CLI is it empowering anybody who can't read code ? We are just contributing to deepen the divide between the computer-litterate elites and the peasants. Remember, we are doing photography software here, not a backend library, not a server infrastructure, but an end-user desktop application.

I have emptied my brain here, to log everything I have learned about image processing design, how I did it, and why. It's important to understand that users, and most developers, know only how one application behaves on their own images. For 8 years, I have received many pathological images that people sent me, showcasing flaws and limitations of the tools. I have around 45 GB of those images on my harddrive, right now. That gives a completely different perspective over actual problems than what all the happy campers may have : I'm the guy who knows what breaks and when. I'm the guy that problems find. I'm the project [Cassandra](https://en.wikipedia.org/wiki/Cassandra). And it's annoying to have to justify all the time, to people who don't see the problems, why _that thing that could be much simpler_ can't be much simpler because there are pathological cases where we need to adapt to input variability. To adapt to input variability, you need user parameters instead of hard-coded constants. Thus GUI bloat. The kind of GUI bloat you cannot avoid without harming usability.

__You can't evaluate the quality of a design if you don't know its requirements__. "Me likes" or "me no likes" is irrelevant. Nobody likes to wear a seat belt, it's still good design if you consider how many lives it saved. Any real critics can only be about how the proposed solution reached the goal, which can happen only if you know the goal. In image processing, the mixed nature of the requirements makes it hard to avoid diving into math and their hieroglyphs at some point, which is where you loose people. But that still doesn't prevent them from giving their useless opinion, starting by "I'm not a programmer"/"I'm not a mathematician"/"I'm not a color scientist"… "BUT"… [cue some random brain noise]. Doing open-source code, where everything is public, allows for that kind of noise all over the place : it's really tiring, and there are many times where I whished the source was closed, just to be able to work in peace. Most of those guys mean well and just want to be part of something, but contributing to information overload is not helping and only contributes to creating fatigue. Communication is where all teamworks looses productivity

Design is not about listening to what people like. If 65% of your test panel likes text written in red, and 72% likes text written over red background, do you write red text over red background ? That makes no sense. We are not doing politics and trying to please electors during a campaign, we are looking for long-term/future-proof solutions to problems. Design is about listening to what people need, which they rarely can clearly express, and finding ways to __factorize those needs__. So the only stats worth computing are on the usecases : how the software is used, what are the most common painpoints, and what is their root cause. Then fix the root cause, which may be far away from the actual manifestation of the problem. That too is a real and precious skill : following the thread of the issue, through hints and smells, to uncover the real origin, and not just patch the end issue or work around it. __"Listening to users"__ doesn't mean listening to every individual and giving them what they individually want : we are not providing therapy. It means listening to the whole userbase, and identifying the commonly-shared needs and pain points as to devise a factored solution that will cover the most needs with the least amount of technology. This doesn't imply to discard anybody deviating from the average by more than a standard-deviation, but those people may have to resort to methods that are not custom-taylored and optimized for their needs.

Design does not exist on an island : there are other image editing apps around. It is again difficult to assess when your design should copy others because users will be used to their UI semantics, and when you should drift apart because the problem you are trying to solve is too different from what the competition does, or your typical user is too different. Open-source is constantly torned between the temptation of copying 1:1 the commercial leaders they so dearly hate (but silently aspires to be), and the craving for outsmarting everyone while reinventing the wheel (often in a worse way). In all that, your Polar Star is to ask yourself, every hour of every day : what is the problem we are trying to solve, and who are the people who face it ? That's how you can adapt things to your audience, rather than falling into [cargo cults](https://en.wikipedia.org/wiki/Cargo_cult) and adopting solutions because they have been successful in a context you don't really understand and is not necessarily yours. 

My main takeaway from all those years is that applying color transforms to pixels in any other space than RGB is doomed to fail, as the _colorbalance RGB_ example shows : while it might sound like a great idea to decorrelate "color" (as in hue/chroma) work from tonal work, it will only ever be a matter of choosing your pain. Increasing chroma at constant lightness degrades colors into fluo, increasing saturation at constant brightness (thus darkening altogether) doesn't respect light mixing theory, and both produce chroma noise due to the instable nature of hue angles. But then RGB is not perceptually-even, and the green-ish range of any HSL/HSV hue scale takes up roughly 30 % of the space, while green is only 1/6th of the perceptual hue ring. The only solution is to handle GUI in perceptual color models, and convert, one way or another into RGB before applying to pixels. Which requires additional abstraction gymnastics for developers. And that's clearly not their strong suit, since the math level is quite low.

I already have a successor, but the future of Ansel will have to be [a cooperative](../../contribute/democracy.md) where users guarantee developers fair working conditions, and developers guarantee users their needs will be covered, reciprocating mutual responsibilities. We can't continue like that, it's not healthy. And, by "like that", I mean on one hand, the "hobbyist developer" with zero accountability, and on the other, the burning-out single dev living under poverty threshold to kinda make things happen. That paradigm has delivered everything it could, it can't be tweaked. The same causes will only lead to the consequences we already know : open-source sucks. If you want more, that paradigm will have to change. Open-source, on one side, is a dictature of developers (those who know and can) over users (those who need), but on the other side, it is exploitation of those who work by those who take. That only feeds mutual resentment, low-key despise and patronizing. Everybody looses.

The alternative is the current enshitification of corporate platforms and the technofascism it is currently enabling. You have been warned.---

# Design : https://ansel.photos/en/tags/design/
---

# Performance : https://ansel.photos/en/tags/performance/
---

# Redesign : https://ansel.photos/en/tags/redesign/
---

# Redesigning the lighttable and the mipmap cache : https://ansel.photos/en/news/redesigning-lighttable-and-mipmap-cache/

Between January 2022 and March 2026, Ansel landed 297 non-merge commits regarding the lighttable grid, its thumbnails, and their rendering pipeline and caching. I tried to make do with the scruffy Darktable lighttable code, only degreased, for as long as I could but unfortunately, it was pure technical debt and it was painfully slow.

Indeed, Darktable "manages" the crappiness of its lighttable by reducing its size: the left and right side panels take a lot of display surface, which leaves even less area for the lighttable to repaint. Since Ansel removed the right side panel, merging its content with the left one and the global menu, there was more surface to paint, more CPU work to do, and the terrible design of the lighttable became all the more harmful.

That project is now complete. This article is a map of what changed, what was removed, what was redesigned, how the new behavior works, and what users actually gain from it.

{{< note >}}
Some application slang used below, for readers who don't live in the source code:

- **Lighttable**: Ansel's browsing view, where you manage, sort, select and inspect many images at once.
- **Darkroom**: Ansel's editing view, where you work on one image in detail.
- **Filmstrip**: the horizontal strip of thumbnails shown below or alongside another view, used for quick navigation through the current collection.
- **Thumbnail**: the small preview image shown in the lighttable or filmstrip, along with badges, overlays and metadata.
- **Thumbtable**: the internal widget managing a whole grid or strip of thumbnails, including scrolling, selection, redraw and thumbnail lifetime.
- **Overlay**: the extra information drawn on top of a thumbnail, such as stars, reject marks, color labels, group borders or metadata.
- **Collection**: the current set of images shown by the lighttable after applying the active filters, search and grouping rules.
- **Grouped images**: several images linked together so they can be shown as one collapsed stack or as expanded members of the same group.
- **Collection filter**: the lighttable tool that narrows the current collection by text, rating, color labels or other criteria. In Ansel, this also absorbed what used to be called culling mode.
- **Mipmap cache**: the subsystem that stores and retrieves precomputed preview images at several sizes, so Ansel does not have to decode and render the source file from scratch every time a thumbnail is shown.
- **History hash**: an internal fingerprint of the editing history of an image, used to know whether a cached thumbnail still matches the current edit state.
{{< /note >}}

## A brief history of bad design

To understand why the 2022-2026 rewrite ended up touching so many files, it helps to look one step further back.

The lighttable view, `src/views/lighttable.c`, had been accumulating responsibilities for more than a decade. It started as the place handling the file-manager grid and basic zooming, then gradually absorbed full preview, sticky preview, culling, panel-state memory, grouping, drag-and-drop reordering, keyboard navigation, overlay policies, filmstrip coordination and a lot of shortcut routing. If you look at the pre-2022 history, many commits are just about keeping these from fighting each other: offset jumps when zooming, preview scrolling bugs, selection not staying in sync, culling and preview re-entering in a half-broken state, panel visibility not being restored properly, group borders glitching, and ratings or color labels moving the grid unexpectedly.

{{< collapse title="For developers, a hilariously bad hack worth mentioning" >}}

- 2015 : [redraw the whole lighttable because some thumbnails didn't refresh properly](https://github.com/darktable-org/darktable/commit/887f2abd07#diff-534e323acd0fde20f32cd1b23cadb652755759d9fdfb348a3866d0112aac6ddaR757-R812)… (also note the Cairo drawing code painting group borders just before SQLite queries and thumbnail fetching from cache… at the discounted price of 3 for 1).
- … [later reverted because it drained CPU](https://github.com/darktable-org/darktable/commit/15f3e2c45f) (no joke…),
- 2020, refactoring: … [made it again at some point as a timeout function](https://github.com/darktable-org/darktable/commit/1c2ae21831#diff-534e323acd0fde20f32cd1b23cadb652755759d9fdfb348a3866d0112aac6ddaL426), which is no more guaranteed to succeed (why 250 ms?), and removed it in February 2020,
- … [made it again and was re-removed in April 2020](https://github.com/darktable-org/darktable/commit/cfe219a700#diff-534e323acd0fde20f32cd1b23cadb652755759d9fdfb348a3866d0112aac6ddaL1050),
- … [because it was moved to the thumbnail](https://github.com/darktable-org/darktable/commit/4f917685fc#diff-2a4577c2a16d72e0dd7c03a4a592660c5acabc169e538d92b11b519ccfedbf60R93-R124),
- … [hardened in October 2020](https://github.com/darktable-org/darktable/commit/0cb239aff4#diff-2a4577c2a16d72e0dd7c03a4a592660c5acabc169e538d92b11b519ccfedbf60R422-R425), aka simply used as a `g_timeout` should be, because it could call a removed thumbnail and cause a segmentation fault (then crash),
- … [until I definitively got rid of it in February 2025](https://github.com/aurelienpierreeng/ansel/commit/609eb38012#diff-2a4577c2a16d72e0dd7c03a4a592660c5acabc169e538d92b11b519ccfedbf60L611), to handle that by… you know, feeding directly the thumbnail pointer to the background job supposed to create the image and let it [call the targeted redrawing when it's ready](https://github.com/aurelienpierreeng/ansel/blob/master/src/dtgtk/thumbnail.c#L394-L552) (and gracefully handle the case where the thumbnail was removed before the pipeline completed).

So this shows a complete inability to keep track of the lifecycle of a cached image (or any data in that godforsaken software, for that matter), while being completely comfortable and relaxed with it. I tackled this issue by simplifying the background asynchronicity, but another option would have been to use a signal. I can't get over the fact that the 3 guys who worked on this over 10 years have been writing C code for twice as long as I have. In 2018, I didn't even know what GLib was.

In case you are interested, the way I tackled it is :

- thumbnail widgets keep a state variable representing their own image validity,
- when GTK wants to redraw the thumbnail, if the state variable says "invalid", the thumbnail widget spawns a background thread computing a pipeline to create the missing image,
- when the background thread returns, it puts the output image into the thumbnail that called it, by pointer, in a (cached) surface, updates the state variable to "valid", then sends a GTK queue redraw event on the widget (not the whole grid),
- if the widget is invisible, nothing happens (GTK doesn't queue redraw events to invisible widgets). The next time GTK wants to redraw the image, it uses the cached surface from within the widget,
- history and metadata changes send an "image info changed" signal that publishes the image ID. The thumbtable has one signal handler listening to that, which finds the thumbnail by ID in the grid (from a hashtable, so `O(N)`) and resets its state variable to "invalid". Again, if 100 images are targeted by the change and all of them are invisible, nothing happens.

The architecture seems more complex, but it's actually less code and it's simply robust: there is no guessing.

{{< /collapse >}}

A COVID-era rewrite attempted to refactor that into `src/dtgtk/thumbtable.c`, which soon became part of the same drift. Once the dedicated thumbtable abstraction appeared, it became the traffic controller between collection state, scrolling, offsets, active images, keyboard navigation, drag-and-drop, smooth scrolling, overlay visibility, zoomable layout, culling synchronization and filmstrip synchronization, __reinventing many native GTK features in a worse and incomplete way__ (the typical Darktable way). The pre-2022 history is full of commits fixing mouse-over after scroll, recalculating rows and offsets, keeping selected images visible after collection changes, avoiding quadratic lag on Home/End, fixing zoomable alignment, making hidden or collapsed images not break the active-image logic, and limiting the number of signals emitted because too many independent updates were being chained together. In other words, `thumbtable.c` had already become the place where view logic, thumbnail lifetime and collection navigation were forced to meet, long before the 2022 rewrite started.

The thumbtable refactor made a new file appear, when the old lighttable and filmstrip code started being factored into a common thumbnail widget and thumbtable as the base object of all that : `src/dtgtk/thumbnail.c`. That was the right direction in principle: instead of drawing thumbnails separately in several places, one widget could centralize activation, selection, overlays, stars, reject flags, drag-and-drop, group borders and zoomable behavior. But it followed a similar path very quickly : the pre-2022 history shows how fast that common widget became overloaded. It was soon dealing with extended overlays, MIPMAP-updated callbacks, culling junctions, scrollbars, active-image handling, filmstrip callbacks, full-preview navigation and CSS-tuned icon placement. In other words, `thumbnail.c` did not merely draw a thumbnail. It became the shared fault line between lighttable, filmstrip, culling and preview.

The filmstrip started as a dedicated navigation strip, then gained centering on the active image, drag-and-drop, map interactions, copy/paste operations, smooth scrolling, HiDPI fixes, CSS customization and eventually parts of the new thumbnail callback system. Again, none of those features is unreasonable on its own. The problem is that filmstrip ended up with just enough custom behavior to diverge from lighttable while still trying to reuse part of the same thumbnail machinery.

On top of that, there was the inconsistent "selection" paradigm in Darktable, sold as a "no click workflow", so many write operations could be handled without explicitly selecting the image(s) to affect. That led to a lot of unwanted effects and accidents, resulting in data loss (attributing the wrong star rating to the wrong image, which would make it disappear from the current collection if you filtered it by rating) and tedious undo sessions. But the whole heuristic of image selection was brittle too: moving the mouse in the window could steal the focus from the picture you explicitly locked with a click or a keyboard selection, but not always.

Not to mention, that "act on mouse hover" triggered several SQL queries into the library database, when hovering a new image, to get updated image metadata and refresh the content of the metadata modules (_metadata_, _EXIF & IPTC metadata_, _tags_). This is all because they couldn't be cached, and they couldn't be cached because Darktable is unable to track the lifecycle of its data, so it needs to refresh everything all the time. And it will drain your battery doing so.

So when the rewrite started after having forked Ansel in 2022, the issue was not that one bad commit had broken the lighttable. The issue was that three layers of history were stacked on top of each other:

- `lighttable.c` still carrying the baggage of several (useless) browsing modes, coded there as second thoughts,
- `thumbtable.c` concentrating scroll, offset, active-image and collection-navigation logic, that partly overlapped with the collection and selection backend,
- `thumbnail.c` concentrating more and more shared GUI behavior plus SQL code.

That is why the later work had to be architectural. At that point, there was no realistic way to keep fixing the symptoms one by one.

## Front-end changes

### Culling was never really (conceptually) a separate view

One important piece of that subtraction was the [culling mode](https://docs.darktable.org/usermanual/development/en/lighttable/lighttable-modes/culling/). In February 2023, the dedicated culling and preview views were removed because they had become badly interleaved with shortcuts, view switching and special-case branching. The code was entangled and cumbersome, but the real problem was in the design itself: it was an over-engineered solution to a much simpler problem, solved in the wrong layer.

The need was to isolate an arbitrary set of pictures that weren't necessarily contiguous in the current collection, in order to decide which one would be the keeper. That didn't need a new layout (or two…); it needed a filter to restrict the collection to an arbitrary selection. We already had filters to restrict collections by rating, color label, edited/unedited status, etc.

That difference matters. A dedicated culling view duplicates problems that the lighttable already has to solve: which image set is active, how selection is restored when exiting the culling view, how shortcuts are routed, how zoom and thumbnail state are initialized, and what happens when you move back to the grid. Every improvement then has to be implemented twice, once in lighttable logic and once in culling logic, and both versions drift apart. Not to mention, the culling mode had a static mode and a dynamic mode, which differed both in how you interacted with them and in their implementation, and very few users understood what they were about.

In Ansel, the culling came back as a collection filter, first as a simple way to narrow the current set, then later explicitly as the _Restrict to selection_ filter button. That means the user-facing intent stayed the same (reducing current collection to an arbitrary selection of images), but the implementation changed completely: instead of entering a special view, you stay in the lighttable and tell the filtering tool to show only the currently selected images or only the images matching the current narrowing criterion.

{{< figure src="filters.jpg" title="The new unified filtering toolbar" >}}
The new, unified filtering toolbar. Filtering criteria are inclusive, and the icons behave as check buttons: uncheck to hide matching images, check everything to show everything (a contextual menu on right click gives you a shortcut to do so in one step). Tooltips appear on hover for more details. The first icon refreshes the current collection depending on filters. For example, if you show images rated 3 stars and you demote an image to 2 stars, it will not be evicted automatically from the current collection until you manually refresh it.
{{< /figure >}}

For users, the benefit is practical. Culling is now composable with the rest of the filtering logic instead of living outside it. You can combine it with ratings, color labels or text search because it is just another collection filter. Shortcut handling is simpler because there is one fewer special view competing for shortcuts ownership. And fixes to scrolling, selection, thumbnail lifetime or overlay behavior automatically benefit culling too, because culling is now using the same lighttable infrastructure instead of a parallel one.

That was also an opportunity to overhaul the filtering toolbar, which mixed buttons and logic: a combobox list for ratings (from _rejected_ to _5 stars_), associated with a comparison combobox list (`≠ = > < >= <=`), but toggle buttons for color labels. Darktable 4.0 replaced that with an over-engineered set of configurable widgets using non-standard GUI paradigms that needed to be explained in tooltips. Ansel has flattened the design: everything is a toggle button working in an "include" mode inside its group, allowing complex selections without complex GUI:

1. you have 3 groups (ratings, color labels, edited status), plus _restrict to selection_ and text search,
2. between those groups, the filters are exclusive, meaning they are a logical `AND`,
3. inside those groups, the filters are additive, meaning they are a logical `OR`.

For example, in the screenshot above, everything is toggled on, so the filtering is in practice disabled (we let everything in). In the example below, we filter in all images that have a color label set and have already been edited, regardless of their rating (all rating buttons are toggled on):

![](filters-2.jpg)

### Selection : mixing backend and GUI states always bites you in the ass

Grouped images were another hidden fault line. Part of the old logic tried to account for grouping in SQL queries, when generating collections of images from the library database, but group borders, hover states, selection behavior and actual visibility are GUI concepts. That mismatch created subtle bugs: all images from a group could be selected, or some ignored according to heuristic rules that did not match what the user saw on screen.

I fixed that by moving group-display decisions back into GUI code. SQL provides the list of images; the interface decides which grouped members are visible, collapsed or hovered. After that, the features added in 2025 became much simpler: group borders could be re-implemented, made optional, extended to the filmstrip, and made more visible on hover. Tooltips could be populated lazily only when the user hovers them. One SQL request per thumbnail at init time could also disappear because the interface already had enough local state to know when group information was actually needed.

For users, the benefit is not abstract. Group borders now mean what they show, selections match exactly the visible group state, and grouped images no longer feel like they are half managed by the database and half by the interface.

Another hidden problem was selection. In the old code, selection was too entangled with collection internals, raw SQL access and fallback logic. That sounds harmless until an image is evicted from the current collection, a group is collapsed, or a view switch happens while another part of the code still assumes the previous selection model. In Darktable, image selections were handled as a SQL backend since they are stored in the library to be restored at the next session. This is not a problem until the selection __backend__ is hacked to guess what images are visible __in the GUI__, and the GUI actually handles the selection through the backend. This is the worst possible split of functionality between backend and GUI layers.

This is why 2025 contains a whole cluster of selection-related commits: rewriting the selection API, removing the old `main_image` notion, unifying the active-image getters/setters, fixing range selection, restoring selection state across view switches, and adding rowid fallbacks when `scroll_to_selected` can no longer find the original image id. The rewrite changed the architecture: the thumbtable now computes visible, user-meaningful selections and passes a list of image ids to the selection layer, instead of the GUI trying to reverse-engineer meaning from collection state after the fact. The selection SQL API has become a simple "save to/restore from database" backend.

The concrete user benefit is that `Shift` + click range selection and "image to act on" are _What You See Is What You Get_: you can't select an image that is not visible on screen, and the backend doesn't have to guess what is visible. And scrolling back to the selected image now fails much less often in edge cases. The hidden problem was not one broken shortcut; it was that selection semantics lived in the wrong layer.

The fix is to clearly split what belongs to backend and library management from what belongs to GUI states and interactions, and to provide a rigid interface between both. That makes the different parts of the code better enclosed, immune to changes in the other parts, and managed centrally.

### Hover and focus glitches came from several valid systems colliding

Mouse hover, keyboard hover, focus and click handling were each implemented for legitimate reasons, but they did not share a single arbitration point. Darktable now even has a proud marketing pitch written in the background of the lighttable, when the collection is empty, praising that "click-less" workflow that allows you to overwrite image metadata without wanting or even knowing it. That feels to me like a car company advertising a car designed so you can use the steering wheel with your knees (and use your hands to drink beer).

That is why older lighttable behavior could feel inconsistent without any single obvious bug: it was broken by design. Clicking on an overlay button to rate the image could also select the whole thumbnail, but rating from keyboard would not. Keyboard movement and mouse movement could both think they owned the "current" image and trigger all sorts of destructive metadata changes merely by hovering the picture and without asking for confirmation. Filtering images by star rating and inadvertently changing the rating of an image could make it disappear unexpectedly. Hidden widgets could still receive hover-related logic. GTK focus could decide one thing while the view layer expected another.

The fixes here were less about adding new features than about choosing one owner for the state. Over-state dispatch was centralized. Keyboard browsing gained a sensible starting point and an explicit cancel path with <kbd>Shift</kbd>+<kbd>Ctrl</kbd>+<kbd>A</kbd>. Focus display was made explicit. Hidden widgets stopped participating in hover logic they could not display. Clicking on overlay buttons stopped leaking into thumbnail selection. No more metadata (over)writing is done without an explicit button push, hover events are all read-only.

Ansel has two simple rules : 

- any action that will (over)write (meta)data is made only on explicitly selected images,
- explicit selection is made only by interacting with something "hard": a mouse click or a keyboard keystroke.

Then, hover events are reserved to read-only events.

The benefit for users is that the lighttable reacts more like a single interaction model. The hidden problem was not that hover was broken. It was that several kinds of hover were simultaneously "right" and therefore collectively wrong.

## Backend changes

### Reparenting the thumbtable between lighttable and filmstrip

One of the more deeply hidden problems was widget ownership. In Darktable, the same thumbtable grid was reparented between lighttable and filmstrip contexts. On paper, that avoided code duplication and widget proliferation, and that sounds clever. In practice, it made scrolling state, thumbnail initialization, garbage collection and event lifetime depend on where the widget had most recently lived. Not to mention, reparenting was slow, so going back and forth between lighttable and darkroom was delayed by around 1 s, the time needed for GTK to recompute new thumbnail sizes, possibly ask the cache for new thumbnails, and redraw widgets.

This surfaced in very concrete ways. Scroll position could jump or become inconsistent. Thumbnails could be hidden, shown or destroyed at the wrong moment. Garbage collection became harder to reason about because the widget hierarchy was not stable. Of course, all that was solved commit after commit, but leaving the code in a state of unmaintainable complexity.

The real fix was to stop trying to be clever. Lighttable and filmstrip now have separate thumbtables. That means a little more explicit structure in code, but far fewer accidental states. The benefit for users is visible in steadier scrolling, fewer layout glitches on view changes, and a filmstrip that behaves like a proper sibling of the lighttable instead of like a reused fragment.

### Menu and layout changes were not cosmetics

The global menu becoming truly global in April 2025 was not a branding exercise. Before that, many commands were implemented as if they belonged to local modules even when they were really application-level actions. Some of them were hidden entirely behind keyboard shortcuts and known only to those who read the documentation. Many of them, I discovered only when removing their code. The same was true of image information, view switching and old view toolboxes. They lived in places that were easy to code around once, but hard to maintain coherently across views.

Moving image information into the global menu bar, moving the view switcher to a clearer top-level location, and later removing the bottom-center toolbar were all consequences of the same diagnosis: the center of the interface should belong to image interaction, not to historical leftovers. The timeline removal and the decision not to show an empty filmstrip are smaller examples of the same logic. Even `Enter` opening the selected image in darkroom fits here: it aligns behavior with the primary job of the lighttable instead of preserving accidental historical habits.

The user benefit is not "the menu is nicer". It is that commands are found in places matching their scope, and the lighttable itself has fewer controls competing with the grid. Not to mention, "modules" that are actually (non-uniform) grids of buttons are just menus in disguise with a worse design, so they are turned into menu entries now.

The global menu was also the opportunity to bring to the GUI features that only lived in hidden shell scripts (!) until now: preloading the thumbnails for the current collection and purging thumbnails from disk cache. Those were requested for years in the GUI, but fit nowhere in the module-first Darktable UI design.

But… that right sidebar of modules that didn't want to be menus was a blessing, because it removed a lot of area to be painted with images and made the thumbnails smaller. Removing it made the bad performance of thumbnail drawing appear in all its glory: nothing was cached, neither image surfaces nor metadata, and everything was refetched all the time. The rewrite was inevitable; the architecture was broken.

### Zoom only became usable after thumbnail sizes were made consistent

The early zoomable-lighttable attempt failed for a structural reason: thumbnail geometry, cached mipmap sizes and view-level navigation did not share the same model. That is why the feature had to be removed first. If I had kept polishing it in place, the result would only have been a better-looking broken feature.

The later work fixed the dependency chain in the opposite order. The view backend gained the scaffolding for lighttable zoom, then the thumbnail cache was taught to reason more coherently about thumbnail sizes, a full-resolution thumbnail API was prepared, then clamping, panning and barycenter-based auto-pan were added on top of that. Only once the lower layers agreed on image size and retrieval strategy did 200% zoom become a defensible feature.

The benefit for users is that zoom is now tied to the cache and to navigation instead of fighting them. Dragging to pan, `Shift` + drag across visible thumbnails, and automatic pan toward the details barycenter all work because the underlying pipeline knows what zoomed thumbnails are supposed to be.

The confusing meaning of "zoom" in the lighttable has been updated: the term "zoom" was used both for outer zooming (number of images per row, which indirectly affects their visible size) and for inner zooming (magnification within the image frame). So now we have _columns_ for the outer zoom, and _zoom_ for magnification.

{{% figure src="zoom.jpg" %}}
Don't mind the cross in place of the `-` symbol in the columns spinbutton, it's a [bug of GTK with KDE/Plasma theme Breeze](https://gitlab.gnome.org/GNOME/gtk/-/issues/7080).
{{% /figure %}}

This made it possible to implement a feature that had been in my mind for a long time: barycentric auto-panning upon thumbnail magnification.

{{< figure src="zoom-before.jpg" caption="Zoom to fit" />}}
{{< figure src="zoom-after.jpg" caption="Enabling zoom at 100 %" />}}

You can see that, when enabling 100% zoom, images are automatically aligned on their content despite having completely different framing and aspect ratios. This uses wavelet-decomposition feature analysis, from which we compute the coordinates of the details barycenter. It's not perfect because it doesn't get the same part of the face across images, but it gives us the face in all instances. That's a project I had on the back burner for a long time, but it was not possible with the previous lighttable design.

Needless to say, dragging into zoomed-in thumbnails has been reinstated, and dragging into __all zoomed-in thumbnails__ has been added too (<kbd>Shift</kbd>+Drag). This feature has been long asked in Darktable but was clearly not possible in their shitty design.

### Self-inflicted metadata slowdown

The old lighttable kept asking the database the same questions one thumbnail at a time. On a small collection this is easy to miss. On a larger one, it surfaces as micro-stutters when opening the view, showing overlays, hovering groups or refreshing history-sensitive data. The hidden problem was not raw SQL being slow in absolute terms; it was the repetition and timing of those queries.

The fix came in stages. Read-only metadata started being cached from image structures. Then, in February 2026, metadata for a whole collection could be fetched with one SQL query instead of one query per image. The thumbtable could seed the image cache from the collection it was about to display. Thumbnail info caching was refactored into a helper and later merged back into `dt_image_t` so metadata did not have to bounce between parallel structures. Even small changes such as removing history-hash pings on redraw matter here, because they cut invisible round-trips from hot paths.

A lot of useless per-thumbnail SQL queries have been removed, along with SQL code in lighttable GUI code, so functional layers are now split properly.

The user benefit is exact: opening large collections stalls less, overlay-heavy displays hesitate less, and history or metadata updates propagate with fewer visible pauses.

### The mipmap cache needed to be fixed too

The mipmap cache is the layer that loads RAW images and pre-generated thumbnails into the RAM, and empties them when memory becomes scarce. That was one of the deepest hidden sources of visible bugs. When everything aligned, it worked well enough. When the source file was smaller than expected, when an embedded JPEG preview was odd, when the history hash was undefined, or when disk cache invalidation lagged behind edits, the user would not experience "a cache bug". They would see stale thumbnails, broken previews or images that refused to refresh.

That is why so much of the 2025 work in the cache looks surgical. Buffer allocation logic was rewritten because ownership was too tangled. Handling of undersized inputs was improved because input size assumptions leaked too far. Embedded previews stopped being discarded so aggressively because too-small previews were still useful for consistency across zoom levels. Sidecar JPEG previews started being used when present instead of the RAW embedded thumbnails. Cache invalidation was hardened, and cached thumbnails were really removed from disk when they should be. Regenerated mipmaps started writing their hash back to the database in sync with history state, and the undefined-history-hash case stopped leaving mipmaps unsaved.

The sneakiest bug I found was buried in the nonsensical complexity of thumbtable, view and mipmap cache interleaving. While rendering thumbnails was deferred to a separate thread for performance (as it should), and more than one thread could be started to (allegedly) process several thumbnails at once (possibly using several GPUs), all the processing threads and the GUI were actually competing to lock the mipmap and image caches, which only made the GUI stall while pipelines were running. It's only because I worked under the assumption that I rewrote things by the book, and I knew the by-the-book way _should be faster than that_, that I kept digging until I found why it still wasn't as fast as expected, simplifying everything layer by layer in the process.

The benefit for users is that the cache is more trustworthy. After edits, the lighttable is less likely to show an obsolete thumbnail. On awkward files, preview generation fails less often. On repeated visits to the same collection, the disk cache behaves more like a cache and less like a stale screenshot archive. But all that, without having to refresh/recompute/redraw everything all the time just to be sure.

Also, the options to use embedded JPEGs or force a recompute were moved from preferences to the global menu and can be changed during runtime:

![](display-embedded.jpg)

This is another example where cleaning up and simplifying the backend paved the road for frontend extension and new features that just make sense.

### Thread-safety and race conditions

A classic reason these bugs lasted so long is that they required the user to be faster than the code: scroll quickly, leave a view while thumbnails are still being built, resize while a background surface is still being produced, or close a widget just before a worker thread pushes an update.

That is why the 2025-2026 thread-safety work matters. Thumbnail fetching was moved deeper into background jobs, then later explicitly deferred so darkroom rendering could keep priority. Thumbnail destruction was moved to safer cleanup points. Background jobs producing thumbnails learned to cancel themselves when the widget they served disappeared. Image surfaces were protected by mutexes. Freed pointers were nullified. Image buffers moved to better-managed allocation paths. The point of all this was not "more threads"; it was to stop old threads from writing into dead state or competing for locks.

The user-facing result is fewer segfaults, fewer random glitches during fast scrolling, and fewer cases where the interface appears to race itself under load.

Several thread locks, added throughout the years to patch problems, were removed too. Many were redundant (but didn't show it under the crazy complexity of the whole thing), some were actively damaging performance, and all of them hid bad design. We now have fewer locking points, but the result is more legible and more robust.

As a result, thumbnail widgets are now fully enclosed objects. They manage internally their own thumbnail rendering pipeline threads, which interact directly with their own cached image surface, so they can spawn or kill them themselves. This cached image is invalidated only when the image history changes, which is made explicit by the development history backend. Because the thumbnail widget knows its own state (visible or not, in need of image refresh or not, size, focus-peaking mode, etc.), it becomes a lot more robust than trying to handle all those things from higher-level layers that fail to communicate with each other.

This was not possible with Darktable design because it kept adding/removing thumbnail widgets dynamically to the current lighttable view, depending on the floating line position, which was an attempt to handle the slowdowns from all the threads that competed for cache access. But also, Darktable thumbnail widgets tried to immediately acquire an image from the mipmap cache, when they were created, which made CPU and memory I/O usage spike, and effectively froze the UI. But "fixing" that by reducing the life-expectancy of thumbnail widgets made it impossible to let them manage their own state internally, so it had to be done by high-level layers, which had to communicate between each other to update states, which did happen in some places but at the expense of unbearable complexity.

Instead of that, Ansel thumbnails are all initialized at once, but they lazily acquire an image from the mipmap cache only once they become visible, and then cache it internally. This keeps scrolling the thumbtable responsive, even with a collection of 500 images that are all generating their image.

### Import previews improved as a side effect

The import window was affected by this work for the same reason the lighttable was: it needs fast preview extraction, metadata inspection and sane fallback behavior on partially-supported files. Once the mipmap and thumbnail paths were rewritten, raw preview loading in import became faster, and TIFF/DNG support improved too.

This is a good example of why the rewrite had to happen low in the stack. If the underlying preview machinery is inefficient or fragile, both lighttable and import inherit the same pain. Once that machinery was rewritten, both benefited.

## Global architectural changes

In my [previous article](../complete-pipeline-overhaul/index.md), I showed how the working pipeline cache makes going back and forth between lighttable and darkroom nearly instant, because it doesn't need to recompute the whole image. The work done here on the GUI solves the same problem of view switching delays, but at the GUI level. So there is no lag when switching between both views.

This is important because the lag of view switching has been used as an excuse to duplicate features (modules/toolboxes) between the lighttable and the darkroom, which only increases GUI clutter. So this whole backend improvement makes it possible to improve the GUI design by specializing each view for a single task:

- metadata handling for the lighttable (plus obviously culling),
- image editing for the darkroom.

As a result, the _metadata_ and _tags_ toolboxes have been removed from darkroom.

## How many lines of code did the rewrite spare?

Counting only non-comment, non-blank lines with `cloc`, and comparing the last tree before January 1, 2022 to the current tree for the main files discussed here, the rewrite spared **4,714 lines of code** overall.

The scope for that count is:

- the whole `data/themes/` directory before 2022, compared to today's `data/themes/ansel.css`,
- `src/common/mipmap_cache.[ch]`,
- `src/dtgtk/thumbtable.[ch]`,
- `src/dtgtk/thumbnail.[ch]`,
- `src/views/view.[ch]`,
- `src/views/lighttable.c`,
- `src/libs/collect.c`,
- `src/libs/tools/filter.c`,
- plus files deleted outright by the redesign or collection-GUI cleanup: `src/dtgtk/culling.[ch]`, `src/libs/tools/view_toolbox.c`, `src/libs/collect.h` and `src/libs/recentcollect.c`.

Within that scope, the total went from **15,257** code lines before 2022 to **10,543** today. The rewrite therefore spared **4,714** code lines overall. The biggest savings came from deleting the culling view (`src/dtgtk/culling.c`) outright (-1,406 lines), shrinking the lighttable (`src/views/lighttable.c`) (-976), removing 7 old theme stylesheets and collapsing them into one (-864 across `data/themes/`), shrinking `src/dtgtk/thumbnail.c` (-460), `src/dtgtk/thumbtable.c` (-458), deleting `src/libs/recentcollect.c` (-358), and shrinking `src/views/view.c` (-314). Some files did grow, especially `src/libs/tools/filter.c` (+215) and `src/libs/collect.c` (+157), because part of the point was to move special-case behavior such as culling and old collection GUI branches back into simpler shared infrastructure rather than keeping them in parallel views and side modules.

Line count is not a quality metric by itself. Plenty of rewrites merely move code around. But here the number matches the design change: less duplicated behavior, fewer parallel views, fewer compensating hacks, and fewer places where the GUI, the view layer and the cache all had to solve the same problem twice.

Everything I fixed, I fixed it by simplifying the logic and the code. There was no workaround allowed.

## Benchmarks

All runtimes were computed on a Lenovo ThinkPad P51 laptop (Intel Xeon CPU E3-1505M v6 @ 3.00GHz, Nvidia Quadro M2200 GPU with 4 GB VRAM, 32 GB RAM, 4K display), CPU in performance mode, Linux Fedora 41 with KDE/Plasma desktop. Pixel pipeline runtimes are not compared (out of scope; see [the previous article](../complete-pipeline-overhaul/index.md)). Ansel Master is taken at commit [09749f1d](https://github.com/aurelienpierreeng/ansel/commit/09749f1da2c97cd54b62a67e169310f0d304724c) (Feb. 21, 2026).

{{< table >}}
| Description | Ansel Master | Darktable 5.0 |
| ----------- | ------------ | ------------- |
| Time from app startup to last lighttable thumbnail drawing (same collection) | 2.12 s | 7.49 s |
| Time to switch from lighttable to darkroom (same image) | 0.2 s | 1.2 s |
| Time to scroll (start->end) through the same collection of 471 images* | 0.7 s | 5.0 s |

*: thumbnails preloaded in disk cache in both cases, 5 thumbnail columns per row, 4K resolution, no right sidebar.
{{< /table >}}

As a "fix", Darktable 5.x blessed us with a gorgeous splash screen, which is a confession more than anything.

The following have been measured on battery, in powersave mode, with the application sitting idle (no user interaction) for 5 minutes, using Intel Powertop. The baseline consumption of the whole idle OS is 1.6 % CPU. (Power is given for the app only, % CPU is given for the whole system):

{{< table >}}
| View | Ansel Master | Darktable 5.0 |
| ---- | ------------ | ------------- |
| Lighttable | 1.8 % CPU, power: 0.85 mW | 2.7 % CPU, power: 103 mW |
| Darkroom   | 1.8 % CPU, power: 7.65 mW | 1.8 % CPU, power: 22 mW |
{{< /table >}}

These figures represent the baseline power consumption of the GUI alone (GTK, background workers, scheduled timers, etc.). Darktable is leaking performance through the GUI, and the tedious work done in 2023-2024 on optimizing pixel processing modules for an extra 15-50 ms is completely irrelevant.

## Conclusion

At this point, I am firmly convinced that the Darktable "project" only attracts "developers" who wouldn't be able to make a cognitive distinction between _GUI_ and _backend_ even if their life depended on it. So GUI issues are solved in the backend, backend issues are solved in the GUI, and this makes code complexity grow out of control with time, which later justifies adding new features by hacking the least amount of code in a codebase that nobody understands anymore. Not to mention, none of that was documented, so I had to reverse-engineer it painfully over several years, recursively simplifying a bit here and a bit there, until it finally converged to a clean overall logic.

Needless to say, all the workarounds and "quick fixes" that were added have been removed. All of them dated from after 2020, which shows a concerning trend in code quality degradation.

This cleanup project stole 4 years of my life, gave me no pleasure, and the people who introduced all the regressions I painfully fixed need to be held accountable for the consequences of their actions. There is a huge difference between not having enough time to do things properly and consuming a lot of man-hours to make things worse. And then, refusing to admit that you made things worse, refusing to accept that there is a problem, and feeding your confirmation bias by listening only to happy-camper feedback is the ultimate proof of stupidity.

Darktable has degraded into shit, and I just explained, technically, why. The Darktable team and its [sidecar forum of tech bros](https://discuss.pixls.us) would like to make people believe that I got angry at them because they wouldn't accept my changes, and that it's all an interpersonal problem. For the layman who doesn't understand what I wrote here and in the previous articles, it's easier to believe in interpersonal anger than to understand how a succession of bad technical decisions over several years made me the victim of the problems they created, because I was the only full-time guy here, depending on it for a living. I got angry at them because they were shitting again and again on my doorstep, and I had to repeatedly clean up. This is a form of violence that is really hard to see and to recognize because it doesn't manifest materially: it's a way of making your life harder, daily, step by step, just because a bunch of amateur middle-aged guys with no skills wanted to be part of something cool without realizing the detrimental effect of their contributions on the whole project.

And it fell to me to clean up the mess because I was apparently the only one to care about the regressions, the weird random bugs and crashes, the worst GUI "innovations" that deterred even my own wife from using Darktable because it's just overwhelming, and all the new slowdowns that keep piling up over the years. As Chris Elston said to me on the Darktable IRC chat, in 2022, before I left it forever, about stuff I had fixed in 2019 and they broke again in 2022: "shut up and fix it". If that's not violence, I don't know what is. And now they are trying to spread the word that I was the toxic one. The whole team is toxic. Their careless working culture is toxic. Their way of getting excited about everything, as long as it's new, without pondering the maintenance cost, feature duplication and overall user overwhelming is toxic. Their lack of concern for the future of the project and for the consequences of the choices they make is toxic.

And what is especially toxic is that, on discuss.pixls.us, every post that says my praise or the praises of Ansel gets flagged and hidden. There is no free speech on a free software forum. They just turned communism into stalinism. 
---

# Split-toning : https://ansel.photos/en/doc/views/darkroom/modules/split-toning/

Blend two CAT16 plus RGB mixer corrections across brightness keyframes.

The _split-toning_ module applies two independent color transforms:

- one keyed on the dark tones,
- one keyed on the bright tones.

Each keyframe combines:

- a chromatic adaptation defined by a correlated color temperature in _CAT16_,
- a 3×3 RGB channel mixer working in the pipeline working RGB space.

The two transforms are then interpolated according to the scene luminance of each pixel. This makes the module suitable both for creative split toning and for technical correction of mixed illuminants, for example cool bounce light in the shadows combined with a warmer direct illuminant in the highlights.

## Scene-referred placement

The module is designed to run in linear scene-referred RGB and is inserted before [_color primaries_](./color-primaries.md) in the pixelpipe.

The keying metric is the scene luminance \(Y\) derived from the current working RGB profile. This means the transition between the two keyframes is driven by brightness rather than by hue.

## General principles

The user interface contains two tabs:

- _dark_ (shown as _shadows_),
- _bright_ (shown as _highlights_).

Each tab defines one brightness keyframe in EV. By default:

- the dark keyframe sits at `-16 EV`,
- the bright keyframe sits at `0 EV`.

For each keyframe, the module first builds a full 3×3 transform:

1. a CAT16 chromatic adaptation derived from the selected temperature,
2. followed by the selected RGB mixer matrix.

The full transform is then blended over brightness as follows:

- below the dark keyframe, the dark transform fades linearly from identity up to full strength,
- between dark and bright, the module linearly interpolates from the dark transform to the bright transform,
- above the bright keyframe, the bright transform fades linearly back to identity.

The fade-out distance outside the `[dark ; bright]` segment is equal to the distance between the two keyframes.

This is important because the module does not blend colors after correction. It blends the full transform matrix itself, which generally gives more natural transitions than mixing already-corrected RGB values.

## Preview

At the top of the module, a preview strip shows a linear gray gradient processed through the current split-toning settings.

This preview is normalized around the current keyframes so you can read at a glance:

- where the dark transform starts to appear,
- where the interpolation happens,
- where the bright transform fades out.

The preview colors are projected through the current display profile, so they reflect the actual monitor rendering rather than raw working-space RGB.

## Module controls

Each tab contains three sections:

- a brightness keyframe,
- a CAT16 temperature,
- one of three RGB mixer GUI modes.

### Brightness

brightness
: Set the brightness keyframe in EV for the current tab.

: The slider includes an area picker. Sample any region in the image and the module will measure the average luminance of that area, convert it to scene \(Y\), then map it to EV to place the keyframe automatically.

: Use this to anchor the _dark_ keyframe on a representative shadow or the _bright_ keyframe on a representative highlight.

### Temperature

temperature
: Set the color temperature of the illuminant assumed for the current keyframe.

: Internally, this builds a CAT16 chromatic adaptation in the current working RGB space. Lower temperatures bias the correction toward warmer light assumptions, higher temperatures toward cooler ones.

: Because the temperature is only one part of the full transform, it can be used on its own for illuminant correction or combined with the channel mixer for more stylized color remapping.

### Mode

mode
: Choose how the RGB mixer matrix is exposed in the GUI.

: The following modes are available:

- _Complete_,
- _Simple_,
- _Primaries_.

These are only different parameterizations of the same backend 3×3 mixer. Switching mode does not select a different processing algorithm. It only changes how the matrix is edited.

#### Complete mode

_Complete_ is the unconstrained 3×3 mixer representation.

For each output row:

- _output red_,
- _output green_,
- _output blue_,

you can set the contribution of:

- _input R_,
- _input G_,
- _input B_.

Each row also provides a _normalize_ checkbox. When enabled, that row is normalized by the sum of its coefficients.

This is the most general representation and is always available.

#### Simple mode

_Simple_ is an exact geometric representation of a restricted subset of normalized mixer matrices. It exposes 6 parameters:

global hue rotation
: A rigid rotation of the normalized chroma plane.

chroma (u,v) axes orientation
: The orientation of the principal axes used to express the chroma transform.

u stretch and v stretch
: Signed gains along the two chroma axes. A value of `1` is neutral. `0` collapses the selected axis to achromatic.

achromatic coupling amount and achromatic coupling hue
: Couple a chroma direction into the achromatic axis while preserving the black-white axis.

This mode is exact only when all three output rows are normalized and have non-zero sums. If the current matrix does not satisfy those constraints, the GUI falls back to _Complete_.

#### Primaries mode

_Primaries_ is another exact GUI representation, this time expressed as a generalized primaries basis in working RGB. It exposes 9 parameters:

- _white hue_,
- _white purity_,
- _red hue_,
- _red purity_,
- _green hue_,
- _green purity_,
- _blue hue_,
- _blue purity_,
- _gain_.

This mode is exact only when the current 3×3 mixer can be interpreted as a non-degenerate affine basis change with non-zero affine sums. If not, the GUI falls back to _Complete_.

## Practical guidance

This module works best when the two keyframes reflect real exposure zones in the scene.

A practical workflow is often:

1. pick a representative shadow area on the _dark_ tab to set the dark brightness keyframe,
2. pick a representative highlight area on the _bright_ tab to set the bright keyframe,
3. use the temperature sliders first to correct mixed illuminants,
4. refine the look with the RGB mixer of each tab,
5. use the preview strip to check the transition region between both anchors.

For technical correction:

- use the dark keyframe to neutralize colored bounce light in the shadows,
- use the bright keyframe to compensate the direct illuminant in the highlights,
- keep the mixer close to identity and rely mostly on CAT16 temperature when you want the most physically grounded result.

For creative grading:

- place the two keyframes closer together to concentrate the transition in the midtones,
- push them farther apart to make the blend more gradual,
- use different mixer modes for exploration, but remember they all write the same backend matrix.

If the image starts to feel synthetic, simplify the transform first:

- reduce the mixer excursions,
- rely more on temperature,
- widen the distance between the two brightness anchors.

This module is strongest when used as a broad tonal and illuminant shaper, not as a local hue-isolation tool.
---

# Color primaries : https://ansel.photos/en/doc/views/darkroom/modules/color-primaries/

Edit RGB/CYM primary control nodes in dt UCS and remap colors through a procedural 3D LUT.

The _color primaries_ module is a secondary color-grading tool built around the six additive and subtractive primaries:

- _red_,
- _yellow_,
- _green_,
- _cyan_,
- _blue_,
- _magenta_.

Each of these control nodes is sampled inside the internal RGB gamut and converted to _dt UCS HSB_. The module lets you change the hue, saturation and brightness of those six reference colors, then interpolates the resulting RGB shifts through a cylindrical local field to build a dense 3D LUT.

This gives a tool that sits between a classic primary/secondary corrector and a palette remapper:

- The controls are sparse and easy to understand,
- The final effect is continuous over the whole RGB volume,
- Achromatic colors remain fixed on the black-white axis.

## Scene-referred placement

The module is designed to run before [_color balance_](./color-balance.md) in the pixelpipe.

Its internal LUT is built and applied in Rec.2020 HLG code values, while the module input and output stay in the pipeline working RGB space. Scene-referred RGB is normalized by the module _white level_ before entering the LUT domain and denormalized afterwards.

## General principles

The six user controls do not sit directly on the RGB cube corners. Instead, the module starts from the RGB and CYM corners and recesses them toward the achromatic axis. The amount of recession is controlled by _gamut coverage_.

At `100 %`, the control nodes reach the RGB cube shell. At lower values, the control hull shrinks inward. This setting is critical because it changes what the node edits mean geometrically:

- At `100 %`, negative saturation changes behave like a gamut compression, because the control hull touches the full internal gamut shell and desaturating the nodes pulls extreme colors back inward.
- Well below `100 %`, the control hull sits inside the gamut. Increasing saturation from there can increase colorfulness by stretching a smaller inner color volume toward the outer gamut.
- At `100 %`, positive saturation changes are dangerous. Since the control nodes already lie on the gamut shell, adding saturation there can push the interpolation toward very large RGB excursions and quickly explode the gamut.

In practice:

- use `100 %` coverage together with negative saturation shifts when your goal is gamut compression,
- lower _gamut coverage_ significantly below `100 %` when your goal is to make colors more vivid,
- avoid using `100 %` coverage together with positive saturation unless you need a very small and carefully controlled effect.

Use the LUT viewer, in the _options_ tab to preview the effect of your changes and validate the gamut is not all squeezed against the cube shell.

## Module controls

The user interface is split into two tabs:

- _colors_,
- _options_.

### Colors tab

The _colors_ tab contains one section for each primary or secondary color, ordered along the light spectrum:

- _red_,
- _yellow_,
- _green_,
- _cyan_,
- _blue_,
- _magenta_.

Each section contains the same three sliders.

hue
: Rotate the selected control node around the dt UCS hue ring.

: This changes the hue of the selected primary or secondary and lets neighboring colors follow through interpolation.

saturation
: Increase or decrease the chroma of the selected control node.

: Negative values pull the node toward the achromatic axis. Positive values push it outward.

: The practical meaning depends strongly on _gamut coverage_:

- with `100 %` coverage, negative saturation is the safe way to compress gamut,
- with lower coverage, positive saturation can be used to increase colorfulness,
- with `100 %` coverage, positive saturation can produce excessive out-of-gamut excursions and should be avoided.

brightness
: Change the dt UCS brightness of the selected control node.

: This is useful to make a primary or secondary color look lighter or darker without changing all other hues equally.

The slider backgrounds are color-coded from the actual node colors and converted to the current display color profile for preview.

{{< note >}}
While we take the chromaticity coordinates of the Rec.2020 RGB primaries, we define their colorshift perceptually in dt UCS HSB space, project the shift back in RGB, and interpolate it in 3D as RGB gradients where the smoothing also happens. This allows a perceptually-grounded color change definition while limiting chroma noise, fringes, and other color artifacts typically associated with manipulating color in perceptual spaces because the 3D interpolation in RGB better preserves image gradients.
{{< /note >}}

### Options tab

white level
: Define the scene-referred white normalization used before the RGB signal enters the LUT domain.

: This is useful when the image contains values above diffuse white and you want the internal LUT to work in a bounded RGB code-value range.

: The slider includes a color picker. Sample a bright reference in the image to set the normalization directly from the scene.

gamut coverage
: Control how far the six control nodes reach toward the RGB cube shell.

: This parameter defines the effective color volume controlled by the module.

: At `100 %`, the control nodes lie on the gamut boundary and desaturation acts like gamut compression.

: Reducing the value recesses the control hull toward the achromatic axis. This is often the better starting point when your goal is to increase colorfulness.

brightness smoothing
: Control how far brightness edits spread along the black-white axis when the deformation field is built.

saturation smoothing
: Control how far saturation edits spread away from the achromatic axis. Increase if you start seing chroma noise.

hue smoothing
: Control how broadly neighboring hue regions contribute to each local deformation. Increase if you start seeing chroma noise or uneven transitions between hues.

neutral protection
: Reduce the strength of color shifts close to the achromatic axis.

: Higher values protect near-neutral colors more strongly from hue and saturation remapping.

interpolation
: Choose the 3D LUT interpolation method used at application time.

: The same methods are available as in [_lut 3D_](./lut-3D.md): _tetrahedral_, _trilinear_ and _pyramid_. With a dense LUT the visual differences are usually subtle, but tetrahedral is the default and safest option.

## LUT viewer

The bottom part of the _options_ tab contains a 3D LUT viewer. It displays the LUT geometry as a projected RGB cube:

- One colored node for the input RGB sample,
- One colored node for the output RGB sample,
- An arrow from input to output.

The viewer can display either:

- the interpolated LUT,
- the sparse control nodes used to build it.

These two modes are mutually exclusive and help answer different questions:

- use the interpolated LUT view to inspect the final color field,
- use the control-node view to understand how the sparse primaries scaffold is arranged in RGB.

The viewer uses the LUT internal RGB coordinates for geometry, but converts node colors to the display color profile for drawing.

### Viewer controls

azimuth
: Rotate the cube around the achromatic axis.

axis tilt
: Tilt the achromatic axis relative to the viewing plane. At `90°`, it shows a chromaticity diagram with the achromatic axis orthogonal to the graph plane. At `0°`, it shows the achromatic axis parallel to the graph plane.

slice depth
: Move the slicing plane through the cube. On dense LUTs, slicing allows an internal view of the cube at a given depth without displaying all nodes in front of and behind the plane.

slice thickness
: Show only node pairs lying within a band around the slicing plane. At `100 %`, slicing is effectively disabled and the graph may become too crowded to read.

color shift threshold
: Hide `input -> output` mappings whose input/output distance is below the selected percentage of RGB code values.

target gamut
: Restrict the displayed samples to those inside the selected RGB gamut.

show control nodes
: Switch the viewer from the interpolated LUT cloud to the sparse control-node cloud.

save to cLUT
: Export the currently displayed LUT to a reusable `.cube` file with metadata describing the LUT color space. This file needs to be applied in HLG Rec.2020 color space.

### Viewer mouse interaction

mouse wheel
: Zoom in and out, centered on the mouse cursor position.

left-button drag
: Pan the view.

middle-button drag horizontally
: Change the azimuth angle.

## Practical guidance

The strongest creative and technical lever in this module is _gamut coverage_. It is not just a reach parameter. It changes whether the module behaves more like a gamut compressor or more like a colorfulness enhancer.

A safe workflow is often:

1. set _white level_ correctly,
2. choose _gamut coverage_ based on the intent,
3. edit the six node saturations first,
4. refine hue and brightness afterwards,
5. inspect the result in the LUT viewer.

As a rule of thumb:

- for gamut compression, start from `100 %` coverage and pull the relevant nodes inward with negative saturation values (desaturation),
- for colorfulness enhancement, reduce _gamut coverage_ first and then add saturation cautiously,
- if colors start to break apart or look synthetic, reduce either the saturation shifts or the coverage before increasing smoothing.

The effect will be stronger in the immediate vicinity of the color control nodes, so defining the _gamut coverage_ also defines where your settings have the most weight. As we move away from the control nodes, the effect becomes weaker, which gracefully blends the colorshifts but can also mute the initial intent.---

# Color equalizer : https://ansel.photos/en/doc/views/darkroom/modules/color-equalizer/

Remap colors through a procedural 3D LUT driven by perceptual hue-ring controls.

The _color equalizer_ module is a secondary color-grading tool. It lets you define color shifts on three hue rings expressed in _dt UCS HSB_ space, then converts those shifts into a dense RGB 3D LUT that is applied to the image.

The user interface is meant to stay perceptually even:

- The editable graphs work in _hue_, _saturation_ and _brightness_ dimensions of _dt UCS HSB_,
- The underlying LUT is built in RGB code values and applied as a 3D cube transform,
- The interpolation between user nodes is smooth and periodic over hue.

Because the final effect is applied through a 3D LUT, hue, saturation and brightness edits can interact in a more global way than in curve-only modules. This makes the module well suited to creative color remapping and palette design, but more robust across dramatic changes.

## Scene-referred placement

The module is designed to run after [_color balance_](./color-balance.md) in the pixelpipe.

Its internal LUT is built and applied in Rec.2020 HLG code values, while the input and output of the module stay in the pipeline working RGB space. Scene-referred RGB is normalized by the module _white level_ before entering the LUT domain and denormalized afterwards.

## General principles

The _color equalizer_ is built around three brightness ranges:

- _shadows_,
- _midtones_,
- _highlights_.

For each range, three inner tabs define how colors should be remapped around the hue ring:

- _saturation_,
- _brightness_,
- _hue_.

Each graph is defined by editable nodes connected with a periodic monotone Hermite interpolation. The horizontal axis always represents hue. The vertical axis represents the selected correction dimension:

- _saturation_ scales chroma,
- _brightness_ scales the dt UCS brightness,
- _hue_ rotates hue around the ring.

From these user-defined ring deformations, the module builds a 64×64×64 RGB 3D LUT. The LUT is constrained to remain inside the RGB gamut cube.

## Module controls

### Primary tabs

shadows, midtones, highlights
: These tabs choose which brightness ring you are editing.

: Each ring corresponds to a fixed brightness anchor in dt UCS HSB space. In practice, the three rings let you define different color remappings for dark, medium and bright colors.

options
: This tab contains the global parameters controlling LUT generation and application.

### Graph tabs

For each primary brightness tab, the following three inner tabs are available:

saturation
: Increase or decrease chroma around the hue ring.

brightness
: Increase or decrease the dt UCS brightness around the hue ring.

hue
: Rotate hues around the ring.

### Graph interaction

left-click + drag
: Move an existing node.

Ctrl+left-click
: Add a new node on the curve.

right-click on a node
: Delete the selected node.

double-click
: Reset the current graph to its default flat state.

The graphs also display a vertical marker when the module color picker is active, showing the hue of the sampled color.

### Live preview interaction

When the module has focus in darkroom, the mouse cursor over the image preview becomes a direct editing control for the currently visible graph:

move the mouse over the image
: Sample the module input through the preview pixelpipe cache at the cursor position.

: The sampled color is projected back to the bounded dt UCS hue-ring control space, then the currently visible graph shows:

- A vertical hue marker at the sampled hue,
- A circular swatch on the curve,
- The original sampled display color in the inner disk,
- The corrected display color in the outer ring,
- An outer intensity arc showing the interpolated correction amount for the active setting.

mouse wheel over the image
: Adjust the currently visible graph around the sampled hue.

: The change is distributed to neighboring nodes with a gaussian window, so edits stay smooth and local on the hue ring.

: On the _saturation_ and _brightness_ graphs, the wheel changes the gain of nearby nodes.

: On the _hue_ graph, the wheel rotates nearby nodes around the hue ring.

Shift+mouse wheel
: Use a larger step.

Ctrl+mouse wheel
: Use a finer step.

right-click over the image
: Add a new node on the currently visible graph at the hue sampled under the cursor.

This preview interaction always targets the active combination of:

- brightness tab: _shadows_, _midtones_ or _highlights_,
- graph tab: _saturation_, _brightness_ or _hue_.

### Options tab

The smoothing parameters are important when doing dramatic local changes : they will help blending color shifts to avoid hash transitions and halos between objects, as well as chroma noise. Increasing all smoothing parameters will make the actual color results drift away from user-set parameters at the benefit of better preserving gradients into the image and limiting edge artifacts.

white level
: Define the scene-referred white normalization used before the RGB signal enters the LUT domain.

: This is useful when the image uses values above diffuse white and you want the LUT to work inside a bounded `[0,1]` RGB code-value range.

brightness smoothing
: Control how far brightness edits spread along the achromatic axis when the local deformation field is built.

saturation smoothing
: Control how far saturation edits spread away from the neutral axis.

hue smoothing
: Control how many neighboring hue anchors contribute to the local deformation.

neutral protection
: Reduce the strength of color shifts close to the achromatic axis.

: Higher values protect near-neutral colors more strongly from hue and saturation remapping.

interpolation
: Choose the 3D LUT interpolation method used at application time.

: The same methods are available as in [_lut 3D_](./lut-3D.md): _tetrahedral_, _trilinear_ and _pyramid_. The difference is usually subtle with a dense LUT, but tetrahedral is the default and safest option.

### Module color picker

A module-wide color picker is available below the graph notebooks.

It samples the module input in RGB, averages the selected area, normalizes the result by the module _white level_, and converts it to dt UCS HSB for feedback.

The picker is used to:

- Draw a vertical hue marker on all graphs,
- Display whether the sampled color lies mostly in _shadows_, _midtones_ or _highlights_.

When the sampled brightness lies between two rings, the label displays the relative distance between them, for example `35% shadows, 65% midtones`.

The module color picker is separate from the live preview cursor editing described above:

- the picker is used to inspect one area and report its hue/brightness position in the controls,
- the live preview cursor is used to edit the currently visible graph directly from the image.

## LUT viewer

The bottom part of the module contains a 3D LUT viewer. It displays the LUT geometry as a projected RGB cube:

- One colored node for the input RGB sample,
- One colored node for the output RGB sample,
- An arrow from input to output.

The viewer uses the LUT internal RGB coordinates for geometry, but converts node colors to the display color profile for drawing.

### Viewer controls

azimuth
: Rotate the cube around the achromatic axis.

axis tilt
: Tilt the achromatic axis relative to the viewing plane. At 90°, it shows a chromaticity diagram with the achromatic axis orthogonal to the graph plane. At 0°, it shows the achromatic axis parallel to the graph plane.

slice depth
: Move the slicing plane through the cube. On dense LUTs, slicing allows to get an "internal" view of the cube at a certain depth, for legibility purposes, without displaying color nodes behind and in front of the slicing plane ± slice thickness. 

slice thickness
: Show only node pairs lying within a band around the slicing plane. At 100 %, it effectively disables the effect of slicing and the graph may be too crowded to be legible.

color shift threshold
: Hide `input -> output` mappings whose input/output distance is below the selected percentage of RGB code values. At 0 %, it shows all mappings, which may crowd the graph beyond legibility.

target gamut
: Restrict the displayed samples to those inside the selected RGB gamut.

save to cLUT
: Export the currently displayed LUT to a reusable `.cube` file with metadata describing the LUT color space. This file will need to be applied in HLG Rec2020 colorspace.

### Viewer mouse interaction

mouse wheel
: Zoom in and out, centered on the mouse cursor position.

left-button drag
: Pan the view.

middle-button drag horizontally
: Change the azimuth angle.

middle-button drag vertically
: Change the axis tilt angle.

double-click
: Reset zoom, pan and view orientation.

These interactions affect only the visualization. They do not modify the LUT itself.

## Typical use

A common workflow is:

1. Use _color balance_ first to establish the global color mood.
2. In _color equalizer_, choose one of the three brightness tabs.
3. Use the module color picker to locate the hue and brightness range you want to target.
4. Use the live preview cursor over the image to inspect how the active graph affects a real sampled color.
5. Adjust _hue_, _saturation_ and _brightness_ graphs for that range, either by dragging nodes in the graph or by scrolling over the image preview.
6. Add nodes from the image preview with right-click when you need a control point exactly at the sampled hue.
7. Inspect the resulting RGB deformation in the LUT viewer.
8. Fine-tune the smoothing parameters in the _options_ tab if the remapping spreads too much or not enough.

The module is most effective for creative palette remapping, hue replacement, and selective saturation/brightness shaping of already-balanced images.

## Limitations

### Changing greys

The module is not allowing to modify the achromatic axis, as to tint greys. This is on purpose because the LUT building steps uses a cylindrical 3D interpolation that guarantees its numerical stability. The grey (achromatic) axis is used as the spine on which we base the whole interpolation process. 

Tinting greys is otherwise known as _white balancing_. It can also be achieved in [_color balance_](./color-balance.md) with regard to luminance. That should be achieved prior to using the _color equalizer_

### Compatibility with Darktable color equalizer

The Darktable "team" chose to steal from Ansel an [earlier prototype](https://github.com/aurelienpierreeng/ansel/pull/283) of _color equalizer_ that was brittle and judged unworthy of going into production by its author, Aurélien PIERRE. This prototype applies color shifts in dt UCS HSB space directly, and uses several guided filter instances to deal with edge artifacts and chroma noise. This is a broken attempt at fixing a broken design, because the issue is trying to do color shifts in another color space than RGB. Because of that, preserving image gradients is not possible and edge artifacts can't be avoided, and no amount of guided filters will make it ok.

Ansel _color equalizer_ is not compatible at all with the Darktable one, which should never have been pushed to production. But again, different projects, different quality standards.
---

# Complete pipeline overhaul : https://ansel.photos/en/news/complete-pipeline-overhaul/

Since I started using Darktable, circa 2012, I have always been surprised by how little RAM it used. People think it is a good thing that an application uses the memory sparingly, and that is surely true if we talk about your desktop environment. But talking of a production software that does heavy pixel rendering on images from 12 to 54 Mpixels, this means the same heavy computations are done again and again instead of being saved to be reused later. That's what a cache is for : avoiding expensive computations. And it should use all the available RAM for that, because computing is wasting power, and that has a concrete impact if you are working on battery. Also, you paid for that RAM and using it doesn't empty your battery. The CPU/GPU on the other end…

Yes, photo-reporters and many digital nomads retouch on battery-running laptops, on location. It's time the open-source world realizes that not all photographers are sitting at their desk on evenings and weekends, after their "real" daytime job. So, computing and recomputing the same things over and over again is simply wasting energy, whether you are plugged into the wall or running on battery.

In the meantime, Darktable has always had that weird little RAM setting that allowed you to define how much you wanted it to use. Except it had zero way of actually tracking how much RAM it used, so it was a guide more than a rule. That was later replaced by a completely idiotic qualitative "resources" preference that both means nothing and actually doesn't tell you how much RAM you allocate to the software.

So, the Darktable RAM cache has never really worked, was underused and there was a need to fix it. Because, as a result, everything was recomputed all the time for no reason.

But… There was a reason why the cache was underused and not really working : that software had zero visibility over the data lifecycle, which is kind of the prerequisite if you want to store data to be reused. That came from a pixel pipeline that was really convoluted (I truly wonder if anybody is able to draw the flowchart of what we do to pixels, start to end, among Darktable "developers"), but also from image metadata that were computed in several places, copied and stored (and then partially updated) in several places…

The pipeline cache had one purpose : allocating modules input and output buffers, and possibly reusing them if already available. But its size was defined in number of entries, which was really limited, and not in amount of memory allocated. That made it mildly useful and forced us to use it far too conservatively, because its actual memory size was fully unpredictable.

Needless to say, "fixing the cache" implied "fixing the pipeline", which in turn implied "making the data lifecycle visible, trackable and managed globally", which implied to redesign the high-level architecture of the software. As we will see, this had unexpected benefits on performance, which largely surpass the module-wise internal optimizations that Darktable did in the same time.

## What changed since 2022

This article is the direct sequel to [Fixing the pipeline cache and 10-year-old bugs](/news/fixing-pipe-cache-10-yo-bugs.md). That previous chapter was about finding the rot and stopping the most embarrassing failures. This one is about what happened next, when it became obvious that the cache bugs were not isolated bugs at all, but symptoms of a renderer whose responsibilities had been mixed together for far too long.

### Where this fits historically

The original problem was not merely "the cache does not cache enough". The actual problem was that the whole pipeline stack had grown around accidental behavior. GUI redraws could end up starting image processing, in an unregulated fashion. Mask preview leaked GUI state into the renderer. Histogram and picker logic depended on special pipeline side effects. Export, thumbnails, preview and darkroom all had their own little variations of "surely we can just copy that buffer one more time". And whenever consistency became hard to prove (during mask display or cropping changes), the fallback strategy was often to flush cache lines and recompute everything, because apparently "set it on fire and start over" counted as architecture for a while.

Some of the code comments were already saying as much. There were warnings about redraw-driven recomputation. There was even an old note saying the cache should eventually become global. So the last few years were not about inventing a new theory from scratch. They were mostly about finally doing the cleanup that the code had been begging for since the 2010's.

The key historical shift is that the pipeline stopped being treated as one recursive blob expected to discover its own needs while running. It was progressively split into clearer execution paths for interactive rendering, headless rendering, GPU processing, CPU processing and GUI-side consumers. At the same time, GUI behavior was pushed back to the GUI. Drawing became drawing again. Processing became processing again. That sounds banal, which is usually a sign that the previous design had become absurd.

### Checksums, or how validity stopped being a guessing game

Once the renderer was no longer entangled with every widget and every side channel, it became possible to define image state explicitly. Each history item now carries a module checksum built from module parameters, blend parameters and any relevant mask forms. That gives every module state a canonical fingerprint as soon as it enters history, instead of leaving validity to be inferred later from whatever happened to run.

That alone was not enough, because a pipeline is not just a pile of module settings. It also depends on order, region of interest, buffer contracts and display-related state. So the planning stage accumulates those module-level fingerprints through the whole chain and folds in the rest of the runtime contract before any pixels move. The result is a stable per-stage identity and a final pipeline identity.

This is where visibility finally appears. The pipeline can know whether it is still valid by comparing its history hash. The displayed backbuffer can know whether it still matches the current pipe and history state. A cache line can know whether it really belongs to the exact stage, ROI and parameters currently requested. That sounds almost offensively reasonable, but it required a lot of cleaning first: history synchronization with pipeline had to be made explicit, cache bypass had to be declared ahead of time (for mask preview and cropping edits), mask display could no longer mutate the rules halfway through execution, and the old redraw-triggered side effects had to go. You do not get reliable hashes on top of unreliable control flow.

The practical benefit is that consistency is no longer maintained by superstition. We no longer need to ask "did the GUI probably invalidate the right thing at the right time?" The answer is in the checksums. Either the state matches, or it does not. And we can grab any module output from the GUI layer, without running a pipeline, because the module checksum is predictable and can be computed ahead of rendering, or in parallel.

### The cache-first architecture

Reliable hashes change the architecture almost by force. Once every meaningful pipeline state has a stable identity known in advance, the cache stops being a best-effort optimization and becomes the center of the dataflow.

That is why the pipeline now has a real planning phase before execution. History is synchronized first. Regions of interest are propagated through the full chain. Each stage gets an explicit input and output contract. Host-side cache policy is sealed before runtime. Global hashes are computed before execution. In other words, the engine knows what it is about to do before it starts doing it, which is a refreshing break from the older method of finding out mid-flight and compensating with hacks.

From there, cache lookups become deterministic. A stage can reopen exactly the buffer that matches its state, so a module can bypass computations entirely by merely looking on the cache if a cacheline already matches its internal state as a checksum lookup. Downstream consumers can reopen published results by hash instead of asking the renderer to perform one more special dance just for them. The cache ceases to be a pile of vaguely reusable memory and becomes the authoritative registry of valid intermediate and final results.

This also explains why so much code could be removed or simplified. Once validity is centralized, you need fewer heuristics, fewer special branches, fewer "except when the GUI is in this mode" clauses, and fewer emergency flushes. The code gets shorter not because the problem got smaller, but because it stopped being solved in six contradictory places at once.

Cachelines also bind GPU and CPU memory, which allows us to use OpenCL pinned memory. There has been an OpenCL user preference that allowed pinned memory to be enabled or not since forever, but I discovered that option was used only for tiled processing, when modules couldn't fit their whole memory needs on the GPU. Pinned memory has now been extended to normal operations too, as it allows RAM and VRAM to be mapped without copying. Using pinned memory on a fully cached GPU pipeline reduces the time spent moving memory buffers by up to a factor of 7.

But force-caching OpenCL module outputs, even with zero-copy pinned buffers, still has some cost, and is more costly than just skipping cache entirely. For some heavy modules (_denoise profiled_, _diffuse or sharpen_, _contrast equalizer_) the caching overhead is definitely worth it, but for fast ones (_white balance_, _exposure_, _input color profile_) the cost of caching OpenCL buffers made the whole modules run actually slower on GPU than on CPU.

So we have now per-module heuristics that decide whether an OpenCL module will publish its output buffer on the RAM cache. Heavy modules and all modules capturing color-picker or histograms are published by default. For the others, users can decide for themselves :

{{< figure src="cache-opencl.jpg" />}}

This will allow users to also disable OpenCL selectively on some modules when they have recurring problems with them. For example, _denoise profiled_ and _dehaze_ consume a lot of GPU memory and may fail during runtime for lack of available memory, even though the initial memory planning found it should be enough. When that happens, the module had already started computing for some time and will fallback to CPU. If that happens frequently, rather bypass GPU on that module all the time then take twice the penalty.

But it doesn't stop there. GPU buffers are kept alive on the GPU too. It means that restarting a partial pipeline from an OpenCL module gets the input from GPU directly when possible, without any copy from RAM. It also means that the lightweight modules are able to reuse their previous GPU buffer if it fits, instead of allocating a new one. This alone opens the possibility to have a realtime pipeline, needed for the new module [_Drawing_](../../doc/views/darkroom/modules/drawing/), with around 60-80 ms of latency between brush strokes and image updates. I would never have thought this would be possible at all.

The same memory reallocation is done in RAM since Ansel now hosts the whole cache in its own [memory arena](https://en.wikipedia.org/wiki/Region-based_memory_management). This scheme not only allows us to pre-reserve a contiguous memory block from the OS when starting the application, leading to faster allocations, it also ensures that we don't overflow the user-defined memory volume. So, when you decide to allocate 8 GB of RAM to Ansel, we are now able to ensure that this actually happens : we will fill all that volume until it's full, after which we start recycling the oldest buffers. All pixel buffers, module input, output, but also temporary internal scratchpads, are allocated in our memory arena.

And, last but not least, there is only one pipeline cache for the whole application, meaning all pipeline types (darkroom preview, darkroom main image, thumbnail export and file export) share the same cache. While this needed to be managed for multithreaded concurrent access, it means that if a pipeline needs a module output that was already computed at the same size by another pipeline, it can directly grab the cacheline and bypass its own recomputation.

To take advantage of that, the previously fixed-size darkroom "preview" (aka the downscaled version of the full image, used in the navigation widget, to sample histograms and color-pickers, and as a transient "blurry" fill-in when the main image is being recomputed) has been changed to be dynamically sized to the "fit" zoom level of the main image (the one displayed in the center of the darkroom). The result is only one image needs to be computed when you enter the darkroom, and zooming back to "fit" zoom is instant.

But, since that fixed-sized preview had been lazily used in many places in the GUI to remap cursor coordinates in widget space to full-resolution RAW image coordinates, by means of copy-pasting the same ugly block of code everywhere (copying even the comment saying "I don't understand what this does"), the whole coordinate system had to be redone to accommodate this change. And, again, those coordinates were computed only inside a pipeline, so the GUI had to wait for the pipeline to recompute the new sizes to know where to position objects, which triggered GUI glitches especially when changing the cropping. But now, all that is computed ahead of time from a unified API, so no more silly copy-pasting multiplied by the number of modules.

### What that changed outside the core

The first external consequence is that copies stopped masquerading as features. Darkroom no longer needs a dedicated GUI backbuffer to display what the renderer already produced. It can grab the published image directly from the cache, bind a Cairo surface on it, and display it in a few milliseconds. Previously, the path was slower and copy-busy for no good reason beyond inherited design confusion. The Darkroom can also directly check if a given pipeline backbuffer is up-to-date with current development history, through checksums. Previously, that relied on a "dirty" flag that could be overwritten by several threads and prevented any "best effort" darkroom redrawing (as in: slap the last-known valid backbuffer pending the recomputation of the up-to-date one). This was important when user changed module parameters faster than the pipeline could recompute : we still need something to fill the view to avoid flickering.

The same logic now extends to export and thumbnails, which can reopen the final published result instead of rebuilding their own private reality. Histogram and picker code also moved away from piggybacking on pipeline execution. Scopes follow explicit completion signals from the preview path, cache their own Cairo surface against source state, and sample from cache-backed backbuffers directly. Raw histogram (sampled after _demosaic_ module) has been added to the old pipeline output on : switching between both doesn't require a pipeline recompute, neither does color-picking in either. That is a much more boring design, which is exactly why it is better.

Another major consequence is that pipeline flavors no longer live in isolated cache silos. Preview, darkroom, export, thumbnails and histogram consumers can now share cache state. That was the intent already hinted at long ago in the cache code; it just took a full cleanup of validity tracking to make it real. Once all those consumers agree on the same hashes, they can finally agree on the same buffers too. The benefit is one pipeline can reuse the already-computed cacheline from another pipe if it fits (same parameter, same input and output size). So the start of the darkroom pipelines have been switched to full-resolution : demosaicing and denoising will be slower when entering darkroom, but then zooming and panning become instant. And exporting an image will grab these early stages straight from cache too. By the way, exporting the same image twice at two different resolutions only recomputes from the scaling module, at the end of the pipeline.

Memory handling followed the same logic. Cache lines became refcounted, lockable, reusable objects instead of disposable blobs passed around through ad-hoc side storage. CPU and GPU paths now share far more of the same ownership model. On the OpenCL side, host and device buffers are managed with explicit lifetime rules, pinned memory is reused instead of churned, and RAM and VRAM stay synchronized through pinned and zero-copy transfers where possible instead of being mirrored through pointless intermediate copies. This does not make the GPU magically faster at math. It mostly stops the surrounding plumbing from wasting the gains.

The benefit is not just benchmark speed. It is also latency, robustness and energy use. Darkroom startup gets shorter because there are fewer copies and fewer duplicated passes. Zoom, pan, mask preview and history navigation stop invalidating half the universe just because proving correctness used to be too hard. OpenCL becomes less random because fallback, tiling and memory ownership are stated more explicitly. The software spends less time moving pixels around just to reassure itself that they still exist somewhere.

As a side effect, error handling has been improved in the pipeline. Modules that failed to allocate memory now immediately throw an error and abort the pipeline. Previously, allocation failures were not checked everywhere, and a whole pipeline could carry `NULL` buffer up to the end… or crash fair and square on `segmentation fault` errors. This is now prevented upstream in an uniform way across the whole software.

Color-picker sampling and histograms can sample data without triggering a pipeline recompute by directly fetching the cache, if the requested cacheline exists. The global _scopes_ toolbox allows you to choose from RAW RGB (sampled after _demosaicing_) or final pipeline RGB (either sampled after _output color profile_ in 32 bits float or in the backbuffer in 8-bit integers) : changing from one to another doesn't restart a pipeline but simply loads a different cacheline, and color-picking happens on that cacheline transparently.

### What was actually fixed by doing it properly

A lot of visible annoyances trace back to this redesign more than to any one micro-optimization. High-DPI ROI handling improved because geometry ownership became clearer and coordinates mapping between GUI and RAW image are now handled in a central API. History-to-pipeline resynchronization became less fragile because history stopped being an incidental side effect of GUI flow and became a first-class input to planning. Real-time mask preview no longer has to fight a cache model designed for another era. Histogram refresh became predictable because it now consumes published data instead of hoping the right intermediate buffer happens to be alive somewhere. Infinite loops, stale cache lines, invalid backbuffers and the classic "preview recomputed the full pipe from module 0 for no obvious reason" all become easier to diagnose once the lifecycle is explicit.

So the interesting part here is not that one big refactor happened in isolation. It is that a long series of cleanups finally aligned around one idea: validity should be computed, named and shared ahead of time. Once that happened, the rest followed almost mechanically. The cache became central because hashes were trustworthy. External consumers could stop copying because cache-published buffers became authoritative. CPU and GPU paths could share more state because ownership became explicit. And the renderer stopped behaving like a haunted house where changing one slider could wake up three unrelated subsystems and a redraw callback from 2013.

## Benchmarks

Benchmarks are done on both applications compiled locally with _Release_ build and GCC14 using their respective `build.sh --build-type Release` script. 

Config 1
: The hardware is a Thinkpad P51, with 8 × Intel Xeon CPU E3-1505M v6 @ 3.00GHz and GPU Nvidia Quadro M2200, running on Fedora 41 Plasma with Wayland. Nvidia driver version is 580.105.08. The Intel Xeon is an high-end CPU released in 2015. The Quadro M2200 is a mid-range professionnal CPU released in 2017.

Config 2
: The hardware is a desktop PC with 4 × Intel Core i3-4130 and GPU AMD Radeon RX 580 Polaris 10, running on Ubuntu 24.04 LTS with Wayland. AMD driver version is RustiCL 25.2.8. The Core i3 is a mid-range CPU released in 2013. The AMD Polaris 10 is a mid-high-range GPU released in 2017.

We show here the output of `ansel -d perf` and `darktable -d perf`. Both Ansel and Darktable config files are set with [this script](https://github.com/aurelienpierreeng/ansel/blob/master/tools/benchmark_darkroom_rc.sh) to ensure the same config baseline. The 45 Mpx test picture is taken from [here](https://discuss.pixls.us/t/backlit-hardcore-flaring-vintage-lens-fun/31270) and the editing XMP can be downloaded [here](2022-06-17__DSC00078.arw.xmp).

### Initial loading in darkroom

Warning: in Ansel, it is important to run this test with no module focused in the GUI (that is, uncollapsed). In this situation, Ansel always caches the output of the previous module so we ensure that, when modifications are made in that focus module, we always restart computations from the immediate input. But forcing the cache like this triggers memory overhead that can be visible for lightweight modules.

#### Config 1

{{< tabs >}}

{{< tab "Ansel Master GPU" >}}
```
3.690316 [dev_pixelpipe] took 0.000 secs (0.000 CPU) to load the image.
4.201079 [dev_pixelpipe] pipeline resync with history took 0.029 secs (0.026 CPU) for pipe virtual-preview
4.260916 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
4.260963 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
4.327523 [dev_pixelpipe] pipeline resync with history took 0.019 secs (0.025 CPU) for pipe preview
4.384013 [dev_pixelpipe] took 0.056 secs (0.072 CPU) initing base buffer [preview]
4.402520 [dev_pixelpipe] took 0.018 secs (0.031 CPU) processed `Raw settings' on GPU, blended on  [preview]
4.421489 [dev_pixelpipe] took 0.016 secs (0.012 CPU) processed `white balance' on GPU, blended on  [preview]
6.912680 [dev_pixelpipe] took 2.491 secs (2.470 CPU) processed `highlight reconstruction' on GPU, blended on GPU [preview]
7.167233 [dev_pixelpipe] took 0.254 secs (0.144 CPU) processed `demosaic' on GPU, blended on  [preview]
7.470521 [dev_pixelpipe] took 0.303 secs (0.173 CPU) processed `lens correction' on GPU, blended on  [preview]
7.629764 [dev_pixelpipe] took 0.159 secs (0.172 CPU) processed `Initial resampling' on GPU, blended on  [preview]
7.635593 [dev_pixelpipe] took 0.006 secs (0.006 CPU) processed `exposure' on GPU, blended on GPU [preview]
7.638110 [dev_pixelpipe] took 0.002 secs (0.001 CPU) processed `input color profile' on GPU, blended on  [preview]
7.639667 [dev_pixelpipe] took 0.002 secs (0.002 CPU) processed `color calibration' on GPU, blended on GPU [preview]
8.726634 [dev_pixelpipe] took 1.087 secs (1.090 CPU) processed `diffuse or sharpen' on GPU, blended on GPU [preview]
8.735900 [dev_pixelpipe] took 0.009 secs (0.009 CPU) processed `color balance rgb' on GPU, blended on GPU [preview]
8.742309 [dev_pixelpipe] took 0.006 secs (0.007 CPU) processed `filmic rgb' on GPU, blended on GPU [preview]
8.765175 [dev_pixelpipe] took 0.023 secs (0.029 CPU) processed `output color profile' on GPU, blended on  [preview]
8.808546 [dev_pixelpipe] took 0.043 secs (0.049 CPU) processed `dithering' on CPU, blended on  [preview]
8.820376 [dev_pixelpipe] took 0.012 secs (0.061 CPU) processed `display encoding' on CPU, blended on  [preview]
8.820439 [pixelpipe] preview internal pixel pipeline processing took 4.493 secs (4.332 CPU)
8.820483 [dev_process_preview] pipeline processing thread took 4.493 secs (4.333 CPU)
8.858716 [dev_pixelpipe] pipeline resync with history took 0.026 secs (0.032 CPU) for pipe full
8.858805 [dev_process_full] pipeline processing thread took 0.000 secs (0.000 CPU)
```
{{< /tab >}}

{{< tab "Darktable 5.4.1 GPU" >}}
```
10,5815 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
10,6799 [dev_pixelpipe] took 0,018 secs (0,029 CPU) initing base buffer [full]
10,6968 [dev_pixelpipe] took 0,017 secs (0,020 CPU) [full] processed `rawprepare' on GPU, blended on GPU
10,7094 [dev_pixelpipe] took 0,013 secs (0,011 CPU) [full] processed `temperature' on GPU, blended on GPU
14,4091 [dev_pixelpipe] took 3,700 secs (3,719 CPU) [full] processed `highlights' on GPU with tiling, blended on CPU
14,8078 [resample_cl] plan 0,000 secs (0,000 CPU) resample 0,010 secs (0,002 CPU)
14,8988 [dev_pixelpipe] took 0,490 secs (0,349 CPU) [full] processed `demosaic' on GPU, blended on GPU
14,9094 [dev_pixelpipe] took 0,010 secs (0,001 CPU) [full] processed `lens' on GPU, blended on GPU
14,9200 [dev_pixelpipe] took 0,010 secs (0,002 CPU) [full] processed `exposure' on GPU, blended on GPU
14,9516 [dev_pixelpipe] took 0,032 secs (0,010 CPU) [full] processed `colorin' on GPU, blended on GPU
14,9654 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0,013 secs (0,000 GPU) [channelmixerrgb]
14,9902 [dev_pixelpipe] took 0,039 secs (0,006 CPU) [full] processed `channelmixerrgb' on GPU, blended on GPU
16,6800 [dev_pixelpipe] took 1,690 secs (1,745 CPU) [full] processed `diffuse' on GPU, blended on GPU
16,6923 [dev_pixelpipe] took 0,012 secs (0,007 CPU) [full] processed `colorbalancergb' on GPU, blended on GPU
16,7037 [dev_pixelpipe] took 0,011 secs (0,005 CPU) [full] processed `filmicrgb' on GPU, blended on GPU
16,7100 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,006 secs (0,000 GPU) [colorout]
16,7555 [dev_pixelpipe] took 0,052 secs (0,020 CPU) [full] processed `colorout' on GPU, blended on GPU
16,8435 [dev_pixelpipe] took 0,088 secs (0,052 CPU) [full] processed `dither' on CPU, blended on CPU
16,8525 [dev_pixelpipe] took 0,009 secs (0,066 CPU) [full] processed `gamma' on CPU, blended on CPU
16,8594 [dev_process_image] pixel pipeline took 6,197 secs (6,048 CPU) processing `2022-06-17__DSC00078.arw'

16,8918 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
16,9470 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
16,9968 [dev_pixelpipe] took 0,000 secs (0,000 CPU) initing base buffer [preview]
16,9986 [dev_pixelpipe] took 0,002 secs (0,002 CPU) [preview] processed `rawprepare' on GPU, blended on GPU
16,9995 [dev_pixelpipe] took 0,001 secs (0,001 CPU) [preview] processed `temperature' on GPU, blended on GPU
17,0698 [dev_pixelpipe] took 0,070 secs (0,081 CPU) [preview] processed `highlights' on GPU, blended on GPU
17,0811 [dev_pixelpipe] took 0,011 secs (0,005 CPU) [preview] processed `demosaic' on GPU, blended on GPU
17,0829 [dev_pixelpipe] took 0,002 secs (0,000 CPU) [preview] processed `lens' on GPU, blended on GPU
17,0853 [dev_pixelpipe] took 0,002 secs (0,001 CPU) [preview] processed `exposure' on GPU, blended on GPU
17,0886 [dev_pixelpipe] took 0,003 secs (0,001 CPU) [preview] processed `colorin' on GPU, blended on GPU
17,0915 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0,003 secs (0,000 GPU) [channelmixerrgb]
17,0952 [dev_pixelpipe] took 0,007 secs (0,002 CPU) [preview] processed `channelmixerrgb' on GPU, blended on GPU
17,4667 [dev_pixelpipe] took 0,371 secs (0,354 CPU) [preview] processed `diffuse' on GPU, blended on GPU
17,4702 [dev_pixelpipe] took 0,003 secs (0,001 CPU) [preview] processed `colorbalancergb' on GPU, blended on GPU
17,4748 [dev_pixelpipe] took 0,004 secs (0,001 CPU) [preview] processed `filmicrgb' on GPU, blended on GPU
17,4776 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,002 secs (0,000 GPU) [colorout]
17,4939 [dev_pixelpipe] took 0,019 secs (0,009 CPU) [preview] processed `colorout' on GPU, blended on GPU
17,5141 [dev_pixelpipe] took 0,020 secs (0,007 CPU) [preview] processed `dither' on CPU, blended on CPU
17,5195 [dev_pixelpipe] took 0,005 secs (0,021 CPU) [preview] processed `gamma' on CPU, blended on CPU
17,5355 [dt_ioppr_transform_image_colorspace_rgb] `Système' -> `sRVB' took 0,016 secs (0,067 CPU) [final histogram]
17,6218 [dev_process_image] pixel pipeline took 0,625 secs (0,657 CPU) processing `2022-06-17__DSC00078.arw'
```
{{< /tab>}}

{{< tab "Ansel Master CPU" >}}
```
4.146713 [dev_pixelpipe] took 0.000 secs (0.000 CPU) to load the image.
4.642451 [dev_pixelpipe] pipeline resync with history took 0.029 secs (0.026 CPU) for pipe virtual-preview
4.703889 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
4.703915 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
4.782635 [dev_pixelpipe] pipeline resync with history took 0.033 secs (0.041 CPU) for pipe preview
4.796521 [dev_pixelpipe] took 0.014 secs (0.018 CPU) initing base buffer [preview]
4.831601 [dev_pixelpipe] took 0.035 secs (0.061 CPU) processed `Raw settings' on CPU, blended on  [preview]
4.862626 [dev_pixelpipe] took 0.031 secs (0.077 CPU) processed `white balance' on CPU, blended on  [preview]
10.978874 [dev_pixelpipe] took 6.116 secs (43.505 CPU) processed `highlight reconstruction' on CPU, blended on CPU [preview]
11.369357 [dev_pixelpipe] took 0.390 secs (2.623 CPU) processed `demosaic' on CPU, blended on  [preview]
11.449953 [dev_pixelpipe] took 0.081 secs (0.474 CPU) processed `lens correction' on CPU, blended on  [preview]
11.797645 [dev_pixelpipe] took 0.348 secs (2.088 CPU) processed `Initial resampling' on CPU, blended on  [preview]
11.802788 [dev_pixelpipe] took 0.005 secs (0.025 CPU) processed `exposure' on CPU, blended on CPU [preview]
11.807751 [dev_pixelpipe] took 0.005 secs (0.024 CPU) processed `input color profile' on CPU, blended on  [preview]
11.877682 [dev_pixelpipe] took 0.070 secs (0.361 CPU) processed `color calibration' on CPU, blended on CPU [preview]
15.025427 [dev_pixelpipe] took 3.148 secs (23.610 CPU) processed `diffuse or sharpen' on CPU, blended on CPU [preview]
15.290600 [dev_pixelpipe] took 0.262 secs (1.469 CPU) processed `color balance rgb' on CPU, blended on CPU [preview]
15.425400 [dev_pixelpipe] took 0.135 secs (0.761 CPU) processed `filmic rgb' on CPU, blended on CPU [preview]
15.438259 [dev_pixelpipe] took 0.013 secs (0.072 CPU) processed `output color profile' on CPU, blended on  [preview]
15.463031 [dev_pixelpipe] took 0.025 secs (0.039 CPU) processed `dithering' on CPU, blended on  [preview]
15.474803 [dev_pixelpipe] took 0.012 secs (0.058 CPU) processed `display encoding' on CPU, blended on  [preview]
15.474885 [pixelpipe] preview internal pixel pipeline processing took 10.692 secs (75.269 CPU)
15.474906 [dev_process_preview] pipeline processing thread took 10.692 secs (75.269 CPU)
15.522324 [dev_pixelpipe] pipeline resync with history took 0.037 secs (0.047 CPU) for pipe full
15.522431 [dev_process_full] pipeline processing thread took 0.000 secs (0.000 CPU)
```
{{< /tab >}}

{{< tab "Darktable 5.4.1 CPU" >}}
```
4,5577 [dt_dev_load_raw] loading the image. took 0,131 secs (0,040 CPU)
5,4913 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
5,6087 [dev_pixelpipe] took 0,017 secs (0,029 CPU) initing base buffer [full]
5,6491 [dev_pixelpipe] took 0,040 secs (0,120 CPU) [full] processed `rawprepare' on CPU, blended on CPU
5,6782 [dev_pixelpipe] took 0,029 secs (0,061 CPU) [full] processed `temperature' on CPU, blended on CPU
12,0060 [dev_pixelpipe] took 6,328 secs (45,005 CPU) [full] processed `highlights' on CPU, blended on CPU
12,6762 [resample_plain] plan 0,001 secs (0,001 CPU) resample 0,249 secs (1,829 CPU)
12,7230 [dev_pixelpipe] took 0,717 secs (4,499 CPU) [full] processed `demosaic' on CPU, blended on CPU
12,7370 [dev_pixelpipe] took 0,013 secs (0,029 CPU) [full] processed `lens' on CPU, blended on CPU
12,7487 [dev_pixelpipe] took 0,012 secs (0,029 CPU) [full] processed `exposure' on CPU, blended on CPU
12,7637 [dev_pixelpipe] took 0,015 secs (0,045 CPU) [full] processed `colorin' on CPU, blended on CPU
12,7693 [dt_ioppr_transform_image_colorspace] IOP_CS_LAB-->IOP_CS_RGB took 0,006 secs (0,028 CPU) [channelmixerrgb]
12,8473 [dev_pixelpipe] took 0,084 secs (0,438 CPU) [full] processed `channelmixerrgb' on CPU, blended on CPU
17,0570 [dev_pixelpipe] took 4,210 secs (32,406 CPU) [full] processed `diffuse' on CPU, blended on CPU
17,3181 [dev_pixelpipe] took 0,261 secs (1,872 CPU) [full] processed `colorbalancergb' on CPU, blended on CPU
17,4583 [dev_pixelpipe] took 0,140 secs (0,898 CPU) [full] processed `filmicrgb' on CPU, blended on CPU
17,4634 [dt_ioppr_transform_image_colorspace] IOP_CS_RGB-->IOP_CS_LAB took 0,005 secs (0,033 CPU) [colorout]
17,4792 [dev_pixelpipe] took 0,021 secs (0,116 CPU) [full] processed `colorout' on CPU, blended on CPU
17,5381 [dev_pixelpipe] took 0,059 secs (0,037 CPU) [full] processed `dither' on CPU, blended on CPU
17,5472 [dev_pixelpipe] took 0,009 secs (0,061 CPU) [full] processed `gamma' on CPU, blended on CPU
17,5543 [dev_process_image] pixel pipeline took 11,962 secs (85,649 CPU) processing `2022-06-17__DSC00078.arw'

17,5861 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
17,6428 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
17,6993 [dev_pixelpipe] took 0,000 secs (0,000 CPU) initing base buffer [preview]
17,7004 [dev_pixelpipe] took 0,001 secs (0,002 CPU) [preview] processed `rawprepare' on CPU, blended on CPU
17,7015 [dev_pixelpipe] took 0,001 secs (0,004 CPU) [preview] processed `temperature' on CPU, blended on CPU
17,8101 [dev_pixelpipe] took 0,109 secs (0,570 CPU) [preview] processed `highlights' on CPU, blended on CPU
17,8261 [dev_pixelpipe] took 0,016 secs (0,071 CPU) [preview] processed `demosaic' on CPU, blended on CPU
17,8308 [dev_pixelpipe] took 0,005 secs (0,006 CPU) [preview] processed `lens' on CPU, blended on CPU
17,8345 [dev_pixelpipe] took 0,004 secs (0,006 CPU) [preview] processed `exposure' on CPU, blended on CPU
17,8393 [dev_pixelpipe] took 0,005 secs (0,016 CPU) [preview] processed `colorin' on CPU, blended on CPU
17,8411 [dt_ioppr_transform_image_colorspace] IOP_CS_LAB-->IOP_CS_RGB took 0,002 secs (0,008 CPU) [channelmixerrgb]
17,8670 [dev_pixelpipe] took 0,028 secs (0,144 CPU) [preview] processed `channelmixerrgb' on CPU, blended on CPU
19,0763 [dev_pixelpipe] took 1,209 secs (8,091 CPU) [preview] processed `diffuse' on CPU, blended on CPU
19,1926 [dev_pixelpipe] took 0,116 secs (0,708 CPU) [preview] processed `colorbalancergb' on CPU, blended on CPU
19,2628 [dev_pixelpipe] took 0,070 secs (0,342 CPU) [preview] processed `filmicrgb' on CPU, blended on CPU
19,2667 [dt_ioppr_transform_image_colorspace] IOP_CS_RGB-->IOP_CS_LAB took 0,003 secs (0,012 CPU) [colorout]
19,2758 [dev_pixelpipe] took 0,012 secs (0,045 CPU) [preview] processed `colorout' on CPU, blended on CPU
19,2860 [dev_pixelpipe] took 0,010 secs (0,003 CPU) [preview] processed `dither' on CPU, blended on CPU
19,2907 [dev_pixelpipe] took 0,005 secs (0,020 CPU) [preview] processed `gamma' on CPU, blended on CPU
19,3052 [dt_ioppr_transform_image_colorspace_rgb] `Système' -> `sRVB' took 0,014 secs (0,071 CPU) [final histogram]
19,3928 [dev_process_image] pixel pipeline took 1,694 secs (10,211 CPU) processing `2022-06-17__DSC00078.arw'
```
{{< /tab >}}

{{< /tabs >}}

#### Config 2

{{< tabs >}}
{{< tab "Ansel Master GPU" >}}
```
36.764111 [dev_pixelpipe] took 0.313 secs (0.056 CPU) to load the image.
37.528673 [dev_pixelpipe] pipeline resync with history took 0.044 secs (0.040 CPU) for pipe virtual-preview
37.691646 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
37.691699 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
37.745145 [dev_pixelpipe] pipeline resync with history took 0.036 secs (0.043 CPU) for pipe preview
37.916228 [dev_pixelpipe] took 0.171 secs (0.115 CPU) initing base buffer [preview]
37.953157 [dev_pixelpipe] took 0.037 secs (0.024 CPU) processed `Raw settings' on GPU, blended on  [preview]
37.959823 [dev_pixelpipe] took 0.007 secs (0.002 CPU) processed `white balance' on GPU, blended on  [preview]
39.377680 [dev_pixelpipe] took 1.418 secs (0.196 CPU) processed `highlight reconstruction' on GPU, blended on GPU [preview]
39.526422 [dev_pixelpipe] took 0.149 secs (0.039 CPU) processed `demosaic' on GPU, blended on  [preview]
41.012847 [dev_pixelpipe] took 1.486 secs (0.271 CPU) processed `lens correction' on GPU, blended on  [preview]
41.266624 [dev_pixelpipe] took 0.254 secs (0.110 CPU) processed `Initial resampling' on GPU, blended on  [preview]
41.299983 [dev_pixelpipe] took 0.033 secs (0.025 CPU) processed `exposure' on GPU, blended on GPU [preview]
41.311230 [dev_pixelpipe] took 0.011 secs (0.009 CPU) processed `input color profile' on GPU, blended on  [preview]
41.313526 [dev_pixelpipe] took 0.002 secs (0.002 CPU) processed `color calibration' on GPU, blended on GPU [preview]
41.471857 [dev_pixelpipe] took 0.158 secs (0.049 CPU) processed `diffuse or sharpen' on GPU, blended on GPU [preview]
41.482377 [dev_pixelpipe] took 0.010 secs (0.008 CPU) processed `color balance rgb' on GPU, blended on GPU [preview]
41.486457 [dev_pixelpipe] took 0.004 secs (0.007 CPU) processed `filmic rgb' on GPU, blended on GPU [preview]
41.508325 [dev_pixelpipe] took 0.022 secs (0.018 CPU) processed `output color profile' on GPU, blended on  [preview]
41.521171 [dev_pixelpipe] took 0.013 secs (0.016 CPU) processed `dithering' on CPU, blended on  [preview]
41.529057 [dev_pixelpipe] took 0.008 secs (0.014 CPU) processed `display encoding' on CPU, blended on  [preview]
41.529178 [pixelpipe] preview internal pixel pipeline processing took 3.784 secs (0.906 CPU)
41.529291 [dev_process_preview] pipeline processing thread took 3.784 secs (0.906 CPU)
41.576629 [dev_pixelpipe] pipeline resync with history took 0.036 secs (0.049 CPU) for pipe full
41.579279 [dev_process_full] pipeline processing thread took 0.000 secs (0.000 CPU)
```
{{< /tab >}}

{{< tab "Darktable 5.4.1 GPU" >}}
```
23.6180 [dt_dev_load_raw] loading the image. took 0.290 secs (0.055 CPU)
24.6549 [dt_dev_process_image_job] loading image. took 0.000 secs (0.000 CPU)
24.7621 [dev_pixelpipe] took 0.023 secs (0.034 CPU) initing base buffer [full]
24.8212 [dev_pixelpipe] took 0.059 secs (0.022 CPU) [full] processed `rawprepare' on GPU, blended on GPU
24.8286 [dev_pixelpipe] took 0.007 secs (0.000 CPU) [full] processed `temperature' on GPU, blended on GPU
26.6983 [dev_pixelpipe] took 1.870 secs (0.433 CPU) [full] processed `highlights' on GPU, blended on GPU
26.7416 [resample_cl] plan 0.002 secs (0.001 CPU) resample 0.032 secs (0.000 CPU)
26.8215 [dev_pixelpipe] took 0.123 secs (0.009 CPU) [full] processed `demosaic' on GPU, blended on GPU
26.8260 [dev_pixelpipe] took 0.004 secs (0.001 CPU) [full] processed `lens' on GPU, blended on GPU
26.8282 [dev_pixelpipe] took 0.002 secs (0.001 CPU) [full] processed `ashift' on GPU, blended on GPU
26.8306 [dev_pixelpipe] took 0.002 secs (0.001 CPU) [full] processed `exposure' on GPU, blended on GPU
26.8347 [dev_pixelpipe] took 0.004 secs (0.001 CPU) [full] processed `colorin' on GPU, blended on GPU
26.8370 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0.002 secs (0.001 GPU) [channelmixerrgb]
26.8384 [dev_pixelpipe] took 0.004 secs (0.001 CPU) [full] processed `channelmixerrgb' on GPU, blended on GPU
27.3567 [dev_pixelpipe] took 0.518 secs (0.022 CPU) [full] processed `diffuse' on GPU, blended on GPU
27.3613 [dev_pixelpipe] took 0.004 secs (0.001 CPU) [full] processed `colorbalancergb' on GPU, blended on GPU
27.4019 [dev_pixelpipe] took 0.041 secs (0.011 CPU) [full] processed `filmicrgb' on GPU, blended on GPU
27.4053 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0.003 secs (0.001 GPU) [colorout]
27.4345 [dev_pixelpipe] took 0.032 secs (0.009 CPU) [full] processed `colorout' on GPU, blended on GPU
27.4814 [dev_pixelpipe] took 0.047 secs (0.025 CPU) [full] processed `dither' on CPU, blended on CPU
27.4962 [dev_pixelpipe] took 0.015 secs (0.045 CPU) [full] processed `gamma' on CPU, blended on CPU
27.4990 [dev_process_image] pixel pipeline took 2.760 secs (0.618 CPU) processing `2022-06-17__DSC00078.arw'

27.5249 [dt_dev_process_image_job] loading image. took 0.005 secs (0.013 CPU)
27.5866 [dt_dev_process_image_job] loading image. took 0.000 secs (0.000 CPU)
27.6513 [dev_pixelpipe] took 0.000 secs (0.000 CPU) initing base buffer [preview]
27.6561 [dev_pixelpipe] took 0.005 secs (0.006 CPU) [preview] processed `rawprepare' on GPU, blended on GPU
27.6580 [dev_pixelpipe] took 0.002 secs (0.001 CPU) [preview] processed `temperature' on GPU, blended on GPU
27.7797 [dev_pixelpipe] took 0.122 secs (0.062 CPU) [preview] processed `highlights' on GPU, blended on GPU
27.7845 [dev_pixelpipe] took 0.005 secs (0.002 CPU) [preview] processed `demosaic' on GPU, blended on GPU
27.7854 [dev_pixelpipe] took 0.001 secs (0.000 CPU) [preview] processed `lens' on GPU, blended on GPU
27.8023 [dev_pixelpipe] took 0.017 secs (0.002 CPU) [preview] processed `ashift' on GPU, blended on GPU
27.8034 [dev_pixelpipe] took 0.001 secs (0.000 CPU) [preview] processed `exposure' on GPU, blended on GPU
27.8058 [dev_pixelpipe] took 0.002 secs (0.000 CPU) [preview] processed `colorin' on GPU, blended on GPU
27.8077 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0.001 secs (0.000 GPU) [channelmixerrgb]
27.8089 [dev_pixelpipe] took 0.003 secs (0.000 CPU) [preview] processed `channelmixerrgb' on GPU, blended on GPU
28.5157 [dev_pixelpipe] took 0.707 secs (0.076 CPU) [preview] processed `diffuse' on GPU, blended on GPU
28.5240 [dev_pixelpipe] took 0.008 secs (0.004 CPU) [preview] processed `colorbalancergb' on GPU, blended on GPU
28.6037 [dev_pixelpipe] took 0.079 secs (0.031 CPU) [preview] processed `filmicrgb' on GPU, blended on GPU
28.6092 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0.005 secs (0.000 GPU) [colorout]
28.6323 [dev_pixelpipe] took 0.028 secs (0.007 CPU) [preview] processed `colorout' on GPU, blended on GPU
28.6675 [dev_pixelpipe] took 0.035 secs (0.010 CPU) [preview] processed `dither' on CPU, blended on CPU
28.6881 [dev_pixelpipe] took 0.021 secs (0.066 CPU) [preview] processed `gamma' on CPU, blended on CPU
28.8183 [dev_process_image] pixel pipeline took 1.167 secs (0.442 CPU) processing `2022-06-17__DSC00078.arw'
```
{{< /tab >}}

{{< tab "Ansel Master CPU" >}}
```
13.969925 [dev_pixelpipe] took 0.271 secs (0.044 CPU) to load the image.
14.519352 [dev_pixelpipe] pipeline resync with history took 0.039 secs (0.037 CPU) for pipe virtual-preview
14.609258 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
14.609291 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
14.685917 [dev_pixelpipe] pipeline resync with history took 0.036 secs (0.048 CPU) for pipe preview
14.724434 [dev_pixelpipe] took 0.038 secs (0.024 CPU) initing base buffer [preview]
14.787570 [dev_pixelpipe] took 0.063 secs (0.064 CPU) processed `Raw settings' on CPU, blended on  [preview]
14.857062 [dev_pixelpipe] took 0.069 secs (0.080 CPU) processed `white balance' on CPU, blended on  [preview]
27.961302 [dev_pixelpipe] took 13.104 secs (32.983 CPU) processed `highlight reconstruction' on CPU, blended on CPU [preview]
28.949286 [dev_pixelpipe] took 0.988 secs (3.057 CPU) processed `demosaic' on CPU, blended on  [preview]
29.075925 [dev_pixelpipe] took 0.127 secs (0.326 CPU) processed `lens correction' on CPU, blended on  [preview]
30.176998 [dev_pixelpipe] took 1.101 secs (3.061 CPU) processed `Initial resampling' on CPU, blended on  [preview]
30.190589 [dev_pixelpipe] took 0.013 secs (0.024 CPU) processed `exposure' on CPU, blended on CPU [preview]
30.192681 [dev_pixelpipe] took 0.002 secs (0.005 CPU) processed `input color profile' on CPU, blended on  [preview]
30.239877 [dev_pixelpipe] took 0.047 secs (0.103 CPU) processed `color calibration' on CPU, blended on CPU [preview]
31.944712 [dev_pixelpipe] took 1.705 secs (4.286 CPU) processed `diffuse or sharpen' on CPU, blended on CPU [preview]
32.096771 [dev_pixelpipe] took 0.152 secs (0.407 CPU) processed `color balance rgb' on CPU, blended on CPU [preview]
32.190015 [dev_pixelpipe] took 0.093 secs (0.228 CPU) processed `filmic rgb' on CPU, blended on CPU [preview]
32.205505 [dev_pixelpipe] took 0.015 secs (0.028 CPU) processed `output color profile' on CPU, blended on  [preview]
32.214424 [dev_pixelpipe] took 0.009 secs (0.011 CPU) processed `dithering' on CPU, blended on  [preview]
32.223982 [dev_pixelpipe] took 0.009 secs (0.021 CPU) processed `display encoding' on CPU, blended on  [preview]
32.224049 [pixelpipe] preview internal pixel pipeline processing took 17.538 secs (44.709 CPU)
32.224090 [dev_process_preview] pipeline processing thread took 17.538 secs (44.710 CPU)
32.276027 [dev_pixelpipe] pipeline resync with history took 0.042 secs (0.054 CPU) for pipe full
32.276152 [dev_process_full] pipeline processing thread took 0.000 secs (0.000 CPU)
```
{{< /tab >}}

{{< tab "Darktable 5.4.1 CPU" >}}
```
12.5778 [dt_dev_process_image_job] loading image. took 0.000 secs (0.000 CPU)
12.6692 [dt_dev_process_image_job] loading image. took 0.000 secs (0.000 CPU)
12.7607 [dev_pixelpipe] took 0.018 secs (0.022 CPU) initing base buffer [full]
12.8098 [dev_pixelpipe] took 0.049 secs (0.105 CPU) [full] processed `rawprepare' on CPU
12.8448 [dev_pixelpipe] took 0.035 secs (0.042 CPU) [full] processed `temperature' on CPU
25.7535 [dev_pixelpipe] took 12.909 secs (44.992 CPU) [full] processed `highlights' on CPU
27.5129 [resample_plain] plan 0.001 secs (0.001 CPU) resample 0.732 secs (2.749 CPU)
27.5515 [dev_pixelpipe] took 1.798 secs (6.012 CPU) [full] processed `demosaic' on CPU
27.5571 [dev_pixelpipe] took 0.006 secs (0.005 CPU) [full] processed `lens' on CPU
27.5603 [dev_pixelpipe] took 0.003 secs (0.003 CPU) [full] processed `ashift' on CPU
27.5634 [dev_pixelpipe] took 0.003 secs (0.005 CPU) [full] processed `exposure' on CPU
27.5963 [dev_pixelpipe] took 0.033 secs (0.100 CPU) [full] processed `colorin' on CPU
27.6038 [dt_ioppr_transform_image_colorspace] IOP_CS_LAB-->IOP_CS_RGB took 0.007 secs (0.018 CPU) [channelmixerrgb]
27.6464 [dev_pixelpipe] took 0.050 secs (0.163 CPU) [full] processed `channelmixerrgb' on CPU
31.0132 [dev_pixelpipe] took 3.367 secs (12.611 CPU) [full] processed `diffuse' on CPU
31.1921 [dev_pixelpipe] took 0.179 secs (0.647 CPU) [full] processed `colorbalancergb' on CPU
31.3065 [dev_pixelpipe] took 0.114 secs (0.406 CPU) [full] processed `filmicrgb' on CPU
31.3321 [dt_ioppr_transform_image_colorspace] IOP_CS_RGB-->IOP_CS_LAB took 0.025 secs (0.091 CPU) [colorout]
31.3475 [dev_pixelpipe] took 0.041 secs (0.124 CPU) [full] processed `colorout' on CPU
31.3754 [dev_pixelpipe] took 0.028 secs (0.020 CPU) [full] processed `dither' on CPU
31.3849 [dev_pixelpipe] took 0.009 secs (0.034 CPU) [full] processed `gamma' on CPU
31.3870 [dev_process_image] pixel pipeline took 18.644 secs (65.292 CPU) processing `2022-06-17__DSC00078.arw'

31.4040 [dt_dev_process_image_job] loading image. took 0.000 secs (0.000 CPU)
31.5018 [dt_dev_process_image_job] loading image. took 0.000 secs (0.000 CPU)
31.5722 [dev_pixelpipe] took 0.000 secs (0.000 CPU) initing base buffer [preview]
31.5824 [dev_pixelpipe] took 0.010 secs (0.014 CPU) [preview] processed `rawprepare' on CPU
31.5861 [dev_pixelpipe] took 0.004 secs (0.008 CPU) [preview] processed `temperature' on CPU
32.7796 [dev_pixelpipe] took 1.193 secs (4.129 CPU) [preview] processed `highlights' on CPU
32.8917 [dev_pixelpipe] took 0.112 secs (0.387 CPU) [preview] processed `demosaic' on CPU
32.9099 [dev_pixelpipe] took 0.018 secs (0.031 CPU) [preview] processed `lens' on CPU
32.9433 [dev_pixelpipe] took 0.033 secs (0.050 CPU) [preview] processed `ashift' on CPU
32.9610 [dev_pixelpipe] took 0.018 secs (0.023 CPU) [preview] processed `exposure' on CPU
33.1087 [dev_pixelpipe] took 0.148 secs (0.532 CPU) [preview] processed `colorin' on CPU
33.1362 [dt_ioppr_transform_image_colorspace] IOP_CS_LAB-->IOP_CS_RGB took 0.027 secs (0.096 CPU) [channelmixerrgb]
33.3353 [dev_pixelpipe] took 0.227 secs (0.825 CPU) [preview] processed `channelmixerrgb' on CPU
55.6386 [dev_pixelpipe] took 22.303 secs (82.427 CPU) [preview] processed `diffuse' on CPU
56.4763 [dev_pixelpipe] took 0.837 secs (3.180 CPU) [preview] processed `colorbalancergb' on CPU
57.0002 [dev_pixelpipe] took 0.523 secs (1.966 CPU) [preview] processed `filmicrgb' on CPU
57.1225 [dt_ioppr_transform_image_colorspace] IOP_CS_RGB-->IOP_CS_LAB took 0.122 secs (0.437 CPU) [colorout]
57.1852 [dev_pixelpipe] took 0.184 secs (0.637 CPU) [preview] processed `colorout' on CPU
57.2043 [dev_pixelpipe] took 0.019 secs (0.018 CPU) [preview] processed `dither' on CPU
57.2471 [dev_pixelpipe] took 0.043 secs (0.155 CPU) [preview] processed `gamma' on CPU
57.4090 [dev_process_image] pixel pipeline took 25.837 secs (94.647 CPU) processing `2022-06-17__DSC00078.arw'
```
{{< /tab >}}

{{< /tabs >}}


#### Report

First of all, when entering the darkroom, __Darktable computes 2 pipelines for the same result as Ansel, that computes only one__. So, regardless of actual runtimes, already it's half useless computations.

Then, Ansel computes the full-resolution image up to the _initial resampling_ module, while Darktable does it only until before _demosaic_. This is important to the comparison.

{{< tabs >}}

{{< tab "Config 1" >}}
{{< table class="align-center">}}
Config 1 (Intel Xeon, Nvidia Quadro)
| Software | GPU | CPU |
|----------|-----|-----|
| Ansel    | 4.493 s | 10.692 s |
| Darktable 5.4.1 | 6.822 s | 13.66 s |
{{< /table >}}
{{< /tab >}}
{{< tab "Config 2">}}
{{< table class="align-center" >}}
Config 2 (Intel i3, AMD Radeon)
| Software | GPU | CPU |
|----------|-----|-----|
| Ansel    | 3.784 s | 17.538 s |
| Darktable 5.4.1 | 3.927 s | 44.481 s |
{{< /table >}}
{{< /tab >}}

{{< /tabs >}}

On config 1, Darktable manages to be slower than Ansel on GPU even looking only at the single _main pipeline_ rendering and even downscaling the picture to display resolution earlier in the pipeline. But we actually see the same effect on CPU, despite all the alleged "optimizations" done on CPU-only modules circa 2023-2024 in Darktable. Overall, Darktable will freeze your computer 2.3 to 3.0 s longer.

On config 2, the CPU path shows 0.8 s penalty for Ansel on the main pipeline only. The AMD GPU shows a bad hit on the _lens correction_ module, that processes full-resolution on Ansel while it processes at screen resolution in Darktable. Taking into account the second pipeline that Darktable runs, Ansel is faster by 0.14 s on GPU. On CPU however, Darktable now freezes the machine 27.07 s longer because of that duplicated pipeline work. Anyway, the slow modules are at the beginning of the pipeline and will not be recomputed for the whole session.

That gives us the following side-by-side module comparisons. We compare only modules running at the same resolution for the main image pipeline only. In the averages tab, factors are averaged with a geometric mean and relative differences with a harmonic mean:

{{< tabs >}}
{{< tab "Config 1">}}
{{< table >}}


| Module / chip | Ansel Master | Darktable 5.4.1 | Ansel speed-up |
| ------------- | ------------ | --------------- | -------------- |
| Raw settings* / CPU | 35 ms | 40 ms | __×1.14 / +13 %__ |
| Raw settings* / GPU | 18 ms | 17 ms | ×0.96 / -5.9 % |
| White balance* / CPU | 31 ms | 29 ms | ×0.94 / -6.9 % |
| White balance* / GPU | 16 ms | 13 ms | ×0.81 / -23 % |
| Highlights reconstruction* / CPU | 6.12 s | 6.33 s | __×1.03 / +3.4 %__ |
| Highlights reconstruction* / GPU | 2.49 s | 3.70 s | __×1.49 / +33 %__ |
| Demosaic* / CPU | 348 ms | 717 ms | __×2.06 / +52 %__ |
| Demosaic* / GPU | 254 ms | 490 ms | __×1.93 / +48 %__ |
| Exposure / CPU | 5 ms | 12 ms | __×2.4 / +58 %__ |
| Exposure / GPU | 6 ms | 10 ms | __×1.67 / +40 %__ |
| Input color profile / CPU | 3 ms | 15 ms | __×3 / +67 %__ |
| Input color profile / GPU | 2 ms | 32 ms | __×16 / +94 %__ | 
| Color calibration / CPU | 70 ms | 84 ms | __×1.20 / +25 %__ |
| Color calibration / GPU | 1 ms | 39 ms | __×39 / +97 %__ |
| Diffuse or sharpen / CPU | 3.15 s | 4.21 s | __×1.34 / +25.2 %__ |
| Diffuse or sharpen / GPU | 1.09 s | 1.69 s | __×1.55 / +35 %__ |
| Color balance RGB / CPU | 262 ms | 261 ms | ×1.00 / -0.4 % |
| Color balance RGB / GPU | 9 ms | 12 ms | __×1.33 / +25 %__ |
| Filmic RGB / CPU | 137 ms | 140 ms | __×1.02 / +2.1 %__ |
| Filmic RGB / GPU | 6 ms | 11 ms | __×1.83 / +45 %__ |
| Output color profile / CPU | 13 ms | 21 ms | __×1.62 / +38 %__ |
| Output color profile / GPU | 23 ms | 52 ms | __×2.26 / +56 %__ |
| Dithering (CPU only) / CPU pipeline | 24 ms | 59 ms | __×2.46 / +59 %__ |
| Dithering (CPU only) / GPU pipeline | 43 ms | 88 ms | __×2.05 / +51 %__ |
| Gamma / display encoding (CPU only) / GPU pipeline | 12 ms | 9 ms | ×0.75 / -33 % |
| Gamma / display encoding (CPU only) / CPU pipeline | 12 ms | 9 ms | ×0.75 / -33 % |
{{< /table >}}
*: modules running at full resolution on both applications.
{{< /tab >}}
{{< tab "Config 2">}}
{{< table >}}
| Module / chip | Ansel Master | Darktable 5.4.1 | Ansel speed-up |
| ------------- | ------------ | --------------- | -------------- |
| Raw settings* / CPU | 63 ms | 49 ms | ×0.78 / -29 % |
| Raw settings* / GPU | 37 ms | 59 ms | __×1.59 / +37 %__ |
| White balance* / CPU | 69 ms | 35 ms | ×0.5 / -97 % |
| White balance* / GPU | 7 ms | 7 ms | ×1 / 0 % |
| Highlights reconstruction* / CPU | 13.10 s | 12.91 s | ×0.99 / -1.5 % |
| Highlights reconstruction* / GPU | 1.42 s | 1.87 s | __×1.32 / +24 %__ |
| Demosaic* / CPU | 0.99 s | 1.80 s | __×1.82 / +45 %__ |
| Demosaic* / GPU | 149 ms | 123 ms | ×0.83 / -21 % |
| Exposure / CPU | 13 ms | 3 ms | ×0.23 / -333 % |
| Exposure / GPU | 33 ms | 2 ms | ×0.06 / -1550 % |
| Input color profile / CPU | 2 ms | 33 ms | __×16.5 / +94 %__ |
| Input color profile / GPU | 11 ms | 4 ms | ×0.36 / -175 % |
| Color calibration / CPU | 47 ms | 50 ms | __×1.06 / 6 %__ |
| Color calibration / GPU | 2 ms | 4 ms | __×2 / +50 %__ |
| Diffuse or sharpen / CPU | 1.71 s | 3.37 s | __×1.97 / +49.4 %__ |
| Diffuse or sharpen / GPU | 158 ms | 518 ms | __×3.28 / +70 %__ |
| Color balance RGB / CPU | 152 ms | 179 ms | __×1.18 / +15 %__ |
| Color balance RGB / GPU | 10 ms | 4 ms | ×0.40 / -150 % |
| Filmic RGB / CPU | 93 ms | 114 ms | __×1.23 / +18 %__ |
| Filmic RGB / GPU | 4 ms | 41 ms | __×10.26 / +90 %__ |
| Output color profile / CPU | 15 ms | 41 ms | __×2.73 / +63 %__ |
| Output color profile / GPU | 22 ms | 32 ms | __×1.45 / +31 %__ |
| Dithering (CPU only) / CPU pipeline | 9 ms | 28 ms | __×3.62 / +72 %__ |
| Dithering (CPU only) / GPU pipeline | 13 ms | 47 ms | __×2.76 / +64 %__ |
| Gamma / display encoding (CPU only) / CPU pipeline | 9 ms | 9 ms | ×1.0 / 0 % |
| Gamma / display encoding (CPU only) / GPU pipeline | 8 ms | 15 ms | __×1.88 / +47 %__ |
{{< /table >}}
*: modules running at full resolution on both applications.
{{< /tab >}}

{{< tab "Averages" >}}
{{< table >}}
Average over config 1 and 2. Factors are averaged with a geometric mean, and relative differences with a harmonic mean.
| Module / chip | Ansel speed-up | Ansel code |
| ------------- | -------------- | ---------- |
| Raw settings* / CPU | ×0.94 | Custom (manually vectorized) |
| Raw settings* / GPU | __×1.23__ | Same as DT 5.4.1 |
| White balance* / CPU | ×0.69 | Custom (manually vectorized) |
| White balance* / GPU | ×0.97 | Same as DT 5.4.1 |
| Highlights reconstruction* / CPU | __×1.1__ | Custom (manually vectorized) |
| Highlights reconstruction* / GPU | __×1.4__ | Custom |
| Demosaic* / CPU | __×1.94__ | Same as DT 4.0 |
| Demosaic* / GPU | __×1.26__ | Custom |
| Exposure / CPU | ×0.74 | Custom (manually vectorized) |
| Exposure / GPU | ×0.32 | Same as DT 4.0 |
| Input color profile / CPU | __×7.04__ | Custom (manually vectorized) |
| Input color profile / GPU | __×2.41__ | Custom | 
| Color calibration / CPU | __×1.13__ | Custom (manually vectorized) |
| Color calibration / GPU | __×8.83__ | Same as DT 4.0 |
| Diffuse or sharpen / CPU | __×1.08__ | Custom |
| Diffuse or sharpen / GPU | __×2.26__ | Custom |
| Color balance RGB / CPU | __×1.08__ | Custom (manually vectorized) |
| Color balance RGB / GPU | ×0.73 | Custom |
| Filmic RGB / CPU | __×1.13__ | Custom (manually vectorized) |
| Filmic RGB / GPU | __×4.33__ | Custom |
| Output color profile / CPU | __×2.10__ | Custom (manually vectorized) |
| Output color profile / GPU | __×1.81__ | Custom |
| Dithering (CPU only) / CPU pipeline | __×2.71__ | Same as DT 4.0 |
| Dithering (CPU only) / GPU pipeline | __×2.72__ | Same as DT 4.0 |
| Gamma / display encoding (CPU only) / CPU pipeline | ×0.87 | Same as DT 5.4.1 |
| Gamma / display encoding (CPU only) / GPU pipeline | __×1.63__ | Same as DT 5.4.1 |
{{< /table >}}
*: modules running at full resolution on both applications.
{{< /tab >}}
{{< /tabs>}}

{{< plotly title="Per-module relative speed (GPU). Higher is better." src="plotly-per-module-gpu.json" class="full-width" >}}

Ansel shows GPU performance improvements even on kernels that didn't change since Darktable 4.0. The speed-ups we see are clearly the benefit of the high-level memory management, using pinned OpenCL memory or plain device-only memory buffers where relevant.

__Across all modules, Ansel is 1.6 times faster than Darktable on GPU__ (geometric mean).

{{< plotly title="Per-module relative speed (CPU). Higher is better." src="plotly-per-module-cpu.json" class="full-width" >}}

On CPU, things are more nuanced. 

Darktable 5.x is supposed to have received CPU optimizations for _dithering_ and _demosaicing (RCD)_, yet both those modules perform worse than Ansel's on both configs.

Ansel has manually-vectorized CPU pixel code for _color balance RGB_, _color calibration_, _input/output color profile_, _raw settings_, _white balance_, _exposure_ and _filmic_, while Darktable's uses compiler hints to achieve vectorization. All of them perform better than Darktable's on the Intel Xeon. On the Intel i3, for basic color matrix work, Ansel performs (much) better, but it's slightly harming basic modules like _raw settings_, _exposure_ or _white balance_. 

_Diffuse or sharpen_ should still use the same Darktable 4.0 code on Ansel and on Darktable 5.4, but Ansel is faster on both configs, which shows the benefit of using our own pre-allocated memory array for the cache, and allocating memory buffers out of the OS circuitery. This shows that optimizing at the module level is the wrong scope, and that global pipeline needed an overhaul first.

But it is of course easier to tackle module-wise optimizations because modules are… modular, meaning self-enclosed and self-reliant. There is little chance to mess up the software as a whole when working on modules because they are isolated from the rest. Application-global architecture changes, on the other hand, are dangerous, complicated and demanding. And they impose to stop the development of any new module at all while the architecture is getting stabilized. Which Darktable will never have the discipline to do, because the project is unmanaged and random pull requests pop up on Github at random times from random contributors.

__Across all modules, Ansel is 1.8 times faster than Darktable on CPU__ (geometric mean).

Combining GPU and CPU pathes over both config, opening the darkroom is __1.5 times faster on Ansel__ (geometric mean).

### Zooming at 100%

In darkroom, from the "zoom to fit" size, zoom at 100% in one click using the middle button of the mouse

{{< tabs >}}
{{< tab "Ansel Master GPU" >}}
```
12.083270 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
12.111607 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe full
12.177976 [dev_pixelpipe] took 0.066 secs (0.095 CPU) processed `Initial resampling' on GPU, blended on  [full]
12.179976 [dev_pixelpipe] took 0.002 secs (0.002 CPU) processed `exposure' on GPU, blended on GPU [full]
12.183194 [dev_pixelpipe] took 0.003 secs (0.005 CPU) processed `input color profile' on GPU, blended on  [full]
12.185327 [dev_pixelpipe] took 0.002 secs (0.002 CPU) processed `color calibration' on GPU, blended on GPU [full]
14.887725 [dev_pixelpipe] took 2.702 secs (2.705 CPU) processed `diffuse or sharpen' on GPU, blended on GPU [full]
14.899213 [dev_pixelpipe] took 0.011 secs (0.013 CPU) processed `color balance rgb' on GPU, blended on GPU [full]
14.907159 [dev_pixelpipe] took 0.008 secs (0.008 CPU) processed `filmic rgb' on GPU, blended on GPU [full]
14.940617 [dev_pixelpipe] took 0.033 secs (0.039 CPU) processed `output color profile' on GPU, blended on  [full]
15.005967 [dev_pixelpipe] took 0.065 secs (0.057 CPU) processed `dithering' on CPU, blended on  [full]
15.030966 [dev_pixelpipe] took 0.025 secs (0.036 CPU) processed `display encoding' on GPU, blended on  [full]
15.031021 [pixelpipe] full internal pixel pipeline processing took 2.919 secs (2.963 CPU)
15.031080 [dev_process_full] pipeline processing thread took 2.919 secs (2.963 CPU)
```
{{< /tab >}}
{{< tab "Darktable 5.4.1 GPU" >}}
```
89,3415 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
89,3440 [dev_pixelpipe] took 0,002 secs (0,001 CPU) initing base buffer [full]
89,3544 [dev_pixelpipe] took 0,010 secs (0,008 CPU) [full] processed `rawprepare' on GPU, blended on GPU
89,3573 [dev_pixelpipe] took 0,003 secs (0,001 CPU) [full] processed `temperature' on GPU, blended on GPU
89,8611 [dev_pixelpipe] took 0,504 secs (0,511 CPU) [full] processed `highlights' on GPU, blended on GPU
89,9200 [dev_pixelpipe] took 0,059 secs (0,023 CPU) [full] processed `demosaic' on GPU, blended on GPU
89,9251 [dev_pixelpipe] took 0,005 secs (0,003 CPU) [full] processed `lens' on GPU, blended on GPU
89,9346 [dev_pixelpipe] took 0,009 secs (0,003 CPU) [full] processed `exposure' on GPU, blended on GPU
89,9480 [dev_pixelpipe] took 0,013 secs (0,003 CPU) [full] processed `colorin' on GPU, blended on GPU
89,9576 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0,009 secs (0,002 GPU) [channelmixerrgb]
89,9736 [dev_pixelpipe] took 0,026 secs (0,008 CPU) [full] processed `channelmixerrgb' on GPU, blended on GPU
94,9286 [dev_pixelpipe] took 4,955 secs (5,096 CPU) [full] processed `diffuse' on GPU, blended on GPU
94,9528 [dev_pixelpipe] took 0,024 secs (0,007 CPU) [full] processed `colorbalancergb' on GPU, blended on GPU
94,9836 [dev_pixelpipe] took 0,031 secs (0,009 CPU) [full] processed `filmicrgb' on GPU, blended on GPU
94,9925 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,009 secs (0,000 GPU) [colorout]
95,0700 [dev_pixelpipe] took 0,086 secs (0,028 CPU) [full] processed `colorout' on GPU, blended on GPU
95,2184 [dev_pixelpipe] took 0,148 secs (0,093 CPU) [full] processed `dither' on CPU, blended on CPU
95,2404 [dev_pixelpipe] took 0,022 secs (0,104 CPU) [full] processed `gamma' on CPU, blended on CPU
95,2537 [dev_process_image] pixel pipeline took 5,912 secs (5,904 CPU) processing `2022-06-17__DSC00078.arw'
95,2634 [dev_process_image] pixel pipeline took 0,010 secs (0,004 CPU) processing `2022-06-17__DSC00078.arw'
```
{{< /tab >}}
{{< /tabs >}}

Because Ansel processes the early stages of the pipeline at full-resolution, they are also __cached__ at full resolution, meaning panning and zooming fetch the cache upstream of the _initial resampling_ module, and recomputes only downstream. So it takes 2.9 s to render. Darktable, on the other hand, has a non-working cache, so it recomputes everything from the beginning, taking 5.9 s to render the same thing. It's __a ×2 performance penalty for Darktable__.

### Panning at 100%

{{< tabs >}}
{{< tab "Ansel master GPU">}}
```
55.456510 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe full
55.536149 [dev_pixelpipe] took 0.080 secs (0.121 CPU) processed `Initial resampling' on GPU, blended on  [full]
55.541720 [dev_pixelpipe] took 0.005 secs (0.003 CPU) processed `exposure' on GPU, recycled cacheline, blended on GPU [full]
55.551346 [dev_pixelpipe] took 0.009 secs (0.006 CPU) processed `input color profile' on GPU, recycled cacheline, blended on  [full]
55.559669 [dev_pixelpipe] took 0.008 secs (0.005 CPU) processed `color calibration' on GPU, recycled cacheline, blended on GPU [full]
58.290943 [dev_pixelpipe] took 2.731 secs (2.819 CPU) processed `diffuse or sharpen' on GPU, blended on GPU [full]
58.310400 [dev_pixelpipe] took 0.016 secs (0.017 CPU) processed `color balance rgb' on GPU, recycled cacheline, blended on GPU [full]
58.322514 [dev_pixelpipe] took 0.012 secs (0.014 CPU) processed `filmic rgb' on GPU, recycled cacheline, blended on GPU [full]
58.364803 [dev_pixelpipe] took 0.042 secs (0.054 CPU) processed `output color profile' on GPU, blended on  [full]
58.411730 [dev_pixelpipe] took 0.045 secs (0.052 CPU) processed `dithering' on CPU, recycled cacheline, blended on  [full]
58.440432 [dev_pixelpipe] took 0.029 secs (0.042 CPU) processed `display encoding' on GPU, blended on  [full]
58.440478 [pixelpipe] full internal pixel pipeline processing took 2.984 secs (3.133 CPU)
58.440516 [dev_process_full] pipeline processing thread took 2.984 secs (3.133 CPU)
```
{{< /tab >}}
{{< tab "Darktable 5.4.1 GPU">}}
```
253,1082 [dev_pixelpipe] took 0,003 secs (0,004 CPU) initing base buffer [full]
253,1132 [dev_pixelpipe] took 0,005 secs (0,004 CPU) [full] processed `rawprepare' on GPU, blended on GPU
253,1152 [dev_pixelpipe] took 0,002 secs (0,001 CPU) [full] processed `temperature' on GPU, blended on GPU
253,5743 [dev_pixelpipe] took 0,459 secs (0,441 CPU) [full] processed `highlights' on GPU, blended on GPU
253,6257 [dev_pixelpipe] took 0,051 secs (0,022 CPU) [full] processed `demosaic' on GPU, blended on GPU
253,6307 [dev_pixelpipe] took 0,005 secs (0,002 CPU) [full] processed `lens' on GPU, blended on GPU
253,6394 [dev_pixelpipe] took 0,009 secs (0,003 CPU) [full] processed `exposure' on GPU, blended on GPU
253,6521 [dev_pixelpipe] took 0,013 secs (0,003 CPU) [full] processed `colorin' on GPU, blended on GPU
253,6610 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0,008 secs (0,001 GPU) [channelmixerrgb]
253,6760 [dev_pixelpipe] took 0,024 secs (0,005 CPU) [full] processed `channelmixerrgb' on GPU, blended on GPU
258,6306 [dev_pixelpipe] took 4,954 secs (4,882 CPU) [full] processed `diffuse' on GPU, blended on GPU
258,6548 [dev_pixelpipe] took 0,024 secs (0,005 CPU) [full] processed `colorbalancergb' on GPU, blended on GPU
258,6857 [dev_pixelpipe] took 0,031 secs (0,009 CPU) [full] processed `filmicrgb' on GPU, blended on GPU
258,6945 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,009 secs (0,000 GPU) [colorout]
258,7725 [dev_pixelpipe] took 0,087 secs (0,034 CPU) [full] processed `colorout' on GPU, blended on GPU
258,9198 [dev_pixelpipe] took 0,147 secs (0,086 CPU) [full] processed `dither' on CPU, blended on CPU
258,9373 [dev_pixelpipe] took 0,017 secs (0,110 CPU) [full] processed `gamma' on CPU, blended on CPU
258,9407 [dev_process_image] pixel pipeline took 5,859 secs (5,614 CPU) processing `2022-06-17__DSC00078.arw'
258,9735 [dev_process_image] pixel pipeline took 0,033 secs (0,014 CPU) processing `2022-06-17__DSC00078.arw'
```
{{< /tab >}}
{{< /tabs >}}

Panning leads to the same situation as zooming : Ansel has the full-resolution image in cache, so we recompute from _initial resampling_, which takes 3 s. Darktable can't manage a cache properly so you are on for a full recomputation and 5,9 s of frozen computer.

### Going back and forth to lighttable

{{< tabs >}}
{{< tab "Ansel master GPU" >}}
```
184.536988 [dev_pixelpipe] took 0.000 secs (0.000 CPU) to load the image.
184.574776 [dev_pixelpipe] pipeline resync with history took 0.019 secs (0.016 CPU) for pipe virtual-preview
185.012395 [dev_pixelpipe] pipeline resync with history took 0.020 secs (0.017 CPU) for pipe virtual-preview
185.046379 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
185.046424 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe virtual-preview
185.049883 [dev_pixelpipe] pipeline resync with history took 0.026 secs (0.049 CPU) for pipe preview
185.049931 [dev_pixelpipe] pipeline resync with history took 0.000 secs (0.000 CPU) for pipe preview
185.068516 [dev_pixelpipe] pipeline resync with history took 0.019 secs (0.020 CPU) for pipe full
185.068568 [dev_process_full] pipeline processing thread took 0.000 secs (0.000 CPU)
```
{{< /tab >}}
{{< tab "Darktable 5.4.1 GPU" >}}
```
3324,5295 [histogram] took 0,006 secs (0,005 CPU) scope draw
3324,5837 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
3324,6371 [dev_process_image] pixel pipeline took 0,002 secs (0,001 CPU) processing `2022-06-17__DSC00078.arw'

3324,7352 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
3324,8074 [dev_pixelpipe] took 0,000 secs (0,000 CPU) initing base buffer [preview]
3324,8155 [dev_pixelpipe] took 0,008 secs (0,005 CPU) [preview] processed `rawprepare' on GPU, blended on GPU
3324,8165 [dev_pixelpipe] took 0,001 secs (0,001 CPU) [preview] processed `temperature' on GPU, blended on GPU
3324,8178 [dev_pixelpipe] took 0,001 secs (0,003 CPU) [preview] processed `highlights' on GPU, blended on GPU
3324,8337 [dev_pixelpipe] took 0,016 secs (0,021 CPU) [preview] processed `demosaic' on GPU, blended on GPU
3324,8351 [dev_pixelpipe] took 0,001 secs (0,002 CPU) [preview] processed `lens' on GPU, blended on GPU
3324,8387 [dev_pixelpipe] took 0,003 secs (0,013 CPU) [preview] processed `exposure' on GPU, blended on GPU
3324,8440 [dev_pixelpipe] took 0,005 secs (0,015 CPU) [preview] processed `colorin' on GPU, blended on GPU
3324,8491 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0,005 secs (0,012 GPU) [channelmixerrgb]
3324,8545 [dev_pixelpipe] took 0,010 secs (0,019 CPU) [preview] processed `channelmixerrgb' on GPU, blended on GPU
3325,6440 [dev_pixelpipe] took 0,789 secs (0,921 CPU) [preview] processed `diffuse' on GPU, blended on GPU
3325,6482 [dev_pixelpipe] took 0,004 secs (0,003 CPU) [preview] processed `colorbalancergb' on GPU, blended on GPU
3325,6534 [dev_pixelpipe] took 0,005 secs (0,003 CPU) [preview] processed `filmicrgb' on GPU, blended on GPU
3325,6569 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,003 secs (0,002 GPU) [colorout]
3325,6747 [dev_pixelpipe] took 0,021 secs (0,008 CPU) [preview] processed `colorout' on GPU, blended on GPU
3325,6979 [dev_pixelpipe] took 0,023 secs (0,009 CPU) [preview] processed `dither' on CPU, blended on CPU
3325,7061 [dev_pixelpipe] took 0,008 secs (0,029 CPU) [preview] processed `gamma' on CPU, blended on CPU
3325,7263 [dt_ioppr_transform_image_colorspace_rgb] `Système' -> `sRVB' took 0,020 secs (0,068 CPU) [final histogram]
3325,7357 [dev_process_image] pixel pipeline took 0,935 secs (1,152 CPU) processing `2022-06-17__DSC00078.arw'
```
{{< /tab >}}
{{< /tabs >}}

Ansel pipeline cache survives the pipelines themselves, meaning that leaving the darkroom and entering it again is nearly-instant : it takes only 60 ms to resynchronize all pipelines with the development history and figure out that we already have the pipeline output in cache through its checksum. Note that there is no special handling of any sort here, it's generic cache behaviour.

For Darktable, it almost works for the main image, but not for the preview one, so you are on for 1 s of useless recomputation. Given that this works on one pipe but not on the other, I'm going to bet it's handled through special cases. Compared to Ansel, it's __a ×1.75 performance penalty__.

### Disabling a module mid-pipeline

I disabled _diffuse or sharpen_ zoomed in "fill" size.

{{< tabs >}}
{{< tab "Ansel master GPU">}}
```
124.206298 [dev_pixelpipe] pipeline resync with history took 0.011 secs (0.010 CPU) for pipe virtual-preview
124.237640 [dev_pixelpipe] pipeline resync with history took 0.011 secs (0.028 CPU) for pipe preview
124.290940 [dev_pixelpipe] took 0.023 secs (0.028 CPU) processed `color balance rgb' on GPU, recycled cacheline, blended on GPU [preview]
124.303176 [dev_pixelpipe] took 0.012 secs (0.012 CPU) processed `filmic rgb' on GPU, recycled cacheline, blended on GPU [preview]
124.331334 [dev_pixelpipe] took 0.028 secs (0.030 CPU) processed `output color profile' on GPU, blended on  [preview]
124.370193 [darkroom] took 0.007 secs (0.011 CPU) redraw
124.382425 [dev_pixelpipe] took 0.051 secs (0.055 CPU) processed `dithering' on CPU, blended on  [preview]
124.410053 [dev_pixelpipe] took 0.028 secs (0.028 CPU) processed `display encoding' on GPU, blended on  [preview]
124.410088 [pixelpipe] preview internal pixel pipeline processing took 0.172 secs (0.197 CPU)
124.410115 [dev_process_preview] pipeline processing thread took 0.172 secs (0.197 CPU)
124.433186 [dev_pixelpipe] pipeline resync with history took 0.013 secs (0.017 CPU) for pipe full
124.433286 [dev_process_full] pipeline processing thread took 0.000 secs (0.000 CPU)
```
{{< /tab >}}
{{< tab "Darktable 5.4.1 GPU" >}}
```
300,9771 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
301,0564 [dev_pixelpipe] took 0,016 secs (0,028 CPU) initing base buffer [full]
301,0936 [dev_pixelpipe] took 0,037 secs (0,037 CPU) [full] processed `rawprepare' on GPU, blended on GPU
301,1102 [dev_pixelpipe] took 0,017 secs (0,017 CPU) [full] processed `temperature' on GPU, blended on GPU
304,8700 [dev_pixelpipe] took 3,760 secs (3,786 CPU) [full] processed `highlights' on GPU with tiling, blended on CPU
305,2630 [resample_cl] plan 0,001 secs (0,000 CPU) resample 0,010 secs (0,001 CPU)
305,3543 [dev_pixelpipe] took 0,484 secs (0,284 CPU) [full] processed `demosaic' on GPU, blended on GPU
305,3648 [dev_pixelpipe] took 0,010 secs (0,002 CPU) [full] processed `lens' on GPU, blended on GPU
305,3764 [dev_pixelpipe] took 0,012 secs (0,001 CPU) [full] processed `exposure' on GPU, blended on GPU
305,4083 [dev_pixelpipe] took 0,032 secs (0,007 CPU) [full] processed `colorin' on GPU, blended on GPU
305,4220 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0,013 secs (0,001 GPU) [channelmixerrgb]
305,4460 [dev_pixelpipe] took 0,038 secs (0,008 CPU) [full] processed `channelmixerrgb' on GPU, blended on GPU
307,1388 [dev_pixelpipe] took 1,693 secs (1,645 CPU) [full] processed `diffuse' on GPU, blended on GPU
307,1500 [dev_pixelpipe] took 0,011 secs (0,003 CPU) [full] processed `colorbalancergb' on GPU, blended on GPU
307,1612 [dev_pixelpipe] took 0,011 secs (0,003 CPU) [full] processed `filmicrgb' on GPU, blended on GPU
307,1675 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,006 secs (0,001 GPU) [colorout]
307,2185 [dev_pixelpipe] took 0,057 secs (0,020 CPU) [full] processed `colorout' on GPU, blended on GPU
307,3095 [dev_pixelpipe] took 0,091 secs (0,056 CPU) [full] processed `dither' on CPU, blended on CPU
307,3235 [dev_pixelpipe] took 0,014 secs (0,065 CPU) [full] processed `gamma' on CPU, blended on CPU
307,3333 [dev_process_image] pixel pipeline took 6,293 secs (5,969 CPU) processing `2022-06-17__DSC00078.arw'
307,3843 [dev_process_image] pixel pipeline took 0,051 secs (0,018 CPU) processing `2022-06-17__DSC00078.arw'

307,4258 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
307,4875 [dev_pixelpipe] took 0,000 secs (0,000 CPU) initing base buffer [preview]
307,4895 [dev_pixelpipe] took 0,002 secs (0,002 CPU) [preview] processed `rawprepare' on GPU, blended on GPU
307,4906 [dev_pixelpipe] took 0,001 secs (0,001 CPU) [preview] processed `temperature' on GPU, blended on GPU
307,5609 [dev_pixelpipe] took 0,070 secs (0,072 CPU) [preview] processed `highlights' on GPU, blended on GPU
307,5722 [dev_pixelpipe] took 0,011 secs (0,005 CPU) [preview] processed `demosaic' on GPU, blended on GPU
307,5740 [dev_pixelpipe] took 0,002 secs (0,000 CPU) [preview] processed `lens' on GPU, blended on GPU
307,5765 [dev_pixelpipe] took 0,002 secs (0,001 CPU) [preview] processed `exposure' on GPU, blended on GPU
307,5798 [dev_pixelpipe] took 0,003 secs (0,002 CPU) [preview] processed `colorin' on GPU, blended on GPU
307,5827 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0,002 secs (0,002 GPU) [channelmixerrgb]
307,5864 [dev_pixelpipe] took 0,007 secs (0,003 CPU) [preview] processed `channelmixerrgb' on GPU, blended on GPU
307,9576 [dev_pixelpipe] took 0,371 secs (0,356 CPU) [preview] processed `diffuse' on GPU, blended on GPU
307,9613 [dev_pixelpipe] took 0,003 secs (0,002 CPU) [preview] processed `colorbalancergb' on GPU, blended on GPU
307,9659 [dev_pixelpipe] took 0,005 secs (0,000 CPU) [preview] processed `filmicrgb' on GPU, blended on GPU
307,9686 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,002 secs (0,000 GPU) [colorout]
307,9847 [dev_pixelpipe] took 0,019 secs (0,008 CPU) [preview] processed `colorout' on GPU, blended on GPU
308,0043 [dev_pixelpipe] took 0,020 secs (0,011 CPU) [preview] processed `dither' on CPU, blended on CPU
308,0103 [dev_pixelpipe] took 0,006 secs (0,023 CPU) [preview] processed `gamma' on CPU, blended on CPU
308,0244 [dt_ioppr_transform_image_colorspace_rgb] `Système' -> `sRVB' took 0,014 secs (0,081 CPU) [final histogram]
308,0353 [dev_process_image] pixel pipeline took 0,553 secs (0,604 CPU) processing `2022-06-17__DSC00078.arw'
```
{{< /tab >}}
{{< /tabs >}}

Again, Ansel cache just works : disabling _diffuse or sharpen_ leads to recomputing only the downstream modules, which takes 172 ms. Again, Darktable cache does __not__ work, which leads to recomputing the whole 2 pipelines (preview and main), for a total cost of 6.9 s. __That's a ×40 performance penalty folks !__ That makes all the +15 % spurious speed-ups of Ralf Brown completely irrelevant ! But hey ! [Keep looking for your lost keys where you can see your feet instead of where you lost them !](https://en.wikipedia.org/wiki/Streetlight_effect).

### Color-picking in a module

We set white balance in color calibration using the color picker, at the "zoom to fit" zoom level.

{{< tabs >}}
{{< tab "Ansel master GPU" >}}
```
colorpicker stats reading took 0.020 secs (0.088 CPU)
colorpicker stats reading took 0.018 secs (0.087 CPU)
12.128030 [dev_pixelpipe] pipeline resync with history took 0.024 secs (0.037 CPU) for pipe virtual-preview
12.137829 [dev_pixelpipe] pipeline resync with history took 0.024 secs (0.056 CPU) for pipe preview
12.157665 [dev_pixelpipe] took 0.020 secs (0.048 CPU) processed `color calibration' on GPU, blended on GPU [preview]
13.327326 [dev_pixelpipe] took 1.170 secs (1.231 CPU) processed `diffuse or sharpen' on GPU, blended on GPU [preview]
13.336618 [dev_pixelpipe] took 0.009 secs (0.008 CPU) processed `color balance rgb' on GPU, recycled cacheline, blended on GPU [preview]
13.345393 [dev_pixelpipe] took 0.009 secs (0.007 CPU) processed `filmic rgb' on GPU, recycled cacheline, blended on GPU [preview]
13.368053 [dev_pixelpipe] took 0.023 secs (0.030 CPU) processed `output color profile' on GPU, blended on  [preview]
13.418643 [dev_pixelpipe] took 0.050 secs (0.061 CPU) processed `dithering' on CPU, blended on  [preview]
13.435846 [dev_pixelpipe] took 0.017 secs (0.024 CPU) processed `display encoding' on GPU, blended on  [preview]
13.435919 [pixelpipe] preview internal pixel pipeline processing took 1.298 secs (1.408 CPU)
13.435949 [dev_process_preview] pipeline processing thread took 1.298 secs (1.408 CPU)
13.526316 [dev_pixelpipe] pipeline resync with history took 0.080 secs (0.376 CPU) for pipe full
13.526443 [dev_process_full] pipeline processing thread took 0.000 secs (0.000 CPU)
colorpicker stats reading took 0.185 secs (0.741 CPU)
image colorspace transform RGB-->RGB took 0.000 secs (0.000 CPU) [[histogram] sample swatch]
image colorspace transform RGB-->RGB took 0.000 secs (0.000 CPU) [[histogram] sample swatch]
image colorspace transform RGB-->RGB took 0.000 secs (0.000 CPU) [[histogram] sample swatch]
```
{{< /tab >}}
{{< tab "Darktable 5.4.1 GPU" >}}
```
16,1190 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
16,1364 dt_color_picker_helper stats reading 4 channels (filters 2492765332) cst 2 -> 2 size 1118880 denoised 0 took 0,001 secs (0,005 CPU)
16,1396 [dev_pixelpipe] took 0,020 secs (0,025 CPU) [preview] processed `channelmixerrgb' on GPU, blended on GPU
16,2071 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
16,6064 [dev_pixelpipe] took 0,467 secs (0,567 CPU) [preview] processed `diffuse' on GPU, blended on GPU
16,6106 [dev_pixelpipe] took 0,004 secs (0,004 CPU) [preview] processed `colorbalancergb' on GPU, blended on GPU
16,6160 [dev_pixelpipe] took 0,005 secs (0,001 CPU) [preview] processed `filmicrgb' on GPU, blended on GPU
16,6192 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,003 secs (0,002 GPU) [colorout]
16,6364 [dev_pixelpipe] took 0,020 secs (0,010 CPU) [preview] processed `colorout' on GPU, blended on GPU
16,6513 [dev_pixelpipe] took 0,015 secs (0,005 CPU) [preview] processed `dither' on CPU, blended on CPU
16,6572 [dev_pixelpipe] took 0,006 secs (0,023 CPU) [preview] processed `gamma' on CPU, blended on CPU
16,6584 dt_color_picker_helper stats reading 4 channels (filters 2492765332) cst 2 -> 2 size 1118880 denoised 0 took 0,001 secs (0,005 CPU)
16,6586 [dt_ioppr_transform_image_colorspace] IOP_CS_RGB-->IOP_CS_LAB took 0,000 secs (0,000 CPU) [gamma]
16,6587 [dt_ioppr_transform_image_colorspace_rgb] `Système' -> `sRVB' took 0,000 secs (0,000 CPU) [primary picker]
16,6754 [dt_ioppr_transform_image_colorspace_rgb] `Système' -> `sRVB' took 0,017 secs (0,063 CPU) [final histogram]
16,6843 [histogram] took 0,026 secs (0,093 CPU) final waveform
16,6847 [dev_process_image] pixel pipeline took 0,566 secs (0,733 CPU) processing `2022-06-17__DSC00078.arw'

16,6854 [dt_dev_process_image_job] loading image. took 0,000 secs (0,000 CPU)
20,3395 [dev_pixelpipe] took 3,606 secs (3,778 CPU) [full] processed `highlights' on GPU with tiling, blended on CPU
20,7350 [resample_cl] plan 0,001 secs (0,000 CPU) resample 0,010 secs (0,001 CPU)
20,8263 [dev_pixelpipe] took 0,487 secs (0,285 CPU) [full] processed `demosaic' on GPU, blended on GPU
20,8389 [dev_pixelpipe] took 0,012 secs (0,002 CPU) [full] processed `lens' on GPU, blended on GPU
20,8505 [dev_pixelpipe] took 0,012 secs (0,002 CPU) [full] processed `exposure' on GPU, blended on GPU
20,8812 [dev_pixelpipe] took 0,031 secs (0,008 CPU) [full] processed `colorin' on GPU, blended on GPU
20,8972 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0,016 secs (0,004 GPU) [channelmixerrgb]
20,9551 [dev_pixelpipe] took 0,074 secs (0,023 CPU) [full] processed `channelmixerrgb' on GPU, blended on GPU
21,8137 [dev_pixelpipe] took 0,000 secs (0,000 CPU) initing base buffer [preview]
21,8160 [dev_pixelpipe] took 0,001 secs (0,005 CPU) [preview] processed `rawprepare' on CPU, blended on CPU
21,8173 [dev_pixelpipe] took 0,001 secs (0,001 CPU) [preview] processed `temperature' on CPU, blended on CPU
21,9879 [dev_pixelpipe] took 0,171 secs (0,758 CPU) [preview] processed `highlights' on CPU, blended on CPU
22,0020 [dev_pixelpipe] took 0,014 secs (0,080 CPU) [preview] processed `demosaic' on CPU, blended on CPU
22,0103 [dev_pixelpipe] took 0,008 secs (0,016 CPU) [preview] processed `lens' on CPU, blended on CPU
22,0127 [dev_pixelpipe] took 0,002 secs (0,014 CPU) [preview] processed `exposure' on CPU, blended on CPU
22,0155 [dev_pixelpipe] took 0,003 secs (0,015 CPU) [preview] processed `colorin' on CPU, blended on CPU
22,0172 [dt_ioppr_transform_image_colorspace] IOP_CS_LAB-->IOP_CS_RGB took 0,002 secs (0,009 CPU) [channelmixerrgb]
22,0457 dt_color_picker_helper stats reading 4 channels (filters 2492765332) cst 2 -> 2 size 1118880 denoised 0 took 0,002 secs (0,013 CPU)
22,0458 [dev_pixelpipe] took 0,030 secs (0,180 CPU) [preview] processed `channelmixerrgb' on CPU, blended on CPU
22,6461 [dev_pixelpipe] took 1,691 secs (5,549 CPU) [full] processed `diffuse' on GPU, blended on GPU
22,6595 [dev_pixelpipe] took 0,013 secs (0,079 CPU) [full] processed `colorbalancergb' on GPU, blended on GPU
22,6725 [dev_pixelpipe] took 0,013 secs (0,073 CPU) [full] processed `filmicrgb' on GPU, blended on GPU
22,6824 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,007 secs (0,041 GPU) [colorout]
22,7443 [dev_pixelpipe] took 0,072 secs (0,398 CPU) [full] processed `colorout' on GPU, blended on GPU
22,8624 [dev_pixelpipe] took 0,118 secs (0,664 CPU) [full] processed `dither' on CPU, blended on CPU
22,8878 [dev_pixelpipe] took 0,025 secs (0,172 CPU) [full] processed `gamma' on CPU, blended on CPU
22,8894 [dev_process_image] pixel pipeline took 6,637 secs (11,596 CPU) processing `2022-06-17__DSC00078.arw'
23,4204 [dev_pixelpipe] took 1,374 secs (8,556 CPU) [preview] processed `diffuse' on CPU, blended on CPU
23,5271 [dev_pixelpipe] took 0,106 secs (0,617 CPU) [preview] processed `colorbalancergb' on CPU, blended on CPU
23,5788 [dev_pixelpipe] took 0,051 secs (0,291 CPU) [preview] processed `filmicrgb' on CPU, blended on CPU
23,5817 [dt_ioppr_transform_image_colorspace] IOP_CS_RGB-->IOP_CS_LAB took 0,003 secs (0,011 CPU) [colorout]
23,5870 [dev_pixelpipe] took 0,008 secs (0,037 CPU) [preview] processed `colorout' on CPU, blended on CPU
23,5913 [dev_pixelpipe] took 0,004 secs (0,004 CPU) [preview] processed `dither' on CPU, blended on CPU
23,5964 [dev_pixelpipe] took 0,005 secs (0,021 CPU) [preview] processed `gamma' on CPU, blended on CPU
23,5973 dt_color_picker_helper stats reading 4 channels (filters 2492765332) cst 2 -> 2 size 1118880 denoised 0 took 0,001 secs (0,006 CPU)
23,5976 [dt_ioppr_transform_image_colorspace] IOP_CS_RGB-->IOP_CS_LAB took 0,000 secs (0,000 CPU) [gamma]
23,5977 [dt_ioppr_transform_image_colorspace_rgb] `Système' -> `sRVB' took 0,000 secs (0,000 CPU) [primary picker]
23,6099 [dt_ioppr_transform_image_colorspace_rgb] `Système' -> `sRVB' took 0,012 secs (0,067 CPU) [final histogram]
23,6215 [histogram] took 0,024 secs (0,097 CPU) final waveform
23,6219 [dev_process_image] pixel pipeline took 6,889 secs (15,651 CPU) processing `2022-06-17__DSC00078.arw'
```
{{< /tab >}}
{{< /tabs >}}

Ansel does not need to start a new pipeline rendering to sample the color-picker because the module input and output are still in cache, so it takes a total of 40 ms to sample. Then the new parameters are committed to the module, which changes the history and triggers a recompute of the _color calibration_ module and all downstream modules. __Total time spent : 1.4 s.__

Darktable needs to restart a new pipeline rendering to sample the color-picker because why not ? That takes 0.6 s. You will be pleased to learn that Darktable samples color on GPU which takes only 1 ms (in the middle of those 0.6 s of pipeline runtime), while Ansel samples on CPU in 20 ms. Then the color-picker parameters are committed to history which restarts a full recomputation of the 2 pipelines. __Total time spent : 7.5 s.__

That's a __×5.4 penalty for Darktable__, all that because they are not improving their pipelines but going on every possible wild goose chase there is.


### Exporting at full resolution

We export after having opened the image in darkroom. This is important. The "high quality" sampling is used in Darktable, in Ansel it is the only mode.

{{< tabs >}}
{{< tab "Ansel Master GPU" >}}
```
18.917740 [dev_pixelpipe] took 0.000 secs (0.000 CPU) to load the image.
18.970877 [export] creating pixelpipe took 0.054 secs (0.074 CPU)
19.215438 [dev_pixelpipe] took 0.244 secs (0.222 CPU) processed `exposure' on GPU, blended on GPU [export]
19.241544 [dev_pixelpipe] took 0.026 secs (0.025 CPU) processed `input color profile' on GPU, blended on  [export]
19.267887 [dev_pixelpipe] took 0.026 secs (0.026 CPU) processed `color calibration' on GPU, blended on GPU [export]
56.171662 [dev_pixelpipe] took 36.904 secs (36.592 CPU) processed `diffuse or sharpen' on GPU with tiling, blended on CPU [export]
56.364588 [dev_pixelpipe] took 0.193 secs (0.156 CPU) processed `color balance rgb' on GPU, blended on GPU [export]
57.293819 [dev_pixelpipe] took 0.929 secs (0.468 CPU) processed `filmic rgb' on GPU with tiling, blended on CPU [export]
57.516252 [dev_pixelpipe] took 0.222 secs (0.176 CPU) processed `output color profile' on GPU, blended on  [export]
57.534628 [dev_pixelpipe] took 0.018 secs (0.017 CPU) processed `Final resampling' on GPU, blended on  [export]
58.462789 [dev_pixelpipe] took 0.928 secs (0.544 CPU) processed `dithering' on CPU, blended on  [export]
58.462829 [pixelpipe] export internal pixel pipeline processing took 39.492 secs (38.227 CPU)
58.462898 [dev_process_export] pixel pipeline processing thread took 39.492 secs (38.227 CPU)
[export_job] exported to `/home/aurelienpierre/Téléchargements/2022-06-17__DSC00078_08.jpg'
```
{{< /tab >}}
{{< tab "Darktable 5.4.1 GPU" >}}
```
526,9861 [export] creating pixelpipe took 0,063 secs (0,091 CPU)
526,9862 [dev_pixelpipe] took 0,000 secs (0,000 CPU) initing base buffer [export]
527,0072 [dev_pixelpipe] took 0,021 secs (0,020 CPU) [export] processed `rawprepare' on GPU, blended on GPU
527,0237 [dev_pixelpipe] took 0,016 secs (0,007 CPU) [export] processed `temperature' on GPU, blended on GPU
530,7560 [dev_pixelpipe] took 3,732 secs (3,868 CPU) [export] processed `highlights' on GPU with tiling, blended on CPU
531,1444 [dev_pixelpipe] took 0,388 secs (0,201 CPU) [export] processed `demosaic' on GPU, blended on GPU
531,9899 [dev_pixelpipe] took 0,845 secs (0,493 CPU) [export] processed `lens' on GPU with tiling, blended on CPU
532,1378 [dev_pixelpipe] took 0,148 secs (0,139 CPU) [export] processed `exposure' on GPU, blended on GPU
532,1968 [dev_pixelpipe] took 0,059 secs (0,047 CPU) [export] processed `colorin' on GPU, blended on GPU
532,2287 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0,031 secs (0,019 GPU) [channelmixerrgb]
532,3206 [dev_pixelpipe] took 0,124 secs (0,089 CPU) [export] processed `channelmixerrgb' on GPU, blended on GPU
575,5645 [dev_pixelpipe] took 43,244 secs (43,220 CPU) [export] processed `diffuse' on GPU with tiling, blended on CPU
575,7422 [dev_pixelpipe] took 0,178 secs (0,167 CPU) [export] processed `colorbalancergb' on GPU, blended on GPU
576,2975 [dev_pixelpipe] took 0,555 secs (0,474 CPU) [export] processed `filmicrgb' on GPU with tiling, blended on CPU
576,3897 [resample_cl] took 0,000 secs (0,000 CPU) 1:1 copy/crop of 7968x5320 pixels
576,4081 [dev_pixelpipe] took 0,111 secs (0,101 CPU) [export] processed `finalscale' on GPU, blended on GPU
576,4403 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,032 secs (0,020 GPU) [colorout]
576,5618 [dev_pixelpipe] took 0,154 secs (0,120 CPU) [export] processed `colorout' on GPU, blended on GPU
577,0202 [dev_pixelpipe] took 0,458 secs (0,456 CPU) [export] processed `dither' on CPU, blended on CPU
577,0204 [dev_process_export] pixel pipeline processing took 50,034 secs (49,403 CPU)
578,2407 [export_job] exported to `/home/aurelienpierre/Téléchargements/2022-06-17__DSC00078_06.jpg'
```
{{< /tab >}}
{{< /tabs >}}

In Ansel, since we opened the image previously in darkroom, the early stages of the pipeline are still in cache, so the export starts from the _exposure_ module and takes 39.5 s. In Darktable, well… business as usual : it's a full pipeline recompute that takes 50 s.

## Exporting again at half resolution

We export at half resolution straight after having exported at full resolution.

{{< tabs >}}
{{< tab "Ansel master GPU">}}
```
65.601240 [dev_pixelpipe] took 0.000 secs (0.000 CPU) to load the image.
65.658874 [export] creating pixelpipe took 0.060 secs (0.079 CPU)
65.912677 [dev_pixelpipe] took 0.254 secs (0.233 CPU) processed `output color profile' on GPU, blended on  [export]
65.972780 [dev_pixelpipe] took 0.060 secs (0.073 CPU) processed `Final resampling' on GPU, blended on  [export]
66.160165 [dev_pixelpipe] took 0.187 secs (0.238 CPU) processed `dithering' on CPU, blended on  [export]
66.160184 [pixelpipe] export internal pixel pipeline processing took 0.501 secs (0.544 CPU)
66.160202 [dev_process_export] pixel pipeline processing thread took 0.501 secs (0.545 CPU)
[export_job] exported to `/home/aurelienpierre/Téléchargements/2022-06-17__DSC00078_09.jpg'
```
{{< /tab >}}
{{< tab "Darktable 5.4.1 GPU">}}
```
607,7584 [export] creating pixelpipe took 0,064 secs (0,088 CPU)
607,7586 [dev_pixelpipe] took 0,000 secs (0,000 CPU) initing base buffer [export]
607,7918 [dev_pixelpipe] took 0,033 secs (0,028 CPU) [export] processed `rawprepare' on GPU, blended on GPU
607,8083 [dev_pixelpipe] took 0,016 secs (0,007 CPU) [export] processed `temperature' on GPU, blended on GPU
611,5555 [dev_pixelpipe] took 3,747 secs (3,865 CPU) [export] processed `highlights' on GPU with tiling, blended on CPU
611,9454 [dev_pixelpipe] took 0,390 secs (0,201 CPU) [export] processed `demosaic' on GPU, blended on GPU
612,7083 [dev_pixelpipe] took 0,763 secs (0,462 CPU) [export] processed `lens' on GPU with tiling, blended on CPU
612,8559 [dev_pixelpipe] took 0,148 secs (0,137 CPU) [export] processed `exposure' on GPU, blended on GPU
612,9149 [dev_pixelpipe] took 0,059 secs (0,045 CPU) [export] processed `colorin' on GPU, blended on GPU
612,9468 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_LAB-->IOP_CS_RGB took 0,032 secs (0,021 GPU) [channelmixerrgb]
613,0390 [dev_pixelpipe] took 0,124 secs (0,090 CPU) [export] processed `channelmixerrgb' on GPU, blended on GPU
656,1897 [dev_pixelpipe] took 43,151 secs (42,852 CPU) [export] processed `diffuse' on GPU with tiling, blended on CPU
656,3673 [dev_pixelpipe] took 0,178 secs (0,167 CPU) [export] processed `colorbalancergb' on GPU, blended on GPU
656,9163 [dev_pixelpipe] took 0,549 secs (0,467 CPU) [export] processed `filmicrgb' on GPU with tiling, blended on CPU
657,0078 [resample_cl] plan 0,001 secs (0,000 CPU) resample 0,001 secs (0,000 CPU)
657,1070 [dev_pixelpipe] took 0,191 secs (0,181 CPU) [export] processed `finalscale' on GPU, blended on GPU
657,1233 [dt_ioppr_transform_image_colorspace_cl] IOP_CS_RGB-->IOP_CS_LAB took 0,016 secs (0,004 GPU) [colorout]
657,1792 [dev_pixelpipe] took 0,072 secs (0,030 CPU) [export] processed `colorout' on GPU, blended on GPU
657,3186 [dev_pixelpipe] took 0,139 secs (0,136 CPU) [export] processed `dither' on CPU, blended on CPU
657,3187 [dev_process_export] pixel pipeline processing took 49,560 secs (48,669 CPU)
657,8107 [export_job] exported to `/home/aurelienpierre/Téléchargements/2022-06-17__DSC00078_07.jpg'
```
{{< /tab >}}
{{< /tabs >}}

Because Ansel always exports at full resolution until _Final resampling_ module (the mode called "high quality" in Darktable), the pipeline stays the same for the most part no matter the final size. Since Ansel's cache works, the previously-exported full-res image is still in cache, so we only recompute the last stages : _output color profile_, _downsampling_, _dithering_. That takes 0.5 s.

Darktable, as usual, doesn't manage what it previously computed, so you are on for another ~50 s full export. Aka a __×100 performance penalty__.

Note that Ansel pipeline cache has a garbage collection that empties all unused cachelines older than 5 minutes, so this wouldn't work if you wait for too long.

## Conclusion

It is hilarious to read, on tech-bros forums like [discuss.pixls.us](https://discuss.pixls.us), that people trust more Darktable than Ansel because the former has a lot more monkeys waving their hands in the air and pretending to work, which is apparently enough to give a completely wrong sense of reliability and trust. It's just the ultimate proof that people have no idea what's going on in the code. But it's a capitalistic disease to mistake agitation for work, and work that doesn't create value can still be sold as progress with the proper marketing.

I don't do opinions, I do benchmarks. The numbers I published here can be entirely reproduced :

{{< plotly title="Darktable performance penalty per benchmark case" src="plotly-darktable-penalty.json" class="full-width" >}}

- individual modules are on average __1.4 times faster on Ansel CPU__ compared to Darktable CPU,
- individual modules are on average __1.6 times faster on Ansel GPU__ compared to Darktable GPU,
- full pipelines are anywhere between __1.5 times and 100 times faster on Ansel__ compared to Darktable GPU, because __Ansel has a working cache__,
- Ansel is not just faster, it's also more energy-efficient, because fewer things are (re)computed.

All this was achieved by removing GUI code from pipeline rendering code, by making the data lifecycle clearer, simpler, and more robust, not by adding more special-case handling and workarounds. Ansel is able to use a lot more RAM than Darktable, while doing the bookkeeping necessary to know how much it actually uses and if it should start recycling memory.

All this is the proof that the Darktable "team" is not making the software better, that its (lack of) management is actively harming the software. There have been a lot of "optimizations" announced in releases notes since 2022. And yet their software is still a painfully slow beast of inefficiency.

Note that the module-wise optimizations done in Darktable __can__ be backported to Ansel, although not all of them seem like actual speed-ups from what is shown here. Some already have. But Darktable cannot backport Ansel's pipeline architecture, which is where most of our speed-ups come from.

During this work, I also lost faith in [Vkdt](https://jo.dreggn.org/vkdt/). Vkdt is a renewed attempt, by the founder of Darktable, Johannes Hannika, to reboot a RAW photo software from scratch and running entirely on GPU through Vulkan. But from 4 years of cleaning up other people's shit, there is a couple of things I have learned : 

1. The slow-downs of Darktable are not to blame on the GTK stack or the OpenCL stack. Granted, those are not great, but that can be managed. They are programmer errors and bad design that wasn't re-evaluated as the needs and requirements of the software changed.
2. A cache-first design allows to hide many latencies by __avoiding__ recomputations, and a full-GPU workflow will soon face the memory limitations of GPUs.
3. GPUs are becoming increasingly expensive, after the rush of crypto-currencies and now the rush of AI.
4. What killed Darktable was a lack of management and a lack of vision, a culture of _"laissez faire"_ and happy randomness, that only works with a handful of developers that all know each other. Vkdt is headed in the same direction. If code and features are allowed to grow without priorities and API tightening, it's a crashing train in slow motion. "newer = better" is only a transient state. The real challenge is to manage growth.

From a developer perspective, Darktable has 3 main diseases :

1. Its developers completely lack (and have for a long time) the abstraction skills to cognitively split what belongs to the GUI layer and what belongs to the backend layer. As such, you will find GUI/GTK states encrusted deep into backend layers, which is all the more annoying given that `darktable-cli` runs headless and forces both modes to be managed. But you also repeatedly find GUI problems solved in the backend layer, and the other way around. Those trigger real reliability issues, on top of making the whole thing complicated, illegible, and entangled.
2. Its developers don't understand the concept of private APIs and enclosure : all data structures are public, all header files are imported throughout the whole software, so the whole software knows about the whole software and is allowed to modify data structures in-place from anywhere in the code. 
3. Complexity is "solved" by more complexity, and nobody seems to see that as a problem called _technical debt_.

Sane codebases protect data by making them private and forcing interactions through getters/setters functions defined as public APIs. This allows easy management of thread safety when the time comes, but also allows us to track what is writing where and when from a central place. And then, you include the minimal set of APIs in each file, to isolate code as much as possible.

Instead of that, Darktable sourcecode writes anywhere from everywhere, without proper interfaces. So any kind of change into the Darktable codebase systematically triggers edge-effects and breaks something unexpected at the other end of the software, in depressing sessions of _whack-a-mole_ born from laziness and stupidity.

So, a lot of tedious work in Ansel has been done to tighten APIs, privatize as many data structures as possible, and isolate libraries from each other to de-entangle somewhat the dependency graph.

## Annex : the plans

### Whole architecture

Here is the high-level flowchart of the whole architecture, from database to backbuffers. Now we can finally draw a flowchart of this mess.

```mermaid
flowchart TD
  subgraph Module
    M["Module instance<br/>dt_iop_module_t"]
  end

  subgraph Develop
    H["dev->history<br/>history items"]
    D["dt_develop_t<br/>history_end<br/>history_hash"]
  end

  subgraph Persist["DB + image cache"]
    DB["SQLite history tables"]
    IC["image cache<br/>dt_image_t"]
  end

  subgraph Pipeline
    F["Pipe dirty flags"]
    P["Pipe nodes / pieces"]
    G["global_hash"]
    B["Backbuf"]
  end

  subgraph Cache
    C["Pixelpipe cache"]
  end

  subgraph Consumers["Backbuffer consumers"]
    DR["darkroom.c"]
    NAV["navigation.c"]
    EXP["imageio.c"]
  end

  M -->|"write snapshot<br/>dt_dev_add_history_item_real()"| H
  H -->|"update history_end / history_hash<br/>dt_dev_add_history_item_real()"| D

  D -->|"write history<br/>dt_dev_write_history_ext()"| DB
  D -->|"write image-side hash<br/>dt_dev_write_history_ext()"| IC

  DB -.->|"read history<br/>dt_dev_read_history_ext()"| H
  IC -.->|"read image metadata<br/>dt_dev_read_history_ext()"| H

  H -->|"apply to modules<br/>dt_dev_pop_history_items_ext()"| M

  D -->|"mark pipes dirty<br/>dt_dev_add_history_item_real()"| F
  D -->|"check history mismatch<br/>dt_dev_darkroom_pipeline()"| F

  H -->|"sync history into pipe<br/>dt_dev_pixelpipe_change()"| P
  F -->|"consume dirty flags<br/>dt_dev_pixelpipe_change()"| P

  P -->|"compute cache keys<br/>dt_pixelpipe_get_global_hash()"| G
  G -->|"key cache entries<br/>dt_dev_pixelpipe_process_rec()"| C

  C -.->|"read exact-hit / input<br/>dt_dev_pixelpipe_process_rec()"| P
  P -->|"publish stage output<br/>dt_dev_pixelpipe_process_rec()"| C

  C -->|"promote final output<br/>dt_dev_pixelpipe_process()"| B

  B -->|"borrow main/preview backbuf<br/>_darkroom_expose()"| DR
  B -->|"copy preview backbuf<br/>_lib_navigation_draw_callback()"| NAV
  B -->|"borrow export backbuf<br/>dt_imageio_export_with_flags()"| EXP

  EXP -->|"update mipmap hash<br/>dt_imageio_export_with_flags()"| IC

```

### RAM/vRAM memory management

Cache buffers management :

```mermaid
flowchart TD
  H["CPU RAM buffer"]
  P["Pinned vRAM buffer<br/>CL_MEM_USE_HOST_PTR"]
  V["Device-only vRAM buffer"]
  G["pixelpipe_process_on_GPU()"]
  C["pixelpipe_process_on_CPU()"]

  H -->|"prepare GPU input<br/>dt_dev_pixelpipe_cache_prepare_cl_input()"| P
  P -->|"sync host/device if needed<br/>dt_dev_pixelpipe_cache_sync_cl_buffer()"| G

  V -->|"reuse existing vRAM input<br/>dt_dev_pixelpipe_cache_prepare_cl_input()"| G

  G -->|"publish pinned output<br/>dt_dev_pixelpipe_cache_release_cl_buffer()"| P
  G -->|"publish device-only output<br/>dt_dev_pixelpipe_cache_release_cl_buffer()"| V

  V -->|"GPU to CPU fallback copy<br/>dt_dev_pixelpipe_cache_restore_cl_buffer()"| H
  H -->|"resume CPU processing<br/>pixelpipe_process_on_CPU()"| C

```

If the module output is planned for caching (because of user preference, need for this buffer for histogram/color-picker, or the next module is the one active in GUI), OpenCL uses a pinned memory buffer (pinned on the RAM cache). We also pin input memory buffers, when the GPU takes input from the RAM cache. This is slightly more expensive than using a pure device-only buffer, but less expensive than copying that device-only buffer to the host. If the OpenCL output is not planned for caching, we use a device-only buffer that may be recycled later but will never be copied to RAM.

In case of an OpenCL error for a particular module (most obvious one : out of memory), the GPU input buffer is transparently copied back on the RAM cache before falling back to the pure-CPU path.

All those buffers are tracked by cache entries, which binds a RAM buffer with an vRAM one (if we use both in a pinned situation), or simply keep track of what is allocated. If a GPU memory allocation fails, we first browse the cache to release all previously-allocated buffers. If the GPU memory allocation fails again after that, we fallback to CPU.

### Cache entries

The cache is a hashtable of cache entries. Their key is a hash/checksum. We have 2 kinds of cache entries :

- internal ones, used as reusable buffers for module inputs/outputs,
- external ones, used as disposable buffers for modules intermediate computations.

Internal cache entries are keyed by the `global_hash` of their parent module, and can be found again later (even from the GUI) because that hash is stable across runs and pipelines. They can be rekeyed if we reuse their memory buffer, so the new key matches the new state of their parent module. Those cache entries can be locked in read or write mode for safe concurrent access between parallel threads. A write lock can be captured by only one thread if no other thread is reading, a read lock can be captured by several threads if no other thread is writing. Since the write lock is released by the module writing its output earlier than the read lock is acquired by the module using the same cache entry as its input, and there is a risk of deletion in-between, they also have a `refcount` mechanism : the producer of the cache entry increments it, the consumer decrements it. The same mechanism is used by the darkroom to fetch the backbuffer with no copy. 

External cache entries are keyed by the memory address of their data buffer. They are supposed to be short-lived.

Adding/removing cache entries automatically increment/decrement the global cache size. We keep adding new entries for as long as the global cache size is below the allowed threshold, after which we start destroying the oldest entries if their `refcount` is 0 and they are not locked.

```mermaid
flowchart TD
  A["absent"]
  B["reserved for write<br/>refcount + 1<br/>write-locked"]
  C["published<br/>exact-hit eligible"]
  D["borrowed by consumer<br/>refcount > 0"]
  E["backbuf-held<br/>one display ref kept"]
  F["auto-destroy flagged"]
  G["removed / freed"]

  A -->|"create new entry<br/>dt_dev_pixelpipe_cache_get()<br/>dt_dev_pixelpipe_cache_get_writable()"| B
  C -->|"rekey reusable entry<br/>dt_dev_pixelpipe_cache_get_writable()<br/>dt_dev_pixelpipe_cache_rekey()"| B

  B -->|"backend finishes and unlocks<br/>dt_dev_pixelpipe_process_rec()"| C

  C -->|"exact-hit / reopen / GUI read-only<br/>dt_dev_pixelpipe_cache_peek()<br/>dt_dev_pixelpipe_cache_get_read_only()"| D
  D -->|"release consumer ref<br/>dt_dev_pixelpipe_cache_unref_hash()<br/>dt_dev_pixelpipe_cache_close_read_only()"| C

  C -->|"keep final output alive for display<br/>_update_backbuf_cache_reference()"| E
  E -->|"backbuf changes or invalidates<br/>_update_backbuf_cache_reference()"| C

  C -->|"mark transient output<br/>dt_dev_pixelpipe_cache_flag_auto_destroy()"| F
  F -->|"reap when no ref and no lock remain<br/>dt_dev_pixelpipe_cache_auto_destroy_apply()"| G

  C -->|"explicit remove / GC / LRU<br/>dt_dev_pixelpipe_cache_remove()<br/>dt_dev_pixelpipe_cache_flush_old()<br/>dt_dev_pixel_pipe_cache_remove_lru()"| G
  C -->|"invalid payload detected<br/>dt_dev_pixelpipe_cache_peek()"| G

```

__A cache entry doesn't know the kind of data it holds__. It knows only the addresses of the buffers, their size, their refcount and their lock state. So it is a generic abstraction of stuff contained in memory.
---

# Photographic grain : https://ansel.photos/en/doc/views/darkroom/modules/photographic-grain/

Simulate photographic grain from a stochastic stack of silver-halide crystal layers.

This module implements a production version of the model presented in [Stochastic photographic grain synthesis from crystallographic structure simulation](https://eng.aurelienpierre.com/2023/07/stochastic-photographic-grain-synthesis-from-crystallographic-structure-simulation/). The goal is not to add generic noise, but to discretize the scene-referred image into finite crystal footprints that capture and deplete light through an emulsion stack.

Unlike simple additive noise, the module works with crystal-shaped grains, a per-layer filling ratio, and a remaining-light model. Each crystal captures a flat tone averaged over its own footprint, so image detail finer than the grain size is not preserved inside the grain.

This way of simulating photographic grain is inspired by physics but is not a 1:1 simulation. The sensitometry aspect was deliberately left since that part is covered by other modules already, and chemical effects of developers over the grain size has not been simulated.

{{< warning >}}
As with any pixel-level modification, the previews are accurate only for a 100% zoom (1:1), and provided in a best-effort manner for any other zoom level.
{{< /warning >}}

## Scene-referred placement

_Photographic grain_ is a scene-referred module and is meant to run before _filmic_ in the pixelpipe.

That matters because the module simulates light being captured by crystals in the emulsion. It therefore belongs before display rendering and before output-referred tone mapping. Grain statistics are built from the scene-referred signal, then later modules shape the final display contrast as usual.

## Reference model

The emulsion is modeled as a stack of elementary crystal layers. For each layer:

- Crystal seeds are placed stochastically from a target filling ratio,
- Each accepted seed randomly picks one crystal footprint from a small precomputed bank,
- The current light field is averaged over that grain footprint,
- The captured amount is limited by the layer sensitivity,
- That flat crystal tone is written to the output over the whole grain surface,
- The same energy is subtracted from the remaining light field before deeper layers are processed.

The implementation keeps the physically important part of the article:

- Light is split in depth through layers,
- Grain is built from finite crystal footprints,
- Capture is limited by the remaining available light,
- The final stack is normalized for average exposure.

## Crystal model

Crystal footprints are regular polygons rasterized on the pixel grid with sub-pixel boundary coverage:

- Average size is controlled by the _crystal average size_ slider,
- Crystal size follows a log-normal distribution,
- Polygon vertex count follows a gaussian distribution centered around six,
- Orientation is randomized,
- Grains with more than five sides are approximated as circles for speed,
- Non-integer radii keep a partial-occlusion boundary instead of collapsing to a hard binary edge.

Each layer precomputes a bank of random crystal footprints, then accepted seeds randomly pick one bank entry. This avoids recomputing crystal geometry inside the hot pixel loop while keeping enough variation inside each layer.

## Modes

### B&W

The image is reduced to one luminance-like scalar light field, one grain stack is synthesized from it, and the resulting luminance modulation is reapplied to RGB while preserving the original color ratios.

This is the closest mode to the scalar reference model and the most neutral starting point.

### Color

Color mode models film as sequential blue-, green-, then red-sensitive sub-stacks. The remaining-light field is updated in depth order, so deeper color-sensitive layers only see the light not already captured above them.

This is not a full chemical simulation of color negative film, but it is more physical than three independent parallel RGB noise syntheses. The geometry can also be partly shared across channels to control how correlated the grain structure is between color-sensitive layers.

## Controls

mode
: Choose between one monochrome grain stack and one sequential color grain stack.

filling
: Surface ratio occupied by crystals in each layer. Ilford B&W film emulsions from the 1960's have been documented to have around 15% of the layer surface covered by silver crystals. A 25% filling ratio visually match better modern film stock.

: This is a per-layer crystal coverage target, not the final visible grain coverage after the full stack has been accumulated.

: Pushing this value to 100% essentially removes all grain.

crystal average size
: Average crystal footprint in pixels at 100% zoom.

: Below 100% zoom, the footprint follows the effective raster scale of the current processing grid. Above 100% zoom it is clamped, so zooming in does not invent larger grains.

: The silver halide crystal is the actual unit sensor of film emulsions, which means details finer than the grain size cannot be recorded. This parameter therefore affects both the coarseness of the grain and the sharpness of the image. 

layers
: Number of crystal layers stacked through the emulsion. In color mode, this is understood as the number of layers per channel.

: More layers generally average the result and reduce the visibility of individual grains. Fewer layers make the grain more abrupt and coarser.

: Real film emulsions have theoritically :
: - around 14 crystal layers for B&W film using non-tabular grains, 
: - 14 crystal layers per color layer for color film using tabular grains,
: - up to 83 layers for B&W film using tabular grains (Kodak T-Max, Iford Delta).

crystal size variability
: Log-normal standard deviation of crystal sizes.

: At `0`, grain size stays very regular. Increasing this produces a broader size distribution and a less synthetic texture.

layer sensitivity
: Per-layer capture bias in EV.

: `0 EV` is the neutral setting. It means one layer captures its nominal `1 / layers` share once normalized by the average rasterized grain surface. Positive values make each layer capture more energy. Negative values make each layer capture less.

inter-channel grain correlation
: In color mode only.

: Probability that blue-, green- and red-sensitive sub-layers reuse the same crystal births and shapes at matching depths. Lower values give more independent chromatic grain. Higher values produce more shared, film-like grain structure.

grain colorfulness
: In color mode only.

: Scales only the chromatic amplitude of the RGB grain residual while keeping its achromatic grain strength unchanged. Lower values mute fluorescent color speckle without desaturating the underlying image.

## Presets

The module ships with two built-in presets:

- _color grain_: filling `85%`, crystal average size `4 px`, `30` layers, layer sensitivity `0 EV`, inter-channel grain correlation `67%`, grain colorfulness `67%`, crystal size variability `0.25`.
- _B&W grain_: filling `25%`, crystal average size `4 px`, `30` layers, layer sensitivity `0 EV`, crystal size variability `0.25`.

These are meant as practical starting points rather than measured film stocks.

## Practical workflow

Start in this order:

1. Choose _mode_.
2. Set _crystal average size_ to establish the grain scale.
3. Tune _filling_ to control how much of each layer is occupied by crystals.
4. Adjust _layers_ and _layer sensitivity_ to control how much energy the stack captures.
5. Add _crystal size variability_ if the structure feels too regular.
6. In color mode, reduce _grain colorfulness_ or increase _inter-channel grain correlation_ if the grain feels too fluorescent.

In practice:

- If the grain is too strong, reduce _filling_ or _layer sensitivity_, or increase _layers_,
- If the grain is too fine, increase _crystal average size_,
- If the pattern is too regular, increase _crystal size variability_,
- If color grain looks synthetic, lower _grain colorfulness_ first, then increase _inter-channel grain correlation_ if needed.

## Preview, zoom and thumbnails

The user-facing grain size is defined relative to the 100% image view, but the implementation compensates for the actual processing grid:

- Zooming in above 100% does not enlarge grains beyond their 100% reference size,
- Zooming out adapts the grain to the current raster scale,
- Reduced mipmap inputs, such as the thumbnail pipeline, are compensated so grain size stays closer to the darkroom reference instead of becoming artificially coarse.

Critical tuning should still be judged near the final viewing scale.

## Masking and blending

This module supports Ansel masks and blending operators.

That lets you:

- Apply different grain sizes or fillings to different regions,
- Keep faces and skin cleaner than backgrounds,
- Stack several instances for more complex emulsions,
- Reduce the visible contribution of one instance with opacity while preserving its internal grain statistics.

## Performance

This module is significantly more expensive than synthetic additive noise because it simulates many layers of crystal capture.

Processing cost mainly increases with:

- Larger crystal average size,
- More layers,
- Larger output images.

The module supports OpenCL. CPU and OpenCL both use the same light-depletion model, but performance still depends strongly on image size and grain settings.

## Notes

- The result is intentionally stochastic. Two parameter sets with similar average strength may still produce different local clustering.
- The module simulates grain structure, not full film sensitometry or print chemistry.
- Color mode is not a complete color-film emulsion simulator, but a sequential color-sensitive grain model built on the same remaining-light framework.
---

# Drawing : https://ansel.photos/en/doc/views/darkroom/modules/drawing/

Paint premultiplied RGBA layers directly in darkroom and save them as pages inside a TIFF sidecar.

This module is a true painting layer system. It is not a local-adjustment mask editor and it is not a clone/heal tool like [retouch](./retouch.md). Instead, it creates and edits independent raster layers that are composited over the current image in the pixelpipe.

The intended use cases are:

- Hand painting,
- Dodging and burning with a brush,
- Color glazing,
- Painterly overlays,
- Matte painting on top of a photograph,
- Texture painting,
- Soft masking or erasing with a pressure-sensitive tablet.

The TIFF sidecar is saved as 16 bits floating point, compressed losslessly, in scene-linear working RGB space (the colorspace ICC profile is included in the file), with premultiplied alpha. It can be modified in any software that supports this. 

Each instance of the module is saved as a different layer in this file. This sidecar can also be used to import any arbitrary image and compose it over the photograph, under the display view transform. Typical masking features are available too.

## What the module does

The module stores one or more drawing layers in a TIFF sidecar linked to the current image. Each layer is a premultiplied RGBA image in the module working space. The layer is loaded into memory, edited live in darkroom, composited over the image preview, and written back to the sidecar when needed.

The module works in three distinct spaces at the same time:

- A full-resolution authoritative layer cache, used for persistence,
- A view-dependent cropped and scaled process tile, used for realtime display while painting,
- The darkroom display buffer, where the layer is blended over the current image.

In practice, this means the brush can stay responsive while painting, even on large images, because the module paints live only into the visible process tile. The finished stroke is then replayed asynchronously at full resolution into the authoritative cache.

## General workflow

A typical workflow is:

1. Create a new drawing layer.
2. Choose a paint mode, fall-off, color and brush parameters.
3. Paint in darkroom.
4. Adjust or switch layers.
5. Save the sidecar explicitly if needed, or let the module flush it when leaving darkroom or closing the application.

The module can manage several layers inside the same sidecar TIFF. Each module instance is linked to one layer name and one layer order. You can therefore stack several drawing module instances if you want several independent painted layers in the history.

## How realtime painting works

The painting engine is split in two stages.

### 1. GUI stroke sampling worker

The first worker receives raw input events from the GUI:

- Button press starts a stroke,
- Pointer motion adds raw samples,
- Button release ends the stroke.

Those raw events include:

- Position,
- Pressure,
- Tilt,
- Acceleration,
- Current brush settings,
- Stroke id and event id.

This worker converts raw input into evenly spaced dabs. It is responsible for:

- Distance-based resampling,
- Smoothing,
- Interpolation of dab properties,
- Pressure/tilt/acceleration mapping,
- Stroke-local opacity and flow behavior,
- Smudge carry state.

The emitted dabs are immutable records. Once a dab has been emitted into the stroke history, later stages are expected to consume it as-is.

### 2. full-resolution replay worker

While you paint, the module updates only the current process tile for display. When the stroke is finished, a second background worker replays the immutable dab history into the full-resolution layer cache.

This design avoids paying the cost of full-resolution rasterization at every live dab. It is the main reason the module can remain interactive on large images.

## Compositing model

The drawing layer is stored as premultiplied RGBA and composited over the incoming image buffer.

That matters for three reasons:

1. Color and opacity are stored together in a way that avoids fringe artifacts at soft edges.
2. Erasing reduces alpha and therefore removes previously-painted contribution cleanly.
3. Layer blending remains stable through the pixelpipe and through OpenCL processing.

## Brush tab

The _Brush_ tab contains the painting controls.

### Paint mode

paint
: Paint the selected color over the current layer.

erase
: Reduce or remove previously-painted layer content by attenuating alpha.

blur
: Sample a local neighborhood and blend a blurred result back into the layer.

smudge
: Pick up already-painted pixels and drag them along the stroke.

### Color

color
: Sets the brush color in display RGB.

pick from
: Choose whether the color picker samples from the module input or output.

HDR exposure
: Raises the brush color intensity above display white. This is useful if the layer is meant to live in a scene-referred HDR pipeline rather than as a simple display-referred overlay.

### Geometry

fall-off
: Chooses the brush alpha profile. The module currently provides four profiles:
: - linear,
: - gaussian,
: - quadratic,
: - sigmoidal.

: The preview widget shows each profile rasterized with the current hardness and sprinkle settings.

size
: Brush radius in pixels.

sampling distance
: Controls dab spacing along the stroke.
: - low values produce very dense stamping,
: - high values space dabs farther apart,
: - very high values are faster but can reveal the individual stamp structure.

: Internally this goes from approximately one-pixel spacing at 0% to roughly one brush diameter at 100%.

smoothing
: Applies stroke smoothing before the dab is emitted. This reduces jitter from the raw pointer stream, at the cost of slightly more predictive motion.

hardness
: Controls the inner solidity of the brush.
: - high values produce a hard center and steeper edge,
: - low values produce a soft brush with a larger transition zone.

### Thickness

opacity
: The target opacity of the stroke.

flow
: Controls how overlapping dabs build up within the same stroke.
: - at 100%, the stroke behaves like watercolor or an airbrush with overlap normalization: overlapping dabs in the same stroke are capped so they do not keep building toward full opacity under normal sampling. Self-intersecting brush strokes will have a constant opacity.
: - at 0%, the stroke behaves more like a marker or highlighter: overlaps accumulate. Self-intersecting brush strokes will build up more opacity at the intersection.
: - anything else is a weighted mix of both strategies.

: This is one of the most important controls to understand. _Opacity_ defines the intended strength of the stroke. _Flow_ defines how that strength is distributed across overlapping dabs.

### Texture

sprinkles
: Adds multiplicative grain/noise to the brush alpha.

sprinkle size
: Controls the spatial size of the grain in layer-space pixels.
: - low values give fine grain,
: - high values give larger speckles or blotches.

coarseness
: Controls the octave mix of the sprinkle noise.
: - 0% favors the coarsest octave,
: - 50% balances the octaves evenly,
: - 100% favors the finest octave.

## Layer tab

The _Layer_ tab manages sidecar layers and preview background.

### Background

image
: Shows the normal image under the drawing layer.

white, grey, black
: Replaces the image background in the preview with a flat color. This is useful to inspect painted alpha and edge quality independently of the photograph.

### Layer management

layer name
: Name of the current drawing layer in the sidecar TIFF.

source layer
: Select an existing layer from the sidecar.

create new layer
: Creates a new blank layer with the current name.

delete layer
: Deletes the linked layer from the sidecar TIFF.

create background from input
: Creates a background layer initialized from the module input. This is useful if you want to paint destructively on a rasterized snapshot rather than on transparency, or if you want to open the sidecar TIFF in another drawing application while still having the reference image in background. Both layers are positionned as they appear.

### Fill

white
: Fills the current layer with opaque white.

black
: Fills the current layer with opaque black.

transparency
: Clears the current layer to full transparency.

## History buttons

At the top of the module, outside the notebook tabs, there are three important buttons:

undo
: Restores the previous full-layer snapshot.

redo
: Reapplies the last undone snapshot.

save sidecar
: Forces the current in-memory layer state to be written immediately to the TIFF sidecar.

This is explicit persistence, not just a history item. It is useful if you want to secure long painting sessions before leaving darkroom.

## Input tab

The _Input_ tab maps tablet or pointer data to brush parameters.

It supports three input sources:

- Pressure,
- Tilt,
- Acceleration.

Each source can modulate four targets:

- Size,
- Opacity,
- Flow,
- Hardness.

For each source, you can also choose a mapping profile:

- Linear,
- Quadratic,
- Square root,
- Inverse linear,
- Inverse square root,
- Inverse quadratic.

These mappings are evaluated per raw input event, before the event is turned into a resolved dab. The cursor preview uses the mapped values too, so the visible cursor should reflect what will actually be emitted into the stroke.

## Stroke sampling details

The stroke engine does not simply stamp one dab per mouse event. Instead, it tries to build a stable sampled path.

The raw pointer path is converted into evenly spaced dabs according to the selected sampling distance. If GUI events arrive too sparsely because of system load or fast mouse motion, the missing dabs are interpolated along the path. If smoothing is enabled, the final dab position and properties are blended with a prediction from the previously-emitted dabs.

The important consequence is that the visible stroke should depend much more on the chosen brush spacing than on the operating system event rate.

## Why this module can be slow

This module is more expensive than an ordinary color adjustment module, because it maintains and updates raster images interactively.

The main cost sources are:

### Large brush footprints

Brush cost grows with the damaged area. A very large soft brush can touch hundreds of thousands of pixels per dab.

### Blur and smudge

These modes are slower than ordinary paint and erase because they need extra neighborhood sampling and, for smudge, a carried runtime state.

### Sprinkles

Sprinkles add procedural grain evaluation to the alpha path. Large sprinkle size and high-strength grain can become expensive, especially with large brushes.

### Full-resolution replay

Live painting updates only the view-dependent process tile. The finished stroke is then replayed asynchronously at full resolution into the authoritative cache. This moves cost away from the live stroke, but it does not remove it.

On very long or very large strokes, the replay worker can remain busy for a while after the stroke ends.

### Saving and leaving darkroom

The sidecar TIFF must reflect the authoritative full-resolution layer. If there are still deferred replay jobs pending when you:

- Save the sidecar,
- Leave darkroom,
- Close the application,

then Ansel must wait for those jobs to finish before the sidecar can be written safely.

The module therefore shows a blocking modal wait dialog when leaving or closing while a layer still needs to be finalized and saved.

## Performance architecture and limitations

The module uses several caches and workers to remain interactive. The first one being that layers are saved at the resolution of the RAW input, so this is a considerably larger painting matte than in most drawing applications.

### Process tile

While painting, the module rasterizes into a process tile representing the current visible area plus a safety margin. This is what makes realtime interaction possible.

The consequence is that the immediate preview is view-dependent. If you pan or zoom, the tile must be rebuilt from the authoritative cache.

### Authoritative full-resolution cache

The real saved layer lives in a full-resolution in-memory cache. The sidecar TIFF is only the persistence format.

This is necessary because rebuilding a full-resolution layer from a previously-scaled preview tile would destroy fine texture and blur procedural details such as sprinkles.

### Deferred replay worker

Finished strokes are replayed into the full-resolution cache in a second worker thread. This reduces live latency, but it introduces a second source of work that must eventually complete.

### OpenCL

The module supports OpenCL in the compositing path. That accelerates the darkroom display path, but the brush rasterizer itself remains CPU-driven.

Practical consequences:

- The pixelpipe can blend the layer on GPU,
- Stroke generation and dab rasterization are still CPU tasks,
- If OpenCL is disabled for the module, the compositing path will run on CPU,
- If OpenCL is enabled but later modules need the GPU at the same time, total responsiveness can still depend on system load and GPU contention.

### First-use costs

The first stroke in a session can still be slightly slower than later ones because some caches, threads and buffers are warmed lazily. Several first-use penalties have been reduced, but a completely cost-free first stroke is unrealistic in a raster painting system that initializes preview and replay resources on demand.

## Practical advice

For best performance:

- Keep brush size reasonable, paint with short strokes,
- Avoid excessive smoothing unless needed,
- Use lower sprinkle values for large brushes,
- Reserve blur and smudge for smaller localized work,
- Let the background replay worker catch up before saving or leaving darkroom if you painted a very long stroke.

For best visual consistency:

- Think of opacity as stroke strength,
- Think of flow as overlap behavior,
- Use the preview background colors to inspect edges and alpha,
- Save the sidecar after important milestones if you are building complex painted layers.

## Caveats

This module is not designed for geometric retouching, source-target healing, or patch-based object removal. Use [retouch](./retouch.md) for that.

This module also does not replace masking-and-blending logic. It paints real pixels into a persistent layer. If you need parametric masks or drawn masks that remain fully procedural, use Ansel masking instead.

Because the layer is stored in a TIFF sidecar and replayed at full resolution, complex sessions can generate a non-trivial amount of CPU work and memory traffic compared with classic adjustment modules.

## Summary

The _drawing_ module is a raster painting layer system for darkroom. It converts stylus or mouse input into sampled dabs, paints them live into a process tile for immediate display, then replays finished strokes asynchronously into a full-resolution authoritative layer cache that is saved in a TIFF sidecar.

Its strengths are:

- Direct painting,
- Tablet-aware input mapping,
- Persistent paint layers,
- Scene-referred HDR-aware color painting,
- Responsive live preview.

Its main limits are:

- Large-brush cost,
- Slower blur and smudge modes,
- Deferred full-resolution replay after strokes,
- Blocking waits when pending work must be saved before leaving darkroom.
---

# History merge made robust and predictable : https://ansel.photos/en/news/history-merge-topology/

_Memoirs of a guy who spent too much time cleaning other people's shit and paying for their bad decisions, episode #too many._

Copy/paste history and styles are core features in Ansel, and the one that makes it deserve (or not) its "workflow app" title. But it is also among the hardest to get right internally. Users see a list of edits, but under the hood those edits depend on the pipeline order, module instances, and masks. If two images have different pipeline topologies, naïvely copying edits can produce inconsistencies.

This update makes history merging **robust, consistent, and transparent**, after tedious work of code cleaning and simplification. It also introduces clear error handling when a perfect merge is mathematically impossible.

## A brief history of bad design

Until early 2019, Darktable was designed around a fixed pipeline: modules had an ordering that was decided at compilation time by a [Python script](https://github.com/darktable-org/darktable/blob/release-2.7.0/tools/iop_dependencies.py), created by Darktable's founder, Johannes Hanika in [2011](https://github.com/darktable-org/darktable/commit/ce67cf893e05da4a456a114c389ddc0d1a45ec69). The one thing you can expect from Jo is to get his math right, and so this script does exactly what should be done in such circumstances:

- let programmers declare modules,
- let them define which modules should go before each module, in a lazy and convenient way that allows to just say "A should be before C", "B should be before A", "D should be before A", etc.
- turn that into a [directed graph](https://en.wikipedia.org/wiki/Directed_graph), which is just the math object glueing all those constraints together,
- solve the directed graph with one of the [topological sorting](https://en.wikipedia.org/wiki/Topological_sorting) algorithms, which date back from the 1960's,
- write the index number of each module in the pipeline, and done.

Unfortunately, that fixed pipeline meant that changing the relative ordering of modules anytime later would break older histories. Which sucked because some design mistakes were made with the position of the display transform and the base curve module, which happened early in the pipeline and made colors go bonkers in HDR situations. To fix that, I had to create the filmic module as another module, in order to be able to put it at the end of the pipeline. And all other display-referred modules would have needed to be duplicated in the code base to be able to insert them where needed without breaking older edits (including mine). Needless to say, the fixed pipeline had had its run, and I was a proponent of this change.

With a fixed pipeline, merging histories together, that is copying/pasting histories between images or applying styles to images (which ends up being the same thing), was easy: it was just a matter of replacing modules parameters and masks 1:1. Even with multi-instances modules, which had been introduced circa 2014, it wasn't that bad because all instances were forced to be consecutive, and numbered relatively to each other. So, after creating the missing instances, at a position that was predictable and invariant (in sequential order after the base instance), it was still replacing modules parameters and masks 1:1.

But the much-needed pipeline re-ordering feature, at runtime and by users, that arrived with Darktable 3.0 was done in the worst possible way. The developer who implemented it handled indexing with arrays of floating point priorities, which is not the right data structure for the problem. Plus, the history management code wasn't refactored and simplified prior to extension, but hacked in place with minimal changes, which made it really complex and obfuscated. Pascal Obry, who had accepted the change in 2019, had to [rewrite the whole pipeline reordering backend](https://github.com/darktable-org/darktable/commit/841b41c37175990e58b433b4ca9acde5aa116a1d) in 2020, using the proper data structure for the task at hand ([linked lists](https://en.wikipedia.org/wiki/Linked_list)), because the previous one was brittle and unmaintainable.

But merging pipelines, which may have different numbers of modules, ordered at unpredictable and non-invariant places, cannot be done any other way than using topological sorting at runtime, because it's not a mere problem of histories (aka module parameters snapshots) anymore, it's a twofold problem that includes histories but also pipeline topology. In other words : we need to solve where to insert module instances that exist in the source pipeline but not in the destination. And yet, the Python script doing that at compilation time was deleted in 2019, and the feature was never ported to C.

So, the way Darktable, to this day, handles pipeline merging is through heuristics hacked from the fixed-pipeline paradigm and broken in general, except in the nice cases that match the conditions of 2018's pipelines and earlier:

- if your source and destination pipelines have the same number of modules ordered the same way, everything goes well.
- if your source pipeline has additional module instances, compared to the destination, but those instances are all sittings immediately after the base instance, everything still goes well,
- but… if you have additional instances, whether in the source or in the destination pipeline, and they have been moved away in the pipeline, then the behaviour is unspecified, unpredictable, and I have been copy-pasting history holding my breath for more than 5 years,
- also, there is a list of "fence" modules that need a specific relative order (like _color calibration_ absolutely needs to go after _input color profile_), but no way to enforce them, no way to fix them when bad orderings happen, only silent error messages in console.

Combine that with raster masks, where the module reusing a raster mask absolutely needs to sit later in the pipe than the module producing it, and where, by the way, you may want to copy the producer along with the consumer, or at least get a warning if you don't… and you get the recipe for madness.

In practice, you will need to open each image and check the module stack in darkroom, which is painstakingly slow.

Because Darktable is defined by bad priority management, a lot of cosmetics have received a lot of work : it will let you edit your pictures with PlayStation gamepads, it's about to get deep-learning masking features, but it still manages to fuck up the core basics, with no improvement in 6 years despite the apparent activity in the project.

## The issue (TL;DR)

Previously, history paste tried to merge modules one by one as it went. That worked in simple cases but became unreliable when:

- the source image had a different pipeline order,
- there were multiple instances of the same module,
- or masks and blending were involved.

Because the merge was not solving the *whole* ordering problem, the result could depend on a lot of things. In some cases, the final pipeline could end up inconsistent with the history stack.

## The fix

I implemented a [topological sorting algorithm](https://github.com/aurelienpierreeng/ansel/blob/2a84c20769742d370aac8623238d65bccaa97655/src/common/topological_sort.c) in C. Or more accurately, ChatGPT did and I checked it (more on that below). 304 lines of code, which, for C, is very few.

We now treat the pipeline order as a **set of constraints** that must be solved globally, not module‑by‑module. In practice, this means:

- We solve the pipeline topology first, and the development history (module parameters) last. Two clear steps that makes error handling possible.
- The merge computes a single, valid pipeline order that satisfies both the source and destination whenever possible, no matter how many module instances and how they are ordered.
- If constraints are incompatible, Ansel can explain the conflict and ask which ordering to preserve.
- If a conflict is unsolvable, Ansel reports it clearly instead of producing a broken or unstable pipeline and give you options to fix it.
- Constraints are put on modules using and consuming raster masks too, and a warning is issued if a raster mask user is copied without the producer.
- Hard-coded constraints are put on modules that require each other before, for technical reasons (highlights reconstruction before demosaicing, input color profile before color calibration, etc.). Those constraints are handled the same as others and solved together (no special case).   

Regarding the second part, the history properly speaking, one thing needs to be clarified here. Darktable and Ansel history is much like an undo/redo list of module's parameters snapshots : each history item is linked to a module, and represents its iternal state of parameters and masks. When loading the history into the pipeline nodes (_nodes being the pixel filters attached to the "modules" you see in GUI_), we read history bottom to top and copy each item/snapshot into modules, meaning that later snapshots always override earlier.

That is to say, history is ordered by time of user modification (again… think undo/redo list of snapshots), not by pipeline nodes order. But, where it gets confusing is, history items also store the pipeline position of a module, which means module reordering leaves history items. That confused Darktable developers, and much more users. So, let us conceptually split both, both in our minds and in the software.

Topology (pipeline nodes ordering) is independent from history. Unlike Darktable, which manages everything through history items (including pipeline order), which is actively harmful in terms of both understanding and code complexity (these go together anyway), we solve the pipeline as a standalone collection of nodes (modules), and then rematch nodes with their last history stage. It took me a couple of years to see through all the overwhelming obfuscation going on in this software, buried in copy-pasted code, and see the light: once abstracted, the problem is pretty easy.

So, once topology is solved, that leaves us with 3 history merging mode : 

- __replace__ : source history replaces the entire destination, mandatory modules like demosaicing for RAW images might still get added on top (so that makes copy-pasting safe across JPEG and RAW). This mode leads to no topological sorting, it's a direct copy of history and pipeline order.
- __append__ : source history goes on top of the destination, so history items that target the same modules in source and destination get overridden by the source,
- __appstart__ : source history goes at the bottom of the destination, so history items that target the same modules in source and destination get overridden by the destination. 

The pipeline order solved by topological sorting is updated in the last history items, both in _append_ and _appstart_ modes, meaning that going back in history will also revert the topological sorting. Histories are not compressed on purpose, when merging, so users retain the ability to revert the merge both using undo/redo features, or by going back in the history toolbox, in darkroom, before the point of merging.

In short: history paste is now deterministic, safe, non-destructive, even for complex edits.

## How to use

There will be one source history (that you copy), and one destination history (where you paste). The same will be applied with styles when they get reimplemented; the source history will be defined by the style instead of another image, and the rest will be the same.

In the global menu __Edit__ → __History pasting mode__, you get to choose between _append_, _appstart_ or _replace_. The setting is global across the whole application. That determines what history (source or destination) takes precedence by overriding common modules.

In __Edit__ → __Nodes pasting mode__, you get to toggle on/off _Copy module order_. If off, the pipeline order of the destination is kept as-is. If on, we try our best to import the source pipeline order into the destination.

As before, in __Edit__ menu, you get the options to copy/paste everything, or only selected modules (through the modal window). Global shortcuts are available and [user-editable](./rewriting-key-shortcuts.md). Note that copy-pasting histories is explicitly forbidden in darkroom view, even from the filmstrip, because it is ambiguous to determine whether you want to copy between thumbnails, from thumbnail to main image or the other way around. In lighttable, you select the source, copy, select the destination, paste, and everything is clear.

Now, there is an important assumption to have in mind : modules that have the same instance name (instance number by default, or user-defined name) are considered to be the same entity on destination and source histories. So, every _Exposure (sky)_ will be merged with every other _Exposure (sky)_ module (case-sensitive), and there should be only one _Exposure (sky)_ instance in destination and source histories. Previously, the code used instance numbers, which is more brittle because they are imposed by the software and incremented in the order of creation, which has no meaning for users.

## GUI and error handling

The beauty of the new solution is that you don't have to open the darkroom to see the mess you created by copy-pasting garbage; you can review it before any harm is done to your edits, in lighttable. Also, when the solver fails to find a solution, which happens with incompatible constraints (A should be preceded by B, but B should be preceded by A) or cycles (more below), it is able to tell _what_ fails, report it, and either request user input to fix it or fallback to the sanest path. Let me show you:

### Trivial cycles

Exposure 1 is before Exposure in source history, but after in destination history. The set of constraints ends up with Exposure → Exposure 1 → Exposure, which is unfeasible. Here is what happens in Ansel :

{{< figure src="trivial-cycle.jpg" />}}

These trivial cycles involving immediate neighbours are caught before solving, so they don't interrupt the control flow.

### Non-trivial cycles

Those non-trivial cycles involve several modules and cannot be detected before attempting to solve the directed graph. When that happens :

{{< figure src="non-trivial-cycle.jpg" />}}

In this case, there is nothing to do : we will automatically retry by using the destination order, since it occurs typically when trying to merge source order into destination.

### Forgotten raster masks

Any module that uses a raster mask should be copied along its mask-producing module, unless you plan on solving that yourself later. Just in case it's a mistake, if you try that :

{{< figure src="raster-mask.jpg" />}}

You get a chance to abort the merge right now if that was not what you wanted.

## The merge report GUI (new)

### Basics

The report dialog is designed to answer a simple user question: *“what exactly happened to my pipeline?”*

It shows four pipelines side by side:

1. **Original** (the destination pipeline before the merge),
2. **Source** (the image you copied from),
3. **Override** (where source edits replaced destination edits),
4. **Destination** (the final pipeline after the merge).

{{< figure src="report.jpg" />}}

Each column lists the active module instances, in GUI order. This view includes additional markers:

- **Brackets** `[name]` indicate modules that were newly inserted.
- An **asterisk** `*` indicates modules that use masks.
- A **bold label** indicates modules whose relative position changed between source and destination.
- **Override arrows** show where the source history actually replaced destination edits (with `→*` when masks were also overridden).

### Candy

The destination column is reorderable with drag & drop, meaning that if you are not happy with the result of the topological sorting, you can fix it yourself right now, before it's even saved to your database and XMP, and without having to open darkroom. This lets you adjust the final pipeline manually before accepting it, or revert everything and don't write back the history.

When you reorder:

- the pipeline order is updated immediately,
- history entries are kept consistent with the new order,
- and the report view updates its labels and “moved” markers accordingly.

This is meant as a safety valve: even if the computed order is valid, you still have a simple way to tune it.


## Why this matters

This directly improves workflows that involve batch editing with complex pipelines :

- copying edits between images,
- mixing RAW and JPEG sources/destinations,
- and heavy multi‑instance or mask‑based edits.

The goal is to make history paste **predictable**, even when the underlying pipelines differ. That reliability is especially important for advanced edits where small ordering differences can change results.

This change doesn’t add flashy new features — it makes one of the most used features *trustworthy*. History merges now behave like users expect: consistent results, clear reporting, and safe fallbacks when constraints conflict.

And I don't understand why, 6 years after, the It Works For Me® guys crashing Darktable in slow motion didn't consider improving such a basic yet critical feature. If that doesn't scream wrong priorities, I don't know what will.

## What made it possible

I want to stress here that this whole rewrite was made possible because I almost entirely rewrote the history handling backend in Ansel first, since it was a mess:

1. There were duplicated functions everywhere, that performed the same operation many times but hidden in calling/caller functions all over the software, some inducing filesystem I/O (XMP writing) for no reason, one writing back the history everytime we opened the darkroom (which screwed the _last change_ timestamp),
2. There were several interleaved thread locks that basically made any change impossible without deadlocking things,
3. There were SQLite3 history-fetching code entangled within C code, many duplicated SQL queries, none of them thread-safe (because SQLite3 itself is not thread-safe), so I buried all the SQL code inside a C interface that handles thread safety centrally, and now all the C code fetches history info from the library database with a single API, meaning we know that everything that reads history will read it the same everywhere in the app,
4. Some parts of the history reading, initialization and merging were done in SQL (leveraging `JOIN` statements, which makes sense, but…), and some others were done in C (because modules safety checks and presets initialization is obviously C). That led to stupid things like manually re-indexing history items in C before saving to database in-between transient writes (because SQLite3 doesn't guarantee the history items will be saved in database in the same order they were passed… that's what primary keys are for). So I rewrote the whole thing in C, which may be slightly slower but ensures data consistency: histories are handled exactly the same way, whether we load them to merge them, we open the darkroom or export an image. If there is a bug somewhere, it will be everywhere and we will find it sooner, plus we will fix it in only one place.
5. The history management code was entangled too with GUI code, but it can run also from the `ansel-cli` (without GUI), so that led to many heuristics checking if we had a GUI or not, in many places.

So, once all that janitorial work had been done, then I started to see the structure of what was actually done and needed to be done. From there, one simplification led to another, until ChatGPT 5.2 Codex did the rest. Before 2 weeks ago, that was still done entirely manually and drove me crazy many times. It's really just paint that held those walls, flaked paint, and trying to clean it up destroyed many things because nothing in this software was modular (aka enclosed). Something you change in one place has unexpected consequences elsewhere, which is why we have encapsulation, modularity and design patterns, because the C programming language was not designed for complex desktop applications like that, and it really needs developer's discipline to avoid becoming the nightmare it is.

## This is entirely vibecoded

So the history cleanup was going on since 2023, managing burnout and software-induced depression. That's a shitty quality of life, you have no idea. Those who think I exaggerate don't know what it entails to shovel other people's brain feces for more than 3 years. Because I have known a time where all that was, if not better, at least less complicated and more manageable. Until the madness of COVID-19 hit, and idiots got too much free time on their hands, that they used to destroy something that was roughly working.

And then I found out about ChatGPT 5.2 Codex 2 weeks ago, and installed it within VS Code editor. So, it took me 3 days of work to do all the stuff I presented here. Without ChatGPT, it would have been a solid 3 weeks, plus the never-ending fiddling with GTK tidbits. Let's talk about the experience.

I disagree with those who try to make us believe GenAI is just a tool. A tool works only in my hand. Not when I sleep. I have made ChatGPT work for me while I was cooking dinner (yes, it's _that_ slow). You don't communicate with a tool, you just use it to the best of your ability. In case of failure, well some blame the tool, but we all know what it means. Problem is ChatGPT doesn't have buttons or sliders, it interpretes  what you tell it, and not necessarily how you mean it. And then a tool doesn't take initiative. Well, ChatGPT surely has an opinion on how code should look like, and sometimes you need to fight it.

GenAI is an intern. An intern has no experience and only knows what is taught in school. An intern can bring fresh new ideas that challenge your habits, and delirious suggestions just the same, that are not remotely relevant to your context and sometimes even not feasible. But an intern needs to work under close supervision and be given clear, non-ambiguous instructions. ChatGPT is much more an intern than a tool.

ChatGPT does a lot of mistakes, and they are sneaky because they are buried in the middle of perfectly valid stuff. It has some weird obsessions (like NULL-checking every pointer that we already know can't be NULL). So, you really need to watch it. Though reviewing and fixing its mistakes is still faster than writing all the code myself, not to mention my first carpal syndrom issue was 10 years ago, so it's always that much to _not_ type. Plus, it does mistakes on logic, but no typos, and at least a lot less than myself.

But where ChatGPT Codex shines, is in 2 things. 

First, the tedious game of `grep`ing functions across the code base to find out (reverse-engineer) the lifecycle of data and check all call sites to build a mental model of what's going on. That takes ages, it's very cognitively demanding, especially in a code base that shitty. ChatGPT works wonders to traverse dozens of files, extract patterns, find out what could be factorized, and follow sequences of execution. Let us be very clear that, in a well-maintained code base, that should not be a need because the code would be self-enclosed in modules, isolated from the rest. But ChatGPT helped a lot to make things more modular.

Second, everything involving GTK and GLib. Those are poorly documented on the web, and many idiomatic patterns of interaction are known only to GTK developers. ChatGPT has obviously ingested lots of open-source code and can produce much better boilerplate GUI code than I could (or care to). Anyway, before ChatGPT, that turned into tedious sessions of googling info, and I can't find any relevant technical info on Google since 2020 or so, when they changed their algorithms to aggressively second-guess everything. But I work to solve problems, and all the GUI boilerplate functions initing widgets and their properties in a declarative style is not worthy of my intelligence, it's only trying to not introduce typos.

But to get a better idea, here is the kind of prompts I had to give it to build what I just presented :

> now, in _hm_try_merge_iop_order_topologically(), build early in the function a GHashtable of all modules IDs tied to mod_list, then to dev_src->iop, then to dev_dest->iop. These will be useful to compute intersection of sets later.
don't modify dev_dest->iop_order_list. For all item in the sorted list (item being a node ID tied to a module op and multi_name) :
> 1. find out if a corresponding module instance exists in dev_dest->iop, if not create it. Because dev_dest->iop is already inited and sanitized upstream, we can safely assume that every module not found should be inserted as a new instance. If the module instance ID is found in the input mod_list, the whole content of the module (parameters, blendop, etc.) should be copied between the source instance to the destination instance. Mind the deep copies that need to happen. 
> 2. overwrite all module->iop_order values with the new index number we just found by solving
> 3. rebuild dev_dest->iop_order_list from scratch and update the module->multi_priority accordingly

> now, in dt_history_merge_module_list_into_image_advanced, the temp history needs to be built as follow:
> 1. deimplement the force_new_modules path for now, we will come back to it later and differently,
> 2. build a temporary history as follow: for each module in mod_list:
>      1. get the associated history item from dev_src->history (that would be the last one matching this module on the history stack),
>      2. get the pipeline ordering info (iop_order, instance, multi_priority) from the corresponding module in dev_dest->iop
>      3. update the existing history item from dev_src->history with pipeline ordering, since it may have changed after the topological sort, from the original history item,
>      4. add this history entry to the temporary history
> 3. concatenate the temporary history with dev_dest->history, first or last depending of append or appstart mode.
> 
> Try to use methods from history.c and dev_history.c as much as possible for the history to/from module handling. Extend the existing ones if you only need minor changes.

> No, revert that. It's not ok to delete history entries past the history_end in general. Whatever is in dev->history should go into the DB. Also, it's not a problem because the history_end is also saved into DB. The problem here is that random history items are added when reading back the history from DB. Everything up to writing history, which happened in C, was ok. Find out why we get extra history entries when reading back from DB, compared to what we have at writing time before.

> at the end of dt_history_merge in history_merge.c , I want you to show a report popup window. A text label will first tell "Copy, merging pipeline in {MERGE_MODE} and history in {STRATEGY} mode", where {MERGE_MODE} depends on merge_iop_order (merge or destination), and {STRATEGY} depends on strategy.
Then I want a GtkTreeView in list mode, with 3 columns:
> 1. the source of the copy, with image ID and filename (not the full path),
> 2. the override,
> 3. the destination of the copy, with image ID and filename.
>
> In columns 1. and 3., each row will show the module instances, starting with their pipeline order, module->name and module->multi_name. Only enabled modules will show. The column 2 will draw an arrow between source and destination instances when the source history overrides the destination history. This is done by checking, in the destination history, if the last entry targeting this module matches destination history or source. In case it matches both, show nothing since it's not an override. The pipeline nodes will be shown in reverse order to match GUI ordering, since it's a kind of layer stack. They should both be aligned on the bottom so the early steps have a chance to be on the same row until topology diverges between both pipes

One thing I found out is you can definitely be _too specific_ with ChatGPT and lead it to a wall. When that happens, the best course of action is to take over manually.

The energy cost of that thing is unbearable, but let's say, divided be the 900-ish guys who starred Ansel on Github (I don't have download stats), it's for the greater good. It just a more efficient way of sparing my brain juice to think about _what_ should be done (design and architecture), rather than _how_ to do it. Probably not how kids vibecode these days, though.

Next: styles.---

# Notes : https://ansel.photos/en/doc/views/toolboxes/notes/

Write per-image notes that live next to your files and render as Markdown. Such notes can be to-do lists, memos, contacts of the persons in the image, tips or instructions for collaborators, etc.

The module opens in **preview** by default; switch to **edit** to change the source text. The Markdown parsing needs Ansel to be build with [CMark](https://github.com/commonmark/cmark), which is optional. If Ansel was not built with this library, it will be rendered as plain text.

Notes are stored as a sidecar text file named `<image basename>.txt` in the same folder as the image. When a local copy exists, the note follows the local copy. When images are imported, copied, moved or renamed with Ansel, the `.txt` sidecars are moved along if present. The text files can be opened with any Markdown or plain-text editor.

All duplicates (variants) of an image will share the same sidecar `<image basename>.txt` file, where you can document the differences between them.

Changes are saved automatically after a short delay and when focus leaves the editor. The content of the sidecar `.txt` can be used through the [variable](../../variables.md) `$(SIDECAR_TXT)` in various places in the software.

{{< note >}}
Though the sidecar text files are a little-known feature, they have been supported in Darktable since at least 2014 and Ansel has kept supporting them just the same, only extending them.
{{< /note >}}

{{<gallery cols="2">}}
{{<figure src="text-notes-edit.jpg" />}}
{{<figure src="text-notes-preview.jpg" />}}
{{</gallery>}}

## Module controls

last modified
: Shows the last modification time of the note file (hidden when no note exists).

edit / preview
: Toggle between editing the Markdown source and viewing the rendered preview.

text editor
: Multi-line editor with word-wrap and automatic saving.

presets
: Store and apply text templates. A built-in **Default** preset provides a checklist and lifecycle line (see below).

## Markdown preview

Supported formatting includes:

- Headings (levels 1–3 are emphasized)
- Emphasis (italic and bold)
- Inline code
- Ordered and unordered lists
- Links (clickable in preview mode)
- Images (local or remote)
- Task lists / checklists

Checklist items can be toggled by clicking their checkbox glyphs in preview mode; the source text is updated accordingly.

Preview spacing matches the edit mode’s blank-line separation between blocks, so switching modes doesn’t shift the layout.

If Ansel is built **without** CMark, preview falls back to raw text (no Markdown rendering).

## Images

Local images referenced with `![]()` are rendered inline.

Remote HTTP/HTTPS images are downloaded and cached under `~/.cache/ansel/downloads`. If an image is not yet cached, it appears as soon as the download completes.

Images are color-corrected using the active display profile.

## Variables and auto-completion

Type `$(` in edit mode to trigger [variable](../../variables.md) auto-completion (the same variables used in path templates).

In preview mode, variables are expanded, for example:

```
Shot on $(EXIF.YEAR)-$(EXIF.MONTH)-$(EXIF.DAY) $(EXIF.HOUR):$(EXIF.MINUTE)
```

## Example preset (Default)

```markdown
## Todo

- [ ] Normalize illuminant & colors
- [ ] Normalize contrast & dynamic range
- [ ] Fix lens distortion and noise
- [ ] Enhance colors

## Resources

- [Documentation](https://ansel.photos/en/doc)

## Lifecycle

Shot on $(EXIF.YEAR)-$(EXIF.MONTH)-$(EXIF.DAY) $(EXIF.HOUR):$(EXIF.MINUTE)
```
---

# Democracy : https://ansel.photos/en/contribute/democracy/

Ansel should be run democratically and fairly to everyone involved. 

## Defining a cooperative organization beyond open-source code

Developers and users landing here are assumed to share a common interest : they want freedom in how they edit their pictures, now and in the future. Meaning :

- having a sufficient technical control over their image content and properties,
- being free from capitalists who may :
    - increase their application price at the sole benefit of shareholders,
    - mine customer's photos to train AI models without consent,
    - extinguish applications without releasing the source code,
- having a right to decide on the contours and implementation of said freedom.

Ansel DNA is more technically-minded and fined-grained than most RAW photo editors, while still emphasizing user-friendliness where it's possible, mostly on typical desktop-centric tasks (interactions with input devices and files, GUI paradigms, etc.). Ansel is not [Darktable](https://darktable.org) or [ART](https://art.pixls.us/) because its vision of what a good image retouching software should be is quite different.

Ansel is released under the [GNU/GPL license](https://www.gnu.org/licenses/gpl-3.0.html), making it a free/open-source software. While this gives a (wrong) sense of long-term availability, the reality is its long-term availability depends solely on the will and ability of some developers to perform the (tedious) day-to-day maintainance to keep it working, keep supporting new cameras, etc. which is an underrated task because it doesn't produce spectacular release notes or keynotes.

Worse, the so-called "freedom" granted by the GNU/GPL license impacts only the source code : it is engineer's freedom. Open-source licenses essentially waive intellectual property on the code, which means engineers can study and modify it, then share their modifications. Given that less than 6 % of the world population[^1] can actually write code, this is a right for only a privileged minority. As for users, well, the GPL license is quite clear :

> This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;

[^1]: Aurélien Pierre, _Who are the Darktable users in 2020 ?_, 2023. [URL](https://eng.aurelienpierre.com/2023/01/who-are-the-darktable-users-in-2020/#ability-to-interact-with-the-computer).

This statement essentially excludes end-users from the transaction : they have no rights other than running the code as-is, especially no right to get support and bugfixes, let alone get the software shaped to their needs. If some support is still provided, it's at the discretion of developers and users should be forever grateful for it. This is abused on so-called "open-source communities" (meaning "software forums") to shutdown and invalidate any user complaint : they simply can't complain since they are using free work, even though the marketing of open-source projects often wax lyrical regarding the "professional" fitness of those applications. But… promises were made, the "pro" word was uttered but not delivered, so there is a misleading inconsistency somewhere.

Even waiving any responsibility towards users, many projects still try to turn them, if not into customers, at least into donors. Which faces a sad state of affairs : 350 years of capitalism have rotten our brains into thinking that open-source applications were the cheapest competitor to proprietary software. Users simply sit on their bottoms waiting for the projects to deliver a product before deciding if they will bother to donate.  This is like starting to fund the NASA after the first rocket was launched to prove the project can indeed deliver : it can't be. You need to pay the work even before there is a product to sell. And when the product is released, projects like Wikipedia still struggle to get less than 3 $/year from 2 % of their user base.

Open-source applications are competitors of proprietary applications just like public hospitals are competitors of private clinics : they are not. On one side, we have a socialist project started by the belief that everybody has a right to healthcare, and if they can't afford it, we will make do by finding extra money into the community. On the other side, we have shareholders trying to make profits, hiring surgeons trying to pay for their Malibu home and their collection of Porsche. How are they competitors ? A public service is a way to ensure people have resources to exert their rights, because freedom is only theoritical until you have the means to actually make use of your rights. A private company selling goods or services is a way to make money out of investments. The difference is political. It is also economical, since not having the shareholders on the payroll makes the final product more affordable anyway.

The blindspot of the "Free Software" movement was to avoid thinking the context and the working structure in which such software happens, is developed and maintained. This can be traced back to the [hacker culture](https://en.wikipedia.org/wiki/Hacker_culture), which is individualistic by nature and capitalism-compatible by proxy, through [technophilia](https://en.wikipedia.org/wiki/Technophilia) and [technosolutionism](https://en.wikipedia.org/wiki/Technological_fix). "Sharing as-is" (your work, your code, your hacks) was mistaken for "taking care of" (other people's needs, limitations, challenges) and entertained with quasi-hagiographic narratives. When you have a hammer, everything looks like a nail : when all you know is code, code is the solution to everything, and trying to wrap it into philosophy and (non-revolutionnary) politics to define "code freedom" still misses the big picture that is the exploitation of everybody's work by a small but powerful and self-reproducing social class. While 6 % of the World population could write code in 2021, the ratio who could even afford a personal computer back in the 1980's, when the _Free Software_ movement started, was much smaller. Privileged minorities are always blind to their privileges : this is how you identify them. They failed to see that _Free software_ was nothing more than the brainchild of those privileges. It is freedom only for those who can afford it. And the fact that the price tag doesn't have a dollar sign attached to it makes it vicious. It still has a price tag.

The _Free Software_ movement was made to fit into this individualistic, free-enterprise, liberal-market capitalism, and made everybody think that it was enough to let everyone start their own project or fork it from others. Then, [social Darwinism](https://en.wikipedia.org/wiki/Social_Darwinism) would sort things out. Yes, but… who pays for the work ? Worse, since capitalism gives value to work only when its product is sold on a liberal market, and that value is indexed on the product scarcity, how exactly are we supposed to convince users that something dematerialised, infinitely downloadable for free has any value ? Many open-source projects have tried many different strategies to fund their work, they are all precarious and crippled because they still want to fit a working structure that is communist by nature into a capitalistic framework (more on that below). 

While non-profit organizations may work for humanitarian projects, where donors will never be the target recepient of the work, for software projects, donations are interested because donors are users. This entertains a consumer-like behaviour, where passive (and somewhat oppressed) consumers wait for the goods delivered by an elite minority of board members who calls the shots. Those non-profit organizations are still privately run, and the public has no other right but to stop donating if they don't feel heard. The [board of the Linux Foundation](https://www.linuxfoundation.org/about/leadership) is almost exclusively composed of executives from hardware vendors and [GAFAM](https://en.wikipedia.org/wiki/Big_Tech), the [board of the Free Software Foundation](https://www.fsf.org/about/staff-and-board) is composed of scientists and computer technicians. There is no representation of users there, there are vertical hierarchical structures of "those who know" over "those who need", perpetuating the same kind of domination as capitalism, minus profits.

This is justified by the fact that the open-source licenses waive any form of responsibility (warranty) from developers towards users, which makes it a one-way transaction that apparently gives all the power of decision to developers. But all that is of course a construct, and not a forgone conclusion. On the other end, because the capitalistic dogma is so deeply impregnated into user's brains, the source code brings a software on a free, dematerialised and non-scarce market that voids any notion of capitalistic value, which makes it ok for users to get the product of the work without contribution : another one-way transaction. Meanwhile, some developers are burning out and exhausting themselves to provide reasonable user support, without getting a reasonable income from their (exploited) work, maybe in the hope that it will end up paying in the long run, once they "make it". Or developers accept the unspoken rule of open-source (it should only be a hobby/part-time), which inevitably sets open-source back, as a forever-poor sibling of proprietary software. Or, finally, some open-source companies like [Automattic](https://blog.pragmaticengineer.com/did-automattic-commit-open-source-theft/) or [RedHat](https://www.jeffgeerling.com/blog/2023/im-done-red-hat-enterprise-linux) start getting more and more aggressively greedy over time, facing the backlash of their communities. This is not sustainable, for neither of the involved parties.

__We need to turn those two parallel one-way transactions into a circle. This is how :__

1. No technology can exist outside of the society that produces it. Technology needs science. Science needs research. Research needs structures where it can be conducted freely. Society is the environment where all of that happens, as well as the support system that makes it happen.
2. No technology can exist without work. If the product of the work escapes the capitalistic system (scarcity-based and competition-based liberal market), then the work producing it should escape it too.
3. No technology that is privately-owned will serve the common good and the public interest. Technologies, not just devices, should be owned by their users, not just by their makers.
4. Work is the only wealth. Workers should benefit from their work, whether sold on a liberal market or not : it should be paid no matter what. Work should happen in a safe and fair environment. It is a collective, social responsibility to make that happen, because the product of the work serves the common good and public interest. Work that doesn't serve those should simply be stopped.
5. Technologies owned by their users open the way for a new kind of production : collaboration between makers and users, instead of competition between makers and coporate wars to win markets through convincing customers of some product's superiority (making users pay for advertisment in the end…). But capitalism doesn't know how to sell the product of cooperation, because there is no separate buyer or seller anymore, only a community of persons who work toward something together : fulfilling their needs, turning their theoritical rights into actual freedom by creating the the context to exert them.
6. There is a mutual responsibility of makers towards users (fulfilling their needs through/with technology), and users towards makers (giving them a safe and fair working environment and material living conditions). This is the very basics of a community[^2]… 
7. Open-source leading to __true freedom__ can only happen inside a [cooperative](https://en.wikipedia.org/wiki/Cooperative) structure. Which is an old, proven and already-working communism, where the company (therefore the open-source project at large) is owned by its clients and by its workers, who share voting powers. This goes well beyond a mere intellectual property waiver (open-source/free license).

[^2]: Unfortunately, the definition of "community" in the open-source world is more like a group of guys who get excited about the same techs, rather than a group of people who take care of each other.

A cooperative is a way of breaking this dichotomy between "us" and "them", makers vs. users, that feeds mutual resentment : developers are the dictators taking the application for their personal playground, to whom users should always be grateful no matter how they destroy it, users are the annoying leaches that keep raising cryptic issues and feature requests, while donating too little.

In a cooperative, makers and users alike are members-shareholders. They all own the project, they all have one vote in the general assembly. The project is to make photo editing free for the foreseable future, under some unified vision of what photo editing is (how technical & fine-grained it should be vs. how easy and user-friendly). To fulfill that goal, many means are involved, like education, advocating, documentation, and obviously a software application. The project is more than just an end-product, which can take some time to appear and be ready : the project is everything above and around, it's a goal and all the means to make it a reality.

The responsibility of the members is to secure a yearly budget to cover all project costs, most likely from yearly members dues. From that budget, a certain amount of working hours at a certain hourly rate is decided in general assembly. How this budget will be spent (what kind of tasks, hardware, tools, etc.) is also decided in general assembly. For technical tasks, like software development, that will be the high-level orientations (like improving masking, batch workflow, etc.) and not on implementation details or actual design, which are poorly suited for democratic processes. Developers might push backend-oriented tasks (like rewriting or refactoring technical debt, to reduce long-term maintainance cost), users might push feature-oriented tasks (like supporting new camera features, image formats, etc.) : the purpose of the general assembly is to deliberate and rank priorities. 

The agreed tasks will then be tackled by paid workers in the order of priority until the work budget is consumed. This is not limited to technological work and development, but applies to any kind of work previously agreed upon. When the budget is consumed, workers will report where they stopped in the todo list, what they could finish, what they couldn't, and what resources were missing to finish. The general assembly will then decide if it is possible and desirable to invest more resources to finish, or to slide the remaining tasks into the next yearly budget. This gives workers visibility over their yearly income without promoting over-committment and burn-out.

Users can form panels to work with developers on understanding the actual issues faced, test and validate solutions into a [design process](./design.md). Any user can be turned into a worker and start collecting income for their work on agreed tasks, upon approval of the general assembly.

This makes it everyone's responsibility to ensure the resources to go forward are gathered, and to give workers what they need to work decently, safely, without getting exploited. No amount of work should be free, no matter the task. Work should not be limited to programming or other technical tasks. 

## The limits of democracy

Democracy by vote is essentially expressing the will of the majority, which has been known to oppress minorities. It is therefore not perfect. Minorities might voice reasonable and legitimate concerns, only they will not be shared by the majority. Disabled persons come to mind : some design details may make the whole application completely unusable for them, even though they don't have the numbers to impose their views in a vote. On the other end, it is not possible to accomodate each and everyone's peculiar needs without creating monsters. This needs to be carefully evaluated and arbitrated.

The responsability of the majority is therefore to identify who are the structural minorities, that is what properties tie those minorities together (health condition, social origin, education, level of income, etc.). If a minority raises an issue that is blocking for them because of one of the defining properties of said minority, there should be a way to bypass or re-weight the majority vote. This remains to be defined, but the easiest way to encompass this is during deliberations, which relies upon the empathy and understanding of the majority.

Another issue of any social group is [groupthink](https://en.wikipedia.org/wiki/Groupthink), because any social group tends to degrade into a club over time. Groupthink is when individuals don't feel free to raise issues that go against the (alleged) group consensus, for fear of repercussions regarding their place and perception in the group. Those repercussions can be very subtle yet very real. It becomes damaging when individuals start supporting decisions, inside the group, that they would not have supported on their own : this is the turning point where individual rationality gets lost. To avoid this, anonymous vote can be used, but it doesn't apply to deliberations where there is necessarily a speaker. There is culture of sane dissension to build, maintain and encourage. One way of solving this issue is to randomly appoint a "devil's advocate" for each session, whose job will be to constantly disagree and expose corresponding arguments.

Any community based on a shared interest for photography & free software is expected to be biased in favor of rich, educated, computer-litterate, English-speaking men. It will have to be a conscious effort to try and include women, less-educated persons, non-western citizens, etc. Combined with democracy-by-vote and groupthink, it can be really damaging to start with such a socially homogeneous group, if not managed carefully. This will be a daily challenge that everybody needs to be aware of. It will also feed the [survivorship bias](https://en.wikipedia.org/wiki/Survivorship_bias), where the outcasts are not here to provide feedback on why they didn't join, participate or feel welcomed and outcasts will need extra care to be reached and welcomed.[^3] No amount of racism or sexism can be tolerated in a such environment, even as a joke, because that would make the majority laugh at the expense of minorities in a context from where they are already filtered out.

[^3]: Which is why attending free-software-centric graphics/imaging events is probably not a good investment.

Democracy is also a slow process. Things may happen that need a prompt decision, like a security breach into the project server or social media accounts, or any pressing legal issue. Someone should be elected for short-term mandates to be able to quickly take those decisions on behalf of the community. 

## The limits of experts vs. laymen

Just like doctors or laywers, developers have niche competences and experience, that will give them some sort of power and even charisma over laymen. This power can be abused to oppress the majority by using its lack of knowledge at the benefit of the oppressor. This relationship will always be asymetrical, and that has to be acknowledged. Also, developers will have to be consulted during deliberations in general assembly, to assess the feasability of tasks and the resources needed, before votes happen.

Just like doctors, lawyers, etc. developers should uphold some sort of code of ethics that go with their symbolic power, among which :

1. a duty to honestly inform, to the best of their knowledge, and to simply explain what can be simply explained,
2. a duty of due process and due diligence when researching and developing solutions,
3. a commitment to work in the best interest of the community.

On the other end, developers (or any other experts) might be perceived as all-knowning, omnipotent wizards by laymen, who may then ask too much of them. Although software is a quite malleable medium, there are still non-negotiable technical constraints that apply, and everything that is technically possible may not be possible _within the current circumstances with the current resources_.

Both these positions will need to be understood as to prevent mutual resentment growing.

## The limits of collaboration

> Everybody is a designer.

Every job you know nothing about looks easy to do. Designing and building technical objects is one of them. It is even exciting to contribute to it. But a software that will be used as a tool by thousands of users other than yourself is not the same thing as your own nightstand. 

Users should be welcomed to participate in problem definition : start from one or several users having a particular problem in a particular workflow, see if that can't already be solved with current tools and perhaps some education, and if not, try to survey how many users share the same problem. If the problem is not exactly the same for everybody, try to find a common expression of the problem that allows to generalize it.

Once the problem is defined begins the [design process](./design.md), which follows formal steps to avoid racing to the first (and most likely not optimal) solution because, again, human intuition is better to build nightstands rather than tools used by thousands. But following the process is not enough.

Psychology and cognitive sciences converge toward a consensus : the best size for working teams is between 4 and 7 members. Starting at 8 members, productivity starts dropping dramatically.[^4][^5][^6] For a team of 4 members, there are 6 interpersonal communication channels, while for 8 members, there are 28 communication channels. This makes it hard to cognitively keep track of everybody involved, who is in charge of what, process non-verbal cues, which creates communication overload. Above 9 members, clans start forming, politics get mixed in the process, and [social loafing](https://en.wikipedia.org/wiki/Social_loafing) embarks.[^4][^5]

[^4]: HACKMAN, J. Richard. Leading teams: Setting the stage for great performances. Harvard Business Press, 2002. [URL](https://books.google.fr/books?id=snfoCQAAQBAJ&lpg=PR5&ots=SCn-vzTdUf&dq=J.%20Richard%20Hackman%20(Harvard%2C%202002)&lr&hl=fr&pg=PR8#v=onepage&q=J.%20Richard%20Hackman%20(Harvard,%202002)&f=false)

[^5]: WHEELAN, Susan A. Group size, group development, and group productivity. Small group research, 2009, vol. 40, no 2, p. 247-262. [URL](https://journals.sagepub.com/doi/abs/10.1177/1046496408328703)

[^6]: ALLEN, Natalie J. et HECHT, Tracy D. The ‘romance of teams’: Toward an understanding of its psychological underpinnings and implications. Journal of occupational and organizational psychology, 2004, vol. 77, no 4, p. 439-461. [URL](https://bpspsychub.onlinelibrary.wiley.com/doi/abs/10.1348/0963179042596469)

This means that work should be split into working teams having each no more than 6 to 7 members, even though it might get exciting for the community at large to take an interest into everything. Communication is the turning point here : you need just the right amount of it, no more, no less, otherwise it cognitively saturates team members and hinders communication itself. There, it's the work in itself that takes a hit.---

# GUI : https://ansel.photos/en/tags/gui/
---

# Rewriting keyboard shortcuts (accelerators) from scratch : https://ansel.photos/en/news/rewriting-key-shortcuts/

In my defining post, [_Darktable: crashing into the wall in slow-motion_](./darktable-dans-le-mur-au-ralenti/index.md), I presented the trainwreck that the new "Great MIDI turducken" was. The purpose of this turducken[^1] was to rewrite the keyboard shortcuts system to extend it for MIDI devices.

[^1]: A turducken is a chicken stuffing a duck stuffing a turkey. That's a decadent amount of meat that will likely go to waste, unless you have 20 persons to feed. Anyway, it will take forever to cook, and the turkey will likely be dry by the time the chicken is well done.

To this day, I'm still mad about this enterprise of mass-destruction, here is a recap of he the reasons:

1. it replaced in 2021 a keyboard shortcuts system that was pretty good, feature-complete, well tested, stable and coded in less than __1500 lines__ (comments included),
2. …to add support for MIDI devices and PlayStation gamepads (!?!)…
3. …but in my 2022 Darktable survey, one year after this new feature, over 1251 users who participated:
    - 81% of users didn't have a MIDI device and didn't plan to get one,
    - 2% didn't even know what a MIDI device was.
    - 8% of users had a MIDI device but didn't use it with Darktable,
    - 6% were considering _maybe_ getting a MIDI device in the future,
    - __2% of users had a MIDI device they actually used in Darktable__,
3. the code was absolutely terrible, in terms of:
    - code quality: unlegible `if`/`switch-case` statements nested on 4 levels, in the middle of 1000-lines functions (I posted example snippets in my [article](./darktable-dans-le-mur-au-ralenti/index.md#keyboard-shortcuts)),
    - code volume:
        - __3546 lines__ of code for Darktable 4.0,
        - __4397 lines__ of code for Darktable 5.0,
        - the increase in volume is a direct consequence of trying to fix bugs in an architecture that can't be fixed because its complexity promotes more complexity. All that stems from the design, but solving issues created by complexity with adding more complexity is not a solution.
    - code complexity:
        - [cyclomatic complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity):
            - __1088__ for Darktable 4.0,
            -  __1245__ for Darktable 5.0 ([details](https://sonarcloud.io/component_measures?id=aurelienpierreeng_darktable-5&metric=complexity&view=list)),
        - [cognitive complexity](https://www.sonarsource.com/blog/cognitive-complexity-because-testability-understandability/):
            - __1885__ for Darktable 4.0,
            - __2098__ for Darktable 5.0 ([details](https://sonarcloud.io/component_measures?metric=cognitive_complexity&view=list&id=aurelienpierreeng_darktable-5)).
        - it is by far the most complex feature of the software, even though it does not operate on images. As a comparison, the second most complex feature is the EXIF metadata decoding, which has a cognitive complexity of __1348__.
4. it doesn't decode key modifiers __by design__, but only deals with hardware key-strokes, which means:
    - "1" input from the numeric pad is decoded `Keypad End`,
    - "1" input from a French AZERTY keyboard is decoded `Shift+&`, or `Shift+"` on BÉPO,
    - you therefore need to duplicate all your number-based shortcuts for each way of entering a number, and be prepared for the shortcut settings window to not contain any actual number in the key combinations.
5. the user-end design is absolutely terrible, with way too many actions and emulations to configure ("effects"), that are not even fully documented 4 years later (what is "ctrl-toggle" ? "right-activate" ?), and the shortcut configuration uses a weird split-window that doesn't make any sense,
6. the implementation is also terrible: the feature is aware of all the software GUI, and the software GUI is aware of the shortcuts code. There is no modularity here, and changing anything in the shortcuts code may have unexpected and undesired effect anywhere in the software.[^12] Just see the dependency graph below,
7. several "shortcuts" (or MIDI bindings) can be attached to the same action, which means every user interaction has to lookup the whole list of available actions, inducing very inefficient shortcut handling, GUI lags in some cases and "unknown key combination" false positives in peculiar cases.

[^12]: Developers call that ["whack-a-mole bug fixing"](https://www.youtube.com/watch?v=8ncQrGuunHY&t=609s).

{{< figure src="shortcuts-broken.png" >}}
Non-decoded number keys and weird window splitting between "action" and "shortcut".
{{< /figure >}}

{{% figure src="accelerators-before.jpg" %}}
The dependency graph of `src/gui/accelerators.c` (Great MIDI turducken) before the rewrite. Guess why we call it "[spaghetti code](https://en.wikipedia.org/wiki/Spaghetti_code)"… This makes it clear that there is a double-sided dependency between the accels code and the rest of the GUI code. This is a nightmare to maintain.
{{% /figure %}}

So, said otherwise, one (very bad) programmer replaced a working and simple feature by a monstruosity, with the approval of the maintainer (who would never write code that bad himself, but is determined to "not loose momentum" on contributions, whatever the cost), to please 2% of the user base. All that for a secondary (tertiary ?) feature.

I call that __wrong priorities__. Especially since it took several months of hard work from one developer and many beta-testers, only to make things worse, and then the hard work became a justification to never revert the change, which is known as the [fallacy of sunk cost](https://en.wikipedia.org/wiki/Sunk_cost#Fallacy_effect).

## I'm an user, I don't care about code

Caring about the code of the applications you use is just like caring about whether the pipes bringing water to your home are made of lead. It's not your job, pipes are buried out of sight, so you have all the reasons not to care — _and the people in charge of the water network have all the reasons for you not to care_ — but they will have an effect on your health, and these effects will be unvisible until it's too late. Software may not have a direct effect on your health,[^6] but the way it is made will have a long-term impact on you.

[^6]: Although the stress and anxiety inflicted upon workers by ever-changing software stacks is largely underrated, since innovation is deemed to increase productivity _by definition_, regardless of user feedback.

I find myself in an uncomfortable position – the [Cassandra](https://en.wikipedia.org/wiki/Cassandra) — having to explain to users that problems they don't know, don't see and don't care about are very much affecting the day-to-day usability and stability of the tools they are using, but in non-obvious ways. In any case, these problems directly affect the probability of any maintainer to, one day, solve the bugs they face.

Bearing bad news makes you the bad news, but bearing bad news nobody cares about makes you an asshole, even though those bad news explain the weird and random issues that get more and more common in Darktable bug trackers since 2021, which don't get solved over years because at that point, the code doesn't __contain__ bugs, it __is__ a bug. But here, you have to face [filter bubbles](https://en.wikipedia.org/wiki/Filter_bubble): depending on where on the web you look at, you will either find people deeply happy about Darktable (performance, stability, design), and people deeply unhappy about it. My empiric observation is the happy campers, in average, own powerful computers and have STEM degrees.[^7] The unhappy people tend to move away and reduce the time they loose with the software, which means that, if you are not actively looking for them, you will only feed your [survivor bias](https://en.wikipedia.org/wiki/Survivorship_bias). And, while I don't see the point in trying to convert users to Darktable (or to any open-source photo editor), because to each their own, filtering out and deterring users based on their computer-litteracy is a big fail for any photo editing app.

As a guy with a Youtube channel explaining how to use Darktable, and giving 1-on-1 training sessions, I tend to attract more various feedback than only the Github issue trackers (where criticisms of design get shutdown quite fast anyway), and witness first-hand on video chats those weird, random and unreproduceable bugs. Any priviledged guy has a tendency to deny, minimize or disregard testimonies regarding issues he doesn't face himself, or even blame the issues on the person reporting them. So, I often have this impression of two parallel universes that don't communicate but look down on each other, when it comes to feedback about Darktable. Of course, Darktable developers chose to look where the sun shines.

[^7]: STEM: Science Technology Engineering Mathematics

And while Darktable looks like an active project with a lot of new features added regularly by a dozen of guys, it's all the more difficult to explain that what's actually happening backstage is destruction of the code base, because the code quality is degrading a lot as time goes by, until it will not be possible to debug at all. Volume of changes says nothing about quality, reliability, or maintainability. But 2 centuries of capitalism have conditionned us to seek shiny new features, whatever the cost, and in this regard, Darktable meets the expectations.

Code is born broken. Any software has bugs. Even worse, we use a lot of third-party libraries to avoid "reinventing the wheel", but those libraries will change their [API](https://en.wikipedia.org/wiki/API) in the future, meaning code that works today will stop working tomorrow and will need to be reworked in the future, when dependencies change. Code is a like a garden: every new season brings its amount of chores. It is called maintenance. Once this is acknowledged, wisdom is in planning ways to make maintenance possible, first, and then easy.

Good code is code easy to maintain. That is, good code is written in ways that are nice to the people having to read, understand and fix it. We don't write code for computers. We don't write code for features to "just work" now and in the next months. Code is not something we hide under a nice hood hoping to never look at it ever again. Code is a living organism that costs energy and time to keep alive.

Maintenance has a cost, one that is often underestimated. It doesn't matter if your product is the best of the market, if your maintainance costs are prohibitive, (informed) customers will avoid it. Maintenance is boring, uninteresting, not sexy. Maintenance is the opposite of introducing shiny new things: it's ensuring dusty old things still work quietly. On your software release notes, it goes at the end. Users don't get excited about maintained features, but they will get mad about unmaintained code and tend to use the wrong metric to assess how well a project is maintained (meaning: agitation and semblance of work). So you will win nothing from maintaining code, but you will loose from not maintaining it, and it will cost you whether you do it or not.

Code is unmaintainable when every bug fix leads to a new bug somewhere else, in a _whack a mole_ way, and the nature of the bug fixes is actually contextual patching and workarounds that only add complexity to the software. When you reach that point, your only option is to rewrite the incriminated code from scratch. Which means that past work has just created more work now. And when that specific past work was already overwritting previous work, we embark in idiocracy.

More code is not a success or an achievement. Achievements are in solving user's problems. Here again, the capitalistic mindset hits us hard: we would like to measure work by the volume of code added. More code is just more liability, more technical debt, more maintenance costs. That's a cognitive dissonance since software is precisely about writing code. But think of it like that: making a car is not about adding more steel over 4 wheels, because at some point the car will have to move. You need the right amount of steel, shaped the right way at the right places: you need to be efficient and parsimonious. Software is about solving user's problems with computers, not about stashing code.

On projects where contributors are not paid, boring maintenance is not where people want to spend their saturdays, and it is a real concern as far as project management goes. It is even more important to make maintainance easy when people work for free. Luckily, Darktable has zero project management, just a hive of random contributors scratching their itch at random times, without unified goal, strategy, schedule or work division. __Which is why I would like everybody to stop using the term "Darktable team"__. There is no _Darktable team_, because there is no work division, no leadership, no schedule, no roadmap, no planning, no goal, no priorities, no vision, no strategy, no method, no design, no communication __ahead__ of doing things, no specifications documents for the problems to solve. This is the highschool computer club, made of disconnected individuals.

There are 3 things a developer needs to do to ensure maintenance is as easy as it can be:

- __write the least amount of code__: tracking a bug among 300 lines will be much faster than among 3000 lines,
- __write the most simple code__: "simple" as in "the functional logic has few steps, few assumptions, few corner cases". Simpler means easier to read, understand and fix, but also that there will be fewer cases to cover while testing. In practice, it means avoiding contextual cases, user options, variants, `if/else` in the code.
- __write self-enclosed code__: split the software functionnality between modules (sub-programs) that communicate through the core and are isolated from each other. As the code volume grows, that guarantees that changes happening within modules will not have unforseen effects outside.

Finally, you have to evaluate if the new features you add are worth the extra maintainance cost. In the case of the shortcuts handler, we are catering to the "needs" (or rather, the luxury) of 2% of the user base with an implementation that is at (very) least twice as complicated as the one it replaced. Ponder that in your capitalistic mind…[^2]

[^2]: I don't see why the capitalistic mindset is ok when it comes to getting excited about new products, but gets boring when it reaches return on investment and hidden/sunk costs…

Code complexity can be measured using graph theory, through cyclomatic complexity, cognitive complexity, or N-path complexity. [Many research articles](https://en.wikipedia.org/wiki/Cyclomatic_complexity#Correlation_to_number_of_defects) correlate code volume and/or code complexity to the number of bugs hidden, which is pretty intuitive: the line of code you don't write is the only one in which you will never have bugs. __Simplicity is the goal__. And, as I found out the hard way, __GUI (front-end) complexity is often a direct consequence of back-end complexity__. There are many guys howling at the moon to conjure the mystical figure of the _UI/UX designer_, hoping to magically solve the terrible GUI of Darktable (complicated __and__ inconsistent). The GUI does not exist in parallel of the back-end, it only wires the input expected by the back-end to graphical controls. Simplifying the front-end cannot be done without simplying the back-end, this is just a stupid dogma made up by people having only soft skills. But simplifying back-ends is a lot more complicated than just drawing mockups.

As it turns out, what happened with the Great MIDI turducken failed on all 3 accounts: code volume, code complexity, code self-enclosedness. And Darktable developers will never learn from their mistakes, as is proven by Darktable 5.0. As keyboard shortcuts bugs piled up over time in Ansel (on top of features terrible by design), I tried to fix them in ways that avoided rewriting the whole thing, until it was pretty clear that I would not be able to avoid the rewrite for much longer.

This is called [technical debt](https://en.wikipedia.org/wiki/Technical_debt). The whole code of the Great MIDI turducken was meant to work once it was written, not to be long-term maintainable. It is basically a proof of concept that should never have made it into production. And the very proof of its unmaintainability is how much the code volume and complexity has grown from bugfix to bugfix, between Darktable 3.8 and 5.0, making it even more unmaintainable as bugs are fixed. This is the definition of gluing yourself into a spider web: the more you move, the more you get stuck.

My mistake is perhaps that I started to raise my critics of the Darktable (lack of) management in 2022, __after__ I got the proof that the disorganized pack of contributors would never recognize and learn from their mistakes. Although the self-inflicted and unsustainable work pace was more akin to create burn-out than reflection. Since then, Pascal Obry, the current self-appointed maintainer of Darktable, has been trying to convince everybody that I'm an unsufferable team member, unable to work with other persons who disagree with me, who started to insult everybody. Of course, no specific and technical answer has been given to the issues I rose in [_Darktable: crashing into the wall in slow-motion_](./darktable-dans-le-mur-au-ralenti/index.md), only vaguely reassuring blanket statements to the tune of "Darktable is an active project, healthy because it has many contributors". As if more disorganized monkeys would finally equal an engineer if you added enough of them.

I think I have been nothing but nice for 4 years – far too nice, in fact — until I realized how badly these guys harmed the project. They don't, and will never learn from their mistakes, because they won't even acknowledge them and the evolution of Darktable 5.0 is only confirming the trend. But having more and more weird, random and unreproduceable bugs pile up on the bug trackers is a pretty clear clue that something is deeply wrong. Especially as investigating those bugs led me to archeology into terrible shitstacks of code that was not even 2 years old, and __even though my knowledge and experience on Darktable code base grew over the years, my ability to fix the root cause of bugs decreased__ and I was facing more and more disconcerting haymazes of [code indirections](https://en.wikipedia.org/wiki/Indirection).[^9]

[^9]: I'm not talking here about the Darktable way of "fixing bugs" that consists into rushing on the visible manifestation of the bug, and adding a fourth level of nested `if` to take care of the pathological corner case. That's working around the cause of the bug by creating more technical debt, not actually fixing the root cause of the bug. And since that root cause will generally stem many visible manifestations, patching all the manifestations is actually more complicated on the long run, and leads to brittle code.

It's hard to explain all that to people who see computers as magic boxes designed by tech wizards. There is no magic in there. It's mostly math and applications. Many developers will confess being bad at math, which means they are bad at programming too. Because math is not just about running arithmetic operations, it's a whole discipline of the mind allowing to abstract complex problems in order to break them into simple problems, that will lead to simple code and simple GUI. Good programmers spend a lot of time thinking about how to write little code. Because code is a liability, code is technical debt, code is costly to maintain. So you pay the debt upfront, without interests, by thinking a lot of your code before you write it, instead of coding first and then spending years extinguishing fires.

All this also raises the question of who owns open-source code and open-source projects. Since the founder of the Darktable project, Hanatos, left the ship, as well as all the first generation of developers, for various reasons, the last standing man from the first generation appointed himself new maintainer. He is a very capable and skilled developer: his code is clean, tidy, and I have not, so far found any bug in a line where [git blame](https://git-scm.com/docs/git-blame/fr) said "Pascal Obry". But his politics regarding code management are terrible: he believes that every contribution is a good contribution, that contribution "momentum" should not be deterred, and is properly incapable of saying "no" to contributions, which means that close to all pull requests get merged. It's France in 1940: everything comes in, gets welcomed with a big smile and the Führer gets twice as many Jews as requested. Meanwhile, the resistants are called terrorists.

But there is a huge difference between the quality of the code that Pascal writes and the quality of the code he accepts and merges. This is a concerning paradox finding its roots between the [fear of missing out](https://en.wikipedia.org/wiki/Fear_of_missing_out) and [radical techno-positivism](https://en.wiktionary.org/wiki/technopositivism), which errs on side of dogma and beliefs, with a complete disregard for design and user perspective. And perhaps and exagerrated trust into the "Community"'s ability to fix bugs later.

There are many cases where doing nothing is better than doing it wrong, especially when you are replacing existing features. Darktable being a non-destructive code editor, you invest into it when you start a database of image edits. Unless you plan on exporting all your images to high-resolution, high-bit-depth files, as soon as you are done editing them, to never change them again. That creates a legitimate expectation of long-term stability and consistency, so your old edits can still be opened, brushed up and exported again, perhaps to new formats, perhaps to higher resolutions. Changing course in the application design is kind of a breach of contract, and even though the GNU/GPL license voids any legal responsibility, it doesn't void the damages on users. Nor does it refund the years of my life I lost to fix their shit.

So, even disregarding developer-centric issues of maintainance, there is also a discussion to have regarding who gets to decide how old and proven features get replaced, especially those basic and universal desktop applications features like handling files (import, export, browsing) or mouse & keyboard interaction, that have been so ubiquituous for so long that the 2020's are 30 years too late to pretend reinventing them.

A lot of work and man-hours were invested into making the keyboard shortcuts worse, for the sake of flawed politics and damaging dogmas, on top of whishful thinking and [social loafing](https://en.wikipedia.org/wiki/Social_loafing) where everybody hopes that _The Community®_ (aka _someone else_) will take care of fixing their own mistakes. Users also had to loose their keyboard configuration and were required to reset everything and relearn everything too. But doing it the right way would actually have cost less work and fewer man-hours. This is a self-feeding loop of madness, creating a toxic work environment where instability promotes more instability, where complexity promotes more complexity, again without any sort of features roadmap that would give a general direction and visibility to everybody involved.

## A brief history of bad design

It's only after I reconstructed the shortcuts feature from scratch that I understood what went wrong in Darktable shortcuts/accelerators.

At first, there is this crippling debauchery of features, which makes it tempting to declutter the GUI by simply hiding features, only to have them handled from the keyboard. The problem is then that such features are not always niche and optional (like the shortcut bypassing mask interactions when drag and dropping the main image preview in darkroom), but are definitely undiscoverable from users. Ansel solved this issue with the global menu.

Some features were hidden through a basic vimkeys support, if you start inputting `:`: `:q` will quit the application, `:set` followed by the name of the slider or combobox will change the value. This is of course documented nowhere in the Darktable manual, and as an user of Darktable for more than a decade, I had never heard of it before deleting its code, because this little joke listens to all your keystrokes to determine whether it should act on your typing or not.

But then, there is also the fact that modules use home-made Gtk widgets (named "Bauhaus", in `src/bauhaus/bauhaus.c`) that don't implement everything you would expect from a GUI widget capturing user events, especially not the accessibility features.

One of the most basic accessibility features is the ability to cycle through focusable widgets. In GUI (and Gtk) slang, a focusable widget is one that can capture key stroke events, once focused. Widgets will typically be focused once clicked, but Gtk also manages internally a [focus chain](https://docs.gtk.org/gtk3/method.Container.get_focus_chain.html) into which you navigate with the <kbd>Tab</kbd> and the arrow keys. The first problem is the <kbd>Tab</kbd> key, in Darktable, was mapped to the "preview" mode (toggle on/off all panels in the view). And in fact, using native Gtk accelerators, this would not have been possible at all, since <kbd>Tab</kbd> is mapped by Gtk and forbidden in user-defined shortcuts, but since Darktable implemented its own shortcut handler, even before the Great MIDI turducken, it was overwritten. So, the tabulation-based focusing chain cycling was disabled simply because the <kbd>Tab</kbd> key was assigned to something else. But the second problem was that the home-made Bauhaus widgets also captured all arrow key strokes. Thus navigating between controls in a sequential manner (next/previous) was also completely impossible, by design. Just like Bauhaus widgets captured all mouse scrolling events, preventing scrolling the sidepanels.

Because sequential/incremental navigation between focusable widgets was impossible by design, all keyboard interactions had necessarily to be made absolute: for each slider, for each combobox, you would have a shortcut increasing/decreasing/resetting the value mapped directly.

But more problems arose when modules were made multi-instanciable, because controls were identified by an accelerator path like `view/module/slider/increase` or `view/module/slider/decrease`, but all module instances would inherit the same path. That triggered a need to manage all that at runtime, with user preferences to decide whether the targetted module would be the first, last or last-interacted-with module.

Generalizing all that to MIDI and gamepads only made it worse, because on top of having one shortcut per possible action per widget, then there was a need to manage typical desktop interaction emulation from other input devices. But instead of managing the emulation layer high-level, at the interface between MIDI and regular keyboard/mouse shortcuts, a terrible developer totally over-engineered an abstraction layer of actions, incrusted into modules, native Gtk widgets (as an overlay), and home-made Bauhaus widgets (deeply incrusted). The problem is that abstraction layer wasn't really a layer but more like a metastatic tumor spreading everywhere. Removing it and [all its dependencies](https://github.com/aurelienpierreeng/ansel/commit/deb12ed816394c8c829eea0079ae8693901b52e2) led to the removal of 7674 lines spread over 163 files, even though it was supposed to be implemented in `src/gui/accelerators.c` (4412 lines of code, comments and whitelines).

## Redesigning keyboard shortcuts/accels from scratch

The zeroth-step of the redesign is those 2 simple requirements:

- Every action should be discoverable in the GUI, keyboard shortcuts are not meant to declutter the GUI ; decluttering the GUI is a matter of breaking the workflow into unit steps and presenting only the controls that matter to the current step.
- The software should be completely usable from the mouse alone and from the keyboard alone. Mixed interaction should be completely optional.

The first constraint of the redesign is therefore to make absolute shortcuts completely optional, thas is designing the keyboard workflow for a sequential/relative access (cycling between next/previous control).

### Relative keyboard navigation

The first piece of solution here is the global menu, that stores many buttons previously hidden into collapsible modules (and sometimes collapsible sections of collapsible modules, since the Darktable contributors have fallen in love with these since 2021). Removing these modules allowed to free the right sidebar in the lighttable, allocating more screen real-estate to the thumbnails. Menu entries have key mnemonics, that is when pressing <kbd>Alt</kbd>, one letter in their label will be underlined and, if pressed while keeping <kbd>Alt</kbd> down, will unfold the corresponding menu. Once a menu is unfolded, all menues can be navigated with arrow keys and the corresponding actions can be triggered with <kbd>Enter</kbd>.

__This is not only completely standard__, in terms of GUI paradigm, it also prevents from even having to setup and remember any shortcut. Cherry on the sunday, menues are native Gtk objects that add very little code overhead and are already wired to all typical accessibility methods, out of the box.

Since the <kbd>Tab</kbd> shortcut has also been detached from the full-preview feature, it is then available to move down the chain of focus, within Gtk native widgets, and I have extended the home-made Bauhaus widgets to handle focusing like any native Gtk widget.

In the darkroom view, the image-processing modules are organized in tabs that can be cycled back and forth with the typical <kbd>Ctrl+Tab</kbd> and <kbd>Ctrl+Shift+Tab</kbd>. Within a tab, individual modules can be browsed (unfolded and focused) up and down using <kbd>Page Up/Down</kbd>. Once a module is focused, cycling through its internal controls can be done with <kbd>Ctrl+Up/Down</kbd>, which supports tabs, such that, when you reach the last control of the visible tab and hit <kbd>Ctrl+Down</kbd>, the next tab will automatically be displayed and its first control will be focused. Home-made Bauhaus widgets and native Gtk widgets alike support the native Gtk <kbd>Up/Down</kbd> focusing (to the previous/next control), but this works only once one of the module's controls is focused and for the visible widgets. Which is why <kbd>Ctrl+Up/Down</kbd> was implemented for better robustness.

All this gives __focus__ to controls, meaning they allow them to record further key strokes. Focused controls are displayed in bold font in the GUI. Focused modules are also highlighted.

### Interaction with focused controls

The split paradigm "focus" then "interact" (which I didn't invent…) is very powerful because it allows to scope key strokes into the right context, which means the same
keystrokes (especially the <kbd>Up/Down/Right/Left</kbd> arrow keys) can be attached more than once in the GUI, and be handled differently depending on which context has the focus. This is more flexible and actually more simple[^10] than the Darktable obssession of wiring everything to global, absolute shortcuts which would then collide on often-reused keys, and lead to having to add more and more key modifiers as a work-around.

[^10]: Flexibility is usually the opposite of simplicity, so you have to enjoy when you can win on both fronts.

So far, navigating through controls only gave them focus. What about actual interaction ?

In the lighttable view, once the thumbtable grid is focused, navigation through thumbnails is done using typical arrow keys, <kbd>Page Up/Down</kbd>, etc. (see the [documentation](../doc/getting-started/keyboard) for all the details) and image selections can be done in various ways (batch, series, individually) from keyboard too.

In the darkroom view, changing values on sliders and comboboxes can be done using the arrow keys (possibly using <kbd>Shift</kbd> for coarse step or <kbd>Ctrl</kbd> for fine step), triggering the color-picker (on sliders supporting them), is done with <kbd>Insert</kbd>, etc. (again, read the doc for details).

So, again, so far no user-defined shortcut, no cryptic key combination to remember, it's mostly arrow keys and yet everything can be accessed.

### Absolute control focusing

That's all great, but it leaves you on a kind of Ikea trail when you need to access directly some area of the application but you have to navigate through all sections from the entry door.

To alleviate that, a very old Darktable feature was the "favourite modules" tab, aka a special tab that would duplicate the GUI of the most-used modules, based on user choice. That's essentially solving bloat with more bloat, and yet users have grown very fond of that. Though if we focus on the goal, rather than the mean, the requirement is a quick access to arbitrary modules, which is perfectly understandable.

So this was re-implemented as a way to define an absolute shortcut immediately focusing an image-processing module or any of its internal controls. Focusing an hidden module or control will automatically make it appear in the GUI. From there, further interaction is done exactly as before, using uniform key combinations. This reduces the number of shortcuts to configure by a lot, makes the whole thing more generic and the shortcuts settings window much more simple.

Absolute shortcuts can also target menu entries (aka global actions), in which case they will be recalled in the menu, next to the action label, which is again a native Gtk feature coming with zero overhead.

### Replacing MIDI controllers

If we leave aside all the hype of having dedicated controllers and feeling like an airplane pilot, the one thing that MIDI controllers have that keyboard and mouse will never have is the ability to attach potentiometers (rotating knobs) directly to GUI sliders. But then, the cost of that is having an extra device taking space (and dust) on your desktop, not to mention future electronic garbage. All the photographers I know who bought MIDI or Loupedecks stored them away "temporarily" to reclaim some destkop space… and never brought them back from storage.

Image-processing controls can be wired to single-key shortcuts, because Ansel uses a custom shortcuts overlay on top of native Gtk accelerator features. I have modified the home-made Bauhaus widgets such that, when hitting one of the absolute focusing shortcuts above and keeping it pressed, mouse wheel scroll will be directly mapped to the widget even if the mouse is not overlaying the proper slider.

So combining one-letter shortcuts to focus a control, with mouse (or touchpad) scroll, you get all the goodness of MIDI rotating knobs without the extra device, extra library for support and marvels of over-engineered emulation layers.

But all that still happens with the one and single absolute shortcut, so you don't have to define and remember several shortcuts per control, only to find yourself short of key combinations available.

### Shortcut-less actions, search engine and vimkeys-like triggers

My shortcut handler is a thin wrapper over Gtk native accelerators API. As such, an action is defined by a text path, like `Ansel/Darkroom/Modules/Exposure/Black level`, which is an unique identifier, readable both by a computer and by an human. The GUI declares a function attached to each of these pathes, containing the code to run to apply the corresponding action. Then the shortcut handler maps a key combination to this path.

So, whenever an user hits some keys, the shortcut handler looks up if we have a known path for this combination, and if it finds one, triggers the function attached to it. This is a dummy-proof design that knows nothing about the internal of Ansel modules, home-made widgets, etc. So it can be extended to many parts of the software without overhead.

But it's actually much more powerful than just that. Because by listing all known pathes, we can then return pathes matching a text search query (searching for a control, module or global menu entry by name), that is find all actions from a list, but then we can also trigger them even though they are not attached to any shortcut.

{{< figure src="actions-search.jpg">}}
The global actions search engine, which allows to lookup and trigger actions whether they are assigned a keys combination or not.
{{< /figure >}}

Since image-processing modules can also be found and displayed this way, this also replaces the module search engine, reclaiming some vertical space for long modules and their masking/blending options (and removing around 200 lines of code). From there, I extended it to all toolboxes and made it global, which means the action search engine works in all views but will only list the actions relevant to the current view.

In darkroom, it also supports module multi-instances, allowing to target directly a specific instance (more on that on the [documentation](../doc/getting-started/keyboard)).

By default, the global action search is mapped to the <kbd>Ctrl+P</kbd> shortcut, and can be accessed from the global menu _Help_, or with a _Search actions_ button in the center of the header bar. This also somewhat replaces the vimkeys that had a very partial support of GUI actions (and would have needed to entirely duplicate accelerators to extend it to an useful state), such that, instead of typing `:` followed by a command, you can type <kbd>Ctrl+P</kbd> and either type the path of the action, or start a query and pick from the list of matches (using arrow keys and then <kbd>Enter</kbd>).

Matches are sorted by decreasing relevance (top to bottom), and relevance is computed from the position of the match. The assumption here is that, action pathes being generic to specific from left to right (`view/module/control`), text matches happening on the end of the path are supposed to match controls rather than modules or views, which we consider to be more specific and therefore more relevant. This may avoid having to scroll down past all modules content that could match a text query searching for a control.

### Shortcuts editing window

Since there is now only one user-configurable shortcut per control, the GUI for listing and editing shortcuts is a simple tree:

{{< figure src="shortcuts-popup.jpg">}}
{{< /figure >}}

Suffice to double-click in the _Keys_ column to start recording a new key combination. Since it is so simple, this window also replaces the cheatsheet all at once. Shortcuts can be looked up by name of the action or by keys used, and the key search has auto-completion for key modifiers (details in the [documentation](../doc/getting-started/keyboard)). The shortcuts popup can be displayed from the global menu _Edit_ → _Keyboard shortcuts…_

### In-software documentation

Here is the thing: maintaining an up-to-date documentation on default shortcuts is an hassle, because it's too fine-grained. This is the kind of document that will be outdated by the time you finish writing it. Since default shortcuts have to be implemented in the application anyway, the best place to document them is straight within, so updating is automatic.

Darktable had 2 redundant shortcuts GUI, one to setup shortcuts, and another "cheatsheet" kind of popup (which, for a long time, was accessible only through… a shortcut. So much for discoverability). The reason was that the settings popup was way too cluttered to be used as any kind of quick reminder. Then, shortcuts were added in the tooltips of some controls… tooltips appearing only when hovering the controls with the mouse. How non-sensical is it that you should use your mouse to discover how to use the keyboard on a case-by-case basis ?

Ansel has shortcuts (key combinations) written in the settings popup, and also recalled on the global action search. But those shortcuts are not limited to the ones that are user-configurable, I extended the shortcuts handler with "virtual shortcuts", that is shortcuts basically mapped to no action but declared just like the rest, and appearing among the rest as "locked" shortcuts.

{{< figure src="shortcuts-doc.jpg" />}}

Those shortcuts described as _contextual interaction on focus_ document the generic actions targeting the focused control, whether the control was focused in a relative or absolute way.

### Implementation details

The source code for the whole shortcuts handler system, including the GUI bits (global actions search and shortcut editing popup), uses __1144__ lines of code, half of them being the GUI, for a cyclomatic complexity of [216](https://sonarcloud.io/component_measures?metric=complexity&selected=aurelienpierreeng_ansel%3Asrc%2Fgui%2Faccelerators.c&view=list&id=aurelienpierreeng_ansel). That's a __fifth of the code volume for a sixth of the complexity__ (compared to Darktable 5.0), even though it provides additional features (keys search with auto-completion, global search, explicit module instances targets, etc.). MIDI support has been dropped because, quite frankly, I don't see what problem it solves that is not already solved by the current simpler design.

Finding the action attached to a key stroke takes around a dozen of nanoseconds, where the Great MIDI turducken took 10 to 50 milliseconds on each stroke.[^15]

[^15]: Remember that the shortcut handler has to listen to __all__ keystrokes before deciding if it's supposed to do something with them or discard them, so this part of the software runs all the time for all users, whether or not they actually use shortcuts.

The dependency graph of the feature can be seen on [the developer documentation](https://dev.ansel.photos/accelerators_8h.html), it is much cleaner than the previous spaghetti bowl. The rest of Ansel GUI code interacts with the shortcuts handler by declaring new accelerator pathes, recording callback functions attached to them, and possibly default shortcuts, all that using a single method from the API. The shortcuts handling is unaware and immune to Ansel internals, in particular it doesn't know anything about modules or home-made Bauhaus widgets, so the design is entirely self-enclosed. Preferences are stored per-language in `~/.config/ansel/keyboardrc-LANG`, using a native [Gtk accelerators map](https://docs.gtk.org/gtk3/type_func.AccelMap.save.html). The API is [fully documented](https://dev.ansel.photos/accelerators_8h.html#details) on Ansel developer documentation for future maintainance and extension, so no reverse-engineering will be needed.

{{<figure src="accels-h-dependency.jpg" />}}
{{<figure src="accels-c-dependency.jpg" />}}


This is how I work because I don't program for fun. I have no fun programming. I solve problems, trying not to create new ones.

## Conclusion

This is the poster case of everything that went wrong in Darktable in terms of relentless over-engineering and how it should have been fixed. The proposed solution here is better because:

1. the whole GUI can be navigated from keyboard without having to remember a single shortcut,
2. controls, modules and other actions have only one direct, absolute user-configurable shorcut, that directly triggers the action or focuses the control widget if any, which makes the settings GUI a lot less overwhelming and removes the need for an additional cheatsheet popup,
3. actions are globally searchable and triggerable, whether or not they are tied to a combination of keys,
4. Key+scroll mixed interactions can be triggered at no additional cost, emulating MIDI rotating knobs and sliders with no need for extra hardware,
5. The code is objectively 5 to 6 times simpler (depending on which metric you consider),
6. The whole thing is fully documented and heavily commented in the code,
7. Any future brain-dead idiot able to read C will be able to maintain this thing, which anyway shouldn't need much maintainance because it does nothing clever and lives outside of the core of the software.
---

# Usability : https://ansel.photos/en/tags/usability/
---

# Announcement : https://ansel.photos/en/tags/announcement/
---

# Welcome Ansel GPT ! : https://ansel.photos/en/news/welcome-ansel-gpt/

After I finally [wired the whole website and docs](../contribute/translating.md) to a water-tight translation workflow (using po4a on top of Hugo), which happens to use the exact same toolset and logic as the Ansel application, I got the idea of [automating empty translations](../contribute/translating.md#auto-tools-and-helper-scripts), first from the software translation files, then through ChatGPT API, which does a very fair job at translating Markdown syntax.

Working alone, you can't rely on [social loafing](https://en.wikipedia.org/wiki/Social_loafing), so you have to be clever. You can see [the list of things I have already automated](../contribute/workflows.md) in background for Ansel.

Having written a whole [AI search engine myself](dev-diary-2.md), which means mostly a web crawler (because that was the most tedious part to write and debug), I got the opportunity to see this website from the eyes of a bot. Piece by piece, I modified the HTML templates and metadata to make it easier to crawl, index and search, first for the internal search engine,[^1] then for [Chantal AI](https://chantal.aurelienpierre.com).

[^1]: Which actually works client-side in Javascript in your own browser, meaning it works offline too, should you install this website as a webapp to keep the documentation locally.

Even with Chantal, I still get too many recurring questions regarding information that is already written somewhere. Sure, the documentation is a bit lagging behind the code, but the [commit messages](https://github.com/aurelienpierreeng/ansel/commits/master/) I write aim at being non-technical enough for power-users to understand what's going on these days in the software.

I get that there is nothing worse than being trapped in your problem without a timely answer, but then I can't possibly serve as help-desk for every guy on every timezone with everything that needs cleanup in this stupid software. Which means there is still a gap to fill.

The main drawback of Chantal is the language model is quite heavy to retrain, and I can't possibly automate it on some server. Then, websites can't be crawled too fast without being blocked by servers, so that doesn't take too much power but that requires some computer to be plugged-in for a week with a stable internet connection. Which means I update the language model and the web index only 4 times a year. As of now, the web index contains 63.452 pages, the language model knows 47.579 words, and I have finally managed to make it fairly compliant with the memory I/O limitations of a shared hosting.

The plan, right now, is to automate website crawling on some server, because that's not too heavy, then retrain the model overnight on my own computer (which takes around 4 h of computation and almost all of my 32 GB of RAM…).

Anyway, during my fiddling with ChatGPT API, I discovered that you can train your own custom GPT.[^2] This is as simple as feeding it text content it can use, and now ChatGPT can also load websites, sitemaps and can be configured to send requests to Rest API. So, without further ado [__meet Ansel GPT__](https://chatgpt.com/g/g-680d2f861a608191a0f7549eadd40f2e-ansel-gpt).

[^2]: Provided you are a ChatGPT Plus subscriber, for a not-so-modest 23€/month.

Ansel GPT is configured to cache and update once a week Github issues, commits, community forum posts, and all of the present website. It is able to provide complex (and fairly accurate) answers regarding what module to use, how and when, in Ansel, as well as on color theory concepts. I also configured it to use Chantal AI backend, which is (almost) a Rest API and the GPT will follow the links indexed by Chantal to improve its answers.

The caveat is using Ansel GPT is reserved to ChatGPT Plus subscribers, so I feel like I worked 2 days to configure everything so OpenAI can bank on my work. But anyway, if that's at least less work for me on the semi-long run, let's call it a win.
---

# Automated workflows : https://ansel.photos/en/contribute/workflows/

Because Ansel is mostly a one-person operation, everything that could be automated was automated. This page keeps track of everything that should be maintained in the future, and where.

## Software

### Nightly builds

Nightly builds prepare a compiled and packaged version of the software, every night at 6am UTC, for:

- [Linux](https://github.com/aurelienpierreeng/ansel/blob/master/.github/workflows/lin-nightly.yml),
- [Windows](https://github.com/aurelienpierreeng/ansel/blob/master/.github/workflows/win-nightly.yml),
- MacOS is currently disabled.

The newest binary files are automatically added to the [pre-release](https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0) assets, are posted on a [Matrix channel](https://matrix.to/#/#ansel-builds:matrix.org) so users can subscribe to updates, and can be downloaded through (constantly up-to-date) permalinks at:

- <https://nightly.link/aurelienpierreeng/ansel/workflows/lin-nightly/master/ansel.stable.AppImage.zip> for the Linux AppImage,
- <https://nightly.link/aurelienpierreeng/ansel/workflows/win-nightly/master/ansel.stable.win64.zip> for the Windows AppImage.

### Developer documentation

The dev docs are [automatically built](https://github.com/aurelienpierreeng/ansel/blob/master/.github/workflows/docs.yml) from the source code with Doxygen, every Sunday at 00:00 UTC, then uploaded to <https://dev.ansel.photos>, which is hosted on Github Pages attached to the Ansel (software) repository.

### Commits and issues

Github new commits and issues are [automatically posted](https://github.com/aurelienpierreeng/ansel/blob/master/.github/workflows/matrix.yml) to a [Matrix channel](https://matrix.to/#/#ansel-dev:matrix.org) for updates.

## Website

The Hugo static website is [automatically built](https://github.com/aurelienpierreeng/ansel-website/blob/master/.github/workflows/hugo.yml) on every new commit to the source code and ever Sunday at 00:00 UTC. This auto-update is intended for [camera support](https://github.com/aurelienpierreeng/ansel-website/blob/master/themes/ansel/layouts/shortcodes/rawspeed.html) which re-parses Libraw, Rawspeed and Ansel source code straight from Github, and dynamically generates the camera support table. It is uploaded to <https://ansel.photos>, which is hosted on Github Pages attached te the Ansel Website repository.

## Documentation

The documentation is imported into the website as a Go/Git module ([see _website_](./website/index.md)). Every new commit to the documentation [fires a workflow dispatch](https://github.com/aurelienpierreeng/ansel-doc/blob/master/.github/workflows/hugo.yml) to the main website to rebuild and update it.

## Forum

New posts on the forum are posted through an RSS bot to the [Matrix channel](https://matrix.to/#/#ansel-dev:matrix.org) for updates.

## Chantal

The page crawler for Chantal search engine database does not yet run automated and needs manual updating.

Converting web pages to their vector representation, through the language model, will probably remain too heavy for any public server and will need to be done on a powerful private server.

## Ansel GPT

The [custom ChatGPT model for Ansel](https://chatgpt.com/g/g-680d2f861a608191a0f7549eadd40f2e-ansel-gpt) is meant to assist users who have questions on Ansel software (installation, compilation, usage) or color theory. It is trained with Ansel website, documentation, Github issues, Matrix chats, and other resources. It also uses Chantal AI JSON API as backend, from where it can tap into the database of 68.800+ imaging-related pages indexed there.

It is configured to automatically recrawl and cache, once a week, all the following resources for all languages:

- [the main website sitemap](https://ansel.photos/sitemap.xml),
- each [language-centric sitemap](https://ansel.photos/en/sitemap.xml),
- the [one-page aggregated website](https://ansel.photos/en/index.md) content for each language,
- Github RSS feed of new commits,
- Github issues,
- the main Community forum RSS feed.

Users can request to see the last update log by asking "show me the last update log" to GPT.
---

# Websites : https://ansel.photos/en/contribute/website/

Ansel website is built using [Hugo 0.146](https://github.com/gohugoio/hugo/releases/tag/v0.146.7). This is a static website generator that allows to build very fast website from Markdown files. A custom template and a lot of custom shortcodes have been built for Ansel. You will need to [install Hugo extended version](https://gohugo.io/installation/) on your computer first, although minor changes can be made directly to the Markdown files without building the whole website.

## Getting the source code

### This website

```bash
$ git clone https://github.com/aurelienpierreeng/ansel-website
# Stored for example in /home/user/dev/ansel-website
$ cd ansel-website
$ sh build-modules.sh
```

### Ansel doc

Ansel Doc is an important part of the Ansel website, but since it's under a different license and forked from GNU/GPL dtdocs, it can't be on this repo. We want to edit both as a pack but we need to be able to commit them separately on different repositories. Here is the solution.

Ansel doc is fetched automatically as a module of this website on your disk as part of the `build-modules.sh` script above, which also auto-generates the translated pages through `.po` files. You will find it in the local folder of the website, under `_vendor/github.com/aurelienpierreeng/ansel-doc/`. No file should be manually edited there, this is only for auto-generated content.

To edit Ansel docs, do

```bash
$ git clone https://github.com/aurelienpierreeng/ansel-doc
# Stored for example in /home/user/dev/ansel-doc
$ cd ansel-doc
```

And then, edit the (English) content of `ansel-doc/content`.

## Interactive editing/Live preview

### Start the development server

Hugo lets you open a rendered version of the website, on a local development server, to preview your changes into your web browser.

If you only want to edit this website, run from `./ansel-website` directory:

```bash
hugo server --disableFastRender
```

If you want to edit the docs and see the results in realtime as part of this website, after you cloned the docs (see previous step), run from `./ansel-website` directory:

```bash
env HUGO_MODULE_REPLACEMENTS="github.com/aurelienpierreeng/ansel-doc -> ../../ansel-doc/" hugo server --disableFastRender
```

This trick will dynamically load the docs module from your local folder rather than from Github, which means the local changes done to the docs will immediately appear into the main website through your development server.

### Updating websites translations

See [Translating](../translating.md).

## Editing files

### Open Obsidian

Open `./ansel-website/content` as an Obsidian vault. Obsidian is able to resolve folder symlinks as if they were local folders, so we basically see the website as a whole, which makes it easier to make internal links between doc and website in the editor.

Working in Obsidian is significantly nicer than working in VS Code to edit "text" text (as opposed to code text in monospace), since the editor is less bloated and monospace fonts are eye-straining after a couple of hours for full paragraphs.

- Hugo Markdown doesn't support [Obsidian call-outs](https://help.obsidian.md/How+to/Use+callouts). You have to use the alerts boxes shown above as Hugo shortcodes, but they will not render in Obsidian,
- Hugo doesn't support [Obsidian wikilinks](https://help.obsidian.md/How+to/Internal+link), so you will have to stick to usual Markdown links with relative pathes. That said, Obsidian provides paths auto-completion for those.
- Obsidian doesn't support [Markdown definition lists](https://www.markdownguide.org/extended-syntax/#definition-lists), but you can still use them (they will just not be rendered in previews),
- Obsidian doesn't support [Markdown headings IDs](https://www.markdownguide.org/extended-syntax/#heading-ids),

However:

- Obsidian supports Hugo tags in Yaml frontmatter, and implements them in a much nicer way that gives a lot more sense to horizontal content linking,
- Obsidian supports Hugo [aliases](https://help.obsidian.md/How+to/Add+aliases+to+note) for pages redirection,

### Dealing with broken links

There is an extension for that : https://github.com/graydon/obsidian-dangling-links. Once installed, it shows the links that point to no existing file throughout the whole vault, including main website and doc :

![](obsidian-screenshots-broken-links.jpg)

In the node graph view, broken links also appear by their path `../../stuff.md` instead of appearing by their filename.

In each page editor, it is possible to see what pages are linking the page currently opened, including the headings anchor, which is useful before changing headings and therefore destroying internal links :

![](obsidian-screenshots-external-links.jpg)


### Improving content meshing

Horizontal linking, through tags and internal links, is just as important as vertical linking, following hierarchical trees.

Obsidian can show the vault-wise available tags for reuse:

![](obsidian-screenshots-available-tags.jpg)

It can also show the best candidate internal links for each keyword in the page, under the "Unlinked mentions" collapsible:

![](obsidian-screenshots-available-links.jpg)


### Checking content organization

It's often hard to follow the chaining of headings in a Markdown page, when using a typical code editor. Obsidian has an "outline" widget that allows to keep the table of contents in sight when writing, to ensure the hierarchy of headings is consistent :

![](obsidian-screenshots-document-outline.jpg)


## Guidelines

The documentation is not a manual or a course. It should answer the questions :

- "what is the GUI doing ?"
- "how can I configure the software ?"
- "what are the bottlenecks, caveats, limitations and traps ?".

The documentation expects the reader to know **what** to do and will explain **how** to do it. Start-to-end workflows, tutorials, scientific background etc., aka the **what** and the **why** go on the website ([resources](../../resources/_index.md), [workflows](../../workflows/_index.md)).

### Content folder

The content folder is located in the `content/` directory and the structure of folders and sub-folders will produce the structure of sections and sub-sections on the website. Files are written in Markdown and end with `.md` extension. Each file should have the following header (frontmatter):

```yaml
---
title: This page title
date: 2022-12-04T02:19:02+01:00
lastmod: 2022-12-31
draft: false
weight: 120
tags:
    - color science
    - pipeline
---
```

- The title is mandatory. Please use initial capitals, like in real language.
- The date is set once for all at the creation of the page and should never change after.
- Update the `lastmod` date with today's date everytime you update a file, and add it if not present. On the internet, every content is perishable and this helps readers to guess wether the page is still relevant at the time of reading or not.
- `draft` set to `true` means the page will be on the repository (in the source code) but will not appear on the website front-end. Set to `false`, the page is visible on front-end.
- `tags` are optional but welcome. The content is, by default, organized vertically (hierarchically). Tags help to create horizontal (thematic) links between pages. Relevant tags could be "film processing", "HDR", "monochrome", etc. Reuse existing tags in priority. Tags should always be a list, even when there is only one (otherwise Hugo build breaks).


### Internal links

Internal links should use relative paths from the current file whenever possible, which is not Hugo default behaviour. The purpose is to be able to follow relative links on the local file system from any modern text editor, like in any `README.md` file. We use our own code to reconnect those relative links to files with Hugo website structure (after compilation).

When building the website, internal links are checked and a critical error (aborting compilation) will be thrown if a page can't be found from internal links. You need to watch them out. Non-critical errors (aka `WARNING [languages] REF_NOT_FOUND`) can also be shown in a console because we twist the Hugo linker in a non-standard way, but those can be disregarded.

Example of internal links:

```markdown
[Filmic](../../module-reference/processing-modules/filmic-rgb.md)
[Filmic](./filmic-rgb.md)
[Some page](./section/index.md)
```

#### Page anchors

If you make links to page anchors, like `/my-post.md#some-heading`, be sure to __not__ insert a slash `/` between the page slug and the hashtag `#` in your Markdown code, or else the file will be taken for a directory and not found.

#### Absolute links

Say you want to link to the [support](../../support.md) page. Here are all the possibilitie to create a link to that page:

- `[support](/support/)` -> valid for Hugo, but works only after the website is compiled, so it can't be debugged easily in code editor. __please avoid__
- `[support](/support.md)` -> invalid for Hugo, will work as a side-effect of our custom link processing, but can't be debugged at all in code editor. __please avoid__
- `[support](./support.md)` -> invalid for Hugo, works as intended by our custom link processing if called from the index page, for example. __please use this__
- `[support](../support.md)` -> invalid for Hugo, works as intended by our custom link processing if called from a subfolder of the site, like `/contribute`. __please use this__

If a page is in what Hugo calls a [page or section bundle](https://gohugo.io/content-management/page-bundles/), please use the link to its `index.md` or `_index.md` file.

Do:
:   ```markdown
    [user manual](/doc/_index.md)
    [user manual](./doc/_index.md)
    [user manual](../../doc/_index.md)
    ```

Don't (even if it technically works):
:   ```markdown
    [user manual](/doc)
    [user manual](/doc/)
    ```


### External links

External links are not checked because that would take too much time at building, and building may happen without network access anyway. Always use `https://` in the external URLs when possible.

### Titles (headings)

H1 titles (encoded `# Title` in Markdown) are reserved for page titles and each page should have exactly one H1. darktable-doc messed up big time here by using H1 as section titles, this is both a SEO and an accessibility mistake. The web is semantic because it's designed for crawlers and screen readers just as much as for humans.

Be aware that Hugo automatically generates anchor links for headings, using the text of the heading. Thus, refrain from using symbols in headings, especially (back)slashes, which will mess-up the anchor links.

Also be aware that these headings anchors may be used in other pages to make direct links. Changing the text of an heading will break its anchor and may break external links. To avoid breaking anchors in external links, you can change the heading text but force their ID to the previous, like so:

```markdown
### My New Heading {#my-old-heading}
```

This will preserve external links to `/my-post/#my-old-heading`. [See the details…](https://www.markdownguide.org/extended-syntax/#heading-ids)

### Embedding images

Hugo treats images as page assets. There are global assets, for images reused on several pages, stored in an `assets/` subfolder of the main source code folder, and local assets, stored in the same folder as the page using them.

As for internal links, everything needs to be linked relatively to the source code as hosted on the local filesystem, not relatively to the compiled HTML.

See [how to translate images](../translating.md#translating-images).

### Screenshots

Screenshots are the basics of any front-end software documentation. The darktable-doc maintainers refuse them on the ground that they can't be translated and they will be soon be obsolete given the frequency of GUI hanges, but it's a huge pedagogic mistake. Even in the wrong language, screenshots help to see what to look for in the window. Use them. They will get obsolete and may not be translated, just as the rest of the text.

### Info, Warnings, Alerts

The Ansel theme from the main website provides shortcodes to create alerts and info boxes using Hugo templating system. Here is the code:

```html
{{</* warning */>}}
This is where you say what users should be aware of, because what may sound like a good idea in general may be really bad in some circumstances.
{{</* /warning */>}}
```

```html
{{</* note */>}}
Your side note here.
{{</* /note */>}}
```

```html
{{</* advice */>}}
Your friendly advice here.
{{</* /advice */>}}
```

```html
{{</* danger */>}}
This is where you remind users that they are free to do shit but there will be consequences.
{{</* /danger */>}}
```

The content of the boxes can use Markdown too.


### Before/after sliders

Again, using Hugo templating system, you can display before/after sliders where both images are overlayed. This resembles the Ansel & darktable darkroom snapshot feature and can efficiently explain the effect of modules and settings in a way that users can reproduce in the GUI. Both before and after images need to have the same size in pixels.

```html
{{</* compare after="./img-after.jpg" before="./img-before.jpg" */>}}
Your slider legend goes here.
{{</* /compare */>}}
```

### Maths

The docs support MathJax configured for LaTeX syntax support. Though the goal is not to write scientific litterature, there are some algorithms made of multiplications and additions that are more easily shown as equations rather than writing blocks of texts.

Inline LaTeX should be enclosed in `$`, block equations enclosed in `$$`. If you use LaTeX, you need to notify Hugo to append the Mathjax script on the page by setting `latex: true` in the header/frontmatter of the Markdown page.

### Mermaid graphs

Ansel makes an heavy use of pipelines, and those are best described with flowcharts. Mermaid.js is now supported natively on Github and within Visual Studio Code and is great for that purpose. You can [try it visually here](https://mermaid.live/edit#pako:eNpVkE1rhDAQhv9KmFML-gc8FLra7sXSQvdU9TDVcRPWfBBHlkX97002FNqcQp73mQzvCr0dCAo4e3RSnKrWiHCem1J6NbPGuRN5_rQdiYW2hm6bODwcrZildU6Z82PKH2JIlGsdYyRYKnPZEyrv_ruhTVRNjY6t6_6S09Vu4qVRHzKM_0-kp2C9NiMWI-Y9elGi7yADTV6jGsLaaxRaYEmaWijC1dDCHqcWsoTGyV57iZ4jXtP8FiTrqcZvmuZksV-CH2lYew9f4ML282Z6KCLKYHEDMlUKQ1H695EGxda_pQbvRWbg0HxZGyIjTjPtPxIKcek) and copy-paste the code of the graphs within Markdown code blocks like this:

```markdown
    ```mermaid
    graph TD
        A[Christmas] -->|Get money| B(Go shopping)
        B --> C{Let me think}
        C -->|One| D[Laptop]
        C -->|Two| E[iPhone]
        C -->|Three| F[fa:fa-car Car]
    ```
```

This renders:

```mermaid
graph TD
    A[Christmas] -->|Get money| B(Go shopping)
    B --> C{Let me think}
    C -->|One| D[Laptop]
    C -->|Two| E[iPhone]
    C -->|Three| F[fa:fa-car Car]
```

Icons from [Font Awesome v5](https://fontawesome.com/v5/search?o=r&m=free&s=solid) are supported by Ansel main website and documentation, using the syntax `fa:fa-YOUR-ICON-CODE` as shown in the example above. Use [Font Awesome v5 search engine](https://fontawesome.com/v5/search?o=r&m=free&s=solid) to get the `fa-` code of the icons you may use.

Mermaid graphs are rendered client-side in SVG at display size and can be translated as text. Hugo is configured to detect these graphs automatically and load the javascript library only when needed. Github can also render Mermaid graphs natively, when displaying Markdown files.


### Changing pages URL

Sometimes, it makes sense to reorganize the content and to changes the path of some pages. To not break external links, you must record the old url of the new page as an alias, in the frontmatter of the new page like so:

```toml
aliases:
    - /my-old-url/
    - /another-even-older-url
```

## Notes

### RSS feed

The documentation has an RSS localized feed, for now :

- https://ansel.photos/en/doc/index.xml,
- https://ansel.photos/fr/doc/index.xml.

This is unusual and is meant to help users keep track of changes and evolutions, by subscribing to the RSS feed or by connecting it with bots.

The date of the documentation pages set in the RSS feed is the `lastmod` parameter, aka the time of last modification. Since RSS doesn't have a `jast modified date`, it's the best I have found for the time being.
---

# Supported cameras and formats : https://ansel.photos/en/resources/supported-cameras/

This page lists all the known cameras and whether or not their raw files are decoded by Ansel, as well as all file types supported in input and output.

## Cameras

### Introduction

Some cameras may use different encoding formats (12 bits, 14 bits, sRAW, compressed or not) and image ratios (4:3, 16:9, 3:2). Because one or more of these formats is supported doesn't imply they all are. Different cameras may share the same sensor and electronics, even though their commercial name is different: those will be found in the _aliases_ columns.

Ansel uses [Rawspeed](https://darktable-org.github.io/rawspeed/) library to decode most raw files. Libraw fallbacks have been introduced because Rawspeed still does not support `.CR3` Canon files (ISOBMFF format). Libraw can also be [manually configured](../../doc/preferences-settings/processing/#libraw) to always load some files, by camera or by extension. Rawspeed support is native and complete in Ansel, whereas Libraw is not fully wired to the application.

__The Ansel project does not have any control over the list of supported cameras and file formats, by Rawspeed or by Libraw__. The Ansel application handles pixels after they are decoded by Rawspeed or Libraw, and metadata (EXIF, IPTC, XMP) after they are decoded by [Exiv2](https://exiv2.org/). Decoding problems must be reported to Rawspeed, Libraw and Exiv2 projects, depending on their nature.

Noise profiles are used by the [_denoise (profiled)_](../../doc/views/darkroom/modules/denoise-profiled) module. Cameras without noise profiles will still be usable, only denoising at high ISO might be of subpar quality because it will use generic noise stats.

### Raw file formats support

Ansel accepts raw file formats among the following list: `3FR, ARI, ARW, BAY, BMQ, CAP, CINE, CR2, CR3, CRW, CS1, DC2, DCR, DNG, GPR, ERF, FFF, EXR, IA, IIQ, JPEG, JPG, K25, KC2, KDC, MDC, MEF, MOS, MRW, NEF, NRW, ORF, PEF, PFM, PNG, PXN, QTK, RAF, RAW, RDC, RW1, RW2, SR2, SRF, SRW, STI, TIF, TIFF, X3F`. This means that the application will allow [importing](../doc/getting-started/import.md) them. To be completely supported (read, decoded), either Rawspeed or Libraw, and Exiv2 will need to support them too.

### Camera support table

Support legend:

- <span class='badge rounded-circle text-bg-success square-badge'>✓</span> Camera and format supported by Rawspeed. In case of problems, submit [bug reports](https://github.com/darktable-org/rawspeed/issues).
- <span class='badge rounded-circle text-bg-danger square-badge'>✗</span> Camera and format unsupported by Rawspeed and Libraw,
- <span class='badge rounded-circle text-bg-warning square-badge'>?</span> Support state unknown because raw samples are missing (see [get your camera supported](#get-your-camera-supported))
- <span class='badge rounded-circle text-bg-info square-badge'>-</span> Camera and format supported by Libraw fallback. This fallback can be higher or lower quality.

Support quality legend:

- 🏆 fully-supported camera,
- 🥈 partially-supported camera, usable with minor compromises,
- 🥉 partially-supported camera, usable with compromises,
- ⁉️ partially-supported camera, practical usability impossible to estimate,
- 💩 unsupported camera.

_This table is automatically generated from parsing Rawspeed, Libraw and Ansel source code. No human verification has been made_.

{{< rawspeed >}}

### Get your camera supported

Users who are not able to perform command-line operations in a terminal will be unable to help getting their cameras supported. They should buy cameras currently supported or stick to proprietary software. There is no guaranty that a brand-new camera will one day be supported, and no indication regarding when it will be. Camera support is a entirely based on an unpaid community effort, limited by everyone's availability.

#### The case of Canon CR3 / ISOBMFF files

Canon `.CR3` files belong to a new type of ISOBMFF containers. These require specific decoders, from Rawspeed, Libraw and Exiv2. This specific decoder is not available in Rawspeed, so _temporary_ and partial support for Libraw has been introduced in Darktable circa 2020. Ansel inherit it, but, as of 2025, Rawspeed still does not support `.CR3`.

But Ansel requires more than Libraw to actually handle `.CR3` : it needs Exiv2 built with ISOBMFF support too, for metadata. ISOBMFF support is optional in Exiv2 for (mostly invented) copyright & legal reasons, and some Linux distributions (Fedora) package this library with ISOBMFF disabled. Users that use Ansel pre-built packages from such distributions, or who build it using the Exiv2 library from the distribution repository, will never have complete `.CR3` support.

#### Helping support

If your camera is not or partially supported:

- upload test raw files to <https://raw.pixls.us>,
- open a bug report [on Rawspeed tracker](https://github.com/darktable-org/rawspeed/issues) and [on Libraw tracker](https://github.com/LibRaw/LibRaw/issues/608).

If your camera does not have noise profiles, you can [generate them yourself](https://pixls.us/articles/how-to-create-camera-noise-profiles-for-darktable/) and submit them to Ansel bug tracker.

## Non-raw codecs

Ansel supports the following file formats and extensions (while reading and writing):

- JPEG: `.jpg`, `.jpeg` (mandatory),
- PNG: `.png` (mandatory):
    - 8/16 bits
    - transparent background unsupported,
- HDR: `.pfm` (Portable Float Map format) and `.hdr` (RGBE format) (mandatory),
- TIFF: `.tif`, `.tiff` (mandatory) :
    - 8/16 bits integer,
    - 16/32 bits floating point,
    - supports exporting blending masks into layers,
- OpenEXR: `.exr` (optional),
- WebP: `.wepb` (optional),
- AVIF: `.avif` (optional),
- HEIF: `.heif`, `.heic`, `.hif` (optional),
- JPEG2000: `.j2c`, `.j2k`, `.jp2`, `.jpc` (optional),
- Through GraphicsMagick/ImageMagick: `.gif`, `.bmp`, `.dcm`, `.jng`, `.miff`, `.mng`, `.pbm`, `.ppm`, `.pgm` (optional)

Note that the optional formats are enabled only if Ansel is built with the corresponding options enabled and if the libraries providing the codecs are found on the system building it. Pre-built binaries provided by the Ansel project enable them all but third-party packagers may choose otherwise.
---

# Film : https://ansel.photos/en/tags/film/
---

# Film scanning : https://ansel.photos/en/workflows/film-scan/

Alain Oguse learned photographic printing with [Claudine](https://www.musee-orsay.fr/fr/ressources/repertoire-artistes-personnalites/claudine-sudre-211535) and [Jean-Pierre Sudre](https://en.wikipedia.org/wiki/Jean-Pierre_Sudre) in the late 1960's, and spent his early career in commercial photography. After retiring, he started to investigate how to bring back the photographic (silver halide) grain in digital scans of film negatives, finding the same sharpness and quality he had with near-point light enlargers in the 1970's.

The point light printing technique uses a very tiny source of light that gives a very precise and detailed reproduction of B&W film negatives, as opposed to diffuse lighting. It is very demanding, as its unforgiving sharpness and contrast do not hide scratches and dust on the film surface. Prints done this way would often need manual (painted) corrections on paper, inducing more work and more costs. By the end of the 1970's, it was usually replaced by diffuse light… better at hiding manipulation mistakes and at maximizing print labs profits.

But cleaning up negatives is no issue once they are digitized, and on this topic, Alain and I share the same values : digital imaging should augment the possibilities offered to photographers, building on top of the analog legacy, instead of settling down for what is easy and fast while trying to re-invent photography as if it was born digital.

As an early Ansel user, Alain contacted me to get help on tuning the [_Diffuse or sharpen_](../doc/views/darkroom/modules/diffuse/) module to dial up or down the photographic grain in a way that closely reproduces the impact of the enlarger light source quality (point or diffused) on the final print, starting with a DSLR scan. After all, light diffusion is what happens here.[^1]

[^1]: Though it should be mentionned that _diffuse or sharpen_ uses thermal diffusion models ([Fourier heat equation](https://en.wikipedia.org/wiki/Heat_equation)) in [wavelets space](https://en.wikipedia.org/wiki/Wavelet). This equation can also model particle diffusion, and its fundamental solution can be identified as a convolution with a Gaussian function (aka producing here a very computationnaly-expensive Gaussian blur with the right settings). But since we apply it in wavelets space and we don't do it at the photon level, I cannot in good faith claim physical accuracy here, with regard to light diffusion. It's rather _physically-inspired_ generalized diffusion.

But he also built a complete apparatus to achieve the initial film scan and was kind enough to document and illustrate all of his process, from preparation to post-processing, translate it into English and allow me to publish it here. You get for free the very finest of what open-source has to give you:

- 50 years of experience from Alain,
- real-life examples and results,
- optical explanations of what's going on,
- complete schematics of the scanning apparatus in point light setting,
- physically-accurate modules from Ansel/Darktable pixel pipeline, and presets for _demosaicing_, _input color profile_ and _diffuse or sharpen_ modules to dial the grain up or down at post-processing,
- thoughts on the work and responsibility of a print lab technician, regarding heritage conservancy and exhibition quality.

_Please note that Alain is a French speaker and this book was mostly translated using machine translation. If you understand French enough, you might want to read the original version._

<object data="https://bw-film-scanning.oguse.fr/document/point-light-article-revised-edition-2026-en.pdf" type="application/pdf" width="100%" height="900px">
  <p>Unable to display PDF file. <a href="https://bw-film-scanning.oguse.fr/document/point-light-article-revised-edition-2026-en.pdf">Download</a> instead.</p>
</object>

[See Alain's website…](https://bw-film-scanning.oguse.fr/en/index.html)


## Summary of Ansel settings

These settings assume you scan B&W negatives using quasi-monochromatic green light with a digital camera.

- Download the color profile [IdentityRGB-elle-V2-g10.icc](https://github.com/ellelstone/elles_icc_profiles/blob/master/profiles/IdentityRGB-elle-V2-g10.icc) and put it into both folders:
  - for Linux/Mac:
    - `~./config/ansel/color/in`
    - `~./config/ansel/color/out`
  - for Windows:
    - `./AppData/Local/ansel/Color/In`
    - `./AppData/Local/ansel/Color/Out`
- Open your film scan into Ansel,
- Set the _demosaicing_ module to use VNG4[^2] mode,
- Set the _input color profile_ module to use `IdentityRGB-elle-V2-g10.icc` as input profile and working profile,
- Set _color calibration_ module :
  - in the  _CAT_ tab, set the adaptation to _none (bypass)_,
  - in the _B&W_ tab, set the green channel to 1,0 and the blue/red channels to 0,0.
- See the book for settings of _diffuse or sharpen_.

These settings allow to avoid any possible cross-talk between channels, either arising from demosaicing (that can use collaborative methods between channels, for methods other than VNG4) or from color spaces conversions (in which cross-talk is built-in by design into the matrix calculus). This way, any possible chromatic aberration linked to optical refraction, which varies depending on light wavelength, is removed and, if you scan under quasi-monochromatic green light, the sharpness of your scan will be maximum because only the green photosites of the camera sensor will be used. In practice, this is equivalent to completely removing trichromy from the graphic pipeline.

This, of course, will not work for color negatives and slides, which will require a broad-spectrum white light and a trichromatic color management.

[^2]: CHANG, Edward, CHEUNG, Shiufun, et PAN, Davis Y. Color filter array recovery using a threshold-based variable number of gradients. In : Sensors, Cameras, and Applications for Digital Photography. SPIE, 1999. p. 36-43. <https://doi.org/10.1117/12.342861>
---

# Negative : https://ansel.photos/en/tags/negative/
---

# Welcome, developer documentation ! : https://ansel.photos/en/news/welcome-dev-docs/

Back in December 2019, I asked that someone [took care of providing AppImages packages](https://github.com/darktable-org/darktable/issues/3925) for Darktable. The obvious benefit would have been enabling early testing, prior to release, from people who can't build the source code themselves, as to _hopefully_ provide early feedback and help debugging __before__ releasing. This has never been a priority, which means that it was ok to have a pre-release __and__ a post-release rush to fix bugs.

I was not joking when I said that Darktable was a burn-out factory, managed like the worst start-ups, except nobody will do an exit and it's basically a dry loss for everybody involved. I'm all the more angry about it that a lot of man-hours have been wasted on cluttering the UI with cosmetics, while having nightly builds for general use would have improved both the quality of the software, but also the comfort and quality of life of its maintainers. These goals obviously didn't matter.

So, since one is never better served than by his own bugs, [I had to do it myself](https://github.com/aurelienpierreeng/ansel/commit/3852b1ba2c0be84c5130d4c21844f3199b3624b2), and in mid-December 2022, Ansel got AppImage nightly builds scripts that Darktable re-used a month later. With the cohorts of IT guys "with no pixel math skills" roaming around Darktable's hype and asking how they could help, of course, the best possible resource management was to divert man-hours from one of the very few math-able guys available on a purely IT task. That only took me something around 50 hours, because having a locally-working script is really not the same as whispering [YAML scripts](https://en.wikipedia.org/wiki/YAML) to Github Action's ears, and I had to learn it as I was doing it. Since Ansel takes 20 to 30 minutes to build (or fail) on Github Action instances, your working day is a long stuttering session of inefficient multi-tasking, while waiting for your script changes to yield a remote result. I hate programming, and I make do for maths and physics, but that kind of menial server configuration is really the worst kind of programming there is, insofar that it is neither creative nor cognitively challenging, it's only a time-consuming way of working around the design limitations of third-party APIs.

So, finally, Ansel was a semi-organized software project, able to include an userbase larger than just the die-hard geeks who look down on everyone unable to get GCC and CMake in building order, with a big "download" button on the [front page](/) that would always point to the latest build, without users having to figure out what the latest one was. Because a photo editor software should not filter out users based on their computer litteracy, whether or not it comes from the Linux world (which really is no excuse), or there is a memo from the free software Fundation that I didn' get.

But the last major issue was the lack of a developer documentation. Back in 2018, when I started contributing to Darktable, I had a very hard time grasping how the internals were wired, from just reading the code. 7 years later, even with all my experience, I still have to reverse-engineer my way through the code, using as much [`grep`](https://en.wikipedia.org/wiki/Grep) as my intuition, because the code is not modular, few places use APIs, comments are lacking, and line-by-line [Git blame](https://git-scm.com/docs/git-blame) doesn't always allow to go back in the history of design changes when someone committed (useless) code formatting changes.

That lack of developer documentation led to many features being implemented more than once, in several places, by several developers (and sometimes even by the same, over the years). At the time of writing, there are still 4 or 5 different ways, in Ansel, to write a picture history from library database to a sidecar XMP file. Some of these ways are rarely used, thus barely tested, and bugs there could go unnoticed for years, until someone reports the super-peculiar bug hidden in the forest of options. Then, for the maintainer in charge, it's a silly game of figuring out why XMP fail only in a some circumstances, which requires archaeology into the code base to figure out that it doesn't use the main XMP writing method. XMP are a specific, and still current, example, but there were many others. You get the idea.

I had noted for several years that (old) Darktable C header files had pretty much all [Doxygen docstrings](https://www.doxygen.nl/manual/docblocks.html). This is a very lazy way of doing documentation: run the command `doxygen -g <config-file>` against your source code directory, and Doxygen will build you an HTML static website of all APIs. Then you can dump all HTML files to a webserver directory, and call that your dev docs : the only tedious bit is to write the configuration file, which you do only once. So I did that: [dev.ansel.photos](https://dev.ansel.photos). API that I have rewritten (like [`selection.h`](https://dev.ansel.photos/selection_8h.html)) are documented as I made progress. Higher-level descriptions of the software architecture are incoming.

The great thing about Doxygen is it also produces dependency graphs of "modules" and APIs. And that's where you can actually see why I call Darktable's code spaghetti. This is the dependency graph of `accelerators.h`, the keyboard and MIDI shortcuts backend:

![](accelerators-before.jpg)

That shows you that the accelerators backend is absolutely not modular: it inherits the whole software. So any change elsewhere may have unforseen effects in there, and the other way around. Which is all the more concerning since this file has the [highest cyclomatic complexity of the whole software](https://sonarcloud.io/component_measures?id=aurelienpierre_darktable&metric=complexity&view=list), which makes it the most challenging file to maintain (I don't even dare to say _extend_, at that point it would be irresponsible). But it's also completely twisted in terms of direction of includes : shortcuts are a basic block that should __be included__ (aka _inherited_) in the GUI places that will implement shortcuts (darkroom/lighttable views, sliders and comboboxes, modules). Instead, the graph (and the file `#include` header) shows that shortcuts are also including (_inheriting_) their "children", so we have a double dependency and that's the worst possible way of doing it.

It's like building a house : the house should be aware of its walls, the walls should be aware of their bricks. Why ? Because the house is _made of_ walls, and the walls are _made of_ bricks, so every sub-component determines the nature and behaviour of the assembly, therefore the assembly has to know its immediate components. You don't make bricks aware of the house, because they won't change their nature depending on the assembly they belong to, and that would be a terribly flawed design. Same as it's an unnecessary level of micro-management to make the house aware of the bricks: once it's aware of its walls, it's up to the walls to account for the behaviour of their bricks, and maybe dispatch relevant info to the house. Object-oriented languages have builtin (and mandatory) ways of dealing with all that cleanly. But it's C, so you can do whatever the fuck you want to do. Doesn't mean it's a good idea, doesn't mean you should. And, well, C __not__ being an intrisincally object-oriented language is not an obstacle to using object patterns, inheritance and modularity. It's just that the developer will get no help from the language syntax to do so.

This is the same file after my complete rewrite of the shortcuts backend:

![](accelerators-after.jpg)

That clearly shows that the new shortcuts handler is a thin wrapper over Gtk native shortcuts, it doesn't know the rest of the software and doesn't care. We can look at the `accelerators.c` file too:

![](accelerators-after-2.jpg)

Now, there are lots of includes there from `darktable.h`, which is used only to get the debug helpers (they should be refactored away from that file), and includes a lot of unneeded crap too. Anyway, `accelerators.c` is aware only of Gtk/Gdk, which means it is a real module: it's completly isolated from the rest of the software. GUI widgets that implement shortcuts will declare their shortcut path, like `Ansel/Global/Menu/File/Import`, their default keys, and a reference (pointer) to themselves. Widget-less actions that have shortcuts will declare a callback function and input data to the shortcuts handler, instead of a pointer to a widget.

We save/restore the pathes and keys associations to/from the `keyboardrc` file, and that's it. There is no window to define shortcuts in GUI at this time, but making one will only have to list (loop over) known pathes and their associated keys. When the shortcuts handler catches a known combination of keys:

- for the widget-attached actions, it will send an `activate` Gtk signal to the relevant widget, and that widget will do its own thing through a callback (which is the same as the callback handling clicks, so the code is uniform between clicks and keyboard activation),
- for the widget-less actions, it will directly call the declared callback function over the declared data.

Both ways, the `accelerators` module communicates with the rest of the application through an interface exchanging 4 data fields, in a completely opaque way. As long as the interface doesn't change, changes can be made everywhere: they will stay enclosed into their module. With proper skills, this logic _could have_ been extended to MIDI devices support. But that's the difference between engineering and prototyping proof of concepts that should never make it into production.

This is unfortunately a trademark of the Darktable's way of doing things: GUI code is incrusted everywhere, even in SQL code. The other way around, SQL code is found in many GUI places (lighttable, history management, image tagging, etc.). Dependency graphs, embedded in Doxygen, are a very nice side-effect that shows the depth of the problem and make the spaghetti very obvious, because, apparently, problems don't exist until you actually see them yourself.

But it doesn't stop there. As I was writing my own Doxygen config file, I got an error: Doxygen reported 2 config files and was confused. It turns out that Darktable had everything wired for automated developer documentation generation [since 2010](https://github.com/darktable-org/darktable/commits/master/doc/doxygen.conf). Why it was never put into production, and actually hosted on some server, is not merely wrong priorities: it's negligence.

Of course, it doesn't help that the guy owning the `darktable.org` domain is not the same as the one managing the server where it's actually hosted. And the guy having actual commit rights over the `darktable.org` website is yet another one. The most pressing thing to do, when you are an open-source project making zero dollar of income, is to reproduce __all__ the mistakes of the corporate world, from the pressure to release half-baked shit at an irresponsible frequency, to the scattering of responsibilities between "services" that don't really communicate with each other (or with great delays). Because, in software (whether opensource or not), mistakes are meant to be reproduced. And since software has taken over the world, even in places that didn't need it, that says a lot about the world we live in.

The one thing I still truly don't get, though is : why the urgency ? Why keep working that badly given that there is no pressure nor financial incentive to do so ? Open-source is (could have been) the one place where we could actually work properly and take the necessary time to produce long-term quality. And even there, the shortsightedness of capitalism has taken over.

_If only users knew the state of deep shit this project fell into and how all the man-hours invested on it are actively making it worse…_

{{< note >}}
The developer documentation is now automatically generated and uploaded on dev.ansel.photos every Sunday at midnight, from the comments (_docstrings_) in the code.
{{< /note >}}
---

# White balance(s) : https://ansel.photos/en/resources/white-balances/

We all know what "white" is. We can picture a white sheet of paper. We know it is white because we learned it. But if you put your paper sheet under a summer sun, or a cloudy sky, or at home with those warm living-room bulbs, that white will change color. It might disturb you for the first few seconds, then you will just forget about it: your brain will adapt. But adapt (to) what ?

## Introduction

### Terms

Let's start by defining some terms and accronyms that will be used and abused here. You may skip that for now and come back later when you find the terms used in the text.

CIE
: [Commission Internationale de l'Éclairage](https://en.wikipedia.org/wiki/International_Commission_on_Illumination) (_International commission on illumination_). International authority for standardization of technical appliances, methods and workflows regarding lighting and color reproduction.

ICC
: [International Color Consortium](https://en.wikipedia.org/wiki/International_Color_Consortium). Professional association of imaging appliances vendors aiming at defining color reproduction workflows across the digital graphic chain, as to ensure color consistency between various output media (screen, paper, etc.). Color profiles respecting ICC specifications are usually dubbed "ICC profiles", which may hide the fact that non-ICC profiles exist too.

Color profile
: Computer file describing the color conversion between an arbitrary RGB space and a reference color space (usually, [CIE XYZ 1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) for the 2° observer). It is often misunderstood that a color profile is not a program or an application in itself, but only a "cooking recipe" to be read and applied discretionary by a color management system (CMS — _which is a computer program_), that may be implemented in various image applications (viewers, editors, file browsers)… or not.

WB
: White balance. Generic operation aiming at rescaling light emissions such that the brightest white area, on scene, would be mapped to $RGB = (1, 1, 1)$ in any standard RGB space (sRGB, Adobe RGB, DCI P3, etc.). This relies on the property of RGB spaces to describe [additive light mixing schemes](https://en.wikipedia.org/wiki/Additive_color), and on the assumption that $RGB = (1, 1, 1)$ always encodes medium white point (paper white or LED backlighting), by design.

CAT
: Chromatic Adaptation Transform. Numerical method allowing to convert colors to account for a white point change, in a way that preserves color perception.
: CAT are used automatically before applying a color profile, if the destination color space has a different white point than the target color space. For example, Ansel (and ICC) pipeline uses D50 white point but all desktop monitors use D65 white point, so CAT is used to map D50 to D65 prior to applying an output color profile.
: CAT can be used manually to perform white balance, in a more accurate way than basic RGB rescaling, under some conditions. In that case, they become a special case of WB.

Illuminant
: Direct light source emitting the photons (aka, light) bouncing on the reflective surfaces of the scene. It is worth noting that graphic pipelines always assume one single illuminant per scene, which is not the reality faced by photographers. Sometimes, scene have more than one light source. Almost always, 3D objects will get a mix of light from the illuminant(s) and from other surfaces reflections around. Non-white surfaces bouncing light on an object can be seen as virtual illuminants that will tint typically shadows and will trigger the need for selective white balancing.

### The problem of "white"

Color scientists and software programmers alike, love to think of color as a 3D object (R/G/B, hue/saturation/brightness, etc.), expressed as 3 intensive and positive coordinates. While this has some merits in terms of light additivity (an LED screen is very much an RGB additive light scheme, by technological construction), it doesn't account for all the contextual corrections that happen in human perception: human vision is better described in terms of contrast, that is by difference between objects (or surfaces) and their neighbourhood.

For instance, see this color checker:

![](SpyderCheckr.jpg)

Now, here is the reproduction of this color checker, shot on film, where something went wrong at developing time. Stare at it for some time:

![](color-checker-green.jpeg "© Guillaume Stutin")

You would swear that the red patch is still red, even though the whole picture is shifted to green, right ? Worse, the "white" patch, feels white.  Well, let's see the vectorscope of this image:

![](color-checker-vectorscope.jpg)

Aside from some blueish noise, the whole image is fully contained within ~6° of hue centered in green. In any case, there is zero red pixel in here. If we see red, it is only because our brain does not really care about red as an absolute chromaticity, but rather extracts color patches, "computes" their differences with the neighbourhood, and then tries to make sense of these differences from a cognitive standpoint.

__This is where we need to define white for an illuminant and for a surface.__ When you coat an object with a couple of layers of white paint, you make it white. This means that, if you light it with any kind of light, the white surface will reflect and bounce the light spectrum "as-is", without favouring any band of the spectrum. Of course, it will absorb some light, only a perfect mirror would reflect close to 100 % of incoming light, but that absorption will be uniformingly scaled over all wavelengths of the spectrum, so the bounce light will be exactly the illuminant light, only less intense. The opposite would be a colored surface: this would selectively attenuate some bands of the spectrum, through [subtractive color synthesis](https://en.wikipedia.org/wiki/Subtractive_color).

The illuminant is the primary light source blasting photons on a scene (flash, light bulb, sun, clouds, etc.), including on white surfaces. So, given that a white surface reflects everything it gets, the illuminant projects its own color on a white surface. Therefore, this is as white as our scene (and subsequent image) will ever get, meaning the color of our white surface will change if we light it with daylight or with an orange bulb.

So, we tend to expect __the color of the brightest surface in a scene or in a picture__ to be the contextual "white" (contextually to our visual field), and our perceptual system uses it as a reference to make sense of all other colors around, by comparing them to that "white" reference.

Now, what happens if I make the achromatic patches manually achromatic (with regard to your current display) ?

![](color-checker-green-frame.jpeg)

The red patch now feels a bit less saturated and more orange, because we compare it to actual white and not green. But the context is still so overwhelmingly green, that even having true white (with regard to your display medium) within and all around the image, we still percieve that "less green" color of the red patch as somewhat red.

Also, the achromatic patches start feeling increasingly magenta as they get darker, again because of the overwhelmingly green surround. So, there is more to "white" than just being the "brightest light emission in visual field" either: immediate context. While normalizing images for the brightest light emission (aka _illuminant_) is supported in all color appearance models (CAM), through the chromatic adaptation transforms (CAT), accounting for local discrepancies (local contrast) is completly overlooked by color maths, since all CAM so far treat chromaticity as 3 positive coordinates. This will need manual tweaking to fill the gap to _perceptually-accurate_ color reproduction (see [below](#real-life-pictures-have-more-than-one-illuminant)).

What should we take from that ?

1. white is highly contextual to the illuminant,
2. white is _cognitively_ related to the illuminant, through the brightest light emission/reflection in the visual field, which may be misleading when there is no actual white surface in the field,
3. __but__ white is also somewhat contextual to the visual surrounding, aka dependent upon local color contrast,
4. __but__ digital imaging pipelines pretend color is only about positive intensities, and don't deal with color differences. Meaning the white balance tooling doesn't exactly deal with what's truly happening, but deals with a rough approximation instead. Meaning complicated cases will need to be handled manually with care.

### No rights, no wrongs, really ?

Before diving further into the hows and whys of white balancing, we should address the elephant in the room:

> But I like my pictures slightly warm, why should I neutralize the white balance and make whites achromatic ? It's an artistic choice, after all.

No, it isn't. I mean, not in the way you think. Artistic choices are always limited by technical constraints, because art is about shaping a material medium, so the rules of physics apply at every step on said medium whether you like it or not. Now, not _knowing_ the technical constraints at play will give you a wrong sense of freedom that will turn into profound frustration when things stop going your way, that is especially when images have more than one illuminant. Bad workflows can work in nice and easy situations. Good workflows give you provisions to deal with difficult situations.

The digital imaging world would largely benefit from seeing the image making process as remapping process, instead of trying to find some absolute description of images that will never have a material existence. More specifically, we can represent the remapping flowchart as follow:

```mermaid
---
title: How digital image editing actually works
---
%%{init: {"flowchart": {"htmlLabels": false, "htmlEdgeLabels": false}} }%%
flowchart TD;
Scene(("`__Scene__
_Illuminant_: N.C.
_Dynamic Range_: N.C
_Color space_: spectral`"))
 --> Sensor["`__Sensor__
_White point_: N.C
_Dynamic Range_: 8-14 EV
_Color space_: sensor RGB`"]
Sensor -- "`WB,
input profile,
CAT`" --> Pipeline["`__Editing Pipeline__
_White point_: D50 (ICC)
_Dynamic Range_: virtually infinite
_Color space_: ITU BT.Rec 2020, CIE Lab 1976`"]
Pipeline --"`tone mapping,
gamut mapping,
output profile`" --> Screen["`__Retoucher's Screen__
aka __Monitoring view__
_White point_: D65
_Dynamic Range_: 10-12 EV
_Color space_: sRGB, DCI P3, Adobe RGB`"]
Pipeline -- "`tone mapping,
gamut mapping,
output profile`" --> ScreenC["`__Customer's Screen__
_White point_: D65
_Dynamic Range_: 10-12 EV
_Color space_: sRGB, DCI P3`"]
Pipeline -- "`tone mapping,
gamut mapping,
output profile`" --> Paper["`__Paper print__
_White point_: D50-D55
_Dynamic Range_: 5-6 EV
_Color space_: N.C.`"]
Pipeline --"`tone mapping,
gamut mapping,
output profile`" --> T["`T-Shirts
Coffee mugs
Stickers
...`"]
```

But, most people still (and wrongly) think it is:

```mermaid
---
title: How mislead people think digital image editing works
---
%%{init: {"flowchart": {"htmlLabels": false}} }%%
flowchart TD;
Scene(("`__Scene__
_Illuminant_: N.C.
_Dynamic Range_: N.C
_Color space_: spectral`"))
 --> Sensor["`__Sensor__
_White point_: N.C
_Dynamic Range_: 8-14 EV
_Color space_: sensor RGB`"]
Sensor --"`WB,
input profile,
CAT`" --> Pipeline["`__Editing Pipeline__
_White point_: D50 (ICC)
_Dynamic Range_: virtually infinite
_Color space_: ITU BT.Rec 2020, CIE Lab 1976`"]
Pipeline --"`tone mapping,
gamut mapping,
output profile`" --> Screen["`__Retoucher's Screen__
aka __Visible Truth__
_White point_: D65
_Dynamic Range_: 10-12 EV
_Color space_: sRGB, DCI P3, Adobe RGB`"]
Screen -- direct --> ScreenC["`__Customer's Screen__
_White point_: D65
_Dynamic Range_: 10-12 EV
_Color space_: sRGB, DCI P3`"]
Screen -- magic --> Paper["`__Paper print__
_White point_: D50-D55
_Dynamic Range_: 5-6 EV
_Color space_: N.C.`"]
Screen -- magic --> T["`T-Shirts
Coffee mugs
Stickers
...`"]
```

And that last flowchart is completely wrong because the retoucher's screen is not a more important medium than any other: it's no reference, no standard, it bears no truth. Output media exist in parallel, not in sequence, and the conversion between pipeline and output media are completely fluid because each have their own optical properties.

In any case, each arrow in both flowcharts represent a remapping step, while each non-rounded rectangle represents a medium, trying to reproduce the scene. A remapping is a general conversion, that can be fully or partially automated but usually requires some user to drive trade-offs, which will aim at adapting the image content for some display medium or intermediate working step. In particular, we need to remap the dynamic range (or contrast ratio), the color space gamut, and the white point, because we want to retain a consistent look throughout media that have vastly varying display abilities and properties.

For any display medium (paper, screen, diapositives, coffee mug, …), maximum luminance is reached only for a very special achromatic color: white. Think of paper… White is a built-in property of the paper (a mix of core fibers and surface coating optics), in terms of tint. As far as your inkjet printer is concerned, "white" means "don't spray ink there". Say you want your picture to look slightly warmer and you settle for amber white… You will need to coat all visible paper surface with at least _some_ yellow and red inks. Doing so will result in darkening the peak luminance, that is your "amber white" will be darker than native medium white, because of the subtractive light scheme of dyes and pigments, so you will reduce the already limited dynamic range paper can offer (5 EV for typical inkjets prints). But how do you instruct your printer to do so ? Input RGB will be clipped at 1 or 100% (or 255 if encoded as 8 bits unsigned integers), and $RGB = ( 1, 1, 1 )$ means medium white by definition of the color space (aka by design). If you try to reach "amber white" by pushing white to $RGB = ( 1.2, 1.2, 1 )$, RGB will get clipped at 1 anyway, so you might get amber gradients that break to medium white at some point. Or, if you know what you are doing, you will push white to $RGB = ( 1, 1, 0.83 )$, and therefore… reduce its luminance. This is how dynamic range connects, in an usually overlooked way, to the medium white point because the peak luminance is __always__ achromatic with regard to a medium.

{{< compare after="WB-warm-4341K.jpg" before="WB-neutral-3832K.jpg" >}}
__Before__: white balance normalized for nearly-achromatic whites (illuminant: daylight 3832 K).\
__After__: white balance kept warm for "artistic reasons" (illuminant: daylight 4341 K).\
The white point is kept the same between both pictures. Note how the gradient inside the Sun disc is harshly breaking to pure white at some point, near the center, on the warm interpretation, because of RGB clipping. I will propose 2 possible corrections for this below.\
Photo licensed under Creative Commons by Andreas Schneider.
{{</ compare >}}

{{< compare after="WB-warm-4341K-minus-half-ev.jpg" before="WB-warm-4341K-minus-half-saturation.jpg">}}
Attempted fixes for the Sun disc gradient break in the warm interpretation above.\
__Before__: reducing the highlights saturation by 50 %\
__After__: reducing the white point luminance by 0.5 EV\
Note that the desaturation strategy partially falls back to neutralizing whites to achromatic, but without adapting color in shadows and midtones in a perceptual way.\
Photo licensed under Creative Commons by Andreas Schneider.
{{</ compare >}}

{{< compare after="WB-warm-4341K-framed.jpg" before="WB-neutral-3832K-framed.jpg">}}
Here we try to see how the image content blends within a generous white frame using the medium white point. This simulates a paper print with white borders.\
__Before__: white balance normalized for nearly-achromatic whites (illuminant: daylight 3832 K)  with darkened white point.\
__After__: white balance kept warm for "artistic reasons" (illuminant: daylight 4341 K) with darkened white point.\
The warm interpretation has the image illuminant (Sun disc) severely clashing with the medium white, looking like an aged picture. The conflicting whites references between "paper" and image content makes it look like a clumsy amateur shot.
{{</ compare >}}

What should we take from that ?

1. you have no control over the medium white point, whether it is a screen backlighting or the paper tone: it is technically defined and you need to adapt to it.
2. when printing with white borders, __you need to mind the clash of white points between image content and paper borders__. This applies too for digital images that will be displayed over white backgrounds on the web,
3. you don't have a lot of latitude to de-neutralize your image white balance for artistic purposes before it starts visually clashing with paper white,
4. you will have to darken the image white point, therefore reducing the printable dynamic range, if you want to make it non-neutral (non-achromatic). Failing to do that will create clipping artifacts in smooth gradients,
5. __peak luminance is expected to be achromatic__ for all typical media: this is what ties dynamic range (or, at least, its upper bound) to a medium white point (white balance).

## How to white balance ?

### The maths of white balance

Applying a white balance is surprinsingly simple when you look at the equations: one per-channel division, one per-channel multiplication. But it's so deeply buried under misunderstood concepts and obfuscated GUI (more on this [below](http://localhost:1313/en/resources/white-balances/#gui-obfuscating-white-with-temperature)), that people got completely side-tracked by the technical distractions. So it is a great example of how 3 equations can undo the havoc that software and GUI have wrecked in user's minds over the past decades, by trying to be "intuitive".


Let's say you are in some RGB space, for example camera sensor RGB. You need to find the chromatic coordinates of your scene illuminant in this RGB space. Let's call them $R_w, G_w, B_w$. Conveniently, if you know nothing about the scene illuminant, simply getting the arithmetic mean of $R, G, B$ values of all pixels in the picture will get you fairly close. [^1] In other words, you need to know the color of the illuminant, and convert its chromaticity as RGB coordinates in your working RGB space.

[^1]: This is called the "grey world assumption". It is actually how cameras detect white balance, though nowadays they might choose to do so in some well-chosen areas of interest instead of doing it blindly on the whole image.

Then, you need to do the same for the expected (reference) white point of your RGB space. Let's call those coordinates $R_{wr}, G_{wr}, B_{wr}$.

Finally, white-balancing the picture is as simple as normalizing each pixel RGB with the scene illuminant RGB and rescaling (denormalizing) it with the target illuminant (or white point) RGB:

$$
\begin{cases}
R_{out} &= R_{in} * \dfrac{R_{wr}}{R_w}\\\\
G_{out} &= G_{in} * \dfrac{G_{wr}}{G_w}\\\\
B_{out} &= B_{in} * \dfrac{B_{wr}}{B_w}\\\\
\end{cases}
$$

See, no need for kelvin temperatures ! Those are only to confuse you in GUI. It's just rescaling pixel RGB with carefully-chosen factors.

If we apply these operations on sensor RGB, we have [input color profiles](../../doc/views/darkroom/modules/input-color-profile/) that expect a D65 illuminant to be encoded with certain RGB values: $R_{wr}G_{wr}B_{rw} = (1, 1, 1)$, by design. The $R_{w}G_{w}B_{r}$ values are then derived from the input color profile. This makes the computation even simpler:

$$
\begin{cases}
R_{out} &= \dfrac{R_{in}}{R_w}\\\\
G_{out} &= \dfrac{G_{in}}{G_w}\\\\
B_{out} &= \dfrac{B_{in}}{B_w}\\\\
\end{cases}
$$

That's what we do in Ansel's old [white balance](../../doc/views/darkroom/modules/white-balance/) module.

Now, the camera sensor RGB is vastly different from our cone cells RGB. In practice, attempting to white-balance in sensor RGB gives poor results, especially for illuminants for from D65, and special "RGB" spaces have been developed to improve the perceptual consistency of the operation over the whole color range. The Bradford, CAT02, CAT16 etc. chromatic adaptation transforms (CAT) apply the same operations but in special spaces. You can [learn more about them](https://acorn.stanford.edu/psych221/projects/2010/JasonSu/adaptation.html), their differences and specifics are beyond the scope of this article.

White-balancing through CAT happens in Ansel's [color calibration](../../doc/views/darkroom/modules/color-calibration/) module.


### White balance in Ansel

If we do a partial pipeline flowchart of the default Ansel's editing pipeline order, we get the following modules:

```mermaid
flowchart TD;
  raw(RAW) --> wb[white balance]
  wb --> demosaicing
  demosaicing --> ip[input color profile]
  ip --> cc[color calibration]
  cc --> filmic
  filmic --> op[output color profile]
```

For Fuji XTrans sensors, the Markesteijn demosacing algorithm relies on a luma/chroma separation, which requires at least a rough white balancing step to happen earlier. This is the old [white balance](../../doc/views/darkroom/modules/white-balance/) module. But we know the result is not accurate, especially for illuminants far away from D65 (especially for low-quality artifical lighting).

The quality of the input color profile also relies on having RGB already normalized to D65. But we already know this can't be accurate either. Some Adobe products now use dual-illuminant DNG profiles, which have one input profile for D65 illuminant and one for illuminant A (incandescent bulb), and the final profile used is computed as a mix of both depending of the color temperature of the scene illuminant. Those fix the issue for daylight-ish illuminants, but still don't address the issue of colored stage lights and other energy-saving light bulbs with terrible color-rendering index (CRI).

Another, more accurate, step of white balancing was later added in [color calibration](../../doc/views/darkroom/modules/color-calibration/), using chromatic adaptation transforms (CAT16, Bradford). It can be configured automatically by extracting a profile from a color checker chart. Empirical studies have shown that the lowest color deviation, after profiling from a color checker, was achieved with color calibration after a preliminary step of white balance, in the old module, so we have retained both:

1. the _white balance_ module will normalize sensor RGB such that D65 white that would have been captured on scene would record at $RGB = (1, 1, 1 )$. If the actual scene illuminant is anything else, it will be dealt with later.
2. _input color profile_ module will convert sensor RGB to CIE XYZ 1931 color space, which acts as a glue for all later color spaces, assuming D65 illuminant (which is wrong in general),
3. _color calibration_ will convert CIE XYZ 1931 to CAT16 or Bradford "RGB" and will normalize any arbitrary scene illuminant (that is, if it's not D65) to the pipeline white point (D50, as per ICC workflow). [^5]
4. _color calibration_ can optionally apply a refinement step of the input color profile, using a channel mixer (aka a matrix color profile) whose parameters can be directly computed from a color checker in the module.

[^5]: It is worth noting than we can't directly convert sensor RGB to CAT16 or Bradford without going through CIE XYZ 1931, so we need the input color profiles to be applied first. But we know the input color profile, and therefore the conversion from sensor RGB to CIE XYZ 1931, will be wrong in general because it relies on having whatever scene "white" balanced to $(1, 1, 1)$ already. In an ideal world, we would have one input profile for each scene illuminant, but that's technically impossible. So this is why the channel mixing in _color calibration_ comes as a nice refinement step.

This dual step of white balancing is somewhat confusing for users, but has proven to yield the most accurate colors. The _color calibration_ has a diagnostic mode allowing to compute the color deviation after profiling, as a [delta E](https://en.wikipedia.org/wiki/Color_difference), from a color checker chart, you can check for yourself.

## GUI: obfuscating white with temperature

In _any_ software around, you will see a white balance setting using a temperature slider (actually, [correlated color temperature](https://en.wikipedia.org/wiki/Correlated_color_temperature)), and then tint. But you saw in the actual equation that white balance internally doesn't deal with anything else than the RGB coordinates of the illuminant, albeit projected in the special RGB space in which we perform the scaling. So, why bother with that confusing kelvin (K) temperature ? Well, there is no reason anymore except legacy.

Physicists have a special radiator, called a [black body](https://en.wikipedia.org/wiki/Black-body_radiation). It's an idealized object which, once heated at a certain temperature, will emit a known light spectrum. More interestingly, the shape of light spectrum can be _entirely predicted_ by the temperature of the black body. Remember how [RGB is a 3D reduction of a light spectrum](../../workflows/scene-referred/index.md) ? Here, it's a 1D reduction, even better ! Meaning one single number suffices to describe unambiguously a full spectrum.

{{< figure src="Black_body.svg.png" >}}
Light spectra (blue, red, green curves) for black body temperatures between 3000 and 5000 K.
By <a href="//commons.wikimedia.org/wiki/User:Darth_Kule" title="User:Darth Kule">Darth Kule</a> - <span class="int-own-work" lang="en">Own work</span>, Public Domain, <a href="https://commons.wikimedia.org/w/index.php?curid=10555337">Link</a>
{{< /figure >}}

The cool property here is, until the 1990's, (home) light bulbs were all incandescent tungsten filament, which are a black body at 2855 K, standardized as _Illuminant A_ by the CIE. Then, daylight falls very close to black bodies spectra between 3000 K (sunset) and 8000 K (cloudy day). So we could reduce illuminant color to a single temperature value, which makes for a simpler GUI, although good luck explaining to the layman why color got suddently reduced to a temperature… Very few photographers understand what's going on there, so much for an "intuitive" GUI…

{{< figure src="Color_temperature_black_body_800-12200K.svg.png">}}
Illuminant color associated with black body temperature (adapted for D65 medium white).
By <a href="//commons.wikimedia.org/wiki/User:Bhutajata" title="User:Bhutajata">Bhutajata</a> - <span class="int-own-work" lang="en">Own work</span>, <a href="https://creativecommons.org/licenses/by-sa/4.0" title="Creative Commons Attribution-Share Alike 4.0">CC BY-SA 4.0</a>, <a href="https://commons.wikimedia.org/w/index.php?curid=44144928">Link</a>
{{< /figure >}}

Problems happened, first with tinted lights (stage lights & gels), second with cheap high-power bulbs (sodium vapour, halogens), then with more recent light bulbs (fluorescent, LED), which light spectrum has peaks and valleys, far from the expected smoothness of a black body spectrum. Those can't be accurately described as a single temperature value.

{{< figure src="FL3.15.png" >}}
Light spectrum of a D65 fluorescent simulator, standardized as CIE illuminant F3.15. The sharp peaks are not expected in a black body 6500 K spectrum.
By <a href="//commons.wikimedia.org/wiki/User:Ellande" title="User:Ellande">Ellande</a> — <span class="int-own-work" lang="fr">Personal work</span>, <a href="http://creativecommons.org/publicdomain/zero/1.0/deed.en" title="Creative Commons Zero, Public Domain Dedication">CC0</a>, <a href="https://commons.wikimedia.org/w/index.php?curid=147285708">Link</a>
{{< /figure >}}

To account for deviations from pure black body spectrum, another "tint" setting was added, making the description of our illuminant 2D instead of 1D. While the temperature roams the Planckian locus in a curved way, the tint allows to move away from it orthogonally.

{{< figure src="Planckian-locus.png">}}
The Planckian locus (bold curve) covers the chromaticities linked to a black body spectrum, here represented in <a href="https://en.wikipedia.org/wiki/CIE_1960_color_space">CIE 1960 UCS</a>. Daylight spectra lie very close but not exactly within the Planckian locus. For any arbitrary color, we can compute the closest correlated color temperature (CCT), using the closest point of the Planckian locus. For $\Delta_{uv} > 0.05$, that is if the color lies too far away from the bold curve, this CCT is meaningless and the CIE discourages to use it. Software developers answered "hold my beer" and added a 2nd coordinate, the tint, orthogonal (in theory) to the Planckian locus (straight lines). In practice, tint is not even that clean, it's usually a green-magenta axis.
By <a href="//commons.wikimedia.org/wiki/User:Adoniscik" title="User:Adoniscik">Adoniscik</a> - <span class="int-own-work" lang="en">Own work</span>, Public Domain, <a href="https://commons.wikimedia.org/w/index.php?curid=3803195">Link</a>
{{< /figure >}}

This could be fine, but it really isn't. Indeed, once we have the RGB coordinates of the illuminant (for example, as the camera detects it internally), finding the correlated color temperature (CCT) is done through various [approximations](https://en.wikipedia.org/wiki/Correlated_color_temperature#Approximation) (each software may use its own). The other way around, knowing the correlated color temperature, we can fetch the RGB coordinates of the illuminant through other [approximations](https://en.wikipedia.org/wiki/Planckian_locus#Approximation). Meaning that the roundtrip RGB → temperature → RGB (or, similarly, temperature → RGB → temperature) will not yield the original value. So in the event of the scene illuminant being anything but a pure black body spectrum, we stack a gross approximation of the arbitrary illuminant by a meaningless (and wrong) temperature on top of the approximation of the RGB coordinates from the temperature, all that only as an intermediate step designed to show a 1D slider in GUI (on 2D if you include tint for difficult cases), whereas all the pixel code cares about is the RGB coordinates of the illuminant anyway, which can perfectly be computed directly in RGB without going through temperature.

For this reason, Ansel has different illuminant settings in [color calibration CAT](../../doc/views/darkroom/modules/color-calibration/). If we find that the camera detected something close enough to daylight or blackbody, we automatically offer the temperature setting. Else, we default to "custom", which provides users with a 2D hue/chroma setting in [CIE Luv 1976](https://en.wikipedia.org/wiki/CIELUV) that will directly allow to define illuminant color without intermediate computation. From hue/chroma to RGB, the illuminant computation uses no approximation, so the roundtrip RGB → hue/chroma → RGB accurately yields the original values.

But the mess created by temperatures doesn't stop here. The black body model links temperatures to colors like this:

{{< figure src="Color_temperature_black_body_800-12200K.svg.png">}}
Illuminant color associated with black body temperature (adapted for D65 medium white).
By <a href="//commons.wikimedia.org/wiki/User:Bhutajata" title="User:Bhutajata">Bhutajata</a> - <span class="int-own-work" lang="en">Own work</span>, <a href="https://creativecommons.org/licenses/by-sa/4.0" title="Creative Commons Attribution-Share Alike 4.0">CC BY-SA 4.0</a>, <a href="https://commons.wikimedia.org/w/index.php?curid=44144928">Link</a>
{{< /figure >}}

Yet, all white balance temperature sliders show this:

![](temperature-wrong.jpg)

Yes, it's reverted. Why ? Because it doesn't show the color of the illuminant, but instead an attempt to show the effect (a color shift) on the image. But… The effect of a white balance setting is to turn a non-white color to white (with regard to the white point of our current working space). So the effect should be white all along… What this colored slider _actually shows_ is how a D65 white captured on the scene would look like after applying our white balance. But why should we care about the look of a virtual D65 white from the scene that don't even exist, if our scene illuminant is not D65 (otherwise we wouldn't need white balance at all) ? It doesn't make sense.

In _color calibration_ module, temperatures are shown accordingly to the color they represent, which matches the color patch of the illuminant that can be found by sampling achromatic surfaces in the picture, using the color picker:

{{< compare before="temperature-right-uncorrected.jpg" after="temperature-right-corrected.jpg" >}}
Disabling the _color calibration_ will show the picture shifting to the color shown in the patch.
{{< /compare >}}

Honoring our remapping way of thinking, this clearly shows what original color from the picture will be remapped to D50 white through the transform, instead of attempting to represent a color _shift_ with an absolute color. Problem is many users now think something is broken in this approach, being impregnated with the nonsensical one.

## Real life: pictures have more than one illuminant

It's a bit late to address another elephant in the room: the core assumption of all chromatic adaptation, __our color pipeline expects one and only one illuminant__. This makes sense, when you print on paper or display on screen: white is defined by the paper tone or by the LED backlighting. But, on the scene, unless you shoot in studio with only black/white walls, floors and ceilings, you have several "illuminants".

Let's explain that with an example. The picture below was shot mid-October, while the tree leaves were turning orange. We have an overhead of foliage, trees left and right, and grass on the ground:

- The green grass on the ground acted as a green reflector, bouncing light mostly under the chin: that's one virtual illuminant.
- The yellowish leaves overhead acted as a yellow reflector, bouncing light mostly on the cheeks and décolleté: that's another virtual illuminant.
- Then the direct sunlight pierced through the leaves, on the bright skin spots, which is the real illuminant (aka source of all photons on the scene).
- Meanwhile, all the green background was likely confusing in-camera automatic white balance detection, and we can't sample any achromatic object for reference once in our photo editor.

{{< compare after="Cécile-DSC_0316-0315.jpg" before="Cécile-DSC_0316-0315-off.jpg" >}}
__Before__: no color correction, defaulting to WB detected in-camera\
__After__: color calibration from color checker, color balance done manually (details below)\
© Aurélien Pierre
{{< /compare >}}

So this is overall one of the most difficult setups: each feature and surface of the (3D) face is getting its light primarily from a different (real or virtual) illuminant, depending on the orientation of the surface in space, and we can't rely in general on WB auto-detection because the whole frame is biaised by foliage, although here it does a fair-enough job. Many photographers still think it is impossible to recover skin tones in this scenario, the above image proves them wrong, though it requires some virtuosity.

The trick here is first and foremost to shoot a color checker frame, to correct the green deviation as much as possible, using the _color calibration_ module. The color checker will be held in front of the subject, pointing towards the camera in such a way that it gets an average mix of all available illuminants (and hopefully no glare). So the _color calibration_ will be used to "mostly get there", trying to account for an average mix of all lights sources. The color checker will save us much manual, frustrating and time-consuming fiddling with parameters.

{{< compare after="Cécile-DSC_0005-0004-colorchecker-on.jpg" before="Cécile-DSC_0005-0004-colorchecker-off.jpg" >}}
Applying the _color calibration_ settings (CAT and channel mixer) computed from the color checker values.\
__Before__: Illuminant detected in-camera: hue 53°, chroma 17.1 % (yellow) - delta E: average 3.47, max 7.94\
__After__: Illuminant computed from color checker: hue 67.5°, chroma 13.0 % (green) - delta E: average 2.54, max 5.36\
© Aurélien Pierre
{{< /compare >}}

If we had artificial objects, with sharp contours, we could then multi-instanciate _color calibration_ and mask in/out the areas we want to selectively white-balance. But here, with an human face, we have rounded surfaces seamlessly blending into each other, and that is not possible.

It is useful to recall, here, that the _color balance_ shadows lift and highlight gain are also mere RGB multiplications, exactly like our white balance. While it is mathematically the same thing, they happen in a special RGB space designed for perceptual uniformity of hues (as to improve color grading), instead of happening in a CAT space, designed specifically for white point adaptation. Nonetheless, lift and gain can be seen as local chromatic adaptation for shadows and highlights, and can be used for this purpose.

So the way to fix this picture is as follow:

1. shadows are expected to have an higher ratio of green bounce light, so turn the _shadows lift_ setting to magenta (always compensate with shifting to opponent color),
2. highlights are expected to have an higher ratio of yellow light (from autumn foliage), so turn the _highlights gain_ setting to blue,
3. the final skin tone can be adjusted using the midtones power, in _color calibration_, to soften the magenta-red shift that will most likely result from the next 2 steps. This will happen by re-introducing some yellow.
4. the direct sunlight spots on the skin can be lazily desaturated to blend, since they are so much brighter than anything else, using _filmic_ saturation (_look_ tab) and/or _color balance_ highlights saturation (_master_ tab).

In any case, fixing this kind of picture starts on the scene, by untangling how the different illuminants play out on the surfaces of your subject. It will be much easier to unroll a post-processing method once you have all the mental notes of what was going on.

## Back to artistic choices

So, you want your golden-hour amber haze ? Ok, but twisting white balance is probably not the right way to do so, because it affects all colors over the whole dynamic range.

There is an easier, better and more suited way, using a neutral white balance, and then tinting the picture using _color balance_ power:

{{< compare after="WB-neutral-3832K-toned-framed.jpg" before="WB-warm-4341K-framed.jpg">}}
__Before__: white balance kept warm for "artistic reasons" (illuminant: daylight 4341 K).\
__After__: white balance normalized for nearly-achromatic whites (illuminant: daylight 3832 K), then amber-red color grading using color balance power.\
This allows us to get a deeper color shift and more of that amber haze in midtones, without tampering with peak emission nor with water blue. The look is overall more natural.
{{</ compare >}}
---

# Troubleshooting color issues : https://ansel.photos/en/resources/troubleshooting-color/

This will apply to upstream Darktable as to Ansel since they share most of their color pipeline. The following procedure will help you troubleshoot your color issues, whether it is inconsistent appearance between export vs. preview, or between screen vs. print, or between 2 apps.

## Setup your desktop properly

The inherent problem of color is it exists only as a perception, and that perception is highly contextual and fluid. If I take any color patch and display it over a white, middle-grey or black background, it will not appear the same even though a colorimeter would confirm it's the exact same color. I have built a little web animation to showcase this effect, displaying sRGB gamut slices at constant hue, over user-defined background lightness : [the sRGB book of color](https://eng.aurelienpierre.com/2021/04/the-srgb-book-of-color/).

Now, if you add on top the lighting discrepancy, it will also look different lit by a powerful 120 W halogen bulb or by a weak nightstand lamp. Finally, the color temperature of the light will also come into play : of course, a "cold white" D50 (5000 K) bulb or a "warm" 3200 K bulb will shift hues in a print, but the cold white will also make colors in the blue-violet region appear more colorful (compared to the others), as the warm white bulb will make colors in the red-orange region appear more colorful.

In all this, your physiological perceptual system will adapt real-time, because it's great piece of biology, but it's specifically what makes our retoucher's like miserable: the perceptual system has no standard (reference) perception. Instead, and because of that, we need to work in standard viewing conditions.

The lighting in your editing room should be as constant as possible across the day. Pick a room with a window exposed to the North (in the Northern hemisphere) so the light doesn't change much over the day, and cover it with cold white opaque sheets to diffuse it (yes, that will be cotton bleached with nasty chemicals to be white, natural cotton is beige-ish).

You should never edit in the dark or in a dim room (even if that makes the screen more legible — it's a trap). For night editing, try to find high quality D65 bulbs (or, next best option : D55 – this is more available and mainstream) having a Color Rendering Index (CRI) of at least 92, ideally more than 95 (if you find more than 98, it's probably a scam – in any case, the theoritical max is 100). I don't recommend editing in full artificial lighting because natural light has a built-in 100 CRI (since it's actually the reference for all CRI). Stay away from the energy-saving fluorescent bulbs, they have terrible CRI and need a variable heating time to reach their full potential (which is not much).

{{< warning >}}
Editing late at night under D55 or D65 light will deeply mess-up your sleep pattern, you should turn those lights off at very least 2h before going to bed, otherwise you won't sleep.
{{< /warning >}}

Keep in mind that everything in your visual field will have an impact on your color perception. The wall behind your screen should be painted matte white. Some people recommend middle-grey, but "middle-grey" paint is not easy to find, and still… it will need a proper lighting to appear actually at 20% of the luminance of a white sheet displayed under your lighting. Since nobody cares about lighting their wall, a white paint poorly lit will be closer to middle-grey and anyway, brighter will cause fewer issues than darker.

The backlighting of your screen should be set such that a 100% white frame displayed on your screen appears at the same brightness as a white sheet of paper next to your screen (that is, lit by your room lighting). You may have heard to set your screen white luminance to 90 Cd/m², but that's only to compare the screen rendering with a physical print displayed in a (standard) proofing booth, and not really for general editing. In any case, it is more important that your screen backlighting matches your actual room surround lighting than some arbitrary value, since human vision reacts only to contrast (so, basically, even the room surround with your screen, both in color temperature and in brightness, to avoid creating any artificial contrast).

Only use middle-grey GUI in all your editing software. Yes, the darker GUI look slicker, but they will tamper with your perceptions and you will get bad surprises when printing.

The general idea behind all these pieces of advice is your screen should look as close as possible as a print displayed next to it : whites should have the same brightness and temperature, and then… for blacks, it's a bit more complicated.

## Undo your manual color management
### Context

The vast majority of photographers don't understand how color management work. As a result, they believe in the magic of the "ICC profile", they will buy a calibration device (Datacolor Spyder or Xrite), bake a custom color profile and hope think that it magically fixed all their problems. It didn't.

Color profiles can be good or bad, and you need to check their quality by doing another step of characterization after the calibration/profiling.

Strictly speaking, calibration is an hardware operation by which you set the backlighting, contrast and brightness settings of your monitor by using the knobs (so it doesn't apply to laptops). Then, profiling is achieved by recording the color deviation (error) of the display device by measuring how standard (previously-known) color patches render on the device, then computing the corrections that should annihilate such deviations. Finally, the characterization will compute the residual error that remains **after** the profile is applied, because unfortunately, that profile will not make color perfectly accurate. That residual error is expressed as a delta E, typically using the CIE 2000 dE formula, and the average delta E should absolutely be below 2, with a max delta E ideally below 2.3.

{{< warning >}}
If the profile characterization shows an average delta E higher than 2, your custom profile might actually be worse than using no profile at all. Quality control matters. Custom profile doesn't automatically imply higher quality.
{{< /warning >}}

### Reset everything to sRGB

In your OS/desktop environment color manager (aka **not** in Ansel), set the global color profile to generic sRGB. In Ansel, set the display color profile to sRGB too. Then, export your files as sRGB. If that fixes issues, then you have a faulty ICC profile somewhere in the pipeline. "Faulty" can either be a profile meant for some device and improperly used for another device (profiles are **not** portable, reusable, or else), or a bad profile (wrong profiling options when doing it, or old profile that doesn't match the screen rendering anymore).

## Profiling the right way

**Never use LUT profiles**. Yes, they are theoretically more accurate, but they are also a lot less forgiving to user mistakes, and it would require a solid 4h course to teach you everything you need to know to use LUT-based profiles without harming yourself. Also, LUT-based profiles can generally not be inverted, so they come with technical limitations too. Just forget about them.

In 99% of the use cases, for your screen, you need to use a matrix + 1D curve kind of profile. The 1D curve is essentially a tone curve that will linearize the brightness of your screen and will be applied to the 3 RGB channels the same. Even though LED and LCD screens are *mostly* linear already, they might need some local corrections near black, so the 1D curve deals with that. White point will be set to "native" (aka as-is), with D65 color temperature, and black point will be set to "native" too. If available, use the "Bradford" chromatic adaptation and avoid the CAM02. Use ICC v2 profiles instead of v4, the v4 brings no improvement for us here and is not as widely supported.

If, and only if, the matrix + 1D curve does not yield proper results, you may try matrix + 3D curves (aka one curve per RGB channel). However, DisplayCal has a hardtime ensuring white-balance consistency across the black-to-white range in this setting, and you should check that shadows will not be significatively colder or warmer (in color temperature) than highlights. This is a last-resort attempt. If you need to go that way, I recommend you build your profile with proprietary Xrite or Spyder software, because DisplayCal has issues there.

Keep in mind that the 1D curve is generally not linear, this means that it will not scale with your backlighting setting. You need to check the kind of curve it yields (Display Cal can generate a full HTML report that is very useful if you know how to read it), and if it is far from a straight "identity" line, which means you are in for problems. In that case, you may want to produce display color profiles at 100% and 50% backlighting intensity, and switch between those depending on your screen setting. (Yes, it is tedious).

If you absolutely need to compare physical prints next to your screen (though I have no idea why you would want to do that unless you own a photo lab), you will need to calibrate for D50 white at 90 Cd/m² and for a contrast (white luminance / black luminance) of 250:1 (that's black at 0.36 Cd/m²). Note that D50 is not the native white point of LED screen and, except for the expensive Eizo displays, distorting the white point by that much will result in a substantial loss of gamut.

If you profile a printer, keep in mind that any color profile is valid **only** for a particular triplet { printer driver + paper + ink cartridges }. In any case, I personally think that printing at home is a not worth the trouble and way too expensive for an amateur, unless you print at least several times a week. Indeed, when you start your printer again after some time unused, it will clean its nozzles first, and will do so with good ink from the cardridges – not only will it not be available for the next 15 min, but half or more of your expensive ink will actually end up being used as detergent.

## Remove local contrast modifiers

There **is** an expected discrepancy though, between full resolution exports, and lower resolutions/cropped image exports : the local contrast modifiers. Those are the modules : *local contrast*, *contrast equalizer*, *blurs*, *low-pass*, *diffuse or sharpen*, *sharpen*, *highlights reconstruction* (in *guided laplacian* mode), the *tone equalizer* if used with the internal guided filter for local contrast preservation, and the *details mask* (in parametric masks options).

### Context

All those filters rely on some amount of neighbouring pixels for their output. So, if you crop the image, you change the neighbourhood. But if you downsize it, first of all you remove sharp details (so, for example, sharpening will act on the coarser details instead of the finer), and then there might be rounding errors (say your filter does stuff by grabbing all pixels within a 3 px distance when zoomed 1:1 — zoom at 1:2, that makes it a 1.5 px distance, but there is no such thing as an half pixel, so that will be rounded to either 1 px or 2 px, and that's a 33% error).

There are tricks and mitigating solutions that try to make the output of these filters visually consistent from afar, no matter the zooming level you are using, but they work until they don't, precisely because of the integer nature of pixels that will necessarily introduce rounding errors.

### Export and preview at 1:1

Exporting can be done using 2 strategies : process the full-resolution image, then downsize last (if needed), or downsize first, then process the low-resolution image. The first option will be slower and may introduce aliasing artifacts (due to the fact that we interpolate non-linear RGB with high-frequencies), but neighbourhood filters will behave as expected. The second option will be faster, but neighbourhood filters can show significantly different output. Choose your poison.

In darkroom, you can also preview at 1:1 but it has one more shortcoming : any guided filter (either in *tone equalizer* or in the masking *feathering*) will only see the currently displayed region (possibly cropped to fit in the screen), instead of the full image. This is of course faster to process, but may generate some discrepancies for very large filters.

## Don't upgrade your OpenCL drivers

OpenCL drivers are notoriously bad, and it has nothing to do with any particular OS/platform or vendor (though AMD tends to be consistently shitty in the 2020's). It happens regularly that some driver update makes OpenCL entirely or subtly broken.

Check **when** your drivers are updated, and don't hesitate to roll back to the previously working version if anything breaks. If you don't have time to deal with that nonsense, simply avoid any driver upgrade once you found the one that worked for you.

## Is your image viewer color-managed ?

Ansel is properly color-managed. This part of the code has been consistently working and tested for the past 10 years, and I have checked it myself several times. Problem is, many image viewers and web browsers are not. So, in case of a disagreement between Ansel's internal color and whatever you see in another image viewer, try to assess first if that app is doing the right thing.

Also, note that the default Windows 10 image viewer has "image enhancements" enabled by default, and you need to find that option to disable it, because it will sharpen and resaturate your picture without asking you.

Similarly, pictures exported to Instagram and some other websites from Mozilla Firefox are re-encoded in a weird way (color will look less saturated), compared to exports through Chromium/Google Chrome (which will look as in Ansel).

There is a common belief that images for the web should be only exported in sRGB, and that's partly wrong. Ansel will put the color profile used to export the image in its metadata, such that any color-managed app can use it and convert the color to whatever display space is being used. So you can technically export pictures in whatever color space you choose. However, there is a common practice that consists in stripping the metadata of the pictures entirely, in websites and CMS, to save some bandwidth. If the image file doesn't provide its color space, then any color management system is supposed to assume it is sRGB (as per ICC recommendations), so if the metadata stripping was done without pre-converting the colors to sRGB, then your picture will be wrongly taken as sRGB all the time. So exporting to sRGB for web is the safe thing to do, but if the images are posted under your control and you know metadata are kept, then do as you please.

## Softproofing may not be what you think it is

Softproofing is a process that tries to emulate on screen the final appearance of a print. It's full of challenges and not great, because the screen is emissive and the print is reflective, so that doesn't start well. But there is worse.

Typical screens have a contrast ratio (white luminance / black luminance) of at least 300:1, and sometimes much more. Inkjet prints on matte paper have a contrast ratio of 50:1, and on glossy paper, maybe 100:1. That's a third to a sixth of what you see on a typical screen.

To emulate the print contrast, the ICC way is to anchor whites (the screen white and the paper white) to the same luminance value (typically, 90 Cd/m²). The adjustment variable will then be black, which means that the softproofing will brighten the blacks on screen as to reproduce the same contrast ratio as the print. This gives a "veiled", "washed" and "milky" black that most people don't expect and call an "artifact", but it is absolutely what should be seen. You can read more on that in the [Printing](../workflows/printing/) workflow page.

Note that reduced (global) contrast (that is, the contrast ratio) will result in a reduced perceived sharpness, because we don't perceive sharpness at all, but it is the local contrast that we associate with sharpness. Changes in global contrast affect local contrast too, and therefore trick us into thinking optical the sharpness is better/worse while it didn't change, and it is, again, just an illusion.

## Colorimetric intents don't work

They simply don't. Try loading any ICC profile and changing those intents, the outcome will not change one bit. I'm not sure what LittleCMS2 does with these, but consider them broken.
---

# Fixing the pipeline cache and 10 years-old bugs : https://ansel.photos/en/news/fixing-pipe-cache-10-yo-bugs/

## Recap of the previous episodes

0. Between 2020 and 2022, [Darktable underwent a mass-destruction enterprise](./darktable-dans-le-mur-au-ralenti/index.md), by a handful of guys with more freetime and benevolence than actual skills,
1. In 2022, [I started noticing an annoying lag](https://github.com/aurelienpierreeng/ansel/issues/29) between GUI interactions with sliders controls and feedback/update of said sliders. For lack of feedback stating that the value change was recorded, users could change it again, thereby starting additionnal pipeline recomputes and effectively freezing their computer because stupid GUI never said "got you, wait for a bit now".
2. I discovered that pipeline recomputations orders were issued twice per click (once on "button pushed", once on "button released" events), and once again for each mouse motion, but also that the GUI states were updated seemingly after pipe recompute.
3. I [fixed that](./undarktable-ing-gui-controls.md) by almost rewriting the custom GUI controls (Bauhaus lib). I thought that preventing reckless recompute orders was gonna solve the lag : it didn't. Then, I discovered that requesting a new pipeline recompute before the previous ended waited for the previous to end, despite a shutdown mechanism implemented many years ago that should have worked.
4. I [fixed that](./implementing-kill-switch.md) by implementing a kill-switch mechanism on pipelines, following comments in the code from the 2010's and internal utilities that may well have never worked. This did not always work because the kill order came often with a noticeable delay. Once again, the GUI lag was not fixed.

…


## Episode 5 : paying the technical debt

What I discovered should really make it to the manuals of computer science in the chapter about what __not__ to do if you want to write a semi-reliable application.

So, whenever an image-processing parameter was changed in a module, a request was sent to add a new history entry into the database (often more than once per interaction, as shown above). History entries are nothing more than a snapshot of one module's internal parameters (including masks). If a change was detected compared to the previous history entry, a `PIPELINE_STATE` flag was set to the value `DIRTY` to indicate that the pipe would need a recompute and a `gtk_widget_queue_draw()` was sent which, as the name suggests, asks Gtk to redraw the main darkroom preview and the navigation thumbnail, but __in an asynchronous way__ (understand : whenever it finds the time, after everything previously started completes). This will have its importance later.

It took me a __very long time__ to figure out how the pipeline was actually started, because none of the code attached to modules and pipeline contained anything saying "go compute that". In other words, none of the module code contained any explicit recompute instruction.

I had to reverse-engineer the pipeline code from the other end, looking for how the pipeline __could__ be started and greping each option, until I figured out the unspeakable : the first generation of Darktable's devs had wired a callback function to the `redraw` event on the darkroom main preview and navigation thumbnail, but in a completely unrelated place in the code. In that __GUI__ callback, the value of the `PIPELINE_STATE` flag was checked, and either sent the backbuffer pixmap directly to the widget if the flag was `VALID` or asked for a pipe recompute if the flag was `DIRTY`, and that recompute __itself requested a `gtk_widget_queue_draw()`__ upon completion.

This method has one merit : it's lazy coding. Then it has a shitload of drawbacks and issues :

1. it's __not__ dev-friendly, especially in a software project were code greping and comments are all the doc we can dream of. It took many hours to understand the logic through program archaeology. If a command is issued, I want to read `command_issued()` in the right place in the code, because C is already difficult enough to follow without mixing riddles into debug.
2. since `gtk_widget_queue_draw()` (called __twice__ in the worst-case scenario) is only added on the queue and processed asynchronously, it adds any lag that Gtk could suffer (while processing other bits of the GUI or previous frames) before any pipeline recompute is only started, which is unnecessary since the pipeline lives in its own thread in parallel,
2. the great MIDI turducken, listening for pointing, keyboard and MIDI events to dispatch shortcuts, seemed to have overloaded the global GUI with listeners looping over all known shortcuts, which made Gtk lag to the point where it became noticeable,
3. it prevents any kill-switch mechanism from being useful, both because of delays and because of flags readings were interleaved with thread locks (and race conditions). In addition, waiting to acquire the pipeline thread lock (mutex) would freeze the GUI thread during the corresponding time, which was probably one of the causes of the slider lag before updating its position,
4. the chained calls to the `redraw` event callback, through `gtk_widget_queue_draw()`, promoted "endless" stuttering loops of (useless) intermediate redraws which seemed to hit people with slow computers more than those with power beasts. Those were particularly difficult to reproduce, depending on hardware performance, so you can find forums were people are convinced that Darktable is the slowest software ever while others report excellent performance.

So I fixed the whole logic by :

1. making the `redraw` callback stupid (drawing whatever pixmap buffer was available, unconditionnaly),
2. handling explicit pipeline recomputes in the module and history code, with the pipeline recomputes asking for a widget redraw upon pipeline completion, (yes, it's more code, and it is tedious, but now you can optimize recomputes manually — performance matters),
3. removing the special handling of "duplicate" history items (leading to some pollution when dealing with masks, this will need to be fixed later).

You might think that was a problem solved and a job well done, but that's leaving Darktable's geniuses out of the equation.

See, the _crop_ and _perspective_ modules are special modules : opening them enables an "editing mode" that disables any cropping to show the full image. This is needed to drag the cropping frame (or adjust other positionnings) from the main preview, over the full original image. Problem is, there was no explicit way of asking for a pipe recompute… other than adding a new history item. So the modules added a fake history item (later reverted) only to invalidate the pipe and call the `gtk_widget_queue_draw()` function. But then, that polluted the history stack with "empty" steps, so another guy added a special handling case that merged history steps if no parameter changes happened. But then, the history stack (from the _history_ module, as stored in database) does not follow the _undo/redo_ history stack, leading users to misunderstandings regarding what _undo/redo_ really does.

__And this, ladies and gentlemen, is how shitty design is promoting more shittier design in an endless sprawl of madness.__

Remember that all that stems from the need to make the pipe kill-switch work, so you can interrupt a recompute in the middle when you know its output will be discarded anyway. So for that I had to move the recompute request out of Gtk code, and call it everywhere required. But then I had to rewire the pipeline updating logic in _crop_, _perspective and rotation_, _liquify_ and _borders_ modules, and I still have to fix _retouch_ (which is the worse PITA of the lot).

Other than making it clearer to read, and possible to optimize the calls, the current logic also starts the pipe outside of the GUI thread, without waiting for Gtk to _please_ find the time to redraw the frame. As usual, people with crazy CPU will notice little to no benefit, performance-wise, which is probably why this is a non-issue in the Darktable team in the first place.

## Episode 6 : paying the back interests on the technical debt

So, at that point, I had made pipeline recomputes explicit from the modules and GUI controls, and dispatched them sparingly (which is the benefit of dispatching them explicitely). And still, I noticed that playing with modules coming late in the pipe was slow. In fact, launching `ansel -d perf` showed that
all the pipeline, starting at the _demosaicing_ module, was recomputed even though I was interacting with a late module that took its input from _color balance_.

Darktable has had a pixel cache forever. It basically stores the intermediate states of the picture, in-between modules. So, having pipe recomputes starting from much below the current module meant it was mostly useless. It turned out that the cache used only 8 cache lines, which is really under-using today's crazy amounts of RAM. But increasing that to 64 didn't help with cache misses : the cache was still mostly useless, and the most part of the pipe was still recomputed.

We need to pause a bit here. Even a mechanical engineer with no proper programming education like myself knows what an [LRU cache](https://en.wikipedia.org/wiki/Cache_replacement_policies) is :

1. you create a fixed list of slots (cache lines),
2. once you have something to cache, you allocate a memory buffer of previously-known size to one of those slots and assign it an unique identifier. That could be a a checksum, a random hash or even a timestamp, it just has to be cooked always the same way and lead to something unique,
3. when you need data associated with some unique identifier, you query the list of slots and search if that ID is known :
    - if it is, you fetch its associated buffer,
    - if it is not :
      - if you still have empty slots, you create the associated buffer and copy the data for later reuse,
      - if you don't, you clear out the oldest slot and reuse it to host your new data.

In that process, you only need to know the size of the buffers and the IDs. It's very general, you can cache anything, even different objects, your cache doesn't have to be aware of the content, not even how the IDs are generated. It's clean, it's elegant, it's unassuming, it's generic, I would trust it with my life because it's far more robust than whatever security system you find in modern cars.

So when something __that__ simple doesn't work, it's usually because someone tried something "clever" and failed. What the Darktable team typically does in that case, is `switch case` their way through all pathological corner-cases and make it into something even more complicated (by handling all exceptions manually with heuristics), just to ensure no one later has a chance to find the root cause of the error.

For example, there were attempts at reweighting the priority of the cache lines to ensure the module before the one currently edited in GUI was cached. Not only did it not work, but it re-inforced the ties between pipeline code and GUI code, in a way that was not even thread-safe (which is why it didn't work). GUI stuff should happen at the input and at the output of the pipeline computations, not in-between, because _again_, different threads, but also it violates the modularity principle (keep program layers separated and enclosed as much as possible), and this software __needs to stop__ making everything depend on everything.

Again, it took me 8 months, including mandatory breaks from that utter shitshow, to get to bottom of the problem in a way that leads to a __simplifying solution__. And I will present the findings in a linear way, like a story, but keep in mind I started discovering things in a fuzzy and random way because it's all scattered in the sourcecode, so it will look less messy than it truly was.

We start with the unique ID. What truly represents a module's state in an unique fashion ? Well, a "cryptographic" checksum of its internal parameters. Cool, so Darktable had that implemented for a long time. Except it didn't account for the module instance number, and dealt with all kinds of `if` in the process. Not complete, not robust, not even needed. Hash everything, the hash will represent the state of variables.

Yeah, but modules can be reordered, so how do we take care of pipeline order ? Well, you take all the hashes of all modules, in pipeline order, and start accumulating linearly. Great. Except Darktable actually had 2 of those, one for GUI purposes that started from the end of the pipeline (so, in reverse order), one for pipeline purposes, in the pipeline order but unaccessible from GUI (for example… to get an histogram), and again, both mixing that with all sorts of checks to handle special cases (color picker, mask preview, etc.).

Not to mention, the module internal state does not vary whether you are in the full preview or in the navigation thumbnail, in darkroom. And yet, the checksum was fully recomputed twice, once for each pipeline. Actually, make that four times, since there is also the GUI checksum (used mostly for _perspective_ and _retouch_ modules)

And, last but not least, when zoomed-in in darkroom, only the visible portion of the image (the _Region Of Interest_, aka ROI) is computed, meaning we need to keep track of where we are in the picture in our caching mechanism. But that was completely left out of the checksum. Big bug here, and old.

So, how did Darktable _still manage to "work"_, you ask ?

Well, by flushing more or less entirely the cache on any pathological operation : zoom, pan, mask preview, color picker, enabling/disabling _crop_ and _perspective_ modules editing state. That's a way of dealing with consistency without dealing with consistency : torch it. Making it mostly useless, as the very low cache hits stats show (just start `ansel -d dev` to show it).

How did I solve the problem ?

1. When a new module history entry is added, the parameters checksum is computed, taking parameters, masks, blending options, instance number, order in pipeline, etc. into account. Meaning all pipelines share the same checksum/ID here (possible future use would be to save it into database),
2. Before a pipeline is computed, we compute the global checksum of all modules, from start to end, taking into account the mask display state, the checksum of previous modules, and the ROI (size and coordinates). This checksum can be directly accessed later, without additional computation.
3. The cache deals with this global checksum, and only that. No ifs, no buts, no heuristics, no conditions, no workarounds.
4. Modules can request a cache bypass, for example when using color picker. This contaminates later modules in the pipeline before the pipe is computed, so the cacheless state is known early and doesn't affect upstream modules. That should only be a workaround before color pickers can actually use cache lines directly, and could be reused for future modules doing non-standard stuff (painting ?).

Benefits :

1. The module-wise, internal, checksum is computed once for all pipelines,
2. Because the pipeline-wise, global, checksum of each module is known before starting the pipeline recompute :
    - it can also be used for GUI synchronization, so I merged both Darktable checksums into one,
    - it is constant within the scope of the pipeline, allowing to share cache lines between several pipelines (for example, demosaicing and denoising) with limited thread locking issues[^2]
3. Modules doing weird things have an uniform and predictable way of requesting a cache bypass from GUI events, should they need it.

[^2]: The source code actually has a 10-years-old `TODO` comment detailing how to do that.

This logic is not only more efficient (fewer computations), it's also simpler and can be extended for interesting features. From the cache perspective, we deal with nothing else than a checksum, every module state of interest is convolved in it.

But, more importantly, the cache is finally useful, especially when going back-and-forth in editing history, using undo/redo, or enabling/disabling modules. Overall responsiveness of the GUI is much better.

I'm sure there are undiscovered caveats and details that I forgot to re-wire to the new logic, and the _retouch_ module is still mostly broken, but adapting to something that simple should be doable.

## Meanwhile in Darktable 4.8

1. Pipeline checksum is computed during pipeline runtime, so it's unknown outside,
2. Because of that, they didn't deduplicate the GUI vs. pipeline checksums… good luck tracking inconsistencies between both in the future,
3. [Their cache handling code](https://github.com/darktable-org/darktable/blob/master/src/develop/pixelpipe_cache.c) is more than twice as large as [mine](https://github.com/aurelienpierreeng/ansel/blob/master/src/develop/pixelpipe_cache.c) and uses heuristics (over the type of pipeline, kind of module, state of masks display, color picker use, and caching hints defined manually in modules) to work around issues. The cache is not content-agnostic anymore and good luck debugging these spaghetti.[^3]
4. They are still entirely computing the module parameters (internal) checksum twice, once for each pipeline,
4. It took them almost 2 years to get there (since 4.0 release),
5. I would love to see their cache hits/misses stats (do I want to revive my PTSD by opening that soft ever again ? I'll pass, thanks).
6. People who think having more monkeys waving their hands in the air guarantees better quality should stop thinking.


## Conclusion

The amount of time spent and recently-broken shit to fix to get there was properly unbearable, but it was made worse by code scattered in a non-modular fashion without a clear distinction between what belongs to the (G)UI, what belongs to the backend, what belongs to  module histories and what belongs to pipeline nodes. The cache thing only took 8 months, mostly archaelogy and reverse-engineering, on top of what was already done in GUI controls and explicit pipeline recomputes.

There are still issues to fix :

- the number of available cache lines is an user preference and doesn't check for available memory left on device,
- the histogram/scopes module is mostly broken by design, because it was handled through special heuristics (now removed) on a module that is invisible in GUI (`gamma.c`). The new logic makes it possible to force-cache it and to fetch the cache line from the GUI thread.
- internal module's histogram are not immediately drawn when going in darkroom,
- color-pickers handling could be simplified and made more elegant,
- history handling still have some corner cases.

However, since I'm refusing to "fix" anything if my fix doesn't make things more simple, that strategy is starting to pay because the code is a lot more linear, with fewer cases to test, and ultimately sligthly faster. As I make progress, it's slowly becoming more readable and more fixable. Then, of course, shaking the core of the software to that extent is bound to break things (which shouldn't break if the code was modular).

There comes the legitimate question asking : why bother fixing Ansel/Darktable ugly legacy and not move on to something better, faster and shinier (like Vkdt) ? Well, Vkdt (or anything else new) will stay a rough prototype, competing with other rough prototypes (that's _Open Source_ in a nutshell), years away from a generally-usable product. Adding another unfinished/half-assed prototype to the landscape will do no good. It would be nice to have something __not__ scruffy and fairly finished, for a change. Besides, the (very) old code of Darktable is clean and sturdy (well, for the most part), it's only the past few years that have taken a turn for the shittiest. `git blame` always shows the same 3 names on the really shitty lines, to a point where I sometimes find myself automatically deleting the corresponding lines when I saw who wrote them, out of habit.

There is also the fear that, no matter how fast Vulkan makes Vkdt, what really makes Darktable shitty is bad decisions, bad priorities, programming mistakes, lessons not learned, and if those mistakes are reproduced over Vkdt, it might take longer to realize the consequences with more horsepower, but ultimately things will go the same way. Having more resources makes it more affordable to be stupid… until it doesn't and you realize how trapped you are.

[^3]: It should be noted that "my" cache code is actualy pretty much how Roman Lebedev and Johannes Hanika wrote it 10 years ago. I simplified a couple of things, mostly removing stuff added since then, and added nothing of my own, because it's a Garbage In/Garbage Out situation where you should rather clean your input rather than trying to handle any corner case internally through unlegible heuristics.
---

# Rewriting the import tool : https://ansel.photos/en/news/rewriting-import/

Ansel inherits from Darktable its database backbone: the non-destructive editing histories are saved per-picture into an SQLite database, along with metadata and other user-defined data. Making the database aware of new pictures is done through "importing" pictures from a disk or a memory card. That's where the import tool comes.

Unfortunately, the Darktable importer is another thing that was butchered circa 2020 and turned into something deeply disconcerning, as it is a file browser that resembles no previously-known file browser, and manages to lack basic features (like Ctrl+F or EXIF preview) while still being bloated with useless ones (see below). This is where we loose many a future user, and it is only step 0 of the workflow. What a great showcase of what a "workflow app" can do !

## A design review of what design by commitee will get you

So Darktable has the import button<em>s</em> on a sidebar widget. There are 3 of them if you plugged-in a camera through USB, 2 otherwise:

![](darktable-import-module.jpg "The import module in Lighttable")

Many forum posts have been lost on explaining times and times again what the difference was between _add to library_ and _import and copy_ (_import_ actually means _add to library_). It is also worth mentionning that the _mounting camera_ button does nothing more than what any system-wide [PTP](https://en.wikipedia.org/wiki/Picture_Transfer_Protocol) or [MTP](https://en.wikipedia.org/wiki/Media_Transfer_Protocol) driver does for you: mounting the internal SD card as an external USB storage, through the USB port of the camera. On Windows this requires installing a custom USB driver that will prevent your camera to work with the rest of the OS. On Linux, depending whether your OS has "auto-mount" enabled, you might have to dismount the camera from your desktop file browser first, otherwise it will lock the link. On all OS, this requires listening to new devices in background all the time so the application can react to hot-plugging, whether or not you are going to use the feature.

And then, there is the _parameters_. Since 2022, the Darktable team has fell madly in love with those collapsible trays that hide the bloat under more bloat. And the bloat you can find in there does nothing more than hitting <kbd>Ctrl+A</kbd> (select all pictures), and then <kbd>0</kbd> or <kbd>1</kbd> (to assign an initial rating of 0 or 1 star) or filling the metadata info (in batch) in the eponymous module, which stands in the lighttable GUI whether you like it or not, and supports presets in case you always reuse the same metadata. The reason why adding these parameters there was even a requirement in the first place is beyond me. Someone, somewhere, is probably sparing 0.5 s per import session, which the rest of the world is losing by scrolling more height.

So, provided you understood the differences between all those options, now you click _import and copy_, which is probably the most used since any photographer needs to empty the SD cards to a more permanent stofrage. And then…

![](darktable-wtf.jpg "Did you pass your file-copying exam before using this feature ?")

This is basically admitting that the design is so terrible that it's verging on unsable without reading the doc. But wait, what are we doing again ? We are copying pictures. We are copying pictures ? So did you pass your picture-copying exam and got your picture-copying degree ? If you didn't, you should. Watch out, they might turn this into a quizz in a future version and disable the feature if you fail it.

So you click on that ominious button saying that you take full responsibility for whatever harm may happen from copying pictures. Or you don't, given that it looks kinda scary. But let's say you do…

![](darktable-import-window.jpg "All I wanted was pictures…")

Wait, where are the pictures ? Is that a log reader ? Throw a 12 dice, and if you do an 8, you will figure out that those eyes icon are responsive and, once clicked, display a thumbnail. Hell, if you click on the eye in the column header, it will show all thumbnails at once. And mess up the row spacing, but we run for the chocolate medal here, so it's fair enough:

![](darktable-import-images.jpg)

Be seated for the rest, you are not ready:

1. The _folders_ thingy on the left column is actually a button: when you click it, that changes the order of appearance of those folders.
2. The _places_ frame tries to emulate the _favourite_ or _bookmarks_ from your OS desktop manager, but don't import them.
3. The _recursive directory_ checkbox is not an option, it's an action. But then it doesn't act on whatever you selected on the right column, because those are only pictures. What it does is take the folder selected on the left column, and populates the list of pictures by recursively crawling the selected folder and its children.
4. The _select new pictures_ is, once again, not an option but an action, aka something that the rest of the world puts in a button.
5. The field _override today's date_ is a workaround for the fact that the picture `EXIF` date can't be used in folder patterns, so by default we use today's date. Except if this field is filled. But it needs to be filled with a datetime following ISO 8601 format. Which I didn't here, and the window is not complaining at all because form inputs validation is less fun than letting users find out at the next step that they have to start again from scratch because of bad datetime format. Boy, that's a lot of background knowledge to have to fill out a goddam date, I'm very lucky I learned all that from reading the source code.
6. The _keep original filename_ option is fully redundant with using the naming pattern `$(FILE_NAME).$FILE_EXTENSION)`,
7. The naming patterns use variables masks `$()` that get replaced at runtime by actual file's properties, but you need to know them to be able to use them. As if a guy named Houz didn't write a library circa 2011 to implement auto-completion in text inputs for those variables, that you can simply call in your code to make it work in 2 lines (it is used in export module).
8. The naming patterns found there are duplicated in Darktable's preferences popup because you might need to change them when you are not importing anything.

This is made of the material of frustration. What is truly amazing is how many persons have worked on this feature over the years to get there. Some would call it [collective intelligence](https://en.wikipedia.org/wiki/Collective_intelligence), but it's actually called [design by commitee](https://en.wikipedia.org/wiki/Design_by_committee).

## A new take, or erasing scruffiness out of step 0

Importing images is truly the step 0 of working with any image editing application. It's unacceptable to lose users so early, especially since we expect so much from them later, when it comes to understanding light and color. But I will argue that light and color are the bread and butter of any graphic artist, and it seems fair to expect from photo-editing users to master the basics of their craft at some point.[^4] On the other side, asking users to get their master's degree in file copying is unacceptable.

[^4]: Yes, I know it's already too much to ask in most cases.

Guillaume Stutin and myself have therefore rewritten the import tool entirely, first the front-end and then the back-end. Here is the result.

![](ansel-import-menu.jpg "The import window is opened from the global menu, optionally with the shortcut")

The option to directly mount cameras memory card from GPhoto2 has been removed entirely, since OS can handle this step directly if needed. There is only one entry point to import. Then you get the filechooser window:

![](ansel-import-window.jpg "The import window is a vanilla Gtk filechooser")

The file browser is a native Gtk filechooser widget, meaning the OS bookmarks (and even here the Nextcloud synchronized folders) are automatically imported from the desktop environment. The picture preview has been extended with EXIF metadata as to better identify images. A look-up into the library database shows whether or not the picture was already imported into Ansel or Darktable, and where.[^1]

[^1]: This reuses the Darktable code but with a clearer display.

Image types (raw, non-raw or all) are filtered using native Gtk filters and the full list of files to import is built straight in the GUI, without second-guessing in the back-end. When folders are selected, their content is automatically crawled recursively and the first image of the collection is shown. The final number of images detected is displayed below the file chooser:

![](ansel-import-recursive.jpg)

Many sorting and display options are natively handled by the Gtk filechooser, including search (through <kbd>Ctrl+F</kbd>), without additional work:

![](ansel-import-search.jpg)

The file handling option enables the import with or without copy, the label explains the difference in full words:

![](ansel-import-copy.jpg)

The _project date_ is automatically set to today's, no override or anything that would suggest something undisclosed is otherwise happening by default. To set the date, a calendar widget is provided which automatically formats date in the right ISO format. If the date is typed directly in the text input, a format validation is performed and an error feedback is displayed:

{{< gallery cols="2" >}}
![](ansel-import-calendar.jpg)
![](ansel-import-date-validation.jpg)
{{</ gallery >}}

Note that only the date (year, month, day) is supported by the calendar widget. Setting the time (hour, minute, second) needs to be done manually in the text field. Surveys show this is rarely needed.

The folder and file naming patterns support variables auto-completion automatically, when `$(` is typed. The new import back-end supports using EXIF fields of images to define folders names, for example a folder pattern like `$(JOBCODE)-$(EXIF.ISO)` would split pictures into several folders named after their ISO metadata. When this happens, the lighttable opens the last created folder when the import window closes:

![](ansel-import-autocompletion.jpg)

Last but not least, an user-requested feature: being able to preview the result of the patterns defined on the currently-selected picture:

![](ansel-import-pattern.jpg)

The pattern result follows the selected files and folders, as well as any change in the main folder and patterns. Note that the base directory (for all projects) doesn't accept patterns anymore, since there is no reason to put patterns everywhere, which allows to use a safe and simple Gtk folder list widget. This way, at least the top-level Ansel folder is set in a robust and user-friendly manner, and if harms happens from naming patterns, it will be enclosed within something predictable.

## Conclusion

It should be noted that this simpler graphical design comes also with a much simpler code structure, fewer lines and a more linear logic. The front-end needs 1200 lines, and the back-end around 400 lines (comments included, _the new backend has Doxygen docstrings almost everywhere_). A lot of the front-end volume of code comes from the recursive folder crawler, which runs in a separate thread to not block the GUI thread, but still updates the file counts periodically for feedback. This has been tested with folders of more than 60.000 images.

Darktable has more than 2700 lines of front-end and more than 800 lines of back-end (no docstrings). Also, the Darktable architecture expects one target folder per import session, which forbids to use per-picture EXIF in folder's naming patterns, and has completely different code pathes to handle the with copy/without copy discrepancies. This makes it difficult to keep both pathes in sync because they still share some bits (adding to the database and initializing metadata). In Ansel, we merged both pathes with only an optional initial I/O to copy the files if needed.

Ansel's front-end and back-end import communicate through a stupid-simple list of file pathes, which means the back-end is not second-guessing whatever user decision was made in the GUI. Also, all GUI code has been removed from the backend. This is a generic and robust design that will allow alternative ways to produce a list of files to import, for example headless (from a script). Previously, things like JPG filtering or files re-ordering happened in the back-end.[^3]

[^3]: The whole notion of back-end vs. front-end is new to the Darktable ecosystem, as you will find GUI (Gtk) functions pretty much everywhere, protected by a recurring `if(darktable.gui)` to prevent segmentation faults when the software runs from CLI.

The main appeal of the scrubbed design is that all the info needed to understand what's what is accessible right into the window, without having to open a documentation. In case of destination filename conflicts during import with copy, users are warned with a popup window and the existing files are never overwritten. Not to mention, the whole thing resembles a typical file browser.

Some users have also expressed the wish to use the editing features of the software without being bothered with the oddity of the internal digital assets management (the lighttable, mostly). This new importer allows them to open single images directly into the darkroom through double-click on file or click on file + click on the _import_ button, effectively bypassing the lighttable entirely.

The naming patterns have, of course, been removed from the preferences popup (which, by the way, can be found from the global menu, through _Edit_ → _Preferences_, as in any sane software).

_Many thanks to Guillaume Stutin for his tedious work on this topic_.

{{< note >}}
The new importer is available for Ansel build versions > 0.0.0+780.
{{</ note >}}
---

# Color lexicon : https://ansel.photos/en/resources/lexicon/

```mermaid
mindmap
  root((COLOR))
    color appearance model
      uniform color space
        chromaticity
          U, V
          a, b
        lightness
          L
        delta E
      chromatic adaptation transform
        illuminant
          color reproduction index
          color temperature
      surround lighting
      background lightness
    dimensions
      Munsell
        hue
        chroma
        value
      natural color system
        blackness
        saturation
        hue
      CIE
        lightness
        brightness
        saturation
        chroma
        colorfulness
        hue
    measure
      colorimetry
        tristimulus
          sensor
            Luther-Ives criterion
            metamerism
            dynamic range
            noise
            mosaicing
              Bayer
              XTrans
              zipper artifacts
          spaces
            rgb(RGB)
              HSV
              HSL
            LMS
              Yrg
            XYZ
              Yxy
                Yuv
                  Ych
            CYM
              CYMK
            primaries
              cone cells
              LED
                ITU BT.Rec 709
                ITU BT.Rec 2020
                DCI P3
              inks
      spectrometry
        light spectrum
          wavelengths
          energy
      photometry
        luminance
    correction
      profile
        matrix
        lookup table
        transfert function
      color grading
        ASC CDL
        channel mixer
        curves
      white balance
```
---

# Implementing kill-switch on pipeline : https://ansel.photos/en/news/implementing-kill-switch/

I have thought, for a very long time, that there was some kill-switch mechanism on the pixel pipeline. The use case is the following :

1. you are changing a module parameter,
2. the previews (the central darkroom one and the thumbnail in left panel, also used for histogram and color pickers) recompute their pipeline to account for that change,
3. one of the previews finishes rendering before the other, and the result is obviously __not__ what you wanted,
4. you change again the module parameter, without waiting for the recomputation to finish.

In that case, you want to kill all active pipelines because their output will not be used, and start recomputing everything immediately with new parameters. Except Darktable doesn't do that, it lets the pipeline finish before restarting it, and looking at the comments in the source code, it seems to be a fairly recent regression and not the originally intended behaviour.

I have (re)implemented this feature in Ansel, but it's tricky because we are dealing with different threads (GUI, editing history and pipeline on CPU/OpenCL) and we need to synchronize them properly.

If this goes bad, you may experience garbled previews from inconsistent cache states. This is a GUI issue only, and the mitigation strategy is to go to the global menu -> Run -> Invalidate all caches.

To debug, you may start Ansel with :

```
$ ansel -d perf
```

When interacting with sliders and comboboxes in GUI (for example here in exposure module), you will get :

```
100,407003 [dev_process_all] sending killswitch signal on running pipelines took 0,000 secs (0,000 CPU)
100,509816 [dev_pixelpipe] took 0,011 secs (0,023 CPU) processed `exposure` on GPU, blended on GPU [full]
100,510498 [dev_pixelpipe] took 0,019 secs (0,035 CPU) processed `exposure` on GPU, blended on GPU [preview]
100,533228 [dev_pixelpipe] took 0,023 secs (0,061 CPU) processed `lens correction` on GPU, blended on GPU [full]
100,558939 [dev_pixelpipe] took 0,026 secs (0,129 CPU) processed `tone equalizer` on CPU, blended on CPU [full]
100,563703 [dev_pixelpipe] took 0,005 secs (0,008 CPU) processed `unbreak input profile` on GPU, blended on GPU [full]
...
```

Normal behaviour is the `[dev_process_all] sending killswitch signal` line should appear at the time of your interaction, and should be followed by a recomputation starting at the module you interacted with (not the one before, not the one after), up to the end (display encoding module).
---

# Un-darktable-ing GUI controls : https://ansel.photos/en/news/undarktable-ing-gui-controls/

Darktable has its own GUI widgets library, for sliders and comboboxes (aka drop-down menus or selection boxes), called Bauhaus (in the source code, it's in `src/bauhaus/bauhaus.c`). While they use Gtk as a backend, Bauhaus are custom objects. And like many things in Darktable, custom equals rotten.

In 2022, ‍I noticed [parasite redrawings and lags](https://github.com/aurelienpierreeng/ansel/issues/29), when using them, leading to a frustrating user experience : the widget redrawing seemed to wait for pipeline recomputations to complete, which meant that users were not really sure their value change was recorded, which could lead them to try again, starting another cycle of expensive recomputation, and effectively freezing their computer for several very frustrating minutes of useless intermediate pipeline recomputations.

As it happens, static code analysis tools find that the Bauhaus library is also the [4th most complex sourcecode file](https://sonarcloud.io/component_measures?metric=complexity&selected=aurelienpierre_darktable%3Asrc%2Fbauhaus%2Fbauhaus.c&view=list&id=aurelienpierre_darktable) in the whole Darktable software in terms of cyclomatic complexity, with a score of 735 and a technical debt estimated to 1 day and 7 hours. If you are no programmer, cyclomatic complexity measures the number of different paths the code can take, and high values make it not only more difficult to understand (hence to debug), but also more prone to edge-cases, bugs and contextually-dependent weird issues. [Cyclomatic complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) is an indirect metric of the probablity that this code will blow up in your face when you least expect it, something to take into account when the most prolific "developers" in your team are a primary school teacher, a pediatrician and a banking consultant.

What's particularly frustrating is I already worked to simplify this file, back in 2019. 3 years later, it was as if I did nothing, thanks to [feature creep](https://en.wikipedia.org/wiki/Feature_creep) and MIDI/gamepad support. When the parasite redrawings showed up, I was left with unintelligible [spaghetti code](https://en.wikipedia.org/wiki/Spaghetti_code) that I was properly unable to fix. A first attempt to correct wrong stuff lead to a dead-end, back in August 2022, and got me discouraged. Trying to work around it was not going to cut it.

So I had to rewrite it almost completely, which was no fun and took me a crazy amount of hours (I stopped counting at 3 weeks, full-time, and that was for the second attempt in August-November 2023).

Did you know that, once a combobox had the focus (either because you clicked on it or gave it focus via a keyboard shortcut), you could start typing the first letters of the label you wanted to select and it would automatically select the closest item in the list ? Neither did I before undertaking this task, because it's documented nowhere. As were many hidden features in there, added to comply with deviant and marginal use cases, but complexifying the code structure for everybody. (Spoiler alert : I kept this particular feature, but removed others).

## ‍List of improvements

- Cursor coordinates (in popups) are computed only at once place, then stored. That saves a lot of intermediate recomputations, some of them being inconsistent because the code was copy-pasted and duplicated instead of using [getters and setters](https://en.wikipedia.org/wiki/Mutator_method). Now, coordinates offsets and changes are handled through unified getters and setters, meaning any future change will need to happen only in one place, and the whole code uses that.
- New values (from sliders and comboboxes) are not dispatched anymore to the pixel pipeline during scrolling or drag-and-dropping, but only at the end. This relies on a machine-learned timeout recording the average time needed to compute a full pipeline. The GUI will wait for 20 ms to 2.5 s to dispatch changes to pipeline, avoiding to recompute at every scrolling or dragging step, which makes for useless and redundant, yet expensive, computations that only make the software lag. Similarly, they are now dispatched only on the button-released event, instead of button-pressed and button-released events (given that a typical mouse click sends both events). It should spare quite a lot of needless pipeline recomputes.
- Widgets are redrawn immediately on user events, before the new values are dispatched to the pixel pipeline. This ensures immediate user feedback, even though the actual pixel result may come later, and limit frustration on slow computers.
- Do not dispatch value-changed events if the widgets got user interaction but their value didn't actually change.
- Capture clicks on the comboboxes chevron (right arrow). Previously, you needed to click on the label to unroll the combobox drop-down menu, which was super frustrating if you came from software with a proper GUI. The chevron itself didn't respond to clicks.
- Do not scroll comboboxes drop-downs. That feature started like a cool project : having the currently-selected item aligned with the label. Problem is the drop-down popup positionning is ultimately handled by the desktop environment, we can only ask it politely to do what we wish, but there is no guarantee our wishes will be honoured. Also, it is possible to make the popup roll outside the viewport, nothing prevents it. All in all, it's brittle and random, better stick with default window positionning.

## Deprecated features

The ability to assign keyboard shorcuts to sliders and comboboxes is for now removed. Sliders and comboboxes are anyway linked to arrow keys and mouse scroll __once they get the focus__[^1], and it is still possible to assign focus capturing to a keyboard shortcut. The current logic is therefore to request focus through keyboard shortcuts, then edit the value using arrow keys. Focus requests also automatically make the widget visible in GUI, scrolling the sidebar if needed.

[^1]: In GUI programming, a widget has the focus when it is the one recording keyboard events. Text entries are the most obvious example, but Darktable hacked that concept to generalize to pretty much every widget.

Anyway, the current shortcuts system will have to be entirely replaced by native Gtk accelerators, which are already used for the global menu (and were used in Darktable prior to 2021). Currently, we have both systems, one of which being a monstruosity of complexity and probably responsible for slow-downs. KISS.

## Caveats

The "format" combobox in export module doesn't initialize its value properly. This is a non-standard Bauhaus widget that needs extra care. For the time being, you will need to refresh the storage option or to reload a preset.

Whenever you remove the flaky old paint that held the rusty walls, you risk doing collateral damages. Be careful with Ansel when using AppImage and Win EXE tagged Ansel-57ed58d from tonight and report anything weird.

## Downloads

- [Ansel-57ed58d-x86_64.AppImage](https://github.com/aurelienpierreeng/ansel/releases/download/v0.0.0/Ansel-57ed58d-x86_64.AppImage)
- [Ansel-57ed58d-win64.exe](https://github.com/aurelienpierreeng/ansel/releases/download/v0.0.0/ansel-57ed58d-win64.exe)

## Geeky details

The current changes have reduced :

- cyclomatic complexity from 735 to 494
- cognitive complexity from 796 to 432
- technical debt from 1 day and 7 hours to 4 hours and 50 min.

[More details](https://sonarcloud.io/component_measures?metric=cognitive_complexity&selected=aurelienpierreeng_ansel%3Asrc%2Fbauhaus%2Fbauhaus.c&view=list&id=aurelienpierreeng_ansel)

If you are no programmer, these metric just mean that the code will be easier and less time-consuming to maintain in the future, and probably less bug-prone.
---

# Changes in distribution support for Linux AppImage package : https://ansel.photos/en/news/changes-appimage/

Rawspeed (the library providing the decoders for camera raw files) has deprecated support for GCC < 12. As a result, I can no longer build the AppImage on Ubuntu 20.04 (using Github runners) but I have to build it on 22.04.

It means any Linux distribution having libc older than 2.35 will not be able to start the new AppImages starting today. That should not affect most users running distributions upgraded in 2021 or more recently. Ubuntu 20.04 and other LTS/old stable distributions (Debian stable) may be affected.
---

# Printing : https://ansel.photos/en/workflows/printing/

The [scene-referred](./scene-referred.md) workflow promises an editing independent from the output medium. It will typically produce an image encoded in sRGB colorspace with 8 bits, that is code values between 0 and 255. To simplify, we will consider here only the 8 bits case. Concepts are the same in 16 bits, only the coding range goes from 0 to 65535, which is anecdotal.

## The printing problem

Unfortunately, nothing guarantees that the printer is able to use the whole encoding range. The minimum density (_Dmin_ in analog) is reached with naked paper, and matches an RGB code value 255. The maximum density (_Dmax_ in analog) is reached with 100% ink coverage.[^1] Problem is, if Dmin matches an RGB code value of 255, Dmax never matches an RGB value of 0.

[^1]: Printers reach deeper blacks by mixing pure black ink with all of CYM inks.

To understand the problem, I generated a synthetic chart of sRGB code values from 0 to 59 (over 255) and printed it on regular office paper, with an old photo printer, then scanned it. The grid in-between the patches is pure black (RGB = 0).

{{<compare before="charte-noirs.jpg" after="chartes-scan.jpg">}}
Before : digital original ; After : scanned print without correction.
{{</compare >}}

The printed blacks are muted, compared to the digital original, but it's not the worst part : the patches below 0.12 % are completely blended into the 0 % grid, which means that all RGB code values below 5 / 255 are printed at the same density and end up in the same black blob.

Said otherwise, our printer black saturates at 5 / 255 and we will not be able to resolve details in deep shadows without proper correction. But why ?

While 8 bits sRGB (with OETF) can theorically encode a contrast ratio of 6588:1 (that is, a dynamic range of 12.69 EV),  LED displays can typically render a contrast of 300:1 to 1000:1 thanks to __emissive__ whites having a controlled intensity. On paper, whites are __reflective__, and the contrast adjustment variable is therefore black ink, absorbing incident light. The renderable contrast on paper prints varies between 50:1 (Dmax 1.7) and 200:1 (Dmax 2.3), that is a dynamic range varying between 5.6 and 7.6 EV.

During printing, we unroll the file dynamic range into the paper dynamic range, starting at white. 5.6 EV below white, we hit the dynamic range limit of black ink on matte paper, but we are not even half-way through the digital file dynamic range. All tones included between -5.6 and -12.7 EV below white, in the digital file, are printed with the same black density : the printer reached the maximum inking.

{{<table>}}
| Object | Contrast ratio | Dmax | Dynamic range |
|--------|----------------:|------:|---------------:|
| 8 bits sRGB (with OETF) | 6588:1 | 3.8 | 12.7 EV |
| 8 bits linear RGB | 510:1 | 2.7 | 9.0 EV |
| 12 bits linear RGB (raw photos) | 8190:1 | 3.9 | 13 EV |
| 14 bits linear RGB (raw photos) | 32766:1 | 4.5 | 15 EV |
| 16 bits linear RGB (raw photos) | 131070:1 | 5.1 | 17 EV |
| matte paper print | 50:1 | 1.7 | 5.6 EV |
| glossy paper print | 200:1 | 2.3 | 7.6 EV |
| ICC standard PCS black point | 287:1 | 2.5 | 8.2 EV |
| Eizo Color Edge CG319X | 1500:1 | 3.2 | 10.6 EV |

_Equivalences of contrast units : all of them represent the same span between pure white and pure black luminances, but measured differently._
{{</table>}}

Printing all digital tones between -12.7 and -5.6 EV to the same density on paper means, in practice, flattening details and texture to a solid black blob. To avoid that, we will need to remap the sRGB dynamic range to the paper dynamic range, which in our example here means pushing RGB code values between 5 and 255.

{{<figure src="sRGB-to-printer-zones.png" caption="The tonemapping problem from digital sRGB to a 50:1 contrast paper, as shown by Filmic zone system graph" />}}

Let's see the difference it makes on a real image having a lot of content in deep shadows :

{{<figure src="Shooting Minh Ly-0155-_DSC0155-Minh-Ly-resized.jpg" caption="Digital original" />}}

{{<compare before="Shooting Minh Ly-0155-_DSC0155-Minh-Ly-no-bpc.jpg" after="Shooting Minh Ly-0155-_DSC0155-Minh-Ly-bpc.jpg">}}
Before : print simulation at contrast 66:1 without black point compensation ; After : print simulation at contrast 66:1 with black point compensation.
{{</compare >}}

Black must be fixed to retain details in the hair, even though it is done at the expense of contrast in the neck.


## Understanding black point compensation

The [black point compensation](https://www.color.org/WP40-Black_Point_Compensation_2010-07-27.pdf) has been invented by Adobe and standardized by the ICC later. It is a simple black offset meant to raise (brighten) all RGB values above the black saturation threshold, such that we recover gradients in deep shadows, acknowledging that blacks will remain more muted than the original no matter what we do.

Unfortunately, the black point compensation by offsetting does not preserve hues and may shift colors. For this reason, [Capture One simply does not support it](https://support.captureone.com/hc/en-us/articles/360002654477-Black-point-compensation).

It should be noted that the black point compensation is the last resort, for when the [perceptual intent](https://www.color.org/v2profiles_v4.pdf) is not available in your output color profile (that is, when the `AtoB` and `BtoA` LUTs are not defined in the profile). This is the most common case when dealing with open-source printer drivers, because those LUTs have to be manually set by someone who understands this, and not by a mere calibration software. Therefore, the perceptual LUTs are typically found only in vendor-shipped color profiles, but those will not be fully accurate for your inkjet cartridges and paper set.

In the absence of defined perceptual intent, the color manager can fallback to the relative colorimetric intent and can use the black point compensation if the profile has some tone curve (the `TRC`). If you miss both the perceptual LUTs and the TRC, that is if you didn't calibrate your printer yourself, then tough luck : you will not be able to perform black point compensation by standard ICC methods.

Fortunately, Filmic lets you remap the scene dynamic range to any arbitrary medium dynamic range, through the _display_ tab, by raising the _target black value_ setting.

{{<figure src="sRGB-to-printer-curve.png" caption="Filmic black point compensation curve (log scale)" />}}

Because Filmic tone-mapping is a generic 3-points map (black, middle-grey, white), it lets you raise the black point without affecting middle-grey and white values, and with minimal impact on global contrast. Since Filmic manages hue and saturation as well, its black point compensation doesn't induce color shifts, unlike the Adobe method. The problem remains to find the proper target black value.

## Adjusting black point without profiling the printer

1. [Download the chart of blacks](charte-noirs.jpg download)
2. print it directly as an sRGB image, with no editing and no correction,
3. on the print, note the darkest patch that you can visually tell apart from the pure black grid, and record its percent value,
4. open the chart image in Ansel, and in Filmic module do the following :
   1. in _scene_ tab, click on the auto-tuner button,
   2. in _look_ tab, set the contrast to the minimum value (0.5),
   3. in _display_ tab, input the target black value you read on the patch previously,
   4. in _options_ tab, set the _contrast in shadows_ parameter to _safe_.
5. export the corrected chart and print again to validate the settings.


This is the result from the example here:

{{< compare before="chartes-scan.jpg" after="chartes-scan-bpc.jpg">}}
Before, the display black point is set at 0 % (no compensation). Patches from 0% to 0.09 % are fully blended in the grid. After, the display black point is set to 0.12 %. All patches stand out of the grid, but the grid itself lost some density. 0.9 % is would be a better setting.
{{< / compare >}}

{{< note >}}
The patches have all integer RGB code values in 8 bits, from 0 to 49 over 255, with the sRGB OETF on, which is what the printer driver will get when printing. The percents translate those values in linear Rec2020 RGB, for Ansel pipeline. It is useless to try and use intermediate percent values for black compensation, since the picture is ultimately converted to 8 bits sRGB by most drivers.
{{</ note >}}

{{<warning >}}
The black saturation level read on patches is not a metric of print contrast. Printers get images through computer drivers and process them with onboard electronics, at least to convert RGB to CYMK. Drivers and onboard electronics apply undisclosed image transformations that prevent us from finding absolute relationships between RGB code values and actual ink density, unless we perform a full profiling. What is done here is a kind of reverse-engineering to guess the sweet spot of the printer black, applied on top of the native image corrections performed by the printer. Refrain from drawing hasty conclusions from these readings.
{{</warning>}}


## Applying the settings to real images

When editing your picture, proceed as usual, without black point compensation. Filmic has a default black point compensation meant to deal with quantization errors when going to 8 bits, it is not linked to any particular medium, but only to 8 bits sRGB.

You just need to mind the brightness of your screen backlighting, compared to the surround brightness of the room in which you edit. If your display is a lot brighter than the surround, you may not be able to spot underexposed pictures until you print them. The correct backlighting is reached when a 100% white rectangle displayed on screen appears to have the same brightness as a white papersheet displayed next to the screen.

Before printing, change the Filmic _display black_ to the value measured on the chart above and export with _relative colorimetric_ intent.

{{< note >}}
A global _display black_ override will be offered in the GUI for temporary changes at export time, without having to change the image parameters, as well as a way to directly extract the black point compensation from a printing ICC profile, if available.
{{</ note >}}


## White point compensation

Users of Fuji Instax pocket printers have reported a similar issue, but with white. Fuji Instax, using a photochemical printing process, seems to add a lot of contrast in highlights, resulting in clipping above 75 % luminance, or so.

You can reproduce the steps above for white, using <a href="/en/workflows/img/charte-blancs.jpg" download>the charts of whites</a>. Note the darkest patch that starts blending into the white grid, and use the corresponding percent value for Filmic _display white_.

## About softproofing

Ansel is able to [softproof a print](/doc/views/toolboxes/soft-proof/), using LittleCMS2, if you give it a proper printer profile. Softproofing means converting the image to the printer color space, that is compensating the white and black points, then remapping color gamut, using the data contained into the profile and standard ICC methods. In practice, it will uglify the picture by desaturating it and removing a lot of contrast ("milky blacks"), in an attempt to come close to the printed result. There are some caveats there, though.

First of all, the softproofing shows what you would get out of a standard ICC pipeline if your printer conformed to ICC specifications. The softproofing is not written in the exported file, so applying the set of changes you see on screen is entirely up to the printer driver and photolab technician.

Then, even with muted contrast, a digital softproof on an emissive medium doesn't come close to an hardcopy on reflective paper. The practical usability of softproofing is at best anecdotal.

Finally, there is not much information to be gathered from a softproof, except that prints suck. The most you can do is check that the automatic color conversion behaves properly, in particular, check that they preserve smooth gradients and don't create solid blobs where your digital original has gradients.

If you enable the gamut check, you will almost always see that the deepest, richest colors are out of the printer gamut. Again, it's nothing to be worried about, these are remapped by the printer perceptual LUTs or Filmic gamut mapping.

My experience with softproofing and gamut check is they needlessly worry semi-skilled users, making them believe that they have something to fix manually to make those out-of-gamut alerts disappear. I have also seen several bug reports mentionning a problem with softproofing, because it veiled blacks, even though it's exactly the point. All scopes are useful only if you know how to read them, and data becomes information only if know what you are looking for.

## Filmic : complex solution to a tricky problem

The ICC pipeline aims at automating color spaces conversions, defining standardized methods using descriptive profiles. Doing so, it puts an heavy burden on photolab technicians, tasked with creating and operating these profiles, and experience has shown that most of them play it by ear in colorimetry.

Far from having made the pipeline more reliable, ICC standards have made it more complex, with magical and incomprehensible black boxes (the _Color Management Systems_), which specifications complexity could make us forget that they are not doing more than freshman-level maths. Printing digital photographs is more than ever a trial and error game, relying on test prints and empirical adjustments, involving software understood only by their developers.

The secret weapon of ICC profiles is the perceptual intent, relying on `AtoB` and `BtoA` tags duly filled in the profile file. These fields are to be found only in vendor profiles, and when they are, the technical trade-offs governing their design are opaque.[^3] In any case, the perceptual approach is limited because the ICC v2 LUT are valid only for a given source color space : typically, printer profiles expect sRGB or Adobe RGB space at the intput, and any other space invalidate their perceptual LUT. Given that any raw photo editing software works in large gamut RGB, you need 2 stages of gamut mapping (large gamut to sRGB, then sRGB to printer), each of them introducing color shifts more or less predictable.[^2]

[^2]: For more edifying details on the horror of gamut mapping intents in ICC profiles, see Argyll CMS documentation : <https://www.argyllcms.com/doc/iccgamutmapping.html>

[^3]: We would like to know, notably, if the gamut mapping picks the closest color, or enforces a constant hue, or constant luminance, etc.

In the absence of those fields, color management systems fallback silently to colorimetric intents, without notifying users. They have therefore no way to know what is going on, and the result is unpredictable. In most cases, switching between perceptual and relative colorimetric intents, in Ansel export, leads to the same result since open-source profiles don't have perceptual LUTs.

Filmic was born as a tone mapping system, with HDR to SDR conversion in mind. Since the beginning, its design has been as generic as possible, without any fixed assumption on black and white points for the output medium. It quickly appeared that gamut mapping could not be decoupled from tone mapping, because we operate RGB signals which modulation drives lightness, saturation and hue altogether, and the "color" vs. lightness decoupling is only a creation of the mind to better grasp things, but without technical reality.

Filmic got more complex over years to transparently solve the problem that ICC failed to solve with the perceptual intent. Instead of relying on a LUT which existence is not certain, built we don't know how, assuming a rigid but undocumented source input color space, Filmic lets users parametrize themselves a perceptual intent by defining :

- the black/white points of the input space (scene) and output space (display),
- the contrast curve for tone mapping,
- an optional normalization of colors through RGB norms preserving original saturation (similar to the saturation intent), or without normalization (similar to perceptual intent), or a mix of both (since version 7),
- a gamut mapping to the output space by saturation soft-clipping at constant luminance and hue (since version 6).

By exporting images to the printer color space (or otherwise to sRGB), by defining the black point compensation in the _target black_ of Filmic and by writing the _relative colorimetric_ intent in the exported file, you can bypass the opaque and unreliable color correction steps of the ICC workflow, and manage color space resizings internally.

The misunderstanding and the ignorance of those problems lead Darktable 4.0 to introduce the _Sigmoid_ module as a simplified alternative to Filmic, which can afford to be far simpler since it fulfills 25% of the requirements and moves the software back 5 years.
---

# Explaining Ansel redesign of module groups : https://ansel.photos/en/news/modules-groups-redesign/

If you come from Darktable, you may be used to this in the darkroom:

{{< figure src="organization.png" />}}


while Ansel offers you this:

{{< figure src="ansel-module-groups.png" />}}


This is no accident, and it's time to explain why, and why this will not be extended with customization options.

## Images are born from pipelines

A pixel pipeline is a sequence of filters in which pixels are processed to end on a medium. Photoshop calls those filters [_layers_](https://helpx.adobe.com/photoshop/using/layer-basics.html), abiding by a methaphor inherited from paper and matte painting. Da Vinci Resolve, Blender, Natron, etc. calls them [_nodes_](https://filmmakingelements.com/everything-about-nodes-in-davinci-resolve/), abiding by a metaphor grounded in directed graphs and [flowcharts](https://en.wikipedia.org/wiki/Flowchart), best known to engineers. Both have a way of showing how those filters are organized, either with a layer stack or with the node graph (aka flowchart).

The important part is, __order matters__.

## A brief history of bad design

Darktable calls those filters modules. But "modules" refers to the [modular programming logic](https://en.wikipedia.org/wiki/Modular_programming) : each module is coded separately, using an uniform API, and doesn't know about the other modules. The pipeline itself doesn't know anything about the internals of modules, it only wires the inputs and the outputs. It's a clean way of developing, but it's completely irrelevant to the end-user.

The problem is Darktable has 2 kinds of modules :

1. the lighttable modules (and the modules in the left panel of the darkroom), which are arbitrary toolboxes and therefore __purely GUI elements/frames__,
2. the darkroom modules, which are both __a pixel filter__ located somewhere in the pipeline, and also __a GUI toolbox.__ (same as the previous modules).

And those different modules, on top of being named the same, look exactly the same…

{{< figure src="ansel-lighttable-modules.png" />}}

That's 3 mistakes here :

1. naming a GUI object by its technical implementation name instead of its functionnal purpose,
2. naming and representing 2 conceptually-different objects the same way,
3. failing to represent the order of the modules in a clear, hierarchical way.


As a result, many users still consider all modules to be arbitrary toolboxes, and have asked for years ways to reorder them arbitrarily in the window, which a spineless (lack of) technical leadership gave to them, in the shape of a terribly-coded (3500 lines of code, subtly broken) and overcomplicated module group, eating 3 % of your CPU even when you don't interact with the application, for as long as your darkroom sits idle.

The cherry on the sunday is the groups are labelled by icons, for the sake of compactness, but those icons are absolutely cryptic and only the long-time users pretend to know what they represent (I have drawn the one representing light rays going through a thin lens, that people take for an UFO – I learned from that mistake).

## Good workflows are pipeline-aware

It's been 3 years that I'm paid by users to explain them the ins and outs of the software, and answering the same question over and over : where to start a workflow and how to unroll it. What still strikes me is that people with a master's degree, who read the doc and watched most of my videos, are still unable to start an image editing workflow by themselves. Either this is screaming bad design or most people with higher education are idiots. Actually, even if people were idiots, it's easier to make the design idiot-proof[^3] than to expect them to grow a brain overnight, so either way the design is bad with regard to the target audience.

[^3]: And I mean "idiot-proof" in a ["prevent pouring water into the acid"](https://www.youtube.com/watch?v=SH3a8ZQLA4I) way, not in a "cancel chemistry labs because acid can burn" way. It's not idiot-proof if the idiot is not allowed to do anything.

If you open Photoshop, the layers stack upon each other pretty intuitively. We all worked with layers for art projects in primary school. It wouldn't strike you to start working on the bottom-most layer after you put some new stuff on top. Well, the 70-something modules of Darktable, organized in tabs by theme, in a way that doesn't account for the pipeline nor for the workflow, are guaranteed to deter the newcomers and to promote bad habits among the old-timers.

Sane workflows are pipeline-aware, which means the order in which you tune the filters should be defined by where those filters sit in the pipeline. But I say pipeline-aware, and not pipeline-defined, because the beginning and the end of the pipeline (properties of the scene and of the display) should be set first, as to have a good look over what we do in-between. Especially if you are going to manipulate HDR signals on an SDR display, you need to put on your HDR sunglasses first to view your signal in SDR. But what you see is not what there is in your pipeline. Hence why the workflow doesn't follow 1:1 the pipeline, but still s pretty close to it.

Imagine you set a color cast in _color balance_ module, targeting highlights through the _gain_ setting. Then you find the picture too dark and brighten it with _exposure_ module. But _exposure_ comes (way) before _color balance_ in your pipe, so now you need to update the color cast setting because it will probably be too heavy on midtones. Now, convolve that with another intermediate module (or more) that would use a parametric mask on any metric of lightness or luminance… You are on for __circular editing__, a particularly unefficient kind of frustrating editing experience where any new setting invalidates the previous. Of course, there are those who think that, photography being an art, it's all a matter of opinion and preferences, so ultimately none of this matters. Art or not, a house of cards will fall entirely anytime you start messing with the lower stories, so ultimately it's about how much time you accept to waste, and this has nothing to do with opinions or preferences. I would also argue that week-end hobbyists are just as time-constrained as professional photographers : the latter for economical reasons, the former because week-ends only have 2 days and they will need to be back at the office on Monday morning with enough fun in their system to endure another week.

So how do you know when to drift away from the pipeline order ? Well, you book a session with me for the demo. But there is another solution (more on that below)…

In any case, offering users more options to customize the UI (and perhaps re-enforcing the initial misconception of modules being only GUI boxes) is not going to solve it. It's actually giving people more options to harm themselves. What you want and what is good for you…

## Re-examining the problem

While Darktable has degraded into a playground for geeks where new means better and every problem calls for more funny code, Ansel is about solving simple problems the simple way, as to produce a reliable workhorse. So let's start again from the top.

We have 70 modules. Though Ansel has deprecated a fair deal of them, there are still "too many", in the sense that they are all useful for some purpose but you don't need them all the time, and not all at the same time. Also the screen real-estate is limited and we definitely can't have all of them displayed at the same time. And even if we could, presenting and Airbus dashboard to your average photographer wouldn't be nice.

So we need to chose what modules to display at what time. Emphasis on __time__.

## Unrolling the time axis

Following on the idea of _just in time_, it seems only natural that the time axis would be splitted into workflow steps. So the selection of all visible modules at a given time matches the ones that you will need right now and in the next minutes. Moving on to the next workflow step, you move on into the GUI and change the view. It's called a slideshow.

This draws a linear path to follow, to get some structure and guidance out of the apparent clutter. GUI are not only meant to expose controls, they are also meant to teach, communicate and advertise the available possibilities.

So each tab is now a slide of our workflow slideshow, which is closely tied to pipeline order. And structure appeared out of clutter.

With some exceptions. For example, denoising modules have to happen early in the pipeline for signal consistency, yet they appear later in the workflow than, say, _color calibration_, because they work at pixel level and will generaly not change the global color cast (unless you have some serious noise damage that might shift the green/magenta axis, but that's typically above 8000 ISO). Same with sharpening algorithms : none of these will dramatically change lightness, hue or chroma as to invalidate previous (workflow-wise) global color and exposure settings, and the proper settings will also be subjected to how much you raised the picture exposure (thereby aggravating the visual strength of noise). Those exceptions to the rule are made clear by numerical analysis of the pixel filters, meaning that people who didn't read the sourcecode with prior signal processing knowledge will have no clue.

## Implementation

### Principle

Workflow steps == module tabs.
: Those tabs have textual names, which might take more GUI space but you don't have to read a doc and/or guess what they mean : it's written on the label.

The first and last tabs are special
: They show respectively the list of enabled modules (pipeline) and the whole list of available modules (all).

Not all tabs are immediately visible
: Depending on sidepanel width, some tabs will be hidden, which is fine because you are going to follow them from left to right in sequence, so you don't really need to know what's coming after

Inside tabs, modules are organized as layers in the pipeline order
: That is from bottom to top. That's how you should set them. So the stack of modules represents the stack of effects/filters/layers on top of the raw image.

TL;DR: __follow GUI order from left to right, and bottom to top (because it's layers), and you have your workflow without tedious doc reading.__

The darkroom modules can be reordered in any tab by holding <kbd>Ctrl+Shift</kbd>[^2] while doing a drag-and-drop with the mouse, over the module headers. Be aware that this reorders modules in the pipeline too, it is not to be used as a GUI convenience. It is best to this in the "pipeline" or "all" tabs, where you have a full look over the pipeline content.

[^2]: It's shitty but that's because Gtk's way of handling drag and drop events sucks.

### Linear navigation

__With the mouse__ and pointing devices, tabs can be navigated by:

- clicking the ones that are visible,
- clicking on the arrows to reveal the previous/next tabs (visible or hidden),
- right-clicking on any tab to reveal a contextual menu containing the list of all tabs,
- scrolling (with the mouse wheel) previous/back on the tabs bar to switch tabs. Once you reached the end, it will cycle back with an "hard push" ensuring it's really what you want to do.

Then modules can be navigated by scrolling them in the sidebar, the scroll event is captured by sliders only after a previous interaction was done with them. Unfolding modules automatically scrolls the bar if needed, as to display them in full if possible, or at least to align their upper border with the viewport.

__With the keyboard__, tabs can be navigated by <kbd>Ctrl+Tab</kbd> and <kbd>Ctrl+Shift+Tab</kbd> to switch between next/previous tabs, as you would expect in any web browser. This will also cycle back when you reached the end of tabs.

Then, within a tab, modules can be navigated with <kbd>Page Up/Down</kbd> to move to previous/next module, vertically in the stack. This automatically unfolds each selected module while folding all the others, and aligns the current module vertically as to try and fit it entirely in the screen.

This sequential way of accessing next/previous module is meant to solve the terrible UX of Darktable where you have to assign exactly one shortcut per module to unfold it, and then have to remember all those special shortcuts while being unable to re-use them in other parts of the software.

### Transversal navigation

If you have already been trapped in an Ikea store, you know the benefits of finding transverse pathes instead of following the intended path.

You can search modules by their names or internal aliases with the search box, which responds to the <kbd>Ctrl+F</kbd> shortcut. The search results can also be navigated in sequential order by <kbd>Page Up/Down</kbd> keys.

Remember that the first tab shows the whole pipeline in its order of application.

### Replacing favourite modules

The current design has no way to define favourite modules in a special tab. I don't see the point of adding more bloat to solve the issue of having bloat initially.

For those special modules, you can assign shortcuts to the "show" (aka open, show, display, unfold) or "enable" events (aka activate). Go to the Edit menu, then at the bottom click Keyboard shortcuts, then with the special cursor you got, click on your soon-to-be favorite module header (on its name). Example here with the exposure module :

{{< figure src="shortcuts.png" />}}

By default, you will be prompted with the "show" event, in the element column (another label I should change). You can change it for the "enable" or "instance" (aka instanciate) event. The effect doesn't matter for this one, I haven't tested in which case it is used and the whole thing is a tangled mess anyway.

In any case, these shortcuts will instantly bring you to your favourite modules without polluting more GUI real-estate.

## Conclusion

This doesn't solve the issue of modules beeing called something that don't interest users, and of image processing modules looking the same as non-image processing ones. I have some ideas regarding this, but that will be for another time.

## Sidenotes

Many other tools previously hidden in cryptic icon-buttons have been merged into the global menu. This menu can be unfolded by hitting <kbd>Alt</kbd> followed by the mnemonic letter of the menu (which will get underscored once hitting <kbd>Alt</kbd>). Once unfolded, the menu are navigable with arrow keys.

Return to the lighttable is now mapped to the <kbd>Escape/Return</kbd> key. In lighttable, the picture text search is also mapped to <kbd>Ctrl+F</kbd> (as you would expect). Browsing pictures can be done with <kbd>Arrow</kbd> keys, selection with <kbd>Space</kbd> key, and opening a picture in darkroom can be done by hitting <kbd>Enter</kbd> key.

This means the application is now almost entirely navigable with the keyboard without having to remember shortcuts. Those shurtcuts are displayed anyway in the menu, right of the entries.

The table of all shortcuts can now be found in the Help menu, previously it was only accessible… through a shortcut.
---

# New build options for Linux : https://ansel.photos/en/news/new-build-options-linux/

I accidentally discovered that the Linux build script used a "package" build, meaning the CPU optimizations are limited to generic ones in order to produce portable binaries that can be installed on any x86-64 platform. By "using", I mean the package build was not explicitely disabled, so it was enabled by default.

Anyway, this is now disabled by default, since the actual packages (.exe and .appimage) are not built through that script, which is primarily meant to help end-users. To get the previous behaviour back, you would need to run:

```
$ sh build.sh --build-package --install --sudo
```

Not using the package build option may increase performance on CPU by 20 to 30 % depending on your hardware, thanks to platform-specific optimizations.

I have also introduced a new argument that will launch the Git updating commands that users seem to forget all the time. There is a caveat, though : updating the source code by calling Git from within the script doesn't update the script for the current run, so this method doesn't work when the script itself is modified. Fortunately, we don't change this script often.

The argument to update the source code and the submodules (Rawspeed, Libraw) :

```
$ sh build.sh --update --install --sudo
```

I have also modified the internals of that script in order to automatically :

- update the Lensfun database of lenses,
- add a global system shortcut (.desktop file) so the software will be globally available from the app menus,
- add a global system command so the ansel is globally available from the terminal.

The goal of all these changes is obviously to make it more user-friendly to use a self-built version of the software, allowing to improve performance, especially for computers without GPU. The one-pit-stop command would be :

```
$ sh build.sh --update --install --sudo --clean-all
```

But of course, you will need to run the Git update manually one last time before, to update the script itself :

```
$ git pull --recurse-submodule
```

Alternatively, you can directly download the build script, and replace the old build.sh one at the root of the source code directory.
---

# Dev diary #2 : introducing Chantal : https://ansel.photos/en/news/dev-diary-2/

2022 was so bad in terms of junk emails and noise that I started the [Virtual Secretary](https://virtualsecretary.aurelienpierre.com/), a Python framework to write intelligent email filters by crossing information between several sources to guess what incoming emails are and whether they are important/urgent or not. When I'm talking about junk emails, it's also Github notifications, pings on pixls.us (thank God I closed my account on that stupid forum), YouTube, and direct emails from people hoping to get some help in private.

Having become "the face" of darktable, mostly because I'm one of the few to bother providing user education and training instead of just pissing code, I didn't see that coming, and I wasn't prepared. A lot of people now mistake me with the front desk, which doesn't help abstract thinking on coding matters, let alone taking time to actually produce art. The problem is all the time lost dealing with info/noise/input is not spent solving problems, and time is the only thing for which you cannot get a refund.

After a while, I figured it would be nice to extend the Virtual Secretary with a machine-learning classifier, which would guess in what folder incoming emails should go, by extracting the content of the emails already in said folder. It's actually much easier to implement than what I thought, but the time-consuming bit is to write text filters to clean-up the input (because garbage in, garbage out, especially for spam emails which are generally improperly formatted).

But the ultimate goal, in my wildest dreams, was to build an autoresponder for people asking questions already answered on one of the many websites I have contributed to over the years. It's a constant frustration to see that all the pages of doc I have written over the years are lost in Internet limbo. On FLOSS-centric forums, benevolent guys also tend to experience the same kind of fatigue : repeating again and again the same info, linking the same pages, to never-ending hords of newbies who don't know what to look for. Just look at Reddit darktable : every 14 days, someone else asks why the lighttable thumbnails don't look like the darkroom preview. Even discarding the amount of frustration and angryness here, the number of man-hours lost in repeating is outstanding. Just because information is lost.

The true problem of search engines is you need to know what keywords to look for. Which is circling back to the fact that newbies don't know the slang. So they don't know what to look for. They don't have any entry point in the matrix. Except other humans. Which sucks for the ones having to do the work, usually for free.

After merging a neural layer of word2vec word embedding (big words to say it's unsupervised machine learning finding how words are contextually related in sentences, that is finding syntactical structures, synonyms and the likes), as a first step in my email classifier (which is now up to 92 % accuracy), I wondered if this wouldn't been usable to build a context-aware and synonym-aware search engine, able to look past exact keywords.

Turns out a couple of guys from Bing had the same idea in 2016, and published their maths, so I implemented them. Then proceeded to add a web interface on top. That gave birth to [Chantal](https://chantal.aurelienpierre.com), the AI you are kindly asked to bother before bothering me. The current version is trained against 101.000 internet pages from my own websites, darktable & Ansel docs, along with some reliable color-science ressources. It indexes 15.500 pages in French and English and can process search queries in either or both of these languages. One of its mean features is to propose you a list of keywords associated to your query, so you can refine/reorient/try things you wouldn't have thought of before.

Hope that helps.

That work showed me how poorly indexable many websites are. To account for the lack of XML sitemap on forums.darktable.fr and color.org, I had to write a recursive crawler. But even then, many pages don't have description meta tags and a proper date tag. It means you need to use regular expressions and indirect methods trying to identify the metadata, and manually tune the HTML parser to extract the actual content part of the webpage (discarding sidebars, menus, asides and advertising if any).

Then, you get to love Q&A forums like Stack Overflow, where proper questions start a thread, proper answers follow, and the best answers are selected by the community. "Thank you" and "me too" messages are explicitly forbidden in the conditions of use. On forums like pixls.us or forums.darktable.fr, proper technical information gets lost in the middle of semi-technical rambling, life stories and bros bonding over tales of software, in a continuous thread where nothing distinguishes relevant from irrelevant, accurate from inaccurate, and gross misunderstandings of color theory. From a machine crawling perspective, there is very little to exploit here, and investing time on such platform is a dry loss.

More (technical) info:

- [Websites suck](https://eng.aurelienpierre.com/2023/04/websites-suck./) : on the technical challenges of crawling and indexing HTML (and… PDF) webpages, in a time where people say big things like "Web 4.0" or "Internet of Things", but basic things like providing page sitemaps or putting the date of internet publications in standard formats is still too much to ask of webmasters and CMS,
- [Designing an AI search engine from scratch in the 2020's](https://eng.aurelienpierre.com/2024/03/designing-an-ai-search-engine-from-scratch-in-the-2020s/) : on how Chantal was built and how the Dumbrish synthetic language was created to generalize natural French and English, plus some regex bonanza,
- [Thoughts on Word2Vec AI for information retrieval applications](https://eng.aurelienpierre.com/2025/05/thoughts-on-word2vec/) : on how the input cleanup is by far the worst time-consuming step of designing an AI and language models that become too semantically accurate don't make better search engines.
---

# Basic Editing : https://ansel.photos/en/workflows/basic-editing/

Here is how to get started with Ansel editing, going through only the most basic steps that should serve you well most of the time.

<!-- more -->

The video was recorded on Darktable 3, but the same modules and principles apply to Ansel.

{{< youtube 5CmsxxxsMDs >}}
---

# Monochrome : https://ansel.photos/en/tags/monochrome/
---

# Monochrome toning : https://ansel.photos/en/workflows/monochrome-toning/

This article will demonstrate how to perform monochrome toning on digital images in Ansel, to emulate the color rendition of cyanotypes, platinotypes, sepia and split-toning developments.

## Step 0 : global preparation

Set the global exposure and filmic scene white and scene black, as in any other editing. See [basic editing steps](./basic-editing.md). This is our base image, by Glenn Butcher :

![](toning-workflow-base.jpg)

If you start from a color image, you need to turn it into black and white : the recommended way is through the [color calibration](../doc/views/darkroom/modules/color-calibration.md) module, using the B&W presets. Here is what we get :

![](toning-workflow-monochrome.jpg)

## Variant 1 : cyanotype

The cyanotype is a development where "black" is replaced by blue, because the typical silver halide (that develop black) are replaced by ferric ferrocyanide, which develops blue.

### Step 1 : switch black and blue

In the [color balance](../doc/views/darkroom/modules/color-balance-rgb.md) module, go to the _4 ways_ tab, and in the _Global offset_ section, define a hue of 259° at a chroma of 1.50%. You will need to right-click on the chroma slider, then input the `1.5` value on the keyboard, because the range of the slider goes as high as 0.75%, which fits typical color-grading uses.

If you just do that, you will note that blacks stay neutral, and don't get tinted to blue. This is because of the internal gamut mapping in the module, that prevents negative RGB values, which would create later problems in the pipeline. To force blacks to be colored, you will need to raise the luminance of the _Global offset_ by the same amount as the chroma, that is 1.50% in this example.

This is the result:

![](toning-workflow-cyanotype-1.jpg)

From there, you can fine-tune the _Global offset_ settings to your taste.

### Step 2 : adjust contrast

The luminance increase of the black point, mandatory to be able to tint black, reduces the contrast and flattens the image. To overcome this, you will need to come back to filmic and to raise the scene black exposure until the density of the darkest parts seems acceptable to you. Here is the result:

![](toning-workflow-cyanotype-2.jpg)

### Step 3 : fine-tune midtones

The _Global offset_ that we adjusted at the step 1 will mostly affect blacks and deep shadows. You may want to drive the fall-off of the tinting toward white, as to get more or less tinted mid-tones.

You will have to go the _mask_ tab of the color balance module, and set the white fulcrum with the color picker on the right of the slider. This is important for the _Power_ setting next. In a display-referred, where white is known beforehand to be at 100%, this wouldn't be necessary, but since we are in a [scene-referred workflow](./scene-referred.md) where white can have any value, we need to define it explicitly.

Then, in the _4 ways_ tab, move to the bottom, at the _Power_ section. In here, use the same hue as before (259°) and a chroma more or less intense depending on how blue you want your midtones to be. This is what we get:

![](toning-workflow-cyanotype-3.jpg)

Once you raised the blueing in the midtones, you may want to soften it slightly in the deep shadows by reducing a bit the chroma of the _Global offset_. Adjust everything to taste and watch out for flat blue surfaces that might indicate over-saturation.

### Conclusion

That's it. The look is pretty consistent with analog cyanotypes. You having nothing to do with whites and highlights, which are defined by the paper tone for analog cyanotypes. For a more vintage look, you can choose a slightly greener hue, that is around 257° or even less, and even delicately shift the highlights gain toward yellow to simulate paper aging.

For a believable look, you absolutely need to watch out for any neutral blacks: you should not have any. Increase the chroma and luminance of the _Global offset_ until they are fully tinted.

## Variant 2 : platinotype

The platinotype uses palladium and platinum in variable proportions, instead of silver halide. Depending on the proportions of each, blacks will be warmer or colder, but will be less dense than with silver halide. We will go here for the warmer look.

It works very much the same as the cyanotype emulation, we only change the hue, so refer to the previous section for the full explanations.

### Step 1 : warm up black

We will use much gentler settings than before, that is hue set at 55°, chroma at 0.20% and luminance at 0.20% in _Global offset_.

![](toning-workflow-platinotype-1.jpg)


### Step 2 : fine-tune midtones

Again, I barely touched the _Power_ settings, with a 0.10% chroma at hue 55°.

![](toning-workflow-platinotype-2.jpg)

### Conclusion

There is not one definite platinotype look because the final tint depends on the proportions of the mix between platinum and palladium in the photo-sensible emulsion. The result presented here is more opinionated than most actual platinotypes I have seen, for educating purposes. You may want to dial it down a notch for a more believable result (reduce chroma).

Selenium toning can be reproduced exactly the same way, only the hue will need to be changed to purple.

## Variant 3 : sepia

The sepia toning is obtained from a typical silver halide print where the silver halide is turned into sulphide, which is more stable and has better longevity. It results in a brown shift.

The digital reproduction of the toning is very similar in spirit to the platinotype, only the hue will be slightly different (70° instead of 55°) and the shift is more pronounced, which will require to use the _Shadows lift_ as well.

The proposed settings are (all hues set to 70°): 

* _Global offset_:
  * chroma : 0.20%
  * luminance : 0.20%
* _Shadows lift_:
  * chroma: 10%
* _Highlights gain_ : don't touch it (same as previously)
* _Power_: 
  * chroma: 1.50%

This gives us:

![](toning-workflow-sepia.jpg)


## Variant 4 : split-toning

Split-toning is a process where highlights and shadows get a different color shift, the most common being teal and orange. It is not specific to monochrome toning, and can be used for color work, where it becomes a particular case of color grading (more on that topic in [ressources](../resources/readings.md#color-grading)). For monochrome, the split-toning will need to be very gentle to keep it believable.

Using the color balance module, we only need to use the _Highlights gain_ and the _Shadows lift_. Here is a proposal for teal and orange :

![](toning-workflow-split-toning.jpg)

The possibilities are limitless, and the typical "teal and orange" should be used carefully because it has been already overused and abused.

## Conclusion

You may have noticed that all the analog-inspired tonings (that is, all except split-toning) don't affect whites. This is because, on an analog print, white is achieved by letting the paper naked, while black and midtones uses photosensitive pigments.

Pure black and pure white are ignored by the _Power_ setting of the color balance module, which is a property of the power function (maths…). However, the more you increase its chroma and the more impact it has on very bright and very dark midtones. If you ever find that it takes too much of highlights once you found a proper setting for midtones, you can always desaturate them in the _Master_ tab, using the _Perceptual saturation grading_ for the _Highlights_ channel (because _perceptual saturation_ is applied after the _4 ways_ settings in the pixel pipeline, while the _linear chroma grading_ is applied before).

The real strength of the color balance module here is to let you fine-tune very precisely how the effects target shadows, midtones or highlights, and how fast the transitions are between them. But with great power comes great responsibilities, and it is certainly a tool that requires some time to master.
---

# Dev diary : https://ansel.photos/en/news/dev-diary-1/

It's been roughly 3 months that I rebranded "R&Darktable" (that nobody seemed to get right), into "Ansel", then bought the domain name and created the website from scratch with Hugo (I had never programmed in Golang before, but it's mostly template code).

Then I spent a total 70 h on making the nightly packages builds for Windows and Linux work for continuous delivery, something that Darktable never got right ("you can build yourself, it's not difficult"), only to see the bug tracker blow up after release (nothing better than chaining the pre-release sprint with a post-release one to reduce your life expectancy).

People keep asking for a Mac build because they have no notion of the amount of work it requires while the Brew package manager breaks lib dependencies on a weekly basis when you are not lucky. Mac OS simply requires an unreasonable amount of care, which becomes a dry loss when you know that not even 9 % of Darktable users run it. Also, for the last time, Github (actually, the Microsoft Azure instances providing Github actions runners) has no ARM system, so anyway a nightly Mac build would necessarily be on AMD64 architecture, that is old MacBook from before Apple decided once again to go full Apple on its own island. Don't expect 90 % of the free world to scurry over a tech nobody needed and barely anybody uses.

From then, I have optimized the local laplacian in highlights reconstruction with a stupid trick : processing a downsized image instead of the full-resolution one. I had this idea in the back of my mind for a long time but feared the detrimental side-effects. But since clipped areas are signal-less anyway, processing a slightly blurrier version is almost invisible. Also, the shoulder of your typical S/filmic tone curve will anyway compress everything close to white, so it reduces percieved sharpness by reducing contrast in highlights no matter what. We are talking 96 % speed-up on CPU (mostly because we can process the image at once with no tiling).

Using that, I developed an experimental noise and chromatic aberrations pre-filter re-using multi-scale guided laplacians. It's not bad, but again quite slow.

Since February, most of the work has been spent on cleaning up the GUI by moving collections of buttons, either the full-text ones or the weird icon ones, to the global menu and rewiring the keyboard shortcuts to that. It makes feature more discoverable while reducing screen real estate.
---

# Darktable : crashing into the wall in slow-motion : https://ansel.photos/en/news/darktable-dans-le-mur-au-ralenti/

What happens when a gang of amateur photographers, turned into amateur developers, joined by a bunch of back-end developers who develop libraries for developers, decide to work without method nor structure on an industry software for end-users, which core competency (colorimetry and psychophysics) lies somewhere between a college degree in photography and a master's degree in applied sciences, while promising to deliver 2 releases each year without project management ? All that, of course, in a project where the founders and the first generation of developers moved on and fled ?

Guess !


## Degrading basic features

The 2020's are 40 years too late to re-invent interaction paradigms between user and computer, being it how we use a keyboard and a mouse to drive the interface or the behaviour of a file browser. Since the 1980's, all the general-audience computer appliances have converged toward more or less unified semantics, where the <kbd>escape</kbd> key closes the current application, double-click opens files and the mouse wheel scrolls the current view. Darktable[^1] takes an ill-placed pleasure to ignore all that and the recent changes worsen things : it is now mandatory to read the documentation to achieve tasks as simple as sorting files or assigning keyboard shortcuts to GUI actions.

[^1]: By the way, I can no longer bare the attempt of being different for the sake of it by writing "Darktable", proper noun, without initial capital. It's childish, it's neither funny or disruptive, and it makes a mess of [freedesktop.org](https://github.com/darktable-org/darktable/blob/master/data/org.darktable.darktable.desktop.in) menus where the capital is anyway added to follow the standard.

### Module groups

Everything begins with the overhaul of the [modules groups](https://docs.darktable.org/usermanual/4.0/en/darkroom/organization/manage-module-layouts/), in 2020, which hides the decision of [not deciding](https://github.com/darktable-org/darktable/pull/5322) of an unified module order.

![](module-groups.jpg)

[Since 2018](https://www.mail-archive.com/darktable-dev@lists.darktable.org/msg03402.html), I fight to clean up the graphical interface of Darktable, and in particular the module organization. A graphical interface should promote best practices by laying out tools in the typical order they should be used. Bad practices are those which increase the risk of colorimetric inconsistencies or of circular editing, where one must go back to repercute changes made later, even though bad practices can work in simple cases. In the context of image processing, highly-technical task where a lot of things are hidden to the end-users beneath the GUI, best practices also enable poorly-qualified persons to use the software in a way that reduces the probability of mistakes.

This order of using is mostly dictated by technical considerations like the order of application of modules in the pipeline sequence and the use of drawn and parametric masks, which effect depends on upstream modules. Ignoring these considerations is equivalent to looking for trouble, even though the hot trend in the 2010's and 2020's is to believe that digital technologies work detached of any material reality for the sole happiness of the user.

For example, the constrained imposed upon the pipeline design to allow an arbitrary order of module use [creates mathematically unsolvable problems](https://github.com/darktable-org/darktable/pull/8673) regarding the computation of mask nodes coordinates, and no programmed solution is possible (aside from relaxing that constraint) because maths said no.

Except that a significant part of the programmer-users revolving around the project on Github and on the dev mailing-list stay convinced that there is no good or bad workflow, only personal preferences, which is probably true when you practice an art without time constraint, budget constraint or result constraint. As such, modules should be able to be reordered at will, being in the pipeline or in the workflow. The confusion comes from the fact that non-destructive editing is wrongfully seen as asynchronous (which would _almost_ be the case if we didn't use masks nor blending modes) whereas the pixel pipeline is sequential and closer to a layer logic, as we find it in Adobe Photoshop, Gimp, Krita, etc.

Decoupling the modules order in the GUI from the pipeline order is equivalent to enabling every use, even pathological, and forces to write [pages and pages](https://docs.darktable.org/usermanual/4.0/en/darkroom/pixelpipe/the-pixelpipe-and-module-order/#changing-module-order) of documentation to warn, explain what to do, how and why ; documentation that nobody will read to end up asking in loop the same questions every week on every forum.

In this story, everyone looses their time thanks to an interface design trying to be so flexible that it can't be made safe and robust by default. In the human body, every joint as _some_ degrees of freedom along certains axes ; if every joint could revolve 340 ° around each axis of the 3D space, the structure would be instable for being too flexible, and unable to work with high loads. The metaphor holds in industry software. We swim in the FLOSS [cargo cult](https://en.wikipedia.org/wiki/Cargo_cult), where people love to have the illusion of choice, that is being offered many options of which most are unusable or dangerous, at the expense of simplicity ([KISS](https://en.wikipedia.org/wiki/KISS_principle)), and where the majority of users don't understand the implications of each option (and don't have the slightest desire to understand).

In the absence of a consensus over the interface module ordering, a complicated tool, brittle and heavy was introduced at the end of 2020 to allow each user to configure the layout of modules in tabs. It provides many useless options and stores the current layout in the database, using the translated name of modules, meaning changing the UI language makes you loose your presets. Entirely configurable, it lets user decide how to harm themselves, without any best practices guide. This garbage is coded with 4000 lines [cheerfully mixing SQL requests in the middle of interface GTK code](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/libs/modulegroups.c#L3499-L3616), and presets are created through [redundant compiler macros](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/libs/modulegroups.c#L1533-L1768), whereas modules have had a [binary flag](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/iop/borders.c#L196) forever, allowing to set their default group… modularly.


More important, it replaces a simple and efficient feature, available until Darktable 3.2 :

![](more-modules.png)

One click over the module name enables it, a second adds it to the column of favorites, a third hides it from the interface. Everything allowing presets and storing the current layout in simple text into the `darktablerc` file. Simple and robust, [coded over 688 lignes](https://github.com/darktable-org/darktable/blob/darktable-3.2.x/src/libs/modulegroups.c) of legible and well-structured code, the feature was therefore not amusing enough for the middle-aged dilettante developer, and it was urgent to replace it by a labyrinthine system.

### Keyboard shortcuts

In 2021 has been added what I call the great MIDI [turducken](https://en.wikipedia.org/wiki/Turducken). The goal is to extend the interface of keyboard shortcuts (already extended in 2019 to support "dynamic shortcuts", allowing to combine mouse and keyboard actions), to support MIDI devices and… video game controllers.

At the end of 2022, that is one and half year after this feature, in the [survey I conducted](https://www.poll-maker.com/S-Quiz-Results?qp=2539714x3792Bf5e-98#), less than 10 % of users own a MIDI device, and only 2 % use it with Darktable. To compare with the 45 % of users who own a graphic tablet (Wacom-like), which support in Darktable is still so flawed that only 6 % use it. Notwithstanding the poor priority management, what I don't tolerate here is the edge effects introduced by this change and the global cost it had, starting with the fact that it doesn't import user-defined shortcuts from versions ealier than 3.2, and it makes the configuration of new shortcuts terribly complicated.

Before the great turducken, only a limited list of GUI actions could be mapped to keyboard or mixed (keyboad + mouse) shortcuts. This list was manually curated by developers. The great MIDI turducken allows to map _every_ GUI actions to shortcuts, presenting users with a list of several thousands of configurable entries, in which it's difficult to find the only 3 you really need, and the text search engine is too basic to be helpful :

![](shortcuts.png)

Note the use of "effects", on which the [documentation](https://docs.darktable.org/usermanual/4.0/en/preferences-settings/shortcuts/) is of no help. It's only by deduction ([because the code is not commented either](https://github.com/darktable-org/darktable/blob/master/src/gui/accelerators.c#L101-L164)) that I ended up understanding they are emulations of typical desktop interactions (mouse and keyboard) destined to be used with MIDI devices and gamepad controllers (but you still need to explain to me what `Ctrl-Toggle`, `Right-activate` or `Right-Toggle` mean in terms of typical destkop interaction).

What is unacceptable is that the use of the numeric keypad is broken [by design](https://github.com/darktable-org/darktable/issues/10628#issuecomment-994795922), noticeably to attribute numbered ratings (stars) to thumbnails in lighttable. Indeed, the key modifiers (numlock and capslock) are not properly decoded by the thing, and numbers are treated differently whether they are input from the typical "text" keyboard or from the numeric keypad. So the `1` from the numpad is decoded `Keypad End`, no matter the state of the numlock. This is how I had to configure number shortcuts on a French BÉPO keyboard and to duplicate the configuration for the numpad :

![](shortcuts-broken.png)

You just have to remember that `Shift+"` and `Kp End` both mean `1` and remember to duplicate all shortcuts for the numpad and the rest of the keyboard. In short, we break a basic user expectation, and we send design critics to hell. The regression is mentionned on all Darktable forums but seems to bother nobody.

The fix of ~~this bug~~ this feature has been made [in Ansel](https://github.com/aurelienpierreeng/ansel/commit/257a70d2d14d8f8639c7fe8cf65f76bbcf5916e3) and the numeric pad keys are remapped to [standard keys directly in the code](https://github.com/aurelienpierreeng/ansel/commit/9cbf3e681b7a277968f8c9cfad43c693554d6f17), for a total of 100 lines of code including comments. Doing this correction has been very hard indeed : I read [the Gtk documentation](https://docs.gtk.org/gdk3/method.Keymap.translate_keyboard_state.html) and took their example line by line. 2 years spent waiting for that…

The cherry on the sunday is, one more time, we replaced [1306 lines of clear and structured code](https://github.com/darktable-org/darktable/blob/darktable-3.6.x/src/gui/accelerators.c) by a [monstruosity of close to 4400 lines](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/gui/accelerators.c), with gems like :

1. the `while` loop of death ([source](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/gui/accelerators.c#L2898-L2918)) :

```C
  gboolean applicable;
  while((applicable =
           (c->key_device == s->key_device && c->key == s->key && c->press >= (s->press & ~DT_SHORTCUT_LONG) &&
           ((!c->move_device && !c->move) ||
             (c->move_device == s->move_device && c->move == s->move)) &&
           (!s->action || s->action->type != DT_ACTION_TYPE_FALLBACK ||
            s->action->target == c->action->target))) &&
        !g_sequence_iter_is_begin(*current) &&
        (((c->button || c->click) && (c->button != s->button || c->click != s->click)) ||
         (c->mods       && c->mods != s->mods ) ||
         (c->direction  & ~s->direction       ) ||
         (c->element    && s->element         ) ||
         (c->effect > 0 && s->effect > 0      ) ||
         (c->instance   && s->instance        ) ||
         (c->element    && s->effect > 0 && def &&
          def->elements[c->element].effects != def->elements[s->element].effects ) ))
  {
    *current = g_sequence_iter_prev(*current);
    c = g_sequence_get(*current);
  }
```

2. The `switch` `case`  containing `if` nested on 2 levels ([source](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/gui/accelerators.c#L892-L954)) :

```C
  switch(owner->type)
  {
  case DT_ACTION_TYPE_IOP:
    vws = DT_VIEW_DARKROOM;
    break;
  case DT_ACTION_TYPE_VIEW:
    {
      dt_view_t *view = (dt_view_t *)owner;

      vws = view->view(view);
    }
    break;
  case DT_ACTION_TYPE_LIB:
    {
      dt_lib_module_t *lib = (dt_lib_module_t *)owner;

      const gchar **views = lib->views(lib);
      while(*views)
      {
        if     (strcmp(*views, "lighttable") == 0)
          vws |= DT_VIEW_LIGHTTABLE;
        else if(strcmp(*views, "darkroom") == 0)
          vws |= DT_VIEW_DARKROOM;
        else if(strcmp(*views, "print") == 0)
          vws |= DT_VIEW_PRINT;
        else if(strcmp(*views, "slideshow") == 0)
          vws |= DT_VIEW_SLIDESHOW;
        else if(strcmp(*views, "map") == 0)
          vws |= DT_VIEW_MAP;
        else if(strcmp(*views, "tethering") == 0)
          vws |= DT_VIEW_TETHERING;
        else if(strcmp(*views, "*") == 0)
          vws |= DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
                 DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
        views++;
      }
    }
    break;
  case DT_ACTION_TYPE_BLEND:
    vws = DT_VIEW_DARKROOM;
    break;
  case DT_ACTION_TYPE_CATEGORY:
    if(owner == &darktable.control->actions_fallbacks)
      vws = 0;
    else if(owner == &darktable.control->actions_lua)
      vws = DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
            DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
    else if(owner == &darktable.control->actions_thumb)
    {
      vws = DT_VIEW_DARKROOM | DT_VIEW_MAP | DT_VIEW_TETHERING | DT_VIEW_PRINT;
      if(!strcmp(action->id,"rating") || !strcmp(action->id,"color label"))
        vws |= DT_VIEW_LIGHTTABLE; // lighttable has copy/paste history shortcuts in separate lib
    }
    else
      fprintf(stderr, "[find_views] views for category '%s' unknown\n", owner->id);
    break;
  case DT_ACTION_TYPE_GLOBAL:
    vws = DT_VIEW_DARKROOM | DT_VIEW_LIGHTTABLE | DT_VIEW_TETHERING |
          DT_VIEW_MAP | DT_VIEW_PRINT | DT_VIEW_SLIDESHOW;
    break;
  default:
    break;
  }
```

3. The nested `switch` `case` of the demon, with additive clauses sneakily hidden ([source](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/bauhaus/bauhaus.c#L3339-L3415)) :

```C
case DT_ACTION_ELEMENT_ZOOM:
    ;
    switch(effect)
    {
    case DT_ACTION_EFFECT_POPUP:
      dt_bauhaus_show_popup(widget);
      break;
    case DT_ACTION_EFFECT_RESET:
      move_size = 0;
    case DT_ACTION_EFFECT_DOWN:
      move_size *= -1;
    case DT_ACTION_EFFECT_UP:
      _slider_zoom_range(bhw, move_size);
      break;
    case DT_ACTION_EFFECT_TOP:
    case DT_ACTION_EFFECT_BOTTOM:
      if((effect == DT_ACTION_EFFECT_TOP) ^ (d->factor < 0))
        d->max = d->hard_max;
      else
        d->min = d->hard_min;
      gtk_widget_queue_draw(widget);
      break;
    default:
      fprintf(stderr, "[_action_process_slider] unknown shortcut effect (%d) for slider\n", effect);
      break;
    }
```

Programmers understand what I'm talking about ; for the others, just know that I don't understand more than you what this does : it's shit code, and if several bugs are not hidden in there, it will be pure luck. Hunting bugs in this shithole is sewer's bottom archaelogy, all the more considering that Darktable does not have a developer documentation and, in the absence of meaningful comments in the code, any modification of the aforementionned code will necessarily start with a reverse-engineering phase becoming harder and harder as time goes by.

The true problem of this kind of code is that you can't improve it without rewriting it more or less entirely : to fix it, you first need to understand it, but the reason why it needs to be fixed is precisely that it's not understandable and dangerous long-term. We call that [technical debt](https://en.wikipedia.org/wiki/Technical_debt). In short, all the work invested on this feature will create extra work because it is unreasonable to keep that kind of code in the middle of a code base of several hundreds of thousands of lines and expect it to not blow up in our face one day.

It's all the more ridiculous in the context of an open-source/free application where the bulk of the staff is non-trained programmers. Clever developers write code understandable by idiots, and the other way around.

### Collection filters

Until Darktable 3.8, the collection filters, at the top of the lighttable, were used to temporarilly restrict the view on a collection. The collection is an extraction of the photo database based on certain criteria, the most common being extracting the content of a folder (which Darktable calls "filmroll" to confuse everybody, because a filmroll is actually a folder's content displayed as a flat list instead of a tree - many people wrongfully thing that Darktable has no file manager).

Having been a Darktable user for more than a decade, I have a database of more than 140.000 entries. Extracting a collection among these 140.00 pictures is a slow operation. But my folders rarely contain more than 300 pictures. Filtering, for example, the pictures rated 2 stars or more, in a collection of 300 files, is fast because it is a subset of 300 elements. And switching from a filter to another is fast too. The filter is only a partial or total view of a collection, optimized for a fast and temporary start-and-go usage.

Under the pretense of refactoring the filtering code, which took all in all [550 lines](https://github.com/darktable-org/darktable/blob/darktable-3.6.x/src/libs/tools/filter.c), the chief [Gaston Lagaffe](https://en.wikipedia.org/wiki/Gaston_(comics)#Props,_inventions_and_other_running_gags) made it a vocation to break this model to turn collection filters into basic collections, by mean of [more than 6.000 lines of code](https://github.com/darktable-org/darktable/tree/darktable-4.2.x/src/libs/filters), not counting the countless bugfixes that only added more lines[^4]. All that, as usual, highly configurable and redundant with the classical [collections module](https://docs.darktable.org/usermanual/4.0/en/module-reference/utility-modules/shared/collections/), which remained there, and served by icons so cryptic that they had to add text tooltips on hover to clarify what they mean..

[^4]: The fact that bugfixes systematically add more lines of code instead of modifying existing lines is a a concerning smell that the programming logic is bad and induces too many particular cases. Rigorous programmers always try to keep their code as generic as possible to avoid [spaghetti code](https://en.wikipedia.org/wiki/Spaghetti_code).

In this quality code, we will found the endless `while` under the `switch case` in the `if` in the `if` in the `for` ([source](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/libs/filtering.c#L1382-L1459)) :

```C
for(int k = 0; k < num_rules; k++)
  {
    const int n = sscanf(buf, "%d:%d:%d:%d:%399[^$]", &mode, &item, &off, &top, str);

    if(n == 5)
    {
      if(k > 0)
      {
        c = g_strlcpy(out, "<i>   ", outsize);
        out += c;
        outsize -= c;
        switch(mode)
        {
          case DT_LIB_COLLECT_MODE_AND:
            c = g_strlcpy(out, _("AND"), outsize);
            out += c;
            outsize -= c;
            break;
          case DT_LIB_COLLECT_MODE_OR:
            c = g_strlcpy(out, _("OR"), outsize);
            out += c;
            outsize -= c;
            break;
          default: // case DT_LIB_COLLECT_MODE_AND_NOT:
            c = g_strlcpy(out, _("BUT NOT"), outsize);
            out += c;
            outsize -= c;
            break;
        }
        c = g_strlcpy(out, "   </i>", outsize);
        out += c;
        outsize -= c;
      }
      int i = 0;
      while(str[i] != '\0' && str[i] != '$') i++;
      if(str[i] == '$') str[i] = '\0';

      gchar *pretty = NULL;
      if(item == DT_COLLECTION_PROP_COLORLABEL)
        pretty = _colors_pretty_print(str);
      else if(!g_strcmp0(str, "%"))
        pretty = g_strdup(_("all"));
      else
        pretty = g_markup_escape_text(str, -1);

      if(off)
      {
        c = snprintf(out, outsize, "<b>%s</b>%s %s",
                     item < DT_COLLECTION_PROP_LAST ? dt_collection_name(item) : "???", _(" (off)"), pretty);
      }
      else
      {
        c = snprintf(out, outsize, "<b>%s</b> %s",
                     item < DT_COLLECTION_PROP_LAST ? dt_collection_name(item) : "???", pretty);
      }

      g_free(pretty);
      out += c;
      outsize -= c;
    }
    while(buf[0] != '$' && buf[0] != '\0') buf++;
    if(buf[0] == '$') buf++;
  }
```

and other `if` nested over 2 levels inside `switch case` necessary to support the keyboard shortcuts ([source](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/libs/filters/rating_range.c#L201-L265)).

This last fucking crap was the straw that broke the camel's back and made me fork Ansel. I refuse to work on a ticking bomb in a team that doesn't see the problem and plays with code over their spare time. Coding may amuse them, not me. And fixing shit done by irresponsible kids twice my age, especially when they break stuff I cleaned up 3 or 4 years ago, infuriates me.

### Lighttable

The lighttable underwent 2 nearly-full rewritings, the first in early 2019 and the second in late 2019, which added many disputable features like the [culling view](https://docs.darktable.org/usermanual/development/en/lighttable/lighttable-modes/culling/).

Quickly, the culling mode is divided into 2 submodes : dynamic and static, which manage the number of images differently. Many users still haven't understood the difference 4 years later. We therefore have the default view (file manager), the zoomable lighttable (that nobody uses), the static culling, the dynamic culling, and the preview mode (a single full-screen picture).

Then, more display options are added to thumbnails in lighttable, allowing to define overlays : basic permanent overlays, extended EXIF permanent overlays, the same but only on hover, and finally the timed hovered overlays (with a configurable timer).

The UI code rendering thumbnails and their overlays must therefore take into account 5 different views and [7 display variants](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/dtgtk/thumbnail.h#L38-L48), that is 35 possible combinations. The code ensuring proper resizing of thumbnails thus needs [a total of 220 lines](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/dtgtk/thumbnail.c#L1565-L1786).

But it doesn't stop there, because the code rendering the thumbnails GUI is shared also with the "filmstrip" bottom bar, which actually makes 36 possible combinations in thumbnail rendering. Multiplied by 3 GUI themes of different base colors, that makes 108 sets of CSS instructions to fully style the GUI… of which many [were forgotten in Darktable 4.0 graphic overhaul](https://github.com/darktable-org/darktable/pull/11835), and how could it be differently ?

In Darktable 2.6, we had __4193__ lines for the pack, which had only the filemanager, zoomable lighttable and fullscreen preview views, with only 2 modes of thumbnails overlays (always visible or visible on hover) :

* 2634 lines [views/lighttable.c](https://github.com/darktable-org/darktable/blob/darktable-2.6.x/src/views/lighttable.c) for the lighttable and the thumbnail rendering,
* 1124 lines in [libs/tools/filmstrip.c](https://github.com/darktable-org/darktable/blob/darktable-2.6.x/src/libs/tools/filmstrip.c) for the filmstrip par, which partially duplicates the lighttable code for the thumbnail rendering,
* 435 lines in [libs/tools/global_toolbox.c](https://github.com/darktable-org/darktable/blob/darktable-2.6.x/src/libs/tools/global_toolbox.c), for the button menu allowing to enable or disable thumbnails overlays.

After Darktable 3.0 and the addition of culling modes, we get __6731 lines__ :

* 5149 lines [views/lighttable.c](https://github.com/darktable-org/darktable/blob/darktable-3.0.x/src/views/lighttable.c),
* 1177 lines [libs/tools/filmstrip.c](https://github.com/darktable-org/darktable/blob/darktable-3.0.x/src/libs/tools/filmstrip.c),
* 405 lines [libs/tools/global_toolbox.c](https://github.com/darktable-org/darktable/blob/darktable-3.0.x/src/libs/tools/global_toolbox.c).

After Darktable 3.2 and the additions of the 7 variants of highly-configurable overlays and some code refactoring, we get __8380 lines__ :

* 1463 lines in [views/lighttable.c](https://github.com/darktable-org/darktable/blob/darktable-3.2.x/src/views/lighttable.c),
* 1642 lines in [dtgtk/culling.c](https://github.com/darktable-org/darktable/blob/darktable-3.2.x/src/dtgtk/culling.c), where the culling view features were detached,
* 2447 lines in [dtgtk/thumbtable.c](https://github.com/darktable-org/darktable/blob/darktable-3.2.x/src/dtgtk/thumbtable.c), where the thumbnails containers are managed for the lighttable and the filmstrip,
* 1736 lines in [dtgtk/thumbnail.c](https://github.com/darktable-org/darktable/blob/darktable-3.2.x/src/dtgtk/thumbnail.c), where the thumbnails themselves are managed,
* 169 lines in [dtgtk/thumbnail_btn.c](https://github.com/darktable-org/darktable/blob/darktable-3.2.x/src/dtgtk/thumbnail_btn.c), where the specific thumbnails buttons are declared,
* 115 lines in [libs/tools/filmstrip.c](https://github.com/darktable-org/darktable/blob/darktable-3.2.x/src/libs/tools/filmstrip.c),
* 808 lines in [libs/tools/global_toolbox.c](https://github.com/darktable-org/darktable/blob/darktable-3.2.x/src/libs/tools/global_toolbox.c).

In Darktable 4.2, after the correction of many bugs, we get to a total of __9264 lines__ :

* 1348 lines in [views/lighttable.c](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/views/lighttable.c),
* 1828 lines in [dtgtk/culling.c](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/dtgtk/culling.c),
* 2698 lines in [dtgtk/thumbtable.c](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/dtgtk/thumbtable.c),
* 2093 lines in [dtgtk/thumbnail.c](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/dtgtk/thumbnail.c),
* 166 lines in [dtgtk/thumbnail_btn.c](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/dtgtk/thumbnail_btn.c),
* 109 lines in [libs/tools/filmstrip.c](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/libs/tools/filmstrip.c),
* 1022 lines in [libs/tools/global_toolbox.c](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/libs/tools/global_toolbox.c).

The number of lines (especially in code taking an ill-placed pleasure in ignoring programming best practices) is a direct indicator of the difficulty to debug anything in there, but also an indirect indicator (in the specific case of GUI code) of CPU load required to run the software.

Indeed, if you start `darktable -d sql` and you hover a thumbnail in lighttable, you will get in terminal :

```bash
140.8252 [sql] darktable/src/common/image.c:311, function dt_image_film_roll(): prepare "SELECT folder FROM main.film_rolls WHERE id = ?1"
140.8259 [sql] darktable/src/common/image.c:387, function dt_image_full_path(): prepare "SELECT folder || '/' || filename FROM main.images i, main.film_rolls f WHERE i.film_id = f.id and i.id = ?1"
140.8271 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
140.8273 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
140.8275 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
140.8277 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
140.8279 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
140.8280 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
140.8282 [sql] darktable/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
140.8284 [sql] darktable/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (104337) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
140.8286 [sql] darktable/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (104337) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
140.9512 [sql] darktable/src/common/act_on.c:156, function _cache_update(): prepare "SELECT imgid FROM main.selected_images WHERE imgid=104337"
140.9547 [sql] darktable/src/common/act_on.c:156, function _cache_update(): prepare "SELECT imgid FROM main.selected_images WHERE imgid=104337"
140.9550 [sql] darktable/src/common/act_on.c:288, function dt_act_on_get_query(): prepare "SELECT imgid FROM main.selected_images WHERE imgid =104337"
140.9552 [sql] darktable/src/libs/metadata.c:263, function _update(): prepare "SELECT key, value, COUNT(id) AS ct FROM main.meta_data WHERE id IN (104337) GROUP BY key, value ORDER BY value"
140.9555 [sql] darktable/src/common/collection.c:973, function dt_collection_get_selected_count(): prepare "SELECT COUNT(*) FROM main.selected_images"
140.9556 [sql] darktable/src/libs/image.c:240, function _update(): prepare "SELECT COUNT(id) FROM main.images WHERE group_id = ?1 AND id != ?2"
140.9558 [sql] darktable/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (104337) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
```

which means that 18 SQL requests are made against the database to fetch image information, and run everytime the cursor hovers a new thumbnail, for no reason since metadata didn't change since the previous hovering.

In Ansel, by removing most options, I managed to spare 7 requests, which still doesn't prevent duplicated requests but still improve the timings somewhat (timestamps are the figures starting each line) :

```bash
12.614534 [sql] ansel/src/common/image.c:285, function dt_image_film_roll(): prepare "SELECT folder FROM main.film_rolls WHERE id = ?1"
12.615225 [sql] ansel/src/common/image.c:356, function dt_image_full_path(): prepare "SELECT folder || '/' || filename FROM main.images i, main.film_rolls f WHERE i.film_id = f.id and i.id = ?1"
12.616499 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
12.616636 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
12.616769 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
12.616853 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
12.616930 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
12.617007 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
12.617084 [sql] ansel/src/common/metadata.c:487, function dt_metadata_get(): prepare "SELECT value FROM main.meta_data WHERE id = ?1 AND key = ?2 ORDER BY value"
12.617205 [sql] ansel/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (133727) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
12.617565 [sql] ansel/src/common/tags.c:635, function dt_tag_get_attached(): prepare "SELECT DISTINCT I.tagid, T.name, T.flags, T.synonyms, COUNT(DISTINCT I.imgid) AS inb FROM main.tagged_images AS I JOIN data.tags AS T ON T.id = I.tagid WHERE I.imgid IN (133727) AND T.id NOT IN memory.darktable_tags GROUP BY I.tagid  ORDER by T.name"
```

The issue is that the source code nests SQL commands inside functions drawing the GUI, and untangling this mess through the different layers inherited from "refactoring" (supposed to simplify the code, but actually nope) is once again archaelogy. And if the issue had been fixed when the code was 6700 lines over 3 files, we wouldn't be looking, 4 years later, for the causes in 2500 additionnal lines now spread in 7 different files (not counting `.h` files).

We are in the poster case where "refactoring" actually complexified code and where merging thumbnail code between filmstrip and lighttable only added more internal `if` (branches) nested on several levels, which complexify even more the structure, only to blindly follow the [code reuse principle](https://en.wikipedia.org/wiki/Code_reuse), which conflicts here with the [modularity principle](https://en.wikipedia.org/wiki/Modular_programming), which a skilled developer would have fixed with [inheritance](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming), because even if it's not easy to do in C, it's perfectly possible (actually, Darktable uses this principle in code from 2009-2010).

## Cosmetics take over stability

Darktable 4.2 introduces the styles preview in darkroom. That would be awesome if styles were not deeply broken, when used with non-default pipeline order and multiple module instances. The problem is a clean and long-term solution involves [directed graphs theory](https://en.wikipedia.org/wiki/Directed_graph), and that's where we lost our beloved copy-pasted code pissers.

In the same spirit, we have large inconsistencies on history copy-pasting in _overwrite_ mode when default user presets are also used (especially in white balance module). But it's far funnier to shit up the interface, so it will stay there for a long time.

Darktable 3.6 and 3.8 introduced many variants of the histogram : vectorscope, vertical waveform, advanced and exotic colorspaces. Except that if you launch `darktable -d perf` in terminal and open the darkroom, you will see a lot of

```bash
23.748084 [histogram] took 0.003 secs (0.000 CPU) scope draw
23.773753 [histogram] took 0.005 secs (0.004 CPU) scope draw
23.783284 [histogram] took 0.001 secs (0.000 CPU) scope draw
```

everytime you move the cursor in the window (and not even over the histogram). It is the histogram that gets redrawn at every interaction between cursor and window. The same problem affects [many custom graphical widgets](https://github.com/darktable-org/darktable/issues/11836) and its cause is unidentified. Note that it doesn't affect Ansel, so the cause should be hidden somewhere in the 23.000 lines of code that I removed.

Twice, I tried to refactor [the shitshow this feature became](https://github.com/darktable-org/darktable/blob/darktable-4.2.x/src/libs/histogram.c), but each time a new feature more urgent was pushed that invalidated my work. I simply gave up.

The rotting state of the histogram is such that a full rewrite would take less time than a refactoring, especially since the histogram is sampled way too late in the pipeline, in the screen colorspace, which makes the definition of an histogram colorspace null given that the gamut is clipped in screen colorspace no matter what. But guess what… Darktable 4.4 will have even more options, with the ability to define color harmonies (fundamental for geeks who paint by numbers and edit histograms).

It remains that, any time you move the cursor, a great number of useless recomputations are started for nothing. How bad is it ? I had the idea of measuring the CPU use of my system when idle, with the Linux tool `powertop`. The protocol is quite simple : a laptop (CPU Intel Xeon Mobile 6th generation), working on battery in powersave mode, backlighting set to minimum, open the app and touch nothing for 4 min, then monitor the global CPU consumption of the system as reported by `powertop` during the 5th minute :

* Base system (no app opened except for `powertop` runing in a terminal) : __3.0 to 3.5 % CPU__
* Ansel :
  * opened on lighttable : __2.9 to 3.4 % CPU__,
  * opened on darkroom : __3.8 to 4.5 %__ (before reverting module groups to Darktable 3.2),
  * opened on darkroom : __3.0 to 3.5 %__ (after reverting module groups),
* Darktable :
  * opened on lightable : __6.6 to 7.1 % CPU__,
  * opened on darkroom : __30.9 to 44.9 % CPU__ _(no, it's not a coma mistake)_,

I don't understand what Darktable computes when we leave it open without touching the computer, because there is nothing to compute. Darktable in lighttable consumes by itself as much as the whole system (Fedora 37 + KDE desktop + password manager and Nextcloud client running in background), and it consumes 10 times as much as the whole system when opened in darkroom.

All this points towards very buggy graphical interface code. In Ansel, I removed a great part of the dirty code, without optimizing anything else, and these figures are only validating my choice : dirty code hides problems undetectable by reading it, and we simply can't continue on this path.

I'm apparently the only one thinking it's unacceptable to deprive the pixel pipeline of a third to a half of the CPU power to paint a stupid interface. However you put it, there is no valid reason for a software left open without touching it to turn the computer into a toaster, especially since we don't buy Russian gas anymore.

## Working against ourselves

We are photographers. The fact that we need a computer to do photography is a novelty (20 years old), linked to the digital imaging technology which replaced for all sorts of reasons (good and bad) a 160 years-old technology, known and mastered. In the process, the fact that we need a computer and a software to produce images is pure and simple [overhead](https://en.wikipedia.org/wiki/Overhead_(business)). Forcing people who don't understand computers to use them to perform tasks they could perfectly manage manually before is also a form of oppression, and hiding it as some technical progress is a form of psychological violence.

Software implies development, maintenance, documentation and project management. That's several layers of overhead atop the previous. Yet the fact that the manpower in open-source projects doesn't ask for compensation should not hinder the fact that the time spent (lost ?) on the software, its use, its development, its maintenance, is in itself a non-refundable cost.

The few examples above give an overlook of the complexification of the source code, but also of its degradation over time in terms of quality, because basic and robust features get replaced by [spaghetti code](https://en.wikipedia.org/wiki/Spaghetti_code), confusing and sneakily bugged. Behind this issue of legibility, the real problem is making the mid-term maintainability harder, which promises a gloomy future for the project, with the maintainer's approval.

Since 4 years that I work full-time on Darktable, 2022 is the first year that I find myself practically unable to identify the cause of most interface bugs, because the working logic has become very obfuscated and the code incomprehensible. The number of bugs fixed is also in constant diminution, both in absolute value and in proportion of the pull requests merged, while the volume of code traffic stays roughly constant (_note 1 : the following counts of lines of code include only C/C++/OpenCL and generative XML files and exclude comments_ [^8]) (_note 2 : the number of opened issues is counted for the lifetime of the previous version_) :

[^8]: You don't need to trust me, the command to reproduce the stats is `git diff release-3.4.0..release-3.6.0 --shortstat -w -G'(^[^\*# /])|(^#\w)|(^\s+[^\*#/])' -- '*.c' '*.h' '*.cpp' '*.xml.in' '*.xsl' '*CMakeLists.txt' '*.cl'`

3.0 (December 2019, one year after 2.6)
: - [1049 issues opened](https://github.com/darktable-org/darktable/issues?q=is%3Aissue+created%3A2018-12-24..2019-12-24+), 66 issues closed / 553 pull requests merged __(12 %)__,
  - 398 files changed, 66 k insertions, 22 k deletions, __(net : +44 k lines)__,

3.2 (August 2020)
: - [1028 issues opened](https://github.com/darktable-org/darktable/issues?q=is%3Aissue+created%3A2019-12-24..2020-08-10), 92 issues closed / 790 pull requests merged __(12 %)__,
  - 586 files changed, 54 k insertions, 43 k deletions __(net : +2 k lines)__,

3.4 (December 2020)
: - [981 issues opened](https://github.com/darktable-org/darktable/issues?q=is%3Aissue+created%3A2020-06-01..2020-12-24+), 116 issues closed / 700 pull requests merged __(17 %)__,
  - 339 files changed, 46 k insertions, 23 k deletions __(net : +23 k lines)__,

3.6 (June 2021)
: - [759 issues opened](https://github.com/darktable-org/darktable/issues?q=is%3Aissue+created%3A2020-12-24..2021-06-01+), 290 issues closed / 954 pull requests merged __(30 %)__,
  - 433 files changed, 53 k insertions, 28 k deletions __(net : +25 k lines)__,

3.8 (December 2021)
: - [789 issues opened](https://github.com/darktable-org/darktable/issues?q=is%3Aissue+created%3A2021-06-01..2021-12-24+), 265 issues closed / 571 pull requests merged __(46 %)__,
  - 438 files changed, 41 k insertions, 21 k deletions __(net : +20 k lines)__,

4.0 (June 2022)
: - [632 issues opened](https://github.com/darktable-org/darktable/issues?q=is%3Aissue+created%3A2021-12-24..2022-06-01+), 123 issues closed / 586 pull requests merged __(21 %)__,
  - 359 files changed, 30 k insertions, 15 k deletions __(net : +15 k lines)__,

4.2 (December 2022)
: - [595 issues opened](https://github.com/darktable-org/darktable/issues?q=is%3Aissue+created%3A2022-06-01..2022-12-24+), 60 issues closed / 409 pull requests merged __(15 %)__,
  - 336 files changed, 14 k insertions, 25 k deletions __(net : -11 k lines)__,
  - (_deletions are mostly due to the removal of the SSE2 path in pixel code, penalizing performance of typical Intel i5/i7 CPUs for the benefit of AMD Threadripper CPUs_),

4.4 (June 2023)
: - [500 issues opened](https://github.com/darktable-org/darktable/issues?q=is%3Aissue+created%3A2022-12-24..2023-06-01), 97 issues closed / 813 pull requests merged __(12 %)__,
  - 479 files changed, 57 k insertions, 41 k deletions __(net : +16 k lines)__,

To make things easier to compare, let's annualize them :

- 2019 : 1049 new issues, 66 closed, 88 k changes, +44 k lines,
- 2020 : 2009 new issues, 208 closed, 166 k changes, +25 k lines,
- 2021 : 1548 new issues, 555 closed, 143 k changes, +45 k lines,
- 2022 : 1227 new issues, 183 closed, 84 k changes, +4 k lines.

It seems I'm not the only one finding the 2022's bugs much more difficult to tackle because a lot fewer of them were fixed compared to 2021, and 2023 shows the same trend so far. The ratios of pull requests (actual work done) versus issues closed (actual problems solved) is simply ridiculous.

[^9]: `git checkout release-3.0.0 & cloc $(git ls-files  -- 'src/views' 'src/gui' 'src/bauhaus' 'src/dtgtk' 'src/libs')`

Between Darktable 3.0 and 4.0, the GUI code grew by 53 %, from 49 k to 75 k lines[^9] (discarding comments and white lines), and reached 79 k lines in 4.4. Letting the poor quality of it aside, I'm really not sure it improved the usability of the software by 53 %. In fact, I'm quite convinced of the contrary. In Ansel, I have so far reduced the GUI code to 53 k lines while removing little functionnality.

All this is just too much too fast for a bunch of hobbyists working on evenings and week-ends without structure and planning. The Darktable team works against itself by trying to bite more than it can chew, supporting too many different options, producing code which outcome depends on too many environment variables, being able to interact in too many different ways. All that to avoid making design decisions that could offend some guys by limiting features and available options. On the end-user side, this results in contextual bugs impossible to reproduce on other systems, so impossible to fix at all.

It's simple : the work done costs more and more work, and the maintenance is not assured, as the decline of closed issues shows, because it's simply too much. In a company, this is the time where you need to stop the bleeding before having emptied the vaults. But a team of amateurs bound to deliver no result can sustain an infinite amount of losses. Only the work created by the work is more tedious, frustrating and difficult as time goes by, and end-users are taken hostage by a gang of self-serving pricks and will pay it in terms of GUI complexity, needless CPU load, and need to relearn how to achieve basic tasks with the software at least once a year.

Actually, I'm expecting the current mass-destruction team to conveniently find less and less freetime to contribute to the project as they realize they trapped themselves in a one-way with a tractor-trailer, leaving their shit to the next ones. But the sooner they give up, the less damage they will cause.

The debauchery of options and preferences, which is the Darktable go-to strategy to (not) manage design disagreements, creates super contextual use cases where no user has the same options enabled and where it's impossible to reproduce bugs in a different environement. And to ask users to attach the `darktablerc` configuration file to bug reports would not help either since that file has currently 1287 lines practically unlegible.

Weird and hardly reproduceable bugs pile up, even on [System 76](https://system76.com/) computers designed specifically for Linux, where we can't invoke drivers issues. Many inconsistent and random bugs I have witnessed while giving editing lessons are not listed on the bug tracker, and it's rather clear that they lie somewhere in the intricacies of the `if` and `switch case` debauchery that are added at an alarming rate since 2020.

Fixing these strange and contextual bugs can only be made by simplifying the control flow of the program and therefore by limiting the number of user parameters. But the pack of geeks flapping their arms on the project won't hear about it and, worse, the bug "fixes" generally only add more lines to deal with pathological cases individually.

In fact, Darktable suffers several issues :

1. An hard core of rather mediocre developers who have a lot of free time on their hands to do random stuff, driven by the best intents in the world but oblivious of the damages they make, (mediocre people are always the more available)
1. The complacency of the maintainer, who lets dirty code through to be nice,
1. A critical lack of skills in pure mathematics, algorithmics, signal processing, color science and generally in abstract thinking, which are required beyond pixel processing code to simplify and factorize features,
1. A despicable habit of "developing" by copy-pasting code fetched elsewhere in the project or in other FLOSS projects that may used a different pipeline architecture but without adapting it accordingly (adapting implies understanding, and that's too much to ask…),
1. A fair and square refusal to prune features to make room to new ones and keep a certain balance,
1. A sampling bias, where the only users interacting with the development through Github are programmers and English-speaking. Fact is the general audience doesn't understand what a code forge is and it's difficult to encourage non-programmers to open a Github account to report bugs. We are talking of a users sample [made of more than 44 % of programmers](https://eng.aurelienpierre.com/2023/01/who-are-the-darktable-users/#Conclusion-of-this-part) and of more than 35 % of university-graduated people (they are respectively 6 % and 15 % in the general population).
1. A forced-march development style, without planning or dialogue, where every Github user can pollute discussions with a non-educated opinion on current work. Fact is image processing looks easy and is harmless, so much so that any person able to compute a logarithm feels competent. But the fundamental mistakes in Darkable colorimetry chain are there to remind us everday of the contrary.
1. A lack of project-wise priorities regarding what features to refactor, stabilize or extend : all projects are open at the same time, even if they conflicts with each other.
1. An amount of activity (emails and notifications) impossible to follow, between comments, off-topic discussions, bugs that are not, code change proposals, actual code changes that may impact your own work in progress, which means you have to be all over the place all the time ; there is a lot to read, very little to keep, discussion for the sake of discussion hinders productivity and the lack of working structure is the main cause of all that,
1. Non-blocking bugs hastily hidden before we are done understanding them, instead of fixing them for real and tackling them at their root, which moves or even aggravates issues long-term without leaving traces into any kind of documentation,
1. A release schedule that we keep no matter the price even when it's not realistic, whereas nobody imposes it upon ourselves,
1. Code changes that can happen anytime anywhere, meaning we work on quicksand and that we have to work as fast and as bad as the others to not be left behind the volume and frequency of the changes (_commits_),
1. New features that degrade usability and complicate usage without solving a definite problem, that serves as recreational projects to developers untrained in design/engineering.

But the most infuriating is this obstinacy to replace simple and functional features with horrors of over-engineering destined to please deviant and marginal uses while making everybody's life more difficult with never-ending lists of thoughtless options. The best place to hide a tree is in the middle of the forest, and many still haven't learned it.

## Mistaking agitation with activity

Any elector likes to criticize the deviance that, in politics, consists in issuing circumstancial laws, ill-written, to appease the public opinion after a special event, to show off that we act, while similar laws already exist and are not or not fully applied for lack of means. We call that agitation : this looks like action, this sounds like action, this has the cost of action, but that leads to nothing tangible or practical.

The team of amateurs without project management getting agitated over Darktable produces only future problems. In the past, Darktable was released once every year with around 1500 to 2000 [commits](https://en.wikipedia.org/wiki/Commit_(version_control)) ahead of the previous version. That's now the volume of change achieved in 6 months. A "work" volume increasing that fast without leading to teamwork methods, including clear priorities for each release and task repartition, and without software quality control based on objective metrics (number of steps or elapsed time to achieve a particular task), it's only dudes stepping on each other's feet while pushing their own agenda with no care for others, nor the project, nor users.

Darktable has become the highschool computer club, where geeks have their fun. It's globally a sum-up of all the worst stories of IT companies, with the difference that the project doesn't make a penny, which makes it urgent to ask ourselves why we impose that upon ourselves : there are no profits to share, but everybody shares the costs. It's a chaotic and toxic working environment which would only manufacture burn-out if the part-time amateurs were bound to deliver results and had to work full-time. Being the only full-time dude on it, I let you imagine the amount of stress and lost energy to stay up-to-date with the permanent cacophony, only to be sure to not miss the 2 % actually relevant to me in the amount of noise produced by unregulated discussions.

On the user side, we praise the effervescence of the Darktable project (yes, there is motion), without realizing that the commits runaway is not activity but agitation, and in particular technical debt which will have to be paid we-don't-know-when by we-don't-know-who. The beauty of a project where nobody has to take responsibility for their horseshit because nobody is accountable for nothing : it's written in the GNU/GPL license. We can therefore screw up the work of the previous ones with total impunity.

We have the beginning of a quality control, through the integration tests, which measure the perceptual error over reference image processings, but they don't trigger any response when we see an average error delta E of 1.3 (small) when the nature of the change should have a strictly zero delta E. If __the only__ test passes (because we test a single SDR image over a studio shot), no question is asked on whether the theory is sound and robust. We turned the test into a discharge, as long as the metric stays under the validation threshold…

With the release of Darktable 4.0 — _Geektable_ —, I saw on Youtube people starting to complain that this release was not very exciting. After years of dosing people with superlative releases, packed with new features we don't have time to properly test (6-8 guys who piss 38-50 k lines every 6 months while working only evenings and week-ends, you still dreaming ?), we made them addict to the overpacked Christmas Tree to ensure that, the day we start being responsible and releasing stable versions (thus boring), that will be held against us.

The reason for this frenetic release pace is the pull requests older than 3 monthes are systematically in conflict with the master branch, given that this one is shaken every month for "generalized tests". But the rare users who build the master branch have no idea what they need to test in particular, unless the dissected the commit history of Git, which implies to understand both the C language and the impact of the changes in practice on the software.

To limit long-lived branches, which will invariably end in conflict with the master, we found a brilliant solution : we release 2 versions each year, making forced-march development based on unfinished and barely-tested code a way of life, without ever realizing that the core problem is first the lack of planning, but also that contributors start coding before being done defining the problem to solve (when there is a real problem to solve, not just a guy who woke up like "it would be cool if…"), working in parallel on both parts without communication.

The dust doesn't have time to settle that we are already shaking the code base again, without enforcing stabilisation phases where we only clean-up bugs (and I'm not talking of the month of feature-freeze prior to release, but of releases dedicated only to code cleaning). The bug tracker implodes in the 3 weeks following each release, because a significant part of users only uses the pre-built packages, which coincide with Christmas and Summer hollidays, where I personnaly have better things to do after the already-stressful sprint that is the month prior to release.

Since 2021, when I update my Git repository with the last changes of Darktable master, it's always asking myself what they broke __this time__. We break faster than we fix, and most of the time, the fixes break something else. The only users finding Darktable stable are actually the ones making a very basic use of it, which is ironical for an app whose selling point is to be advanced.

And then, I'm being served the fact that it's free work as if it was an excuse. But it is actually an aggravating circumstance : why do we impose such working conditions upon ourselves if it's not even profitable ??? In addition to the fact that this free work gives me a post-release burn-out per year, it costs more and more in maintenance and the maintenance is more and more despicable to do. It's not free work, it's worse : it's work that costs without paying.

Anyway, the work is provided by people who have limited time and energy. If the resource is limited, cut the bullshit : we are in the same profitability constraints as a business, minus social contributions, except our exchange money is time and it's not refundable. Without priority management, we will get overtaken by technical debt that we will not have the resource to maintain.

## What are we waiting for to be happy ?

The fact that Darktable is a steamroller in runaway mode and without a driver, which generates an increasing amount of work, is a bad smell for a 15 years-old project. Normally, a mature project slows-down because it's complete enough to be usable and because people working on it found their cruising speed and efficient working methods.

I'm full-time on it since 2018, for a monthly income between 800 and 900 €, and it's an understatement to say that it's ill-payed to endure the desastrous consequences of disorganized amateurs trying to have their fun at the expense of the quality of the final product and its usability by computer Muggles. Besides, Muggles are despised too, as a principle.

If I'm crawling under a rock for a month to develop a perceptual color space, when I get out, it's to discover the new labyrinthine system violating a bit more the view-model-controller paradigm and being told that I come too late to oppose it. If I take 3 weeks of vacation in August, it's to discover that the maintainer bypassed (one more time) my review on a mathematical change over the aforementionned color space, which requires to sit down calmly and think, all this because… we needed to move fast ? For what emergency, exactly ?

I probably got the notification somewhere in the middle of the 2234 emails Github sent me between January and August 2022 (in 2021, it was 4044), without mentionning users who ping me everywhere, on Youtube, Reddit, Matrix, Github, Telegram, directly by emails, and previously on pixls.us (693 emails in 2022, 948 in 2021). All this for people completely out of step who don't realize that I'm doing this all week, that photography may be their hobby but is my job, and that I would just appreciate people off my back during week-ends and hollidays. You can guess that the most annoying are not the ones financially supporting my work. People respect work only if they got billed an high price for it.

I don't have time to be researcher, designer, on top of secretary, while doing technical baby-sitting for a team of Gaston Lagaffe who need both to be trained and to be watched because they are unable to :

1. make a development planning with a list of priorities of new features to work on,
2. provide a specicifications book of needs and issues, with a real use case, __before__ hurrying up on their code editor and doing whatever to develop a new feature in search for a problem to solve.
3. evaluate the maintenance cost of the change before inventing the bionic bacon pump using reversed osmosis which only works on even days if Jupyter is out of phase with Saturn,
4. limit the expenses and cut down the losses when they trap themselves into design one-ways introducing regressions worse than the hypothetical benefits expected,
5. take upon themselves and delay a release if the code is obviously not ready (or I didn't understand anything and the shareholders will ask for our heads if we release late ???).

Management (or team management) is overhead that costs some work, but the Darktable team has reached a scale where the lack of management costs actually more work, especially since none of the project founders are still in the team and the initial design blueprints need to be reverse-engineered with `grep` in the code everytime something needs change. That was sustainable with a reduced team where everybody knew each other, but Darktable has become an high-traffic project during Covid lock-downs and this way of working is not sustainable with current personnel.

Just go see the code ! Compare the branche `darktable-2.6.x` with `darktable-4.2.x`, file by file, and enjoy !

All I have heard so far are canned sentences like "it's like many other opensource projects" and "there is nothing we can do about it". People are afraid by the amount of work that a fork is (I got emails trying to convince me it was dividing productivity), without realizing the amount of resources currently wasted by the Darktable project and the permanent stress of having to base your work on an unstable code base shaken up all the time. So far, Ansel cost me less fatigue [and I solved a significant number of problems](/doc/from-darktable/) among which some were reported since 2016 without signs of interest from the bloody "community".

Besides, Ansel provides automatically-built nightly packages for Linux (.AppImage) and Windows (.exe), as to allow __real__ generalized tests, including by people unable to build the software themselves. [I asked for that back in 2019](https://github.com/darktable-org/darktable/issues/3925), but apparently, geeks have better things to do, and I had to invest 70 h myself to make that happen. The operation is already a success and allowed to fix in a matter of days Windows bugs that would have taken weeks to spot in Darktable. (And Darktable grabbed 3 weeks later my AppImage build script without proper credits, but that's a detail).

Talking about productivity, let us recall that the lighttable was rewritten almost entirely twice since 2018 (and the last version is not better nor faster) and the big change if collection filters introduced in [April 2022](https://github.com/darktable-org/darktable/pull/11066) has overwritten another similar change (but only using 600 lines instead of 6000) introduced in [February 2022](https://github.com/darktable-org/darktable/pull/10694) (the February version is the one in Ansel). We can't decently utter the word "productivity" when the work of one contributor litterally erases previous work of another's in a timeframe of one month, for simple lack of project management. It's called stepping on each other's feet.

So what are we doing to solve the issue ? Suffering in silence ? Living in denial ? Keeping on fixing stuff that another one will break in the next year when we will not be watching ? Keeping on making the Muggle believe, at photo forum length, that opensource is just as good as proprietary, while keeping as joker the fact that it's free so you get no right to complain ? Isn't that a bit too easy and dishonest, this double speech ?

Wouldn't you like to stop making habits pass as experience and mistaking fatalism with wisdom, but rather tackle the problem at its core ? Don't you think that you and me deserve better than software designed by amateurs whose sole talent is to have spare time and can afford to work for free since they moved to management and the kids are off to the university ?

Or I got mistaken since the beginning, and opensource is about giving over-complicated tools to geeks who don't really need them, while trying to convince the rest of the world that open-source is not an hyper-niche for developers ?

## Four years of work to get there

After 4 years of working on Darktable full-time for 70 % of minimal wage, and 2 years bearing the chronic dissatisfaction of staining my name by contributing to shit, I forked Ansel and will not go back.

In 4 years, I brought to this software something that sorely lacked : an unified workflow, based on a set of modules __designed__ to work together, but acting each on a distinct aspect, where Darktable modules were rather a collection of disparate plugins. We are talking about :

* filmic,
* tone equalizer,
* the physically-accurate blurs module,
* both versions of the color balance,
* color calibration, including the GUI to profile with color checkers straight in darkroom and the white balancing using CIE standards,
* the negadoctor module to invert film negatives based on Kodak Cineon,
* the diffuse and sharpen module for addition and removal of blur based on thermal diffusion,
* the guided laplacian reconstruction of highlights.

I also developed more fundamental tools providing bases for the previous modules :

* a 4th order anisotropic partial differential equations solver in wavelets space for diffuse and sharpen,
* an adaptation of the predious as the guided laplacian for RGB signal reconstruction by gradients propagation,
* a perceptual color appearance model taking the Helmholtz-Kohlrausch effect into account in the saturation computation, to limit the "fluo" effect that typically comes with intense saturation settings, in color balance,
* a theoritical help in developing the exposure-invariant guided filter (EIGF), in tone equalizer,
* a linear vector equations solver by Choleski method,
* various interpolation methods of order 2, 3 and radial-based.

In the GUI, I notably did :

* refactor style declaration, removing styling from C code to map them to the CSS stylesheet, allowing to have multiple themes for the UI, including user-defined ones,
* introduce the preview mode [focus-peaking](https://docs.darktable.org/usermanual/4.0/en/module-reference/utility-modules/shared/focus-peaking/) and [ISO 12 646 color assessment mode](https://docs.darktable.org/usermanual/4.0/en/module-reference/utility-modules/darkroom/color-assessment/),
* introduce the color vocabulary [in the global color picker](https://docs.darktable.org/usermanual/4.0/en/module-reference/utility-modules/darkroom/global-color-picker/), allowing to name the picked color from its chromaticity coordinates, targetting color-blind photographers.

After this, I wrote dozens of documentation pages in 2 languages, published articles and dozens of hours of video on YouTube to demonstrate how to use modules, in what context and for what benefit, including quick edits using only 3 to 5 modules to process 75 to 80 % of pictures, no matter their dynamic range. In the open-source world, except perharps for projects backed-up by foundations (like Krita and Blender), this level of support and documentation simply doesn't exist, and it's not the developers themselves who handle this work.

Despite all this, I never had more than 240 donators, to compare with roughly  1800 unique respondents [who participated in the 2020 and 2022 Darktable surveys](https://eng.aurelienpierre.com/2023/01/who-are-the-darktable-users/#Conclusion-of-this-part), and who declare spending [between 500 and 1000 €/an](https://www.poll-maker.com/S-Quiz-Results?qp=2539714x3792Bf5e-98#) on photography.

I will not watch them destroy the usability of this software while trying to convince myself that it's progress and there is nothing we can do about it. Instead of progress, it's the delusional vision of progress by a bunch of fifty-something dilettantes. It's been 2 years that I shut up patiently, trying to be nice, but looking at the degradation of base features, complexified to comply with the fads of mad programmers, I should have been despicable earlier. Playing nice solved nothing because the trend not only carried on, but accelerated, and there will be no realization before the point of no return. We can't expect from those who created the problems to be the ones solving them.

So if I have to work for a "community" who is in mostly for the subscription-free aspect of the software, and who decided that my work is not worth minimal wage, well, I will do it under my terms and with my standards.

In terms of features, Darktable already has too much and we need to prune. It's been 10 years that I use it and it's already been packed with ill-design stuff. The challenge now is to present the features cleverly, and to fix annoying bugs __before__ these idiots introduce new ones, or even fix them in their own special way : by hiding the dust under the rug. Keeping in mind that Darktable's pipeline is 15 years old, and we can't optimize it much more than that, given that it was already tortured a lot to avoid a full rewrite (and a full rewrite offers no benefit if we have to keep Gtk as graphical backend since it's the primary performance bottleneck).

The solutions needed by Darktable imply to remove code and options, not to add always more. Robustness is at that price. The Darktable team does the exact opposite without learning from its mistakes.

With Ansel, I want a way to finish this work peacefully so Linux users have a reliable, consistent and performant tool for their artistic photography. Before switching to [Vkdt](https://github.com/hanatos/vkdt/) because the current design is shows its limits.
---

# Final resampling : https://ansel.photos/en/doc/views/darkroom/modules/finalscale/

This module has always been in the pixel pipeline but was hidden in darktable GUI. Ansel 0.0 exposes it to allow users to fix a mistake in the default pipeline order of darktable.

## Action

When exporting images to save them to a file, the picture is processed at full resolution and then rescaled to the specified output size by this module.

_Final resampling_ is never used when producing previews, for the lighttable thumbnails or in the darkroom, because in this case images are resampled earlier in the pipeline, for better rendering speed. It has no effect if the image is exported at its full (original) resolution either.

## Problem

In darktable legacy pipeline order as well as in the darktable v3.0 default pipeline order, the final resampling is positionned after the [output color profile](./output-color-profile.md), which means it resamples RGB data encoded with an [OETF](https://en.wikipedia.org/wiki/Transfer_functions_in_imaging) (improperly called "gamma" in ICC color profiles), meaning it processes non-linear RGB.

Resampling non-linear RGB is known to create artifacts such as fringes around sharp and contrasted edges, and should be avoided in general. However, in a majority of cases, it makes no perceptible difference, especially when processing low-dynamic-range images.

## Solution

Ansel exposes the GUI widget of the _final resampling_ module to give users the opportunity to correct themselves the position of the module in the pipeline, pending a new default pipeline order.

It is recommanded to place the _final resampling_ before the _output color profile_ module, and even before [_filmic_](./filmic.md) for best results.

## Controls

The type of interpolation used for this final rescaling is defined globally in the software, in the [_preferences_](../../../preferences-settings/processing.md), under the _pixel interpolator (scaling)_ parameter.

## Enhancements

A common practice is to apply some sharpening after the downsampling. Ansel does not do it by default, but you can by duplicating an instance of [_diffuse and sharpen_](./diffuse.md), [_sharpen_](./sharpen.md) or [_contrast equalizer_](./contrast-equalizer.md) and moving it after (above) the _final resampling_ module. In that case, ensure _final resampling_ is before the display view transform (_filmic_ or the _base curve_) such that your sharpening instance can go in-between. You should keep your sharpening at the pixel level, with a radius of 1-2 px maximum.
---

# Sampling : https://ansel.photos/en/tags/sampling/
---

# Astrophoto denoise : https://ansel.photos/en/doc/views/darkroom/modules/astrophoto-denoise/

Remove image noise while preserving structure.

This is accomplished by averaging each pixel with some surrounding pixels in the image. The weight of such a pixel in the averaging process depends on the similarity of its neighborhood with the neighborhood of the pixel being denoised. A patch with a defined size is used to measure that similarity.

As denoising is a resource-intensive process, it slows down pixelpipe processing significantly. Consider activating this module late in your workflow.

## Module controls

patch size
: The radius of the patch used for similarity evaluation.

strength
: The strength of the denoising.

luma
: The amount of denoising to apply to luma. Select carefully in order not to lose too much structure.

chroma
: The amount of denoising to apply to chroma. You can be much more aggressive with this parameter.
---

# Batch Editing : https://ansel.photos/en/tags/batch-editing/
---

# Blurs : https://ansel.photos/en/doc/views/darkroom/modules/blurs/

Simulate physically-accurate blurs in scene-referred RGB space.

## Blur types

Three types of blur are provided:

1. _lens blur_: Simulates a lens diaphragm with a configurable number of blades and blade curvature to create synthetic bokeh.
2. _motion blur_: Simulates the effect of camera motion with a configurable path.
3. _gaussian blur_: This is not really an optical blur but can be used for denoising or for creative effects using [blend modes](../masking-and-blending/blend-modes.md)

A diagram at the top of the module shows the shape of the blurring operator (known as the [_point spread function_](https://en.wikipedia.org/wiki/Point_spread_function)). The module will turn each luminous point from the scene into a blot shaped like the displayed blurring operator, with the size of the blot defined by the _blur radius_.

## Module controls

### General

blur radius
: The spreading size of the blur.

blur type
: Choose between the different blur variants (above).

### Controls specific to lens blur

diaphragm blades
: The number of blades that the diaphragm is composed of. Older lenses used typically 5 or 7 blades, newer lenses typically use 9 or 11 blades. In any case, real lenses have an odd number of blades and any number greater than 11 blades comes very close to producing a perfect disc. If you degenerate the diaphragm settings with the _concavity_ to create a star or an asterisk, this control defines how many branches it has.

concavity
: - a concavity of 1 ensures the diaphragm is a [regular convex polygon](https://en.wikipedia.org/wiki/Regular_polygon#Regular_convex_polygons) (triangle, pentagon, heptagon, etc.).
: - a concavity greater than 1 but lower than `number of blades - 1` turns the shape into a star.
: - a concavity greater than `number of blades - 1` but lower than `number of blades` turns the shape into an asterisk, when decreasing _linearity_ below 1.
: - a concavity greater than or equal to `number of blades` degenerates the shape into a "burst pattern".

linearity
: - a linearity of 0 creates a disc, no matter the number of blades or the concavity.
: - a linearity of 1 makes all the outer bounds of the shape straight.
: - a linearity between 0 and 1 makes the outer bounds of the shape more or less curved.

rotation
: Allows the shape to be rotated with respect to its center -- mostly useful with a small number of blades, when a particular orientation is needed.

### Controls specific to motion blur

direction
: The orientation of the motion's path in angular degrees. 0° is horizontal motion.

curvature
: The curvature of the motion. Zero produces a straight line, a negative value produces a concave curvature, a positive value produces a convex curvature.

offset
: Shifts along the motion path following its curve. This is useful to select a portion of the curved path that is symmetrical, which produces a coma shape (example 1: direction = -45°, curvature = +2, offset = +0.5 ; example 2 : direction = -45°, curvature = +1, offset = +1).

## Caveats

This module is implemented using a "naive" convolution, which is a slow algorithm. Faster approaches are available (using FFT) but not yet implemented. The GPU implementation, through OpenCL, should hide this issue somewhat. In any case, the runtime of the module will increase with the square of the blur radius.

The blurring process does not take scene depth and depth-of-field into account, but blurs the whole image as a flat object. It is therefore not suitable for creating fake depth-of-field. Using Ansel's general masking will only partially work to isolate the foreground of an image, since it will still be blurred into the background.

## Tips and tricks

All images are usually (even a tiny bit) noisy. If you blur only a part of the image, the blurred region will look suspiciously clean compared to the rest of the image. It is therefore a good idea to add a bit of noise on top of the blurred part to blend it with the rest, using either the [grain](./grain.md) or the [censorize](./censorize.md) modules.
---

# Censorize : https://ansel.photos/en/doc/views/darkroom/modules/censorize/

Degrade parts of the image in an aesthetically pleasing way, in order to anonymize people/objects or hide body parts.

Censorize works in linear RGB color space to apply a physically-accurate gaussian blur and gaussian luminance noise.

Aside from anonymization, this module can also be used for a wide range of creative purposes, for example:

- Combine a simple blur with a multiply [blend mode](../masking-and-blending/blend-modes.md) to create a realistic bloom (Orton effect).
- Combine a simple blur with a subtract blending mode and low opacity to create an [unsharp mask](https://en.wikipedia.org/wiki/Unsharp_masking), similar to the [sharpen](./sharpen.md) module but in an RGB scene-referred space.
- Add noise to create artificial grain.

---

**Note:** The anonymizing methods provided by this module are not forensically safe in order to favor aesthetics. Some forensic techniques may still be able to reconstruct the censorized content based on its structure, especially for simple shapes and text (e.g. license plates, street numbers).

If forensically safe anonymization is required, the only way to achieve this is to paint the surfaces with a solid color.

**The Ansel team does not accept responsibility for poorly anonymized pictures leading to the identification of individuals or personal property.**

---

## Workflow

You are advised to leave the module's controls at their default values while you [mask](../masking-and-blending/masks/drawn.md) the areas of the image that you wish to censorize, in order that the details of the image remain visible.

## Module controls

input blur radius
: The strength of the first pass of the gaussian blur.

pixellation radius
: The size of the "big pixels" created after the first pass of gaussian blur.

output blur radius
: The strength of the second pass of the gaussian blur, applied after the pixellation.

noise level
: The strength (standard deviation) of the luminance gaussian noise applied after the second pass of the gaussian blur. Adding noise can fake details in the blurred regions and make content detection more difficult for artificial intelligence algorithms.
---

# Chromatic aberrations : https://ansel.photos/en/doc/views/darkroom/modules/chromatic-aberrations/

Correct chromatic aberrations.

In contrast to the [_raw chromatic aberrations_](./raw-chromatic-aberrations.md) module, this module does not require raw data as input.

## Workflow

To obtain the best result, you are advised to proceed as follows:
1. Attenuate the chromatic aberrations as much as possible in the [_lens correction_](./lens-correction.md) module using the TCA sliders.
2. Increase the strength slider in this module to better see its effect.
3. Increase the radius until the chromatic aberrations disappear. If this is insufficient, try enabling the "very large chromatic aberrations" setting.
4. Choose the guide that gives the best result in term of sharpness and artifacts.
5. Reduce the strength to avoid washing out the colors too much.

For more complicated cases you could also try the following:
- Use several instances with different correction modes -- for example, a first instance in "brighten only" mode, and a second in "darken only" mode.
- Use several instances with low strength to correct the chromatic aberrations a little at a time without degrading colors too much.
- Use the module with parametric or drawn masks.
- Use RGB red, green and blue blend modes to restrict the effect to a particular channel.

## Module controls

guide
: The color channel that will be used as a reference for the correction.

radius
: The radius of the effect. Increase until chromatic aberrations are eliminated. This is the most important slider of the module.

strength
: This slider acts as a safeguard and can help to preserve colorful areas that do not suffer from chromatic aberrations. Increase for stronger correction, decrease for stronger preservation.

correction mode
: Allows you to restrict the effect to brighten or darken pixels only. For full control, this can be used in combination with R, G, B blend modes and multiples instances.

very large chromatic aberrations
: Makes the algorithm iterative to help in reducing very large chromatic aberrations.
---

# Clipping warning : https://ansel.photos/en/doc/views/toolboxes/clipping/

Highlight areas of the image that may exhibit luminance or gamut clipping.

When an image is sent to a display device, each pixel is normally represented as a set of 3 numbers, representing the intensity of the red, green and blue primary colors in the output color space. Because the output color space is usually closely related to hardware with physical limations, there is a maximum permitted value for the [R,G,B] channels, representing the maximum available intensity for that color space. Similarly, there is also a minimum value below which pixel values will be mapped to zero. When we try to convert from a larger color space to the final output color space, any values exceeding this maximum will be clamped to the maximum value, and any values below the minimum will be clamped to zero. This process is called "clipping" and it will lead to lost detail, or "incorrect" colors for any pixels with clipped channels.

Click the clipping-warning icon in the [bottom toolbar](../darkroom/darkroom-view-layout.md#bottom-panel) to enable the clipping warning.

There are two ways in which a pixel might become clipped when represented in the output color space.

- _luminance clipping_: This can occur when a pixel is too bright to be represented in the output color space. The pixel luminance is calculated as a weighted average of the [R,G,B] channels. If this average exceeds the maximum allowed value, it is an indication of over-exposure. The overall luminance of a pixel can also be too dark to be represented by an [R,G,B] value in the output color space, in which case it will simply be shown as black. We normally deal with luminance clipping by carefully adjusting tone mappings and exposure levels.

- _gamut clipping_: The output color space defines a set of primary colors that, mixed together in certain ratios, produce the final output color. However, there are only so many colors that can be produced by mixing together a combination of those three primary colors. Highly saturated colors in particular can be difficult to represent, especially for pixels that are very bright or very dark. If there is no set of positive [R,G,B] values that can represent a given color at a given level of brightness, we say that the color is "out of gamut", and we need to settle for another color instead that _can_ be represented by permitted [R,G,B] values within the color space. We can handle gamut clipping by being careful not to over-saturate colors in the highlights and shadows, and possibly by using some color grading/color mapping techniques.

The "clipping warning" module highlights those pixels that cannot be accurately represented in the output color space, either due to luminance or gamut clipping. It offers several modes to help you differentiate between luminance and gamut clipping, so that you can make better decisions about how to address any issues.

As the clipping warning runs at the end of the preview pixelpipe, it receives data in display color space then converts it to histogram color space. If you are using a display color space that is not "well behaved" (this is common for a device profile), then colors that are outside of the gamut of the display profile will clip or distort.

The clipping warning module, described here, warns you about clipping caused by image processing and the limitations of the output color space. It should not be confused with the following similar tools:

- The [raw overexposed warning](./raw-overexposed.md) indicates where pixels in the original raw file are clipped due to physical limitations in the dynamic range of the camera sensor. This module highlights information that was permanently lost at the point of image capture, and you need to deal with it as best you can using highlight recovery techniques.

- The [gamut check](./gamut.md) module also provides information about clipping arising from image processing. It is based on the external littleCMS library, and is more or less equivalent to the _full gamut_ mode in the clipping warning module. The downsides of the gamut check module are that it doesn't allow you to distinguish between clipping caused by luminance and gamut mapping, and it is much slower than the clipping warning indicator.

## Module controls

Right-click on the clipping icon to show the following options:

clipping preview mode
: Choose the type of clipping that you want to highlight:

: - _any RGB channel_: Provides an over-clipping indication if any one of the three [R,G,B] channels exceeds the maximum permitted value for the histogram color space, or an under-clipping indication if the three [R,G,B] channels are too dark and are all forced to black.
: - _luminance only_: Indicates any pixels that are clipped because their luminance falls outside of the range set in the "upper threshold" and "lower threshold" sliders. If this happens, it generally means that tone mapping or exposure settings have been poorly set
: - _saturation only_: Indicates where over-saturated colors have pushed one or more of the [R,G,B] channels towards a value outside the permitted range of the histogram color space, even though the overall luminance of the pixel may lie within acceptable limits. This means that the pixel's color is impossible to represent in the histogram color space, and can arise from poorly set gamut mapping or saturation settings,
: - _full gamut_: Shows a combination of the three previous options. This is the default mode, and it gives the most complete indication of potentially problematic pixels.

color scheme
: By default, the indicator marks pixels with _red_ where the upper threshold is exceeded (over-clipping) and with _blue_ where the lower threshold is breached (under-clipping). This color scheme can be changed to _black & white_ or _purple & green_ for "over & under" indicators, which may be useful to improve visibility for some images.

lower threshold
: Expressed in EV relative to the white point (which is nominally EV = 0). If the [R,G,B] channels all fall below this value, an under-clipping indicator is shown warning that the pixel may be end up being crushed to black. Use the following reference to set this threshold, depending on your intended output medium:

: - _8-bit sRGB_ clips blacks at --12.69 EV
: - _8-bit Adobe RGB_ clips blacks at --19.79 EV
: - _16-bit sRGB_ clips blacks at --20.69 EV
: - _fine art matte prints_ typically produce black at --5.30 EV
: - _color glossy prints_ typically produce black at --8.00 EV
: - _black & white glossy prints_ typically produce black at --9.00 EV

upper threshold
: How close a pixel should be to the upper limit before being flagged by the clipping warning, expressed as a percentage (default 98%). In the case of gamut checks, this controls how close the saturation of the pixel is allowed to get to the limits of the color space's gamut before a clipping indication is flagged.
---

# Color assessment : https://ansel.photos/en/doc/views/toolboxes/color-assessment/

Assess colors and brightness in your image using ISO 12646:2008 recommended viewing conditions.

When developing an image, the way we perceive brightness, contrast and saturation is influenced by the surrounding ambient conditions. If an image is displayed against a dark background, this can have a number of adverse effects on our perception of that image:

- Exaggeration of the perceived exposure makes the image seems brighter than it really is. This is nicely illustrated by the [Adelson checkerboard shadow effect](https://en.wikipedia.org/wiki/Checker_shadow_illusion).
- A decrease in the perceived saturation in the image makes the colors seem less rich than they really are (the Hunt effect).
- A decrease in the perceived contrast in the image makes the tones seem flatter than they really are (Bartleson-Breneman effect 3)

The end result is that the final image can end up being too dark and overly-processed in terms of contrast and color saturation. To avoid this, the "ISO 12646:2008" standard makes some recommendations about the conditions under which the colors of an image should be assessed. The _color assessment_ module in the darkroom places a frame around the image to help the user better assess the colors in the image, along the lines of those recommendations.

![the color-assessment frame around an image](color-assessment-overview.jpg)

When the color-assessment button is selected in the [bottom toolbar](../darkroom/darkroom-view-layout.md#bottom-panel), the image is zoomed out so that a thick mid-gray border appears around the image to act as a reference against which to compare the image's tones. A thinner white border is placed immediately around the image to give the eyes a basis for comparison when looking at parts of the image that are meant to be a bright white.

Although the color assessment mode provides a mid-gray surrounding to the image, it is recommended that you also set your user interface (in [preferences > general](../../preferences-settings/general.md)) to one of the "grey" themes. These themes are designed to provide a user interface that is close to middle gray (it is actually slightly darker to allow better contrast with the text in the user interface). When one of these themes is used together with the color assessment mode, this will help to avoid the above perception issues.

Color assessment mode has no default keyboard shortcut, but you can assign one to it, or trigger it from the [global action search](../../getting-started/keyboard.md#vimkey-like-global-action-search).
---

# Color balance : https://ansel.photos/en/doc/views/darkroom/modules/color-balance/

An advanced module which brings color-grading tools from cinematography into the photographic scene-referred pipeline.

This module is not suitable for beginners with no prior knowledge of color theory, who might want to stick to the _global chroma_ and _global vibrance_ settings until they have a good understanding of the [dimensions of color](../../../color-management/color-dimensions.md).

## Introduction

Color-grading is an important part of image editing. It can help to remove unwanted color casts and can also deliver a creative color twist that will add atmosphere to your images. In the days of film photography, most of the color ambiance was obtained with the film emulsion and the developing chemicals, with some color timing being performed under the enlarger with color heads. This consumed expensive resources and was mostly reserved for the cinema industry, where the job was performed by a colorist.

In the digital age, where raw images look flat and even, color-grading assumes the same role that film emulsions did, by re-introducing color shifts for aesthetic purposes. It can also serve to harmonize the color palette of a series of images (which may have been shot under different conditions) to achieve a consistent global look. For this task, the [vectorscope](../../toolboxes/scopes.md#vectorscope) is also extremely useful.

Colorists usually split color-grading into two distinct steps:

1. _Primary color-grading_ aims to fix unwanted color casts and create a neutral starting point,
2. _Secondary color-grading_ gives the image its final look and atmosphere.

Primary color-grading is best left to the [_color calibration_](./color-calibration.md) module, which operates in a physical framework better suited to illuminant correction. _color balance_, on the other hand, is mostly concerned with secondary color-grading. Performing a truly neutral primary color-grading should make the secondary color-grading easy to transfer between images (via styles, presets or copy & paste) with a similar effect.

## General principles

The _color balance_ module is an improvement over the [American Society of Cinematographers Color Decision List](https://en.wikipedia.org/wiki/ASC_CDL) (ASC CDL), and uses alpha masks to allow the effect to be properly split between shadows and highlights. The classic CDL acts on the entire luminance range, and each of its parameters is given more weight on some parts of the image only as a side-effect of the mathematics.

This module works, for the most part (_4 ways_, _chroma_, _vibrance_, _contrast_), in a linear RGB color space designed specifically for color-grading. This color space exhibits a uniform spacing of perceptual hues while retaining a physically-scaled luminance[^1]. The perceptual part of the module (_saturation_ and _brilliance_) works in the JzAzBz[^2] color space, which provides a perceptual scaling of both lightness and chromaticity suitable for HDR images. Both color spaces ensure that saturation and chroma changes take place at constant hue, which is not the case for most other saturation operators in Ansel (notably in the older [_color balance_](./color-balance.md) module).

[^1]: Richard A. Kirk, Chromaticity coordinates for graphic arts based on CIE 2006 LMS with even spacing of Munsell colours, 2019.

[^2]: Safdar et al., Perceptually uniform color space for image signals including high dynamic range and wide gamut, 2017. <https://doi.org/10.1364/OE.25.015131>

The _color balance_ module expects a scene-referred linear input and produces a scene-referred RGB output, which may or may not be linear, depending on the module settings (_contrast_ and _power_ will delinearize the output).

At its output, _color balance_ checks that the graded colors fit inside the pipeline RGB color space (Rec 2020 by default) and applies a soft saturation clipping at constant hue, aiming to retarget out-of-gamut color to the nearest in-gamut color by scaling both chroma and lightness. This prevents the chroma and saturation settings from pushing colors outside of the valid range and allows more drastic adjustments to be safely used.

_Note that this module abides by the CIE definitions of chroma and saturation, as explained in the [dimensions of color](../../../color-management/color-dimensions.md) section._

## Module controls

### Master tab

hue shift
: Rotate all colors in the image by an angle over the chromaticity plane, at constant luminance and chroma. You can use this control to remove spilled colored light on a subject or to quickly change the color of some object. This setting is usually best applied locally, using [masks](../../../views/darkroom/masking-and-blending/masks/_index.md).

global vibrance
: This affects the chroma dimension of color over the entire image, prioritizing those colors with low chroma. This allows the chroma of neutral colors to be increased without exaggerating already-colorful pixels.

contrast
: This setting is applied on the luminance channel at constant hue and chroma. The fulcrum setting (in the [_masks_](#masks-tab) tab, under _contrast gray fulcrum_) allows you to set the neutral point of the contrast curve:
: - at the fulcrum, the contrast curve leaves the luminance unchanged,
: - below the fulcrum, the contrast curve decreases the luminance for positive contrast values, or increases it for negative values,
: - above the fulcrum, the contrast curve increases the luminance for positive contrast values, or decreases it for negative values.

: The fulcrum has a value of 18.45% by default, which is consistent with the current scene-referred workflow and should fit most use cases (assuming that global brightness has been fixed as recommended using the [_exposure_](./exposure.md) module).

: The contrast algorithm gives natural results that mimic the central part of the contrast curve of analog film. However, it will also increase the image's dynamic range, which may void _filmic_ settings in the pipe. For global contrast adjustments, you should normally use the [_tone equalizer_](./tone-equalizer.md) module -- the _color balance_ contrast slider is best used with masks, e.g. for selective corrections over the foreground or background.

#### Linear chroma grading

Linear chroma grading affects the chroma dimension proportionally to its input value, at constant hue and luminance. It does this globally, with a flat coefficient (using the _global chroma_), as well as on each of the _shadows_, _mid-tones_ and _highlights_ masks (defined in the [_masks_](#masks-tab) tab under _luminance ranges_).

#### Perceptual saturation grading

Perceptual saturation grading affects both the luminance and the chroma dimensions, in a perceptual space, proportionally to its input value, at constant hue. It does this globally, with a flat coefficient (using the _global saturation_), as well as on each of the _shadows_, _mid-tones_ and _highlights_ masks (defined in the [_masks_](#masks-tab) tab under _luminance ranges_).

#### Perceptual brilliance grading

Perceptual brilliance grading affects both the luminance and the chroma dimensions, in a perceptual space, proportionally to its input value, at constant hue, and in a direction orthogonal to the saturation. Its effect is close to that of changing exposure, but scaled perceptually. It does this globally, with a flat coefficient (using the _global saturation_), as well as on each of the _shadows_, _mid-tones_ and _highlights_ masks (defined in the [_masks_](#masks-tab) tab under _luminance ranges_).

### 4 ways tab

Each of the settings in the 4 ways tab is composed of the same three components, which define a color using independent coordinates:

1. _luminance_,
2. _hue_,
3. _chroma_.

Color input like this defines a color shift applied to the image globally or over the specified luminance range.

Each hue slider has a color picker, which may be used to compute the opponent color of the selected region. This is useful to revert unwanted color casts (e.g. skin redness), since shifting the color to its opponent cast neutralizes it.

#### Global offset

This is equivalent to the ASC CDL _offset_ and falls back to adding a constant RGB value to all pixels, quite like the _black offset_ in the _exposure_ module. This control does not use masking.

#### Shadows lift

This is conceptually equivalent to the _lift_ from _lift/gamma/gain_, although implemented differently, and falls back to multiplying the masked pixels by a constant RGB value. It is applied using the _shadows_ mask.

#### Highlights gain

This is equivalent to the ASC CDL _slope_, and falls back to multiplying a the masked pixels by a constant RGB value. It is applied using the _highlights_ mask.

#### Global power

This is equivalent to the ASC CDL _power_, and falls back to applying a constant RGB exponent. It is not masked and needs to be normalized, since the power function has a different behaviour above and below 1, and we are in an unbounded pipeline where white is typically greater than 1. The normalization parameter is available in the [_masks_](#masks-tab) tab under _white fulcrum_.

### Masks tab

This tab defines auxiliary controls for the previous tabs. Masking controls typically don't require any user modification since the defaults are calibrated to suit most needs and fulfil the normal scene-referred pixel pipeline expectations. You should only need to change these settings in specific scenarios.

#### Luminance ranges

The graphs show the opacity (on the _y_ axis) of the 3 luminance masks relative to the pixel luminance (on the _x_ axis). The darkest curve represents the _shadows_ mask, the brightest represents the _highlights_ mask, and the third curve represents the _mid-tones_ mask.

Only the _shadows_ and _highlights_ masks can be controlled directly -- the _mid-tones_ mask is computed indirectly from the others and acts as an adjustment variable.

shadows fall-off
: Control the softness or hardness of the transition from fully opaque (100%) to fully transparent (0%) for the shadows mask.

mask middle-gray fulcrum
: Set the luminance value where all three masks have 50% opacity. In practice, this is used to define how the image is separated into shadows and highlights.

highlights fall-off
: Control the softness or hardness of the transition from fully opaque (100%) to fully transparent (0%) for the highlights mask.

For each of these settings, a mask button, provided to the right of the slider, displays the appropriate mask (shadows, mid-tones, highlights), overlaid as a checker board. The still-visible area of the image (not hidden by the mask) is the area that will be affected by the shadows, mid-tones and highlights sliders in the other tabs.

All mask previews display the output of the module, including any color changes made, so you can also activate them while editing, to see only the affected part of the image.

Luminance masks are computed at the input of the module, which means that they are insensitive to any luminance changes made inside the module.

#### Thresholds

white fulcrum
: Set the white point luminance in EV. This is used to normalize the _power_ setting in the [_4 ways_](#4-ways-tab) tab. Display-referred implementations of power functions assume that white is at 100%, which removes the need for normalization. For scene-referred purposes this needs to be taken into account.

: The color picker to the right of the slider automatically sets the white fulcrum to the maximum luminance from the selected region, which should be sufficient in most cases.

contrast gray fulcrum
: Set the fulcrum for the _contrast_ setting in the [_master_](#master-tab) tab. This corresponds to the luminance value that will be left unchanged by the contrast adjustment. This setting usually matches the middle-gray linear value. If you followed the scene-referred workflow recommendations and set the global brightness early in the pipeline, using the _exposure_ module, the correct value should usually be around 18-20%.

: The color picker to the right of the slider automatically sets the contrast gray fulcrum to the average luminance from the selected region. This relies on the assumption that the average luminance is usually close to middle-gray, which is not true if you have specular highlights or primary light sources in the frame, or for low/high-key images.

#### Saturation formula

Note that this setting is not really appropriate for the _masks_ tab (since it is not technically related to the masks) but is placed here because it is not meant to be used regularly and in the spirit of saving some display real-estate. Two options are provided:

JzAzBz (2021)
: This mode is the original saturation algorithm. It uses the JzAzBz uniform color space (UCS) to compute the saturation. This color space is not meant for color changes and its lightness does not account for the [Helmholtz-Kohlrausch effect](https://en.wikipedia.org/wiki/Helmholtz%E2%80%93Kohlrausch_effect), which states that colorful colors will look brighter than neutral or near-neutral colors (greys and pastels) having the same luminance. It also suffers from non-smooth behaviour near black, with colors being darkened too much.

Ansel UCS (2022)
: The Ansel Uniform Color Space has been [designed from the ground up](https://eng.aurelienpierre.com/2022/02/color-saturation-control-for-the-21th-century/), using psychoperceptual measurement datasets, for the sole purpose of the color manipulation (saturation) performed by this module. This color space does account for the [Helmholtz-Kohlrausch effect](https://en.wikipedia.org/wiki/Helmholtz%E2%80%93Kohlrausch_effect) and has a built-in gamut mapping formula that is more accurate and efficient than can be achieved in JzAzBz. It displays a smoother behaviour which makes saturation changes more even across the lightness range.

#### Mask preview settings

These settings apply to the mask previews, displayed by clicking the mask buttons in the _luminance ranges_ section. These settings are saved globally, so will be applied to all subsequent images unless changed.

checker board color 1 and 2
: Set the two colors for the background checker board mask underlay. You can set them to opponent colors of the current image to aid legibility.

checker board size
: Set the width of the checker board cells in pixels (adjusted according to the display's DPI setting).

## FAQ

### Saturation or chroma?

As described in the [dimensions of color](../../../color-management/color-dimensions.md) section, saturation and chroma roam the (lightness, chroma) plane in different directions. In addition, the chroma of _color balance_ uses a scene-referred linear space, while the saturation uses a perceptual space, which rescales color for even spacing.

In practice, you should use the chroma setting if you want to preserve the scene-linearity of the light emission and/or keep the luminance unchanged. However, these changes might affect some hues more heavily than others, due to the fact that the color space is not fully perceptually-scaled.

Saturation is closer to the effect of mixing white paint with some base color. Reducing the saturation of red will degrade it to pink, while reducing its chroma will degrade to a gray shade at the same luminance. Saturation is perhaps a more intuitive way to interact with color, due to its connection with painting.

Choosing one or the other is mostly a matter of deciding where on the (lightness, chroma) graph you want to push your colors, and where they are to begin with. To reach pastel colors, saturation is the way to go. To reach laser-like colors (almost monochromatic), at the risk of looking synthetic, chroma is the way to go.

### What is the connection with lift/gamma/gain?

The _lift/gamma/gain_ algorithm relies on a display-referred color space, since it assumes a bounded and symmetric dynamic range, with white point at 100% and gray at 50%. As such, it is simply unusable in a scene-referred space. However, the only incompatible part is the _lift_. The _gamma_ is exactly the ASC CDL _power_, and the _gain_ is exactly the ASC CDL _slope_.

The _color balance_ module simply has two slopes instead of one: the _gain_, applied on the highlights extracted from the whole image by a mask, and the _lift_, applied similarly but on the shadows.

### Changing contrast

While _color balance_ is mostly about color (other modules handle the global contrast in chromaticity-preserving ways) luminance is as much a part of color as hue or chroma, and it needs to be dealt with here too, because the perception of saturation relies on it. If you wish to turn red into pink, for example, reducing its chroma will turn it gray, so you need to increase its luminance as well.

There are several ways to change the contrast in _color balance_, either locally (with masks) or globally (without):

- In the [_master_](#master-tab) tab, use the _contrast_ setting (possibly alongside the _contrast gray fulcrum_ in the [_masks_](#masks-tab) tab). Be aware that this will raise the white point and therefore increase the dynamic range of the image, which may void filmic settings later in the pipeline.
- In [_perceptual saturation grading_](#perceptual-saturation-grading), desaturate highlights and resaturate shadows to produce a luminance contrast boost,
- In [_perceptual brilliance grading_](#perceptual-brilliance-grading), add brilliance in the highlights and remove brilliance in the shadows to produce to a luminance contrast boost,
- In the [_4 ways_](#4-ways-tab) tab, set the _shadows lift_ luminance to negative values and the _highlights gain_ luminance to positive values, which also produces a luminance contrast boost.

The difference between these methods is how the effect will be weighted relative to the input of the module. You are advised to do the majority of luminance contrast adjustments in the _filmic_ and _tone equalizer_ modules, and then undertake final changes in _color balance_ while examining the colors.

### Internal processing

The following is the internal order of operations within the module:

1. Transform from pipeline RGB to Kirk/Filmlight Ych space,
2. Apply hue shift at constant chroma and constant luminance,
3. Compute luminance masks with Y,
4. Apply the _linear chroma_ and _vibrance_ settings at constant hue and luminance,
5. Transform to Kirk/Filmlight RGB space,
6. Apply the _4 ways_ settings (except _luminance power_),
7. Transform to Kirk/Filmlight Yrg space,
8. Apply _luminance power_ and _contrast_ on Y,
9. Transform to JzAzBz space,
10. Apply the _perceptual saturation_ and _perceptual brilliance_ settings,
11. Soft-clip the chroma using pipeline RGB gamut at constant hue and lightness,
12. Transform back to pipeline RGB.

## Caveats

Setting the global chroma to -100% will not produce a real monochrome image, as is customary with other algorithms. The reason for this is that the RGB space used has a D65 white point defined in CIE LMS 2006 space, while Ansel uses a white point defined in CIE XYZ 1931 space, and there is no exact conversion between these spaces. The result will therefore be a slighly tinted black & white image. If your intent is to get a real black & white image using the luminance channel, the _color calibration_ module offers a _B&W : luminance-based_ preset that does exactly the same thing but without the white-point discrepancy.

This module has its gamut-mapping (against pipeline RGB) permanently enabled. This means that if your original image contains some largely out-of-gamut colors to start with, simply enabling _color balance_ with no particular setting will slightly alter its colors. This is probably for the best.

The maximum saturation allowed in the pipeline working RGB space is recorded for each hue when initializing the module, and is later cached in a LUT (look-up table) to save performance. If the working profile is later changed, _color balance_ is not notified, meaning that it will not update its cached hue/saturation LUT. To force a LUT update, you can simply change any setting in the _color balance_ module, then change it back again. It is not recommended that you change the working RGB space half-way through an editing session, as this could result in unexpected chroma and hue changes.

For performance reasons, the non-linear conversions from and to the working RGB space are bypassed, meaning that the internal colorimetry will be wrong when using non-linear color spaces. Note that there is no reason to use non-linear spaces as working RGB since they make alpha blending more challenging for no benefit.
---

# Color calibration : https://ansel.photos/en/doc/views/darkroom/modules/color-calibration/

A fully-featured color-space correction, white balance adjustment and channel mixer module.

This simple yet powerful module can be used in the following ways:

- To adjust the white balance ([chromatic adaptation](#white-balance-in-the-chromatic-adaptation-transformation-cat-tab)), working in tandem with the [_white balance_](./white-balance.md) module. Here, the _white balance_ module makes some initial adjustments (required for the [_demosaic_](./demosaic.md) module to work effectively), and the _color calibration_ module then calculates a more perceptually-accurate white balance after the input color profile has been applied.

- As a simple RGB [channel mixer](#channel-mixing), adjusting the R, G and B output channels based on the R, G and B input channels, to perform cross-talk color-grading.

- To adjust the [color saturation, brightness and B&W output](#outputs-tab), based on the relative strength of the R, G and B channels of each pixel.

- To produce a [grayscale output](#outputs-tab) based on the relative strengths of the R, G and B channels, in a way similar to the response of black and white film to a light spectrum.

- To improve the color accuracy of the input color profile using a [color checker](#extracting-settings-using-a-color-checker) chart.

## White Balance in the Chromatic Adaptation Transformation (CAT) tab

Chromatic adaptation aims to predict how all surfaces in the scene would look if they had been lit by another illuminant. What we actually want to predict, though, is how those surfaces would have looked if they had been lit by the same illuminant as your monitor, in order to make all colors in the scene match the change of illuminant. White balance, on the other hand, aims only at ensuring that whites and grays are really neutral (R = G = B) and doesn’t really care about the rest of the color range. White balance is therefore only a partial chromatic adaptation.

Chromatic adaptation is controlled within the Chromatic Adaptation Transformation (CAT) tab of the _color calibration_ module. When used in this way the _white balance_ module is still required as it needs to perform a basic white balance operation (connected to the input color profile values). This technical white balancing ("camera reference" mode) is a flat setting that makes grays lit by a standard D65 illuminant look achromatic, and makes the demosaicing process more accurate, but does not perform any perceptual adaptation according to the scene. The actual chromatic adaptation is then performed by the _color calibration_ module, on top of those corrections performed by the _white balance_ and _input color profile_ modules. The use of custom matrices in the _input color profile_ module is therefore discouraged. Additionally, the RGB coefficients in the _white balance_ module need to be accurate in order for this module to work in a predictable way.

The _color calibration_ and _white balance_ modules can be automatically applied to perform chromatic adaptation for new edits by setting the chromatic adaptation workflow option ([preferences > processing > auto-apply chromatic adaptation defaults](../../../preferences-settings/processing.md)) to "modern". If you prefer to perform all white balancing within the _white balance_ module, a "legacy" option is also provided. Neither option precludes the use of other modules (such as [_color balance_](./color-balance.md)) for creative color grading further along the pixel pipeline.

By default, _color calibration_ performs chromatic adaptation by:

- Reading the RAW file's Exif data to fetch the scene white balance set by the camera,
- Adjusting this setting using the camera reference white balance _D65 (daylight)_ preset from the _white balance_ module,
- Further adjusting this setting with the input color profile in use (standard matrix only).

{{< warning >}}
The default settings initialization, for _color calibration_ chromatic adaptation, __does not work__ if you used an automatic preset changing the RGB coefficients in _white balance_ module. It only reads the defaults settings of _white balance_, which are _D65 (daylight)_. You will need to manually reset the _color calibration_ module (using the reset button) after the first pipeline run so it can read your custom coefficients if you use some.
{{< /warning >}}

For consistency, the _color calibration_ module's default settings always assume that the standard matrix is used in the _input color profile_ module -- any non-standard settings in this module are ignored. However, _color calibration_'s defaults can read any auto-applied preset in the _white balance_ module.

It is also worth noting that, unlike the _white balance_ module, _color calibration_ can be used with [masks](../masking-and-blending/masks/_index.md). This means that you can selectively correct different parts of the image to account for differing light sources.

To achieve this, create an instance of the _color calibration_ module to perform global adjustments using a mask to exclude those parts of the image that you wish to handle differently. Then create a second instance of the module reusing the mask from the first instance (inverted) using a [raster mask](../masking-and-blending/masks/raster.md).

### CAT tab workflow

The default illuminant and color space used by the chromatic adaptation are initialised from the Exif metadata of the RAW file. There are four options available in the CAT tab to set these parameters manually:

- Use the color picker (to the right of the color patch) to select a neutral color from the image or, if one is unavailable, select the entire image. In this case, the algorithm finds the average color within the chosen area and sets that color as the illuminant. This method relies on the "gray-world" assumption, which predicts that the average color of a natural scene will be neutral. This method will not work for artificial scenes, for example those with painted surfaces.

- Select "_(AI) detect from edges_", which uses a machine-learning technique to detect the illuminant using the entire image. This algorithm finds the average gradient color over the edges in the image and sets that color as the illuminant. This method relies on the "gray-edge" assumption, which may fail if large chromatic aberrations are present. As with any edge-detection method, it is sensitive to noise and poorly suited to high-ISO images, but it is very well suited for artificial scenes where no neutral colors are available.

- Select "_(AI) detect from surfaces_", which combines the two previous methods, also using the entire image. This algorithm finds the average color within the image, giving greater weight to areas where sharp details are found and colors are strongly correlated. This makes it more immune to noise than the _edge_ variant and more immune to legitimate non-neutral surfaces than the naïve average, but sharp colored textures (like green grass) are likely to make it fail.

- Select "_as shot in camera_" to restore the camera defaults and re-read the RAW Exif.

The color patch shows the color of the currently calculated illuminant projected into sRGB space. The aim of the chromatic adaptation algorithm is to turn this color into pure white, which does not necessarily means shifting the image toward its *perceptual* opponent color. If the illuminant is properly set, the image will be given the same tint as shown in the color patch when the module is disabled.

To the left of the color patch is the _CCT_ (correlated color temperature) approximation. This is the closest temperature, in kelvin, to the illuminant currently in use. In most image processing software it is customary to set the white balance using a combination of temperature and tint. However, when the illuminant is far from daylight, the CCT becomes inaccurate and irrelevant, and the CIE (International Commission on Illumination) discourages its use in such conditions. The CCT reading informs you of the closest CCT match found:

- When the CCT is followed by "_(daylight)_", this means that the current illuminant is close to an ideal daylight spectrum ± 0.5 %, and the CCT figure is therefore meaningful. In this case, you are advised to use the "_D (daylight)_" illuminant.
- When the CCT is followed by "_(black  body)_", this means that the current illuminant is close to an ideal black body (Planckian) spectrum ± 0.5 %, and the CCT figure is therfore meaningful. In this case, you are advised to use the "_Planckian (black body)_" illuminant.
- When the CCT is followed by "_(invalid)_", this means that the CCT figure is meaningless and wrong, because we are too far from either a daylight or a black body light spectrum. In this case, you are advised to use the _custom_ illuminant. The chromatic adaptation will still perform as expected (see the note below), so the "_(invalid)_" tag only means that the current illuminant color is not accurately tied to the displayed CCT. This tag is nothing to be concerned about -- it is merely there to tell you to stay away from the daylight and planckian illuminants because they will not behave as you might expect.

When one of the above illuminant detection methods is used, the module checks where the calculated illuminant sits using the two idealized spectra (daylight and black body) and chooses the most accurate spectrum model to use in its _illuminant_ parameter. The user-interface will change accordingly:

- A temperature slider will be provided if the detected illuminant is close to a _D (daylight)_ or _Planckian (black body)_ spectrum, for which the CCT is meaningful.
- Hue and chroma sliders in CIE 1976 Luv space are offered for the _custom_ illuminant, which allows direct selection of the illuminant color in a perceptual framework without any intermediate assumption.

---

**Note:** Internally, the illuminant is represented by its absolute chromaticity coordinates in CIE xyY color space. The illuminant selection options in the module are merely interfaces to set up this chromaticity from real-world relationships and are intended to make this process faster. It does not matter to the algorithm if the CCT is tagged "invalid" -- this just means that the relationship between the CCT and the corresponding xyY coordinates is not physically accurate. Regardless, the color set for the illuminant, as displayed in the patch, will always be honored by the algorithm.

---

When switching from one illuminant to another, the module attempts to translate the previous settings to the new illumninant as accurately as possible. Switching from any illuminant to _custom_ preserves your settings entirely, since the _custom_ illuminant is a general case. Switching between other modes, or from _custom_ to any other mode, will not precisely preserve your settings from the previous mode due to rounding errors.

Other hard-coded _illuminants_ are available (see below). Their values come from standard CIE illuminants and are absolute. You can use them directly if you know exactly what kind of light bulb was used to illuminate the scene and if you trust your camera's input profile and reference (D65) coefficients to be accurate. Otherwise, see [_caveats_](#caveats) below.

### CAT tab controls

adaptation
: The working color space in which the module will perform its chromatic adaptation transform and channel mixing. The following options are provided:

: - _Linear Bradford (1985)_: This is accurate for illuminants close to daylight and is compatible with the ICC v4 standard, but produces out-of-gamut colors for more difficult illuminants.
: - _CAT16 (2016)_: This is the default option and is more robust in avoiding imaginary colors while working with large gamut or saturated cyan and purple. It is more accurate than the Bradford CAT in most cases.
: - _Non-linear Bradford (1985)_: This can sometimes produce better results than the linear version but is unreliable.
: - _XYZ_: This is the least accurate method and is generally not recommended except for testing and debugging purposes.
: - _none (disable)_: Disable any adaptation and use the pipeline working RGB space.

illuminant
: The type of illuminant assumed to have lit the scene. Choose from the following:

: - _same as pipeline (D50)_: Do not perform chromatic adaptation in this module instance but just perform channel mixing, using the selected _adaptation_ color space.
: - _CIE standard illuminant_: Choose from one of the CIE standard illuminants (daylight, incandescent, fluorescent, equi-energy, or black body), or a non-standard "LED light" illuminant. These values are all pre-computed -- as long as your camera sensor is properly profiled, you can just use them as-is. For illuminants that lie near the Planckian locus, an additional "temperature" control is also provided (see below).
: - _custom_: If a neutral gray patch is available in the image, the color of the illuminant can be selected using the color picker, or can be manually specified using hue and saturation sliders (in LCh perceptual color space). The color swatch next to the color picker shows the color of the calculated illuminant used in the CAT compensation. The color picker can also be used to restrict the area used for AI detection (below).
: - _(AI) detect from image surfaces_: This algorithm obtains the average color of image patches that have a high covariance between chroma channels in YUV space and a high intra-channel variance. In other words, it looks for parts of the image that appear as though they should be gray, and discards flat colored surfaces that may be legitimately non-gray. It also discards chroma noise as well as chromatic aberrations.
: - _(AI) detect from image edges_: Unlike the _white balance_ module's auto-white-balancing which relies on the "gray world" assumption, this method auto-detects a suitable illuminant using the "gray edge" assumption, by calculating the Minkowski p-norm (p = 8) of the laplacian and trying to minimize it. That is to say, it assumes that edges should have the same gradient over all channels (gray edges). It is more sensitive to noise than the previous surface-based detection method.
: - _as shot in camera_: Calculate the illuminant based on the white balance settings provided by the camera.

temperature
: Adjust the color temperature of the illuminant. Move the slider to the right to assume a more blue illuminant, which will make the white-balanced image appear warmer/more red. Move the slider to the left to assume a more red illuminant, which makes the image appear cooler/more blue after compensation.

: This control is only provided for illuminants that lie near the Planckian locus and provides fine-adjustment along that locus. For other illuminants the concept of "color temperature" doesn't make sense, so no temperature slider is provided.

hue
: For custom white balance, set the _hue_ of the illuminant color in LCh color space (derived from CIE Luv space).

chroma
: For custom white balance, set the _chroma_ (or saturation) of the illuminant color in LCh color space (derived from CIE Luv space).

gamut compression
:  Most camera sensors are slightly sensitive to invisible UV wavelengths, which are recorded on the blue channel and produce "imaginary" colors. Once corrected by the input color profile, these colors will end up out of gamut (that is, it may no longer be possible to represent certain colors as a valid [R,G,B] triplet with positive values in the working color space) and produce visual artifacts in gradients. The chromatic adaptation may also push other valid colors out of gamut, at the same time pushing any already out-of-gamut colors even further out of gamut.
: _Gamut compression_ uses a perceptual, non-destructive, method to attempt to compress the chroma while preserving the luminance as-is and the hue as close as possible, in order to fit the whole image into the gamut of the pipeline working color space. One example where this feature is very useful is for scenes containing blue LED lights, which are often quite problematic and can result in ugly gamut clipping in the final image.

clip negative RGB from gamut
: Remove any negative RGB values (set them to zero). This helps to deal with bad black level as well as the blue channel clipping issues that may occur with blue LED lights. This option is destructive for color (it may change the hue) but ensures a valid RGB output no matter what. It should never be disabled unless you want to take care of the gamut mapping manually and understand what you are doing. In that case, use the _black level correction_ in the _exposure_ module to get rid of any negative RGB (RGB means light, which is energy, and which should always be a positive quantity), then increase the _gamut compression_ until no solid black patches remain in the image. Proper denoising may help getting rid of odd RGB values too. Note that this approach may still be insufficient to recover some deep and luminous shades of blue.

---

**Note 1**: It has been reported that some OpenCL drivers don't play well when negative RGB values are present in the pixel pipeline, because many pixel operators use logarithms and power functions (_filmic_, _color balance_, all the CIE Lab <-> CIE XYZ color space conversions), which are not defined for negative numbers. Although the inputs are sanitized before sensitive operations, it is not enough for some OpenCL drivers, which will output isolated `NaN` (Not a Number) values. These `NaN` values may be subsequently spread by local filters (blurring and sharpening operations, like _sharpness_, _local contrast_, _contrast equalizer_, _low pass_, _high pass_, _surface blur_, and _filmic_ highlights reconstruction), resulting in large black, gray or white squares.

In all these cases, you **must** enable the "clip negative RGB from gamut" option in the _color calibration_ module.

**Note 2**: A common case for failure of the color algorithms in _color calibration_ (especially the gamut compression) is pixels that have a luminance value of 0 (Y channel of the CIE 1931 XYZ space), but non-zero chromaticity values (X and Z channels of the CIE 1931 XYZ space). This case is a numerical oddity that matches no physical reality (a pixel with no luminance should have no chromaticity either), will produce a division by zero in xyY and Yuv color spaces, and will create `NaN` RGB values as a result. This issue is **not** corrected inside _color calibration_ because it is a symptom of a bad input profiling and/or a bad black point level, and needs to be addressed manually either by adjusting the input color profile with the channel mixer or in the _exposure_ module's _black level correction_.

---

## Channel mixing

The remainder of this module is a standard channel mixer, allowing you to adjust the output R, G, B, colorfulness, brightness and gray of the module based on the relative strengths of the R, G and B input channels.

The _Mixer_ tab now offers several modes, but these are only different GUI representations of the same backend reality. Internally, the processing still uses the same mixer coefficients in the selected _adaptation_ space. Switching between _Complete_, _Simple_ and _Primaries_ does not select a different algorithm and does not change the backend by itself. It only changes how the same underlying matrix is exposed in the GUI.

Channel mixing is performed in the color space defined by the _adaptation_ control on the [CAT tab](#cat-tab-controls). For all practical purposes, these CAT spaces are particular RGB spaces tied to human physiology and proportional to the light emissions in the scene, but they still behave in the same way as any other RGB space. The use of any of the CAT spaces can make the channel mixer tuning process easier, due to their connection with human physiology, but it is also possible to mix channels in the RGB working space of the pipeline by setting the _adaptation_ to "none (bypass)". To perform channel mixing in one of the _adaptation_ color spaces without chromatic adaptation, set the _illuminant_ to "same as pipeline (D50)".

---

**Note**: The actual colors of the CAT or RGB primaries used for the channel mixing, projected to sRGB display space, are painted in the background of the RGB sliders, so you can get a sense of the color shift that will result from your altered settings.

---

Channel mixing is a process that defines a boosting/muting factor for each channel as a ratio of all the original channels. Instead of entering a single flat correction that ties the output value of a channel to its input value (for example, \(R\_{\mathrm{out}} = R\_{\mathrm{in}} \times \mathrm{correction}\)), the correction to each channel is dependent on the input of _all_ of the channels for each pixel (for example, \(R\_{\mathrm{out}} = R\_{\mathrm{in}} \times R\_{\mathrm{correction}} + G\_{\mathrm{in}} \times G\_{\mathrm{correction}} + B\_{\mathrm{in}} \times B\_{\mathrm{correction}}\)). Thus a pixel's channels contribute to each other (a process known as "cross-talk") which is equivalent to rotating the primary colors of the color space in 3D. This is, in effect, digital simulation of physical color filters.

Although rotating primary colors in 3D is ultimately equivalent to applying a general hue rotation, the connection between the RGB corrections and the resulting perceptual hue rotation is not directly predictable, which makes the process non-intuitive. "R", "G" and "B" should be taken as a mixture of 3 lights that we dial up and down, not as a set of colors or hues. Also, since RGB tristimulus does not decouple luminance and chrominance, but is an additive lighting setup, the "G" channel is more strongly tied to human luminance perception than the "R" and "B" ones. All pixels have a non-zero G channel, which implies that any correction to the G channel is likely to affect all pixels.

The channel mixing process is therefore tied to a physical interpretation of the RGB tristimulus (as additive lights), which makes it well-suited for primary color grading and illuminant corrections, and blends the color changes smoothly. However, trying to understand and predict it from a perceptual point of view (luminance, hue and saturation) is going to fail and is discouraged.

---

**Note**: The "R", "G" and "B" labels on the channels of the color spaces in this module are merely conventions formed out of habit. These channels do not necessarily look "red", "green" and "blue", and users are advised against trying to make sense out of them based on their names. This is a general principle that applies to _any_ RGB space used in any application.

---

### Mixer modes

All mixer modes describe the same backend 3×3 transform for the RGB output channels. They differ only by the parameterization used in the GUI and by the mathematical constraints required for that parameterization to exist.

#### Simple mode

_Simple_ is an exact geometric representation of a restricted subset of mixer matrices. It has 6 degrees of freedom:

global hue rotation
: A rigid rotation of the normalized chroma plane. This is a robust and global hue change.

chroma (u,v) axes orientation
: The orientation of the principal axes used to express the chroma transform. This parameter lets you align your UV space on the color primaries you want. It has no effect on the image by itself, it only builds the UV space for the next settings.

u stretch and v stretch
: The two signed gains along those principal chroma axes. The neutral value is 1. At 0, it will turn the image achromatic, no matter the axes orientation. Above 1, it will increase the color contrast in the selected axis. Below 0, it will invert colors along the selected axis. Below 1.5, it will increase the color contrast on inverted colors.

achromatic coupling amount and achromatic coupling hue
: The magnitude and direction of the chroma vector that is coupled into the achromatic axis, expressed in a fixed chroma basis. When increasing _amount_, the selected hue will be bleached to achromatic (both desaturated and brightened), while its opponent color is saturated and darkened, but in a way that preserves the achromatic axis. All other colors will be more or less saturated/desaturated, depending on their hue angular distance with the _coupling hue_.

This representation is only valid when the 3 output rows are normalized and their sums are non-zero. In practice, _Simple_ is available only if the 3 _normalize channels_ checkboxes of the RGB mixer rows are enabled and the row sums are not zero. If these requirements are not met, Ansel cannot express the current matrix in this coordinate system and the GUI falls back to _Complete_.

_(U,V) stretches_ can be understood as color contrast along the axis.

A typical use for this would be correcting blue stage lights when the escape gamut and turn magenta : the right angle should be around 0° to put the damaging blue shade on the V axis. Then reduce the _v stretch_ until that blue is not so overwhelming, and adjust _u stretch_ to taste to get a proper color balance.

The _achromatic coupling_ allows to gracefully remap a selected hue to achromatic, while reinforcing the colorfulness of its complementary color. In the case of the stage blue stage lights, after the previous _(u, v) stretch_, you would choose the right blue tint and increase the _coupling amount_ until blue becomes acceptably desaturated.

#### Primaries mode

_Primaries_ is another exact GUI representation, this time expressed as a generalized primaries model in the current mixer basis. It has 9 degrees of freedom:

- 2 for the achromatic axis: _white hue_ and _white purity_,
- 2 for each basis vector: _red_, _green_ and _blue hue/purity_ pairs, for a total of 6,
- 1 _gain_ correction, that accounts for global normalization. 

This mode is exact only for matrices that can be interpreted as a non-degenerate affine basis change in the current adaptation space. In practice, the current 3×3 matrix must be non-singular and the 3 basis vectors plus the white vector must all have non-zero sums. If these requirements are not met, Ansel cannot rebuild the current matrix as primaries parameters and the GUI falls back to _Complete_.

### Complete mode

At its most basic level, you can think of the _Complete_ mode of the _color calibration_ module as a type of matrix multiplication between a 3x3 matrix and the input RGB vector. This is in fact very similar to what a matrix-based ICC color profile does, except that the user can input the matrix coefficients via the Ansel GUI rather than reading the coefficients from an ICC profile file.

$$
\begin{pmatrix}
R_{\mathrm{out}} \\\
G_{\mathrm{out}} \\\
B_{\mathrm{out}}
\end{pmatrix} =
\begin{pmatrix}
R_r & R_g & R_b \\\
G_r & G_g & G_b \\\
B_r & B_g & B_b
\end{pmatrix}
\begin{pmatrix}
R_{\mathrm{in}} \\\
G_{\mathrm{in}} \\\
B_{\mathrm{in}}
\end{pmatrix}
$$

This mode therefore has 9 degrees of freedom: every output row has 3 coefficients mixing the 3 input channels. It is the most general representation and is always available. Whenever another mode cannot represent the current matrix exactly, the GUI falls back to _Complete_.

If, for example, you've been provided with a matrix to transform from one color space to another, you can enter the matrix coefficients into the _channel mixer_ as follows:

- In the _Mixer_ tab, select _Complete_ mode
- In the _output red_ section, set the Rr, Rg and Rb values using the R, G and B input sliders
- In the _output green_ section, set the Gr, Gg and Gb values using the R, G and B input sliders
- In the _output blue_ section, set the Br, Bg and Bb values using the R, G and B input sliders

By default, the mixing function in _color calibration_ just copies the input RGB channels straight over to the matching output channels. This is equivalent to multiplying by the identity matrix:

$$
\begin{pmatrix}
R_{\mathrm{out}} \\\
G_{\mathrm{out}} \\\
B_{\mathrm{out}}
\end{pmatrix} =
\begin{pmatrix}
1 & 0 & 0 \\\
0 & 1 & 0 \\\
0 & 0 & 1
\end{pmatrix}
\begin{pmatrix}
R_{\mathrm{in}} \\\
G_{\mathrm{in}} \\\
B_{\mathrm{in}}
\end{pmatrix}
$$

For a more intuitive understanding of how the mixing sliders in the _output red_, _output green_ and _output blue_ sections behave, consider the following:

- For the _R_ destination channel, adjusting sliders to the right will make the R, G or B areas of the image more "red". Moving the slider to the left will make those areas more "cyan".
- For the _G_ destination channel, adjusting sliders to the right will make the R, G or B areas of the image more "green". Moving the slider to the left will make those areas more "magenta".
- For the _B_ destination channel, adjusting sliders to the right will make the R, G or B areas of the image more "blue". Moving the slider to the left will make those areas more "yellow".

### Complete mode controls

The following controls are shown for each of the _output red_, _output green_ and _output blue_ sections:

input R/G/B
: Choose how much the input R, G and B channels influence the output channel relating to the tab concerned.

normalize channels
: Select this checkbox to normalize the coefficients to try to preserve the overall brightness of this channel in the final image as compared to the input image.

## Outputs tab

The _Outputs_ tab groups the previous _colorfulness_, _brightness_ and _B&W_ controls into a single page with section labels. These controls are independent from the _Mixer_ tab modes above, but they follow the same overall idea: they use the relative strengths of the R, G and B input channels to build output quantities.

The brightness and colorfulness (color saturation) of pixels in an image can also be adjusted based on the R, G and B input channels. This uses the same basic algorithm that the [_filmic_](filmic.md) module uses for tone mapping (which preserves RGB ratios) and for mid-tones saturation (which massages them).

saturation algorithm
: This control allows you to upgrade the saturation algorithm to the new 2021 version, for edits produced prior to darktable 3.6 -- it will not appear for edits that already use the latest version.

### Colorfulness section controls

input R/G/B
: Adjust the color saturation of pixels, based on the R, G and B channels of those pixels. For example, adjusting the _input R_ slider will affect the color saturation of pixels containing a lot of "R" more than pixels containing only a small amount of "R".

normalize channels
: Select this checkbox to try to keep the overall saturation constant between the input and output images.

### Brightness section controls

input R/G/B
: Adjust the brightness of certain colors in the image, based on the R, G and B channels of those colors. For example, adjusting the _input R_ slider will affect the brightness of colors containing a lot of R channel much more than colors containing only a small amount of R channel. When darkening/brightening a pixel, the ratio of the R, G and B channels for that pixel is maintained, in order to preserve the hue.

normalize channels
: Select this checkbox to try to keep the overall brightness constant between the input and output images.

### B&W section

Another very useful application of _color calibration_ is the ability to mix the channels together to produce a grayscale output -- a monochrome image. In the _Outputs_ tab, use the _B&W_ section and set the R, G and B sliders to control how much each channel contributes to the brightness of the output. This is equivalent to the following matrix multiplication:

$$
\mathrm{GRAY}\_{\mathrm{out}} =
\begin{pmatrix}
r & g & b
\end{pmatrix}
\begin{pmatrix}
R\_{\mathrm{in}} \\\
G\_{\mathrm{in}} \\\
B\_{\mathrm{in}}
\end{pmatrix}
$$

When dealing with skin tones, the relative weights of the three channels will affect the level of detail in the image. Placing more weight on R (e.g. [0.9, 0.3, -0.3]) will make for smooth skin tones, whereas emphasising G (e.g. [0.4, 0.75, -0.15]) will bring out more detail. In both cases the B channel is reduced to avoid emphasising unwanted skin texture.

### B&W section controls

input R/G/B
: Choose how much each of the R, G and B channels contribute to the gray level of the output. The image will only be converted to monochrome if the three sliders add up to some non-zero value. Adding more B will tend to bring out more details, adding more R will tend to smooth skin tones.

normalize channels
: Select this checkbox to try to keep the overall brightness constant as the sliders are adjusted.

## Spot color mapping

The spot mapping feature is designed to help with batch-editing a series of images in an efficient way. In this scenario, you typically develop a single reference image for the whole batch and then copy&paste the development stack to all of the other images in the batch.

Unfortunately, the light color temperature often changes slightly between shots, even within the same series captured in the same conditions. This can be the result of a cloud passing by the sun in natural light, or a different ratio between colored bounce light and main light. Each image will still need some individual fine-tuning if you want a perfectly even look over the whole series, and this can be both time-consuming and frustrating.

Spot color mapping allows you to define a target chromaticity (hue and chroma) for a particular region of the image (the control sample), which you then match against the same target chromaticity in other images. The control sample can either be a critical part of your subject that needs to have constant color, or a non-moving and consistently-lit surface over your series of images.

The mapping process consists of two steps.

### Step 1: set the target

There are two ways of setting the target chromaticity for your control sample:

1. If you know or expect an arbitrary color for the control sample (for example, a gray card, a color chart, a product or a logo of a specified color), you can set its L, h and c values directly, in Lch derived from CIE Lab 1976 space,
2. If you simply want to match the development of your reference image, set the _spot mode_ to _measure_, then enable the color picker (to the right of the color patch) and draw a rectangle over your control sample. The _input_ column will then be updated with the L, h, c values of the control sample before the color correction, and the _target_ column will show the resulting L, h, c values of the control sample after the current calibration setting is applied.

If you reset the L, h, c values, the default value is a neutral color at 50% lightness (middle-gray) -- this can be useful to quickly set the average white balance of any image. If you want to match the control sample against neutral gray, you only need to reset the chroma slider because the lightness and hue settings have no effect on chromaticity for neutral grays.

Note that the target value is not reset when you reset the module itself, but is stored indefinitely in Ansel's configuration and will be available on next launch as well as for the next image you develop.

The _take channel mixing into account_ option lets you choose where the target is sampled. If disabled, the target color is measured immediately after the _CAT_ (Chromatic Adaptation Transform) step, which takes place before any channel mixing. This means that if you have a calibrated profile in effect within the channel mixer, this profile will be discarded. If enabled, the target color is measured after the _CAT_ and the channel mixing steps, including any calibrated profile. This is the recommended option for most use cases.

---

**Note**: If you are defining your target from a gray patch, you should know that the gray patch on color checkers is never entirely neutral. For example, Datacolor Spyder has a slightly warm gray (hue = 20°, chroma = 1.2) while X-Rite pre-2014 has a colder but more neutral gray (hue = 240°, chroma = 0.3) and X-Rite post-2014 is almost perfectly neutral (hue = 133°, chroma = 0.2). In general, it is not desirable to match the control sample against a perfectly neutral gray target, and it is actually wrong to do so when using gray cards and color checkers as a control sample.

---

### Step 2 : match the target

When you open a new image, the _spot mode_ is automatically reset to _correction_. Using the color picker attached to the color patch, you can then directly reselect your control sample in the new image. The proper illuminant settings required for the control sample to match the memorized target chromaticity will be automatically computed, and the setting will be updated in the same operation.

The _take channel mixing into account_ option will need to be set the same as when the measurement of the target was performed to ensure consistent results. Note that the target matching only defines the illuminant settings used in the Chromatic Adaptation Transform  -- it does not alter the channel mixer settings, since the calibration is handled in the color checker calibration tool. However, the channel mixer settings can be used or discarded in the computation of the illuminant settings, depending on this option.

This operation can be repeated as many times as you have images in your series with no further work.

---

**Note:** Perfectly matching your control sample against the target chromaticity may still not yield a similar perceptual result, even if the numbers are exactly the same. The ratio of lightness between the control sample and its surrounding, as well as the color contrasts at play in the frame, will alter the perception of colors in ways that are very difficult to model. To build an intuition of this problem, see the [gray strawberries illusion](https://www.sciencealert.com/in-spite-of-what-your-eyes-tell-you-these-strawberries-aren-t-red).

---

## Extracting settings using a color checker

Since the channel mixer is essentially an RGB matrix (similar to the [_input color profile_](./input-color-profile.md) used for RAW images) it can be used to improve the color accuracy of the input color profile by computing ad-hoc color calibration settings.

These computed settings aim to minimize the color difference between the scene reference and the camera recording in a given lighting situation. This is equivalent to creating a generic ICC color profile but here, the profile is instead stored as module settings that can be saved as presets or styles, to be shared and reused between images. Such profiles are meant to complement and refine the generic input profile but do not replace it.

This feature can assist with:

* Handling difficult illuminants, such as low [CRI](https://en.wikipedia.org/wiki/Color_rendering_index) light bulbs, for which a mere white balancing will never suffice,
* Digitizing artworks or commercial products where an accurate rendition of the original colors is required,
* Neutralizing a number of different cameras to the same ground-truth, in multi-camera photo sessions, in order to obtain a consistent base look and share the color editing settings with a consistent final look,
* Obtaining a sane color pipeline from the start, nailing white balance and removing any bounced-light color cast at once, with minimal effort and time.

### Supported color checker targets

Users are not currently permitted to use custom targets, but a limited number of verified color checkers (from reputable manufacturers) are supported:

- X-Rite / Gretag MacBeth Color Checker 24 (pre- and post-2014),
- Datacolor SpyderCheckr 24 (pre- and post-2018),
- Datacolor SpyderCheckr 48 (pre- and post-2018).

Users are discouraged from obtaining cheap, off-brand, color targets as color constancy between batches cannot possibly be asserted at such prices. Inaccurate color checkers will only defeat the purpose of color calibration and possibly make things worse.

IT7 and IT8 charts are not supported since they are hardly portable and not practical for use on-location for ad-hoc profiles. These charts are better suited for creating generic color profiles, undertaken using a standard illuminant, for example with [Argyll CMS](https://encrypted.pcode.nl/blog/index.html%3Fp=594.html).

---

**Note**: X-Rite changed the formula of their pigments in 2014 and Datacolor in 2018, which slightly altered the color of the patches. Both formulas are supported in Ansel, but you should be careful to choose the correct reference for your target. If in doubt, try both and choose the one that yields the lowest average delta E after calibration.

---

### Prerequisites

In order to use this feature you will need to take a test shot of a supported color checker chart, on-location, under appropriate lighting conditions:

* Frame the chart in the center 50% of the camera's field, to ensure that the image is free of vignetting,
* Ensure that the main light source is far enough from the chart to give an even lighting field over the surface of the chart,
* Adjust the angle between the light, chart and lens to prevent reflections and gloss on the color patches,
* For the best quality profile you should capture an image with the appropriate brightness. To achieve this, take a few bracketed images (between -1 and +1 EV) of your color checker and load them into Ansel, ensuring that all modules between _color calibration_ and _output color profile_ are disabled. Choose the image where the white patch has a brightness L of 94-96% in CIE Lab space or a luminance Y of 83-88% in CIE XYZ space (use the global color picker). This step is not strictly necessary -- alternatively you can take a single image and apply the exposure compensation as recommended in the profile report.

If the lighting conditions are close to a standard D50 to D65 illuminant (direct natural light, no colored bounced light), the color checker shot can be used to produce a generic profile that will be suitable for any daylight illuminant with only a slight adjustment of the white balance.

If the lighting conditions are peculiar and far from standard illuminants, the color checker shot will be only usable as an ad-hoc profile for pictures taken in the same lighting conditions.


### Usage

The settings used in color calibration depend on the chosen CAT space and on any color settings defined earlier in the pipe within the _white balance_ and _input color profile_ modules. As such, the results of the profiling (e.g. the RGB channel mixing coefficients) are valid only for a rigid set of _CAT space_, _white balance_ and _input color profile_ settings. If you wish to create a generic style with your profile, don't forget that you will need to include the settings from these modules as well.

Use the following process to create your profile preset/style:

1. Enable the [_lens correction_](./lens-correction.md) module to correct any vignetting that might mislead the calibration,
2. On the bottom of the _color calibration_ module, click on the arrow near the _calibrate with a color checker_ label, to show the controls,
3. Pick the correct model and manufacturer of your color checker from the _chart_ drop-down,
4. In the image preview, an overlay of the chart's patches will appear. Drag the corners of the chart so that they match the visual references (dots or crosses) around the target, to compensate for any perspective distortion,
5. Click the _refresh_ button to compute the profile,
6. Check the _Profile quality report_. If it is "good", you can click on the _validation_ button. If not, try changing the optimization strategy and refresh the profile again.
7. Save the profile in a preset or style, or simply copy & paste the module settings to all of the pictures taken under the same lighting conditions, from within the lighttable view or filmstrip.

---

**Note:** You don't _need_ to use the standard matrix in the _input color profile_ module when performing a calibration, but be aware that the "as shot in camera" default white balance will not work properly with any other profile, and that you will need to always use the same input profile whenever you reuse such calibration settings.

---

### Reading the profile report

The profile report helps you to assess the quality of the calibration. The settings in color calibration are only a "best fit" optimization and will never be 100% accurate for the whole color spectrum. We therefore need to track "how inaccurate" it is in order to know whether we can trust this profile or not.

Bad profiles can happen and will do more harm than good if used.

#### Delta E and the quality report

The [CIE delta E 2000](https://en.wikipedia.org/wiki/Color_difference#CIEDE2000) (ΔE) is used as a perceptual metric of the error between the reference color of the patches and the color obtained after each step of calibration:

- ΔE = 0 means that there is no error -- the obtained color is exactly the reference color. Unfortunately, this will never happen in practice.
- ΔE = 2.3 is defined as the Just Noticeable Difference (JND).
- ΔE < 2.3 means that the average observer will not be able to tell the difference between the expected reference color and the obtained color. This is a satisfactory result.
- ΔE > 2.3 means that the color difference between the expected reference and the obtained color is noticeable for the average observer. This is unsatisfactory but sometimes unavoidable.

The quality report tracks the average and maximum ΔE at the input of the module (before anything is done), after the chromatic adaptation step (white balance only), and at the output of the module (white balance and channel mixing). At each step, the ΔE should be lower than at the previous step, if everything goes as planned.

#### Profile data

The data generated by the profiling process comprises the RGB 3×3 matrix and the detected illuminant. These are expressed in the CAT _adaptation_ space defined in the _CAT_ tab and are provided in case you want to export these coefficients to other software. If the detected illuminant is _daylight_ or _black body_, the matrix should be fairly generic and reusable for other _daylight_ and _black body_ illuminants with the addition of a small white balance adjustment.

#### Normalization values

These are the settings that you should define, as-is, for the _exposure_ and _black level correction_ parameters in the [_exposure_](./exposure.md) module, in order to obtain the lowest possible error in your profile. This step is optional and is useful only when the utmost precision is required, but beware that it can produce negative RGB values that will be clipped at various points in the pipeline.

#### Overlay

![color checker](color-checker.jpg)

The chart overlay displays a disc in the center of each color patch, which represents the expected reference value of that patch, projected into the display RGB space. This helps you to visually assess the difference between the reference and the actual color without having to bother with ΔE values. This visual clue will be reliable only if you set the _exposure_ module as instructed in the _normalization values_ of the profile report.

Once the profile has been calibrated, some of the square patches will be crossed in the background by one or two diagonals:

* Patches that are not crossed have ΔE < 2.3 (JND), meaning they are accurate enough that the average observer will be unable to notice the deviation,
* Patches crossed with one diagonal have 2.3 < ΔE < 4.6, meaning that they are mildly inaccurate,
* Patches crossed with two diagonals have ΔE > 4.6 (2 × JND), meaning that they are highly inaccurate.

This visual feedback will help you to set up the optimization trade-off to check which colors are more or less accurate.

### Enhancing the profile

Because any calibration is merely a "best fit" optimization (using a weighted least-squares method) it is impossible to have all patches within our ΔE < 2.3 tolerance. Some compromise will therefore be required.

The _optimize for_ parameter allows you to define an optimization strategy that attempts to increase the profile accuracy in some colors at the expense of others. The following options are available:

- _none_: Don't use an explicit strategy but rely on the implicit stategy defined by the color checker manufacturer. For example, if the color checker has mostly low-saturation patches, the profile will be more accurate for less-saturated colors.
- _neutral colors_: Give priority to grays and less-saturated colors. This is useful for desperate cases involving cheap fluorescent and LED lightings, having low CRI. However, it may increase the error in highly-saturated colors more than not having any profile.
- _saturated colors_: Give priority to primary colors and highly-saturated colors. This is useful in product and commercial photography, to get brand colors right.
- _skin and soil colors_, _foliage colors_, _sky and water colors_: Give priority to the chosen hue range. This is useful if the subject of your pictures is clearly defined and has a typical color.
- _average delta E_: Attempt to make the color error uniform across the color range and minimize the average perceptual error. This is useful for generic profiles.
- _maximum delta E_: Attempt to minimize outliers and large errors, at the expense of the average error. This can be useful to get highly saturated blues back into line.

No matter what you do, strategies that favor a low average ΔE will usually have a higher maximum ΔE, and vice versa. Also, blues are always the more challenging color range to get correct, so the calibration usually falls back to protecting blues at the expense of everything else, or everything else at the expense of blues.

The ease of obtaining a proper calibration depends on the quality of the scene illuminant (daylight and high CRI illuminants should always be preferred), the quality of the primary _input color profile_, the _black point compensation_ set in the _exposure_ module, but first and foremost on the mathematical properties of the camera sensor's filter array.

### Profile checking

It is possible to use the _color space check_ button (first on the left, at the bottom of the module) to perform a single ΔE computation of the color checker reference against the output of the _color calibration_ module. This can be used in the following ways:

1. To check the accuracy of a profile calculated in particular conditions against a color checker shot in different conditions.
2. To evaluate the performance of any color correction performed earlier in the pipe, by setting the _color calibration_ parameters to values that effectively disable it (CAT _adaptation_ to _none_, everything else set to default), and just use the average ΔE as a performance metric.

## Caveats

The ability to use standard CIE illuminants and CCT-based interfaces to define the illuminant color depends on sound default values for the standard matrix in the _input color profile_ module as well as reasonable RGB coefficients in the _white balance_ module.

Some cameras, most notably those from Olympus and Sony, have unexpected white balance coefficients that will always make the detected CCT invalid even for legitimate daylight scene illuminants. This error most likely comes from issues with the standard input matrix, which is taken from the Adobe DNG Converter.

It is possible to alleviate this issue, if you have a computer screen calibrated for a D65 illuminant, using the following process:

1. Display a white surface on your screen, for example by opening a blank canvas in any photo editing software you like
2. Take a blurry (out of focus) picture of that surface with your camera, ensuring that you don't have any "parasite" light in the frame, you have no clipping, and are using an aperture between f/5.6 and f/8,
3. Open the picture in Ansel and extract the white balance by using the spot tool in the _white balance_ module on the center area of the image (non-central regions might be subject to chromatic aberrations). This will generate a set of 3 RGB coefficients.
4. Save a preset for the _white balance_ module with these coefficients and auto-apply it to any color RAW image created by the same camera.
---

# Color look up table : https://ansel.photos/en/doc/views/darkroom/modules/color-look-up-table/

A generic color look up table implemented in Lab space.

The input to this module is a list of source and target points and the complete mapping is interpolated using splines. The resulting look up tables (LUTs) are editable by hand and can be created using the darktable-chart utility to match given input (such as hald-cluts and RAW/JPEG with in-camera processing pairs).

## Module controls

color board
: The color board grid shows a list of colored patches. The colors of the patches are the source points. The target color of the selected patch is shown as offsets which are controlled by sliders beneath the color board. An outline is drawn around patches that have been altered (where the source and target colors differ).

: Click a patch to select it, or use the combo box or color picker. The currently-selected patch is marked with a white square, and its number is displayed in the combo box below.

: By default, the module will load the 24 patches of a classic color checker and initialise the mapping to identity (no change to the image). Configurations with more than 24 patches are shown in a 7x7 grid.

interaction
: To modify the color mapping, you can change the source and target colors, though the main use is to change the target colors.

: Start with an appropriate palette of source colors (either from the presets menu or from a style you have downloaded). You can then change the lightness (L), green-red (a), blue-yellow (b), or saturation \(c\) of the patches' target values with the sliders.

: To change the source color of a patch you can select a new color from your image by using the color picker and Shift+click on the patch you want to replace.

: Double-click a patch to reset it; Right-click a patch to delete it; Shift+click on empty space to add a new patch (with the currently picked color as the source).
---

# Color reconstruction : https://ansel.photos/en/doc/views/darkroom/modules/color-reconstruction/

Recover color information in blown-out highlights.

Due to the nature of digital sensors, overexposed highlights lack valid color information. Most frequently they appear neutral white or exhibit some color cast, depending on what other image processing steps are involved. This module can be used to “heal” overexposed highlights by replacing their colors with better fitting ones. The module acts on pixels whose luminance exceeds a user-defined threshold. Replacement colors are taken from neighboring pixels. Both the spatial distance and the luminance distance (range) are taken into account for color selection.

Due to a limitation of the underlying algorithm, reconstructed colors may sometimes be displayed incorrectly if you zoom into the image in the darkroom view. If this happens you might observe a magenta shift in highlight areas close to high contrast edges, or you might see colorless highlight areas when used alongside the “reconstruct color” method of the [_highlight reconstruction_](./highlight-reconstruction.md) module. These artifacts only influence on-screen image display -- the final output remains unaffected. It is recommended that you finetune the parameters of this module only when viewing the fully zoomed-out image.

Note that similar functionality is also available in the _reconstruct_ tab of the [_filmic_](./filmic.md#reconstruct) module.

## Module controls

threshold
: The color reconstruction module replaces the color of all target pixels having luminance values above this threshold. Only pixels with luminance values below the threshold are taken as valid source pixels for replacement colors. Setting this parameter too high will cause the module to have no effect on any pixels. Setting it too low will minimize the “pool” of replacement colors -- if no suitable colors are available, the original colors are retained. This parameter therefore exhibits a “sweet spot” characteristic with an optimum setting depending on the individual image.

spatial extent
: The spatial distance that source pixels may have from target pixels in order for them to contribute to color replacement. Higher values cause ever more distant pixels to contribute. This increases the chance of finding a replacement color but can make the replacement color less well suited for the reconstruction.

range extent
: The range difference (in luminance) that source pixels may have from target pixels in order for them to contribute to color replacement. Higher values cause more pixels to contribute even if their luminance differs more strongly from the target pixels. This again increases the chance of finding a replacement color but at the same time increases the risk of unfitting colors creeping in.

precedence
: This combobox defines whether certain replacement colors shall take precedence over others as follows:

: - _off_ (default): All pixels contribute equally
: - _saturated colors_: Pixels contribute according to their chromaticity -- the more highly-saturated a color, the more it contributes
: - _hue_: Give precedence to a specific hue (see below)

hue
: This slider is only visible if you set the _precedence_ to “hue”. It allows you to select a preferred hue for replacement colors. This only has an effect if the preferred hue is actually present within the selected spatial and range distance of the target pixels (see above). A typical use case is repairing highlights on human skin in situations where diverging colors are in close proximity (e.g. textiles or hair with a luminance close that of to skin). Setting a hue preference on skin tones prevents these other colors from creeping in.
---

# Color zones : https://ansel.photos/en/doc/views/darkroom/modules/color-zones/

Selectively adjust the lightness, saturation and hue of pixels based on their current lightness, saturation and hue.

This module works in CIE LCh color space, which separates pixels into _lightness_, chroma (_saturation_) and _hue_ components. It allows you to manipulate the lightness, saturation and hue of targeted groups of pixels through the use of curves.

You first need to choose whether you wish to adjust (select) pixels based on their lightness, saturation or hue. You can then use three curves, on their respective tabs, to adjust the lightness, saturation and hue of ranges of pixels selected via this method.

---

_**Note:** This module should be used with care, as transitions between colors may not be graceful. Instead, use the [color balance rgb](./color-balance.md) module with a [parametric mask](../masking-and-blending/masks/parametric.md)._

---

## Pixel selection method

The _color zones_ module offers three different methods for selecting which pixels you want to adjust. They are:

select by hue (default)
: Select pixels to manipulate based on their hue. For example, you may want to darken a blue sky, or to change a red porche into a yellow one. The following image shows the full range of hues that you can choose to operate on:

: ![color zones choose hue](color-zones-choose-hue.jpg)

select by lightness
: Select pixels to manipulate based on their lightness. For example, you may want to make your shadows brighter, or to make your highlights more yellow. The following image shows the range of lightness levels that you can choose to operate on, from dark to light:

: ![color zones choose lightness](color-zones-choose-lightness.jpg)

select by saturation
: Select pixels to manipulate based on their saturation. For example, you may want to tone down the saturation of some already highly saturated pixels, or to change their hue. The following image shows the range of saturation levels that you can choose to operate on, from a completely unsaturated monochrome gray through to the most highly saturated color:

: ![color zones choose saturation](color-zones-choose-saturation.jpg)

## Pixel manipulation curves

Once you have chosen a pixel selection method, the selected range of lightness, saturation or hue levels will appear along the horizontal axis of the three pixel manipulation curves, which can be viewed and adjusted by choosing the appropriate tab (lightness, saturation, hue).

If, for example, you were to choose to _select by hue_ (the default), the horizontal axis (below the manipulation curves) would show the range of hues you can work with, and the three pixel manipulation curves would appears as follows:

lightness
: By adjusting the lightness curve up or down in a given (hue) location, you can brighten or darken pixels matching hues where the curve has been raised or lowered, respectively. In the example below the blue sky in an image has been darkened for dramatic effect:

: ![color zones adjust lightness](color-zones-adjust-lightness.jpg)

saturation
: By adjusting the saturation curve up or down in a given (hue) location, you can desaturate (make less colorful) or resaturate (make more colorful) pixels matching hues where the curve has been raised or lowered, respectively. In the example below, a red object in the background has been desaturated so it is less of a distraction to the main subject of the photo:

: ![color zones adjust saturation](color-zones-adjust-saturation.jpg)

hue
: By adjusting the hue curve up or down in a given (hue) location, you can shift the hue of pixels matching hues where the curve has been raised or lowered, allowing you to replace one color with another. In the example below, a pink toy in an image has been changed to blue:

: ![color zones adjust hue](color-zones-adjust-hue.jpg)

The curves work similarly in the lightness- and saturation-based selection modes as well.

Note that these examples are somewhat contrived in order to illustrate the module's usage. In practical use, they would likely need to be combined with [drawn](..//masking-and-blending/masks/drawn.md) and/or [parametric](../masking-and-blending/masks/parametric.md) masks to further isolate their effect.

## Range selection

When adjusting the pixel manipulation curves, it can sometimes be difficult to judge exactly where on the horizontal axis pixels will fall. To the right of the tab controls are a pair of color pickers that can be used to assist with this.

If you click the left-hand color picker and choose a pixel in the image, you will see a dark vertical line showing where that pixel falls on the horizontal axis. If you Ctrl+click or right-click on the same color picker you can choose a rectangular area from the image -- the range of values represented within the selected rectangle will be shaded vertically, with a similar dark line showing the median value.

If you click on the right-hand color picker, you can similarly choose a rectangular area on the image and the display will be shown as described above (a shaded area with a dark vertical line). However, in this case the color picker will also automatically add some control points to the curve for you, representing the highlighted range (see below). Simply drag on the center node to raise or lower the curve within the selected range. Alternatively, hold Ctrl while selecting a range to automatically create a positive curve (push up the selected range) or hold Shift while selecting to create a negative curve (pushed down).

![color zones overview](color-zones-overview.jpg)

## Module controls

The following controls are available in the _color zones_ module:

lightness, saturation & hue tabs
: Each tab displays a pixel manipulation curve to allow you to alter “lightness”, “saturation”, or “hue” based on the pixel selection method.

edit by area
: Choose how to interact with the curve. This setting is disabled by default, allowing the control points for the curve to be freely placed. Check the box to fall back to the legacy "edit by area" mode, which functions in a similar way to the spline curve controls used in wavelet modules.

 ![mask-icon](icon-mask.jpg) mask display
: Enable the _mask display_ to highlight pixels that have been affected by _color zones_ adjustments in yellow.

select by
: Define the horizontal axis (the range of values to work on). You can choose between “lightness”, “saturation”, and “hue” (the default). Changing this parameter resets all pixel manipulation curves to their default state (horizontal straight lines). If you want to work on multiple ranges, you need to create additional instances of the module.

process mode
: Choose between a “smooth” (default) or “strong” processing mode. The default mode is less likely to cause artifacts.

mix
: Use this parameter to tune the strength of the overall effect.

interpolation method
: Define how the curve is interpolated using the user-defined control points.
---

# Contrast equalizer : https://ansel.photos/en/doc/views/darkroom/modules/contrast-equalizer/

Adjust luminance and chroma contrast in the wavelet domain.

This versatile module can be used to achieve a variety of effects, including bloom, denoise, clarity, and local contrast enhancement.

It works in the wavelets domain and its parameters can be tuned independently for each wavelet detail scale. The module operates in CIE LCh color space and so is able to treat luminosity and chromaticity independently.

A number of presets are provided, which should help you to understand the capabilities of the module.

## Module controls

The _contrast equalizer_ module decomposes the image into various detail scales. On each detail scale, you can independently adjust the contrast and denoise splines for lightness ("luma") and chromaticity (“chroma”, or color saturation), as well as adjusting the edge-awareness (“edges”) of the wavelet transform. The luma, chroma and edges splines are provided on separate tabs, and some examples of their usage are given in the following sections.

Below the spline graphs is a _mix_ slider, which can be used to adjust the strength of the effect, and even to invert the graph (with negative values). While your mouse is over the spline graph, the curve will be displayed as if the _mix_ slider was set to 1.0, to allow for easier editing. When you move your mouse away, the graph will be re-adjusted to account for the _mix_ slider.

In the background of the curve you can see a number of alternating light and dark stripes. These represent the levels of detail that are visible at your current zoom scale -- any details without these stripes are too small to be seen your current view. Adjustments made to control points within the striped section _may_ produce a visible effect (depending on the strength of the adjustment). Adjustments outside of the striped region _will not_. Zoom in to see higher levels of detail and make adjustments to the more detailed areas of the image.

---

**Tip:** if you are having trouble visualising which parts of the curve will affect which details in the image, you can set the [blend mode](../masking-and-blending/blend-modes.md) to "difference". This will make the image go black except for those areas where the output of the module differs from the input. By raising the curve at one of the control points, you will be able to see which details in the image are represented by that point.

---

### Luma tab

The luma tab allows you to adjust the local contrast in the image's luminance (brightness). Adjustments are represented by a white spline that begins as a horizontal line running across the centre of the graph (indicating that no change will be made). Raise or lower this spline at the left end of the graph to increase or decrease the local contrast of coarse detail in the image. Perform similar adjustments towards the right side of the graph to adjust the local contrast of the fine details in the image.

When you hover the mouse pointer over the graph, a white circle indicates the radius of influence of the mouse pointer -- the size of this circle can be adjusted by scrolling with the mouse wheel. The larger the circle of influence, the more control points will be affected when you adjust the curve. A highlighted region in the background shows what the spline would look like if you pushed the currently-hovered control point all the way to the top or bottom on the graph -- see the screenshot below for examples of these features.

The following image shows the default state of the contrast equalizer module before any adjustments have been made:

![contrast-equalizer-luma-orig](contrast-equalizer-luma-orig.jpg)

Raising the two control points at the right-hand end of the graph will increase the sharpness of the fine details (the eye and feathers of the bird) while leaving coarser details (the rocks in the background) largely unaffected. The example below has been exaggerated to better illustrate the effect.

![contrast-equalizer-luma-high](contrast-equalizer-luma-high.jpg)

Increasing the local contrast can also amplify the luma noise in the image. A second spline located at the bottom of the graph can be used to denoise the selected detail scales. Raise this spline (by clicking just above one of the triangles at the bottom of the graph and dragging the line upwards) to reduce noise at the given wavelet scale. In the example above, the dark denoising spline has been raised at the fine-detail end of the graph.

### Chroma tab

The chroma tab allows the color contrast or saturation to be adjusted at the selected wavelet scales. See the following example:

![contrast-equalizer-chroma-orig](contrast-equalizer-chroma-orig.jpg)

Say you wanted to bring out the green color of the anthers at the end of the stamen. The pink petals of the flowers are already quite saturated, but using contrast equalizer you can selectively boost the saturation on the small scale of the anthers without impacting the saturation of the petals. By raising the third control point from the right, you can target the saturation of the anthers only:

![contrast-equalizer-chroma-high](contrast-equalizer-chroma-high.jpg)

As in the luma tab, the chroma tab also has a denoising spline at the bottom of the graph. This can be used to handle chroma noise at different scales within the image. Chroma denoising can generally be more aggressive on larger wavelet scales and has less effect on a smaller scale.

### Edge tab

The basic wavelet _à trous_ transform has been enhanced in the _contrast equalizer_ to be "edge-aware", which can help to reduce the gradient reversals and halo artifacts that the basic algorithm can produce. The _edges_ tab does not directly act on the edges in an image; rather it adjusts the edge awareness feature of the wavelet transform. If you have not adjusted the luma or chroma splines, adjusting the edge awareness spline will have no effect.

To see the sorts of artifacts that the _edges_ curve tries to combat, here is an example taken from the original paper "Edge-Optimized À-Trous Wavelets for Local Contrast Enhancement with Robust Denoising" (Hanika, Damertz and Lensch 2011):

![contrast-equalizer-edge](contrast-equalizer-edge.jpg)

In the image on the left, the edges spline has been reduced to a minimum, effectively disabling the edge awareness and resulting in halos. In the middle image, the edges spline has been increased too much, resulting in gradient reversals. In the image on the right the edges spline has been set somewhere in between the two extremes, resulting in nice clean edges.

Usually the default central position of the spline is a good starting point, but if there are objectionable artifacts around the edges, this control can be helpful in mitigating them. Some experimentation will be required.
---

# Crop : https://ansel.photos/en/doc/views/darkroom/modules/crop/

Crop an image using on-screen guides.

This module appears late in the pipeline, meaning that the full image can remain available for source spots in the [_retouch_](./retouch.md) module. For best results, you are advised to use the [_rotate and perspective_](./rotate-perspective.md) module to perform rotation and perspective correction (if required), and then perform final creative cropping with this module.

Whenever this module is in focus, the full uncropped image will be shown, overlaid with crop handles and optional guiding lines.

![screen controls](screen-controls.jpg)

Resize the crop by dragging the border and corner handles.

Move the crop rectangle by clicking and dragging inside the crop area. Constrain movement to the horizontal/vertical axis by holding Ctrl/Shift, respectively while dragging. Commit changes by giving focus to another module.

## Module controls

The _crop_ module controls are split into two sections as follows:

### Crop settings

aspect
: Set the aspect ratio of the crop, constraining the width:height ratio of the crop rectangle to the chosen aspect. Many common numerical ratios are pre-defined. A few special aspect ratios deserve explanation:
: - _freehand_: Crop without any restrictions
: - _original image_: Retain the aspect ratio of the original image
: - _square_: Constrains the aspect ratio to be 1:1
: - _golden cut_: The golden ratio (1.62:1)

: You can also enter any other ratio after opening the combobox by typing it in the form of “x:y” or as a decimal (e.g. "0.5" to apply a ratio of 2:1).

: The button beside the aspect combobox allows you to switch between portrait and landscape orientation if you have selected a rectangular aspect ratio.

: If you want to add an aspect ratio to the pre-defined drop-down list you can do this by including a line of the form "`plugins/darkroom/clipping/extra_aspect_ratios/foo=x:y`" in Ansel's configuration file `$HOME/.config/Ansel/Anselrc`. Here “`foo`” defines the name of the new aspect ratio and “`x`” and “`y`” the corresponding numerical values (`x` and `y` must be integers). Note that you can only add new entries for ratios not already present in the drop-down list.

---

**Note:** When resizing an image in freehand mode you may retain the currently-set aspect ratio by holding Shift while dragging on any of the resize controls.

---

### Margins

These sliders allow you to directly set how much of the image to crop from each side. They are automatically updated if you move or resize the crop area on the image using the mouse.

As this section is rarely used, it is collapsed by default.

left
: The percentage of the image that should be cropped from the left side.

right
: The percentage of the image that should be cropped from the right side.

top
: The percentage of the image that should be cropped from the top.

bottom
: The percentage of the image that should be cropped from the bottom.

### Guides

Tick the box to show guide overlays whenever the module is activated. Click the icon on the right to control the properties of the guides. See [guides & overlays](../../toolboxes/guides-overlays.md) for details.
---

# Curve : https://ansel.photos/en/doc/views/darkroom/modules/curve/

A classic digital photography tool to alter an image's tones using curves.

Activate the color picker on the left to show the picked values in the graph (Ctrl+click or right-click to use the picker in area mode). Numerical (Lab) values of the input and output (see below) at the selected spot or area are shown at the top left of the widget.

A second color picker to the right can be used to automtaically create new nodes based on the sampled area. Ctrl+click+drag to alter the created nodes to have a positive curve for the selected area; Shift+click+drag to create a negative curve.

## Module controls

mode
: RGB is a linear color space designed to capture and display images in additive synthesis. It is related to the capture and display media and does not isolate color and lightness information in the same way that Lab and XYZ color spaces do. This module works in ProPhoto RGB. Adding contrast in RGB space is known to desaturate highlights and boost saturation in the shadows, but this has been proven to be the most reliable way to edit contrast, and is the standard method in most software

: Depending on the desired intent you can apply the RGB curve in two different modes

: - _RGB, linked channels_: Apply the L-channel curve to all three channels in the RGB color space.
: - _RGB, independent channels_: R, G and B curves can be adjusted independently.

compensate middle gray
: Select this to change the histogram display in the module. This option does not alter the processing but may assist when editing the curve.
---

# Demosaic : https://ansel.photos/en/doc/views/darkroom/modules/demosaic/

Control how raw files are demosaiced.

## Bayer filters

The sensor cells of a digital camera are not color-sensitive -- they are only able to record different levels of lightness. In order to obtain a color image, each cell is covered by a color filter (red, green or blue) that primarily passes light of that color. This means that each pixel of the raw image only contains information about a single color channel.

Color filters are commonly arranged in a mosaic pattern known as a Bayer filter array. A demosaic algorithm reconstructs the missing color channels by interpolation with data from the neighboring pixels. For further reading see the Wikipedia articles on [demosaicing](https://en.wikipedia.org/wiki/Demosaicing) and the [Bayer filter](http://en.wikipedia.org/wiki/Bayer_filter).

Ansel offers several demosaic algorithms, each with it's own characteristics. The differences between them are often very subtle and might only be visible while pixel-peeping. However, as the program works on a pixel-by-pixel basis and demosaic generates the base data for the other modules, the choice of the algorithm can have a visually significant effect on the quality of very fine details in the image. This can include the appearance of false maze patterns as well as the rendering quality of colored edges.

Demosaic interpolation algorithms are often prone to produce artifacts, typically visible as [Moiré patterns](https://en.wikipedia.org/wiki/Moire_pattern) when zooming into the image. The chosen algorithm might handle _pre-existing_ Moiré- or Maze-like patterns in the raw data in a better or worse way. In these circumstances _VNG4_ and _LMMSE_ are often more stable.

The following demosaic algorithms are available for sensors with Bayer filters:

- _PPG_ used to be Ansel's default demosaic algorithm. It is fast, but other algorithms generally yield better results.

- _AMaZE_ and _RCD_ offer better reconstruction of high-frequency content (finer details, edges, stars) but might struggle with color reconstruction overshoots or added noise in areas of low contrast. While _AMaZE_ often retains more high-frequency details it is also more prone to color overshoots than _RCD_. Since _RCD_ now offers similar performance to _PPG_, but with better results, it is now the default algorithm.

- _LMMSE_ is better suited for use on high ISO and noisy images than _AMaZE_ or _RCD_, both of which tend to generate overshooting artefacts when applied to such images. It can also be useful to manage images that exhibit Moiré patterns with other algorithms.

- _VNG4_ is better suited for use on images with low-frequency content (e.g. low contrast regions such as sky) but, compared to _AMaZE_ and _RCD_, it often causes loss of some high-frequency details and can sometimes add local color shifts. VNG is no longer really recommended -- for most images, other available algorithms usually provide better results.

- _downscale_ is a last-resort alternative that will actually not demosaic, but interpolate by nearest-neighbour and reduce the resolution by 4 (2 on each side). The rationale behind is that many sensors now have 36, 45, 52… Mpx while we still consume images mostly in Full HD (1920×1080 px = 2 Mpx).[^1] For very noisy pictures, the demosaicing step will actually worsen the noise, by creating patterns, blotches and shifting colors toward green or magenta. Downscaling those very-high-resolution images, when noisy, prevents aggravating the problem and also makes denoising less computationally-intensive, while still producing at least 9 usable Mpx at the end. The _downscale_ option has an iterative color filtering step that uses the guided Laplacian (same as [_highlights reconstruction_](./highlight-reconstruction.md)) to limit moiré, fringes and noise altogether in a multi-scale setup. As a side effect, downscaling without post-filtering is the only method that fully preserves the sensor noise statistics, which is useful when using [noise profiles](./denoise-profiled.md).

[^1]: Even a 8.5×11 inch print at 300 DPI is still only 8.7 Mpx…

---

**Note:** The performance of the demosaic algorithms differs significantly, _AMaZE_ being by far the slowest.

---

## Sensors without bayer filters

There are a few cameras whose sensors do not use a Bayer filter. Cameras with an "X-Trans" sensor have their own set of demosaic algorithms. The default algorithm for X-Trans sensors is _Markesteijn 1-pass_, which produces fairly good results. For slightly better quality (at the cost of much slower processing), choose _Markesteijn 3-pass_. Though _VNG_ is faster than _Markesteijn 1-pass_ on some computers, it is more prone to artifacts.

## Special algorithms

_passthrough (monochrome)_ is only useful for cameras that have had the color filter array physically removed from the sensor (e.g. scratched off). Demosaic algorithms usually reconstruct missing color channels by interpolation with data from the neighboring pixels. However, if the color filter array is not present, there is nothing to interpolate, so this algorithm simply sets all the color channels to the same value, resulting in a monochrome image. This method avoids the interpolation artifacts that the standard demosaic algorithms might introduce.

_photosite_color_ is not meant to be used for image processing. It takes the raw photosite data and presents it as red, blue or green pixels. This is designed for debugging purposes in order to see the raw data and can assist with analysis of errors produced by the other demosaic algorithms.

## Dual demosaic algorithms

Some images have areas best demosaiced using an algorithm that preserves high frequency information (like _AMaZE_ or _RCD_) and other areas that might profit from an algorithm more suited to low frequency content (like _VNG4_).

In dual demosaic algorithms (e.g. _RCD + VNG4_) the sensor data is demosaiced twice, first by _RCD_, _AMaZE_ or _Markesteijn 3-pass_ and then by _VNG4_. Both sets of demosaiced data are retained for subsequent processing.

The data from the high frequency algorithm is then analysed for _local data change_ and, using a threshold (there is a bit more of maths involved here), the output image is written pixel-by-pixel for each color channel using data from each demosaic algorithm weighed by the local data change.

In general, areas with greater detail are demosaiced by the algorithm best suited to that purpose (_RCD, AMaZe, Markesteijn 3-pass_) and any flat areas (like blue sky) are demosaiced using the second algorithm (_VNG4_).

_The 'local data change' is technically implemented as a gaussian-blurred single channel selection mask calculated from a combination of the threshold value and the pixels' luminance._

### Selecting the threshold

An automatically-calculated threshold is difficult to implement. Instead, the "display blending mask" button can be used to display the _selection mask_ so you can control the selection of the algorithm manually. The brighter the pixel in the displayed mask, the more the output is taken from the high-frequency algorithm.

## Module controls

method
: The demosaic algorithm to use (see above).

edge threshold (_PPG_ only)
: The threshold for an additional median pass. Defaults to “0” which disables median filtering.

lmmse refine (_LMMSE_ only)
: Refinement steps for use with the LMMSE demosaic algorithm. Median steps average the output. Refinement steps add some recalculation of red and blue channels. While the refinement options work well for luma noise, they may decrease quality on images with heavy chroma noise.

color smoothing
: Activate a number of additional color smoothing passes. Defaults to “off”.

match greens
: In some cameras the green filters have slightly varying properties. This parameter adds an additional equalization step to suppress artifacts. Available options are “disabled”, “local average”, “full average” and “full and local average”. This option is not shown for X-Trans sensors.

switch dual threshold _(dual demosaic modes only)_
: Set the contrast threshold for dual demosaic modes. Lower values favor the high frequency demosaic algorithm and higher values favor the low frequency algorithm.

display blending mask _(dual demosaic modes only)_
: Show the blending mask that is used to differentiate between high and low frequency areas (adjusted by the "switch dual threshold" parameter). For each pixel, the brighter the mask, the more the module's output is taken from the high frequency demosaic algorithm.
---

# Denoise (profiled) : https://ansel.photos/en/doc/views/darkroom/modules/denoise-profiled/

An easy to use and highly efficient denoise module, adapted to the individual noise profiles of a wide range of camera sensors.

One issue with a lot of denoising algorithms is that they assume that the variance of the noise is independent of the luminosity of the signal. By profiling the noise characteristics of a camera's sensor at different ISO settings, the variance at different luminosities can be assessed, and the denoising algorithm can be adjusted to more evenly smooth out the noise.

Currently, Ansel has sensor noise profiles for over 300 popular camera models from all the major manufacturers. If you generate your own noise profile for a camera that is not yet supported by Ansel, be sure to share it with the Ansel development team so they can include it in the next release! Please see Ansel's [camera support](https://github.com/darktable-org/darktable/wiki/Camera-support) page for more information.

## Modes

The _denoise (profiled)_ module implements two algorithms, each of which is available in either an easy-to-use "auto" mode, or a more advanced manual mode with additional controls:

non-local means
: This algorithm works in the spatial domain in much the same way as the [_astrophoto denoise_](./astrophoto-denoise.md) module. It averages each pixel with some surrounding pixels in the image. The weight of such a pixel in the averaging process depends on the similarity of its neighborhood with the neighborhood of the pixel being denoised. A patch with a defined size is used to measure that similarity.

: Note that this algorithm is quite resource-intensive.

wavelets (default)
: This algorithm works in the wavelet domain, and provides a simplified user interface. Wavelet decomposition allows you to adjust the denoise strength depending on the coarseness of the noise in the image. This mode can be used in either _Y0U0V0 color mode_ (which allows you to independently control luminance and chroma noise) or _RGB color mode_ (which allows you to independently control noise for each RGB channel).

: The wavelet algorithm is less resource-intensive than _non-local means_.

### Luma versus chroma noise

Both “non-local means” and “wavelet” algorithms can efficiently tackle luma (lightness) noise and chroma (color) noise.

In the past, it was suggested that you use two separate instances of this module to tackle _chroma_ and _luma_ noise independently (using _chroma_ and _lightness_ blending modes). This is no longer recommended, since the _denoise (profiled)_ module is placed before the _input color profile_ module in the pixelpipe (so that the profile parameters are accurate) and color blending modes should only be used after the input color profile has been applied.

The new algorithms in this module now provide their own methods to separately handle luma and chroma noise, and in both cases this can be handled with a single module instance.

## Module controls

The _denoise (profiled)_ module provides some controls that are independent of the algorithm used. These are described first, before moving on to the algorithm-specific controls.

When describing the controls specific to an algorithm, we will first cover the simplified interface, and then move on to the more advanced controls for that algorithm.

Note that sliders are provided with minimum and maximum values by default. However, these are only soft limits and, where needed, higher values can be entered by right-clicking on the slider and typing a new value.

### Common controls

profile
: Ansel automatically determines the camera model and ISO based on the Exif data of your raw file, and searches for a corresponding profile in its database. If your image has an intermediate ISO value, settings will be interpolated between the two closest datasets in the database, and this interpolated setting will show up as the first line in the combo box. You can also manually override this selection if necessary. Re-selecting the top-most entry in the combo box will return you to the default profile.

mode
: Choose which denoising algorithm to use (see above), and whether to present the simplified ("auto") or full manual interface for that algorithm.

whitebalance-adaptive transform
: As the white balance amplifies each of the RGB channels differently, each channel exhibits different noise levels. This checkbox makes the selected algorithm adapt to the white balance adjustments. This option should be disabled on the second instance if you have applied a first instance with a color blend mode.

adjust autoset parameters (auto modes only)
: Automatically adjust all the other parameters on the current denoising algorithm using a single slider. This is particularly useful when you have had to increase the exposure on an under-exposed image, which normally introduces additional noise (as if you had taken the shot with a higher ISO). This control compensates for that by using settings similar to those used for a higher ISO image. The "effective ISO" used by the denoise algorithm is the actual ISO used, multiplied by the value of this slider.

strength
: Fine-tune the strength of the denoising. The default value has been chosen to maximize the peak signal-to-noise ratio. It's mostly a matter of taste -- whether you prefer a low noise level at the cost of fine details, or you accept more noise to better preserve fine detail.

preserve shadows (advanced mode only)
: Lower this control to denoise the shadows more aggressively. Usually, as noise increases, you will need to decrease this parameter.

bias correction (advanced mode only)
: Correct any color cast that may appear in the shadows. Increase this value if dark shadows appear too green, decrease if they appear too purple.

### Non-local means auto sliders

central pixel weight (details)
: Control the amount of detail that should be preserved by the denoising algorithm. By default, this will have a low value, meaning that the algorithm will treat both luma and chroma noise equally. Move this slider to the right to reduce the amount of luma denoising, so that the algorithm will primarily affect chroma noise. By adjusting this slider together with the _strength_ slider, you can find a good balance between luma and chroma denoising.

### Non-local means advanced sliders

When you take non-local means out of auto mode, the _adjust autoset parameters_ slider is replaced with the following controls. You can use the auto-adjust slider to arrive at some initial settings then, when you switch to manual mode, the sliders will show the equivalent manual settings. You can then continue to fine-tune the manual settings from the auto-set starting point.

patch size
: Control the size of the patches being matched when deciding which pixels to average -- see [_astrophoto denoise_](./astrophoto-denoise.md) for more information. Increase this for images with more noise but be aware that high values may smooth out fine edges. The effect of this slider on processing time is minimal.

search radius
: Control how far away from a pixel the algorithm will attempt to find similar patches. Increasing the value can give better results for very noisy images when coarse grain noise is visible, but the processing time is hugely impacted by this parameter (the processing time increases with the square of this parameter). A lower value will make execution faster, a higher value will make it slower. It most cases it is better to use the scattering parameter, which has a similar effect but without the heavy processing cost.

scattering (coarse-grain noise)
: Like the _search radius_, this slider controls how far from a pixel the algorithm will attempt to find similar patches. However, it does this without increasing the number of patches considered. As such, processing time will stay about the same. Increasing the value will reduce coarse grain noise, but may smooth local contrast. This slider is particularly effective at reducing chroma noise.

### Wavelet curves

Wavelet curves are shown when one of the “wavelet” modes is selected.

The noise in an image usually consists of both fine grain and coarse grain noise, to varying degrees. The wavelet curves allow the strength of the denoising to be adjusted depending on the coarseness of the visible noise. The left end of the curve will act on very coarse grain noise, while the right of the curve will act on very fine grain noise. Raising the curve will increase the amount of smoothing, while lowering the curve will decrease it.

For example, you can preserve very-fine-grain noise by pulling the right-most part of the curve down. When tackling chroma noise (e.g. on a U0V0 curve, or on a second module instance with a color blend mode) you can safely raise the right side of the curve, since colors do not change a lot on fine scales. This can be useful if you see some noisy isolated pixels with the wrong color.

### Wavelets _Y0U0V0_ color mode

The preferred way to use wavelets is with the _Y0U0V0_ color mode. This mode separates the denoising curves into luminance (_Y0_) and color (_U0V0_) components. You can then use the _Y0_ curve to control the level of luma denoising, and the _U0V0_ curve to control the level of chroma denoising.

![denoise-y0u0v0](denoise-y0u0v0.jpg)

### Wavelets _RGB_ color mode

Before the _Y0U0V0_ color mode was introduced, wavelet-based denoising could only be performed directly on the _R_, _G_ and _B_ channels, either together or individually.

![denoise-rgb](denoise-rgb.jpg)

If you want to denoise the RGB channels independently, the best way to do this is to use an instance of the [_color calibration_](./color-calibration.md) module placed immediately before the _denoise (profiled)_ module so that it outputs a gray channel based on the _red_ channel only, then denoise that monochrome image using the _red_ wavelet curve. Repeat this procedure for the blue and green channels. This procedure is time-consuming, but gives the best result because looking at the color of a noisy pixel is not a reliable way to determine which channel to adjust. For example, a noisy red pixel could be due to a noise peak on the red channel, but could also be due to a noise lull on the blue and green channels.

The issue with independently denoising the RGB channels is that there can still be some residual chroma noise at the end that requires excessive smoothing to eliminate. This was in fact one of the key motivations behind implementing the _Y0U0V0_ color mode.

### Wavelets advanced sliders

When you take _wavelets_ out of _auto_ mode, the _adjust autoset parameters_ slider is replaced with the _preserve shadows_ and _bias correction_ controls listed above in the [common controls](#common-controls) section.
---

# Diffuse or sharpen : https://ansel.photos/en/doc/views/darkroom/modules/diffuse/

Diffusion is a family of physical processes by which particles move and spread gradually with time, from a source that generates them. In image processing, diffusion mostly occurs in two places:

- Diffusion of photons through lens glass (blur) or humid air (hazing),
- Diffusion of pigments in wet inks or watercolors.

In both cases, diffusion makes the image less sharp by "leaking" particles and smoothing local variations.

The _diffuse or sharpen_ module uses a generalized physical model to describe several kinds of diffusion, and can be used by image makers to either simulate or revert diffusion processes.

As it is a highly technical module, several presets are provided to demonstrate its use for various purposes.

Diffusion can be removed in order to:

- Recover the original image from sensors with an anti-aliasing filter or mitigate the blur created by most demosaicing algorithms (use one of the _sharpen demosaicing_ presets and move the module before the _input color profile_ module in the pipeline),
- Reverse static lens blurring/defocusing (use one of the _lens deblur_ presets),
- Remove atmospheric haze (use the _dehaze_ preset),
- Add extra acutance for better legibility (use the _local contrast_ preset).

Note that motion blurs cannot be reverted by undoing the diffusion process, as they are not diffusive in nature.

Diffusion can be added in order to:

- Create a bloom or Orton effect (use the _bloom_ preset),
- Inpaint missing or damaged parts of an image (use the _inpaint highlights_ preset),
- Denoise in an edge-preserving way (use one of the _denoise_ presets)
- Apply a surface blur (use the _surface blur_ preset).

Since the process is physical, even its glitches may be used for creative purposes. For example, you can:

- Simulate line drawing or watercolor (use the _simulate line drawing_ and _simulate watercolor_ presets),
- Create random patterns and textures by increasing noise (over time, with iterations, noise will connect with neighbours to create random specks).

---

**Note:** This module is highly resource-intensive, as it is actually an anisotropic, multiscale, partial differential equation solver. The module's runtime increases with the number of iterations and OpenCL is therefore strongly recommended. Some "fast" presets are also provided for use on systems without OpenCL.

---

## Concepts

### Time

Diffusion is a time-dependent process: the more time it has, the further the particles can spread. In this module, time is simulated using the number of iterations (the number of times the algorithm runs on top of itself). More iterations can make reconstruction (deblurring, denoising, dehazing) more accurate if properly set, but can also cause it to degenerate.

### Direction

Natural diffusion usually takes place from points with a high potential (high energy or high concentration of particles) to those with a low potential (low energy or low concentration of particles). In an image, this means that diffusion always occurs from the brightest pixels to the darkest.

This particular implementation can simulate natural diffusion, using what is called an isotropic diffusion (all directions have the same weight, like heat diffusion), but can also force a weighted direction parallel to the gradients (forcing diffusion across object edges and creating ghost edges), or a weighted direction perpendicular to the gradients, called isophote (forcing diffusion to be contained inside edges, like in a droplet of watercolor). The relative weight of each direction (gradient and isophote) is user-defined and can be found in the [_direction_](#direction-1) section of the module.

### Speed

Depending how fluid the environment is, particles can move more or less freely and therefore more or less fast. The speed of diffusion can be set in the [_speed_](#speed-sharpen--diffuse) section of the module.

When performing reconstruction (denoising, deblurring, dehazing), it is advisable to use smaller speeds for better accuracy. This prevents numerical overshoots (and therefore degeneration of the solution) and may require more iterations. For small numbers of iterations, higher speeds may be used. Note that large blurs need many iterations for proper reconstruction, so the speed should be adjusted to avoid degenerating the solution.

All speeds are added (first to fourth orders), and the sums "`first order + second order`" and "`third order + fourth order`" should never exceed ±100%, unless you want to produce [glitch art](https://en.wikipedia.org/wiki/Glitch_art).

### Scale

Natural diffusion is supposed to happen only to the closest neighbouring coordinates. That is, at each iteration, each pixel should only interact with its 9 nearest neighours.

Here, we fast-track things a bit to save time and reuse the multi-scale wavelets scheme from the [_contrast equalizer_](./contrast-equalizer.md) module, so that we can diffuse at different scales. The maximal scale of diffusion is defined by the _radius span_ parameter.

Regardless of the diffusion, a _sharpness_ parameter allows you to increase or decrease the details at each scale, much like the spline controls of the _contrast equalizer_. Along with the _edge sensitivity_ slider, this provides the same features as the _contrast equalizer_ module (_luma_ and _edges_ tabs) but in a scene-referred RGB space.

## Module controls

### Properties

iterations
: The number of times the algorithm should run on top of itself. High values slow the module down but allow more accurate reconstructions, provided that the diffusion speeds are low enough.

central radius
: The main scale of the diffusion. Zero causes the diffusion to act more heavily on fine details (used for deblurring and denoising). Non-zero values define the size of details to be heavily diffused (used to increase local contrast).

radius span
: This allows you to select the band of details radii to act on, around the central radius. The span of diffusion defines a range of detail scales (between `center - span` and `center + span`) within which the diffusion is confined. High values diffuse on a large band of radii, at the expense of computation time. Low values diffuse closer around the central radius. If you plan to deblur, the radius span should be approximately the width of your lens blur and the central radius should be zero. If you plan to increase the local contrast, but don't want to affect sharpness or noise, the radius span should be 3/4 of your central radius maximum.

The radii are expressed in pixels of the full-resolution image, so copy+pasting settings between images of different resolution may lead to slightly different results, except for pixel-level sharpness.

For electrical engineers, what is set here is a band-pass filter in wavelets space, using a gaussian frequential window centered on `central radius` with a fall-off (standard deviation) of `radius span`. Wavelet scales are analogous to harmonic frequencies and each wavelet scale defines the radius of the details to act on.

### Speed (sharpen ↔ diffuse)

In the following controls, positive values apply diffusion, negative values undo diffusion (i.e. sharpen) and zero does nothing.

1st order speed (gradient)
: The speed of diffusion of the low-frequency wavelet layers in the direction defined by the _1st order anisotropy_ setting.

2nd order speed (laplacian)
: The speed of diffusion of the low-frequency wavelet layers in the direction defined by the _2nd order anisotropy_ setting.

3rd order speed (gradient of laplacian)
: The speed of diffusion of the high-frequency wavelet layers in the direction defined by the _3rd order anisotropy_ setting.

4th order speed (laplacian of laplacian)
: The speed of diffusion of the high-frequency wavelet layers in the direction defined by the _4th order anisotropy_ setting.

### Direction

In the following controls, positive values cause diffusion to avoid edges (isophotes), negative values make diffusion follow gradients more closely, and zero affects both equally (isotropic).

1st order anisotropy
: The direction of diffusion of the low-frequency wavelet layers relative to the orientation of the gradient of the low-frequency (_1st order speed_ setting).

2nd order anisotropy
: The direction of diffusion of the low-frequency wavelet layers relative to the orientation of the gradient of the high-frequency (_2nd order speed_ setting).

3rd order anisotropy
: The direction of diffusion of the high-frequency wavelet layers relative to the orientation of the gradient of the low-frequency (_3rd order speed_ setting).

4rd order anisotropy
: The direction of diffusion of the high-frequency wavelet layers relative to the orientation of the gradient of the high-frequency (_4th order speed_ setting).

### Edge management

sharpness
: Apply a gain on wavelet details, regardless of properties set above. Zero does nothing, positive values sharpen, negative values blur. This is mostly useful as an adjustment variable when blooming or blurring, to retain some sharpness while adding a glow around edges. You are not advised to use this for sharpening alone, since there is nothing to prevent halos or fringes with this setting.

edge sensitivity
: Apply a penalty over the diffusion speeds when edges are detected. This detection uses the local variance around each pixel. Zero disables the penalty, higher values make the penalty stronger and more sensitive to edges. Increase if you notice edge artifacts like fringes and halos.

edge threshold
: Define a variance threshold, which affects mostly low-variance areas (dark or blurry areas, or flat surfaces). Positive values will increase the penalty for low-variance areas, which is good for sharpening or increasing local contrast without crushing blacks. Negative values will decrease the penalty for low-variance areas, which is good for denoising or blurring with a maximal effect on black and blurry regions.

### Diffusion spatiality

luminance masking threshold
: This control is useful if you want to in-paint highlights. For values greater than 0%, the diffusion will only occur in regions with a luminance greater than this setting. Note that gaussian noise will be added in these regions to simulate particles and initialize the in-painting.

## Workflow

The main difficulty with this module is that while its output can vary dramatically depending on its input paramaters, these parameters have no intuitive link to everyday life. Users are likely to be overwhelmed, unless they are already familiar with Fourier partial differential equations. This section proposes some ways to approach this module without the burden of having to understand the underlying theory.

### General advice

If you intend to deblur your image using this module, always start by properly correctiong any chromatic aberrations and noise in the image, since the deblurring may magnify these artifacts. It is also important that you don't have clipped black pixels in your image. These can be corrected with the _black level correction_ of the [_exposure_](./exposure.md) module.

Since it works on separate RGB channels, it is better to apply this module after [_color calibration_](./color-calibration.md), so that you start with a fully neutral, white-balanced, input image. Note that increasing local contrast or sharpness will also lead to a slight color contrast and saturation boost, which is usually a good thing. Since it uses a variance-based regularization to detect edges, it is also better to put this module before any non-linear operation.

### Starting with presets

The provided presets have been tuned by the developer and tested on a range of images for typical purposes. The easiest way is simply to start from the presets, and then tweak them as needed:

- If the effect seems too strong, decrease the number of iterations,
- If edge artifacts appear, increase the edge sensitivity,
- If deblurring starts to affect valid blurry parts (bokeh), reduce the radius,
- If deblurring seems correct in bright areas but excessive in dark areas, increase the edges _threshold_,
- If deblurring clips black pixels, lower the _black level correction_ in _exposure_ module,
- Fine-tune the _sharpness_ to your taste.

### Starting from scratch

The module's default settings are entirely neutral and will do nothing to your image. The spirit of the module is that each order affects the texture of the image in a particular way.

Start by tuning the first order parameters (speed and anisotropy) to get an initial base. Then adjust the radius. This will affect coarser textures (either blur or sharpen them). Remember that the first order acts on the low frequencies of the wavelet scale and follows a direction parallel or perpendicular to the gradient of the low frequencies.

Next, start to tune the second order parameters (speed and anisotropy). The second order also acts on the low frequencies of the wavelet scale but this time follows a direction parallel or perpendicular to the gradient of the _high_ frequencies, which can either be the direction of maximal sharpness or of noise. This can be used to reduce noise (using the second order in diffusion mode, with positive values) when you used the first order in sharpening mode (with negative values).

These two steps can be performed on the zoomed-out image. Remember that, while great care has been taken to make the algorithm's visual result fairly scale-invariant, the preview will be exact only when zoomed 1:1. In any case, anything happening at pixel level (radius < 2px) will not be visible for zoom levels lower than 50%.

At this point, you may want to tweak the edge sensitivity to take care of any edge artifacts. In theory, diffusing in the isophote direction ensures that diffusion is contained inside edges, but this is not sufficient when corners and sharp convex shapes are present in the image.

When the edge sensitivity control has been adjusted to produce satisfying results, the image usually becomes quite soft. In most cases it will be necessary, at this point, to increase the number of iterations in order to compensate. This will come with a performance penalty so tread carefully with the performance/quality trade-off depending on your hardware. If you can't increase the number of iterations, you will have to increase the diffusing speed.

The final step is to fine-tune the third and fourth order, which take care of the high frequencies of each wavelet scale. You will need to be a lot more gentle with these settings than for the first and second orders, as they can cause noise to blow-up really fast.

The third order follows the gradient or isophote direction of the low frequency layer, so can be used to guide the high frequency diffusion in a direction that is more likely to be legitimate regarding real edges (and less prone to catch noise).

The fourth order follows the gradient or isophote direction of the high frequency layer and is more likely to catch noise. Diffusing on the fourth order is the best way to reduce noise without affecting sharpness too much, either as a stand-alone denoiser, or as a regularization step in a deblurring process.

## Using multiple instances for image reconstruction

Noise post-filtering may benefit from **introducing** a diffusion process -- this can be applied as an extra step after the [_denoise (profiled)_](./denoise-profiled.md) module.

Conversely, the following optical issues may benefit from reconstruction by **undoing** the diffusion process:

1. Blur introduced by a sensor's low-pass filter (LPF) and/or anti-aliasing performed by the [_demosaic_](./demosaic.md) module,
2. Static lens blur,
3. Haze/fog,
4. Light diffusion (using a diffuser that is too large), leading to even lighting and lack of local contrast on the subject.

While more than one of these issues can affect the same picture at the same time, it is better to try to fix them separately using multiple instances of the module. When doing so, ensure the issues are corrected from coarse scale to fine scale, and that denoising always happens first. That is, your instances should appear in the following [pipe order](../pixelpipe/the-pixelpipe-and-module-order.md):

1. Denoise,
2. Local contrast enhancement,
3. Dehaze,
4. Lens blur correction,
5. Sensor and demosaic correction.

Starting with the coarser-scale reconstructions reduces the probability of introducing or increasing noise when performing the finer-scale reconstructions. This is unintuitive because these processes don't happen in this order during the formation of the image. For the same reason, denoising should always happen before any attempt at sharpening or increasing acutance.

## Notes and warnings

While this module is designed to be scale-invariant, its output can only be guaranteed at 100% zoom and high quality or full-size export. Results at lower zoom levels or export dimensions may or may not match your expectations.

When setting a deblurring algorithm, try to bear in mind that many of the greatest images in the history of photography were taken with lenses that were not remotely as sharp as those available today. Although the current trend is to build and sell increasingly sharp lenses and have software apply insane amounts of sharpening on top, this fashion does _not_ lead to better imagery and makes the retouching process more tedious. Soft focus and a bit of blurriness have some poetic merits too, which surgically-sanitized HD images may fail to convey.

It should be noted that global contrast (using simple tone curves or black/white levels) also affects our perception of sharpness, which is quite different from optical sharpness (optical resolution). Human eyes are only sensitive to local contrast, which may come from optical sharpness (e.g absence of diffusion -- thin edges) as well as from amplified tonal transitions. If some global tone mapping is in place to increase the contrast, the image will look sharper. If a tone mapping is used to decrease the contrast, the image will look more blurry. In none of these cases are the actual edges of objects affected in any way, and the perceptual consequences are pure illusion.

Part of the aging process is a loss of eyesight. The amount of sharpening that people over 50 find pleasing may not be the same as for people in their 20s. It is worth considering sharpening to obtain a _plausible_ result (matching your everyday perception) rather than a _pleasing_ result (that may look good only to people with the same eyesight as yours).

Finally, assessing the sharpness of images zoomed to 1:1 (100%) or more is a foolish task. In museums, exhibitions and even on screen, the general audience looks at images as a whole, not with a magnifying glass. Moreover, in most practical uses, photographs rarely exceed a resolution of 3000×2000 pixels (roughly a 300 DPI print at A4/letter dimensions) which, for 24 Mpx sensors, means downscaling by a factor of 4. When examining a 24 Mpx file at 1:1, you are actually looking at an image that will never exist. Sharpening at pixel level, in this context, is a waste of time and CPU cycles.
---

# Dithering : https://ansel.photos/en/doc/views/darkroom/modules/dithering/

This module eliminates some of the banding artifacts that can result when Ansel's internal 32-bit floating point data is transferred into discrete 8-bit or 16-bit integer output format for display or export.

Although not an inherent problem in any of Ansel's modules, some operations may provoke banding if they produce a lightness gradient in the image. To mitigate possible artifacts you should consider activating dithering when using the [_vignetting_](./vignetting.md) or [_graduated density_](./graduated-density.md) modules. This is especially relevant for images with extended homogeneous areas such as cloudless sky. Also watch out for banding artifacts when using a gradient [drawn mask](../masking-and-blending/masks/drawn).

Viewing an image dithered into a very low bit depth from some distance (e.g. “Floyd-Steinberg 1-bit b&w”) will give the impression of a homogeneous grayscale image. Ansel attempts to mimic this impression when rendering zoomed-out images in the center view, the navigation window and thumbnails. This is accomplished by dithering those images into a higher number of grayscale levels. Note that, as a consequence, the [scopes](../../toolboxes/scopes.md) module -- the data for which is derived from the navigation window -- will show this increased number of levels and is therefore not a full match to the output image.

## Module controls

method
: Choose the dithering method to use.

: _Floyd-Steinberg_: Systematically distribute quantization errors over neighboring pixels. This method can be selected with some typical output bit depths. Alternatively, you can select _Floyd-Steinberg auto_, which automatically adapts to the desired output format.

: _random dithering_: This method just adds some level of randomness to break sharp tonal value bands.

damping ("random" method only)
: Controls the level of added random noise expressed as a damping factor in a 10*log 2 basis. A value of -80 is a good fit for 8-bit output formats; -160 for 16-bit output.
---

# Duplicate manager : https://ansel.photos/en/doc/views/toolboxes/duplicate-manager/

View and switch between the versions (duplicates) of the current image. Every version shares the same underlying file but keeps its own independent editing history, stored in its own XMP sidecar, so each can be edited without affecting the others.

In the darkroom [left panel](../darkroom/darkroom-view-layout.md#left-panel), the duplicate manager lists each version of the current image with a preview thumbnail and a version number:

- **Press and hold** a thumbnail to preview that version in the center view.
- **Double-click** a thumbnail to switch to that version and edit it.

## Creating versions

New versions are created from the [global menu](../global-menu.md), so they also work on a lighttable selection:

- _Edit → Create new history_ (<kbd>Ctrl</kbd>+<kbd>N</kbd>) makes a **virgin** version, with an empty history.
- _Edit → Duplicate existing history_ (<kbd>Ctrl</kbd>+<kbd>D</kbd>) makes an **exact duplicate** of the current edit.

A version's description is stored in the _version name_ metadata field, which you can edit from the [metadata editor](metadata-editor.md) in the lighttable.
---

# EXIF and IPTC : https://ansel.photos/en/doc/views/toolboxes/image-information/

Display the EXIF and IPTC information (metadata) embedded in an image, plus a number of extra fields Ansel maintains (filmroll, version, local copy, etc.). The module is available in most views, in the left panel.

As you **hover** thumbnails in the lighttable or filmstrip, the panel updates to show the image under the cursor. This is a read-only preview and changes nothing.

When several images are selected and no single image is highlighted, the module shows only the fields that are identical across the selection; differing fields display _\<various values\>_ instead.

In the lighttable, **double-click** the _filmroll_ field to jump to a collection of all images in that film roll.

## Choosing the displayed fields

Open _preferences…_ from the module's presets menu to get the list of all available fields. The **visible** checkbox toggles each field on or off, and you can drag a row to reorder the display. These choices can be saved as module presets. The module's reset button restores all fields, in their default order.
---

# Export : https://ansel.photos/en/doc/views/toolboxes/export/

Export the selected images to final files.

Open the export panel from the [global menu](../global-menu.md), _File → Export…_ (<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd>). Files can be exported to a file on disk, email, various online storage locations, a web album, or a book template.

## Module controls

### Storage options

target storage
: The type of location to store your selected images. A number of different back-ends are implemented, including file on disk, LaTeX book template and various web albums. Depending on the selected target, you will be asked to provide additional information, such as filenames or account name and password.

filename template
: Define the folder and file to which the image will be exported. This can be automatically generated using several pre-defined variables. See the [variables](../../variables.md) section for details.

output directory selector
: The button beside the filename template entry opens a dialog to select the parent directory for export.

on conflict
: Choose what to do if the generated filename conflicts with an existing file on export:

: - _create unique filename_: Automatically choose a unique new file name by appending an integer to name of the conflicting file.

: - _overwrite_: Automatically overwrite existing files. This option will present you with a confirmation dialog in order to protect you from accidental data loss -- you can disable this in [preferences > security > ask before exporting in overwrite mode](../../preferences-settings/security.md). _**Note:** This dialog is not presented per-file but as a one-off confirmation before the export job starts._

: - _skip_: Do not export images where the destination filename already exists.

### Format options

file format
: Choose the file format for the exported image. Additional options will appear (below) depending on the selected format.

quality
: The quality of the exported file. Higher values lead to larger file sizes. The default quality (95) is a good setting for very high quality exports (e.g. for archiving or printing purposes). If you need a good compromise between size and quality (e.g. for online image display or uploads) you should consider a value of “90” instead.

bit depth
: The number of bits used for each color channel. More bits means less posterization/color banding.

compression
: The type of compression to use.

compression level
: For export formats where _compression_ can be specified, the _compression level_ specifies how much compression to apply. The higher the level, the more the data will be compressed, at the cost of more CPU cycles.

b&w image
: For TIFF export format, it is possible to save a monochrome image. This setting controls whether the resulting file encodes the shades of gray as separate RGB channels, or as a single grayscale channel. The latter option will result in smaller files.

### Global options

set size
: Choose how to measure the maximum size of your exported image

: - _in pixels (for file)_: Enter the maximum width and height in pixels.

: - _in cm (for print)_: Enter the maximum width and height in cm and define the image's dpi. The equivalent size in pixels will be automatically calculated.

: - _in inch (for print)_: Enter the maximum width and height in inches and define the image's dpi. The equivalent size in pixels will be automatically calculated.

: - _by scale (for file)_: Enter a multiplier to specify by how much the exported image should be scaled compared to the input image. For example, entering a value of 0.5 will result in an output image with half the width and height (in pixels) of the original image.

dpi
: If units of cm or inches are chosen, set the dpi of the output image. The dpi will also be stored in the Exif data of the exported image. It will be automatically set to 300 if "in pixels" or "by scale" is chosen.

max size
: Set the maximum width and height of the exported image(s) in pixels, cm or inches (depending on the selected unit) -- zero means that no constraint will be set on that dimension. Exported images will be constrained so as not to exceed either of these values, while retaining the correct aspect ratio. Set both to zero to export with the original dimensions (after cropping). If the entered values exceed the original dimensions Ansel will either export with the original dimensions or upscale the image, depending on the "allow upscaling" parameter.

allow upscaling
: Set to “yes” to perform an upscaling step if the user-defined maximum width and height exceed the original dimensions of the image. If set to “no” the exported image's dimensions will not exceed the dimensions of the original image (after cropping).

high quality resampling
: Set this to 'yes' to perform high quality resampling on the image. The image will be processed in full resolution and only downscaled at the very end. This can sometimes result in better quality, but will always be slower.

store masks
: Store masks as layers in exported image. Only works for some image formats (currently TIFF and XCF).

profile
: The output color profile. Select “image settings” if you want the settings in the [_output color profile_](../darkroom/modules/output-color-profile.md) module of the individual images to be respected.

intent
: This option lets you define the intent -- the way in which Ansel will handle out-of-gamut colors. See [rendering intent](../../color-management/rendering-intent.md) for a more detailed description of the available options.

style
: Choose a [style](./styles.md) which Ansel will combine with the existing history stack to generate the output image. These history items are only added temporarily -- the original history stack is not overwritten. You can use this feature to add processing steps and parameters that you want to be applied specifically to images before export. For example you may define a style that adds a stronger level of sharpening when you produce scaled-down JPEG files for the internet or add a certain level of exposure compensation to all of your output images.

mode
: When applying a style during export this option defines whether the history stack items of that style replace the original history stack of the image or are appended to it. Technically speaking, in append mode history stack items of the style will constitute separate instances of the respective modules on top of any existing ones. As a consequence the original history stack will remain in effect with the new items being applied in addition. This way you can apply an overall adjustment (e.g. exposure) to a number of exported images while respecting the settings of each individual image.

export
: Press this button to start a background job to export all selected images. A bar at the bottom of the left hand panel displays the progress of the export job. Furthermore a notification message pops up reporting the completion of each individual export. You may click on the pop-up to make it disappear. You may abort the export job by clicking on the "x" icon located close to the progress bar.

---

**Note:** Images that are selected but currently hidden (because they are members of a collapsed group) will not be exported.

---

## Metadata preferences

The “preferences…” option in the presets menu brings up a dialog where you can configure what metadata to include in exported files. The parameters entered into this dialog are saved along with other export parameters to user presets, and the last entered values are retained when Ansel is closed.

### General settings

The left-hand-side of this dialog allows you to choose which groups of metadata are to be exported with the image. The following options are available:

exif data
: Export the source image's Exif data.

metadata
: Export metadata defined in the [metadata editor](./metadata-editor.md) module. Only metadata fields that are tagged as _visible_ and are not tagged as _private_ will be exported.

geo tags
: Export geo tags.

tags
: Export tags  created in the [tagging](./tagging.md) module (to `Xmp.dc.Subject`). Three additional options can also be selected:
: - _private tags_: Export private tags
: - _synonyms_: Export tag synonyms
: - _omit hierarchy_: Only export the last part of hierarchical tags

hierarchical tags
: Export hierarchical tags (to `Xmp.lr.Hierarchical Subject`)

develop history
: Export the entire development history (history stack and shapes) where supported (e.g. JPEG, JPEG2000, TIFF). The development history will be stored as XMP tags within the output file. This information can later be used to reconstruct the parameters and settings that were used to produce the exported image.

---

**Caution:** For various reasons embedding XMP tags into output files may fail without notice, e.g. if certain size limits are exceeded. Users are therefore advised to not rely on this feature for their backup strategy. To back up your data always make sure to save your input (raw) file as well as all of Ansel's XMP sidecar files.

---

### Per metadata settings

The right-hand-side of this dialog allows you to define formulas to populate image metadata. The formulas defined here have priority over the settings in the left-hand-side of the dialog. The first column identifies the entry to be edited. The second column allows you to define how to calculate the value for that metadata entry using a formula.

See the [variables](../../variables.md) section for details of the variables you can use in your metadata formula. Press Enter to validate the formula. Leave the formula empty to prevent a given metadata entry from being exported (for example `Exif.GPSInfo.GPSVersionID`).

Use the “--” icon to remove a metadata entry from the list and the “+” icon to add a new one from a predefined list of available metadata tags.

Click on the "add" button to add a metadata entry to the list.

The formulas allow you virtually define all the metadata you need to qualify your images in tagging and export the values in the XMP or IPTC tags of your choice. The exported tags can be different from one export to the next depending on the destination of the images. Tags and Categories are displayed separately in image information.

_Remember that a tag set up as a category is never exported._

### Tips

- To prevent a specific metadata field from being exported, add it to the list and leave the formula empty.
- To force a specific exif metadata field to be exported when exif export is disabled, add it to the list and enter `=` into the formula.

### Examples

example 1
: A first level tag called places is set as a category, and is followed by four levels of information (or keywords): country, region, city and location (e.g. `places|France|Nord|Lille|rue Nationale`). Each level can be retrieved (when it is defined) by one of the variables `$(CATEGORY0(places))`, `$(CATEGORY1(places))`, `$(CATEGORY2(places))` and `$(CATEGORY3(places))`. In this example, the returned values are "France", "Nord", "Lille" and "rue Nationale", respectively. These keywords can also be retrieved as simple tags using the variable `$(TAGS)`. The last keyword level defined (the leaf) is displayed in [image information](./image-information.md), here "rue Nationale".

example 2
: A first level tag called creator is followed by the name of the photographer, both set as categories: `creator|firstname lastname`. The formula copyrights (`$(YEAR) $(CATEGORY0(creator))`) builds the text associated with image rights. Here, [image information](./image-information.md) displays "creator: firstname lastname" as categories. Neither creator nor "firstname lastname" appear in the tags list and they are not exported as simple tags.

---

**Note:** tagging is not appropriate to define free text metadata, like a title or a description, which may be specific to each image. Use the [metadata editor](./metadata-editor.md) for this type of information.

---
---

# Exposure : https://ansel.photos/en/doc/views/darkroom/modules/exposure/

Increase or decrease the overall brightness of an image.

This module has two modes of operation:

manual
: Set the _exposure_, _black level_ and _clipping threshold_ manually

automatic (RAW images only)
: Use an analysis of the image's histogram to automatically set the exposure. Ansel automatically selects the exposure compensation that is required to shift the selected _percentile_ to the selected _target level_ (see definitions below). This mode is particularly useful for automatically altering a large number of images to have the same exposure. A typical use case of automatic mode is deflickering of time-lapse photographs.

## Module controls

mode
: Choose the mode of operation (automatic/manual).

compensate camera exposure (manual mode)
: Automatically remove the camera exposure bias (taken from the image's Exif data).

exposure (manual mode)
: Increase (move to the right) or decrease (move to the left) the exposure value (EV). To adjust by more than the default limits shown on the slider, right click and enter the desired value up to +/-18 EV.
: The color picker tool on the right sets the exposure such that the average of the selected region matches the target lightness defined in [spot exposure mapping](#spot-exposure-mapping) options.

clipping threshold (manual mode)
: Define what percentage of bright values are to be clipped in the calculation of the _exposure_ and _black level correction_. Use the color picker to sample a portion of the image to be used for this calcuation.

percentile (automatic mode)
: Define a location in the histogram to use for automatic exposure correction. A percentile of 50% denotes a position in the histogram where 50% of pixel values are above and 50% of pixel values are below that exposure.

target level (automatic mode)
: Define the target level for automatic exposure correction (EV) relative to the white point of the camera.

black level correction (manual and automatic modes)
: Adjust the black level point to unclip negative RGB values.

---

**Note:** Do not use the black level correction to add more density in blacks as this can clip near-black colors out of gamut by generating negative RGB values. This can cause problems with some modules later in the pixelpipe. Instead, use a tone mapping curve to add density to the blacks. For example, you can use the _relative black exposure_ slider on the _scene_ tab of the [filmic rgb](filmic.md#scene) module.

---

## Spot exposure mapping

The spot mapping feature is designed to help with batch-editing a series of images in an efficient way. In this scenario, you typically develop a single reference image for the whole batch and then copy&paste the development stack to all of the other images in the batch.

Unfortunately, the light often changes slightly between shots, even within the same series captured in the same conditions. This can be the result of a cloud passing by the sun in natural light, surface reflections having less "shine" from a different angle, or simply due to unavoidable variability in the mechanical diaphragm aperture. Each image will still need some individual fine-tuning if you want a perfectly even look over the whole series, and this can be both time-consuming and frustrating.

Spot exposure mapping allows you to define a target brightness, in terms of exposure, for a particular region of the image (the control sample), which you then match against the same target brightness in other images. The control sample can either be a critical part of your subject that needs to have constant brightness, or a non-moving and consistently-lit surface over your series of images.

The mapping process consists of two steps.

### Step 1: set the target

There are two ways of setting the target brightness for your control sample:

1. If you know or expect an arbitrary lightness for the control sample (for example, a gray card, a color chart, a product or a logo of a specified brightness), you can set its L value directly, in CIE Lab 1976 space,
2. If you simply want to match the development of your reference image, set the _spot mode_ to _measure_, then enable the color picker (to the right of the _exposure_ slider) and draw a rectangle over your control sample. The _input_ column will then be updated with the lightness value of the control sample before the exposure correction, and the _target_ column will show the resulting lightness of the control sample after the current exposure setting is applied.

If you reset the lightness value, the default value is 50% (middle-gray) -- this can be useful to quickly set the average exposure of any image.

Note that the target value is not reset when you reset the module itself, but is stored indefinitely in Ansel's configuration and will be available on next launch as well as for the next image you develop.

### Step 2 : match the target

When you open a new image, the _spot mode_ is automatically reset to _correction_. Using the color picker attached to the exposure slider, you can then directly reselect your control sample in the new image. The proper exposure setting required for the control sample to match the memorized target lightness will be automatically computed, and the setting will be updated in the same operation.

This operation can be repeated as many times as you have images in your series with no further work.


---

**Note 1:**  Trying to match lightness of moving parts of a subject across frames can prove tricky because they can have legitimate changes in their illumination as their orientation changes with respect to the main light source. For example, a part of the face can be fully lit in some frames and partially shaded in others. An inconsistently-lit control sample will generally not provide a robust reference for lightness matching across a series and may result in more work than manually matching it with visual feedback.

**Note 2:** The exposure module works (by default) in the scene-referred, linear, camera RGB part of the pixel pipeline, before the input color profile is applied. However, the conversion from camera RGB to CIE Lab 1976 space relies on the input color profile. All of the lightness L metrics given in the _spot mapping_ settings will use the input profile defined later in the [_input color profile module_](./input-color-profile.md) to perform the conversion accurately, but the conversion itself assumes a linear (RAW) signal and will not work for JPEG and PNG images (which are non-linearly encoded before the _input color profile_ module). If you want to use this feature on non-RAW images, you will need to move the exposure module to after _input color profile_ or to use the [_module order_](../../toolboxes/module-order.md) preset _v3.0 for JPEG/non-RAW input_.

**Note 3:** Perfectly matching your control sample against the target lightness may still not yield a similar perceptual result, even if the numbers are exactly the same. For example, if your subject sits in front of a background made of some bright parts and some dark parts, the ratio of bright areas / dark areas will affect the perception of contrast and brightness. If this ratio changes across your series, the subject brightness will not appear constant even though the lightness value is exactly constant. For more details, see the [checker shadow illusion](https://en.wikipedia.org/wiki/Checker_shadow_illusion) and the [Chubb illusion](https://en.wikipedia.org/wiki/Chubb_illusion).

---
---

# Filmic : https://ansel.photos/en/doc/views/darkroom/modules/filmic/
Remap the tonal range of an image by reproducing the tone and color response of classic film.

This module can be used either to expand or to contract the dynamic range of the scene to fit the dynamic range of the display. It protects colors and contrast in the mid-tones, recovers the shadows, and compresses bright highlights and dark shadows. Highlights will need extra care when details need to be preserved (e.g. clouds).

The module is derived from another module of the same name in [Blender 3D modeller](https://www.blender.org/) by T. J. Sobotka. While it is primarily intended to recover high-dynamic-range images from raw sensor data it can be used with any image. The following video (by the developer of this module) provides a useful introduction: [filmic: remap any dynamic range in darktable 3](https://www.youtube.com/watch?v=zbPj_TqTF880).

_filmic_ is the successor to the _filmic (legacy)_ module from darktable 2.6. While the underlying principles have not changed much, the default settings and their assumptions have, so users of the previous version should not expect a 1:1 translation of their workflow to the new version.

---

**Note**: Despite the technical look of this module, the best way to set it up is to assess the quality of the visual result. Do not overthink the numbers that are presented in the GUI to quantify the strength of the effects.

---

## Prerequisites

In order to get the best from this module, your images need some preparation:

capturing (ETTR)
: In-camera, it is recommended that you use a technique known as "Expose To The Right" (ETTR). This means exposing the shot so that the exposure is as bright as possible without clipping the highlights. It is called "exposing to the right" because the in-camera histogram should be touching all the way up to the right hand side without peaking at the right hand side (which could indicate clipping). This technique ensures you make maximum use of the dynamic range of your camera's sensor.

: The default auto-exposure metering mode in your camera will normally expose the image so that the average brightness in the image tends towards middle-gray. Sometimes, for scenes dominated by light tones, the camera will underexpose the image to bring those light tones more towards middle-gray. For scenes dominated by dark tones, it may over-expose the image and end up clipping the highlights. In such cases you can use the exposure compensation dial in your camera to raise or lower the exposure -- the Ansel exposure module can automatically take this into account when processing your image.

: In some cases (e.g. specular highlights reflecting off shiny objects) it may be acceptable to have some clipping, but be aware that any clipped data in your image is irrevocably lost. Where data has been clipped, _filmic_ offers a "highlight reconstruction" feature to help mitigate the effects of the clipping and blend it smoothly with the rest of the image. The settings for this feature are on the [_reconstruct_](#reconstruct) tab. Some cameras also offer a "highlight priority" exposure metering mode that can help to maximise exposure while protecting the highlights, and many offer features such as "zebras" or "blinkies" in the live view to alert the photographer when parts of the image are being clipped.

adjust for the mid-tones
: In the [_exposure_](./exposure.md) module, adjust the exposure until the mid-tones are clear enough. Don't worry about losing the highlights at this point -- they will be recovered as part of the filmic processing. However, it is important to avoid negative pixels in black areas else the computations performed by _filmic_ may produce unpredictable results. For some camera models (Canon, mainly), rawspeed (the raw decoding library of Ansel) may set an exaggerated black level, resulting in crushed blacks and negative pixel values. If so, brighten the blacks by setting a negative black level correction value in the [_exposure_](./exposure.md) module.

white balance, denoise, demosaic
: If you plan on using _filmic_'s auto-tuners, use the [_white balance_](./white-balance.md) module to first correct any color casts and obtain neutral colors. In RGB color spaces, luminance and chrominance are linked, and _filmic_'s luminance detection relies on accurate measurements of both. If your image is very noisy, add an initial step of denoising to improve the black exposure readings, and use a high quality [_demosaic_](./demosaic.md) algorithm. You don't need to worry about noise if you are planning to set up filmic manually, without using the auto-tuners.

## Usage

The _filmic_ module is designed to map the dynamic range of the photographed scene (RAW image) to the dynamic range of the display.

This mapping is defined in three steps, each handled in a separate tab in the interface:

 - The [_scene_](#scene) tab contains the “input” settings of the scene, defining what constitutes white and black in the photographed scene.

 - The [_reconstruct_](#reconstruct) tab offers tools to handle blown highlights.

 - The [_look_](#look) tab contains the artistic intent of the mapping that is applied to the input parameters (as defined in the scene tab). This part of the module applies an S-shaped parametric curve to enhance the contrast of the mid-tones and remap the gray value to the middle-gray of the display. As a general guideline, you should aim to increase the latitude as much as possible without clipping the extremes of the curve.

 - The [_display_](#display) tab defines the output settings required to map the transformed image to the display. In typical use cases, the parameters in this tab rarely require adjustment.

 - The [_options_](#options) tab includes some optional advanced settings and parameters.

_filmic_ tends to compress local contrast, so after you have finished adjusting settings here, you may wish to compensate for this using the [_local contrast_](local-contrast.md) module. You may also want to increase the saturation in the [_color balance_](color-balance.md) module, and perhaps to further adjust the tones using the [_tone equalizer_](tone-equalizer.md).

The ranges of _filmic_'s sliders are limited to typical and safe values, but you can enter values outside of these limits by right-clicking and entering values with the keyboard.

---

**Note**: _filmic_ cannot be set with entirely neutral parameters (resulting in a "no-operation") -- as soon as the module is enabled, the image is always at least slightly affected. You can, however, come close to neutral with the following settings:

- In the [_look_](#look) tab, set contrast to 1.0, latitude to 99 % and mid-tones saturation to 0 %,
- In the [_options_](#options) tab, set contrast in shadows and in highlights to a _soft_ (polynomial) curve type.

In this configuration, filmic will only perform a logarithmic tone mapping between the bounds set in the [_scene_](#scene) tab.

---

## Graphic display

The graphic display at the top of the _filmic_ module offers multiple views to help you to understand its functionality. You can cycle through these views using the ![view-icon](view-icon.jpg) icon.

The following views are available:

look only
: This is the default view. The main bright curve shows how the dynamic range of the scene (in EV) is compressed into the display-referred output range. The orange dot shows the middle-gray point, the white dots either side mark out the latitude range, and the orange part of the curve at the bottom and top indicates an overshoot problem with the spline (the [_look_](#look) tab has some controls to deal with this).

: ![filmic-rgb-look-only](filmic-look-only.jpg)

look + mapping (linear)
: This view shows the mapping of input values [0,1] to output values in linear space, including the dynamic range mapping and the output transfer function. Note that in a scene-referred workflow, input values are allowed to exceed 1, however the graph only shows in/out values in the interval [0,1] in order to make the shape of the graph comparable to other tone curve mapping tools such as _base curve_ or _tone curve_. The actual value of the scene white point is shown in brackets on the _X axis_ (expressed as a percentage of an input value of 1).

: ![filmic-rgb-look-mapping-lin](filmic-look-mapping-lin.jpg)

look + mapping (log)
: The same as the previous view, but plotted in log space.

: ![filmic-rgb-look-mapping-lin](filmic-look-mapping-log.jpg)

dynamic range mapping
: This view is inspired by the Ansel Adams Zone System, showing how the zones in the input scene (EV) are mapped to the output. Middle gray from the scene is always mapped to 18% in the output (linear) space, and the view shows how the tonal ranges towards the extremes of the scene exposure range are compressed into a smaller number of zones in the display space, leaving more room for the mid-tones to be spread out over the remaining zones. The latitude range is represented by the darker gray portion in the middle.

: ![filmic-rgb-look-mapping-lin](filmic-dynamic-range-map.jpg)

---

**Note:** When some parameters are too extreme, resulting in an unfeasible curve, _filmic_ will sanitize them internally. Sanitizing is illustrated in two ways on the look views:

- A dot becoming red indicates that the linear part of the curve is pushed too far towards the top or the bottom. In the [_look_](#look) tab, reduce the _latitude_ or recenter the linear part using the _shadows ↔ highlights balance_ parameter.
- A dot becoming a half circle indicates that contrast is too low given the dynamic range of the image. Increase _contrast_ in the [_look_](#look) tab, or the _dynamic range_ in the [_scene_](#scene) tab.

---

## Module controls

### Scene

The controls in the _scene_ tab are similar in principle to those of the typical _levels_ tools, as found in other software. The difference is that _levels_ assumes display-referred pixels values (between 0 and 100%), whereas _filmic_ allows you to work on scene-referred pixels (between --infinity EV and +infinity EV), which forces the use of a different interface.

middle-gray luminance (hidden by default)
: This setting allows you to decide what luminance in the scene should be considered the reference middle-gray (which will be remapped to 18% in display). Use the color picker tool to read the average luminance over the drawn area. If you have a photograph of a gray card or a color chart (IT8 chart or colorchecker) shot in the scene lighting conditions, then the gray color picker tool can be used to quickly sample the luminance of the gray patch on that image. In other situations, the color picker can be used to sample the average luminance of the subject.

: This has an effect on the picture that is analogous to a brightness correction. Values close to 100% do not compress the highlights but fail to recover shadows. Values close to 0% greatly recover the shadows but compress the highlights more harshly and result in local-contrast losses.

: When modifying the middle-gray luminance, the white and black exposures are automatically adjusted accordingly, to prevent the dynamic range from clipping and to help you set the right parameter faster. If you are not happy with the auto adjustment performed by the gray slider, you can correct the white and black exposure parameters afterwards.

---

**Note:** You are not advised to use this control to set middle-gray, hence it is now hidden by default. You should instead use the _exposure_ module to set the middle-gray level (see [_usage_](#usage), above). However, if you wish to make this slider visible, you can enable it with the _use custom mid-gray values_ checkbox in the [_options_](#options) tab.

---

white relative exposure
: The number of stops (EV) between the scene middle-gray luminance and the scene luminance to be remapped to display white (peak-white). This is the right bound of the scene dynamic range that will be represented on the display -- everything brighter than this value on the scene will be clipped (pure white) on the display. The color picker tool reads the maximum luminance in RGB space over the drawn area, assumes it is pure white, and sets the white exposure parameter to remap the maximum to 100% luminance.

black relative exposure
: The number of stops (EV) between the scene middle-gray luminance and the scene luminance to be remapped to display black (maximum density). This is the left bound of the scene dynamic range that wll be represented on the display -- everything darker than this value on the scene will be clipped (pure black) on the display. The color picker tool reads the minimum luminance in RGB space over the drawn area, assumes it is pure black, and sets the black exposure parameter to remap the minimum to 0% luminance. The black color picker measurement is very sensitive to noise, and cannot identify whether the minimum luminance is pure black (actual data) or just noise. It works better on low ISO pictures and with high quality demosaicing. When the color picker puts the black exposure at --16 EV, this is a sign that the measurement has failed and you will need to adjust it manually.

: The black relative exposure allows you to choose how far you want to recover lowlights.

dynamic range scaling and auto-tune
: The auto-tune color picker combines the above color pickers, and allows you to set the white and black exposures at the same time, using the maximum of the drawn region as the white and the minimum as the black. This gives good results in landscape photography but usually fails for portraits and indoor scenes.

: When no true white and black are available on the scene, the maximum and minimum RGB values read on the image are not valid assumptions any more. Dynamic range scaling symmetrically shrinks or enlarges the detected dynamic range and the current parameters. This works with both color pickers, and adjusts the current values of white and black relative exposures.

---

**Note**: There is no direct relationship between your camera sensor's dynamic range (to be found in DxoMark.com or PhotonsToPhotos.org measurements) and the dynamic range in filmic (scene white EV -- scene black EV). Many things happen before _filmic_ in the pipeline (for example a black raw offset that could map black to 0) such that _filmic_ sees a theoretically infinite dynamic range at its input. This has to do only with pixel encoding manipulation in software, not actual sensor capabilities.

The _scene-referred_ workflow forces a black level correction of --0.0002, in the _exposure_ module, which ensures that the dynamic range seen by _filmic's_ input is around 12.3 EV most of the time. Decrease this value even more if setting the black relative exposure in _filmic_ to --16 EV fails to unclip blacks.

---

### Reconstruct

This tab provides controls that blend transitions between unclipped and clipped areas within an image and can also help to reconstruct colors from adjacent pixels. It is designed to handle spotlights that could not possibly be unclipped when taking the shot (such as naked light bulbs or the sun disc in the frame) and aims at diffusing their edges as film would do. It is not designed to recover large areas of clipped pixels or in-paint missing parts of the image.

It can sometimes be useful to disable the [_highlight reconstruction_](./highlight-reconstruction.md) module in order to provide additional data to the reconstruction algorithm (_highlight reconstruction_ clips highlight data by default). You should note that this can lead to magenta highlights, which will need to be handled with the _gray/colorful details_ slider.

Firstly, a mask needs to be set up to identify the parts of the image that will be affected by the highlights reconstruction. There are then some additional controls to fine-tune some of the trade-offs made by the reconstruction algorithm.

#### _highlights clipping_

These controls allow you to choose which areas of the image are impacted by the highlight reconstruction algorithms.

threshold
: Any pixels brighter than this threshold will be affected by the reconstruction algorithm. The units are in EV, relative to the white point set in the _scene_ tab. By default, this control is set to +3 EV, meaning that pixels need to be at least +3 EV brighter than the white point set in the [_scene tab_](#scene) in order for the highlight reconstruction to have any effect. In practise, this means that highlight reconstruction is effectively disabled by default (for performance reasons -- it should only be enabled when required). Therefore, to use the _highlights reconstruction_ feature, first click the _display highlight reconstruction mask_ icon to show the mask, and lower this threshold until the highlight areas you want to reconstruct are selected in white by the mask. It may be useful to first review the image using the [raw overexposed warning](../../toolboxes/raw-overexposed.md) to show you which pixels in the raw file have been clipped, and whether those pixels are clipped on just one RGB channel or all of them.

transition
: Use this control to soften the transition between clipped and valid pixels. Moving this control to the right will increase the amount of blur in the mask, so that the transition between clipped and non-clipped areas is softer. This allows for a smoother blending between the clipped and non-clipped regions. Moving this control to the left will reduce the blur in the mask, making the transition in the mask much sharper and therefore reducing the amount of feathering between clipped and non-clipped areas.

display highlight reconstruction mask
: Click on the icon to the right of this label to toggle the display of the highlight reconstruction mask. It is recommended that you turn this on while adjusting the above controls.

#### _balance_

These controls allow you to balance the trade-offs between the various reconstruction algorithms.

structure ↔ texture
: Use this to control whether the reconstruction algorithm should favor painting in a smooth color gradient (structure), or trying to reconstruct the texture using sharp details extracted from unclipped pixel data (texture). By default, the control is in the middle at 0%, which favors both strategies equally. If you have lots of areas where all three channels are clipped, there is no texture detail available to reconstruct, so it is better to move the slider to the left to favor color reconstruction. If you have lots of areas where only one or two channels are clipped, then there may be some texture detail in the unclipped channel(s), and moving the slider to the right will place more emphasis on trying to reconstruct texture using this unclipped data.

bloom ↔ reconstruct
: Use this to control whether the algorithm tries to reconstruct sharp detail in the clipped areas (reconstruct), or apply a blur that approximates the blooming effect you get with traditional film (bloom). By default, this is set to 100%, which tries to maximise the sharpness of the detail in the clipped areas. Move this slider to the left if you want to introduce more blur in these areas. Introducing more blur will usually tend to darken the highlights as a by-product, which may lead to a more colorful reconstruction.

gray ↔ colorful details
: Use this to control whether the algorithm favors the recovery of monochromatic highlights (gray) or colorful details. Move the slider to the right if you want more color in the highlights. Move the slider to the left if you want to reduce the saturation of the highlights. It can be helpful to reduce the saturation in the highlights if you start seeing magenta or out-of-gamut colors.

### Look

When working on the _look_ tab, it is recommended that you monitor the S-curve spline on the _look only_ graph. This curve starts from the scene/display black levels at the bottom left of the graph, and should smoothly increase up to the scene/display white levels at the top right. Sometimes, if the constraints on the S-curve are too tight, the splines in the shadows and/or highlights regions can "overshoot" the limits of the display, and an orange warning is shown on those parts of the spline.

If you see the orange warning indicator at either end of the S-curve, corrective actions should be performed to bring the S-curve back to a smooth monotonically increasing curve. This may involve:

- Reducing the latitude and/or contrast,

- Adjusting the shadows/highlights slider to shift the latitude and allow more room for the spline,

- Ensuring that the scene-referred black and white relative exposure sliders on the _scene_ tab have been properly set for the characteristics of the scene,

- Choosing a different curve type for the shadows/highlights contrast on the [_options_](#options) tab.

If the _target black luminance_ setting on the [_display_](#display) tab is non-zero, this can also make it difficult for _filmic_ to find a smooth monotonic spline, and reducing this can also help to relax the constraints. See the [_display_](#display) section to understand the implications of this.

contrast
: The _filmic_ S-curve is created by computing the position of virtual nodes from the module parameters and interpolating them. This is similar to how the tone curve module operates, but here, the nodes cannot be moved manually. The curve is split into three parts -- a middle linear part, and two extremities that transition smoothly from the slope of the middle part to the ends of the exposure range.

: The contrast slider controls the slope of the middle part of the curve, as illustrated in the graph display. The larger the dynamic range, the greater the contrast should be set to, in order preserve a natural-looking image. This parameter mostly affects the mid-tones. Note that global contrast has an impact on the acutance (perceived sharpness) -- a low-contrast image will look unsharp even though it is optically sharp in the sense of the [Optical Transfer Function (OTF)](https://en.wikipedia.org/wiki/Optical_transfer_function).

: Setting the contrast to 1 almost completely disables the S-curve, though there will be a very small residual effect from the splines in the highlights and shadows.

hardness (previously _target power factor function_)
: Known as the _target power factor function_ slider in older versions of _filmic_, this slider is hidden by default, and is adjusted automatically based on values in the [_scene_](#scene) tab. To make this slider visibile, you need to uncheck _auto adjust hardness_ in the [_options_](#options) tab.

: This parameter is the power function applied to the output transfer function, and it is often improperly called the _gamma_ (which can mean too many things in imaging applications, so we should stop using that term). It is used to raise or compress the mid-tones to account for display non-linearities or to avoid quantization artifacts when encoding in 8 bit file formats. This is a common operation when applying ICC color profiles (except for linear RGB spaces, like REC 709 or REC 2020, which have a linear “gamma” of 1.0). However, at the output of _filmic_, the signal is logarithmically encoded, which is not something ICC color profiles know to handle. As a consequence, if we let them apply a gamma of 1/2.2 on top, it will result in a double-up, which would cause the middle-gray to be remapped to 76% instead of 45% as it should in display-referred space.

shadows / highlights
: These two sliders directly set the position of the toe node (_shadows_) and of the shoulder node (_highlights_) of the S-curve: the points where the central linear portion of the curve ends and the roll-off toward black or white begins. Each is expressed as a percentage of the available room between middle-gray and the point where the current slope would hit the display black (respectively white) level. They replace the _latitude_ and _shadows ↔ highlights balance_ controls of older versions, which set the same two nodes but in linked coordinates (a global width plus an offset) that made adjusting one end without disturbing the other cumbersome. Internally, the module still stores latitude and balance for compatibility -- the sliders are a pure GUI-layer conversion, and old edits are unaffected.

: The range enclosed between the two nodes -- the latitude -- is the luminance range that is remapped in priority, at the constant slope defined by the contrast parameter. With the default _perceptual_ curve type (see _contrast in shadows/highlights_ in the [_options_](#options) tab), the nodes also act as **tension** controls: values close to 0 % hand the whole curve to the smooth sigmoid roll-off, while large values force the roll-off into a short, sharp turn near the extremes. With the older polynomial curve types it was advisable to keep the latitude as large as possible; with the perceptual sigmoid the logic is reversed and the small default is the appearance-matched optimum -- raise the nodes only if you deliberately want a harder transition.

: The latitude also defines the range of luminances that are not desaturated at the extremities of the luminance range (See _mid-tones saturation_).

mid-tones saturation / extreme luminance saturation
: At extreme luminances, the pixels will tend towards either white or black. Because neither white nor black have color associated with them, the saturation of these pixels must be 0%. In order to gracefully transition towards this 0% saturation point, pixels outside the mid-tone latitude range are progressively desaturated as they approach the extremes. The darker curve in the _filmic_ graph indicates the amount of desaturation that is applied to pixels outside the latitude range. Moving the slider to the right pushes the point where desaturation will start to be applied towards the extremes, resulting in a steeper desaturation curve. If pushed too far, this can result in fringing around the highlights. Moving the slider to the left brings the point at which color desaturation will start to be applied closer to the center, resulting in a gentler desaturation curve. If you would like to see more color saturation in the highlights, and you have checked that the white relative exposure in the [_scene_](#scene) tab is not yet clipping those highlights, move the mid-tones saturation slider to the right to increase the saturation.

: Please note that this desaturation strategy has changed compared to previous versions of _filmic_ (which provided a different slider control labelled _extreme luminance saturation_). You can revert to the previous desaturation behaviour by selecting "v3 (2019)" in the _color science_ setting on the [_options_](#options) tab. Since _filmic_ _v6_ and _v7_ use accurate gamut mapping to the output color space, the desaturation curve is removed and the extreme luminance desaturation becomes in practice an highlights bleaching control.

: This control is set to 0 by default and it is now recommended that saturation is handled earlier in the pipeline. A preset "add basic colorfulness" has been added to the [_color balance_](./color-balance.md) module for this purpose.

: With the _v8 (AgX)_ color science, this slider is relabelled _color preservation_ and controls **how much of the per-channel hue drift to keep** -- it does not affect saturation. How much saturation the rendering keeps is set by the chosen _v8_ variant, not by this slider: the _no bleach_ variant preserves the saturation of valid diffuse colors -- skin tones, product colors, blue skies -- completely, because mandatory bleaching of valid midtone colors whitens non-Caucasian skin tones, which is a bias no variant is allowed to inflict on skin (the more-bleached variants trade saturation on non-skin colors deliberately -- see [variants](#variants)). Strongly compressed colors (speculars, clipped lights) bleach at any setting. At **-100 %** the transform runs as pure AgX: the full per-channel hue drift is present (the "film" character). At **0 %** (the default) half of that hue drift is removed. At **+100 %** the original hues are restored exactly, while the tonal bleaching of extreme highlights is unchanged. See the [background](#background) section.

### Display

The parameters in this tab should rarely require adjustment.

target black luminance
: The destination parameters set the target luminance values used to remap the tones. The default parameters should work 99% of the time, the remaining 1% being when you output in linear RGB space (REC709, REC2020) for media handling log-encoded data. These settings should therefore be used with caution because Ansel does not allow separate pipelines for display preview and file output.

: The target black luminance parameter sets the ground-level black of the target medium. By default it is set to the minimum non-zero value that can be encoded by the available number of bits in the output color space. Reducing it to zero means that some non-zero luminances will be mapped to 0 in the output, potentially losing some detail in the very darkest parts of the shadows. Increasing this slider will produce raised, faded blacks that can provide something of a "retro" look.

target middle-gray
: This is the middle-gray of the output medium that is used as a target for the S-curve's central node. On gamma-corrected media, the actual gray is computed with the gamma correction (middle-gray^(1/gamma)), so a middle-gray parameter of 18% with a gamma of 2.2 gives an actual middle-gray target of 45.87%.

target white luminance
: This parameter allows you to set the ceiling level white of the target medium. Set it lower than 100% if you want dampened, muted whites to achieve a retro look.

: To avoid double-ups and washed-out images, _filmic_ applies a “gamma” compression reverting the output ICC gamma correction, so the middle-gray is correctly remapped at the end. To remove this compression, set the destination power factor to 1.0 and the middle-gray destination to 45%.

### Options

color science
: This setting defaults to _v7 (2023)_ for new images, and defines the algorithms used by the _filmic_ module (e.g. the extreme luminance desaturation strategy). To revert to the behavior of previous versions of _filmic_, set this parameter to _v3 (2019)_, _v4 (2020)_, _v5 (2021)_ or _v6 (2022)_. The difference between these methods lies in the way in which they handle desaturation close to pure black and pure white (see the [background](#background) section for details). If you have previously edited an image using older versions of _filmic_, the color science setting will be kept at the earlier version number in order to provide backward compatibility for those edits. The _v7 (2023)_ method removes the _preserve chrominance_ option, and the _v8 (AgX)_ method applies the tone curve to each RGB channel separately inside a dedicated rendering color space (see the [background](#background) section for details on both). _v8 (AgX)_ comes in **five variants** -- _no bleach_, _low bleach_, _medium bleach_, _high bleach_ and _extra bleach_ -- which sit on a single trade-off between keeping saturation and keeping hue accurate; see the table in the [background](#background) section to choose one.

preserve chrominance
: _(This setting is not available with the v7 and v8 color sciences)_. Define how the chrominance should be handled by _filmic_ -- either not at all, or using one of the three provided norms.

: When applying the S-curve transformation independently on each color, the proportions of the colors are modified, which modifies the properties of the underlying spectrum, and ultimately the chrominance of the image. This is what happens if you choose "no" in the preserve chrominance parameter. This value may yield seemingly “better” results than the other values, but it may negatively impact later parts of the pipeline, for example, when it comes to global saturation.

: The other values of this parameter all work in a similar way. Instead of applying the S-curve to the R, G and B channels independently, _filmic_, divides all the three components by a norm (N), and applies the S-curve to N. This way, the relationship between the channels is preserved.

: The value of the preserve chrominance parameter indicates which norm is used (the value used for N):

: - _no_ means that the ratios between the RGB channels are not preserved. This will tend to saturate the shadows and desaturate the highlights, and can be helpful when there are out-of-gamut blues or reds.

: - _max RGB_ is the maximum value of the R, G and B channels. This is the same behaviour as the original version of the _filmic_ module. It tends to darken the blues, especially skies, and to yield halos or fringes, especially if some channels are clipped. It can also flatten the local contrast somewhat.

: - _luminance Y_ is a linear combination of the R, G and B channels. It tends to darken and increase local contrast in the reds, and tends not to behave so well with saturated and out-of-gamut blues.

: - _RGB power norm_ is the sum of the cubes of the R, G, and B channels, divided by the sum of their squares (R³ + G³ + B³)/(R² + G² + B²). It is usally a good compromise between the max RGB and the luminance Y values.

: - _RGB euclidean norm_ has the property of being RGB-space-agnostic, so it will yeild the same results regardless of which working color profile is used. It weighs more heavily on highlights than the power norm and gives more highlights desaturation, and is probably the closest to a color film look.

: There is no "right" choice for the norm, and the appropriate choice depends strongly on the image to which it is applied. You are advised to experiment and decide for yourself which setting gives the most pleasing result with the fewest artifacts.

spline handling
: This setting selects how the _latitude_, _balance_ and _contrast_ place the toe and shoulder **nodes** of the curve -- not the shape of the segments between them, which is chosen by _contrast in shadows/highlights_ below. _v3 (2021)_ is recommended; _v1_ and _v2_ are kept for backward compatibility with older edits. (The shape of the roll-off, including the modern sigmoid, used to live here as a mislabelled "v4"; it now belongs to the curve-type controls.)

contrast in highlights
: This control selects the shape of the highlights roll-off of the curve. **_perceptual_** (the default) leaves the straight mid-tone section at exactly the mid-tone slope and then glides smoothly to white -- a "slope-matched" roll-off with no fixed strength: it adapts to the scene, staying nearly straight for a low-dynamic-range studio shot (little to compress) and rolling off more firmly for a wide-range landscape (more to compress), and never over-compresses the brightest stop. The other three are the legacy segment types: _safe_ (rational, guaranteed not to over- or under-shoot but muted near white), _hard_ (sharper, more tonal compression, can overshoot) and _soft_ (gentler). Pick a legacy type only if you want a fixed roll-off character instead of the adaptive perceptual default.

contrast in shadows
: The same control for the shadows end of the curve. **_perceptual_** (the default) keeps shadow gradients open down to the deepest exposures (tuned for a dim room and a low-flare display); the legacy _hard_/_soft_/_safe_ segment types behave as for highlights.

use custom middle-gray values
: Enabling this setting makes the _middle-gray luminance_ slider visible on the [_scene_](#scene) tab. With the current version of _filmic_, you are advised to use the _exposure_ module to set the middle-gray level, so this setting is disabled by default (and the _middle-gray luminance slider_ is hidden).

auto-adjust hardness
: By default, this setting is enabled, and _filmic_ will automatically calculate the power function (aka "gamma") to be applied on the output transfer curve. If this setting is disabled, a _hardness_ slider will appear on the [_look_](#look) tab so that value can be manually set.

iterations of high-quality reconstruction
: Use this setting to increase the number of passes of the highlight reconstruction algorithm. More iterations means more color propagation into clipped areas from pixels in the surrounding neighbourhood. This can produce more neutral highlights, but it also costs more in terms of processing power. It can be useful in difficult cases where there are magenta highlights due to channel clipping.

: The default reconstruction works on separate RGB channels and has only one iteration applied, whereas the _high quality_ reconstruction uses a different algorithm that works on RGB ratios (which is a way of breaking down chromaticity from luminance) and can use several iterations to graduately propagate colors from neighbouring pixels into clipped areas. However, if too many iterations are used, the reconstruction can denegenerate, which will result in far colors being improperly inpainted into clipped objects (color bleeding) -- for example white clouds being inpainted with blue sky, or the sun disc shot through trees being inpainted with leaf-green.

add noise in highlights
: This artificially introduces noise into the reconstructed  highlights to prevent them from looking too smooth compared to surrounding areas that may already contain noise. This can help to blend the reconstructed areas more naturally with the surrounding non-clipped areas.

type of noise
: This specifies the statistical distribution of the added noise. It can be helpful to match the look of the artificially generated noise with the naturally occurring noise in the surrounding areas from the camera's sensor. The _poissonian_ noise is the closest to natural sensor noise but is less visually pleasing than _gaussian_, which is probably closer to film grain. Also note that most denoising modules will turn the sensor noise from poissonian to slightly gaussian, so you should pick the variant that blends better into the actual noise in your image.

### Background

The _color science_ parameter (in the _options_ tab) defines the strategy that is used to desaturate colors near pure white (maximum display emission) and pure black (minimum display emission). The problem can be explained with the graph below, which represents the gamut of the sRGB color space at the constant hue of its green primary, with varying lightness (vertical axis) and chroma (horizontal axis):

![Gamut cone](sRGB-green.jpg)

As we approach pure black and pure white, the chroma available in gamut shrinks considerably until it reaches zero for lightness = 0 and lightness = 100% of the medium emission. This means that very bright (or very dark) colors cannot be very saturated at the same time if we want them to fit in gamut, with the gamut being imposed by the printing or displaying device we use.

If colors are left unmanaged and are allowed to escape gamut, they will be clipped to valid values at the time of conversion to display color space. The problem is that this clipping is generally not hue-preserving and definitely not luminance-preserving, so highlights will typically shift to yellow and appear darker than they should, when evaluated against their neighborhood.

To overcome this, filmic has used various strategies over the years (the so-called _color sciences_) to desaturate extreme luminances, forcing a zero saturation at minimum and maximum lightness and a smooth desaturation gradient. These strategies were all intended to minimize the hue shifts that come with gamut clipping.

Since all of these strategies were approximations (and often over-conservative ones) _v6 (2022)_ introduces a more accurate and measured approach. It performs a test-conversion to display color space, checks if the resulting color fits within the [0; 100]% range, and if it doesn't, computes the maximum saturation available in gamut at this luminance and hue, finally clipping the color to this value. This ensures a minimal color distortion, allowing for more saturated colors and better use of the available gamut, but also enforces a constant hue throughout the whole tone mapping **and** gamut mapping operation.

This gamut mapping uses the output color profile as a definition of the display color space and automatically adjusts to any output space. However, only _matrix_ or _matrix + curve(s)_ ICC profiles are supported. _LUT_ ICC profiles are not supported and, if used, will make the gamut mapping default to the pipeline working space (Rec 2020 by default).

Note that the hue used as a reference for the gamut mapping is the hue before any tone mapping, sampled at the input of filmic. This means that even the _none_ chrominance preservation mode (applied on individual RGB channels regardless of their ratios) preserves hue in _v6_. This mode will only desaturate highlights more than the other modes, and a mechanism is in place to prevent it from resaturating shadows -- this behaviour can be bypassed by increasing the _extreme luminance saturation_ setting.

The _v7 (2023)_ color science improves over _v6_ and simplifies the chroma preservation options, by removing them. The chroma preservation modes aim at anchoring saturation and hue across the tone-mapping operation, by preserving RGB ratios compared to a norm. The choice of the norm is important when it comes to managing how the gamut is used and how the contrast of bright objects relatively to their neighbourhood is rendered by the tone-mapper. Several norms have been proposed since filmic _v1_, in 2018: none of them have been found to be a clear winner, and only one of them (max RGB) has some theoritical justification (allowing to reach display peak primary colors after the transform).

The _v7_ approach is to offer a mix between the _max RGB_ norm and the no-preservation option (where the output hue and saturation are still forced to their input values). The proportions of the mix are driven by the _extreme luminance saturation_ setting:

- 0% is an average of both,
- -50% is strictly equivalent to the _v6_ no-preservation option,
- +50% is strictly equivalent to the _v6_ _max RGB_ option,
- Intermediate values are weighted averages between both,
- Values beyond ±50% (up to ±200%) are linear extrapolations.

Positive values will favour saturated highlights and will be suitable for skies but need to be handled with care for portraits (producing accurate skin tones… which is not what people actually find too saturated and "beefy"[^1]), negative values will favour highlights bleaching, which is the preconceived idea many people have of "film look" (which is disproved by positive film slides and Technicolor movies, in addition of being highly questionnable to render black and tanned skin, as it removes ethnical features and whitens them).

[^1]: D. L. MacAdam, "Quality of Color Reproduction," in _Journal of the Society of Motion Picture and Television Engineers_, vol. 56, no. 5, pp. 487-512, May 1951, doi: 10.5594/J06314.

The saturation control gives a fine control over the amount of saturation vs. bleaching expected in highlights. In any case, the saturation algo will not allow the output saturation to be higher than the input one, and it should be made very clear that this setting is not designed for creative purposes, but only to drive the complicated trade-off coming from remapping RGB values from one color space to another, having different gamut and dynamic range.

The _v8 (AgX)_ color science implements the one genuinely useful idea popularized by Blender's AgX view transform and its darktable port: applying the tone curve to each RGB channel **separately**, inside a rendering color space whose primaries have been slightly compressed and rotated. Per-channel curves couple color to tonality -- highlights bleach toward white and shadows sink toward black *as a function of the tonal compression itself*, which produces the smooth, progressive desaturation of bright saturated subjects (flames, LEDs, stained glass) that norm-based tone mapping renders as flat colored patches. The rendering-space compression controls how fast that bleaching happens and steers the direction of the hue drifts that per-channel curves inevitably produce.

Where _v8_ differs from darktable/Blender AgX:

- **The rendering space is derived, not hand-tuned.** The compression and rotation constants in AgX are unexplained numbers inherited from a forum thread. In Ansel they are computed by an optimization with stated objectives -- neutral (zero-average) hue drift measured in a perceptual hue metric, guaranteed positivity, bounded worst-case drift -- against the module's default curve, and the derivation scripts ship with the source code. Notably, there is **no built-in warm/yellow shift**: AgX's skew toward yellow is a creative decision hard-coded in its constants; in Ansel, if you want warmth, you add it yourself where it belongs (see the emulation section below).
- **Valid colors keep their saturation, by construction (in the _no bleach_ variant).** AgX (and any per-channel tone mapper with a neutral or under-expanding outset) mandatorily desaturates every color the curve touches, midtones included -- olive skin tones get bleached toward Caucasian-looking, which is a racial bias, not a style. In _v8_, the rendering is fitted (against a published human skin-tone database and diffuse reflectances) so that valid diffuse colors recover their chroma through the transform itself, for any dynamic range; only strongly compressed colors (speculars, clipped lights) bleach. The default _no bleach_ variant preserves skin-tone saturation completely (see the [variants table](#variants)); the _low_, _medium_ and _high bleach_ variants deliberately trade a little of it for tighter hue. The _color preservation_ slider then dials the per-channel hue drift continuously (full at -100 %, half at 0 %, none at +100 %); it is a hue control only and leaves the tonal bleaching untouched. The hue handling is performed in a perceptual color space, not HSV.
- **The output is gamut-mapped.** _v8_ keeps filmic's _v6/v7_ gamut mapping against the export color profile; AgX has none, and its output can leave the display gamut freely.
- **Everything else is regular filmic.** Scene white/black exposures, contrast, the shadows/highlights nodes, highlight reconstruction, and the display targets work exactly as in the other color sciences -- _v8_ only changes the color handling, not the tone machinery.

### The five _v8_ variants {#variants}

Per-channel curves inevitably produce three coupled side effects on every color they touch, and the rendering-space compression cannot flatten all three at once -- pushing one down pushes another up:

- **desaturation** -- colors bleach toward white as they brighten (chroma is lost);
- **hue drift** -- the classic blue-to-purple, red-to-orange rotation;
- **apparent-brightness drift** -- a change in how *bright a color looks for its luminance* (the Helmholtz--Kohlrausch effect, below), which is what makes an over-cooked red read "self-luminous".

The five variants are five points on this trade-off, from _no bleach_ (maximum saturation, largest hue drift) to _extra bleach_ (maximum hue and skin fidelity, most muted, most "film-like"). The crucial asymmetry: **hue drift is recoverable** -- the _color preservation_ slider restores it exactly -- but **lost saturation is not**; nothing downstream puts back chroma the transform bled away. So picking a variant is mostly deciding *how much saturation to keep versus how hue-stable and film-like* the render should be, knowing you can always pull hue back with the slider.

#### What the numbers mean

Beyond plain saturation and hue, two perceptual measures are used to fit and describe the variants:

- **$\Delta E$ (color-shift distance)** -- one number for "how far did this color move", folding chroma loss and hue drift together. It is **not** the CIE 1976 or CIE 2000 $\Delta E$: those are built for small differences between reflective print colors and misbehave on the very bright, very saturated colors a tone mapper pushes around. Ours is measured in filmic's own perceptual working space and is *chroma-weighted* -- a hue error on a near-grey color barely counts, while the same error on a vivid color counts fully, the way the eye actually weighs it. Read it as **$0$ = color unchanged, $\approx 1$ = fully bleached to grey.**
- **H-K apparent-brightness drift** -- the Helmholtz--Kohlrausch effect: a saturated color looks *brighter* than a grey of the same luminance (strongest for blue, red and magenta). Per-channel tone mapping changes this "extra glow" unevenly from hue to hue, which is what makes some renders look garish. "H-K drift" measures how much a variant shifts that glow relative to the original scene; **near $0$ means the render keeps the scene's natural brightness balance between colors** -- no hue popping out or sinking relative to its neighbours.

#### Measured behaviour

Measured over a human skin-tone database and a circle of diffuse "memory" colors (foliage, sky, skin, products) swept across the exposures a photographer would give them. **Saturation drift** is the fraction of chroma lost; **hue drift** is the raw rotation *before* the _color preservation_ slider is applied; averages and maxima are over the whole set.

_Skin tones_

{{< table >}}
| variant | saturation drift (avg / max) | hue drift ° (avg / max) | $\Delta E$ (avg / max) | H-K drift (avg / max) |
|---|---|---|---|---|
| no bleach | 0.0% / 0.0% | 10.5 / 15.4 | 0.18 / 0.27 | +0.030 / +0.081 |
| low bleach | 0.0% / 0.0% | 7.8 / 11.8 | 0.14 / 0.21 | +0.028 / +0.079 |
| medium bleach | 0.0% / 0.5% | 5.3 / 8.7 | 0.09 / 0.15 | +0.026 / +0.076 |
| high bleach | 0.1% / 3.9% | 2.8 / 5.8 | 0.05 / 0.10 | +0.023 / +0.071 |
| extra bleach | 1.0% / 7.1% | 1.1 / 3.4 | 0.02 / 0.08 | +0.021 / +0.064 |
{{< /table >}}

_Reflective colors_

{{< table >}}
| variant | saturation drift (avg / max) | hue drift ° (avg / max) | $\Delta E$ (avg / max) | H-K drift (avg / max) |
|---|---|---|---|---|
| no bleach | 5.0% / 58.9% | 5.0 / 23.1 | 0.12 / 0.61 | +0.031 / −0.260 |
| low bleach | 6.2% / 54.8% | 3.9 / 19.7 | 0.11 / 0.56 | +0.027 / −0.244 |
| medium bleach | 7.6% / 56.6% | 2.9 / 18.9 | 0.11 / 0.57 | +0.023 / −0.245 |
| high bleach | 8.9% / 58.9% | 2.1 / 18.3 | 0.11 / 0.60 | +0.020 / −0.248 |
| extra bleach | 10.1% / 62.1% | 1.7 / 17.4 | 0.12 / 0.65 | +0.016 / −0.255 |
{{< /table >}}

The single trade-off is visible across every column: from _no_ to _extra bleach_, hue drift and $\Delta E$ fall while saturation drift rises. The high reflective **max** saturation drift (55--62 %) in every variant is the intended bleaching of near-clipping bright colors -- flames, LEDs, speculars -- the effect you chose AgX for; it is roughly the same in all five. Skin is protected in every variant (≤ 1 % average drift), so **no variant whitens skin** the way raw AgX does. Note that skin hue drift is *large* in _no bleach_ (10.5°) and small in _extra bleach_ (1.1°): _no bleach_ spends hue accuracy -- the recoverable quantity -- to protect skin *chroma*, and you buy the hue back with the slider.

#### Per-hue behaviour

For twelve reference hues, the raw hue drift (degrees, before the slider) and the rendered chroma (saturation of the output). Every column is **monotone** from _no_ to _extra bleach_: choosing a stronger variant moves every hue the same way, so the five are consistent renderings of one look, not five different looks.

_Signed hue drift (°), before the slider_

{{< table >}}
| hue | no | low | medium | high | extra |
|---|---|---|---|---|---|
| red | 5.5 | 3.8 | 2.3 | 0.7 | −0.8 |
| red-orange | 6.1 | 4.4 | 2.8 | 1.3 | −0.2 |
| orange | 4.5 | 3.5 | 2.6 | 1.7 | 0.7 |
| yellow-green | 1.2 | 0.9 | 0.6 | 0.3 | −0.1 |
| green | 3.2 | 2.3 | 1.5 | 0.7 | 0.0 |
| cyan | 7.8 | 5.4 | 3.1 | 0.7 | −1.5 |
| cyan-blue | 6.1 | 4.1 | 2.0 | −0.2 | −2.4 |
| blue | 1.5 | 1.4 | 1.2 | 1.0 | 0.8 |
| blue-magenta | −1.1 | −0.7 | −0.4 | −0.2 | −0.1 |
| magenta | 2.4 | 1.6 | 1.0 | 0.4 | −0.1 |
{{< /table >}}

_Rendered chroma (decreases as bleach increases)_

{{< table >}}
| hue | no | low | medium | high | extra |
|---|---|---|---|---|---|
| red | 0.163 | 0.158 | 0.154 | 0.148 | 0.143 |
| red-orange | 0.156 | 0.147 | 0.139 | 0.132 | 0.126 |
| orange | 0.212 | 0.199 | 0.187 | 0.175 | 0.165 |
| yellow-green | 0.277 | 0.263 | 0.251 | 0.239 | 0.226 |
| green | 0.159 | 0.153 | 0.148 | 0.143 | 0.137 |
| cyan | 0.126 | 0.121 | 0.116 | 0.111 | 0.106 |
| cyan-blue | 0.228 | 0.223 | 0.216 | 0.208 | 0.199 |
| blue | 0.246 | 0.242 | 0.237 | 0.230 | 0.223 |
| blue-magenta | 0.275 | 0.272 | 0.269 | 0.267 | 0.264 |
| magenta | 0.343 | 0.340 | 0.338 | 0.337 | 0.336 |
{{< /table >}}

#### How each variant was made, and its strengths and flaws

**None of these is "correct" and none is best in every situation.** They are deliberate compromises on a trade-off with no free lunch; the fitting favoured a different priority for each end and interpolated the middle:

- **_no bleach_ -- keep every bit of color, fix hue later.** Fitted to lose the *least* chroma and $\Delta E$ possible, protecting saturation absolutely (skin 0 %, reflective 5 % avg). *Strength:* the most vivid, punchy render; nothing is washed out. *Flaw:* the largest hue drift (skin 10.5°, reds swing orange), which you must correct with the _color preservation_ slider. *Choose it* when losing saturation is the worst outcome and you don't mind using the slider.
- **_extra bleach_ -- maximum hue and skin fidelity, film wash-out.** Fitted to minimize hue drift and skin $\Delta E$ and to hold every hue's *apparent brightness* steady, spending chroma to do it. *Strength:* hue-accurate and calm even with the slider at −100 % (the pure "film" character); reds/magentas never read self-luminous. *Flaw:* the most muted colors, and the loss is permanent. *Choose it* for the film-like highlight roll-off as a look, or when hue accuracy without touching the slider matters most.
- **_low_, _medium_, _high bleach_ -- the interpolated middle.** Each is built as the perceptual **midpoint** of two neighbours (so apparent brightness, hue *and* saturation all step evenly): _medium_ bisects _no_ and _extra_, _low_ bisects _no_ and _medium_, _high_ bisects _medium_ and _extra_. *Strength:* a smooth, even ramp -- pick the point on the saturation-versus-fidelity line you like and every color follows consistently. *Flaw:* none is a specialist; each is a compromise by construction. _medium_ is the neutral all-rounder; _low_ leans vivid; _high_ leans film-like.

#### Choosing a variant

- **Losing saturation is the worst outcome** (vivid subjects, sunsets, product shots, colorful fashion) → _no_ or _low bleach_, and pull hue back with the slider.
- **Hue accuracy without touching the slider matters most** (skin-critical portraits, mixed lighting, neutral reproduction) → _high_ or _extra bleach_.
- **You want the pronounced "film" highlight wash-out as a look** → _extra bleach_.
- **Unsure / general-purpose** → _medium bleach_, the neutral middle (the shipped default is _low bleach_).
- Remember the asymmetry: any variant's hue drift is reversible with _color preservation_; its saturation loss is not. When in doubt, err toward *less* bleach.

### Caveats

#### Color artifacts

As filmic v6 (then v7) is so far the best version to retain saturated colors at constant hue, it gets also much less forgiving to __invalid__ colors like chromatic aberrations and clipped magenta highlights, that are much better hidden (albeit __not solved__) by simple curves applied on individual channels (no chrominance preservation) with no care given to their ratios.

It is not the purpose of a tone mapping and gamut mapping operators to reconstruct damaged signals, and these flaws need to be corrected earlier in the pipeline with the specialized modules provided. However, there is a mechanism in filmic v6 that ensures that any color brighter than the _white relative exposure_ degrades to pure white, so a quick workaround is to simply set the _white relative exposure_ to a value slightly lower than the exposure of the clipped parts. In other words: if it is clipped at the input, let it be clipped at the output. Chrominance preservation options that work the best for this purpose are the _luminance_ and _euclidean_ norms, or simply _none_.

#### Inconsistent output

With filmic v6, if you export the same image to sRGB and Adobe RGB color spaces, and then compare both images side by side on a large-gamut screen (that can cover Adobe RGB), the sRGB export _should_ have more desaturated highlights than the Adobe RGB version. Since the sRGB color space is shorter than Adobe RGB, its gamut boundary is closer to the neutral grey axis, and therefore the maximum allowed chroma is lower for any given luminance. This is by no means a bug but rather is proof that the gamut mapping is actually doing its job.

## Emulating darktable AgX in Ansel

The darktable _AgX_ module packs 33 parameters into a single module: a tone curve, a channel mixer applied before and after it, an ASC CDL color grading stage ("look"), a gamut compression, and exposure heuristics. This is a pipeline within the pipeline, and it contradicts Ansel's design: one module, one job, so that every job can benefit from masking, blending and multiple instances. Everything AgX does is available in Ansel through dedicated modules -- usually with better color science, and always with more control. Here is the mapping:

tone curve, white/black relative exposure, pivot, contrast
: _filmic_ itself, [_scene_](#scene) and [_look_](#look) tabs. AgX's pivot corresponds to middle-gray, its "curve y gamma" to filmic's hardness (auto-computed), its toe/shoulder powers to the _contrast in shadows/highlights_ presets combined with the _shadows_/_highlights_ node sliders. Set the color science to _v8 (AgX)_ for the per-channel rendering.

per-channel bleaching and hue drift ("primaries inset/rotation")
: Built into the _v8_ color science with derived constants; the _color preservation_ slider scales the strength (negative half) or recovers the original colors (positive half). If you want *creative* control over primaries beyond that -- what AgX's twelve inset/outset/rotation sliders attempt -- use [_color calibration_](./color-calibration.md) in its **primaries** GUI mode, placed before _filmic_ in the pipeline. It is mathematically the same operation (a 3×3 matrix on RGB), presented with the same primaries-style controls, and it supports masks and multiple instances, which AgX's built-in version does not.

selectively bleaching a region of the chromaticity plane
: What AgX's inset does globally, _color calibration_'s **simple** GUI mode does surgically: rotate the chroma axes onto the hue you need, compress the U or V axis, and use the _achromatic coupling_ to remap a chosen hue toward the achromatic axis -- desaturating and brightening it at once. This recovers overwhelming stage lights or brings saturated highlights back into gamut with far more precision than a global primaries compression.

the "look" block (slope / offset / power / saturation)
: [_color balance_](./color-balance.md), which implements the full ASC CDL in a proper perceptual space, with per-range (shadows/mid-tones/highlights) controls, masks and instances -- AgX's look block is a reduced copy of it computed in a worse space.

the baked-in warm shift
: AgX skews brights toward yellow by construction; Ansel's _v8_ is neutral by design. To add warmth deliberately: a white-balance nudge in _color calibration_ (chromatic adaptation), or a per-range shift in _color balance_, or -- for the mixed-lighting look where highlights warm up while shadows stay cool -- the [_split-toning_](./split-toning.md) module, which applies two chromatic adaptations weighted by luminance. The point: the warm shift becomes an explicit, adjustable, maskable decision instead of an unlabeled constant.

hue-specific adjustments
: For color shifts confined to the saturated vertices of the gamut (deepening blues without touching neutrals, taming oranges), use [_color primaries_](./color-primaries.md); for hue-wise shifts driven by tonal range, use the [_color equalizer_](./color-equalizer.md). Both blend in RGB and preserve gradients.

gamut compression of out-of-gamut input
: Handled inside _v8_ (negatives compression, generalized to the working profile) plus filmic's gamut mapping to the export profile -- which AgX lacks entirely. For difficult cases (deep blue LEDs), prefer fixing the input with _color calibration_'s gamut compression, which is where the problem actually lives.

The workflow difference is philosophical: AgX invites you to fix color inside the tone mapper, at the end of the pipeline, with controls that cannot be masked and whose interactions are opaque. Ansel's approach is divide and conquer -- calibrate color first (_color calibration_), grade it (_color balance_, _split-toning_, _color equalizer_, _color primaries_), then let _filmic_ do one job: compress the dynamic range, with the _v8_ color science reproducing the per-channel rendering AgX is known for, minus its hard-coded look. The same results are reachable step by step, and each step is inspectable, maskable and reversible on its own.
---

# Filmstrip : https://ansel.photos/en/doc/views/toolboxes/filmstrip/

The filmstrip can be used to quickly switch between images. The images shown are the same as those displayed in the lighttable view and are defined by the currently-selected collection.

![filmstrip](filmstrip.jpg)

The filmstrip can be switched on and off using the shortcut Ctrl+F. The height of the filmstrip panel can be changed by clicking and dragging its top border.

Quickly navigate through the images in the filmstrip by scrolling with the mouse. In the darkroom you can change the photo currently being processed by clicking on another image in the filmstrip.

In the darkroom, the image currently being processed is selected and highlighted.
---

# Find location : https://ansel.photos/en/doc/views/toolboxes/find-location/

Search for a location on the map. You must be connected to the internet to use this feature.

To use this module, type in a place name or address, press Enter and a list of results will be shown. Click on an item in the list and the map will zoom to that location. An outline covering that location or a pin pointing at the location will be displayed.

An outline (polygon) can be used to create a user location. Check the _max polygon points_ parameter in the [map settings](./map-settings.md) module to ensure that sufficient points are available for a polygon to be displayed.
---

# Focus peaking : https://ansel.photos/en/doc/views/toolboxes/focus-peaking/

Identify which parts of the image contain high-contrast details, like edges and textures, which usually indicates that those areas are in focus.

Enable focus peaking from the [bottom toolbar](../darkroom/darkroom-view-layout.md#bottom-panel) in the darkroom, or via _Overlay focus peaking_ in the [lighttable display options](../lighttable/_index.md#overlays). The sharp parts of the image are highlighted with a yellow, green and blue overlay:

Focus peaking works by filtering out most of the image noise, measuring the intensity gradients in the image and calculating average and standard deviation statistics. When the gradient of an edge differs significantly from the mean, the associated pixels are marked with a "heat map" indicating how sharp the edge is.

- _yellow_ represents a large (6σ) jump in gradient, indicating a very sharp edge.
- _green_ represents a medium (4σ) jump in gradient, indicating a reasonably sharp edge.
- _blue_ represents a small (2σ) jump in gradient, indicating a slightly sharp edge.

![focus peaking overlaid on a shallow depth-of-field image](focus-peaking-overview.jpg)

On an image shot with a wide aperture and shallow depth of field, the overlay clusters on the plane the lens focused on (and on any other details that happen to fall within the zone of acceptable sharpness around it), which makes it easy to see where focus actually landed.

{{< note >}}
The focus peaking detects local contrast and uses it as an estimation of sharpness. This is the only known method to do so. While local contrast and sharpness are fairly correlated most of the time, edges between two objects having very different luminances (bright sky and tree leaves) will have a very high local contrast even when blurry, which will make them detected as sharp by focus peaking. A mechanism is in place to mitigate this issue in most cases, but very high-contrast edges will still be detected as sharp no matter their actual sharpness.
{{</ note >}}

{{< warning >}}
The focus peaking does __not__ detect __if__ the images are sharp, but __where__ the sharpest region of the image is, which is likely to be on the focal plane. This can help to spot front/back-focus, if any. But there is no guaranty that the most sharp region of the picture is actually sharp by some metric of definition of sharpness.
{{</ warning >}}

{{< warning >}}
Downscaled images have their gradients affected by interpolation methods, where noise is made less visible and local contrast is lost around small details, making focus peaking on small thumbnails inaccurate.
{{</ warning >}}

{{< warning >}}
The focus peaking is runs the detection on the final image as it appears in the graphic interface, after all processing steps if any. It will therefore take into account all the blurring and sharpening, as well as contrast enhancements, that may have happened in the processing.
{{</ warning >}}
---

# Framing : https://ansel.photos/en/doc/views/darkroom/modules/framing/

Generate a frame around the image.

The frame consists of a border (with a user-defined color) and a frame line within that border (with a second user-defined color). Various options are available to control the geometry and color of the frame.

## Module controls

border size
: The size of the frame as a percentage of the underlying full image.

aspect
: The aspect ratio of the final module output (i.e. the underlying image plus the frame)

orientation
: The orientation of the frame (portrait/landscape). Select 'auto' for Ansel to choose the most reasonable orientation based on the underlying image.

horizontal/vertical position
: Select from a set of pre-defined ratios to control where the underlying image will be positioned on the horizontal/vertical axis. You can also right click and enter your own ratio as "x/y".

frame line size
: The percentage of the frame line size, relative to the border size at its smallest part.

frame line offset
: The position of the frame line, relative to the underlying image. Choose 0% for a frame line that touches the image. Choose 100% for a frame line that touches the outer border.

border color / frame line color
: A pair of color selectors which allow the border and frame line colors to be defined. Clicking on the colored field will open a color selector dialog which offers a choice of commonly-used colors, or allows you to define a color in RGB color space. You can also activate a color picker to take a color probe from the image.

show guides
: Tick the box to show guide overlays whenever the module is activated. Click the icon on the right to control the properties of the guides. See [guides & overlays](../../toolboxes/guides-overlays.md) for details.
---

# Gamut check : https://ansel.photos/en/doc/views/toolboxes/gamut/

Highlight areas of the image that may exhibit gamut clipping.

Click the gamut-check icon in the [bottom toolbar](../darkroom/darkroom-view-layout.md#bottom-panel) to enable it. Gamut check and [soft proof](./soft-proof.md) are mutually exclusive modes.

Right-click on the icon to open a dialog with configuration parameters -- these are the same as for the [soft proof](./soft-proof.md) option.

You might also like to consider using the [clipping warning](./clipping.md), which also provides under- and over-exposure warnings as well as a gamut check similar to that offered by this module.
---

# Geotagging : https://ansel.photos/en/doc/views/toolboxes/geotagging/

Import and apply GPX track data to a selection of images, and correct image date/time.

This module — labelled **Datetime and GPS** in the panel — is common to the [lighttable](../lighttable/_index.md) and [map](../map/_index.md) views. The map view provides an enhanced mode that allows you to preview the position of the images along the GPS tracks while adjusting the images' date/time offset and time zone.

## Workflow overview

A GPS receiver calculates its current position based on information it receives from satellites, and records it in a GPX file together with the current date and time. The Exif data of the images also contains a time stamp defined by the camera settings. The _geotagging_ module takes the time stamp of the image, looks up the position in the GPX file at that time, and stores the appropriate coordinates (latitude/longitude/elevation) in its database and the image's XMP sidecar file.

Two problems may occur during this process:

- In contrast to GPS devices, most cameras don't record the time accurately.
- The time stored in the Exif data does not include the time zone. Most people set their camera to local time, whereas GPS devices store the time in the UTC (Universal Time, Coordinated, i.e. Greenwich (London)) time zone. If the time zones of the camera and GPX file differ, than the related location will be wrong.

If your image already shows the correct date/time and carries the UTC time stamp, you can directly apply the GPX track file without further adjustments.

Otherwise follow the following process to correlate the time of the images and GPS track files

1. Fix the camera time setting for a single image by manually entering the correct date/time for that image into the _date/time_ field. A good way to do this accurately is to take a photograph of a reliable time source. This can be any precise clock or, even better, the time displayed on your GPS device (bearing in mind that GPS devices normally _show_ the local time, even though they _store_ universal time). The difference (offset) between the time entered and the time stored in the image's Exif data will be shown in the _date/time offset_ field.

2. Press the _lock_ button to lock the calculated offset in the module.

3. Select all of the images you wish to adjust and click _apply offset_ to apply the calculated offset to those images.

4. If the camera time zone is not UTC, set the time zone in the _camera time zone_ field.

With the time setting corrected, you can now apply GPX tracking data using the _apply GPX track file_ button.

## Module controls (common)

date/time
: This field is initialized with the date/time read from the first selected image (format `yyyy:mm:dd hh:mm:ss`) and can be modified to correct the date/time for that image. The individual date/time fields can be altered by scrolling over them with your mouse. If any field reaches its limit, the neighboring fields are automatically updated. For example, if you go over 60 on the minute field, the hour field will automatically be incremented. It is also possible to use milliseconds in this module if you enable [preferences > lighttable > show image time with milliseconds](../../preferences-settings/lighttable.md).

original date/time
: The original date/time of the image is shown here for reference.

date/time offset
: The calculated difference between the original date/time and that keyed in the _date/time_ field. If the calculated difference is greater than 99 days 23 hours 59 minutes and 59 seconds, the offset is invalidated.

lock button
: If the lock button is activated, the offset value is frozen. If you subsequently change the selected image(s), the new image date/time is updated using the locked offset. This allows you to apply the same offset to multiple groups of images.

apply offset button
: Apply the offset to selected images.

apply date/time button
: It is sometimes useful to be able to set the absolute date/time for an image, for example where this information is missing. This button allows you to apply the date/time entered in the _date/time_ field to the selected images, without considering the previous value. You can use Ctrl+Z to undo any unwanted changes.

camera time zone
: Select the camera's time zone. Start typing to show a list of permitted values.

apply GPX track file (lighttable view only)
: Apply a GPX track file. Click the corresponding button and navigate to the GPX file. You can use Ctrl+Z to undo any unwanted changes. Within the file chooser window, the preview button lists the tracks of the selected GPX file along with the following information: track name, start and end date/time (local time), number of track points and number of selected images that will be geotagged.

## Module controls (map view)

gpx file
: The path of the gpx file selected.

track list
: This table shows the start date/time of each track, along with the number of track points and the number of matching images. When a check button is activated, the related track is displayed on the map. A check button in the table header allows you to select or de-select all of the tracks at once. Hover over a row with your mouse to display the start and end times both in local time (LT) and UTC.

preview images
: If checked, the matching images are displayed on the map along the visible tracks.

select images
: If you don't wish to apply an offset to all of the selected images, but only to the matching images, use this button to select images. If you don't want to lose the current offset you may want to lock it before changing the selection.

counts
: A count of the number of matching images and the number selected is displayed to the right of the _select images_ button.

apply geo-location
: This button is displayed when the offset is null. The _apply geo-location_ button applies the GPX data to matching images on selected tracks.

apply offset and geo-location
: This button is displayed once an offset has been entered. As a reminder, the _apply offset_ button applies the offset to all selected images. Unlike _apply offset_, the _apply offset and geo-location_ button applies the offset _and the GPX data_ to matching images on selected tracks.

You can use Ctrl+Z to undo any unwanted changes (twice in case of _apply offset and geo-location_).
---

# Global color picker : https://ansel.photos/en/doc/views/toolboxes/global-color-picker/

The global color picker samples colors from the current image, displays their values in several color spaces, and lets you store and compare samples from different locations. It is part of the [scopes](scopes.md) module (the _Color picker_ and _Live samples_ sections), in the darkroom [left panel](../darkroom/darkroom-view-layout.md#left-panel).

Activate it with the color-picker button. It keeps sampling until you turn it off or leave the darkroom.

{{< note >}}
This **global** color picker is distinct from the **local** color pickers found inside many [modules](../darkroom/modules/_index.md) (e.g. [RGB curve](../darkroom/modules/curve.md)), which set that module's parameters. The two do not work in the same color space: the global picker samples at the **end of the preview pipeline** (display space), whereas a local picker reads the input or output of the module it belongs to.
{{< /note >}}

## Controls

Point / area mode
: **Click** the picker button for point mode (a small spot under the cursor). **<kbd>Ctrl</kbd>+click** or **right-click** for area mode (the average over a drawn rectangle).

Statistic (mean / min / max)
: In area mode, choose whether to display the **mean**, **minimum** or **maximum** channel values of the sampled region. (In point mode the three are identical.)

Color mode
: Choose the color space in which the sampled values are shown. Values converted to spaces other than the native display space are approximate.

Color swatch
: A patch of the sampled color is shown next to the numerical values; click it to enlarge it. Right-click the numerical values to copy them to the clipboard.

Hover any value for a tooltip with more detail, including RGB and Lab values and an approximate color name.

## Live samples

Press **add** to store the current sample as a _live sample_. Each live sample keeps its own swatch and values (with its own statistic and color mode).

- Live samples are **not locked** by default: they update as you change the edit, which lets you watch how a parameter affects different parts of the image. Click a live sample's swatch to **lock** it (a lock icon appears) so later changes no longer affect it — handy for before/after comparisons.
- Hover a live sample's **delete** button to highlight its region in the preview.

Display samples on image
: Show the live-sample locations on the image and on the [vectorscope](scopes.md#vectorscope).

Restrict scope to selection
: Restrict the [scopes](scopes.md) to only the values inside the picked point/area, to inspect the tones present in that region. When a local color picker is active in a module, this restricts the scope to that module's picked area instead.
---

# Graduated density : https://ansel.photos/en/doc/views/darkroom/modules/graduated-density/

Simulate a graduated density filter in order to correct exposure and color in a progressive manner.

A line is shown on screen allowing the position and rotation of the gradient to be modified with the mouse.

This module is known to provoke banding artifacts under certain conditions. You should consider activating the [_dithering_](./dithering.md) module to alleviate these issues.

## Module controls

density
: Set the density of the filter (EV). A low value underexposes slightly whereas a high value creates a strong filter.

hardness
: The progressiveness of the gradient. A low value creates a smooth transition, whereas a high value makes the transition more abrupt.

rotation
: The rotation of the filter. Negative values rotate clockwise. The rotation can also be set by dragging the end of the gradient line with the mouse.

hue
: Choose a hue to add a color cast to the gradient.

saturation
: The saturation of the color cast to add to the gradient (defaults to a neutral color cast of 0)
---

# Grain : https://ansel.photos/en/doc/views/darkroom/modules/grain/

Simulate the grain of analog film. The grain is processed on the L channel of Lab color space.

## Module controls

coarseness
: The grain size, scaled to simulate an ISO number.

strength
: The strength of the effect.
---

# Guides & overlays : https://ansel.photos/en/doc/views/toolboxes/guides-overlays/

A number of commonly-used compositional guides can be overlaid on your image while you are editing. These can be enabled either globally (all the time) or locally (when certain modules are active).

Other darkroom functionality also draws colored overlay lines on the image (for example, drawn masks). An option is also provided to change the color of those overlays (see below).

## Global guides

 Left-click the guides-and-overlays icon in the [bottom toolbar](../darkroom/darkroom-view-layout.md#bottom-panel) to globally display guide overlays. The overlays remain on until you click the button a second time to switch them off.

 Right-click the icon to show the settings dialog (see below).

## Local guides

A more common use is to switch the guides on only when a specific module is activated. A guides control is added by default to all modules that crop or distort the image (currently [crop](../darkroom/modules/crop.md), [orientation](../darkroom/modules/orientation.md), [framing](../darkroom/modules/framing.md), [liquify](../darkroom/modules/liquify.md), [lens correction](../darkroom/modules/lens-correction.md), [retouch](../darkroom/modules/retouch.md) and [rotate and perspective](../darkroom/modules/rotate-perspective.md)).

Tick the box to show guide overlays whenever the module is active. Click the icon on the right to show the settings dialog (see below).

## Global guide overlay settings

Please note that, while you can choose to switch guide overlays on and off either globally or locally, the following settings are stored globally and cannot be set independently for each module.

type
: The type of compositional guide lines to display.

flip
: Some guides are asymmetrical. This option allows you to flip such guides horizontally/vertically.

horizontal lines, vertical lines, subdivisions
: When the _grid_ overlay type is selected, set the parameters of the grid.

overlay color
: The color of the overlay lines. Note that this impacts _any_ lines that are drawn directly over the image, for example, drawn masks.

contrast
: The contrast between the lightest and darkest parts of any overlays -- usually, the contrast between the "on" and "off" parts of dashed lines.
---

# Haze removal : https://ansel.photos/en/doc/views/darkroom/modules/haze-removal/

Automatically reduce the effect of dust and haze in the atmosphere. This module may also be employed more generally to give pictures a color boost specifically in low-contrast regions of the image.

Haze absorbs light from objects in the scene but it is also a source of diffuse background light. The haze removal module first estimates, for each image region, the amount of haze in the scene. It then removes the diffuse background light according to its local strength and recovers the original object light.

Setting both of the module's controls to unity maximizes the amount of haze removal but is also likely to produce some artifacts. Removing the atmospheric light entirely may render the image flat and result in an unnatural looking style. Optimal values are typically below unity and are rather image dependent, but also a matter of personal aesthetic preferences.

## Module controls

strength
: The amount of haze removal. At unity, the module removes 100 percent of the detected haze up to the specified distance. Negative values increase the amount of haze in the image.

distance
: Limit the distance up to which haze is removed. For small values, haze removal is restricted to the foreground of the image. Haze is removed from the entire image if the distance parameter is set to unity. If the _strength_ is negative the distance control has no effect.
---

# Highlight reconstruction : https://ansel.photos/en/doc/views/darkroom/modules/highlight-reconstruction/

Attempt to reconstruct color information for pixels that are clipped in one or more RGB channel.

If these pixels are left partially clipped it can result in unrealistic colors appearing in the image. For example pixels with both green and blue channels clipped will appear to be red without any highlight reconstruction.

Three methods of reconstruction are available:

clip highlights
: Clamp all pixels to the white level (i.e. clip the remaining color channels). This method is most useful in cases where clipped highlights occur in naturally desaturated objects (e.g. clouds).

reconstruct in LCh
: Analyse each pixel with at least one clipped channel and attempt to correct the clipped pixel (in LCh color space) using the values of the other (3 for Bayer or 8 for X-Trans) pixels in the affected sensor block. The reconstructed highlights will still be monochrome, but brighter and with more detail than with “clip highlights”. This method works fairly well with a high-contrast base curve, which renders highlights desaturated. As with _clip highlights_ this method is a good option for naturally desaturated objects.

reconstruct color
: Use an algorithm that transfers color information from unclipped surroundings into the clipped highlights. This method works very well on areas with homogeneous colors and is especially useful on skin tones with smoothly fading highlights. Please note that this method can produce maze-like artifacts on highlights behind high-contrast edges, for example well-exposed fine structures in front of an overexposed background.

guided laplacians
: Use an algorithm (derived from the [_diffuse or sharpen_](./diffuse.md) module) to replicate details from valid channels into clipped channels and to propagate color gradients from valid surrounding regions into clipped regions. This is a slow and computationally-intensive method designed for maximum smoothness and seamless blending of the reconstructed regions into their neighborhood, and is designed primarily to reconstruct spotlights and specular reflections. This mode is available for Bayer sensors only.

---

**Note:** When using the highlight reconstruction included with the [_filmic_](./filmic.md) module it may be useful to avoid using this module in _clip highlights_ mode (so that _filmic_ has more information to work with).

---

## Module controls

method
: The method used to reconstruct highlights (see above).

clipping threshold
: Manually adjust the clipping threshold against magenta highlights. You shouldn't need to adjust this.

: Click the icon beside the slider to visualise what areas of the image are considered clipped (the clipping mask). If the clipping mask does not match the [RAW over-exposed warning](../../toolboxes/raw-overexposed.md), you may need to correct this value.

noise level (_guided laplacians_ mode only)
: Add Poisson noise (natural photon noise such as you would find in sensor readings) to the clipped regions. For high-ISO images, the valid regions of the image will be noisy, but the reconstructed clipped areas will be smooth, which may look odd. Adding some noise in the reconstruction helps to visually blend the result with the rest of the image.

iterations (_guided laplacians_ mode only)
: The _guided laplacians_ mode is an iterative process that extrapolates gradients and details from the neighborhood. Each new iteration refines the previous reconstruction but adds more computations that will make the module slower. The default (1 iteration) will typically not be sufficient to completely recover magenta highlights, so you will need to increase this parameter gradually but carefully, to manage the speed/quality trade-off.

inpaint a flat color (_guided laplacians_ mode only)
: Inpainting a flat color is an algorithmic trick that may help recover magenta highlights in difficult cases (large blown areas) by smoothing RGB ratios. It can be seen as a "reconstruction booster" that may reduce the number of iterations required to entirely remove magenta in clipped highlights. However, this also makes the reconstruction less accurate and can lead to non-smooth reconstructed edges and unrelated colors being inpainted (e.g. blue sky or green leaves bleeding into white clouds). Use this setting with caution.

diameter of the reconstruction (_guided laplacians_ mode only)
: The _guided laplacians_ mode uses a multi-scale algorithm that tries to recover details from each scale independently. The _diameter of the reconstruction_ is the largest scale used by the algorithm. Large scales will increase memory consumption as well as runtimes, and may also cause unrelated colors or details to be inpainted in clipped regions. You are advised to use a diameter roughly twice as large as the largest clipped area to be reconstructed. It is also possible that a given diameter may not suit all clipped areas, in which case you should use several instances at different scales and mask the clipped areas accordingly.

## Comparison with filmic's highlight reconstruction

It is important to note that the _highlight reconstruction_ module is quite early in the pixel pipeline -- before [_input color profile_](./input-color-profile.md) and the full chromatic adaptation in [_color calibration_](./color-calibration.md) (if you use the _modern chromatic adaptation_ workflow). A common trick to solve clipped highlights is to simply desaturate them to white but, because white is not defined before the full chromatic adaptation and the input color profiling, it is not possible to use this trick here. Technically, there is no color yet at this point in the pipeline, only an arbitrary 3D signal.

The _guided laplacians_ approach has been designed specifically to be immune to white-balance discrepancies and to avoid any concept or method related to color (so there is no explicit desaturation). It only handles gradients (transitions) in the signal and aims at connecting them smoothly, in order to fill the missing parts. This process is quite heavy though, since it falls into the category of supervised machine learning (gradient-based optimization through multi-scale curvature), which is a sub-branch of artificial intelligence.

[_Filmic's highlight reconstruction_](./filmic.md#reconstruct) uses a simpler color propagation algorithm coupled with a desaturation option that can favor an _achromatic_ reconstruction. Not only does it _know_ about color (because it comes after the full color profiling and chromatic adaptation) but it also uses a simplified and faster version of the algorithm used by the _guided laplacians_ approach. Namely, this variant will not try as hard to restore details and will favor a smooth blur instead.

The filmic reconstruction is good enough for very large clipped patches and offers the benefit of being able to degrade to white as a last resort. It is also better and faster to inpaint solid color into clipped areas, at the expense of details. Its main drawback is that it is not as selective in the source of the colors being inpainted in clipped parts, so it may inpaint unrelated colors.

All in all, you are advised to use the _guided laplacians_ highlight reconstruction mode to:

1. Smooth the boundaries of clipped areas,
2. Recover spotlights and clipped areas of diameter below approximately 256px (on the full-resolution RAW),
3. Remove chromatic aberrations, which can occur during [demosaicing](./demosaic.md) (the next module in the pipeline) at the boundary between clipped and valid regions.

If you find yourself having to increase the _diameter of reconstruction_ past 512px to get a full recovery from magenta, the best approach is usually to cap the diameter to 512px, do the most you can with this setting, and then enable filmic's highlight reconstruction to finish the work. This will give more bearable run-times with a very similar result.
---

# History stack : https://ansel.photos/en/doc/views/toolboxes/history-stack/

The **History of changes** module, in the darkroom [left panel](../darkroom/darkroom-view-layout.md#left-panel), shows and lets you navigate the [history stack](../darkroom/pixelpipe/history-stack.md) of the current image.

It lists every change of state — a module being enabled, disabled, moved, or having a parameter changed — for the current image, newest at the top. Each entry is one user change, so a module can appear several times.

## Navigating the history

- **Click** an entry to return the image to that point in its history.
- **<kbd>Shift</kbd>+click** an entry to expand the corresponding module in the right-hand panel **without** changing the current edit — useful for inspecting a step.
- **Hover** an entry for a tooltip listing exactly what changed in that step compared to the previous one, which helps track down unintended adjustments.

The selected history state is remembered: it is safe to quit, leave the darkroom or switch image after stepping back, and you will return to the same state.

{{< warning >}}
If you select an earlier entry and then make a new change, every step **above** the selected one is discarded. It is easy to lose work this way — <kbd>Ctrl</kbd>+<kbd>Z</kbd> can usually undo it.
{{< /warning >}}

## Compressing, resetting and reusing

These operations now live in the [global menu](../global-menu.md), so they work on the whole selection and not just the open image:

- **Compress history** (_Edit → Compress history_) collapses the stack to the shortest sequence reproducing the current image.
- **Delete history** (_Edit → Delete history_) discards the stack and resets the image.
- **Create a style** (_Styles → Create new style…_) turns the current history into a reusable [style](styles.md).
- **Copy / paste history** (_Edit → Copy history / Paste history_) transfers an edit to other images. See [copy and paste history](../lighttable/history-copy-paste.md) for how the merge works.
---

# Hot pixels : https://ansel.photos/en/doc/views/darkroom/modules/hot-pixels/

Automatically detect and eliminate hot pixels.

Hot pixels are pixels which have failed to record a light level correctly. Detected hot pixels are replaced by an average of their neighbors.

## Module controls

threshold
: How strong a pixel's value needs to deviate from that of its neighbors to be regarded as a hot pixel.

strength
: The blending strength of the hot pixels with their surrounding.

detect by 3 neighbours
: Extend the detection of hot pixels -- regard a pixel as hot if a minimum of only three (instead of four) neighbor pixels deviate by more than the threshold level.

mark fixed pixels
: Visually mark the corrected pixels on the image and display a count of hot pixels that have been fixed.
---

# Input color profile : https://ansel.photos/en/doc/views/darkroom/modules/input-color-profile/

Define how Ansel will interpret the colors of the image.

This module takes the color space used by the image source (e.g. camera, scanner) and converts the pixel encodings to a standardized working color space. This means that subsequent modules in the pipeline don't need to be concerned with the specifics of the input device, and can work with and convert to/from a common working color space.

Where an image has been captured in a raw file, the input color profile module will normally apply either a standard or enhanced color matrix specific for that camera model, which will be used to map the colors into the working profile color space. If color space information is embedded in the image, the _input color profile_ module will use this information when mapping the colors to the working profile color space. The user can also explicitly specify a color space for the incoming image, and can even supply a custom ICC color profile specifically made for the input device.

As part of the mapping from the input color space to the working profile space, the colors can be confined to a certain gamut using the _gamut clipping_ options, which can help to mitigate some (infrequent) color artifacts. This is also influenced by the chosen [rendering intent](../../../color-management/rendering-intent.md).

Note that the final color profile that will be used when exporting the image is controlled by the [_output color profile_](output-color-profile.md) module.

## Module controls

input profile
: The profile or color matrix to apply. A number of matrices are provided along with an enhanced color matrix for some camera models. The enhanced matrices are designed to provide a look that is closer to that of the camera manufacturer.

: You can also supply your own input ICC profiles and put them into `$Ansel/share/Ansel/color/in` or `$HOME/.config/Ansel/color/in` (where `$Ansel` is the Ansel installation directory and `$HOME` is your home directory). One common source of ICC profiles is the software that is shipped with your camera, which often contains profiles specific to your camera model. You may need to activate the [_unbreak input profile_](./unbreak-input-profile.md) module to use your own profiles.

: If your input image is a low dynamic range file like JPEG, or a raw file in DNG format, it might already contain an embedded ICC profile, which Ansel will use by default. You can restore this default by selecting “embedded icc profile”.

working profile
: The working profile used by Ansel's processing modules. Each module can specify an alternative space that it will work in, and this will trigger a conversion. By default Ansel will use "linear Rec2020 RGB", which is a good choice in most cases.

gamut clipping
: Activate a color clipping mechanism. In most cases you can leave this control in its default “off” state. However, if your image shows some specific features such as highly saturated blue light sources, gamut clipping might be useful to avoid black pixel artifacts. See [possible color artifacts](../../../color-management/color-artifacts.md) for more information.

: Choose from a list of RGB profiles. Input colors with a saturation that exceeds the permissible range of the selected profile are automatically clipped to a maximum value. “linear Rec2020 RGB” and “Adobe RGB (compatible)” allow for a broader range of unclipped colors, while “sRGB” and “linear Rec709 RGB” produce a tighter clipping. Select the profile that prevents artifacts while still maintaining the highest color dynamics.
---

# Lens correction : https://ansel.photos/en/doc/views/darkroom/modules/lens-correction/

Automatically correct for (and simulate) lens distortion, transversal chromatic aberrations (TCA) and vignetting.

This module identifies the camera/lens combination from the image's Exif data and uses the external [lensfun library](https://lensfun.github.io/) to provide correction parameters.

If your system's lensfun library has no correction profile for the automatically identified camera/lens combination, the controls for the three photometric parameters (below) are replaced with a warning message. You may try to find the right profile yourself by searching for it in the menu.

If your lens is present in the list but has not been correctly identified, this may require some adjustment within the exiv2 program (see [this post](https://dev.exiv2.org/boards/3/topics/2854) for details). Note that you may need to re-import the images once such adjustments have been made as the lens name is retrieved as part of the import process.

By default, only the lenses that are directly compatible with your camera's mount are listed and automatically identified. If you are using lenses for a different mount with an adapter (for example a Four Thirds lens adapted to a Micro Four Thirds body), then you must run the [`lensfun-add-adapter`](https://lensfun.github.io/manual/v0.3.2/lensfun-add-adapter.html) tool to enable those lenses.

If you can't find your lens, check if it is in the list of [currently supported lenses](https://lensfun.github.io/lenslist/), and try running the [`lensfun-update-data`](https://lensfun.github.io/manual/v0.3.2/lensfun-update-data.html) tool. If there is still no matching profile for your lens, a [lens calibration service](https://www.darktable.org/2013/07/have-your-lens-calibrated/) is offered by Torsten Bronger, one of darktable's users. Alternatively you may visit the [lensfun project](https://lensfun.github.io/lenslist/) to learn how to generate your own set of correction parameters. Don't forget to share your profile with the lensfun team!

## Module controls

camera
: The camera make and model as determined by the image's Exif data. You can override this manually and select your camera from a hierarchical menu. Only lenses with correction profiles matching the selected camera will be shown.

lens
: The lens make and model as determined by the image's Exif data. You can override this manually and select your lens from a hierarchical menu. This is mainly required for pure mechanical lenses, but may also be needed for off-brand / third party lenses.

photometric parameters (focal length, aperture, focal distance)
: Lens corrections depend on certain photometric parameters that are read from the image's Exif data: focal length (for distortion, TCA, vignetting), aperture (for TCA, vignetting) and focal distance (for vignetting). Many cameras do not record focal distance in their Exif data, in which case you will need to set this manually.

: You can manually override all automatically selected parameters. Either take one of the predefined values from the drop-down menu or, with the drop-down menu still open, just type in your own value.

corrections
: Choose which corrections (distortion, TCA, vignetting) Ansel should apply. Change this from its default "all", if your camera has already performed some internal corrections (e.g. vignetting), or if you plan to undertake some corrections with a separate program.

geometry
: In addition to correcting lens flaws, this module can change the projection type of your image. Set this combobox to the desired projection type (e.g. "rectilinear", "fish-eye", "panoramic", "equirectangular", "orthographic", "stereographic", "equisolid angle", "thoby fish-eye").  To correct the aspect ratio of an anamorphic lens, use the [_rotate and perspective_](./rotate-perspective.md) module.

scale
: Adjust the scaling factor of your image to avoid black corners. Press the auto scale button (to the right of the slider) for Ansel to automatically find the best fit.

mode
: The default behavior of this module is to _correct_ lens flaws. Switch this combobox to "distort" in order to instead _simulate_ the flaws/distortions of a specific lens (inverted effect).

TCA overwrite
: Check this box to override the automatic correction parameters for TCA. This will expose the TCA red and TCA blue parameters below. Un-check the box to revert back to automatic corrections.

TCA red; TCA blue
: Override the correction parameters for TCA. You can also use these sliders to manually set the parameters if the lens profile does not include TCA correction. Look out for colored seams at features with high contrast edges and adjust the TCA parameters to minimize those seams.

corrections done
: Occasionally, for a given camera/lens combination, only some of the possible corrections are supported by lensfun's profiles. This message box will tell you which corrections have actually been applied to the image.

---

**Note:** TCA corrections will not be applied to images that have been identified as monochrome by their metadata, like the files produced by the Leica M10 Monochrom.

**Note:** The lens correction module will fill in missing data at the borders by repeating the borders' pixels. For strong corrections, this filling can be visible (especially on noisy images). Crop the image if necessary.

---

show guides
: Tick the box to show guide overlays whenever the module is activated. Click the icon on the right to control the properties of the guides. See [guides & overlays](../../toolboxes/guides-overlays.md) for details.
---

# Levels : https://ansel.photos/en/doc/views/darkroom/modules/levels/

Adjust black, white and mid-gray points in RGB color space.

The rgb levels tool shows a histogram of the image, and displays three bars with handles. Drag the handles to modify the black, middle-gray and white points in lightness (in "RGB, linked channels" mode) or independently for each of the R, G and B channels (in "RGB, independent channels" mode).

Moving the black and white bars to match the left and right borders of the histogram will make the output image span the full available tonal range. This will increase the image's contrast.

Moving the middle bar will modify the mid-tones. Move it to the left to make the image look brighter and move it to the right to make it darker. This is often referred to as changing the image's gamma.

Three color pickers are available for sampling the black, white and gray points from the image.

---

**Note:** Under certain conditions, especially with highly saturated blue light sources, the _levels_ module may produce black pixel artifacts. See the "gamut clipping" option of the [_input color profile_](./input-color-profile.md) module for information about how to mitigate this issue.

---

## Module controls

mode
: The mode of operation. "RGB, linked channels" (default) provides a single levels tool which updates all channels, taking into account the selected color preservation method (see "preserve colors" below). "RGB, independent channels" provides separate levels controls for each of the R, G and B channels.

auto
: Auto-adjust the black and white point and put the gray point exactly in the mean between them. Use the color picker to auto-adjust based on a selected region of the image.

preserve colors
: Choose a color preservation method when using "RGB, linked channels" mode (default "luminance").
---

# Library : https://ansel.photos/en/doc/views/toolboxes/collections/

Filter the images shown in the lighttable view and filmstrip panel using image attributes. This set of filtered images is known as a _collection_.

Importing images into Ansel stores information about them (filename, path, Exif data, data from XMP sidecar files etc.) in Ansel's library database. A collection is defined by applying filtering rules to these attributes, thus creating a subset of images to display in the lighttable view and the filmstrip module.

The default collection is based on the _film roll_ attribute and displays all images of the last imported or selected film roll.

{{< note >}}
We carry the ~~burden~~ legacy of Darktable here, that makes a weird difference between _film rolls_ and _folders_.

When importing an image into Ansel's library, its parent folder is saved as a property of the image named _film roll_. At any given time, when we query the content of a _film roll_, we list all the pictures that Ansel knows of (imported in library) that were imported from that folder.

It means that Ansel never check the content of the actual filesystem folder after the images have been imported: it may contain more images than what the library is aware of, or the images may have been deleted or moved elsewhere on the filesystem, or the whole folder might have been deleted entirely. So Ansel _film roll_ is not directly equivalent to filesystem folder, even if it's related and linked at import time.

Now, where it gets confusing: what Darktable called _folders_ is actually just _film rolls_ but displayed as a treeview instead of a flat list. Yet _folders_ are listed as a different querying option as _film rolls_, suggesting that it's a different piece of data, though it is only the same content presented differently.

Ansel solves this in the _Folders_ tab: Darktable's _film rolls_ or _folders_ are presented as a _list_ or _treeview_ display option, and that's the end of the story. However, they remain separate objects in the (advanced) _Queries_ tab. 
{{< /note >}}

## The three tabs

The Library module is organised in three tabs, each tuned to a different way of building a collection. The tabs share a single value list (the box at the bottom that shows the available values for the active attribute, along with the image count for each value).

Folders
: Browse and manage the folders and film rolls known to Ansel. The _View_ selector at the top of this tab switches between a flat **List** of film rolls and a hierarchical **Tree** of folders. This is the tab where you relocate or remove film rolls (one at a time or in batches). It corresponds to a single _film roll_ or _folder_ rule.

Collections
: Browse and manage your tags, shown as a hierarchical tree. Besides selecting images by tag, this is where you rename or delete tags (in batches). It corresponds to a single _tag_ rule.

Queries
: Build an arbitrary collection from one or more rules, combining any of the image [attributes](#filtering-attributes) with logical operators. This tab also exposes a [raw SQL](#raw-sql-queries) escape hatch for power users.

Switching tabs only reconfigures the controls and refreshes the value list; it does not re-run the collection query. The collection is only rebuilt when you actually click a value or edit a rule.

## Filtering attributes

The images in a collection can be filtered using the following image attributes. All of them are available in the _Queries_ tab; the _Folders_ and _Collections_ tabs are pre-set to the folder/film-roll and tag attributes respectively.

### Files

film roll
: The name of the film roll to which the image belongs (which is the same as the name of the folder in which the image resides). In the _Folders_ tab, choose **List** in the _View_ selector to browse film rolls. Right-click a film roll to remove its contents from the Ansel library, or to tell Ansel that its location has changed in the file system (see [updating the folder path of moved images](#updating-the-folder-path-of-moved-images)).

folder
: The folder (file path) where the image file is located. In the _Folders_ tab, choose **Tree** in the _View_ selector to browse the folder hierarchy. Click a folder to select its images; enable the **include sub-folders** checkbox to also include every image located in its sub-folders. Right-click a folder to remove its contents from the Ansel library or to relocate it.

filename
: The image's filename.

### Metadata

tag
: Any tag that is attached to the image. Untagged images are grouped under the "not tagged" entry. Tags are displayed as a hierarchical list.

title
: The image's metadata “title” field.

description
: The image's metadata “description” field.

creator
: The image's metadata “creator” field.

publisher
: The image's metadata “publisher” field.

rights
: The image's metadata “rights” field.

notes
: The image's metadata "notes" field.

version name
: The image's metadata "version name" field.

rating
: The image's star rating.

color label
: Any color label attached to the image ("red", "yellow", "green", "blue", "purple").

geotagging
: The geo location of the image (see [locations](./locations.md)).

### Times

date taken
: The date the photo was taken, in the format `YYYY:MM:DD`.

date-time taken
: The date & time the photo was taken, in the format `YYYY:MM:DD hh:mm:ss`.

import timestamp
: The date/time the file was imported, in the format `YYYY:MM:DD hh:mm:ss`.

change timestamp
: The date/time the file's history stack was last changed, in the format `YYYY:MM:DD hh:mm:ss`.

export timestamp
: The date/time the file was last exported, in the format `YYYY:MM:DD hh:mm:ss`.

print timestamp
: The date/time the file was last printed, in the format `YYYY:MM:DD hh:mm:ss`.

### Capture details

camera
: The Exif data entry describing the camera make and model.

lens
: The description of the lens, as derived from Exif data.

aperture
: The aperture, as derived from Exif data.

exposure
: The shutter speed, as derived from Exif data.

focal length
: The focal length, as derived from Exif data.

ISO
: The ISO, as derived from Exif data.

### Ansel

grouping
: Choose between "group followers" and "group leaders".

local copy
: Show files that are, or are not, copied locally.

history
: Choose images whose history stacks have been altered or not.

module
: Filter based on the processing modules that have been applied to the image.

module order
: Choose images with "v3.0", "legacy" or "custom" module orders.

## The Folders tab

view selector
: The combobox at the top toggles between a flat **List** of film rolls and a hierarchical **Tree** of folders.

include sub-folders
: When the _Tree_ view is active, this checkbox controls whether clicking a folder also includes the images contained in all of its sub-folders. Internally it appends a `*` wildcard to the folder path; if you type a path ending in `*` or `%` by hand, the checkbox updates itself to stay in sync.

sort by / sort direction
: Choose whether film rolls are ordered by **name** (folder path) or by **id** (roughly the order in which they were first imported), and toggle ascending/descending order. These controls only affect the flat **List** view (the **Tree** is always sorted by path), so they are hidden in the Tree view.

folder levels
: The number of folder levels shown in film-roll names, counting from the right. Only meaningful (and only shown) in the **List** view.

You can **drag images** from the lighttable/filmstrip and drop them onto a folder or film-roll row to physically move the files into that folder (see [drag and drop](#drag-and-drop)).

Right-click a folder or film-roll row for management actions: **remove from library…**, **relocate…**, or **pre-render thumbnails** (fills the on-disk thumbnail cache for every image in the selected folders, as a background task).

## The Collections tab

This tab lists your tags as a hierarchical tree. Click a tag to filter the collection by it:

- **click** a tag to include that tag *and* all of its sub-tags (appends the `*` suffix);
- **shift+click** to include only the exact tag, not its sub-tags (no suffix);
- **ctrl+click** to include only the sub-tags, excluding the tag itself (appends the `|%` suffix).

You can **drag images** from the lighttable/filmstrip and drop them onto a tag row to attach that tag to them (see [drag and drop](#drag-and-drop)).

Right-click a tag to **rename** it, to **delete** one or more selected tags (deleting a tag also detaches it from every image), or to **pre-render thumbnails** of the tagged images.

no 'uncategorized' group
: When enabled, tags that have no children are not grouped under a synthetic "uncategorized" entry.

## The Queries tab

This tab is the general-purpose collection builder.

### Defining filter criteria

Each rule is made of an attribute selector, an optional comparison operator, and a search field:

image attribute
: The combobox on the left chooses which [attribute](#filtering-attributes) the rule filters on.

comparison operator
: For numeric, date/time and rating attributes, a small operator selector appears between the attribute and the search field, offering `=`, `<`, `≤`, `>`, `≥` and `≠`. It is hidden for text attributes.

search pattern
: In the text field, write a matching pattern. This pattern is compared against all database entries with the selected attribute, matching if the attribute *contains* the pattern. Use `%` as a wildcard. Leave the field empty to match all images that have the attribute. Where applicable, a tooltip appears when you hover over the attribute or the search field.

: Numeric and date/time attributes can also be combined with the comparison operators above, or with a range expressed as `[from;to]` (inclusive on both ends).

select by value
: Instead of typing, you can pick from the value list below the search field. It shows the values of the selected attribute that are present in the currently-matching images, with the image count for each, and updates continuously as you type. Clicking a value populates the search field automatically. For numeric and date-time attributes you can select a range of values by clicking the first and last entries.

### Combining multiple filters

You can combine several rules to build more complex collections. Each rule beyond the first carries a logical operator that defines how it combines with the rules above it.

Click the button at the end of a rule row to open a menu:

clear this rule
: Remove the current rule, or reset it if it is the only rule defined.

narrow down search
: Add a new rule combined with the previous rule(s) using a logical **AND**. An image is kept only if it *also* satisfies the new criteria.

add more images
: Add a new rule combined with the previous rule(s) using a logical **OR**. Images that satisfy the new criteria are *added* to the collection.

exclude images
: Add a new rule combined with the previous rule(s) using a logical **AND NOT** (except). Images that satisfy the new criteria are *removed* from the collection.

The button of each non-final rule shows its current operator (**AND**, **OR** or **AND NOT**). Click it to change the operator for that rule.

### Raw SQL queries

For advanced needs that the rule builder cannot express, enable **edit as raw SQL** on the _Queries_ tab. This directly exposes the SQL backend that is actually used underneath the GUI in other modes, where the GUI only builds SQL queries from more user-friendly (and standardized) controls.

The text field then accepts a single SQL `WHERE` expression that is injected directly into the collection query against your *local* library database. Press <kbd>Enter</kbd> to run it.

This is a power-user escape hatch:

- The expression is **not** sanitised — it is your responsibility to write valid SQL.
- It is **read-only**: the expression can only filter rows, it cannot modify the database.
- A malformed expression makes the underlying query fail gracefully and yields an **empty collection**; it does not crash Ansel.
- Enabling raw SQL replaces all the rules currently defined in the _Queries_ tab. Disabling it returns to a single empty film-roll rule.

Your expression is evaluated against one row per image. The following columns are available directly (they come from the `images` table); their names, SQL types and meaning are:

| Column             | Type    | Meaning |
|--------------------|---------|---------|
| `id`               | INTEGER | Unique image id (primary key). |
| `group_id`         | INTEGER | Id of the group-leader image. An image is a group leader when `id = group_id`. |
| `film_id`          | INTEGER | Id of the film roll (folder) the image belongs to. |
| `filename`         | TEXT    | File name with extension, without the path (e.g. `IMG_1234.CR2`). |
| `maker`            | TEXT    | Camera manufacturer, from Exif. |
| `model`            | TEXT    | Camera model, from Exif. |
| `lens`             | TEXT    | Lens description, from Exif. |
| `aperture`         | REAL    | f-number, e.g. `2.8`. |
| `exposure`         | REAL    | Shutter speed **in seconds**, e.g. `0.004` for 1/250 s. |
| `focal_length`     | REAL    | Focal length in millimetres. |
| `iso`              | REAL    | ISO sensitivity. |
| `aspect_ratio`     | REAL    | Width / height after cropping. |
| `flags`            | INTEGER | Bit-field. Bits 0–2 (`flags & 7`) hold the star rating 0–5; bit 3 (`flags & 8`) marks a rejected image; the remaining bits are internal (local copy, etc.). |
| `version`          | INTEGER | Duplicate/version number (`0` for the original). |
| `position`         | INTEGER | Manual ordering index used by the lighttable. |
| `datetime_taken`   | INTEGER | Capture date/time, as microseconds since `0001-01-01 00:00:00`. `0` means unknown. |
| `import_timestamp` | INTEGER | Import date/time, same microsecond encoding. `-1` means never. |
| `change_timestamp` | INTEGER | Last history-change date/time, same encoding. `-1` means never. |
| `export_timestamp` | INTEGER | Last export date/time, same encoding. `-1` means never. |
| `print_timestamp`  | INTEGER | Last print date/time, same encoding. `-1` means never. |

Examples:

```sql
iso > 800 AND lens LIKE '%50mm%'
```

```sql
aperture <= 2.8 AND exposure < 0.004
```

Because the date/time columns are stored as integer microseconds, filtering on them in raw SQL is awkward — for date ranges, prefer the dedicated _date taken_, _import timestamp_, etc. rules of the standard builder, which accept human-readable `YYYY:MM:DD` text.

The columns above are the ones exposed directly to the expression. Attributes that live in other tables (tags, metadata, color labels, edit history, folder paths) are not columns of this row, but you can still reference them through a sub-query on the image `id`. The relevant library tables are:

- `main.tagged_images(imgid, tagid)` joined with `data.tags(id, name, synonyms)` — tags;
- `main.meta_data(id, key, value)` — title, description and the other text metadata;
- `main.color_labels(imgid, color)` — color labels (`0`=red, `1`=yellow, `2`=green, `3`=blue, `4`=purple);
- `main.history(imgid, operation, …)` — applied modules / edit history;
- `main.film_rolls(id, folder)` — film-roll folder paths.

For example, to select images tagged under `landscape`:

```sql
id IN (SELECT imgid FROM main.tagged_images ti
       JOIN data.tags t ON t.id = ti.tagid
       WHERE t.name LIKE 'landscape%')
```

## Drag and drop

You can drag images out of the lighttable or the filmstrip and drop them directly onto a row of the Library module. What happens depends on the kind of row you drop them on — a folder/film-roll row, or a tag row:

```mermaid
flowchart TD
    A["Select one or more images<br/>in the lighttable or filmstrip"] --> B["Drag them onto a row<br/>of the Library module"]
    B --> C{"Row under the cursor?"}
    C -->|"folder / film roll<br/>(Folders tab)"| D{"Confirm the move?"}
    C -->|"tag<br/>(Collections tab)"| G["Attach the tag to the images<br/>(no file is moved)"]
    D -->|yes| E["Files are physically moved on disk<br/>into that folder, and the<br/>library database is updated"]
    D -->|no| F["Nothing happens"]
    E --> H["The film-roll/tag tree and the<br/>lighttable refresh automatically"]
    G --> H
```

The drop always applies to the single row located **under the mouse cursor** when you release the button, regardless of which rows happen to be selected.

drop on a folder or film roll
: The dragged images are **physically moved on disk** into that folder (a film roll is created for the folder if one does not exist yet), and the library is updated to match. Because this touches the file system, you are asked to confirm first. Any duplicates of the moved images follow along.

drop on a tag
: The tag of the row is **attached** to the dragged images. This only edits metadata — no file is moved on disk — and the change is written to the images' XMP sidecars.

{{< note >}}
Drag and drop only works from Ansel's own lighttable/filmstrip (it relies on the internal image identifiers). Dragging image files from an external file manager into this module is not supported — use the regular import for that.
{{< /note >}}

## Updating the folder path of moved images

While it is best not to touch imported files behind Ansel's back, this module can help you recover when you have moved or renamed image folders after importing them. The process is:

1. Open the _Folders_ tab.
1. A film roll or folder whose location can no longer be found on disk is shown with ~~strikethrough~~ formatting.
1. Right-click the folder or film roll name and select **relocate…**, then choose the new location of the folder. Selecting several rows first lets you relocate them in one operation by picking their new common parent folder.

This updates Ansel's library database only; it does not move any files on disk.

## Returning to a previous collection

Your recent collections are kept in a history list. You can revert to a previously-defined collection from the **collections** entry in the top menu bar, which lists the most recent collections you have used. The number of remembered collections is configurable from the preferences of that recent-collections list (**number of collections to be stored**).

## Settings

The Library module no longer hides its settings behind a separate "preferences…" popup: every setting now lives directly in the relevant tab.

do not set the 'uncategorized' entry for tags
: The **no 'uncategorized' group** checkbox on the _Collections_ tab. When enabled, tags that have no children are not grouped under a synthetic "uncategorized" entry (default off).

number of folder levels to show in lists
: The **folder levels** spinner on the _Folders_ tab (List view). The number of folder levels to show in film-roll names, counting from the right (default 1).

sort film rolls by
: The **sort by** selector on the _Folders_ tab (List view). Sort film rolls by either their **name** (folder path) or **id** (roughly equivalent to the date the film rolls were first imported) (default "id").

sort collection descending
: The **sort direction** toggle on the _Folders_ tab. Sorts "film roll" (when sorted by folder), "folder", and date/time attributes (e.g. date taken) in descending order (default on).
---

# Liquify : https://ansel.photos/en/doc/views/darkroom/modules/liquify/

Move pixels around by applying freestyle distortions to parts of the image using points, lines and curves.

## Nodes

Each of liquify's tools is based on nodes. A point consists of a single node and a line or curve consists of a sequence of linked nodes defining a path.

Each instance of the liquify module is limited to a maximum of 100 nodes -- for more nodes, use additional instances. However, please note that the liquify module consumes a lot of system resources.

Drag the central point of a node to move the node around. The radius describes the area of the effect (distortion occurs only within this radius). To change the radius drag the handle at the circumference. A strength vector starting from the center describes the direction of the distortion, and its strength is depicted by the length of the vector. Change the vector by dragging its arrow head.

## Points

A point consists of a single node and strength vector.

Click the point icon to activate the point tool and then click on the image to place it. Hold Ctrl while clicking on the point icon to add multiple points without having to click the icon again each time. Right-click to exit creation mode.

### Point modes

The strength vector of a point has three different modes. These can be toggled by holding Ctrl and clicking on the arrowhead of the strength vector.

linear
: A linear distortion inside the circle, starting from the opposite side of the strength vector and following the vector's direction. This is the default mode.

: ![linear](liquify_ex1.jpg)

radial growing
: The strength vector's effect is radial, starting with a strength of 0% in the center and increasing away from the center. This mode is depicted by an additional circle with the arrow pointing outwards.

: ![radial growing](liquify_ex4.jpg)

radial shrinking
: The strength vector's effect is radial, starting with a strength of 100% in the center and decreasing away from the center. This mode is depicted by an additional circle with the arrow pointing inwards.

: ![radial shrinking](liquify_ex3.jpg)

### Feathering

default mode
: By default the strength varies linearly from 0% to 100% between the center and the radius of the control point. It is possible to modify the feathering effect by clicking on the center of the circle.

: ![default](liquify_ex1.jpg)

feathered mode
: In "feathered" mode, two control circles are displayed, which can be modified independently to feather the strength of the effect. Note that clicking the center of the circle again hides the feathering controls but does _not_ return to the default mode.

: ![feathered](liquify_ex2.jpg)

### Removing points

A point can be removed by right-clicking on the center of the node.

## Lines and curves

Lines and curves are sequences of points linked together by straight or curved lines. The effect is interpolated by a set of associated strength vectors.

Click the appropriate icon to activate the line or curve tool and then click on the image to place a sequence of points forming the path. Right-click anywhere when the last point has been placed in order to finish drawing the line/curve.

Hold Ctrl while clicking on the line/curve icon to add multiple lines/curves without having to click the icon again each time. Right-click a second time to exit creation mode after the final line or curve has been completed.

lines
: ![lines](liquify_ex5.jpg)

curves
: ![curves](liquify_ex6.jpg)

Ctrl+click on a line or curve segment to add a new control point. Ctrl+right-click on the center of a node to remove a control point.

Right-click on a segment to remove the shape completely. Ctrl+Alt+click on a segment to change that segment from a line to a curve and vice versa.

## Link modes

Ctrl+click on the center of a node to change the way the points of a curve are linked together. There are four modes, which correspond to different ways of handling the steepness of the bezier curve using control handles:

autosmooth
: This is the default mode, in which control handles are not displayed -- controls are automatically computed to give a smooth curve.

cusp
: Control handles can be moved independently. This mode is depicted by a triangle symbol in the node's center.

smooth
: Control handles always give a smooth curve. This mode is depicted by a diamond symbol in the node's center.

symmetrical
: Control handles are always moved together. This mode is depicted by a square symbol in the node's center.

## View and edit nodes

Click the node tool icon to activate or deactivate the node edit tool. This displays all currently-defined distortion objects and their controls. Alternatively you can right-click on the image at any time for the same effect.

## Warps and nodes count

This information field displays the number of warps (individual distortion objects) and nodes currently in use.

## Show guides

Tick the box to show guide overlays whenever the module is activated. Click the icon on the right to control the properties of the guides. See [guides & overlays](../../toolboxes/guides-overlays.md) for details.
---

# Local contrast : https://ansel.photos/en/doc/views/darkroom/modules/local-contrast/

Enhance the image's local contrast.

This is achieved using either a _local laplacian_ (default) or _unnormalized bilateral_ filter. Both modes work exclusively on the L channel from Lab. The _local laplacian_ filter has been designed to be robust against unwanted halo effects and gradient reversals along edges.

## Module controls

mode
: Choice of _local laplacian_ filter or _bilateral grid_. The following sections define the controls available for each of these modes.

### Bilateral grid

coarseness
: Adjust the coarseness of the details to be adjusted.

contrast
: Control how strongly the algorithm distinguishes between brightness levels. Increase this parameter for a more contrasty look.

detail
: Add or remove detail. Higher values increase local contrast.

### Local laplacian

To understand the parameters of the local laplacian filter, one can think of it as applying a curve to the image, similar to the following graph:

![local laplacian curve](local-laplacian-curve.jpg)

This curve is applied to the image in a way that works locally and avoids halo artifacts.

The local laplacian mode also supports shadow lifting and highlight compression.

detail
: Add or remove detail. Higher values increase local contrast. This inserts an S shaped element in the center of the curve, to increase or decrease local contrast.

highlights
: This affects one end of the S shaped contrast curve, effectively increasing or compressing contrast in the highlights. A low value pulls the highlights down.

shadows
: Similar to the highlights parameter, this affects the other end of the contrast curve, and increases or decreases contrast in the shadows. A higher value gives more contrast in the shadows. A lower value lifts the shadows and can effectively simulate a fill light. Note that this is done with local manipulation of the image. However, this means that a completely dark image cannot be brightened in this way -- only dark objects in front of bright objects are affected.

mid-tone range
: This controls the extent of the S shaped part of the contrast curve. A larger value makes the S wider, and thus classifies more values as mid-tones and fewer values as highlights and shadows. In higher dynamic range settings it can be useful to reduce this value to achieve stronger range compression, by lowering the contrast in the highlights and the shadows.

: This setting can cause banding artifacts in the image if pushed to extreme values. This is due to the way in which Ansel computes the fast approximation of the local laplacian filter.
---

# Locations : https://ansel.photos/en/doc/views/toolboxes/locations/

Create areas or locations and organize them as hierarchical tags.

A location is shown as a shape on the map when selected. Initially each location is represented as a square or circle and can be changed to a rectangle or ellipse by adjusting the shape's width and/or height.

A location can also be created from an OpenStreetMap region (city/country) polygon. To achieve this, first make sure the _max polygon points_ parameter is large enough (some country polygons use more than 150,000 points). Then select the desired location in the [find location](./find-location.md) module. When the corresponding region shape is displayed, a polygon symbol becomes available in the "shape" control (see below). Select it to create the new location.

Each location is stored as tag entry under the geotagging collection in the [collections](./collections.md) module. The pipe “|” character can be used to insert a new level (a group of locations).

## Module controls

shape
: Select the circle or rectangle symbol to choose the default shape for new locations. A polygon symbol is available when a shape is displayed by the [find location](./find-location.md) module.

new location / new sub-location
: When no location is selected you can use the _new location_ button to create a location at root level. When a location is selected use the _new sub-location_ button to create a sub-location within the selected location.

show all
: Show or hide all locations that lie within the visible area of the current map.

### Actions on the locations list

click
: Select or de-select a location. If the location is not currently visible on the map, the map is automatically centered on that location.

Ctrl+click
: Edit the name of the location. Press Enter to save changes or Esc to close the editing window without saving.

right-click
: Open a sub-menu which allows you to:
: - Edit the name of the location.
: - Delete the location.
: - Update the filmstrip -- the filmstrip will be populated with all images in the selected location.
: - Switch to the lighttable view and show a collection that contains all images in the selected location.

### Actions on locations in the map

_Note: The following actions have no effect on polygon locations._

click and drag
: Move the location shape to a new position on the map.

Ctrl+click or Ctrl+Shift+click
: Move an image or a group of images and place them inside a location shape.

mouse scroll
: When inside a location shape (but not over an image), increase or decrease the size of that shape.
: When hovering over an image, cycle through the thumbnails of images located at that position on the map.
: When outside a location shape (and not over an image) zoom the map in or out.

Shift+scroll
: Increase or decrease the width of the location shape.

Ctrl+scroll
: Increase or decrease the height of the location shape.

click on a location shape
: Select a different location when the _show all_ checkbox is selected.
---

# Lua scripts installer : https://ansel.photos/en/doc/views/toolboxes/lua-scripts-installer/

This module provides an interface for installing darktable [lua scripts](../../lua/_index.md). The first time it is run, instructions are displayed in the module.

It can be disabled with an option in [preferences > lua options](../../preferences-settings/lua-options.md).
---

# Lut 3D : https://ansel.photos/en/doc/views/darkroom/modules/lut-3d/

Transform RGB values with a 3D LUT file.

A 3D LUT is a tridimensional table that is used to transform a given RGB value into another RGB value. It is normally used for film simulation and color grading.

This module accepts `.cube`, `.3dl`, `.png` (haldclut) and `.gmz` files. Uncompressed 3D LUT data is not saved in the database or the XMP file, but is instead saved to the 3D LUT file path inside the 3D LUT root folder. It is therefore important to back up your 3D LUT folder properly -- sharing an image with its XMP is pointless if the recipient doesn't also have the same 3D LUT file in their own 3D LUT folder.

The compressed format `.gmz` is only available when GMIC is installed. This format can hold a full library of LUTs, and LUT data loaded from this type of file _can_ be saved to the database and XMP files.

---

**Note**: the module clips all values outside of the range [0,1]. You may have to reduce the range of the input before applying.

---

## Usage

LUTs are most commonly used in Ansel for color grading or film look simulation. For this reason, by default, the module is placed after the [_filmic_](./filmic.md) module in the pixelpipe and should be applied to a neutral image (without first applying a specific look). While you can find hundreds of free LUTs on the internet, you should note that not all of them are compatible with the Ansel environment and workflow -- incompatible LUTs will not produce the advertised look. To limit the risk, a color grading LUT should have been created to work with one of the available "application color spaces" (see below), for both the input and the output of the module.

Camera log LUTs (as F-log or S-Log3) are different to color-grading and film-look-simulation LUTs, and are intended to convert the camera log raw data into something (linear raw data or other color space) that Ansel is able to understand. In this case the _lut 3D_ module should be manually placed between the [_demosaic_](./demosaic) and [_input color profile_](./input-color-profile.md) modules. Once you have done this, you can no longer choose an "application color space". The "input profile" of input color profile module should be aligned with the output of the LUT. _Please note that this use case has not yet been tested._

## Module controls

file selection
: Choose the 3D LUT file to use. File selection is inactive if the 3D LUT root folder has not been defined in [preferences > processing](../../../preferences-settings/processing.md).

application color space
: A 3D LUT is defined relative to a specific color space. Choose the color space for which the selected 3D LUT file has been built. Cube files are usually related to REC.709 while most others are related to sRGB.

interpolation
: This defines how to calculate output colors when input colors are not exactly on a node of the RGB cube described by the 3D LUT. There are three interpolation methods available: tetrahedral (default), trilinear and pyramid. Usually you won't see any difference between interpolation methods except with smaller sized LUTs.
---

# Map settings : https://ansel.photos/en/doc/views/toolboxes/map-settings/

Select preferred map data from various providers. Some will provide additional layers (satellite view etc.) which you can toggle.

## Module controls

map source
: Choose the provider to source map information from.

max polygon points
: The [find location](./find-location.md) module doesn't display polygons with more points than this for performance reasons. Usually a country polygon has between 50,000 and 150,000 points.

show OSD
: Choose whether to display the OSD controls at the top-left of the center view.

filtered images
: Check this box to display only the images from the current collection (those shown in the filmstrip) in the center view. Un-check the box to display all images in the current library, where those images have associated GPS data. You can also toggle this option by pressing Ctrl+S.

max images
: The maximum number of thumbnails to display on the map.

group size factor
: Increase or decrease the size of area that causes images to be grouped.

min images per group
: The minimum number of images that need to be placed in the same position in order to automatically create an image group for them.

thumbnail display
: Define what information to show on the map display
: - _thumbnails_: Display image thumbnails along with a counter.
: - _count_: Just display the number of images (to free space on the map). Hover over the number of images to show the corresponding thumbnail(s). A count-only marker behaves the same way as a normal image thumbnail, in terms of color coding, scrolling, drag and drop etc.
: - _none_: Show nothing.
: You can also cycle through these options by pressing Shift+S.
---

# Mask manager : https://ansel.photos/en/doc/views/toolboxes/mask-manager/

The mask manager — labelled **Masking & Blending** in the darkroom [left panel](../darkroom/darkroom-view-layout.md#left-panel) — creates, renames, edits, groups and deletes the drawn shapes used by masks. It shares its shape system with the per-module [masking & blending](../darkroom/masking-and-blending/_index.md) controls: a shape drawn here is the same object a module can use as a mask, and vice-versa.

The top row of buttons creates new shapes — the same brush, circle, ellipse, path and gradient tools as the [drawn mask](../darkroom/masking-and-blending/masks/drawn.md) interface. Below them is a list of every mask and shape defined for the current image.

Groups of shapes that form a module's mask appear with a heading of the form `grp <module name>`, with their component shapes nested below. After the groups comes a list of every individual shape that exists for the image. A symbol to the right of a shape name indicates that it is in use by a mask.

## Shapes

Each new shape gets an automatic name made of its type (_brush_, _circle_, _ellipse_, _path_, _gradient_) and an incrementing number. Double-click a name to rename it — meaningful names help a lot when reusing a selection across masks.

- **Click** a shape name to display just that shape and its controls on the canvas. This is the reliable way to grab one shape among many overlapping ones. Selecting a shape on the canvas from within a module's mask controls likewise highlights it here.
- **Right-click** a shape name for options to remove it, or to remove all shapes not currently in use.

{{< note >}}
Ansel keeps every shape ever defined for an image until you explicitly remove it. If you export with the development history, all defined shapes are written to the XMP. A very long list of shapes can exceed the size limit of some file formats and make XMP writing fail on export — prune unused shapes when in doubt.
{{< /note >}}

## Masks and groups

A mask is a group of shapes applied in list order (top to bottom), each combining with the running mask through one of four [set operators](#set-operators). Because order matters, shapes can be moved up and down the list.

- **Click** a group name to expand it and show its shapes (also drawn on the image). Showing a mask from within a module expands the matching group here.
- **Right-click** a group name to add new or existing shapes, clean up unused shapes, or delete the group.
- **Right-click** a shape inside a group to control its contribution:

remove from group
: Remove the shape from the mask.

use inverted shape
: Invert the polarity of the shape.

mode
: Choose the [set operator](#set-operators) combining this shape with the preceding mask.

move up / down
: Reorder the shape within the group.

To build your own group, select the shapes, right-click and choose **group the forms**.

## Set operators

Set operators define how each shape combines with the mask built from the shapes above it. Taking a pixel to be "selected" when its opacity is greater than zero. The examples below combine a gradient with a path, showing the effect of each operator applied to the path:

![a gradient mask combined with a path](mask-manager_ex1.jpg)

union
: The default (![union icon](masks_union.jpg)). The result selects pixels that are in the existing mask **or** in the added shape (the maximum value is taken where they overlap).

: ![union result](mask-manager_ex3.jpg)

intersection
: ![intersection icon](masks_intersection.jpg) The result selects only pixels that are in **both** the existing mask **and** the added shape (the minimum value is taken where they overlap) — useful to "imprint" one shape onto another.

: ![intersection result](mask-manager_ex4.jpg)

difference
: ![difference icon](masks_difference.jpg) The result keeps pixels that are in the existing mask but **not** in the added shape — useful to cut a region out of a selection.

: ![difference result](mask-manager_ex5.jpg)

exclusion
: ![exclusion icon](masks_exclusion.jpg) The result selects pixels that are in the existing mask **or** the added shape but **not both** (an exclusive or).

: ![exclusion result](mask-manager_ex6.jpg)
---

# Metadata editor : https://ansel.photos/en/doc/views/toolboxes/metadata-editor/

Edit the metadata of selected images.

Metadata is freeformat text (title, description, creator, publisher, rights etc.) that describes your images.

When several images are selected having different values for a given metadata field, the module displays _<leave unchanged>_ for that field -- if you choose to apply changes, these fields will not be changed. If you right-click on the field the different values are listed at the end of the contextual menu. Select one of the values in the menu to apply that value to all of the selected images -- the change will be saved once you press the "apply" button or the Enter/Tab key.

## Module controls

reset
: Delete visible (see below) metadata from the selected images.

metadata entry fields
: A separate field is displayed for each metadata item. Hold Ctrl while scrolling with your mouse to increase the height of a field. Press Ctrl+Enter to insert a new line. Double-click on a field's label to delete the contents of that field.

apply
: Write the metadata from the entry fields to the selected images.

The _rights_ field offers a set of ready-made Creative Commons licenses (CC BY, CC BY-SA, CC BY-ND, CC BY-NC, CC BY-NC-SA, CC BY-NC-ND) and _all rights reserved_ as presets, so you can fill it in without typing the full text.

## Keyboard

You may use the keyboard to navigate and apply changes while any of the metadata entry boxes have focus:

- The Tab key saves the current field and moves the cursor to the next field. When the last field is reached, the Tab key returns focus to the first field.

- Shift+Tab works the same as Tab, but in the opposite direction.

- The Enter key saves the current field without moving the cursor.

## Preferences

The "preferences…" option in the presets menu brings up a dialog where you can configure how metadata is handled within Ansel. For each metadata item, two check boxes allow you to restrict how metadata is handled:

visible
: Show or hide this metadata field. Hidden fields are not included in exported images.

private
: Keep this metadata field private. Private fields are not included in exported images.
---

# Navigation : https://ansel.photos/en/doc/views/toolboxes/navigation/

Zoom and pan the current darkroom image.

At the top of the darkroom [left panel](../darkroom/darkroom-view-layout.md#left-panel), the navigation module shows a full preview of the current image with a rectangle marking the area currently visible in the center view. Drag the rectangle to pan the zoomed-in view.

The current zoom level is displayed to the right of the preview. Click it for quick access to common zoom levels.

The module can be expanded/collapsed like any toolbox — from its header, through the [global action search](../../getting-started/keyboard.md#vimkey-like-global-action-search), or via a shortcut you assign to its focusing action. Center-view zoom is also controlled directly: see [zoom and pan](../darkroom/_index.md#zoom-and-pan).
---

# Negadoctor : https://ansel.photos/en/doc/views/darkroom/modules/negadoctor/

Process scanned film negatives.

You can obtain an image of a negative using a film scanner, or by photographing it against a white light (e.g. a light table or computer monitor) or off-camera flash.

## Preparation

If the image of the negative was obtained using a digital camera, then in order to obtain accurate colors in the final image, you will need to take the following points into consideration:

- When taking the photograph, adjust the exposure to fully utilise the entire dynamic range of your camera sensor -- i.e. "expose-to-the-right", so that the histogram in your camera touches the right hand side without clipping the image.

- Ensure the white balance is correctly set up to compensate for the light source used to illuminate the negative. You can take a profiling picture of the light source with no film negative in front of it, and then use the "from image area" feature in the [_white balance_](./white-balance.md) module to obtain a reference white-balance setting. This reference white balance setting can then be made into a style or simply pasted onto the images taken from your film negatives.

- Apply the standard or enhanced camera matrix in the [_input color profile_](./input-color-profile.md) module.

When scanning or photographing your film negative, make sure you include some unexposed part of the film within the captured image. This is required to set the _Dmin_ parameter (see below). If this is not possible (e.g. your film holder completely obscures the unexposed parts of the film), you can take a separate image of an unexposed part of the film, measure the _Dmin_ parameter from that image, and then paste that setting to the rest of the images from that film.

When developing the scanned/photographed film negatives, it is recommended that you disable any tone mapping modules such as [_filmic_](filmic.md).

The _working profile_ parameter in Ansel's [_input color profile_](input-color-profile.md) module should be set to either _linear Rec2020 RGB_, or to an ICC profile representing the actual color space of your film emulsion. Some examples of such ICC profiles may be found in the following forum posts:

- [https://discuss.pixls.us/t/any-interest-in-a-film-negative-feature-in-rt/12569/177](https://discuss.pixls.us/t/any-interest-in-a-film-negative-feature-in-rt/12569/177)

- [https://discuss.pixls.us/t/input-color-profile-to-use-for-negatives/20271/13](https://discuss.pixls.us/t/input-color-profile-to-use-for-negatives/20271/13)

---

**Note:** if you want to use the [_tone equalizer_](tone-equalizer.md) with _negadoctor_, you'll need to move the _tone equalizer_ module **after** _negadoctor_ in the [pixelpipe](..//pixelpipe/the-pixelpipe-and-module-order.md), since the _tone equalizer_ is not designed to work with negatives.

---

## Module controls

It is strongly recommended that you set the parameters following the order in which they are presented in the GUI. Start by setting the _film stock_, then work through each of the tabs ([_film properties_](#film-properties), [_corrections_](#corrections), [_print properties_](#print-properties)) in order, working from top to bottom in each tab.

When using color pickers, be careful to avoid including dust and scratches, which can skew the data taken from the sampled region.

film stock
: The first step is to choose "color" or "black and white" in the _film stock_ drop-down. If you select "black and white", any sliders that are only used for color will be hidden from view.

### Film properties

This tab contains a number of basic settings. If, after adjusting these settings, your image is still not quite as you would like it, you can make further adjustments on the [_corrections_](#corrections) tab. These are technical settings, and serve a similar purpose to the _scene_ tab in the [_filmic_](filmic.md) module, in that they adjust the black and white points and hence define the dynamic range of the negative.

color of the film base
: Sample an area of the base film stock from your scan. This is the area just outside of the image (an unexposed part of the film). If you are working with black and white negatives, you can leave this slider at its default value (white). If working on color film, click the color picker to the right of the color bar, which will create a bounding box covering about 98% of your image. Then, click and drag across an area of your negative which contains only unexposed film stock. This will automatically calculate values for the _D min_ slider(s). It is likely at this point that your image will still look too dark, but you can correct this later.

D min
: If the _film stock_ is set to "black and white", this slider indicates the minimum value corresponding to the unexposed film stock. If the _film stock_ is set to "color", this control will consist of 3 separate sliders, one for each of the red, green and blue channels.

D max
: This slider represents the dynamic range of your film, and it effectively sets the film's white point. Dragging this slider to the left will make the negative brighter. Dragging it to the right will make the negative darker. When adjusting this slider manually, it's a good idea to closely watch your histogram to ensure that you don't clip the highlights (where the histogram has been pushed over too far off the right hand side of the graph). If you click the color picker icon (on the right) negadoctor will automatically calculate this value to ensure maximal use of the histogram without clipping. To use the color picker, click and drag to draw a rectangle across only the exposed parts of the negative. Don't include the unexposed film stock, as this will skew the result.

scan exposure bias
: This slider allows you to set the black point. It is a technical adjustment that ensures a proper zeroing of the RGB values and a spreading of the histogram between [0, 1] values for robustness in the operations that follow. Dragging this to the left will make the negative brighter. Dragging to the right will make the negative darker. When adjusting this slider manually, it's a good idea to closely watch your histogram to ensure that you don't clip the shadows (where the histogram is pushed too far off the left hand side of the graph). If you click the color picker negadoctor will automatically calculate any required offset. To use the color picker, select a region in the darkest lowlights, or select the entire image without including any unexposed film stock. Double-check the histogram to ensure the left part of it doesn't clip.

### Corrections

This tab contains sliders that allow you to make color cast corrections within both the shadow and highlight regions.

The settings on this tab should not be needed for most well-preserved negatives. It is mostly useful for old and poorly-preserved negatives with a decayed film base that introduces undesirable color casts. Note that the shadows color cast setting will have no effect if the _scan exposure bias_ setting in the [_film properties_](#film-properties) tab is set to a non-zero value.

The other case where these color cast corrections may be needed is if the white balance properties of the light used to scan the film negative are significantly different to the light source under which the original film camera took the shot. For example, if you illuminate the film with an LED light, but the original shot was taken under daylight, this may require some additional color cast corrections.

shadows color cast
: These three sliders allow you to correct for color casts in the shadows by adjusting the _red_, _green_ and _blue_ channels individually. Use the color picker to set the sliders automatically by selecting a neutral gray shadow region requiring correction. Because the shadows sliders can also affect color casts in the highlights, it is important to finish setting the shadows sliders before moving on to the highlights sliders.

highlights white balance
: These three sliders allow you to correct the white balance in the highlights by adjusting the _red_, _green_ and _blue_ channels individually. Use the color picker to set the sliders automatically by selecting a neutral gray region in the highlights that is not properly balanced.

### Print properties

This tab contains settings that mimic the tonal effect of the photochemical papers that would have been used to create the hard copy image if you were not developing the photo digitally. These are creative settings, and serve a similar overall purpose to the creative tone curve settings on the _look_ tab of the [_filmic_](filmic.md) module.

The _print exposure_, _paper black_ and _paper grade_ are analogous to the _slope_, _offset_ and _power_ controls in the [_color balance_](color-balance.md) module (which in turn is based on the ASC CDL standard). These settings define a creative tone curve to enforce your contrast intent after the inversion, at the end of the module. The equation governing this slope/offset/power behaviour is:

_RGB_out_ = ( _RGB_in_ × _exposure_ + _black_ ) ᵍʳᵃᵈᵉ

paper black (density correction)
: For this slider, select the color picker and click and drag to select a region that encompasses only the exposed part of the film negative. If you can see unexposed film stock around the edges of your image, ensure that these areas are excluded from the drawn rectangle when calculating the _paper black_ setting. Paper black represents the density of the blackest silver-halide crystal available on the virtual paper. In the analog development process, this black density always results in non-zero luminance, but the digital pipeline usually expects black to be encoded with a zero RGB value. This slider setting lets you remap paper black to pipeline black via an offset. You can use the color picker to select a region of the image that should be mapped to black in the final image.

paper grade (gamma)
: This slider is your gamma (contrast) control, and it defaults to a value of 4. If all has gone well, this value (4) minus the value of D max (from the “film properties” tab) should normally leave you with a value between 2 and 3.

paper gloss (specular highlights)
: This slider is essentially a highlights compression tool. As you drag this slider to the left, you will see in the histogram that the highlight values are being compressed (pushed to the left). Adjust this accordingly, so that your highlights are not clipped in the histogram. You can also use this to simulate a matte photo print with low-contrast highlights.

print exposure adjustment
: This slider offers one final opportunity to correct any clipping of the highlights. If you have followed all the previous instructions carefully, you shouldn't need to adjust this setting. Note that you can increase the print exposure while at the same time decreasing the paper gloss, which allows you to brighten the mid-tones without losing any highlights. You can use the color picker to select the brightest highlights, or select the entire image without including any unexposed film stock. This will set the exposure so that the brightest part of the selected region is not clipped. Double-check the histogram to make sure that the right part of the histogram doesn't clip.
---

# Orientation : https://ansel.photos/en/doc/views/darkroom/modules/orientation/

Rotate the image 90 degrees at a time or flip the image horizontally and/or vertically.

The module is enabled by default and the orientation (rotation) is automatically set based on the image's Exif data.

## Module controls

transform
: Double click the label to reset to the default transformations

![rotate counter-clockwise](rotate-counter-clockwise.jpg) rotate counter-clockwise
: Rotate the image 90 degrees counter-clockwise

![rotate clockwise](rotate-clockwise.jpg) rotate clockwise
: Rotate the image 90 degrees clockwise

![flip horizontally](flip-horizontal.jpg) flip horizontally
: Flip the image (mirror) horizontally

![flip vertically](flip-vertical.jpg) flip vertically
: Flip the image (mirror) vertically

show guides
: Tick the box to show guide overlays whenever the module is activated. Click the icon on the right to control the properties of the guides. See [guides & overlays](../../toolboxes/guides-overlays.md) for details.
---

# Output color profile : https://ansel.photos/en/doc/views/darkroom/modules/output-color-profile/

Manage the output profile for export and the rendering intent to be used when mapping between color spaces.

Ansel comes with pre-defined profiles _sRGB_, _Adobe RGB_, _XYZ_ and _linear RGB_. You can provide additional profiles by placing them in `$Ansel/share/Ansel/color/out` and `$HOME/.config/Ansel/color/out` (where `$Ansel` is the Ansel installation directory and `$HOME` is your home directory).

The output color profile may also be defined within the [export](../../toolboxes/export.md) module.

## Module controls

output intent
: The rendering intent for output/export. Rendering intent can only be selected when using LittleCMS2 to apply the output color profile (this can be changed in [preferences > processing](../../../preferences-settings/processing.md)). If Ansel's internal rendering routines are used, this option is hidden. For more details see [rendering intent](../../../color-management/rendering-intent.md).

output profile
: The profile used to render colors for output/export. The profile data will be embedded into the output file (if supported by the file format) allowing other applications to correctly interpret its colors. As not all applications are aware of color profiles, the general recommendation is to stick to sRGB unless you know what you are doing and have a good reason to do otherwise.
---

# Print settings : https://ansel.photos/en/doc/views/toolboxes/print-settings/

Manage settings for the [print view](../print/_index.md) and initiate printing.

## Module controls

### Printer

printer
: Select one of the installed printers.

media
: The type of media loaded on the printer (Plain Paper, Luster Photo Paper, etc.).

profile
: The printer's ICC profile for the loaded paper. This is the profile specific to the printer and paper. This profile is the last color space transformation applied to the picture whose goal is to create a high quality print.

intent
: The print rendering intent (“perceptual”, “relative colorimetric”, “saturation” or “absolute colorimetric”). See [rendering intent](../../color-management/rendering-intent.md) for more details.

black point compensation
: Whether to adjust the black point of the output profile, which is often lighter than the input profile. This should be “on” when the _intent_ is set to “relative colorimetric”.

### Page

paper size
: The size of the paper on which to print.

orientation
: Portrait or landscape (note that darktable chooses the best fit by default).

units
: The unit to use for setting the margins: “mm”, “cm”, or “inch”.

margins
: Set each margin separately, or all together by clicking on the middle “lock” button.

display grid
: Select the grid size using the entry field (expressed in the currently selected unit). Tick the option to display the grid on the canvas.

snap to grid
: Help setting the image areas by snapping them to the grid for proper alignment.

borderless mode required
: Indicates whether the printer borderless mode is to be activated. This item is activated when the user's margins are smaller than the printer hardware margins. Note that it is only an indicator as it does not activate the borderless mode automatically.

### Image layout

image width/height
: This information field displays the actual image width and height (given with the selected units) on the paper.

scale factor
: This information field displays the scaling of the image to fit on the paper. If this value is less than 1 the image is down-scaled, otherwise it is up-scaled. This is an important factor to watch -- a value that is too large (up-scale) may result in a low quality print. The corresponding dpi (dots per inch) is also displayed.

alignment
: Select the alignment of the image on its area.

new image area button
: Create a new image area. Drag and drop on the canvas to place it. If the option snap to grid is activated the area can be easily aligned to the grid lines. An image can be placed into this area by dragging it from the filmstrip and dropping it on the new area.

delete image area button
: Remove the currently selected image area from the composition.

clear layout button
: Remove all the image areas leaving the canvas empty.

The following four fields represent the position of currently selected area on the page -- the top/left corner on the first line and the width/height of the area on the second line.

When hovering an image area its position and size are displayed. It is also possible to grab the side and corner of the area to change the size or to drag the whole area to change its position.

The page layout can be recorded using a preset.

### Print settings

profile
: The export profile to use. This profile is the entry point used for the next transformation using the printer's ICC profile. Usually it is better to prefer a large gamut (e.g. Adobe RGB) rather than a smaller one (e.g. sRGB).

intent
: The rendering intent to use when exporting the image. For more information see [rendering intent](../../color-management/rendering-intent.md).

style
: Choose a style to apply when exporting the image -- defaults to “none”. See the [export](../toolboxes/export.md) module for a more detailed discussion of applying a style during export.

mode
: Whether the chosen style should be appended to the existing history stack or replace it completely. See the [export](../toolboxes/export.md) module for more details.

### Print button

When clicked, the images are first exported using the selected options, then composed on the page and finally sent to the printer.
---

# Processing modules : https://ansel.photos/en/doc/views/darkroom/modules/
---

# Raw black/white point : https://ansel.photos/en/doc/views/darkroom/modules/raw-black-white-point/

Define camera-specific black and white points.

This module is activated automatically for all raw images. Default settings are applied for all supported cameras. Changes to the defaults are not normally required.

## Module controls

black level 0-3
: The camera-specific black level of the four pixels in the RGGB Bayer pattern. Pixels with values lower than this level are not considered to contain valid data.

white point
: The camera-specific white level. All pixels with values above this are likely to be clipped and will be handled accordingly in the [_highlight reconstruction_](./highlight-reconstruction.md) module.

flat field correction
: Use flat field correction to compensate for lens shading. This field only appears for applicable Raw files and will automatically use any GainMap embedded in the Raw. You can choose to disable this correction if desired.
---

# Raw chromatic aberrations : https://ansel.photos/en/doc/views/darkroom/modules/raw-chromatic-aberrations/

Correct chromatic aberrations of raw images.

As this module operates on non-demosaiced raw data, the underlying model assumes an uncropped photographic image as input. It is therefore likely to fail when you zoom in, as it is only able to operate on the visible portion of the image. The module will therefore be bypassed at high zoom levels, and a "bypassed" message will appear at the top of the module. This limitation only applies to interactive work and not to the final export.

This module currently only works for raw images recorded with a Bayer sensor (the sensor used in the majority of cameras) -- for other types of image, you should use the [_chromatic aberrations_](./chromatic-aberrations.md) module instead.

The module will also not apply any corrections to any photos that have been identified as monochrome by their metadata, like the files produced by the Leica M10 Monochrom.

## Module controls

iterations
: The number of iterations. For most images, "twice" is sufficient, and is the default value. Occasionally, increasing this control can give better results.

avoid colorshift
: If the module causes purple tinting, tick this box to apply a correction.
---

# Raw denoise : https://ansel.photos/en/doc/views/darkroom/modules/raw-denoise/

Perform denoising on raw image data before it is [demosaiced](./demosaic.md).

This module has been ported from [dcraw](https://www.dechifro.org/dcraw/).

## Module controls

noise threshold
: The threshold for noise detection. Higher values lead to stronger noise removal and greater loss of image detail.

coarse/fine curves
: The noise of an image is usually a combination of fine-grained and coarse-grained noise. These curves allow the image to be denoised more or less depending on the coarseness of the visible noise. The left of the curve will act on very coarse grain noise, while the right of the curve will act on very fine grain noise.

: Raising the curve will result in more smoothing, while lowering it will result in less smoothing. As an example, you can preserve very fine-grained noise by pulling down the rightmost point of the curve to the minimum value.

: If you are tackling chroma noise with a [blend mode](../masking-and-blending/blend-modes.md), you can raise the rightmost part of the curve quite high, as colors do not change a lot on fine grain scales. This will help especially if you see some isolated pixel left un-denoised.

: The best way to use the R, G, and B curves is to examine each of the channels in turn using the [_color calibration_](./color-calibration.md) module in gray mode, denoise that channel, and then repeat for the other channels. This way, you can take into account the fact that some channels may be noisier than others. Beware that guessing which channel is noisy without actually seeing the channels individually is not straightforward and can be counterintuitive. A pixel which is completely red may not be caused by noise on the R channel, but actually by noise on G and B channels.
---

# Raw overexposed warning : https://ansel.photos/en/doc/views/toolboxes/raw-overexposed/

Highlight areas of the image where color channels of the raw input file are clipped.

Clipped color channels imply an overexposed image with loss of information in the affected areas. Some of this information may be recoverable using the [_highlight reconstruction_](../darkroom/modules/highlight-reconstruction.md), [_color reconstruction_](../darkroom/modules/color-reconstruction.md) or [_filmic_](../darkroom/modules/filmic.md) modules.

Click the raw-overexposed icon in the [bottom toolbar](../darkroom/darkroom-view-layout.md#bottom-panel) to show/hide the warning overlay. Right-click the icon to open a dialog containing the following configuration parameters.

mode
: Choose how to mark clipped areas:

: - _mark with CFA color_: Display a pattern of the respective primary colors (red, green, and blue) to indicate which color channels are clipped.

: - _mark with solid color_: Mark clipped areas with a user defined solid color (see below) independent of the affected color channels.

: - _false color_: Set clipped color channels to zero in the affected areas.

color scheme
: Choose the solid color for the _mark with solid color_ mode.

clipping threshold
: Set the threshold defining what values are considered to be overexposed. You can safely leave this slider at its default value of 1.0 (white level) in most cases.
---

# Retouch : https://ansel.photos/en/doc/views/darkroom/modules/retouch/

Remove unwanted elements from your image by cloning, healing, blurring and filling using drawn shapes.

This module includes a "heal" tool (based on the heal tool from GIMP), as well as "fill" and "blur" modes. It can also take advantage of wavelet decomposition, allowing the image to be separated into layers of varying detail (from coarse to fine) which can be selectively retouched before being recombined to produce the output image.

## Clone and heal

Cloning allows a part of the image (the _target_) to be hidden by replacing it with an area copied from elsewhere in the image (the _source_). For example, you may wish to remove a small cloud from a blue sky:

![retouch-original](rt-original.jpg)

The simplest way to do this is with the _cloning_ tool ![retouch-clone-icon](rt-clone-icon.jpg). The following example uses a circular shape to clone out the cloud using the circle of blue sky beside it:

![retouch-clone](rt-clone.jpg)

In many cases, the edges of the _source_ shape won't precisely match the surroundings of the _target_, leading to unnatural looking results. In this example, the sample of sky we chose as the source was slightly darker than the target, leaving a faint outline of the circular shape used in the cloning process:

![retouch-clone-nocontrol](rt-clone-nocontrol.jpg)

In such cases, the _heal_ tool ![retouch-heal-icon](rt-heal-icon.jpg) is more appropriate. With this tool, the color and luma of the sample is blended to fit better with the surroundings. In this example, using _heal_ instead of _clone_ produces a much more pleasing result:

![retouch-heal-nocontrol](rt-heal-nocontrol.jpg)

### Source and target shapes

Once you have chosen _heal_ or _clone_ mode, you must choose the shape you wish to use (_circle_, _ellipse_, _path_ or _brush_ -- see the [drawn masks](../masking-and-blending/masks/drawn.md) section for details). The source and target patches will both use the same shape.

When you hover over the image with your mouse, a plus symbol (+) will appear to indicate where the _source_ shape will be placed by default. Your normal mouse cursor will indicate the position of the target shape:

![retouch-source-cross](rt-source-cross.jpg)

It is recommended that you choose the position of the source shape first, followed by the position of the target, as follows:

- Shift+click to set the position of the source shape in "relative mode". The position of the "plus" (+) symbol will move to the clicked location and will remain in a fixed position _relative to your cursor_ until you click on the image to begin placing your target shape. If you place subsequent shapes without first changing the target location, the source shape will be placed at the same offset from the target shape as was used for the first stroke.

- Ctrl+Shift+click to change the position of the source shape in "absolute" mode. As above, the position of the "plus" (+) symbol will move to the clicked location, but will _remain in the same absolute position_ even if you move your mouse. You can then click on the image to begin placing the target shape. If you place subsequent shapes without first changing the target location, exactly the same source position will be used, fixed in the absolute coordinate system of the image.

Once you have placed your source and target shapes on the image, they can be adjusted manually with your mouse.

---

**Note:** For _circle_ and _ellipse_ shapes only, you can place both the source and target shapes with a single click-and-drag motion: Click on the desired target location and then drag, releasing your mouse button when you reach the desired source location. This operation does not affect the placement of subsequent shapes.

---

## Fill and blur

The _clone_ and _heal_ tools both require the use of another part of the image to "fill in" the region being hidden. Sometimes there is no suitable sample in the image to use for this purpose. In such cases, the _retouch_ module offers two further options:

![retouch-fill-icon](rt-fill-icon.jpg) fill tool
: Fill the drawn region with a selected color.

![retouch-blur-icon](rt-blur-icon.jpg) blur tool
: Apply a blur to the drawn region, smoothing out any details.

These two options are most useful when used together with wavelet decomposition, where they can be used to smooth over features within a selected detail layer.

## Wavelet decomposition

Wavelets allow an image to be decomposed into a number of layers each containing varying levels of detail, so that you can work on each detail layer independently and then recombine them at the end. This is particularly useful in portrait photography, where you can deal with skin blotches and blemishes on a coarse layer of detail, leaving the finer skin texture untouched.

This method can be used with the healing tool, for example, to paint over a spot that appears in one of the coarse detail layers, while leaving the hairs in the fine detail layers intact:

![retouch-beard-preserve](rt-beard-preserve.jpg#w50)

It can also be used with the blur tool to even out coarse blotches in the skin, again without impacting the finer details.

## Module controls

![retouch-overview](rt-overview.jpg)

### Retouch tools

The _retouch tools_ section consists of two items:

shapes
: The number after the _shapes_ label indicates how many shapes have been placed on the image, either directly or within a wavelet layer.

: Click on one of the shape icons to draw a new shape on the image (see [drawn masks](../masking-and-blending/masks/drawn.md) for details).

: Ctrl+click on a shape icon to draw multiple shapes continuously (right-click to cancel).

: Click the _show and edit shapes_ ![retouch-shapes-icon](rt-shapes-icon.jpg) button to show and edit any existing shapes for the currently-selected wavelet scale.

algorithms
: Choose a retouching algorithm (clone, heal, fill or blur). Ctrl+click to change the algorithm used for the currently-selected shape. Shift+click to set the default algorithm (used for new images or when you reset module parameters).

### Wavelet decompose

The _wavelet decompose_ section centres around a bar graph that shows how the image has been decomposed into detail (scale) layers. The key features of the bar graph are:

- The black square on the left represents the entire image before decomposition.
- The gray squares show the various wavelet detail layers, with fine details to the left, and coarse details to the right.
- The white square on the far right represents the residual image (the remainder of the image after the detail layers have been extracted).
- A light gray dot in a square indicates the currently-selected layer. Click on another square to select a different layer.
- The light gray bar running along the top indicates which levels of detail are visible at the current zoom level. Zoom in to see finer details.
- The triangle at the bottom shows how many layers the image has been decomposed into. Drag the triangle to the right to create more layers. Drag it to the left to decrease the number of layers. By default no wavelet decomposition is performed.
- The triangle at the top shows the current value of the "merge from" parameter (see below).
- The orange lines under the squares indicate which layers have retouch edits applied.

The remaining items in this section are:

scales
: Shows how many detail layers the image has been decomposed into. Zero indicates that no wavelet decomposition has been performed.

current
: Shows which layer is currently selected (also marked with the light gray dot on the bar graph).

merge from
: This setting allows a single edit to be applied to multiple consecutive scales within a group starting from the coarsest scale (excluding the residual image) down to the scale selected. For example, if "merge from" is set to 3 and the maximum scale is 5 then all edits that are added to scale 5 will be applied to scales 3, 4 and 5. Edits added to scale 4 will be applied to scales 3 and 4, and edits added to scale 3 will be applied to scale 3 only. If you set _merge from_ to 0, then merging is disabled, and all edits apply only to the scale that they are defined in. Setting _merge from_ to the highest scale available (in this example, 5) also disables merging.

```
           merge_from
               v
   0   1   2   3   4   5    scale
               <-------o    scale 5 edits
               <---o        scale 4 edits
               o            scale 3 edits
           o                scale 2 edits
       o                    scale 1 edits
```

![retouch-display-icon](rt-display-icon.jpg) display wavelet scale
: Display the currently-selected wavelet layer on the center image. Selecting this option brings up an additional control -- _preview single scale_.

preview single scale
: An additional control that allows the black, white and gray points of the wavelet scale preview to be adjusted to make it easier to see. Click the ![auto-levels-icon](rt-auto-levels-icon.jpg) to set these values automatically. This does not affect the module's operation -- only the wavelet scale preview.

![retouch-cut-icon](rt-cut-icon.jpg) cut
: Cut all shapes from the currently-selected layer and place them into the clipboard.

![retouch-paste-icon](rt-paste-icon.jpg) paste
: Move the shapes on the clipboard to the currently-selected layer.

![retouch-hide-icon](rt-hide-icon.jpg) temporarily switch off shapes
: Toggle all shapes (whether on the current layer or not) on or off, temporarily removing the module's effect.

![retouch-mask-icon](rt-mask-icon.jpg) display masks
: Show the target shapes associated with the currently-selected layer with a yellow overlay.

### Shapes

This section allows you to modify settings related to the currently-selected shape:

shape selected
: Indicates which shape is currently selected, and what type of shape it is.

fill mode
: If the _fill_ algorithm has been chosen for the currently-selected shape, choose whether to "erase" or fill the selected shape with a chosen "color".

fill color
: If a _fill mode_ of "color" has been chosen, select the color to fill the shape with. You can click to select or enter a custom rgb value or use the color picker to take a sample from the image.

brightness
: If the _fill_ algorithm has been chosen for the currently-selected shape, fine-tune the color's brightness. This slider also works in "erase" mode.

blur type
: If the _blur_ algorithm has been chosen for the currently-selected shape, choose whether to apply a "gaussian" or "bilateral" blur.

blur radius
: If the _blur_ algorithm has been chosen for the currently-selected shape, choose the radius of the blur.

mask opacity
: Alter the opacity of the mask associated with the currently-selected shape. An opacity of 1.0 indicates that the shape is completely opaque and the module's effect is fully applied, whereas a value less than 1.0 indicates that the effect applied by the shape is blended with the underlying image to the degree indicated by the slider.

### Show guides

Tick the box to show guide overlays whenever the module is activated. Click the icon on the right to control the properties of the guides. See [guides & overlays](../../toolboxes/guides-overlays.md) for details.

### Panning and zooming the image

While creating or editing a shape, mouse actions are applied to the current shape.  If you need to move or zoom the portion of the image shown in the center view, hold down the 'a' key while dragging the mouse or using the scroll wheel.  While the key is held down, the mouse actions will apply to the entire image rather than the current shape. Holding down the key will also temporarily suppress generating new shapes in continuous-creation mode.
---

# Rotate and perspective : https://ansel.photos/en/doc/views/darkroom/modules/rotate-perspective/

Automatically correct for converging lines, a form of perspective distortion. The underlying mechanism is inspired by Markus Hebel's [_ShiftN_](http://www.shiftn.de/) program. This module also allows for the rotation of the image to be adjusted.

Perspective distortions are a natural effect when projecting a three dimensional scene onto a two dimensional plane and cause objects close to the viewer to appear larger than objects further away. Converging lines are a special case of perspective distortions frequently seen in architectural photographs -- parallel lines, when photographed at an angle, are transformed into converging lines that meet at some vantage point within or outside of the image frame.

This module is able to correct converging lines by warping the image in such a way that the lines in question become parallel to the image frame. Corrections can be applied in a vertical and horizontal direction, either separately or in combination. In order to perform automatic correction the module is able to analyze the image for suitable structural features consisting of line segments. You can also set the line structures manually by drawing a "perspective rectangle" or drawing multiple horizontal and vertical lines onto the image. Based on these (automatic or manually-drawn) line segments a fitting procedure is initiated, which determines the best values for the module's parameters.

As the most common use case for this module is for rotation, the perspective correction controls are hidden by default. Click the "perspective" header to expand the controls.

While the module is active (and none of the structure buttons are selected) you can right-click and drag anywhere on the image to define a horizontal or vertical line. This will cause the rotation parameter to be automatically adjusted to make the drawn line horizontal/vertical with respect to the image frame.

## Perspective correction workflow

### Structure

The first step is to obtain details about the horizontal and/or vertical structures in the image. Three alternative methods are provided to do this:

#### Manually draw structure lines

Click on the ![draw-structure-lines-icon](icon-draw-structure-lines.jpg) and then click on one of the "fit" icons to complete the process. You can re-enter this mode to edit your drawn lines at any time. Edit a line by clicking and dragging on the line or the end nodes, and right-click a line to delete it. Once you are happy with your changes, re-select a "fit" icon to complete the process.

#### Manually define a perspective rectangle

Click on the ![draw-structure-rectangle-icon](icon-draw-structure-rectangle.jpg)icon to enable perspective rectangle drawing mode. This will draw a rectangle on the screen and you can grab and move the corners of the rectangle so that the left and right sides fall on lines you wish to make vertical, and the top and bottom fall on lines you wish to make horizontal. Once you are happy with your rectangle, click one of the "fit" icons to complete the process. You can re-enter this mode to edit your drawn rectangle at any time. Once you are happy with your changes, re-select a "fit" icon to complete the process.

#### Automatically detect structure

Click the ![get-structure-icon](icon-get-structure.jpg) icon to analyze the image for structural elements -- Ansel will automatically detect and evaluate line elements. Shift+click to apply a contrast enhancement step before performing further analysis. Ctrl+click to apply an edge enhancement step before performing further analysis. Both variations can be used alone or in combination if the default analysis is not able to detect a sufficient number of lines.

Only lines that form a set of vertical or horizontal converging lines are used for subsequent processing steps. Line segments are displayed as overlays on the image canvas, with the type of line identified by color as follows:

green
: Vertical converging lines

red
: Vertical lines that do not converge

blue
: Horizontal converging lines

yellow
: Horizontal lines that do not converge

gray
: Other lines that are not of interest to this module

Lines marked in red or yellow are regarded as outliers and are not taken into account during the automatic fitting step. This outlier elimination involves a statistical process using random sampling which means that each time you press the "get structure" button the color pattern of the lines will look slightly different.

You can manually change the status of line segments: Left-Click on a line to select it (turn the color to green or blue) and Right-click to deselect it (turn the color to red or yellow). If you keep the mouse button pressed, you can use a sweeping action to select/deselect multiple lines in a row. The size of the select/deselect brush can be changed with the mouse wheel. Hold down the Shift key and keep the left or right mouse button pressed while dragging to select or deselect all lines in the chosen rectangular area.

Once you are happy with the detected lines, select a "fit" icon to complete the process.

### Fit

Once you are happy with the identified horizontal and vertical lines, using one of the methods above, click on one of the "fit" icons to automatically set the module's parameters based on the defined structure. The image and the overlaid lines are then displayed with perspective corrections applied.

You may choose to automatically apply just the vertical corrections ![vertical-icon](icon-vertical.jpg). Ctrl+click on any of the icons to apply a rotation without the lens shift. Shift+click on any of the icons to apply the lens shift without any rotation.

### Rotate

Once you are happy with the applied perspective corrections, you may wish to perform a final rotation correction either by adjusting the rotation parameter or right-clicking and dragging the image to define a horizontal/vertical line.

## Module controls

rotation
: Control the rotation of the image around its center to correct for a skewed horizon. To rotate by more than the default soft limit of ten degrees, right click and enter the desired value up to 180 degrees.

automatic cropping
: When activated, this feature crops the image to remove any black areas at the edges caused by the distortion correction. You can either crop to the "largest area", or to the largest rectangle that maintains the original aspect ratio ("original format"). In the latter case you can manually adjust the automatic cropping result by clicking in the clip region and moving it around. The size of the region is modified automatically to exclude any black areas.

lens shift (horizontal)
: Correct converging horizontal lines (i.e. to make the _blue_ lines parallel).

lens shift (vertical)
: Correct converging vertical lines (i.e. to make the _green_ lines parallel). In some cases you can obtain a more natural looking image if you correct vertical distortions to an 80 ~ 90% level rather than to the maximum extent. To do this, reduce the correction slider after having performed the automatic correction.

shear
: Shear the image along one of its diagonals. This is required when correcting vertical and horizontal perspective distortions simultaneously.

guides
: When activated, a grid is overlaid on the image to help you judge the quality of the correction.

lens model
: This parameter controls the lens focal length, camera crop factor and aspect ratio that used by the correction algorithm. If set to "generic" a lens focal length of 28mm on a 35mm full-frame camera is assumed. If set to “specific”, the focal length and crop factor can be set manually using the sliders provided.

focal length
: If the _lens model_ is set to "specific", set the lens focal length. The default value is taken from the image's Exif data, and can be overridden by adjusting the slider manually.

crop factor
: If the _lens model_ is set to "specific", set the camera crop factor. You will normally need to set this value manually.

aspect adjust
: If the _lens model_ is set to "specific", this parameter allows for a free manual adjustment of the image's aspect ratio.  This is useful for "unsqueezing" images taken with an anamorphic lens (which changes the ratio of image height to width).

structure
: Define horizontal and vertical lines in the image using a manual or automatic method (see workflow section for details).

fit
: Set the distortion correction sliders automatically based on the identified structure (see workflow section for details).

show guides
: Tick the box to show guide overlays whenever the module is activated. Click the icon on the right to control the properties of the guides. See [guides & overlays](../../toolboxes/guides-overlays.md) for details.

## Examples

Here is an image with a skewed horizon and converging lines caused by directing the camera upwards:

![prespective-correction-example-before](perspective-correction-example-before.jpg)

Here is the image after having corrected for vertical and horizontal perspective distortions using automatic structure detection. Note the framing adjustment made by the automatic cropping feature and the still-visible overlay of structural lines:

![prespective-correction-example-after](perspective-correction-example-after.jpg)
---

# Rotate pixels : https://ansel.photos/en/doc/views/darkroom/modules/rotate-pixels/

The sensors of some cameras (such as the Fujifilm FinePix S2Pro, F700, and E550) have a diagonally oriented Bayer pattern instead of the usual orthogonal layout.

Without correction this would lead to a tilted image with black corners. This module applies the required rotation.

Ansel detects images that need correction using their Exif data and automatically activates this module where required. For other images the module always remains disabled.

The module has no controls.
---

# Scale pixels : https://ansel.photos/en/doc/views/darkroom/modules/scale-pixels/

Some cameras (such as the Nikon D1X) have rectangular instead of the usual square sensor cells. Without correction this would lead to distorted images. This module applies the required scaling.

Ansel detects images that need correction using their Exif data and automatically activates this module where required. For other images the module always remains disabled.

The module has no controls.
---

# Scopes : https://ansel.photos/en/doc/views/toolboxes/scopes/

The scopes module, at the top of the darkroom [left panel](../darkroom/darkroom-view-layout.md#left-panel), provides graphical depictions of the image's tones and [chromaticity](../../color-management/color-dimensions.md#definitions). It also hosts the [global color picker](global-color-picker.md).

Drag the bottom edge of the module to resize the scope vertically.

{{< note >}}
For performance, the scopes are computed from the lower-resolution preview pipeline (the same image shown in the [navigation](navigation.md) module), not from the full-resolution center image. Fine detail may therefore not be represented exactly.
{{< /note >}}

## Choosing what to show

Use the _hamburger_ menu in the module header to access the following sub-menu items:

Show data from
: Where in the [pixelpipe](../darkroom/pixelpipe/_index.md) the data is sampled, which determines the color space of the plot:
    - **Raw image** — straight off the sensor, before color processing.
    - **Output color profile** — at the [output color profile](../darkroom/modules/output-color-profile.md) stage.
    - **Final display** — after the full pipeline, in display space (what you actually see).

Display
: The type of scope to draw: 
    - **Histogram**, 
    - **Waveform (horizontal)**, 
    - **Waveform (vertical)**, 
    - **Parade (horizontal)**, 
    - **Parade (vertical)**,
    - **Vectorscope**.

## Scope types

### Histogram

The histogram shows the distribution of pixels by lightness for each RGB channel. The x-axis runs from 0 % to 100 % lightness; the y-axis is the pixel count at each lightness. Values piling up against the left or right edge indicate shadow or highlight clipping.

![histogram](histogram.jpg)

### Waveform

The waveform shows the same tonal distribution as the histogram, but in a spatial context.

In the **horizontal** waveform, the x-axis matches the x-axis of the image: the left of the plot corresponds to the left of the image. The y-axis is lightness — the top is 100 % (values above may clip), the middle 50 %, the bottom 0 %. The brightness of each point is the number of pixels at that image position and lightness.

![horizontal waveform](waveform.jpg)

The **vertical** waveform maps the image's y-axis instead, which can be more readable for portrait-format images.

![vertical waveform](waveform-vertical.jpg)

### Parade

The parade shows the same data as the waveform, but with the red, green and blue channels drawn side by side (again in a horizontal or vertical variant). This is useful for matching the intensities of the three channels and for judging color casts.

![horizontal RGB parade](parade.jpg)

### Vectorscope

The vectorscope shows [chromaticity](https://en.wikipedia.org/wiki/Chromaticity) independently of lightness and of spatial position. Distance from the center is chroma; angle is hue. Regions are tinted with the color they represent, and more frequently used colors appear brighter, so the plot conveys color "volume". A hue ring marks the maximum chroma of each hue for the current profile, with the RGB primaries and secondaries marked by circles.

![vectorscope](vectorscope.jpg)

The vectorscope is computed in the [CIELUV](https://en.wikipedia.org/wiki/CIELUV) color space.

#### Caveats

- The hue ring is not a [gamut check](gamut.md): a color can sit inside the ring yet still be out of gamut because of its lightness.
- There is deliberately no "skin tone line", which is a flawed generalization rather than a standard.
- The vectorscope is a colorimetric encoding, which inevitably diverges from a viewer's perception.
- To neutralize a color cast from a reference chart, the color-checker workflow in [color calibration](../darkroom/modules/color-calibration.md#extracting-settings-using-a-color-checker) is faster and more accurate.
---

# Sharpen : https://ansel.photos/en/doc/views/darkroom/modules/sharpen/

Sharpen the details in the image using a standard UnSharp Mask (USM).

This module works by increasing the contrast around edges and thereby enhancing the _impression_ of sharpness of an image. This module is applied to the L channel in Lab color space.

---

_**Note:** The USM algorithm used in this module performs blurs in Lab color space, which can produce undesirable effects, and is no longer recommended. Instead use the presets offered by the [contrast equalizer](./contrast-equalizer.md) module for deblurring or the [local contrast](./local-contrast.md) module for general sharpness._

---

## Module controls

radius
: The unsharp mask applies a gaussian blur to the image as part of its algorithm. This parameter controls the radius of that blur which, in turn, defines the spatial extent of the edge enhancement. Very high values will lead to ugly over-sharpening.

amount
: The strength of the sharpening.

threshold
: Contrast differences below this threshold are excluded from sharpening. Use this to avoid amplification of noise.
---

# Snapshots : https://ansel.photos/en/doc/views/toolboxes/snapshots/

Store snapshots of the center image and compare them against the current edit.

A snapshot is a frozen bitmap of the darkroom center view, taken at any point during editing and later overlaid onto the current view for a side-by-side comparison while you tune a module. It can also be combined with the [history of changes](history-stack.md) to compare a snapshot against different stages of development.

## Taking and comparing snapshots

Click **take snapshot** to capture the current center view. Each snapshot is listed above the button; its name reflects the module selected in the history and its position at the time it was taken.

Click a snapshot's name to display it: this splits the view between the saved snapshot and the current image. Drag the split line to move it. Hover the split line to reveal a small rotation icon at its center; click it to rotate the split orientation (the snapshot and current image cycle between top/bottom/left/right). An arrow marked **S** always indicates which side is the snapshot.

Click the snapshot name again to hide the overlay and return to editing. Use the module's reset button to clear all snapshots.

{{< note >}}
Snapshots are kept for the duration of your Ansel session, so you can also use them to compare against a duplicate edit of the same image: take a snapshot, switch to the other version, and enable the snapshot overlay as usual.
{{< /note >}}
---

# Soft proof : https://ansel.photos/en/doc/views/toolboxes/soft-proof/

View your image rendered using a selected color profile.

Click the soft-proof icon in the [bottom toolbar](../darkroom/darkroom-view-layout.md#bottom-panel) to activate soft-proof display mode. This lets you preview your image rendered through a printer profile, to see how colors will end up on the final print. A “soft proof” message at the bottom-left of the image confirms you are in soft-proof mode.

Right-click on the icon to open a dialog with the following configuration parameters. For each of these parameters, the list of available profiles is read from `$ANSEL/share/ansel/color/out` and `$HOME/.config/ansel/color/out` (where `$ANSEL` represents Ansel's installation directory and `$HOME` your home directory).

display profile
: Set the color profile for the display. The option “system display profile” is the preferred setting when working with a calibrated display. The profile is taken either from your system's color manager or from your X display server. The method Ansel uses to detect your system display profile can be changed in [preferences > miscellaneous](../../preferences-settings/miscellaneous.md). For more information see the [display profile](../../color-management/display-profile.md) section.


intent
: Set the rendering intent for your display -- only available if rendering with LittleCMS2 is activated. See [rendering intent](../../color-management/rendering-intent.md) for a list of available options. This option appears twice -- once for the "display profile" and once for the "preview display profile".

softproof profile
: Set the color profile for soft proofing. Typically these profiles are supplied by your printer or generated during printer profiling.

histogram profile
: Set the color profile of the histogram. None of the available options are ideal, however, "system display profile" is probably the _least bad_ setting, since all other profiles are derived from the display color space and at least the values will conform to what you see on screen.
---

# Surface blur : https://ansel.photos/en/doc/views/darkroom/modules/surface-blur/

Smooth image surfaces while preserving sharp edges using a bilateral filter.

This module _can_ be used to denoise images, however you should be aware that bilateral filters are susceptible to overshoots and Ansel offers much better alternatives. For example, the [_astrophoto denoise_](./astrophoto-denoise.md) module uses a non-local means denoising algorithm, and the [_denoise (profiled)_](./denoise-profiled.md) module provides a choice between non-local means and wavelet denoising algorithms.

The _surface blur_ module blurs noise within the surfaces of an image by averaging pixels with their neighbors, taking into account not only their geometric distance but also their distance on the range scale (i.e. differences in their RGB values). It can be particularly useful if one RGB channel is more noisy/needs more smoothing than the others. In such a case, use the [_color calibration_](./color-calibration.md) module to examine the channels one by one, in order to set the blur intensities accordingly.

The module can also be used as a creative filter to provide interesting effects, for example to lend an image a cartoon-like appearance. When used with chrominance blending, it can also be used to even out the color of a surface (for example, to remove skin redness).

This module is resource-intensive and slows down pixelpipe processing significantly. You should consider activating it late in your workflow.

## Module controls

radius
: The spatial extent of the gaussian blur.

red, green, blue
: The blur intensity for each of the RGB channels.
---

# Tagging : https://ansel.photos/en/doc/views/toolboxes/tagging/

Attach tags to images, and manage the tag dictionary.

Tags provide a means of adding information to images using a keyword dictionary. You can also manage tags as a hierarchical tree, which can be useful when their number becomes large.

Tags are physically stored in [XMP sidecar files](../lighttable/digital-asset-management/sidecar.md) as well as in Ansel's library database and can be included in [exported](./export.md) images.

In Ansel, attaching tags is also _the_ way to build [collections](./collections.md): a collection is the dynamically-queried set of all images sharing a given tag, always kept up to date. So the everyday job of this module — attaching a tag to images — is the same act as “putting images into a collection”. The tag _dictionary_ itself (creating, renaming, deleting, importing and exporting tags) is a separate, occasional, housekeeping task and lives in its own window.

## Definitions

The following definitions assume that you have set up a single tag named "`places|France|Nord|Lille`".

tag
: A tag is a descriptive string that may be attached to an image. A tag can be a single term or a sequence of connected terms forming a path, separated by the pipe symbol. For example, "`places|France|Nord|Lille`" defines a single tag, where each term in the path forms a smaller subset of those before it. You can attach as many tags to an image as you like.

: You can assign properties (name, private, category, synonyms and image order) to a tag.

node
: Any path that forms part of a tag is a node. In the above example, "`places`", "`places|France`", "`places|France|Nord`" and "`places|France|Nord|Lille`" are all nodes. In the hierarchical tree view, the nodes form the branches and leaves of the tree.

free node
: Any node that is not explicitly defined as a tag is called a free node. In the above example, "`places`", "`places|France`" and "`places|France|Nord`" are all free nodes. You cannot set any properties, except “name”, for a free node and you cannot add a free node to an image. See the “multiple tags” section below for more information.

category
: Any tag can be flagged by the user as being a “category”.

## Multiple tags

The above definitions considered a simple example – a single tag and its properties. Consider instead the scenario where the following four pipe-delimited tags are each separately defined in Ansel.

```
places|France|Nord|Lille
places|France|Nord
places|France
places|England|London
```

In this case the nodes are

```
places
places|France
places|France|Nord
places|France|Nord|Lille
places|England
places|England|London
```

The only free nodes are "`places`" and "`places|England`". Both of these free nodes are also (by implication) categories.

You can attach any of these tags to any image. Any tags attached to an image, except category tags, can be included when that image is [exported](./export.md).

If you attach the "`places|France|Nord|Lille`" tag to an image, the "`places|France|Nord`" and "`places|France`" tags are also implicitly attached to that image (you don’t need to attach them manually). Note that this is only true here because those additional tags have been separately defined -- the "`places`" node is not included because it is a "free node" (not a tag).

## Module layout

The tagging experience is split across two surfaces, matching the two distinct jobs of culling (fast, frequent) and dictionary maintenance (occasional):

```mermaid
flowchart TB
    subgraph Sidebar["Tags sidebar — lighttable / map (top to bottom)"]
        C["view (list/tree) · sort (name/count)"]
        B["Tag entry + ✓ validate button —<br/>type or pick a tag, then Enter or ✓ to attach"]
        A["Attached tags list — tags on the hovered / selected<br/>images, shown as a list or a tree, trash icon on each row"]
        D["☑ show system tags"]
    end
    M["module gear menu → “manage tags…”"]
    subgraph Popup["“manage tags…” window"]
        E["Tag dictionary — list / tree, multiple selection,<br/>no per-image checkboxes"]
        F["search box · new · import… · export… · tree/list · suggestions"]
        G["suggestion settings: confidence · recent-tags count"]
    end
    M -->|opens| Popup
```

The **sidebar toolbox** (the _Tags_ module in the left panel) is for the “one image → many tags” workflow: it shows the tags already on the current image and lets you attach more. The **manage tags window** is for the dictionary itself (create / rename / delete / import / export) and its suggestion settings. It never attaches anything to your images — it only edits the tags. Open it from the module's gear (preset) menu → **manage tags…**.

### Sidebar toolbox

From top to bottom, the sidebar contains:

- **Display controls** — a row with two combo boxes:

  view
  : Chooses how the attached list below is rendered: **list** shows each tag as its whole `a|b|c` path on one line; **tree** breaks the paths on the pipe `|` and shows them hierarchically, so common parents are grouped.

  sort
  : Chooses whether to order the attached tags by **name** (alphabetically) or by **count** (how many of the targeted images carry each tag).

- **Tag entry** — a text box where you type or pick a tag to attach. As you type, an autocompletion list of matching existing tags appears below it; pick one with the arrow keys/mouse, or keep typing a brand-new name. A small clear (✕) icon inside the box empties it.

  **validate** — the check-mark button to the right of the entry attaches the typed (or picked) tag to the target images, exactly like pressing Enter. It is there so that, after choosing a tag from the autocompletion list with the mouse, you do not have to reach for the keyboard.

- **Attached tags list** — the tags attached to the image(s) currently under your mouse cursor (if hovering over an image in the lighttable), or to the currently selected image(s) (if not hovering). Each user tag row carries a trash icon on its right that detaches that single tag in one click. Automatically generated Ansel system tags (with names starting “`Ansel|`”) are shown for information only — only when _show system tags_ is enabled — and have no trash icon. The number in brackets next to a tag indicates how many of the targeted images carry it. You can adjust the height of this list by holding Ctrl and scrolling with your mouse wheel. The list is rendered as a flat list or a hierarchical tree according to the _view_ control above (see [Detach tag](#detach-a-tag) for the right-click actions).

- **show system tags** — a checkbox at the bottom that reveals the system tags Ansel manages automatically (the `Ansel|…` family). They are hidden by default.

The [manage tags window](#manage-tags-window) is opened from the module's gear (preset) menu, not from the sidebar.

### Manage tags window

Open this window from the module's gear (preset) menu → **manage tags…**. It is a separate window dedicated to maintaining the dictionary, and contains:

- a **search box** at the top that filters the tag list as you type (its ✕ icon clears it);
- the **tag dictionary**, listing every tag known to Ansel, either as a flat _list_ or as a hierarchical _tree_. Multiple tags can be selected at once (Ctrl-click / Shift-click) for bulk operations;
- a row of buttons:

  new
  : Create a new tag using the name typed in the search box.

  import...
  : Import tags from a Lightroom keyword file.

  export...
  : Export the whole dictionary to a Lightroom keyword file.

  suggestions
  : Show a list of suggested keywords based on the keywords already associated with the selected images (see the _suggestion settings_ below). CAUTION: this view queries the database so it might be slow.

  list / tree
  : Toggle the dictionary between the flat _list_ view and the hierarchical _tree_ view.

- the **suggestion settings** (formerly a separate _preferences_ dialog), applied immediately:

  suggested tags level of confidence
  : Level of confidence used to include a tag in the suggestions list (default 50):
  : - 0: display all associated tags,
  : - 99: match tags with a 99% confidence level,
  : - 100: an essentially unreachable level of confidence, so no matching tags are returned. Use 100% to disable the best-matched suggestions list (faster).

  number of recently attached tags
  : Number of recently attached tags to include in the suggestions list (default 20). A value of "-1" disables the most-recently-attached suggestions list.

In the hierarchical _tree_ view, a name in italics represents either a free node or a category. You can adjust the height of the dictionary by holding Ctrl while scrolling with your mouse wheel.

All other dictionary operations (rename, change path, delete, “set as a tag”, navigation to a tag collection, …) are reached by right-clicking a tag — see [Usage](#usage) below.

{{< note >}}
The management window only edits the tag dictionary; it cannot attach tags to images. To attach a tag, use the sidebar entry, the quick-tag box (Ctrl+T), or drag images onto a tag row in the [collections](./collections.md) tab of the _Library_ module.
{{< /note >}}

## Usage

### Attach a tag

To attach an existing or new tag to the image(s) under the cursor (or, failing that, the selected images):

- Type its name in the sidebar **tag entry** — picking from the autocompletion list if it already exists — then press Enter or click the **✓ validate** button. Hierarchical tags are created using the pipe symbol “`|`” to separate nodes. If the tag does not yet exist it is created, then attached.
- Press **Ctrl+T** to open a small text box at the bottom of the central lighttable view, type a tag name and press Enter. The tag is created if needed and attached to all the selected images.
- **Drag** an image or group of images from the lighttable/filmstrip and **drop them onto a tag row** in the [collections](./collections.md) tab of the _Library_ module. This attaches that tag to the dragged images (no file is moved).

For a tag that is attached to only _some_ of the targeted images (shown with a count lower than the number of images), right-click it in the attached list and choose **attach tag to all** to extend it to every targeted image.

When hovering over images in the lighttable you can check which tags are attached either in the attached list here, or in the _tags_ attribute of the [image information](./image-information.md) module.

### Detach a tag

From the **attached tags** list in the sidebar:

- click the **trash icon** on the tag's row to detach that one tag;
- **double-click** a tag to detach it;
- select one or several tags and press the **Delete** / Backspace key;
- select one or several tags, **right-click**, and choose **detach tag(s)** to detach them all in one go;
- **right-click anywhere** in the list (even on empty space) and choose **detach all tags** to remove every tag from the targeted images at once.

### Create a tag

There are several ways to create a new tag:

- _Type into the sidebar entry and press Enter (or ✓)._ The tag is created and attached to the target images in one step.
- _Use “create tag…”_ in the dictionary's right-click menu (manage tags window). The tag is created under the selected node (hierarchical) or at the root, and is **not** attached to any image.
- _Use “set as a tag”_ in the right-click menu to turn a free node (e.g. “`places|England`”) into a real tag, so that it gets implicitly attached to all images carrying its sub-tags (e.g. “`places|England|London`”).
- _Import a Lightroom keyword text file_ (the _import…_ button). Existing tags are updated, new ones are created. You can export your tags, edit the file, and re-import it.
- _Import already-tagged images._ Tags found in imported images are added to the dictionary (no opportunity to rename or re-categorize them during import).

A number of tags are generated automatically by Ansel for certain actions — for example “`Ansel|exported`” and “`Ansel|styles|your_style`” identify images that have been exported or had a style applied.

### Edit / rename a tag

In the manage tags window, right-click a single tag:

edit…
: Change the tag's name (you cannot move it to another node here — the pipe “`|`” is not allowed in this field), and set its _private_ and _category_ flags and its _synonyms_. The command is aborted if the new name already exists. These attributes are recorded in the `XMP-dc Subject` and `XMP-lr Hierarchical Subject` metadata. Which tags end up in exports is controlled in the [export](./export.md) module.

: - A tag set as “private” is, by default, not exported.
: - A tag set as “category” is not exported in `XMP-dc Subject`, but is exported in `XMP-lr Hierarchical Subject` (which holds the organization of your tags).
: - “synonyms” enrich the tag information and mainly assist search engines — e.g. “juvenile”, “kid” or “youth” as synonyms of “child”. They can also be used to translate tag names to other languages.

change path…
: Available in _tree_ view only. Lets you change the full path of a node, including the nodes it belongs to (use the pipe “`|`” to specify the hierarchy). The dialog shows how many tagged images would be impacted. This is powerful but can significantly rewrite your images' metadata, so use it carefully. The operation is aborted if it would conflict with an existing tag.

A quick way to reorganize the structure is **drag and drop** of nodes in the _tree_ view: drag any node and drop it onto another node to make it (and its descendants) a child of the target. Dragging over a node opens it automatically (drag over the node's selection indicator to avoid opening it). Drop a node onto the top of the window to move it to the root level. Conflicting moves are aborted.

The “copy to entry” right-click item copies the selected tag into the search box, so you can tweak its name and use _new_ to create a similar tag.

### Delete a tag

Deleting a tag removes it from **all** images (selected or not) and from the database. Because this can impact many images, a confirmation dialog shows how many images are affected. **Take this warning seriously — there is no undo** (short of restoring your database and/or XMP sidecars from a backup).

In the manage tags window:

- right-click a tag and choose **delete tag**;
- select **several** tags (Ctrl-click / Shift-click) and right-click → **delete tags** to remove them all after a single confirmation;
- right-click a branch node and choose **delete node** to delete that node together with all its child tags.

Tags can also be deleted (and renamed) from the [collections](./collections.md) tab of the _Library_ module.

### Import / export

The **import…** button reads a text file in the Lightroom tag file format: existing tags are updated, missing ones are created. The **export…** button writes the entire dictionary to such a file. Round-tripping (export, edit, re-import) is a convenient way to bulk-edit tags.

### Keyboard

- In the sidebar entry, **Enter** attaches the typed/picked tag (creating it if needed). **Shift+Tab** moves focus to the first user tag in the attached list.
- In the attached list, **Tab** returns focus to the entry; **Delete** / **Backspace** detaches the selected tag(s).
- In the manage tags window, the dictionary's **Left/Right** arrows collapse/expand the selected node in _tree_ view; **Tab** / **Shift+Tab** move focus to/from the search box.

### Navigation

To see the images carrying a particular tag, right-click it in the manage tags window and choose **go to tag collection**. This opens a [collection](./collections.md) showing all images with that tag.

Choose **go back to work** from the same menu to return to the collection you had open before, as long as you have not selected another collection in the meantime.
---

# Tone equalizer : https://ansel.photos/en/doc/views/darkroom/modules/tone-equalizer/

Dodge and burn while preserving local contrast.

It works in linear RGB space and utilizes a user-defined mask to guide the dodging and burning adjustments, helping to preserve local contrast within the image.

The following diagram describes how the tone equalizer works:

![tone-equalizer-mask](tone-equalizer-overview.jpg)

1. Create a monochrome [_guided mask_](#masking-tab) that divides the input image into regions of similar luminosity. The resulting mask should blur the fine details within the image so that pixels within each region are all treated similarly, preserving local contrast.

2. Adjust the sliders in the [_simple_](#simple-tab) tab or the equalizer graph in the [_advanced_](#advanced-tab) tab to alter the brightness of the underlying image, based on the brightness of the mask. Exposure can also be adjusted by scrolling with the mouse while hovering the cursor over the preview image (see [_cursor indicator/control_](#cursor-indicatorcontrol) for details).

   In the simple tab, each slider corresponds to a single brightness zone (EV) in the mask, which can be raised or lowered to adjust the exposure of the image where the mask's brightness lies in that zone. Similarly, in the equalizer tab, the horizontal axis of the graph corresponds to the brightness level of the mask, and the vertical axis specifies the required exposure adjustment to pixels where the mask matches that brightness level.

3. The exposure of each pixel of the input image is adjusted using the mask and the equalizer graph. For each pixel, the module looks up the brightness of the mask at that point, finds the matching brightness on the horizontal axis of the equalizer graph, and increases or decreases the exposure accordingly (using the vertical axis of the graph).

It is important that the mask separates the image into regions of similar brightness, and that a suitable amount of blur is applied within those regions. This means that all of the pixels in each region will have their exposure adjusted similarly, without adversely affecting local contrast. Examine your image beforehand to identify which regions you wish to dodge and burn, and use the controls on the [_masking_](#masking-tab) tab to ensure that those areas are reasonably separated in tone within the final mask. This will allow those regions to be adjusted independently.

## Module controls

The controls of the _tone equalizer_ module are divided between three tabs.

display exposure mask
: Click on the icon to the right of this label to show/hide the module's guided mask over the top of the image. This control is available in all three tabs.

### Simple tab

This tab splits the brightness of the guided mask into nine zones (from --8 to 0 EV) and allows you to alter each zone independently. This is a simplified interface and is used to generate the same tone adjustment curve as shown in the [_advanced_](#advanced-tab) tab.

--8 EV ... 0 EV
: Each slider adjusts the exposure of all pixels where the _guided mask_ has the given brightness. If the mask's histogram is evenly spread over the entire tonal range, sliders towards the top generally affect the shadows, whereas sliders towards the bottom generally affect the highlights. You can check the spread of the histogram within the [_advanced_](#advanced-tab) tab.

### Advanced tab

This tab allows you to control the same intensity levels as in the simple tab, though here they are represented as control points on a curve. Behind the curve is a histogram representing the intensity levels of the _guided mask_ (not the underlying image). If the histogram is too bunched up, this means your mask doesn't have a good spread of intensity levels, which makes it harder to independently control the brightness of the different parts of your image. It is therefore recommended that the histogram be adjusted so that it extends the entire range, covering as many control points as possible for maximum flexibility. You can adjust the mask using the controls in the [_masking_](#masking-tab) tab.

Click+drag the control points on the curve to adjust the brightness of all pixels where the mask has the given intensity. If the mask's histogram is evenly spread over the entire tonal range, control points to the left will generally affect the shadows, and control points to the right will generally affect the highlights. Moving a single control point will also affect control points on either side to ensure the curve remains smooth. This behaviour can be adjusted using the _curve smoothing_ control.

curve smoothing
: Control how the curve is interpolated between control points. Move the slider to the right to make the transitions between the control points more gradual, but beware that going past about 0.6 can introduce some instability (oscillations) in the curve due to mathematical constraints. Move the slider to the left for a more well-behaved curve, but beware that this can result in harsher tonal transitions that may damage local contrast.

### Masking tab

This tab contains controls for adjusting the guided mask.

The purpose of the guided mask is to separate out areas with different tonal ranges so that they can be independently brightened or darkened by the tone equalizer. The masking filters are designed to allow sharp edges between these areas to be preserved, while blurring details within a given tonal range, so that the brightness can be adjusted without adversely impacting local contrast. Ideally the mask histogram shown in the _advanced_ tab should be spread out across all of the control points.

To avoid having to switch back and forth between the _advanced_ and _masking_ tabs, a gray bar under the "mask post processing" label displays a representation of the middle 80% of the histogram. By using the controls in this tab to center and spread out this gray bar, you can expect to have a nicely shaped histogram when you return to the "advanced" tab. If you see orange at either end of the gray bar, this means that part of the histogram is outside of the 9 EV range of the mask, and needs to be further adjusted.

![tone-equalizer-mask-histogram](tone-equalizer-mask-histogram.jpg)

When setting up the guided mask you will need to strike a balance between obtaining a smooth blur within tonal regions (to preserve local contrast) and preservation of the boundaries between those regions. Some experimentation will be required to find the best settings. Often the key controls to adjust are the _exposure/contrast compensation_ sliders at the bottom of the module.

Displaying the guided mask while making these adjustments will help you to understand these controls and better judge the quality of the mask.

luminance estimator
: Choose the method by which the luminance of a pixel will be estimated when mapping it to a mask intensity value (default _RGB euclidean norm_).

preserve details
: Choose the smoothing algorithm used to blur the mask:
 - _no:_ Do not smooth the mask (the effect is the same as using normal tone curves). When the module is used to compress dynamic range, this option can cause compression of local contrast. This can be useful when increasing (local and global) contrast.
 - _guided filter:_ Use the original guided filter algorithm to blur the mask while attempting to preserve edges. One of the limitations of this algorithm is that the guided filter is exposure-sensitive, meaning that shadows tend to be blurred more than highlights. Note that this limitation can sometimes be an asset: if one wants to lighten shadows a lot, the guided filter can provide very good local contrast preservation.
 - _average guided filter:_ Use this option in cases where the effect of the guided filter is too strong. In this mode, a geometric mean is taken between the output of the original _guided filter_ algorithm and the output given by the _no_ option.
 - _eigf (default):_ The _exposure-independent guided filter_ solves the problem of the original _guided filter_, in that it makes the degree of blurring independent of the exposure. This means the degree of blurring applied to the highlights and the shadows regions should be about the same. This improved algorithm is now the default option.
 - _averaged eigf:_ This option takes the geometric mean between the _eigf_ mask and the mask generated by the _no_ option, and is useful in cases where the degree of blurring in the mask needs to be mitigated.

filter diffusion
: By default this is set to a value of 1, meaning that the filtering algorithm is run once on the input image to produce the blurred monochrome mask.

: If you increase this to 2, the filtering algorithm will be run once on the input image to produce an intermediate mask, and then a second time on the intermediate mask. As a result, the final mask will be more blurred than when using a single iteration. Progressively higher values will diffuse the mask even further. However, because the mask filtering algorithm is run multiple times, each iteration will increase the processing time.

smoothing diameter
: This controls how much of the surrounding image to take into account when calculating the mask's blur at a particular point, defined as a percentage of the length of the image's longer side (default 5%). With lower values, the transition between darker and lighter areas of the mask will be more pronounced. As the value is increased, transitions become smoother/softer. For the default _exposure-independent guided filter (eigf)_, you should typically use blurring radii around 1-10%. With the original _guided filter_, blurring radii around 1-25% will typically yield better results.

edges refinement/feathering
: Higher values force the mask to follow high contrast edges more closely. Lower values give smoother gradients, but may introduce halos. If required, feathering can be set to values as high as 10,000.

mask post-processing
: This bar provides a representation of the current span of the mask's histogram. It covers the middle 80% of the histogram, dropping the first and last decile to prevent outliers from skewing the indicator too much. Orange indicators at either end mean that the histogram exceeds the upper or lower bounds of its 9 EV range.

mask quantisation
: Apply a degree of posterisation to the mask, so that it tends to centre round a few discrete levels. In some cases, this may be useful to help separate out areas of your image into distinct masking levels.

mask exposure compensation
: Adjust the mask's histogram to the left or right. If you have used the _exposure_ module to adjust the image brightness, you may need to offset that adjustment by using this slider to re-centre the mask's histogram. Click on the wand icon to the right of the slider to set the exposure compensation such that the average of the mask's histogram will coincide with the central --4EV control point. The slider can then be fine-tuned as required.

mask contrast compensation
: Dilate (spread out) or compress the mask's histogram. The wand icon to the right of the slider will propose a reasonable starting point, which can then be fine-tuned to optimize the spread of the histogram under the tone equalizer control points.

### Cursor indicator/control

When the _tone equalizer_ module is enabled and expanded, you can move the mouse pointer over the preview image to show a cursor that displays information about the pixel under the pointer. When this cursor is shown, the mouse wheel can be used to brighten or darken the areas of your image that match the mask intensity level at that point. This provides a convenient way to quickly brighten or darken specific parts of the image.

![tone-equalizer-simple](tone-equalizer-cursor.jpg)

- The cross-hairs indicate the position of the pixel under the cursor
- The text label shows the intensity of the guided mask at that point, in EV
- The shade of the outer circle indicates the intensity of the mask at that point
- If the tone equalizer has brightened or darkened pixels matching this mask intensity, the magnitude of the adjustment is indicated by an arc on the left-hand-side. The longer the arc, the greater the brightness adjustment,
- If there has been an exposure adjustment, the shade of the inner circle indicates the amount of brightening or darkening, relative to the mask's intensity at that point (as indicated by the outer gray circle). That is to say, if the pixel under the crosshairs has been brightened, the inner circle will be a lighter shade of gray than the outer circle; if the pixel has been darkened, the inner circle will be a darker shade of gray than the outer circle.

If you need to move or zoom the portion of the image shown in the center view while the module is expanded, hold down the 'a' key while dragging the mouse or using the scroll wheel.  As long as the key is held down, mouse actions adjust the viewport rather than adjusting the tone curve.

### Presets

The _tone equalizer_ comes with several presets that can use used to compress shadows and highlights. Each comes in two variants, using either the guided filter (gf) or the exposure-independent guided filter (eigf). The variants using the guided filter tend to preserve local contrast in the shadows better those that use the exposure-independent guided filter, but at the price of reducing the local contrast in the highlights. Either of these variants may lead to better results, depending on the image. In both cases, the presets preserve middle-gray, so you shouldn't need to adjust the global exposure after activating the tone equalizer.
---

# Unbreak input profile : https://ansel.photos/en/doc/views/darkroom/modules/unbreak-input-profile/

Add a correction curve to image data. This is required if you have selected certain input profiles in the [_input color profile_](./input-color-profile.md) module.

If you decide to use an ICC profile from the camera manufacturer in the [_input color profile_](./input-color-profile.md) module, a correction curve frequently needs to be pre-applied to image data to prevent the final output from looking too dark. This extra processing is not required if you use Ansel's standard or enhanced color matrices.

The correction curve is defined with a linear part extending from the shadows to some upper limit and a gamma curve covering mid-tones and highlights. For further information please see Ansel's neighboring project [UFRaw](http://ufraw.sourceforge.net).

linear
: The upper limit for the region counted as shadows and where no gamma correction is performed. Typically values between 0.0 and 0.1 are required by the profile.

gamma
: The gamma value required to compensate the input profile. Often the required value is 0.45 (the reciprocal of the 2.2 gamma used by some manufacturer's profiles).
---

# Vignetting : https://ansel.photos/en/doc/views/darkroom/modules/vignetting/

Apply a vignetting effect to the image.

Vignetting is a modification of the brightness and saturation at the borders of the image in a specified shape. Many of the parameters listed below can also be modified with a graphical control that overlays the image when the module has focus, showing the shape and extent of the effect.

---

**Note:** This module is known to provoke banding artifacts under certain conditions. You should consider activating the [_dithering_](./dithering.md) module to alleviate this.

---

_**Also note:** This module can produce unnatural-looking results and should be used with care. Instead, use the [exposure](./exposure.md) module with an [elliptical mask](../masking-and-blending/masks/drawn.md) with a large transition area and, if necessary, use the [color balance rgb](./color-balance.md) module with the same mask to reduce saturation at the edges._

---

## Module controls

scale
: The radius of the vignetting area.

fall-off strength
: The progressiveness of the fall-off. Lower values cause a steeper transition.

brightness
: The intensity of brightening (positive values) or darkening (negative values).

saturation
: Control how strong colors become when desaturated/saturated in the darkened/brightened vignetting area.

horizontal/vertical center
: Shift the center of the vignetting area horizontally/vertically.

shape
: The shape of the vignetting effect. The default value of 1 creates a circular or elliptical area. Smaller values shift the shape to being more square; higher values turn it into a cross-like shape.

automatic ratio
: Automatically adjust the width/height ratio of the vignetting area to match the aspect ratio of the underlying image. This typically causes the vignetting area to become elliptical.

width/height ratio
: Manually adjust the width/height ratio of the vignetting area.

dithering
: Activate random noise dithering to alleviate banding artifacts caused by vignette gradients. Select “8-bit output” to prevent banding on monitor display and for JPEGs. When set to “16-bit output”, only a small amount of dithering is applied, just strong enough to compensate for banding on the fine grained 16-bit level. It is now recommended that you instead use the [_dithering_](./dithering.md) module to alleviate banding artifacts.
---

# Watermark : https://ansel.photos/en/doc/views/darkroom/modules/watermark/

Render a vector-based overlay onto your image. Watermarks are standard SVG documents and can be designed using [Inkscape](http://www.inkscape.org/).

You can also use bitmap (PNG) images.

The SVG processor in Ansel can also substitute strings within an SVG document, allowing image-dependent information to be included in the watermark.

User-designed watermarks should be placed into the directory `$HOME/.config/Ansel/watermarks`. Once in place, use the reload button update the list of available watermarks.

The following is a list of variable strings that are supported for substitution within an SVG document.

In addition to this list you can also use the variable strings defined in the [variables](../../..//variables.md) section.

```
$(WATERMARK_TEXT)             A short free text (max. 63 characters)
$(WATERMARK_COLOR)            The color to use for $WATERMARK_TEXT
$(WATERMARK_FONT_FAMILY)      The font family to use for $WATERMARK_TEXT
$(WATERMARK_FONT_STYLE)       The font style (normal, oblique, italic)
$(WATERMARK_FONT_WEIGHT)      The font weight (boldness)
```

## Module controls

marker
: Choose the watermark to apply. Use the reload button to update the list to include any newly-added watermarks. The extension of the file (png/svg) is shown in brackets.

text
: A free text field in which you can enter up to 63 characters to be printed where referenced by the corresponding watermark. An example is supplied as `simple-text.svg`.

text color
: The color of the text. Click on the colored field to open a color selector dialog which offers you a choice of commonly used colors, or allows you to define a color in RGB color space.

text font
: The font to use (default "DejaVu Sans Book"). Click on the field to open a dialog box showing the fonts available on your system. Fonts can be searched by name and a preview is shown next to the font name. You may specify your own sample text.

opacity
: The opacity of the watermark's rendering.

scale
: The scale of the watermark, with respect to the option selected in the "scale on" parameter.

rotate
: The rotation angle of the watermark.

scale on
: The reference for the scale parameter. The default setting “image” scales the watermark relative to the horizontal image size. Alternatively you can scale the watermark relative to the “larger border” or “smaller border”.

alignment
: Use these controls to align the watermark to any edge or the center of the image.

x offset
: Pixel-independent offset relative to the choice of alignment on the x-axis.

y offset
: Pixel-independent offset relative to the choice of alignment on the y-axis.
---

# White balance : https://ansel.photos/en/doc/views/darkroom/modules/white-balance/

Adjust the white balance of the image by altering the temperature and tint, defining a coefficient for each RGB channel, or choosing from list of predefined white balance settings.

The default settings for this module are derived from the camera white balance stored in the image's Exif data.

White balance is not intended as a "creative" module -- its primary goal is to technically correct the white balance of the image ensuring that neutral colored objects in the scene are rendered with neutral colors in the image. For creative color operations, it is usually better to use other modules such as [_color calibration_](./color-calibration.md) or [_color balance_](./color-balance.md).

---

**Note:** The [_color calibration_](./color-calibration.md) module now provides a more modern and flexible method of controlling white balance. The _color calibration_ module can be enabled by default for new images by selecting "modern" in [preferences > processing > auto-apply chromatic adaptation defaults](../../../preferences-settings/processing.md). Some basic settings are still required (and applied automatically) in the _white balance_ module, so that [_demosaic_](./demosaic.md) works correctly.

---

## Module controls

### Scene illuminant temp

This section provides scene-illuminant _temperature_ and _tint_ controls to adjust the white balace of the image. Click on the 'scene illuminant temp' section label to cycle between 3 different [color modes](#colored-sliders) for the temperature/tint sliders.

temperature
: Set the color temperature in kelvin.

tint
: Alter the color tint of the image, from magenta (tint < 1) to green (tint > 1).

### White balance presets

setting
: Choose from a predetermined list of white balances. The available settings are derived from the presets available in the camera used to take the photograph. The following options are provided in addition to any camera-defined white balance presets.

: - _as shot_ (default): The white balance as reported by the camera

: - _from image area_: Draw a rectangle over a neutral color in the image to calculate white balance from that area.

: - _user modified_: The most recently modified setting. Manual adjustment of temperature, tint or r/g/b channel coefficients will automatically select this option. Choose this setting after selecting any other preset to return parameters to the most recent user-modified state

: - _camera reference_: Set the temperature to the camera reference white point, which is assumed to be D65 (or ~6502K). The white balance channel multipliers are calculated such that pure white in the camera colorspace is converted into pure white in sRGB D65 (where pure white means that each color channel has an equal value).

For convenience the final four modes can also be set by clicking on one of the buttons in the [button bar](#button-bar) above the _setting_ drop-down.

finetune
: Finetune a camera-specific white balance preset. This is only shown if it is available for the camera in question. The direction of adjustment is dependent on the provided presets. If your camera doesn't have white balance presets available, check [this guide](https://github.com/darktable-org/darktable/wiki/White-balance-presets) to see how you can submit your own.

### Channel coefficients

The RGB channel coefficients are automatically calculated from the above parameters and, as such, are hidden by default. You can expand/collapse the channel coefficients section by clicking on either the 'channel coefficients' label or the adjacent triangular button.

red/green/blue
: Set the value of each RGB channel coefficient from 0 to 8

## Additional functionality

### Colored sliders

By default the module's sliders are monochrome. However, two flavors of colored sliders can be enabled in [preferences > darkroom > white balance slider colors](../../../preferences-settings/darkroom.md) or by clicking on the 'scene illuminant temp' section label in the module.

no color (default)
: The background of the sliders is not colored.

illuminant color
: The slider colors represent the color of the light source (the color you are adjusting to in order to achieve neutral white).

effect emulation
: The slider colors represent the effect the adjustment would have had on the scene. This is how most other raw processors show temperature/tint sliders colors.

### Button bar

The button bar is simple addition that allows one-click access to the internal white balance settings. You can disable this by editing your `Anselrc` file. Find the line that says

```
plugins/darkroom/temperature/button_bar=TRUE
```

and change `TRUE` to `FALSE`.

## Usage warning

The only parameters that are used internally by this module's operation are the _rgb channel coefficients_. The _temperature_ and _tint_ sliders are provided as a more user-friendly way to adjust those parameters. The relationship between the channel coefficients and temperature/tint sliders depends on characteristics specific to the camera used to take the photograph. This means that applying white balance settings from an image made with one camera to an image made with another will, in general, not give consistent results.

The mathematical relationship between the two sets of values is not straightforward. It is possible to set the channel coefficients such that there is no valid equivalent temperature and tint setting (mainly where very high temperature values are calculated from the sliders). Editing white balance using temperature and tint on an image previously edited using channel coefficients may therefore give odd results, at least where high temperature values are involved.
---

# Color Science : https://ansel.photos/en/tags/color-science/
---

# Pipeline : https://ansel.photos/en/tags/pipeline/
---

# The scene-referred workflow : https://ansel.photos/en/workflows/scene-referred/

In this article, you will learn what the scene-referred workflow is, how Ansel uses it and why it benefits digital image processing at large.


## Introduction

The scene-referred workflow is the backbone of the Ansel's imaging pipeline. It is a working logic that comes from the cinema industry, because it is the only way to achieve robust, seamless compositing (also known as *alpha blending*) of layered graphics, upon which movies rely heavily to blend computer-generated special effects into real-life footage. For photographers, it is mostly for high dynamic range (HDR) scenes (backlit subject, sunsets, etc.) that it proves itself useful.

If you have been using *any* kind of image processing software so far, you should be already familiar with the *display-referred* workflow, but without knowing neither its name nor its core assumptions. Explaining how *scene-referred* differs from this unspoken _display-referred_ you are used to is going to be a challenge without explaining what *display-referred* was before.

## Making an image

When you take a picture of a scene, your camera sensor turns a light spectrum into 3 electric signals, much like the retina cone cells do it before sending electric impulses to the optical nerve. The specifics are beyond the scope of this article, but let us pretend the light spectrum gets split into three R, G, and B intensities such that the ratio of R, G and B in each pixel is a fair-enough representation of the shape of the spectrum.

```mermaid
graph TD;
	O[fa:fa-bulb Light source] --> A;
    A[<img src='/FL1_lightspectrum.png' width='800' height='447' />] --> B["fa:fa-camera Sensor (Nikon D5100)"];
    B --> C[Sensor RGB<br><img src='/FL1_seen_by_NikonD5100.png' width='400' height='226' />];
    A --> D["fa:fa-eye Human eye"];
    D --> E[Cone LMS<br><img src='/FL1_seen_by_human.png' width='400' height='226' />];
    E ------> F["fa:fa-brain Human brain"];
    F --> G[Color stimulus<br><img src='/FL1_color_scene.png' width='400' height='397' />];

    C --> H["fa:fa-laptop Lots of digital work"];
    H --> HH[sRGB<br><img src='/FL1_sRGB.png' width='400' height='304' />];
    HH --> I[fa:fa-desktop Screen];
    I --> J[<img src='/FL1-sRGB_spectrum.png' width='400' height='223' />];
    J --> K["fa:fa-eye Human eye / fa:fa-brain Human brain"];
    K --> L[Color stimulus<br><img src='/FL1_color_display.png' width='400' height='397' />];
```

{{< note >}}
The graphs above are generated by the author from real data. The CIE FL1 illuminant is a standard daylight fluorescent (energy-saving) light bulb. The "human RGB" is produced using the retina cone cells response (LMS) for the CIE 2015 2° Standard Observer. The sensor RGB is produced from spectral sensitivity measurements. The actual color of the light spectrum is a daylight "white" (close to D65).
{{</ note >}}

We should note that the beginning and the end of the graphic pipeline are a light spectrum which, if identical, will produce the same color stimulus for the observer[^4]. But the light spectra are actually quite different : the large spikes from the FL1 illuminant have been smoothened once displayed on an sRGB screen. The difference between the original scene and its rendition on screen comes from the signal losses that are unavoidable when reducing a spectrum to RGB, which is designed to work only with daylight-like (smooth) spectra. This assumption is often forgotten, and the problem we see here comes at no surprise, given that the FL1 illuminant has a [CRI](https://en.wikipedia.org/wiki/Color_rendering_index) of 76 %. In fact, we can predict how a such illuminant, although technically balanced for D65, will affect color rendition:

{{< figure src="/fl1-report.png" />}}

So this illuminant will make red and purple surfaces look less saturated than they should, compared to other colored surfaces, and will also shift their hues. Blue and green surfaces will appear mostly unaffected. However, we can note that the actual perceived color of a such illuminant (that is, "the shade of white") shows no visible shift despite the spectra difference. The relative color deviation is indeed lower than 0.001 % in chromatic coordinates u'v'. This is all to show that the intrications between spectra (from the illuminant as well as from light reflected by material, colored, surfaces) and color perception are far from intuitive and clearly not easily predicted.

Similarly, both technological organs involved in the signal capture and its restitution work in RGB. But, none of the involved RGB spaces actually match the cone LMS. If we want the digital image to look remotely close to the human perception of the scene, we will need to work hard to make that happen, by digitally manipulating the raw RGB signal, but without necessarily caring about perception in the process. It will be enough to ensure the consistency of the light spectrum at both ends.

[^4]: This is assuming the dynamic range of the scene is small enough. For very high dynamic range, highlights will shift to yellow (Bezold-Brücke shift), as can be observed with the sun disc or flames, while the same spectrum at a lower intensity will appear red.

This is a misunderstood aspect of digital photography where digital manipulation is often thought by purists as faking or tampering content, and the raw image is often seen as a sort of "neutral" or "objective" truth because it was made by a machine. The image produced by the machine is actually quite off and digital manipulation is absolutely necessary to make it look like the original scene despite all the optical distortions that happened in the camera.

Fixing the raw RGB to make it look somewhat like what the viewer experienced on the scene requires to manipulate colours, using at least an input color profile and a white balance adjustment. Unfortunately, these are inaccurate and will still not exactly match human vision, especially when the light source of the scene is not a natural daylight (with a "full" spectrum). In the example above, the fluorescent bulb shows a spiky spectrum that will make some very particular colors appear more saturated and bright than the rest of them, which will be challenging to correct.[^3]

[^3]: The quality of a particular lighting is measured by its [Color Rendering Index (CRI)](https://en.wikipedia.org/wiki/Color_rendering_index), which expresses how close the light spectrum is from natural daylight.

The property of this raw RGB signal is to be scene-linear : the RGB code values are roughly proportional to the energy of the light emission. This is the closest representation we can take of a light spectrum, pending a fully spectral pipeline (as in [Manuka](https://www.wetafx.co.nz/research-and-tech/technology/manuka/)).

Unfortunately, in most cases we can't just send this linear RGB to the computer display, even after correcting colors to match human vision, usually because :

1. the dynamic range that sensors can capture is much larger than what screens can render,
2. to better use this dynamic range, camera manufacturers "under-expose" the scene by roughly 2/3 of EV, which will make the raw RGB appear quite dark.

Therefore, we need at least to brighten the mid-tones and usually to compress the highlights, which is the job of the display transform.

In camera firmwares and in typical image editing applications, this display transform is commonly achieved through a "curve" (though the curve is only the graphical representation of the transform, not the transform itself) resembling this :

{{< figure src="display-transform.png" caption="A typical 'S' curve used commonly as SDR display transform. The 3 nodes represent respectively the black, middle-grey and white points. The dark diagonal in background is the 'no-operation' line, meaning the pixels will have the same RGB values after the transform as before." width="600" class="align-center" />}}

{{< compare after="display-calibrated.jpg" before="raw-calibrated.jpg" >}}
The result of applying a display transform for SDR screen (after) over calibrated raw RGB (before). Note how we lost local contrast in highlights, due to the compression, for the sake of brightening mid-tones. This is a trade-off difficult to avoid without introducing other much uglier artifacts.
{{</ compare >}}

The slope of this curve determines the global contrast. Many proprietary applications will apply a such curve without telling you and without letting you disable it, so you may have no idea what happens backstage. Some applications only let you choose a base look between "default", "neutral", "portrait", "intense", "HDR", etc. which will load a different curve. Some applications even embed the curve in the input color profile.

{{< note >}}
For a commercial software publisher, the choice of this default curve is crucial because it determines the first impression that the customer has when opening his photo, and this first impression often conditions the feeling of quality of the software. However, advanced users often regret that the first step of their editing is to cancel or soften the default look, which is not always easy. You will find people saying they like "Capture One colors" or rather "Lightroom colors", which is nothing more than an aesthetic choice of the publisher regarding the default look.
{{</ note >}}

## Working on an image

In the previous section, we learned that we would have to work to not only to reconstruct a believable color rendition from the raw RGB, but also to remap the scene dynamic range properly for the target display device. Here, we will see how this work is actually done, by studying more specifically the "_Lots of digital work_" step of the previous section flowchart.

If we generalize how _any_ image processing software works, being typical desktop editors, in-camera firmware, mobile apps, no matter what workflow they use, we come to this flowchart :

```mermaid
graph TD
    A((fa:fa-sun Scene)) --> B([Raw sensor reading]);
    B --> C;

    subgraph Software;
      C["(Linear) Color profiling"];
      C --> D[Scene-referred processing];
      D --> E["(Non-linear) Display transform"];
      E --> I[Display-referred processing];
      I --> G[Color Management System];
    end;

    G --> H([Screen buffer]);
    H --> J((fa:fa-lightbulb Display));

    style D fill:#BAFFD2;
    style I fill:#FFB7B5;
```

The typical image applications don't make use of the scene-referred processing at all, or only for some technical image reconstruction filters, so they go directly from the color profiling to the display transform. The digital manipulation happens solely in display space, where "white" is forced at 100 % (or the code value `255` when working in 8 bits RGB), "black" is forced at 0 % (or the code value `0` in 8 bits RGB), and middle-grey is typically in the middle at 45-50 % (see more details below).

The problem of this logic lies in the life cycle of the picture :

```mermaid
graph LR;
	A[fa:fa-tree Scene] --> B[fa:fa-camera Camera];
	B --> C[__WORKFLOW__];
	C --> D[fa:fa-poop Color Management System];
	D --> E[fa:fa-desktop SDR screen];
	D --> F[fa:fa-sun HDR Screen];
	D --> G[fa:fa-print Fine-art print];
	D --> H[fa:fa-book Books/Magazines];
	D --> I[fa:fa-tshirt Merch];
	D --> J[fa:fa-save Archives];
```

Because the target display medium can now be anything from a T-shirt to an HDR screen, and everything in-between, we need to use different display transforms to account for the properties of the target medium. But if we use the display transform as the first step of our editing, changing it will often void the subsequent editing, especially if it used parametric masks. This practically means you need to redo your editing for each output medium, which is tedious.

{{< note >}}
Typical color management systems (CMS) rely on the ICC v2 and ICC v4 specifications, which are designed for the printing industry with small dynamic range (SDR) in mind. They only take care of converting pixels from RGB space to RGB space but do not handle dynamic range rescaling, color adaptation to compensate for viewing conditions, and handle gamut mapping pretty coarsely. They do not fall under what we call "display transform" here, and are not ready for HDR, which means we need to rely on them as little as possible and bake them an SDR signal before using them.
{{</ note >}}

Working in the scene-referred part of the pipeline means we work before the display transform and our editing will be immune to the output medium discrepancies. This is like working on a "master edit" that will stay the same, no matter the output, and then dealing with the specifics of the output when we export the master. Removing the display-referred part of the workflow allows to collapse the display transform and the color management steps, which is very desirable because they all deal with the same task : mapping the "master edit" to whatever output medium we have, by correcting its peculiarities to try and preserve the intent color appearance for the audience.

The challenge of working in the scene-referred part is that "white" can't be assumed to be anchored at a specific value anymore, but can be anything positive up to infinity. To circumvent this lack of reference, we switch from a "white-centric" pipeline to a "grey-centric" pipeline, where the reflective middle-grey (the one of the grey cards) is expected to be anchored at 0.18-0.20. Since the HDR "white" can be 4 times as bright as the SDR "white", all we know is that all devices will have a middle-grey around 0.20 and all devices will be able to display it, no matter their dynamic range. It also turns out that images of natural scenes have most of their histogram centered around this value.

We can summarize the assumption of each workflow below :
{{< table >}}
| Assumption | Scene-referred | Display-referred (SDR) |
| -----------| ---------------| -----------------|
| Black point RGB encoding| > 0              | 0 % of the encoding range               |
| White point RGB encoding| unspecified    | 100 % of the encoding range      |
| Middle-grey RGB encoding| 0.18   | 18 % our 45 % of the encoding range |
{{</ table >}}

The RGB encodings are the digital representation of the image inside the computer. They are not  _directly_ connected to the real-world luminance, either recorded on the original scene or on the rendering display. For example, the luminance of black pixels measured on physical displays will be around 0.1 Cd/m², whereas will be encoded `0` in RGB. In the scene-referred workflow, we often need to offset black to non-zero RGB values to reconnect with its luminous meaning, upon which we rely.

When working in RGB spaces encoded with an [OETF](https://en.wikipedia.org/wiki/Transfer_functions_in_imaging) (wrongly dubbed "_gamma_"), like sRGB, Adobe RGB or Prophoto RGB, middle-grey is expected around 45 % of the encoding range. [^1] Applications having an 8 bit integer pipeline have to use OETF-encoded RGB to avoid posterization in gradients. Applications having a 16 bits integer pipeline often choose to do the same for consistency, although it's not a technical requirement in this case. Middle-grey will be encoded at 18 % of the encoding range in display-referred workflows that use linear RGB, that is RGB spaces stripped off of their OETF/gamma.

[^1]: This property is extensively used in graphical interfaces, like in typical [levels tools](https://www.cambridgeincolour.com/tutorials/levels.htm), because it puts the middle-grey effectively in the middle of the black-white range which makes for a nice usability, even though it comes at an high price in terms of colorimetry.

We can now match the assumptions of each workflow on the RGB code values to their meaning in terms of real-life luminance :
{{< table >}}
| Assumption | Scene-referred | Display-referred (SDR) |
| -----------| ---------------| -----------------|
| Black point actual luminance | user-defined, scene-dependent | optionally defined in ICC profiles |
| White point actual luminance | user-defined, scene-dependent | 80-160 Cd/m² |
| Middle-grey actual luminance | grey card   | 14-29 Cd/m² |
{{</ table >}}

The Cd/m² values in the display-referred column come from the usual pre-print ISO standards. The black point luminance of the display can optionally be defined in the medium ICC profile, which is often the case for professionally-made printer profiles, in order to enable the [black point compensation](https://www.color.org/WP40-Black_Point_Compensation_2010-07-27.pdf). Since the OETF gets decoded inside the screen, the luminance value of middle grey is connected to the linear RVB code value and will be found at 18-20 % of the screen white luminance.

For the scene-referred workflow, black and white luminances match the minimum and maximum luminances found on the scene. The middle-grey reference is the middle-grey patch of a Color Checker (or a grey card) lit under the same conditions as the subject of the image. It is therefore possible to set the middle-grey directly from sampling the luminance of a grey card in a test photo.

## Practical benefits of the scene-referred workflow

We have seen in the previous section that the scene-referred workflow allows you to work your master edit independently from any fixed target medium. The benefits of it don't stop there.

First, because scene-referred is designed around the idea that "white" has no particular fixed value, it can scale to any input dynamic range, which means that the same tools and workflow can be used to process digital photographs, synthetic renderings or any kind of composite HDR.

Then, where it really shines is for all optically-defined digital filters trying to mimic real-life effects, like blurring, deblurring, removing noise or reconstructing signals. The example below showcases the difference it makes to apply a digital bokeh filter, simulating a lens diaphragm, before or after the display transform.

{{< figure src="initial.jpg" caption="Base image" />}}

{{< compare after="blur-after.jpg" before="blur-before.jpg" >}}
The result of applying a lens blur before (scene-referred) or after (display-referred) the display transform. The relationship in brightness between the bokeh bubbles and the dark surround is destroyed and inconsistent in display-referred.
{{</ compare >}}

Since lens blur happens on light, and scene-linear RGB is the closest digital representation we can have of real light, it only makes sense to apply optically-defined digital filters in the scene-linear part of the pipeline, but the above example visually confirms the validity of the reasoning.

Similar effects will be observed when working with masks and alpha compositing, when one wants to feather and smooth the edges of the masks to better blend them with the surrounding (which, again, is a blur).

## How is it implemented in Ansel ?

Ansel is able to use both the display-referred and scene-referred workflows, because it inherits some legacy modules from darktable. Most display-referred modules have been replaced by scene-referred counterparts, and the remaining ones should follow in 2023.

Soon, Ansel will be entirely scene-referred, allowing for more clever display transforms combined with gamut mapping and ICC profiles extractions.

It is generally not possible to use display-referred modules in the scene-referred part of the pipeline, because they expect a white point at 100 %, and will generally clip RGB values above 100 % (some need to do it to avoid numeric instabilities in algorithms). Some even expect a grey point at 50 %, like the alpha [blending modes](https://en.wikipedia.org/wiki/Blend_modes) _screen_, _soft light_, _hard light_, _overlay_, _dodge_, _burn_ which process differently pixels which values are greater or lower than the  50 % threshold.
---

# Ansel : https://ansel.photos/en/

<div class="pt-5 my-5 text-center">
  <h1 class="display-4 fw-bold">Your digital darkroom</h1>
  <div class="col-lg-6 mx-auto">
    <p class="lead mb-4"><em>Ansel</em> is an open-source photo-editing software for digital artists, designed to help you achieve your own interpretation of raw digital photographs.</p>
  </div>
<div class="my-5">
{{< slideshow images="ansel-0.1-lighttable.jpg,ansel-0.1-import.jpg,ansel-0.1-darkroom.jpg,ansel-0.1-masks.jpg,ansel-0.1-actions.jpg" >}}
</div>
</div>

<div class="container">

{{< quote author="Ansel Adams" >}}
The negative is the score, and the print is the performance.
{{< /quote >}}

<div class="lead">

Pressing the camera shutter merely started a process ending when the on-screen picture looks like the one you had in mind. _Ansel_ proposes to put the artists back at the center of the creative process and enables them with an interface to manipulate images with precision and nuance, using state-of-the-art color science and independent color controls.

</div>


{{< divider >}}

## Install

{{% row %}}
{{% card icon="linux fab" title="Linux" %}}
Distribution-agnostic, portable AppImage executable
{{< release-assets release="https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0" extension=".appimage" label="Download latest" >}}
{{< release-assets release="https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0" extension=".appimage" label="Download previous" offset="1" display="link" >}}
{{% /card %}}

{{% card icon="windows fab" title="Windows" %}}
Windows 7 to 11 installer
{{< release-assets release="https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0" extension=".exe" label="Download latest" >}}
{{< release-assets release="https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0" extension=".exe" label="Download previous" offset="1" display="link" >}}
{{% /card %}}

{{% card icon="apple fab" title="MacOS X" %}}
Distribution for MacOS X 15 and newer.
{{< release-assets release="https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0" extension="arm64.dmg" label="Download latest (Apple M)" >}}
{{< release-assets release="https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0" extension="i386.dmg" label="Download latest (Intel)" >}}
{{% /card %}}

{{% card icon="terminal" title="Build from source" %}}
Best performance for your hardware
{{< button url="/doc/install" label="Building instructions" icon="wrench" >}}
{{% /card %}}
{{% /row %}}

---

{{% row %}}

{{% column class="text-center" %}}
{{< button url="/doc/install/" label="Installation help" icon="mouse" >}}
{{% /column %}}

{{% column class="text-center" %}}
{{< button url="/doc/getting-started/" label="Getting started" icon="hands-helping" >}}
{{% /column %}}

{{% /row %}}

---

### Is it stable ?

_Stats updated automatically every 4h from opt-in Sentry.io crash logs collection. Last update : {{% build-time %}}_

{{% row %}}
{{% column %}}
{{% card %}}

{{< plotly title="Crash-free sessions and Mean Time Before Failure (MTBF) per nightly build (data collected by opt-in sentry.io)" caption="false" src="reliability.json" class="full-width" dynamic="true" >}}

{{% /card %}}
{{% /column %}}
{{% column %}}
{{% card %}}

{{< plotly title="Crash-free users and number of pictures edited without crashing, per nightly build (data collected by opt-in sentry.io and posthog)" caption="false" src="reliability-users.json" class="full-width" dynamic="true" >}}

{{% /card %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
{{% card %}}

{{< plotly title="Reported issues (bugs, tasks and features) already resolved versus still open, per release (source: Github). Much has already been done; what remains is tracked openly." caption="false" src="bugs.json" class="full-width" dynamic="true" >}}

{{% /card %}}
{{% /column %}}

{{% column %}}

Ansel has not published a stable release yet and there is no ETA for one : a new release is published when the list of all bugs have been cleared, so we _know_ the software and stable. So far, Ansel only publishes __revisions__, which are intermediate states of the sourcecode. Once a revision has been tested by at least 35 unique users, its reliability stats will show above. An asterisk (\*) marks revisions we shipped as a packaged nightly build.

On the two charts above, the crash-free rate is shown as a percentage whose **bar opacity encodes our confidence** : the solid part (*almost certain*) is the rate we are confident the software reaches, the *likely* range goes up to the currently recorded average, then *optimistic* reaches up to the best plausible rate written in parentheses on top. The fewer people tested a revision, the more incertainty there is on the average, the larger the {*likely* + *optimistic*} range gets : it is the margin of error of the current average at 95% confidence.

Bugs need to be reported on [Github](https://github.com/aurelienpierreeng/ansel/issues) or they will never be fixed. This free software is only guaranteed to work on the computers of its developers, who are not in front of yours. __Ansel saves your editing histories immediately after each change__: in case of a crash you loose at most the last action.

The links above always point to the latest nightly build of the "fairly stable" branch. If you want a particular revision or need to roll back, [you can find all intermediate versions on Github](https://github.com/aurelienpierreeng/ansel/releases/tag/v0.0.0).


{{% /column %}}
{{% /row %}}

{{< divider >}}

## Why Ansel ?

{{% row %}}
{{% column %}}

Many solutions already exist to produce ready-to-consume photographs for masses, from smartphones filters to out-of-cameras JPEGs, followed recently by AI-driven automagic toy filters. These make photography easier than ever, but are the produced images really __your__ images and, in any case, the images __you__ expected ?

{{% /column %}}
{{% column %}}

_Ansel_ lets you interpret your raw photographs much like a music instrument, when most software tries to automatically play the score for you. It aims at being an underwhelming, boring, tool that just does what you ask of it, without getting in your way.

{{% /column %}}
{{% /row %}}

<div class="text-center my-5">
<span class="display-5">Get excited by your results</span><br />
<span class="fs-4">Not by your toys</span>
</div>

{{% row %}}
{{% column %}}

When you like music, you can choose between learning how to play or buying recordings. It's easier to buy, but more satisfying to play. Photo editing applications have lied to users for decades, pretending they could play without learning, because the software would deal with technical complexities for them, and entirely hide them.

{{% /column %}}
{{% column %}}

It turns out that users have only been fighting applications they understand less and less, for control over their results, and to recover those tricky cases where automation fails. As time will go by, expect to lose more and more time fighting AIs to get natural-looking results… manually. Why not simply cut the middle-man ?

{{% /column %}}
{{% /row %}}

{{< divider >}}

## What can Ansel do for you ?

Ansel allows you to manage your collections of pictures, to edit your raw digital photographs and film scans non-destructively and to export the result to common file formats. It stores your editing histories as text and lets you go back in time at any editing step you like, anytime.

### Color work

Ansel ships a recent color science, compatible with HDR : the chromatic adaptation CIE CAT 2016, the HDR color space JzAzBz (2017) and the perceptual color space darktable UCS 2022, developed specifically to manipulate color saturation without the fluorescent effect.

{{% row %}}
{{% column %}}
<h5>Color calibration</h5>
{{% compare after="calibration-after.jpg" before="calibration-before.jpg" cols="2" %}}
Fix white balance and get __high-fidelity__ colors in just a few clicks, by calibrating colors with a Color Checker directly in the darkroom.
{{% /compare %}}
{{% /column %}}

{{% column %}}
<h5>Color-grading</h5>
{{< compare after="grading-after.jpg" before="grading-before.jpg" cols="2" >}}
Give ambiance and character to your pictures by polishing their color palette with nuanced and fine-grained controls, in RGB, Ych or HSB color spaces, for creative and corrective purposes.
{{</ compare >}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
<h5>Color matching</h5>
{{< compare after="matching-after.jpg" before="matching-before.jpg" cols="2">}}
Force the chromatic adaptation such that any selected object matches a predetermined color, input from CIE Lab coordinates (for logos and brand colors), or by sampling the color of the same object in another shot, as to even the color rendition over the series.
{{</ compare >}}
{{% /column %}}

{{% column %}}
<h5>Hue qualifying and keying</h5>
{{% compare after="masking-after.jpg" before="masking-before.jpg" cols="2" %}}
Use the hue, chroma and lightness qualifiers to quickly define masks and apply selective effects. Combine parametric masks with drawn masks and boolean operations. Refine and feather the edges of masks by blurring or using clever edges detection.
{{% /compare %}}
{{% /column %}}
{{% /row %}}


### Tonal work

The tonal working methods are designed to manipulate luminance without affecting hue nor saturation, in order to respect the color work, done apart.

{{% row %}}
{{% column %}}
<h5>HDR tone mapping</h5>
{{% compare after="filmic-after.jpg" before="filmic-before.jpg" cols="2" %}}
Recover deep shadows and compress the dynamic range while retaining original saturation and hue, with gamut mapping to ensure the colors fit in the output color space. _(Photo : Andreas Schneider)_
{{% /compare %}}
{{% /column %}}

{{% column %}}
<h5>Zone-system editing</h5>
{{% compare after="toneeq-after.jpg" before="toneeq-before.jpg" cols="2" %}}
Balance densities based on exposure zones, by preserving local contrast thanks to an edge detection algorithm, and select the exposure zones to affect directly from the picture, through the interactive cursor. _(Photo : Andreas Schneider)_
{{% /compare %}}
{{% /column %}}
{{% /row %}}

### Image reconstruction

{{% row %}}
{{% column %}}
<h5>Lens deblurring</h5>
{{% compare after="sharpen-after.jpg" before="sharpen-before.jpg" cols="2" %}}
Unleash the power of multiscale, gradient-based machine learning to rejuvenate old lenses, recover focusing mistakes or add emphasis on your subject, but without the typical edge artifacts, over-sharpening oddities or added noise.
{{% /compare %}}
{{% /column %}}

{{% column %}}
<h5>Dehazing</h5>
{{% compare after="dehaze-after.jpg" before="dehaze-before.jpg" cols="2" %}}
Restore some depth in foggy and hazy shots by bringing back textures and saturation in colors, without overaccentuating already sharp details.
{{% /compare %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
<h5>Denoising</h5>
{{< compare after="denoise-after.jpg" before="denoise-before.jpg" cols="2">}}
Remove chromatic noise, soften and blend luminance noise.
{{</ compare >}}
{{% /column %}}

{{% column %}}
<h5>Highlights reconstruction</h5>
{{< compare after="highlights-after.jpg" before="highlights-before.jpg" cols="2" >}}
Salvage both color and texture in highlights, recover blown areas by propagating gradients while the gamut-mapping watches your back to ensure colorful highlights can still be printed at their proper hue. You don't have to bleach highlights to hide problems anymore.
{{</ compare >}}
{{% /column %}}
{{% /row %}}

### Specialized features

{{% row %}}
{{% column %}}
<h5>Automatic perspective correction</h5>
{{< compare after="perspective-after.jpg" before="perspective-before.jpg" cols="2" >}}
Let the machine learning detect automatically vertical and horizontal lines and compute the best geometric correction to rotate, straighten and crop the picture, optionaly taking into account the kind of lens used.
{{</ compare >}}
{{% /column %}}

{{% column %}}
<h5>Censoring</h5>
{{< compare after="censorize-after.jpg" before="censorize-before.jpg" ols="2" >}}
Anonymize people, license plates, etc. and play with use conditions of socially prude network without defiling your pictures too much.
{{</ compare >}}
{{% /column %}}
{{% /row %}}

{{< divider >}}


## Compatibility

{{% row %}}
{{% card title="Edits" icon="desktop" %}}
Ansel is based on darktable 4.0 and is fully compatible with darktable 2.x up to 4.0 database and XMP files. Coming from darktable ?
{{< button url="/doc/special-topics/from-darktable/" label="Find out what is changed" icon="sync" >}}
{{% /card %}}
{{% card title="Cameras" icon="camera" %}}
Ansel uses Rawspeed and Libraw to decode raw photographs. New cameras may need up to 24 months to be fully supported after their commercial release.
{{< button url="/resources/supported-cameras" label="Supported cameras" icon="wrench" >}}
{{% /card %}}
{{% card title="Languages" icon="language" %}}
The software is integrally translated in English, French, Italian and simplified Chinese. Partial translations are available in German, Spanish, Portugese, Ukranian, etc.
{{< button url="/contribute/translating/" label="Improve translations" icon="comment" >}}
{{% /card %}}
{{% /row %}}

{{< divider >}}

## Darktable, but better

{{% row %}}
{{% column %}}

<div class="no-hyphenation lead">

Ansel is what Darktable 4.0 could have been if it didn't die of [feature creep](https://en.wikipedia.org/wiki/Feature_creep).

</div>

Between 2020 and 2023, Darktable has suffered [massive code additions of peripheral features](./news/darktable-dans-le-mur-au-ralenti/), often ill-coded, poorly designed and penalizing usability, performance and maintenance. Too many workarounds failed to fix bugs, but layered new problems on top of legacy code : welcome in maintenance nightmare.

In fairness, without project management or feature planning, this was bound to happen. Darktable has always struggled to be more than a pack of individual plugins.

{{% /column %}}
{{% column %}}

The result is a weird, frustrating, app trying to reinvent GUI paradigms on its own, trying to do everything for everyone, slower and less stable than before, and absolutely terrible to debug.

With a scene-referred-centric user interface, many modules merged into a global menu, rewritten import tool and sparing image pipeline recomputations, Ansel is a Darktable 4.0 variant where 30.000 lines of poorly-written code and half-broken features have been removed, and 11.000 lines rewritten : __it runs faster, smoother, uses less power and requires less configuration__.

With a decreased code complexity, its maintenance should be easier in the future too.

{{% /column %}}
{{% /row %}}

{{< divider >}}

## Beyond documentation

{{% row %}}
{{% column %}}

The typical, recurring painpoint of open-source software projects is documentation. When there is none, users complain about it. When there is one, they complain it is too long, not complete enough, or it doesn't include use cases. Developers expect users to have a linear reading of the project documentation. It will simply not happen and developers will serve as parrots. That only builds up frustration on both ends. __Documentation is not enough__.

{{% /column %}}
{{% column %}}

Chantal is a bilingual (French-English) language model trained specifically for image processing, color theory and photography, that understands technical slang, synonyms and some translations. __Its web interface allows search queries through a central index__ of open-source software documentation, bug reports, user forums, YouTube channels, scientific publications and standards organizations (CIE, ICC, ACES).

{{% /column %}}
{{% /row %}}

<div class="text-center my-5">
<span class="display-5">Meet <a href="https://chantal.aurelienpierre.com" target="_blank">Chantal</a></span><br />
<span class="fs-4">your image processing AI librarian</span>
</div>

{{% row %}}
{{% column %}}

<div class="no-hyphenation lead mx-auto">

Chantal is Ansel's knowledge infrastructure :

- quickly find relevant information among trusted sources,
- avoid asking questions already answered.

</div>

{{% /column %}}
{{% column %}}

<span class="no-hyphenation lead mx-auto">Designed to make users cleverer, instead of making the software dumber.</span> The artificial intelligence feeds you learning material on topics of your choice. Let's invest in natural intelligence.

{{% /column %}}
{{% /row %}}

{{< divider >}}

## Source code

Ansel software and documentation are released under the GNU/GPL v3 license and versionned with Git. The website is copyrighted but publicly readable. The work repositories are hosted on Github and are mirrored on Gitlab for backup.

{{% row %}}

{{% card title="Software" icon="desktop" %}}
GNU/GPL v3 license.
{{< button url="https://github.com/aurelienpierreeng/ansel" label="Github (original)" icon="github fab">}}
{{< button url="https://gitlab.com/aurelienpierreeng/ansel" label="Gitlab (mirror)" icon="gitlab fab">}}
{{% /card %}}

{{% card title="Documentation" icon="book" %}}
GNU/GPL v3 license.
{{< button url="https://github.com/aurelienpierreeng/ansel-doc" label="Github (original)" icon="github fab">}}
{{< button url="https://gitlab.com/aurelienpierreeng/ansel-doc" label="Gitlab (mirror)" icon="gitlab fab">}}
{{% /card %}}

{{% card title="Website" icon="globe" %}}
Copyright.
{{< button url="https://github.com/aurelienpierreeng/ansel-website" label="Github (original)" icon="github fab">}}
{{< button url="https://gitlab.com/aurelienpierreeng/ansel-website" label="Gitlab (mirror)" icon="gitlab fab">}}
{{% /card %}}

{{% /row %}}

{{< divider >}}

## How is Ansel used ?

The charts below come from the opt-in PostHog telemetry.

{{% row %}}
{{% column %}}
{{% card %}}
{{< plotly title="Active users per day." caption="false" src="usage-active.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
{{% card %}}
{{< plotly title="Distribution of session lengths (P10 / median / P90 marked)." caption="false" src="usage-session-length.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% column %}}
{{% card %}}
{{< plotly title="Number of images edited per session (P10 / median / P90 marked)." caption="false" src="usage-images.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
{{% card %}}
{{< plotly title="Number of image files opened by file type (data collected by opt-in posthog)." caption="false" src="usage-files.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
{{% card %}}
{{< plotly title="Most-used features: views, panels and editing modules, by number of activations (data collected by opt-in posthog)." caption="false" src="usage-modules.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
{{% card %}}
{{< plotly title="Operating systems in use, by number of users." caption="false" src="usage-os.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% column %}}
{{% card %}}
{{< plotly title="Users running with or without GPU acceleration (OpenCL)." caption="false" src="usage-opencl.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
{{% card %}}
{{< plotly title="Linux distributions used." caption="false" src="usage-distro.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% column %}}
{{% card %}}
{{< plotly title="Desktop environment used by Linux users." caption="false" src="usage-de.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% column %}}
{{% card %}}
{{< plotly title="Display server used by Linux users." caption="false" src="usage-display.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
{{% card %}}
{{< plotly title="Graphics cards in use, by number of users." caption="false" src="usage-gpu.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% column %}}
{{% card %}}
{{< plotly title="Graphics card vendors, by number of users." caption="false" src="usage-gpu-vendor.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
{{% card %}}
{{< plotly title="Number of CPU logical cores, by number of users." caption="false" src="usage-cpu.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% column %}}
{{% card %}}
{{< plotly title="Installed memory (RAM), in 2 GB classes, by number of users." caption="false" src="usage-ram.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
{{% card %}}
{{< plotly title="Screen size (width×height@scaling factor), by number of users." caption="false" src="usage-screen.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% /row %}}

{{% row %}}
{{% column %}}
{{% card %}}
{{< plotly title="User density per operating system and build channel (nightly = our packaged builds, self-build = compiled from source)." caption="false" src="usage-os-channel.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% column %}}
{{% card %}}
{{< plotly title="GPU acceleration (OpenCL) versus whether a graphics card is present." caption="false" src="usage-opencl-gpu.json" class="full-width" dynamic="true" >}}
{{% /card %}}
{{% /column %}}
{{% /row %}}




</div>
---

# Ansel : https://ansel.photos/en/tags/ansel/
---

# Darktable : https://ansel.photos/en/tags/darktable/
---

# Photo Editing : https://ansel.photos/en/tags/photo-editing/
---

#  : https://ansel.photos/en/authors/aurelien-pierre/
---

#  : https://ansel.photos/en/authors/guillaume-marty/
