Simulate a film stock from its measurements, not from someone's grade
FR-DEV-3f asks for look emulation and proposes HaldCLUT import to inherit
the free film-simulation ecosystem. This takes the other road for the
stocks where the measurements exist: run the physics.
A stock here is its manufacturer's own datasheet -- spectral sensitivity,
characteristic curves, dye densities. Light exposes three emulsion layers,
the layers develop to densities, the densities are dyes that absorb, and
what is left is what reaches the eye. A colour negative comes out orange
and upside down because that is what a colour negative is; it becomes a
photograph when a paper profile prints it, with the enlarger's filtration
solved rather than dialled.
What that buys over a LUT is that the parameters stay physical. Opening up
a stop moves the picture along the film's real characteristic curve,
shoulder and all, instead of scaling a number baked at one exposure. The
data cost runs the other way too: a stock is 17 kB of published
measurements where one HaldCLUT is 800 kB of one person's grade.
It looks like it needs a spectral integration per pixel. It does not, and
that is the whole design:
- Exposure is a 3x3 matrix. The reconstructed scene spectrum is linear
in the sRGB triple, so the integral collapses into nine numbers,
exactly -- no approximation.
- The characteristic curve is three 1D functions, sampled exactly.
- Everything after that -- dye absorption, the print through the
negative, the paper, the viewing illuminant, the adaptation -- takes
exactly three numbers in, so it bakes into one 32^3 lookup.
Per pixel: a matrix multiply, three curve taps, one fetch. Splitting the
curve out of the 3D lookup rather than baking one LUT over exposure is
measured, not assumed: the curve carries the sharp shape and the dye
mixing is smooth, so folding them together would need three times the
resolution for the same error. At 32^3 the worst error is 0.003 in linear
sRGB, under one 8-bit code value, and a test says so.
No wgpu dependency, deliberately, and the same isolation argument dr-lens
makes: the model is plain f32 with a documented layout, so every property
worth asserting is asserted on the CPU. Binding it to a texture is dr-gpu's
job and is not done here yet.
The expected values in tests/ came from a Python prototype running against
a different colour-science stack. Agreement to three decimals is evidence
about the model rather than about one implementation of it -- a transposed
matrix or a mispasted observer row would pass every unit test and fail
that one.
Profiles are converted from spektrafilm by Andrea Volpato, CC BY-SA 4.0.
The converter is in the tree and runnable, so what was changed from
upstream is auditable rather than taken on trust; profiles/CHANGELOG.txt
records it, including the one deliberate deviation -- Mallett & Yuksel's
1 kB basis instead of Hanatos's 4 MB table, which costs accuracy at the
gamut edge and saves four megabytes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "dr-film"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# Isolated from dr-pipeline for the same reason dr-lens is: that crate has no
|
||||
# dependencies so its codegen stays testable without a device (ARCH §6.5a), and
|
||||
# a YAML parser plus the stock profiles do not belong in it. The pipeline
|
||||
# consumes the baked tables this crate produces and never links the profiles.
|
||||
#
|
||||
# No wgpu dependency either, deliberately. What comes out of here is plain
|
||||
# `f32` data with a documented layout; deciding it is a 3D texture is dr-gpu's
|
||||
# job, and keeping that decision out of here is what lets the whole spectral
|
||||
# model be tested on the CPU.
|
||||
[dependencies]
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_norway.workspace = true
|
||||
@@ -0,0 +1,76 @@
|
||||
# Film stocks
|
||||
|
||||
One file per stock in [`profiles/`](profiles/). Adding a stock is adding a
|
||||
file — no code change, no shader, no new operation — for the same reason
|
||||
`dr-decode`'s base curves work that way: under the GPLv3 a stock should be
|
||||
contributable without a release.
|
||||
|
||||
## What a profile is
|
||||
|
||||
Three measured tables, all of them published in the manufacturer's datasheet:
|
||||
|
||||
| Field | What it decides |
|
||||
|---|---|
|
||||
| `log_sensitivity` | what each emulsion layer *sees*, per wavelength |
|
||||
| `density_curves` | contrast, latitude, and where the stock clips |
|
||||
| `dye_density` | what the developed stock *looks* like, per wavelength |
|
||||
| `base_density` | the support: film base, and a colour negative's orange mask |
|
||||
|
||||
Plus `kind` (negative or positive), `support` (film or paper), and the two
|
||||
illuminants the data is referenced to. A print paper is a stock like any
|
||||
other; `support` exists so an interface can offer papers separately, not
|
||||
because the renderer treats them differently.
|
||||
|
||||
## Why it is not a LUT
|
||||
|
||||
Because the parameters stay physical. Opening up a stop moves the picture along
|
||||
the film's own characteristic curve — toe, shoulder and all — instead of
|
||||
scaling a number somebody baked at one exposure. A scanned negative comes out
|
||||
orange and inverted because that is what a negative *is*, and it becomes a
|
||||
photograph when a paper profile prints it, exactly as it would in a darkroom.
|
||||
|
||||
The data cost runs the other way from a LUT collection too: a stock is about
|
||||
17 kB of measurements, where one HaldCLUT is roughly 800 kB of one person's
|
||||
grade.
|
||||
|
||||
## How it runs
|
||||
|
||||
The spectral chain reduces to three tables, and the reduction is exact where it
|
||||
matters — see [`src/bake.rs`](src/bake.rs) for the argument:
|
||||
|
||||
1. **A 3×3 matrix**, linear sRGB to the three layers' exposure. Exact, not an
|
||||
approximation: the reconstructed scene spectrum is linear in the sRGB
|
||||
triple, so the integral collapses into nine numbers.
|
||||
2. **Three 1D curves**, log exposure to density, sampled at 256 points.
|
||||
3. **One 32³ lookup**, density to linear sRGB — dye absorption, the print
|
||||
through the negative, the paper, the viewing illuminant and the chromatic
|
||||
adaptation, all of which take exactly three numbers in.
|
||||
|
||||
Per pixel that is a matrix multiply, three curve taps and one texture fetch.
|
||||
Splitting 2 from 3, rather than baking one LUT over exposure, is measured
|
||||
rather than assumed: the curve carries all the sharp shape and the dye mixing
|
||||
is smooth, so folding the curve into the 3D lookup would need it three times
|
||||
larger for the same error. At 32³ the worst interpolation error is about 0.003
|
||||
in linear sRGB, below one 8-bit code value, and there is a test that says so.
|
||||
|
||||
## Adding a stock
|
||||
|
||||
If spektrafilm has it, add its name to `STOCKS` in
|
||||
[`tools/film-profiles/convert.py`](../../tools/film-profiles/convert.py) and
|
||||
re-run it. Otherwise write the YAML by hand from the datasheet; the loader
|
||||
validates the table lengths and says which file and field is wrong.
|
||||
|
||||
Either way, list it in `BUILT_IN` in [`src/lib.rs`](src/lib.rs) to compile it
|
||||
in — or drop it in the profile directory at runtime, which is the path meant
|
||||
for stocks that ship separately from the binary.
|
||||
|
||||
## Provenance
|
||||
|
||||
The shipped profiles are converted from
|
||||
[spektrafilm](https://github.com/andreavolpato/spektrafilm) by Andrea Volpato,
|
||||
licensed CC BY-SA 4.0. See [`profiles/LICENSE-PROFILES.txt`](profiles/LICENSE-PROFILES.txt)
|
||||
for the licence and [`profiles/CHANGELOG.txt`](profiles/CHANGELOG.txt) for what
|
||||
the conversion changed and what it deliberately did not.
|
||||
|
||||
The sRGB reflectance basis is Mallett & Yuksel (2019); the observer is the CIE
|
||||
1931 2°.
|
||||
@@ -0,0 +1,74 @@
|
||||
Changes made to the spektrafilm profiles shipped in this directory
|
||||
=================================================================
|
||||
|
||||
The profiles here are derived from spektrafilm by Andrea Volpato
|
||||
(https://github.com/andreavolpato/spektrafilm), licensed CC BY-SA 4.0. The
|
||||
full licence is in LICENSE-PROFILES.txt and is reproduced unchanged.
|
||||
|
||||
CC BY-SA 4.0 section 3(a)(1)(B) requires that a modified copy say it was
|
||||
modified. It was. This file says how, and tools/film-profiles/convert.py
|
||||
performs the modification, so it can be re-run against upstream and the result
|
||||
compared rather than taken on trust.
|
||||
|
||||
|
||||
What was changed
|
||||
----------------
|
||||
|
||||
1. Format. Upstream ships JSON; these are YAML, so that adding or correcting a
|
||||
stock is editing a legible file rather than a minified one. No value is
|
||||
altered by the reformat.
|
||||
|
||||
2. Trimmed to the fields this renderer reads:
|
||||
|
||||
kept info.*, data.wavelengths (implicitly, as the fixed grid),
|
||||
data.log_sensitivity, data.channel_density (renamed
|
||||
dye_density), data.base_density, data.log_exposure (kept as its
|
||||
two endpoints, since it is uniformly sampled),
|
||||
data.density_curves
|
||||
|
||||
dropped data.density_curves_model - a 3-CDF fit of the curves; the
|
||||
sampled curves are shipped
|
||||
instead, and reproduce it to
|
||||
0.004 density
|
||||
data.density_curves_layers - per-sublayer curves, used for
|
||||
grain, which is not implemented
|
||||
yet. Worth restoring when it is:
|
||||
real grain is per sublayer.
|
||||
data.hanatos2025_adaptation_* - parameters for a spectral
|
||||
upsampling method this renderer
|
||||
does not use; see below
|
||||
data.midscale_neutral_density - null in every profile shipped
|
||||
|
||||
Dropping fields loses nothing for the stocks shipped, but it does mean a
|
||||
re-run of the converter is needed to pick up an upstream field later.
|
||||
|
||||
3. Numbers are written at 6 significant figures (5 for the density curves).
|
||||
The inputs are digitised datasheet curves, so this is well inside their
|
||||
measurement error; it is what takes a profile from 207 kB to 17 kB.
|
||||
|
||||
4. Nulls made explicit. Upstream uses null where a datasheet has no reading.
|
||||
In log_sensitivity that means the layer is blind there, written here as the
|
||||
sentinel -9; in the density tables it means no absorption, written as 0.
|
||||
|
||||
|
||||
What was NOT changed
|
||||
--------------------
|
||||
|
||||
No measured value has been rescaled, shifted, smoothed or refitted. The
|
||||
renderer's own calibration conventions - mid-grey at 0.184, exposure
|
||||
normalised on the green layer - are taken from spektrafilm's reference
|
||||
implementation rather than invented, because the profile data is calibrated
|
||||
against them.
|
||||
|
||||
|
||||
Known deviation from upstream's rendering
|
||||
-----------------------------------------
|
||||
|
||||
Upstream reconstructs a spectrum from an RGB triple with Hanatos (2025), which
|
||||
needs a 4 MB coefficient table. This renderer uses the Mallett & Yuksel (2019)
|
||||
sRGB basis instead, which is three curves and about 1 kB, at some cost in how
|
||||
faithfully very saturated and out-of-gamut colours are handled. The
|
||||
hanatos2025_adaptation_* parameters in the upstream profiles are therefore
|
||||
unused here. This is a deliberate trade of accuracy at the gamut edge against
|
||||
shipping four megabytes, and it is the first thing to revisit if saturated
|
||||
colours look wrong.
|
||||
@@ -0,0 +1,520 @@
|
||||
================================================================================
|
||||
License for spektrafilm profiles, LUTs, and direct derivatives
|
||||
by Andrea Volpato
|
||||
License: CC BY-SA 4.0 | Preamble v1.1 (2026-05-28)
|
||||
================================================================================
|
||||
|
||||
|
||||
This license applies to the original spektrafilm profiles that can be found at
|
||||
https://github.com/andreavolpato/spektrafilm, in the subfolder
|
||||
src/spektrafilm/data/profiles, and to all direct derivatives of the profiles,
|
||||
such as copies in other projects, LUTs, or any other format that encodes the
|
||||
same content.
|
||||
|
||||
LUTs and similar artifacts are interpreted as direct encodings of the
|
||||
information in the original profiles. I chose not to release them under GPLv3
|
||||
because GPL's software-shaped "derivative work" model fits LUTs poorly and would
|
||||
have made adoption more difficult. CC BY-SA 4.0 preserves the same share-alike
|
||||
spirit in a form that matches how creative-grading assets are actually handled.
|
||||
|
||||
Please do not modify this file. Changes to profiles and LUTs should be tracked
|
||||
in a separate CHANGELOG.txt file shipped with them. A copy of this license is
|
||||
available at:
|
||||
https://github.com/andreavolpato/spektrafilm/blob/main/SPEKTRAFILM_LICENSE.txt
|
||||
|
||||
---
|
||||
|
||||
A note before the legal text.
|
||||
|
||||
spektrafilm is a film-emulation project, designed in the open, given away
|
||||
freely, and made to stay that way. Use it on anything. Share it widely. Build
|
||||
on it. The works you make are yours, completely.
|
||||
|
||||
What follows is the full text of the Creative Commons Attribution-ShareAlike
|
||||
4.0 International Public License (CC BY-SA 4.0). It is the legal floor. The
|
||||
spirit it exists to protect, in three lines:
|
||||
|
||||
- Free use forever, on any production, of any size.
|
||||
- Full credit to the source, on every copy and every derivative.
|
||||
- No parasitism.
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
WHAT YOU CAN DO
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Use these profiles and LUTs freely to grade anything - personal, educational,
|
||||
or commercial; small project or feature film; any platform, any deliverable.
|
||||
The works you make are yours. No royalties. No copyleft on your finished film
|
||||
or photograph. No obligation flowing to your distributor, broadcaster, or
|
||||
audience.
|
||||
|
||||
Share the files freely with friends, collaborators, vendors, and post houses.
|
||||
|
||||
Modify, remix, regrade, recombine. Modified profiles or LUTs must stay
|
||||
CC BY-SA 4.0 and credit spektrafilm.
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
ATTRIBUTION - ALWAYS, EVERYWHERE, FOREVER
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Every copy, redistribution, modification, or derivative of these files - in
|
||||
any format, in any context, across any chain of hands - must preserve:
|
||||
|
||||
- Andrea Volpato, named as the original author.
|
||||
- https://github.com/andreavolpato/spektrafilm, as the canonical source.
|
||||
- The CC BY-SA 4.0 license notice (this file, or a link to it).
|
||||
- A note that the file has been modified, if it has, and by whom.
|
||||
|
||||
This applies whether the LUT is shipped inside a .cube header, an ICC profile
|
||||
field, an app's About screen, a paid plugin, a derived .3dl, training-data
|
||||
documentation, or anywhere else. The form can vary; the information cannot
|
||||
disappear.
|
||||
|
||||
Removing or hiding this information ends the license grant immediately
|
||||
(CC BY-SA 4.0, Section 6(a)).
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
SUGGESTED ATTRIBUTION TEXT
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
For unmodified files:
|
||||
|
||||
spektrafilm by Andrea Volpato
|
||||
https://github.com/andreavolpato/spektrafilm
|
||||
Licensed CC BY-SA 4.0
|
||||
|
||||
For modified files:
|
||||
|
||||
Derived from spektrafilm by Andrea Volpato
|
||||
https://github.com/andreavolpato/spektrafilm
|
||||
Licensed CC BY-SA 4.0
|
||||
Modified by [your name].
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
WHAT I ASK, BEYOND THE LEGAL FLOOR
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
These are not legal requirements. They are the spirit of the project, stated
|
||||
plainly, so nobody has to guess.
|
||||
|
||||
- Keep this file unchanged when sharing profiles/LUTs, and if changes were
|
||||
made describe them in a file CHANGELOG.txt shipped together with this
|
||||
license.
|
||||
|
||||
- Credit spektrafilm in the human layer too - project docs, about pages,
|
||||
credits, breakdowns. Not required on a finished film or photograph, but
|
||||
appreciated wherever it's natural.
|
||||
|
||||
- Don't use "spektrafilm" or my name in product branding without asking.
|
||||
The license covers the files; the name is not part of that grant.
|
||||
Factual reference is welcome and even encouraged, for example "graded with
|
||||
spektrafilm", or "this app uses spektrafilm LUTs".
|
||||
|
||||
- Paid apps and tools that add real value are welcome. Charge for what you
|
||||
built - UI, performance, support, a service. Please don't repackage the
|
||||
LUTs themselves as a paid LUT pack: CC BY-SA technically allows it, but
|
||||
that's not the spirit of the project, and any buyer can legally
|
||||
redistribute for free anyway.
|
||||
|
||||
- Please don't train commercial AI models on the LUTs or their outputs.
|
||||
CC BY-SA doesn't forbid this, but it is asked. Models that approximate
|
||||
or replace the spektrafilm look defeat the purpose of giving these away
|
||||
freely. Non-commercial research training is fine.
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
CONTACT
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
GitHub Issues or Discussions on the spektrafilm repository.
|
||||
|
||||
For private inquiries about commercial licensing, name use, or custom work,
|
||||
write to andrea.volpato@outlook.com.
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
A small thank you
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
If spektrafilm finds its way into your work, that is already the reward. If
|
||||
you want to credit it generously, share what you made, or just say hi, please do
|
||||
not hesitate. Make beautiful images.
|
||||
|
||||
Andrea Volpato
|
||||
|
||||
|
||||
================================================================================
|
||||
END OF SPEKTRAFILM PREAMBLE - BEGIN CC BY-SA 4.0 LICENSE TEXT (verbatim)
|
||||
================================================================================
|
||||
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and
|
||||
conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
l. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
m. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
including for purposes of Section 3(b); and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public licenses. Notwithstanding,
|
||||
Creative Commons may elect to apply one of its public licenses to material it
|
||||
publishes and in those instances will be considered the "Licensor." The text
|
||||
of the Creative Commons public licenses is dedicated to the public domain
|
||||
under the CC0 Public Domain Dedication. Except for the limited purpose of
|
||||
indicating that material is shared under a Creative Commons public license or
|
||||
as otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the use of
|
||||
the trademark "Creative Commons" or any other trademark or logo of Creative
|
||||
Commons without its prior written consent including, without limitation, in
|
||||
connection with any unauthorized modifications to any of its public licenses
|
||||
or any other arrangements, understandings, or agreements concerning use of
|
||||
licensed material. For the avoidance of doubt, this paragraph does not form
|
||||
part of the public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
@@ -0,0 +1,450 @@
|
||||
# Generated by tools/film-profiles/convert.py from spektrafilm.
|
||||
# Do not edit by hand: re-run the converter instead.
|
||||
#
|
||||
# spektrafilm by Andrea Volpato, https://github.com/andreavolpato/spektrafilm
|
||||
# Licensed CC BY-SA 4.0. Modified for DarkRoom: trimmed to the fields the
|
||||
# renderer uses and reformatted; see profiles/CHANGELOG.txt.
|
||||
|
||||
version: '0.3.2'
|
||||
stock: kodak_kodachrome_64
|
||||
name: 'Kodak Kodachrome 64'
|
||||
kind: positive # negative | positive
|
||||
support: film # film | paper
|
||||
reference_illuminant: D55
|
||||
viewing_illuminant: D50
|
||||
|
||||
# log10 spectral sensitivity per layer, 380-780nm at 5nm, in R,G,B layer
|
||||
# order. A null upstream means the datasheet has no reading there, which is
|
||||
# blindness, so it is written as the sentinel the loader reads as such.
|
||||
log_sensitivity:
|
||||
- [-4.46214, -3.15351, -0.995926] # 380nm
|
||||
- [-4.40933, -3.09598, -0.929097] # 385nm
|
||||
- [-4.35484, -3.03587, -0.857493] # 390nm
|
||||
- [-4.29857, -2.973, -0.780586] # 395nm
|
||||
- [-4.24045, -2.90719, -0.697763] # 400nm
|
||||
- [-4.18038, -2.83825, -0.608314] # 405nm
|
||||
- [-4.11826, -2.76598, -0.51141] # 410nm
|
||||
- [-4.054, -2.69015, -0.406081] # 415nm
|
||||
- [-3.98749, -2.61058, -0.29748] # 420nm
|
||||
- [-3.91861, -2.5271, -0.283544] # 425nm
|
||||
- [-3.84725, -2.43971, -0.304033] # 430nm
|
||||
- [-3.77328, -2.3488, -0.336903] # 435nm
|
||||
- [-3.69658, -2.25636, -0.381643] # 440nm
|
||||
- [-3.61702, -2.17402, -0.438245] # 445nm
|
||||
- [-3.5345, -2.11369, -0.492545] # 450nm
|
||||
- [-3.44893, -2.05413, -0.545499] # 455nm
|
||||
- [-3.36028, -1.99915, -0.598252] # 460nm
|
||||
- [-3.26865, -1.95299, -0.651178] # 465nm
|
||||
- [-3.17445, -1.89947, -0.713178] # 470nm
|
||||
- [-3.07897, -1.7772, -0.802671] # 475nm
|
||||
- [-2.98655, -1.62251, -0.91109] # 480nm
|
||||
- [-2.92017, -1.49243, -1.0519] # 485nm
|
||||
- [-2.90144, -1.38199, -1.21827] # 490nm
|
||||
- [-2.88287, -1.26603, -1.39796] # 495nm
|
||||
- [-2.85493, -1.15267, -1.60482] # 500nm
|
||||
- [-2.80572, -1.02914, -1.81046] # 505nm
|
||||
- [-2.72966, -0.920272, -2.03045] # 510nm
|
||||
- [-2.63272, -0.817001, -2.25779] # 515nm
|
||||
- [-2.55447, -0.725702, -2.48721] # 520nm
|
||||
- [-2.49041, -0.645483, -2.72649] # 525nm
|
||||
- [-2.43807, -0.567227, -2.96141] # 530nm
|
||||
- [-2.39766, -0.481455, -3.18441] # 535nm
|
||||
- [-2.36086, -0.392548, -3.38958] # 540nm
|
||||
- [-2.32322, -0.313171, -3.57896] # 545nm
|
||||
- [-2.2732, -0.271903, -3.75431] # 550nm
|
||||
- [-2.18787, -0.271253, -3.91714] # 555nm
|
||||
- [-2.05752, -0.306203, -4.06874] # 560nm
|
||||
- [-1.9276, -0.368685, -4.21023] # 565nm
|
||||
- [-1.78811, -0.479639, -4.3426] # 570nm
|
||||
- [-1.62896, -0.655083, -4.46669] # 575nm
|
||||
- [-1.44392, -0.924283, -4.58326] # 580nm
|
||||
- [-1.23269, -1.28153, -4.69297] # 585nm
|
||||
- [-1.03875, -1.72149, -4.79642] # 590nm
|
||||
- [-0.92718, -2.15137, -4.89412] # 595nm
|
||||
- [-0.842319, -2.49342, -4.98653] # 600nm
|
||||
- [-0.783739, -2.73332, -5.07408] # 605nm
|
||||
- [-0.731348, -2.94159, -5.15715] # 610nm
|
||||
- [-0.679261, -3.19096, -5.23606] # 615nm
|
||||
- [-0.620022, -3.42608, -5.31112] # 620nm
|
||||
- [-0.514782, -3.64814, -5.3826] # 625nm
|
||||
- [-0.398351, -3.8582, -5.45076] # 630nm
|
||||
- [-0.305333, -4.0572, -5.51583] # 635nm
|
||||
- [-0.237242, -4.246, -5.578] # 640nm
|
||||
- [-0.178503, -4.42535, -5.63746] # 645nm
|
||||
- [-0.150224, -4.59596, -5.6944] # 650nm
|
||||
- [-0.204724, -4.75844, -5.74897] # 655nm
|
||||
- [-0.36457, -4.91337, -5.8013] # 660nm
|
||||
- [-0.564382, -5.06125, -5.85155] # 665nm
|
||||
- [-0.820564, -5.20256, -5.89982] # 670nm
|
||||
- [-1.17385, -5.33773, -5.94624] # 675nm
|
||||
- [-1.62004, -5.46715, -5.99091] # 680nm
|
||||
- [-1.96204, -5.59117, -6.03392] # 685nm
|
||||
- [-2.24109, -5.71013, -6.07536] # 690nm
|
||||
- [-2.49926, -5.82433, -6.11533] # 695nm
|
||||
- [-2.74838, -5.93406, -6.1539] # 700nm
|
||||
- [-2.99905, -6.03956, -6.19113] # 705nm
|
||||
- [-3.23858, -6.14108, -6.2271] # 710nm
|
||||
- [-3.46769, -6.23884, -6.26188] # 715nm
|
||||
- [-3.68705, -6.33305, -6.29551] # 720nm
|
||||
- [-3.89728, -6.4239, -6.32806] # 725nm
|
||||
- [-4.09892, -6.51155, -6.35957] # 730nm
|
||||
- [-4.2925, -6.59618, -6.3901] # 735nm
|
||||
- [-4.47848, -6.67795, -6.4197] # 740nm
|
||||
- [-4.65732, -6.75699, -6.44839] # 745nm
|
||||
- [-4.8294, -6.83343, -6.47623] # 750nm
|
||||
- [-4.99511, -6.90741, -6.50325] # 755nm
|
||||
- [-5.1548, -6.97905, -6.52948] # 760nm
|
||||
- [-5.30878, -7.04844, -6.55497] # 765nm
|
||||
- [-5.45736, -7.1157, -6.57974] # 770nm
|
||||
- [-5.60081, -7.18092, -6.60382] # 775nm
|
||||
- [-5.7394, -7.24419, -6.62724] # 780nm
|
||||
|
||||
# Spectral density of each layer's dye at unit density, same grid and order.
|
||||
dye_density:
|
||||
- [0, 0, 0] # 380nm
|
||||
- [0, 0, 0] # 385nm
|
||||
- [0, 0, 0] # 390nm
|
||||
- [0, 0, 0] # 395nm
|
||||
- [0, 0, 0] # 400nm
|
||||
- [0, 0, 0] # 405nm
|
||||
- [0, 0, 0] # 410nm
|
||||
- [0, 0, 0] # 415nm
|
||||
- [0, 0, 0] # 420nm
|
||||
- [0.132063, 0.304413, 1.0302] # 425nm
|
||||
- [0.110557, 0.285921, 1.09084] # 430nm
|
||||
- [0.0907636, 0.26894, 1.12892] # 435nm
|
||||
- [0.0731545, 0.254546, 1.14915] # 440nm
|
||||
- [0.0582581, 0.242581, 1.15153] # 445nm
|
||||
- [0.0465229, 0.235263, 1.13547] # 450nm
|
||||
- [0.0375913, 0.233914, 1.10335] # 455nm
|
||||
- [0.030681, 0.240389, 1.05635] # 460nm
|
||||
- [0.0263108, 0.258726, 0.993064] # 465nm
|
||||
- [0.0233067, 0.288314, 0.916001] # 470nm
|
||||
- [0.0218301, 0.337849, 0.821932] # 475nm
|
||||
- [0.0203692, 0.398233, 0.714279] # 480nm
|
||||
- [0.019749, 0.461557, 0.60949] # 485nm
|
||||
- [0.0184053, 0.52715, 0.513298] # 490nm
|
||||
- [0.0184053, 0.596095, 0.425455] # 495nm
|
||||
- [0.0184053, 0.669677, 0.345381] # 500nm
|
||||
- [0.0184053, 0.74439, 0.277062] # 505nm
|
||||
- [0.0187644, 0.82762, 0.221787] # 510nm
|
||||
- [0.0205399, 0.901469, 0.175232] # 515nm
|
||||
- [0.0224509, 0.962613, 0.134817] # 520nm
|
||||
- [0.0254577, 1.01137, 0.103614] # 525nm
|
||||
- [0.029295, 1.04935, 0.0783271] # 530nm
|
||||
- [0.0344928, 1.07011, 0.0609916] # 535nm
|
||||
- [0.0431791, 1.07603, 0.0444904] # 540nm
|
||||
- [0.0535629, 1.06696, 0.0341927] # 545nm
|
||||
- [0.0682108, 1.04256, 0.0264798] # 550nm
|
||||
- [0.0846369, 1.00057, 0.0206373] # 555nm
|
||||
- [0.103981, 0.949336, 0.015902] # 560nm
|
||||
- [0.12617, 0.886999, 0.0123947] # 565nm
|
||||
- [0.152656, 0.814541, 0.0088987] # 570nm
|
||||
- [0.184423, 0.733298, 0.00755192] # 575nm
|
||||
- [0.222236, 0.647432, 0.00667624] # 580nm
|
||||
- [0.27164, 0.566835, 0.0054423] # 585nm
|
||||
- [0.338617, 0.490093, 0.0054423] # 590nm
|
||||
- [0.418873, 0.419349, 0.0054423] # 595nm
|
||||
- [0.512248, 0.356261, 0.0054423] # 600nm
|
||||
- [0.624482, 0.301552, 0.0054423] # 605nm
|
||||
- [0.746012, 0.252773, 0.0054423] # 610nm
|
||||
- [0.871893, 0.212174, 0.0054423] # 615nm
|
||||
- [0.997155, 0.180612, 0.0054423] # 620nm
|
||||
- [1.10993, 0.15459, 0.00662863] # 625nm
|
||||
- [1.20916, 0.131815, 0.00685166] # 630nm
|
||||
- [1.27283, 0.11411, 0.00826102] # 635nm
|
||||
- [1.30487, 0.0995825, 0.00826102] # 640nm
|
||||
- [1.30128, 0.088025, 0.00826102] # 645nm
|
||||
- [1.27159, 0.0781854, 0.00826102] # 650nm
|
||||
- [1.21934, 0.0702896, 0.00826102] # 655nm
|
||||
- [1.15587, 0.0633927, 0.00826102] # 660nm
|
||||
- [1.08056, 0.0573937, 0.00826102] # 665nm
|
||||
- [1.0013, 0.0516546, 0.00826102] # 670nm
|
||||
- [0.922994, 0.0462942, 0.00826102] # 675nm
|
||||
- [0.850116, 0.041061, 0.00826102] # 680nm
|
||||
- [0.780485, 0.0358446, 0.00826102] # 685nm
|
||||
- [0.716448, 0.0309058, 0.00826102] # 690nm
|
||||
- [0.655474, 0.0257217, 0.00826102] # 695nm
|
||||
- [0, 0, 0] # 700nm
|
||||
- [0, 0, 0] # 705nm
|
||||
- [0, 0, 0] # 710nm
|
||||
- [0, 0, 0] # 715nm
|
||||
- [0, 0, 0] # 720nm
|
||||
- [0, 0, 0] # 725nm
|
||||
- [0, 0, 0] # 730nm
|
||||
- [0, 0, 0] # 735nm
|
||||
- [0, 0, 0] # 740nm
|
||||
- [0, 0, 0] # 745nm
|
||||
- [0, 0, 0] # 750nm
|
||||
- [0, 0, 0] # 755nm
|
||||
- [0, 0, 0] # 760nm
|
||||
- [0, 0, 0] # 765nm
|
||||
- [0, 0, 0] # 770nm
|
||||
- [0, 0, 0] # 775nm
|
||||
- [0, 0, 0] # 780nm
|
||||
|
||||
# The support's own density -- film base plus, for a colour negative, the
|
||||
# orange mask. Flat zero where the datasheet does not give it.
|
||||
base_density: [0.199556, 0.199555, 0.199552, 0.199546, 0.199533, 0.199509, 0.199466, 0.199391, 0.199269, 0.19908, 0.198804, 0.198418, 0.197904, 0.19725, 0.196451, 0.195511, 0.194443, 0.193267, 0.192002, 0.190673, 0.189298, 0.187897, 0.186483, 0.185071, 0.183673, 0.182303, 0.180975, 0.179705, 0.178506, 0.177392, 0.17637, 0.175444, 0.17461, 0.173862, 0.173185, 0.172568, 0.171995, 0.171455, 0.17094, 0.170445, 0.169968, 0.169511, 0.169079, 0.168678, 0.168314, 0.167994, 0.167723, 0.1675, 0.167325, 0.167194, 0.1671, 0.167036, 0.166995, 0.166969, 0.166954, 0.166946, 0.166942, 0.16694, 0.166939, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938, 0.166938]
|
||||
|
||||
# The characteristic curves: density against log10 exposure, sampled
|
||||
# uniformly over [-3, 4].
|
||||
log_exposure_min: -3
|
||||
log_exposure_max: 4
|
||||
density_curves:
|
||||
- [2.9085, 2.8022, 2.258]
|
||||
- [2.9086, 2.8023, 2.2581]
|
||||
- [2.9088, 2.8026, 2.2583]
|
||||
- [2.9091, 2.8028, 2.2585]
|
||||
- [2.9092, 2.803, 2.2587]
|
||||
- [2.9093, 2.803, 2.259]
|
||||
- [2.9094, 2.803, 2.2592]
|
||||
- [2.9096, 2.803, 2.2593]
|
||||
- [2.9097, 2.8032, 2.2592]
|
||||
- [2.9098, 2.8034, 2.2591]
|
||||
- [2.9099, 2.8035, 2.2589]
|
||||
- [2.9098, 2.8034, 2.2588]
|
||||
- [2.9096, 2.8032, 2.2587]
|
||||
- [2.9095, 2.8029, 2.2588]
|
||||
- [2.9093, 2.8028, 2.2588]
|
||||
- [2.9092, 2.8028, 2.2589]
|
||||
- [2.9091, 2.8029, 2.259]
|
||||
- [2.9091, 2.8031, 2.259]
|
||||
- [2.9092, 2.8032, 2.2589]
|
||||
- [2.9093, 2.8032, 2.2588]
|
||||
- [2.9094, 2.8032, 2.2585]
|
||||
- [2.9095, 2.8031, 2.2582]
|
||||
- [2.9094, 2.8029, 2.258]
|
||||
- [2.9092, 2.8027, 2.2578]
|
||||
- [2.9088, 2.8025, 2.2577]
|
||||
- [2.9084, 2.8024, 2.2576]
|
||||
- [2.908, 2.8024, 2.2574]
|
||||
- [2.9077, 2.8025, 2.257]
|
||||
- [2.9075, 2.8025, 2.2565]
|
||||
- [2.9074, 2.8024, 2.2559]
|
||||
- [2.9071, 2.802, 2.2552]
|
||||
- [2.9066, 2.8016, 2.2545]
|
||||
- [2.9059, 2.8012, 2.2537]
|
||||
- [2.905, 2.8008, 2.2527]
|
||||
- [2.904, 2.8006, 2.2515]
|
||||
- [2.903, 2.8002, 2.2498]
|
||||
- [2.9019, 2.7995, 2.2478]
|
||||
- [2.9006, 2.7985, 2.2455]
|
||||
- [2.899, 2.797, 2.2429]
|
||||
- [2.897, 2.7953, 2.2401]
|
||||
- [2.8945, 2.7936, 2.2369]
|
||||
- [2.8916, 2.7919, 2.2334]
|
||||
- [2.8883, 2.7901, 2.2293]
|
||||
- [2.8846, 2.788, 2.2246]
|
||||
- [2.8805, 2.7854, 2.2192]
|
||||
- [2.8758, 2.7821, 2.2129]
|
||||
- [2.8706, 2.7781, 2.2057]
|
||||
- [2.8647, 2.7734, 2.1974]
|
||||
- [2.8578, 2.768, 2.1883]
|
||||
- [2.8498, 2.762, 2.1784]
|
||||
- [2.8406, 2.7552, 2.1676]
|
||||
- [2.8301, 2.7477, 2.156]
|
||||
- [2.8185, 2.7394, 2.1433]
|
||||
- [2.8059, 2.7302, 2.1295]
|
||||
- [2.7924, 2.7199, 2.1142]
|
||||
- [2.7776, 2.708, 2.0973]
|
||||
- [2.7611, 2.6942, 2.0784]
|
||||
- [2.7428, 2.6785, 2.0579]
|
||||
- [2.7227, 2.6613, 2.0363]
|
||||
- [2.7008, 2.6431, 2.0137]
|
||||
- [2.6772, 2.6241, 1.9898]
|
||||
- [2.6521, 2.6041, 1.9641]
|
||||
- [2.6254, 2.5823, 1.9362]
|
||||
- [2.5972, 2.5582, 1.9062]
|
||||
- [2.5672, 2.5314, 1.8746]
|
||||
- [2.535, 2.5023, 1.8416]
|
||||
- [2.5002, 2.4711, 1.8077]
|
||||
- [2.4627, 2.4381, 1.7726]
|
||||
- [2.4228, 2.4032, 1.7365]
|
||||
- [2.3812, 2.3659, 1.6995]
|
||||
- [2.3386, 2.3259, 1.6619]
|
||||
- [2.2954, 2.2837, 1.6238]
|
||||
- [2.2515, 2.24, 1.5851]
|
||||
- [2.2063, 2.1958, 1.5453]
|
||||
- [2.1589, 2.1512, 1.5038]
|
||||
- [2.1092, 2.1059, 1.4608]
|
||||
- [2.0575, 2.0587, 1.4169]
|
||||
- [2.0049, 2.0092, 1.3731]
|
||||
- [1.9523, 1.9576, 1.3304]
|
||||
- [1.9, 1.9049, 1.2892]
|
||||
- [1.8479, 1.852, 1.2489]
|
||||
- [1.7954, 1.7992, 1.2091]
|
||||
- [1.7423, 1.7462, 1.169]
|
||||
- [1.6886, 1.6924, 1.1287]
|
||||
- [1.6351, 1.6376, 1.0884]
|
||||
- [1.5821, 1.5827, 1.0484]
|
||||
- [1.5296, 1.529, 1.0089]
|
||||
- [1.4774, 1.4772, 0.97011]
|
||||
- [1.425, 1.4273, 0.93214]
|
||||
- [1.3728, 1.3779, 0.89522]
|
||||
- [1.3213, 1.3278, 0.85962]
|
||||
- [1.2712, 1.2767, 0.82559]
|
||||
- [1.223, 1.2257, 0.7932]
|
||||
- [1.1767, 1.1761, 0.76213]
|
||||
- [1.1317, 1.1289, 0.73162]
|
||||
- [1.0874, 1.0842, 0.70096]
|
||||
- [1.0435, 1.0413, 0.67004]
|
||||
- [1.0001, 0.99908, 0.63933]
|
||||
- [0.95744, 0.95715, 0.60948]
|
||||
- [0.9158, 0.91569, 0.58086]
|
||||
- [0.87518, 0.8753, 0.55346]
|
||||
- [0.83535, 0.83644, 0.527]
|
||||
- [0.79618, 0.79905, 0.50124]
|
||||
- [0.75785, 0.76267, 0.47618]
|
||||
- [0.72084, 0.72682, 0.45194]
|
||||
- [0.68564, 0.69139, 0.42854]
|
||||
- [0.65237, 0.65673, 0.40576]
|
||||
- [0.62063, 0.62336, 0.38322]
|
||||
- [0.58962, 0.59158, 0.36067]
|
||||
- [0.55861, 0.56127, 0.33821]
|
||||
- [0.52729, 0.53202, 0.31633]
|
||||
- [0.49595, 0.5034, 0.29558]
|
||||
- [0.46524, 0.4752, 0.27623]
|
||||
- [0.43579, 0.44746, 0.25814]
|
||||
- [0.40784, 0.42029, 0.24087]
|
||||
- [0.38137, 0.39387, 0.22388]
|
||||
- [0.3562, 0.36837, 0.20686]
|
||||
- [0.33206, 0.3438, 0.18997]
|
||||
- [0.3086, 0.31988, 0.17386]
|
||||
- [0.28567, 0.29648, 0.15923]
|
||||
- [0.26352, 0.27399, 0.14621]
|
||||
- [0.24263, 0.25313, 0.13429]
|
||||
- [0.22323, 0.23426, 0.12277]
|
||||
- [0.20505, 0.21681, 0.1114]
|
||||
- [0.18763, 0.19986, 0.10047]
|
||||
- [0.17067, 0.18278, 0.090413]
|
||||
- [0.15431, 0.16571, 0.081429]
|
||||
- [0.139, 0.14939, 0.073327]
|
||||
- [0.12518, 0.13464, 0.065691]
|
||||
- [0.11293, 0.1218, 0.058226]
|
||||
- [0.10196, 0.11055, 0.050983]
|
||||
- [0.09181, 0.10019, 0.044298]
|
||||
- [0.082155, 0.090144, 0.038512]
|
||||
- [0.07297, 0.080304, 0.033704]
|
||||
- [0.064433, 0.071013, 0.029651]
|
||||
- [0.056692, 0.06271, 0.026]
|
||||
- [0.049767, 0.055569, 0.022537]
|
||||
- [0.043556, 0.049429, 0.019363]
|
||||
- [0.037922, 0.043989, 0.016755]
|
||||
- [0.032771, 0.038987, 0.014852]
|
||||
- [0.028088, 0.034266, 0.013455]
|
||||
- [0.02393, 0.02978, 0.012125]
|
||||
- [0.020379, 0.025583, 0.010495]
|
||||
- [0.017439, 0.021771, 0.0085081]
|
||||
- [0.014967, 0.018403, 0.0064391]
|
||||
- [0.012763, 0.015457, 0.0046916]
|
||||
- [0.010736, 0.012876, 0.0035544]
|
||||
- [0.0089286, 0.010648, 0.0030383]
|
||||
- [0.0074304, 0.0088009, 0.0028922]
|
||||
- [0.0062662, 0.0073444, 0.0027804]
|
||||
- [0.0053567, 0.0062203, 0.0024881]
|
||||
- [0.0045781, 0.0053078, 0.0020191]
|
||||
- [0.0038543, 0.0044887, 0.001533]
|
||||
- [0.0031959, 0.0037139, 0.0011894]
|
||||
- [0.0026499, 0.0030152, 0.0010179]
|
||||
- [0.0022153, 0.0024513, 0.00090555]
|
||||
- [0.0018127, 0.0020369, 0.00069597]
|
||||
- [0.0013485, 0.0017151, 0.00031643]
|
||||
- [0.0008214, 0.0013997, -0.00016165]
|
||||
- [0.00036673, 0.0010497, -0.00056707]
|
||||
- [0.00017774, 0.00071158, -0.00073838]
|
||||
- [0.00034171, 0.00047793, -0.00060922]
|
||||
- [0.00076269, 0.0004524, -0.00025992]
|
||||
- [0.0011692, 0.00067021, 0.00012444]
|
||||
- [0.001241, 0.0010155, 0.00038129]
|
||||
- [0.00086169, 0.0012732, 0.00046591]
|
||||
- [0.00026144, 0.0012636, 0.00046137]
|
||||
- [-0.0001334, 0.000958, 0.00048917]
|
||||
- [-5.5336e-05, 0.00049013, 0.00058925]
|
||||
- [0.00036801, 6.3205e-05, 0.00067619]
|
||||
- [0.0007325, -0.00017662, 0.00061886]
|
||||
- [0.00072513, -0.00021671, 0.00037106]
|
||||
- [0.00036552, -0.00011557, -1.0987e-06]
|
||||
- [-2.5902e-05, 4.8655e-05, -0.00035504]
|
||||
- [-0.00010116, 0.00019943, -0.00054649]
|
||||
- [0.00021386, 0.00026529, -0.00051014]
|
||||
- [0.00064057, 0.00018964, -0.00029747]
|
||||
- [0.0007655, -3.8185e-05, -4.8039e-05]
|
||||
- [0.00038647, -0.00035067, 9.0634e-05]
|
||||
- [-0.00029492, -0.00060922, 4.8954e-05]
|
||||
- [-0.0008038, -0.00067051, -0.00012446]
|
||||
- [-0.00075863, -0.00048123, -0.00029445]
|
||||
- [-0.00018969, -0.00013211, -0.00032643]
|
||||
- [0.00045097, 0.000182, -0.00017628]
|
||||
- [0.00061513, 0.0002841, 7.5426e-05]
|
||||
- [8.0946e-05, 0.00013819, 0.0002707]
|
||||
- [-0.00085257, -0.00011985, 0.00027651]
|
||||
- [-0.0015592, -0.00027633, 7.6242e-05]
|
||||
- [-0.0015325, -0.00019738, -0.00021264]
|
||||
- [-0.00075622, 6.6026e-05, -0.00041065]
|
||||
- [0.00028047, 0.00031004, -0.00039291]
|
||||
- [0.00093235, 0.00033118, -0.00017229]
|
||||
- [0.00085257, 9.0093e-05, 0.00010842]
|
||||
- [0.00021762, -0.00024001, 0.00027098]
|
||||
- [-0.00043182, -0.00038625, 0.00021843]
|
||||
- [-0.00059978, -0.00017656, -5.3081e-06]
|
||||
- [-0.00018224, 0.00031533, -0.00025111]
|
||||
- [0.00049482, 0.00079007, -0.00036657]
|
||||
- [0.00094501, 0.0009022, -0.00028892]
|
||||
- [0.00086622, 0.00048514, -6.6985e-05]
|
||||
- [0.00036907, -0.00028899, 0.00017012]
|
||||
- [-0.00021405, -0.0010194, 0.00033786]
|
||||
- [-0.00064117, -0.0013644, 0.00045045]
|
||||
- [-0.00084149, -0.0012406, 0.00054246]
|
||||
- [-0.00080635, -0.00080673, 0.00057742]
|
||||
- [-0.00054163, -0.00030449, 0.00047883]
|
||||
- [-0.00014085, 9.0816e-05, 0.00025062]
|
||||
- [0.00020645, 0.00032748, 2.2587e-05]
|
||||
- [0.00039606, 0.00042477, -7.1076e-05]
|
||||
- [0.00046421, 0.00045154, -3.5319e-06]
|
||||
- [0.00045927, 0.00050805, 0.00014307]
|
||||
- [0.00041221, 0.00062938, 0.00021518]
|
||||
- [0.00032428, 0.00075855, 9.2604e-05]
|
||||
- [0.00019112, 0.00078669, -0.0002217]
|
||||
- [3.4831e-05, 0.00063167, -0.00059374]
|
||||
- [-8.8526e-05, 0.00030183, -0.00084102]
|
||||
- [-0.00011636, -9.7907e-05, -0.00084713]
|
||||
- [-2.3143e-05, -0.00042194, -0.00062749]
|
||||
- [0.00015297, -0.00056606, -0.00030374]
|
||||
- [0.00032266, -0.0005232, -1.3704e-05]
|
||||
- [0.00039221, -0.00037849, 0.00017119]
|
||||
- [0.00031597, -0.00024908, 0.00026249]
|
||||
- [0.00012212, -0.00020961, 0.00030531]
|
||||
- [-0.00010306, -0.00025131, 0.00031688]
|
||||
- [-0.00026154, -0.000299, 0.00027163]
|
||||
- [-0.00029259, -0.00027061, 0.00014416]
|
||||
- [-0.000198, -0.00013689, -3.495e-05]
|
||||
- [-3.3834e-05, 5.8756e-05, -0.00017183]
|
||||
- [0.00012171, 0.00023137, -0.00017006]
|
||||
- [0.00020459, 0.00031205, -1.5488e-05]
|
||||
- [0.00018864, 0.00029076, 0.00018992]
|
||||
- [9.0619e-05, 0.00021548, 0.00028646]
|
||||
- [-3.9992e-05, 0.00015062, 0.00017361]
|
||||
- [-0.00013957, 0.00012675, -0.00010314]
|
||||
- [-0.00015232, 0.00011888, -0.00037067]
|
||||
- [-5.2034e-05, 6.9298e-05, -0.00044858]
|
||||
- [0.00014165, -6.1556e-05, -0.00028559]
|
||||
- [0.00036103, -0.00025352, -5.6164e-06]
|
||||
- [0.00050974, -0.00042611, 0.00017901]
|
||||
- [0.0005025, -0.00048282, 0.00012788]
|
||||
- [0.00031031, -0.00037305, -0.0001142]
|
||||
- [-1.2991e-05, -0.00013156, -0.00034072]
|
||||
- [-0.00034329, 0.00013132, -0.00033198]
|
||||
- [-0.00053914, 0.00028491, -1.9892e-05]
|
||||
- [-0.00051235, 0.00024706, 0.00045446]
|
||||
- [-0.00027822, 2.7433e-05, 0.00083498]
|
||||
- [4.533e-05, -0.00027584, 0.0009199]
|
||||
- [0.00028471, -0.0005081, 0.00068138]
|
||||
- [0.00031968, -0.0005594, 0.00034871]
|
||||
- [0.00017724, -0.00042893, 0.00014744]
|
||||
- [-1.6195e-05, -0.00019955, 9.3372e-05]
|
||||
- [-0.00015589, 3.0964e-05, 5.2417e-05]
|
||||
- [-0.00023099, 0.00020411, -6.4328e-05]
|
||||
- [-0.00028939, 0.00031064, -0.00021919]
|
||||
- [-0.00035502, 0.00036863, -0.00033587]
|
||||
- [-0.00039868, 0.00039829, -0.00039436]
|
||||
@@ -0,0 +1,451 @@
|
||||
# Generated by tools/film-profiles/convert.py from spektrafilm.
|
||||
# Do not edit by hand: re-run the converter instead.
|
||||
#
|
||||
# spektrafilm by Andrea Volpato, https://github.com/andreavolpato/spektrafilm
|
||||
# Licensed CC BY-SA 4.0. Modified for DarkRoom: trimmed to the fields the
|
||||
# renderer uses and reformatted; see profiles/CHANGELOG.txt.
|
||||
|
||||
version: '0.3.2'
|
||||
stock: kodak_portra_400
|
||||
name: 'Kodak Portra 400'
|
||||
kind: negative # negative | positive
|
||||
support: film # film | paper
|
||||
reference_illuminant: D55
|
||||
viewing_illuminant: D50
|
||||
target_print: kodak_portra_endura
|
||||
|
||||
# log10 spectral sensitivity per layer, 380-780nm at 5nm, in R,G,B layer
|
||||
# order. A null upstream means the datasheet has no reading there, which is
|
||||
# blindness, so it is written as the sentinel the loader reads as such.
|
||||
log_sensitivity:
|
||||
- [-3.66312, -1.9105, -1.05544] # 380nm
|
||||
- [-3.62778, -1.75123, -0.812042] # 385nm
|
||||
- [-3.5912, -1.58419, -0.577251] # 390nm
|
||||
- [-3.55332, -1.41524, -0.439554] # 395nm
|
||||
- [-3.51406, -1.36541, -0.421037] # 400nm
|
||||
- [-3.47335, -1.38313, -0.42822] # 405nm
|
||||
- [-3.43111, -1.41667, -0.454152] # 410nm
|
||||
- [-3.38725, -1.45601, -0.465475] # 415nm
|
||||
- [-3.34168, -1.49309, -0.46525] # 420nm
|
||||
- [-3.29429, -1.52477, -0.461761] # 425nm
|
||||
- [-3.24497, -1.55499, -0.462887] # 430nm
|
||||
- [-3.19361, -1.58089, -0.477981] # 435nm
|
||||
- [-3.14007, -1.59898, -0.49958] # 440nm
|
||||
- [-3.08423, -1.60768, -0.513982] # 445nm
|
||||
- [-3.02592, -1.59969, -0.505317] # 450nm
|
||||
- [-2.965, -1.58737, -0.47808] # 455nm
|
||||
- [-2.90129, -1.57299, -0.44441] # 460nm
|
||||
- [-2.83461, -1.54727, -0.465573] # 465nm
|
||||
- [-2.76478, -1.45629, -0.576974] # 470nm
|
||||
- [-2.69162, -1.31698, -0.757578] # 475nm
|
||||
- [-2.615, -1.17901, -1.04004] # 480nm
|
||||
- [-2.53494, -1.05228, -1.31219] # 485nm
|
||||
- [-2.45211, -0.9545, -1.55602] # 490nm
|
||||
- [-2.37103, -0.889429, -1.74412] # 495nm
|
||||
- [-2.30372, -0.844224, -1.93419] # 500nm
|
||||
- [-2.22635, -0.80203, -2.14501] # 505nm
|
||||
- [-2.1563, -0.757219, -2.35322] # 510nm
|
||||
- [-2.09593, -0.708979, -2.56234] # 515nm
|
||||
- [-2.05102, -0.659683, -2.76203] # 520nm
|
||||
- [-2.03118, -0.600564, -2.94796] # 525nm
|
||||
- [-2.03124, -0.52984, -3.12148] # 530nm
|
||||
- [-2.04938, -0.460433, -3.28381] # 535nm
|
||||
- [-2.06575, -0.406837, -3.436] # 540nm
|
||||
- [-2.06866, -0.387961, -3.57896] # 545nm
|
||||
- [-2.01996, -0.42299, -3.71352] # 550nm
|
||||
- [-1.8979, -0.471326, -3.84038] # 555nm
|
||||
- [-1.76573, -0.525351, -3.9602] # 560nm
|
||||
- [-1.62369, -0.585682, -4.07354] # 565nm
|
||||
- [-1.41794, -0.647456, -4.18091] # 570nm
|
||||
- [-1.18867, -0.777194, -4.28278] # 575nm
|
||||
- [-0.982419, -1.0025, -4.37955] # 580nm
|
||||
- [-0.825985, -1.29162, -4.47161] # 585nm
|
||||
- [-0.742621, -1.64695, -4.55928] # 590nm
|
||||
- [-0.678892, -2.02199, -4.64287] # 595nm
|
||||
- [-0.608978, -2.38502, -4.72266] # 600nm
|
||||
- [-0.542076, -2.73117, -4.79891] # 605nm
|
||||
- [-0.495836, -3.06158, -4.87184] # 610nm
|
||||
- [-0.46373, -3.37731, -4.94167] # 615nm
|
||||
- [-0.448918, -3.67931, -5.00859] # 620nm
|
||||
- [-0.445041, -3.96846, -5.07278] # 625nm
|
||||
- [-0.443984, -4.24556, -5.1344] # 630nm
|
||||
- [-0.40515, -4.51135, -5.1936] # 635nm
|
||||
- [-0.287594, -4.76652, -5.25053] # 640nm
|
||||
- [-0.237399, -5.01167, -5.3053] # 645nm
|
||||
- [-0.266077, -5.24739, -5.35805] # 650nm
|
||||
- [-0.414397, -5.47422, -5.40888] # 655nm
|
||||
- [-0.729115, -5.69265, -5.4579] # 660nm
|
||||
- [-1.09275, -5.90314, -5.5052] # 665nm
|
||||
- [-1.49817, -6.10611, -5.55086] # 670nm
|
||||
- [-1.89931, -6.30195, -5.59498] # 675nm
|
||||
- [-2.28672, -6.49105, -5.63762] # 680nm
|
||||
- [-2.65374, -6.67373, -5.67887] # 685nm
|
||||
- [-3.00194, -6.85032, -5.71879] # 690nm
|
||||
- [-3.33273, -7.02113, -5.75744] # 695nm
|
||||
- [-3.64738, -7.18642, -5.79488] # 700nm
|
||||
- [-3.94705, -7.34647, -5.83117] # 705nm
|
||||
- [-4.23278, -7.50151, -5.86636] # 710nm
|
||||
- [-4.50552, -7.65179, -5.9005] # 715nm
|
||||
- [-4.76614, -7.79751, -5.93364] # 720nm
|
||||
- [-5.01543, -7.93888, -5.96582] # 725nm
|
||||
- [-5.25411, -8.07609, -5.99707] # 730nm
|
||||
- [-5.48285, -8.20933, -6.02745] # 735nm
|
||||
- [-5.70225, -8.33876, -6.05698] # 740nm
|
||||
- [-5.91287, -8.46454, -6.0857] # 745nm
|
||||
- [-6.11524, -8.58683, -6.11365] # 750nm
|
||||
- [-6.30982, -8.70577, -6.14085] # 755nm
|
||||
- [-6.49706, -8.8215, -6.16734] # 760nm
|
||||
- [-6.67736, -8.93413, -6.19314] # 765nm
|
||||
- [-6.85111, -9.04381, -6.21827] # 770nm
|
||||
- [-7.01865, -9.15063, -6.24277] # 775nm
|
||||
- [-7.18032, -9.25472, -6.26666] # 780nm
|
||||
|
||||
# Spectral density of each layer's dye at unit density, same grid and order.
|
||||
dye_density:
|
||||
- [0, 0, 0] # 380nm
|
||||
- [0.403908, 0.0667059, 0.131477] # 385nm
|
||||
- [0.367788, 0.0606233, 0.225086] # 390nm
|
||||
- [0.334623, 0.0551188, 0.311665] # 395nm
|
||||
- [0.30152, 0.0506611, 0.396483] # 400nm
|
||||
- [0.264513, 0.0473523, 0.485257] # 405nm
|
||||
- [0.221389, 0.0446968, 0.580102] # 410nm
|
||||
- [0.173337, 0.0416705, 0.679412] # 415nm
|
||||
- [0.124541, 0.0370168, 0.779979] # 420nm
|
||||
- [0.0799763, 0.0296143, 0.878155] # 425nm
|
||||
- [0.0431787, 0.0188834, 0.969734] # 430nm
|
||||
- [0.0155139, 0.00522799, 1.04986] # 435nm
|
||||
- [-0.00319846, -0.00966512, 1.11373] # 440nm
|
||||
- [-0.013822, -0.0230595, 1.15771] # 445nm
|
||||
- [-0.017493, -0.0319997, 1.17951] # 450nm
|
||||
- [-0.0155223, -0.0340031, 1.17738] # 455nm
|
||||
- [-0.00931813, -0.0266502, 1.14991] # 460nm
|
||||
- [-0.000425396, -0.00643381, 1.09657] # 465nm
|
||||
- [0.00957477, 0.0311778, 1.01844] # 470nm
|
||||
- [0.0193053, 0.0897715, 0.918527] # 475nm
|
||||
- [0.0277613, 0.170248, 0.801755] # 480nm
|
||||
- [0.0344012, 0.270943, 0.674456] # 485nm
|
||||
- [0.039108, 0.388631, 0.543319] # 490nm
|
||||
- [0.0420001, 0.518745, 0.414641] # 495nm
|
||||
- [0.0431986, 0.655203, 0.294549] # 500nm
|
||||
- [0.0427053, 0.791143, 0.188993] # 505nm
|
||||
- [0.0404019, 0.920428, 0.10285] # 510nm
|
||||
- [0.0360661, 1.03835, 0.0387973] # 515nm
|
||||
- [0.0293948, 1.14135, -0.00323375] # 520nm
|
||||
- [0.0201419, 1.22614, -0.0257816] # 525nm
|
||||
- [0.00841956, 1.28885, -0.0330331] # 530nm
|
||||
- [-0.00501966, 1.32518, -0.0297272] # 535nm
|
||||
- [-0.018723, 1.33234, -0.0203227] # 540nm
|
||||
- [-0.0306564, 1.31034, -0.00860152] # 545nm
|
||||
- [-0.0383851, 1.26141, 0.00253253] # 550nm
|
||||
- [-0.0392928, 1.18834, 0.0113352] # 555nm
|
||||
- [-0.0309223, 1.09412, 0.0172494] # 560nm
|
||||
- [-0.0114667, 0.982893, 0.0205307] # 565nm
|
||||
- [0.0198622, 0.860868, 0.0217858] # 570nm
|
||||
- [0.0628668, 0.735986, 0.0216915] # 575nm
|
||||
- [0.116549, 0.616053, 0.0208882] # 580nm
|
||||
- [0.179172, 0.506574, 0.0199193] # 585nm
|
||||
- [0.248268, 0.410247, 0.0191115] # 590nm
|
||||
- [0.320861, 0.327992, 0.0184567] # 595nm
|
||||
- [0.394136, 0.259757, 0.0176575] # 600nm
|
||||
- [0.466336, 0.204598, 0.0163398] # 605nm
|
||||
- [0.53702, 0.160639, 0.0142776] # 610nm
|
||||
- [0.606468, 0.125418, 0.0115514] # 615nm
|
||||
- [0.674916, 0.0966418, 0.00857863] # 620nm
|
||||
- [0.742318, 0.0728261, 0.00594004] # 625nm
|
||||
- [0.808706, 0.0531795, 0.00403784] # 630nm
|
||||
- [0.874452, 0.0370944, 0.00287428] # 635nm
|
||||
- [0.93984, 0.0238931, 0.00219026] # 640nm
|
||||
- [1.0045, 0.0128995, 0.00172239] # 645nm
|
||||
- [1.0676, 0.00359228, 0.0013275] # 650nm
|
||||
- [1.12851, -0.00429998, 0.000968393] # 655nm
|
||||
- [1.18682, -0.0108354, 0.000654986] # 660nm
|
||||
- [1.24178, -0.0159821, 0.00040166] # 665nm
|
||||
- [1.29179, -0.0197322, 0.000211292] # 670nm
|
||||
- [1.33462, -0.0221459, 7.44371e-05] # 675nm
|
||||
- [1.36839, -0.0233499, -2.79391e-14] # 680nm
|
||||
- [1.39252, -0.023508, -4.55659e-15] # 685nm
|
||||
- [1.40741, -0.0227797, -7.04121e-16] # 690nm
|
||||
- [1.41307, -0.0213116, -1.03094e-16] # 695nm
|
||||
- [1.40823, -0.0192756, -1.43022e-17] # 700nm
|
||||
- [1.39096, -0.0168542, -1.87996e-18] # 705nm
|
||||
- [1.35994, -0.0141712, -2.3414e-19] # 710nm
|
||||
- [1.31526, -0.0112948, -2.76301e-20] # 715nm
|
||||
- [1.25857, -0.00826775, -3.08936e-21] # 720nm
|
||||
- [1.19237, -0.00512446, 9.34392e-06] # 725nm
|
||||
- [1.11913, -0.00189787, 0.000144643] # 730nm
|
||||
- [1.04097, 0.00138255, 0.000236008] # 735nm
|
||||
- [0.959902, 0.00469464, 0.000243281] # 740nm
|
||||
- [0.877953, 0.00802695, 0.0001264] # 745nm
|
||||
- [0, 0.0113815, -1.94521e-27] # 750nm
|
||||
- [0, 0, 0] # 755nm
|
||||
- [0, 0, 0] # 760nm
|
||||
- [0, 0, 0] # 765nm
|
||||
- [0, 0, 0] # 770nm
|
||||
- [0, 0, 0] # 775nm
|
||||
- [0, 0, 0] # 780nm
|
||||
|
||||
# The support's own density -- film base plus, for a colour negative, the
|
||||
# orange mask. Flat zero where the datasheet does not give it.
|
||||
base_density: [0, 0, 0, 0, 0.688743, 0.668732, 0.674145, 0.704822, 0.738875, 0.77018, 0.793902, 0.808976, 0.816583, 0.818014, 0.815048, 0.807834, 0.799308, 0.790429, 0.781571, 0.773041, 0.76365, 0.752376, 0.743825, 0.747508, 0.759366, 0.775658, 0.787589, 0.770607, 0.738775, 0.69906, 0.659996, 0.638018, 0.623696, 0.612488, 0.602398, 0.591425, 0.576479, 0.55225, 0.519473, 0.475224, 0.424808, 0.370525, 0.318688, 0.275608, 0.241346, 0.219229, 0.205387, 0.197521, 0.193575, 0.190765, 0.188882, 0.188114, 0.188779, 0.191225, 0.194555, 0.197605, 0.200865, 0.204898, 0.209047, 0.212126, 0.215084, 0.218257, 0.220278, 0.221012, 0.220415, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
|
||||
# The characteristic curves: density against log10 exposure, sampled
|
||||
# uniformly over [-3, 4].
|
||||
log_exposure_min: -3
|
||||
log_exposure_max: 4
|
||||
density_curves:
|
||||
- [-0.00073111, -0.00065213, -0.00069304]
|
||||
- [-0.00062474, -0.00054039, -0.0006216]
|
||||
- [-0.00045032, -0.00034771, -0.00049642]
|
||||
- [-0.00026227, -0.00014774, -0.00033606]
|
||||
- [-0.00010948, -2.4766e-05, -0.00014274]
|
||||
- [-4.6701e-06, -9.5833e-06, 7.363e-05]
|
||||
- [8.2384e-05, -4.0322e-05, 0.00026447]
|
||||
- [0.00019143, -1.1913e-05, 0.00035835]
|
||||
- [0.00032496, 0.00011845, 0.00032078]
|
||||
- [0.00043695, 0.00028078, 0.00019266]
|
||||
- [0.00047319, 0.00035737, 5.8608e-05]
|
||||
- [0.00042036, 0.00029493, -2.3099e-05]
|
||||
- [0.0002958, 0.00013055, -3.5339e-05]
|
||||
- [0.00014169, -4.3263e-05, 8.8129e-06]
|
||||
- [3.5432e-06, -0.00013931, 8.7678e-05]
|
||||
- [-8.8492e-05, -0.00012422, 0.00018558]
|
||||
- [-0.00012241, -2.485e-05, 0.00028953]
|
||||
- [-9.5307e-05, 0.00010244, 0.00037738]
|
||||
- [-6.7857e-06, 0.00020926, 0.00041647]
|
||||
- [0.0001354, 0.00027201, 0.00038168]
|
||||
- [0.00030203, 0.00028483, 0.00028172]
|
||||
- [0.00043785, 0.00024749, 0.00016974]
|
||||
- [0.00047889, 0.00016645, 0.00012254]
|
||||
- [0.00038775, 6.8028e-05, 0.00019692]
|
||||
- [0.00018506, 3.9085e-06, 0.00039217]
|
||||
- [-4.8248e-05, 3.1824e-05, 0.00064801]
|
||||
- [-0.00020636, 0.00017717, 0.00088341]
|
||||
- [-0.00021438, 0.00040487, 0.0010516]
|
||||
- [-7.0388e-05, 0.00063107, 0.0011739]
|
||||
- [0.00015421, 0.00077627, 0.001327]
|
||||
- [0.00035798, 0.00082635, 0.0015938]
|
||||
- [0.00046989, 0.00085444, 0.0020096]
|
||||
- [0.00048943, 0.0009801, 0.0025429]
|
||||
- [0.00048239, 0.0012882, 0.0031211]
|
||||
- [0.00053767, 0.0017637, 0.0036831]
|
||||
- [0.00071445, 0.0022925, 0.0042229]
|
||||
- [0.0010124, 0.0027348, 0.0047962]
|
||||
- [0.0013806, 0.0030236, 0.005492]
|
||||
- [0.0017541, 0.0032226, 0.006391]
|
||||
- [0.0020941, 0.0035018, 0.0075396]
|
||||
- [0.0024076, 0.0040442, 0.0089499]
|
||||
- [0.002742, 0.0049461, 0.010615]
|
||||
- [0.0031634, 0.0061729, 0.012522]
|
||||
- [0.0037348, 0.0075954, 0.014649]
|
||||
- [0.0045013, 0.0090772, 0.016968]
|
||||
- [0.0054825, 0.010554, 0.019451]
|
||||
- [0.0066658, 0.012057, 0.022103]
|
||||
- [0.0080042, 0.013687, 0.024997]
|
||||
- [0.0094266, 0.015555, 0.028279]
|
||||
- [0.010863, 0.017761, 0.032112]
|
||||
- [0.01229, 0.020356, 0.036574]
|
||||
- [0.013801, 0.023376, 0.041665]
|
||||
- [0.015601, 0.026884, 0.04739]
|
||||
- [0.017895, 0.030911, 0.053724]
|
||||
- [0.020733, 0.035346, 0.06052]
|
||||
- [0.023983, 0.039929, 0.067561]
|
||||
- [0.02747, 0.04445, 0.074797]
|
||||
- [0.031136, 0.04901, 0.082506]
|
||||
- [0.035063, 0.054073, 0.091121]
|
||||
- [0.039359, 0.060157, 0.1008]
|
||||
- [0.044172, 0.067527, 0.11131]
|
||||
- [0.049652, 0.076026, 0.12221]
|
||||
- [0.0559, 0.085161, 0.13323]
|
||||
- [0.062913, 0.094424, 0.14438]
|
||||
- [0.070545, 0.10361, 0.15598]
|
||||
- [0.078526, 0.11294, 0.16832]
|
||||
- [0.086576, 0.12278, 0.18153]
|
||||
- [0.09458, 0.13334, 0.19549]
|
||||
- [0.10273, 0.14443, 0.2101]
|
||||
- [0.11149, 0.15565, 0.22537]
|
||||
- [0.1214, 0.16675, 0.24143]
|
||||
- [0.13274, 0.17796, 0.25832]
|
||||
- [0.14532, 0.18991, 0.27574]
|
||||
- [0.15852, 0.2032, 0.29311]
|
||||
- [0.17166, 0.21791, 0.30988]
|
||||
- [0.18437, 0.23346, 0.32589]
|
||||
- [0.19687, 0.24893, 0.34155]
|
||||
- [0.20977, 0.26369, 0.35762]
|
||||
- [0.22367, 0.2778, 0.37472]
|
||||
- [0.23873, 0.29186, 0.39293]
|
||||
- [0.25462, 0.30648, 0.41182]
|
||||
- [0.27074, 0.32174, 0.43068]
|
||||
- [0.28666, 0.33713, 0.44895]
|
||||
- [0.30237, 0.35199, 0.46646]
|
||||
- [0.31815, 0.36612, 0.48329]
|
||||
- [0.33425, 0.37998, 0.49963]
|
||||
- [0.35053, 0.39441, 0.51558]
|
||||
- [0.36654, 0.40996, 0.53119]
|
||||
- [0.38187, 0.42632, 0.54658]
|
||||
- [0.39654, 0.44244, 0.56191]
|
||||
- [0.411, 0.45721, 0.5774]
|
||||
- [0.42586, 0.47033, 0.59328]
|
||||
- [0.44143, 0.48245, 0.60966]
|
||||
- [0.45761, 0.49469, 0.62626]
|
||||
- [0.47388, 0.50786, 0.64245]
|
||||
- [0.4897, 0.52196, 0.6576]
|
||||
- [0.50472, 0.53638, 0.6716]
|
||||
- [0.51898, 0.55034, 0.68494]
|
||||
- [0.53278, 0.56343, 0.69826]
|
||||
- [0.54637, 0.57584, 0.712]
|
||||
- [0.55978, 0.5881, 0.7262]
|
||||
- [0.57283, 0.60064, 0.74066]
|
||||
- [0.5854, 0.61348, 0.7552]
|
||||
- [0.59772, 0.62628, 0.76986]
|
||||
- [0.6103, 0.63863, 0.78481]
|
||||
- [0.62373, 0.65046, 0.80013]
|
||||
- [0.63821, 0.66212, 0.81559]
|
||||
- [0.6534, 0.67416, 0.83079]
|
||||
- [0.66852, 0.68695, 0.8454]
|
||||
- [0.68279, 0.70045, 0.85951]
|
||||
- [0.69583, 0.71429, 0.87364]
|
||||
- [0.70791, 0.72805, 0.88849]
|
||||
- [0.71976, 0.74151, 0.90444]
|
||||
- [0.73208, 0.75467, 0.92136]
|
||||
- [0.74525, 0.76764, 0.93866]
|
||||
- [0.75926, 0.7806, 0.95557]
|
||||
- [0.77393, 0.79374, 0.97152]
|
||||
- [0.78891, 0.80704, 0.98663]
|
||||
- [0.80372, 0.82014, 1.0018]
|
||||
- [0.81807, 0.8328, 1.0182]
|
||||
- [0.83225, 0.84548, 1.036]
|
||||
- [0.84696, 0.85917, 1.0545]
|
||||
- [0.86258, 0.87443, 1.0725]
|
||||
- [0.87871, 0.89052, 1.0895]
|
||||
- [0.89458, 0.90605, 1.1058]
|
||||
- [0.90962, 0.92, 1.1224]
|
||||
- [0.92396, 0.93241, 1.1397]
|
||||
- [0.93841, 0.94438, 1.1572]
|
||||
- [0.95382, 0.95729, 1.1743]
|
||||
- [0.97053, 0.97184, 1.1903]
|
||||
- [0.98807, 0.98761, 1.2054]
|
||||
- [1.0055, 1.0034, 1.2201]
|
||||
- [1.0223, 1.0183, 1.2355]
|
||||
- [1.0382, 1.0319, 1.2518]
|
||||
- [1.0537, 1.0449, 1.2687]
|
||||
- [1.0693, 1.0583, 1.2853]
|
||||
- [1.085, 1.0725, 1.3013]
|
||||
- [1.1009, 1.0873, 1.317]
|
||||
- [1.1165, 1.1023, 1.3331]
|
||||
- [1.1318, 1.1169, 1.3502]
|
||||
- [1.1468, 1.1309, 1.3679]
|
||||
- [1.1618, 1.1442, 1.3851]
|
||||
- [1.1772, 1.1569, 1.4007]
|
||||
- [1.1931, 1.1695, 1.4148]
|
||||
- [1.2093, 1.1821, 1.428]
|
||||
- [1.2252, 1.1948, 1.4417]
|
||||
- [1.2408, 1.2076, 1.4567]
|
||||
- [1.2561, 1.2206, 1.4732]
|
||||
- [1.2717, 1.2339, 1.4907]
|
||||
- [1.2877, 1.2476, 1.508]
|
||||
- [1.304, 1.2618, 1.5247]
|
||||
- [1.3201, 1.276, 1.5406]
|
||||
- [1.336, 1.2902, 1.5565]
|
||||
- [1.3518, 1.3041, 1.5727]
|
||||
- [1.3677, 1.318, 1.5894]
|
||||
- [1.3836, 1.332, 1.6063]
|
||||
- [1.3995, 1.3462, 1.6229]
|
||||
- [1.415, 1.3606, 1.6389]
|
||||
- [1.4302, 1.3747, 1.6546]
|
||||
- [1.4454, 1.3886, 1.6705]
|
||||
- [1.4616, 1.4025, 1.6872]
|
||||
- [1.4789, 1.4165, 1.7049]
|
||||
- [1.497, 1.431, 1.7232]
|
||||
- [1.5151, 1.4461, 1.7416]
|
||||
- [1.5319, 1.4615, 1.7595]
|
||||
- [1.5471, 1.4765, 1.7767]
|
||||
- [1.5614, 1.4904, 1.7934]
|
||||
- [1.5764, 1.5034, 1.8101]
|
||||
- [1.593, 1.5157, 1.8269]
|
||||
- [1.6106, 1.5279, 1.8433]
|
||||
- [1.628, 1.5405, 1.8591]
|
||||
- [1.6438, 1.5534, 1.874]
|
||||
- [1.6583, 1.5665, 1.8882]
|
||||
- [1.6725, 1.5794, 1.9021]
|
||||
- [1.6875, 1.592, 1.9162]
|
||||
- [1.7036, 1.604, 1.9306]
|
||||
- [1.7198, 1.6153, 1.9451]
|
||||
- [1.7346, 1.6259, 1.9592]
|
||||
- [1.7475, 1.6359, 1.9725]
|
||||
- [1.7591, 1.6457, 1.9849]
|
||||
- [1.771, 1.6556, 1.9965]
|
||||
- [1.7842, 1.6657, 2.0076]
|
||||
- [1.7986, 1.6757, 2.0185]
|
||||
- [1.8126, 1.6852, 2.0294]
|
||||
- [1.8247, 1.6938, 2.04]
|
||||
- [1.8345, 1.7014, 2.0499]
|
||||
- [1.8429, 1.7084, 2.0588]
|
||||
- [1.8515, 1.7153, 2.0669]
|
||||
- [1.8616, 1.7222, 2.0743]
|
||||
- [1.873, 1.729, 2.0814]
|
||||
- [1.8844, 1.7353, 2.0885]
|
||||
- [1.8943, 1.7409, 2.0954]
|
||||
- [1.902, 1.7457, 2.102]
|
||||
- [1.9081, 1.75, 2.108]
|
||||
- [1.9138, 1.7542, 2.1131]
|
||||
- [1.9201, 1.7587, 2.1176]
|
||||
- [1.9272, 1.7632, 2.1217]
|
||||
- [1.9342, 1.7674, 2.1257]
|
||||
- [1.9403, 1.7707, 2.1296]
|
||||
- [1.945, 1.7731, 2.1333]
|
||||
- [1.9488, 1.7749, 2.1368]
|
||||
- [1.9521, 1.7765, 2.1398]
|
||||
- [1.9553, 1.7783, 2.1425]
|
||||
- [1.9586, 1.7805, 2.145]
|
||||
- [1.962, 1.7829, 2.1471]
|
||||
- [1.9653, 1.7851, 2.1489]
|
||||
- [1.9686, 1.787, 2.1503]
|
||||
- [1.9715, 1.7886, 2.1516]
|
||||
- [1.974, 1.7898, 2.1529]
|
||||
- [1.976, 1.7909, 2.1541]
|
||||
- [1.9778, 1.7919, 2.1554]
|
||||
- [1.9794, 1.7929, 2.1564]
|
||||
- [1.9807, 1.7937, 2.1571]
|
||||
- [1.9819, 1.7944, 2.1575]
|
||||
- [1.9828, 1.7948, 2.1578]
|
||||
- [1.9837, 1.795, 2.1582]
|
||||
- [1.9846, 1.7951, 2.1587]
|
||||
- [1.9855, 1.7952, 2.1593]
|
||||
- [1.9864, 1.7954, 2.16]
|
||||
- [1.9872, 1.7957, 2.1606]
|
||||
- [1.9878, 1.7961, 2.161]
|
||||
- [1.9882, 1.7964, 2.1613]
|
||||
- [1.9884, 1.7967, 2.1616]
|
||||
- [1.9885, 1.7968, 2.1618]
|
||||
- [1.9887, 1.7969, 2.1619]
|
||||
- [1.9889, 1.797, 2.1619]
|
||||
- [1.9893, 1.7972, 2.1619]
|
||||
- [1.9896, 1.7975, 2.1618]
|
||||
- [1.9899, 1.7977, 2.1619]
|
||||
- [1.9902, 1.7978, 2.1621]
|
||||
- [1.9903, 1.7979, 2.1623]
|
||||
- [1.9903, 1.7978, 2.1625]
|
||||
- [1.9902, 1.7978, 2.1624]
|
||||
- [1.9902, 1.7978, 2.1622]
|
||||
- [1.9903, 1.7978, 2.162]
|
||||
- [1.9904, 1.7978, 2.162]
|
||||
- [1.9906, 1.7977, 2.1621]
|
||||
- [1.9909, 1.7976, 2.1624]
|
||||
- [1.991, 1.7975, 2.1626]
|
||||
- [1.9911, 1.7974, 2.1625]
|
||||
- [1.9909, 1.7975, 2.1623]
|
||||
- [1.9906, 1.7977, 2.1621]
|
||||
- [1.9904, 1.798, 2.1622]
|
||||
- [1.9902, 1.7981, 2.1624]
|
||||
- [1.9902, 1.7981, 2.1628]
|
||||
- [1.9905, 1.7979, 2.1631]
|
||||
- [1.9907, 1.7976, 2.1632]
|
||||
- [1.991, 1.7975, 2.163]
|
||||
- [1.991, 1.7974, 2.1627]
|
||||
- [1.9909, 1.7975, 2.1626]
|
||||
- [1.9907, 1.7977, 2.1625]
|
||||
- [1.9906, 1.7979, 2.1625]
|
||||
- [1.9905, 1.798, 2.1624]
|
||||
- [1.9905, 1.7981, 2.1623]
|
||||
- [1.9904, 1.7982, 2.1622]
|
||||
- [1.9904, 1.7982, 2.1621]
|
||||
@@ -0,0 +1,450 @@
|
||||
# Generated by tools/film-profiles/convert.py from spektrafilm.
|
||||
# Do not edit by hand: re-run the converter instead.
|
||||
#
|
||||
# spektrafilm by Andrea Volpato, https://github.com/andreavolpato/spektrafilm
|
||||
# Licensed CC BY-SA 4.0. Modified for DarkRoom: trimmed to the fields the
|
||||
# renderer uses and reformatted; see profiles/CHANGELOG.txt.
|
||||
|
||||
version: '0.3.2'
|
||||
stock: kodak_portra_endura
|
||||
name: 'Kodak Professional Portra Endura'
|
||||
kind: negative # negative | positive
|
||||
support: paper # film | paper
|
||||
reference_illuminant: TH-KG3
|
||||
viewing_illuminant: D50
|
||||
|
||||
# log10 spectral sensitivity per layer, 380-780nm at 5nm, in R,G,B layer
|
||||
# order. A null upstream means the datasheet has no reading there, which is
|
||||
# blindness, so it is written as the sentinel the loader reads as such.
|
||||
log_sensitivity:
|
||||
- [-2.06329, -0.66685, -0.121172] # 380nm
|
||||
- [-2.03304, -0.305851, 0.0973343] # 385nm
|
||||
- [-2.00191, 0.0741485, 0.250639] # 390nm
|
||||
- [-1.96987, -0.310914, 0.383591] # 395nm
|
||||
- [-1.93687, -0.31851, 0.551078] # 400nm
|
||||
- [-1.90287, -0.326725, 0.635971] # 405nm
|
||||
- [-1.86783, -0.383764, 0.62894] # 410nm
|
||||
- [-1.83168, -0.518279, 0.593405] # 415nm
|
||||
- [-1.7944, -0.581634, 0.619546] # 420nm
|
||||
- [-1.7559, -0.561393, 0.687253] # 425nm
|
||||
- [-1.71615, -0.514748, 0.796341] # 430nm
|
||||
- [-1.67507, -0.454635, 0.865381] # 435nm
|
||||
- [-1.6326, -0.380725, 0.954704] # 440nm
|
||||
- [-1.58867, -0.298006, 1.00643] # 445nm
|
||||
- [-1.54319, -0.213912, 1.07115] # 450nm
|
||||
- [-1.49609, -0.12172, 1.17427] # 455nm
|
||||
- [-1.44728, -0.0411163, 1.29397] # 460nm
|
||||
- [-1.39665, 0.031887, 1.44968] # 465nm
|
||||
- [-1.34412, 0.0901617, 1.58062] # 470nm
|
||||
- [-1.28957, 0.152138, 1.62687] # 475nm
|
||||
- [-1.23288, 0.25906, 1.43016] # 480nm
|
||||
- [-1.17392, 0.384654, 0.994591] # 485nm
|
||||
- [-1.11255, 0.470883, 0.412335] # 490nm
|
||||
- [-1.04863, 0.551546, -0.0851402] # 495nm
|
||||
- [-0.981986, 0.632001, -0.621981] # 500nm
|
||||
- [-0.914599, 0.663976, -1.17939] # 505nm
|
||||
- [-0.876885, 0.65417, -1.57519] # 510nm
|
||||
- [-0.861842, 0.625837, -2.03941] # 515nm
|
||||
- [-0.86324, 0.603816, -2.47047] # 520nm
|
||||
- [-0.866, 0.638158, -2.87181] # 525nm
|
||||
- [-0.838642, 0.719433, -3.24639] # 530nm
|
||||
- [-0.798993, 0.811755, -3.5968] # 535nm
|
||||
- [-0.727273, 0.960897, -3.92531] # 540nm
|
||||
- [-0.668813, 1.11846, -4.23391] # 545nm
|
||||
- [-0.697153, 1.24462, -4.52436] # 550nm
|
||||
- [-0.875581, 1.11215, -4.79821] # 555nm
|
||||
- [-0.997296, 0.522578, -5.05685] # 560nm
|
||||
- [-1.03559, -0.298703, -5.3015] # 565nm
|
||||
- [-1.06471, -0.778553, -5.53328] # 570nm
|
||||
- [-1.03236, -1.08038, -5.75318] # 575nm
|
||||
- [-1.0246, -1.37283, -5.96208] # 580nm
|
||||
- [-1.00021, -1.74401, -6.16079] # 585nm
|
||||
- [-0.972478, -2.11701, -6.35003] # 590nm
|
||||
- [-0.949305, -2.47678, -6.53048] # 595nm
|
||||
- [-0.914915, -2.82135, -6.70272] # 600nm
|
||||
- [-0.864414, -3.15085, -6.86731] # 605nm
|
||||
- [-0.820752, -3.4659, -7.02474] # 610nm
|
||||
- [-0.765307, -3.76728, -7.17547] # 615nm
|
||||
- [-0.707123, -4.05576, -7.31992] # 620nm
|
||||
- [-0.64149, -4.3321, -7.45848] # 625nm
|
||||
- [-0.595538, -4.59704, -7.59149] # 630nm
|
||||
- [-0.55543, -4.85124, -7.71929] # 635nm
|
||||
- [-0.532502, -5.09533, -7.84217] # 640nm
|
||||
- [-0.533161, -5.32989, -7.96042] # 645nm
|
||||
- [-0.544821, -5.55546, -8.07428] # 650nm
|
||||
- [-0.55516, -5.77255, -8.18401] # 655nm
|
||||
- [-0.537088, -5.98162, -8.28981] # 660nm
|
||||
- [-0.498599, -6.1831, -8.39191] # 665nm
|
||||
- [-0.451245, -6.37741, -8.49048] # 670nm
|
||||
- [-0.370217, -6.56491, -8.58571] # 675nm
|
||||
- [-0.275692, -6.74595, -8.67777] # 680nm
|
||||
- [-0.164173, -6.92087, -8.76681] # 685nm
|
||||
- [-0.0304521, -7.08996, -8.85297] # 690nm
|
||||
- [0.0915279, -7.25352, -8.93641] # 695nm
|
||||
- [0.180394, -7.4118, -9.01723] # 700nm
|
||||
- [0.219145, -7.56507, -9.09557] # 705nm
|
||||
- [0.161057, -7.71355, -9.17153] # 710nm
|
||||
- [0.0281059, -7.85747, -9.24523] # 715nm
|
||||
- [-0.153701, -7.99702, -9.31675] # 720nm
|
||||
- [-0.423982, -8.13242, -9.38621] # 725nm
|
||||
- [-0.711793, -8.26383, -9.45368] # 730nm
|
||||
- [-0.959498, -8.39144, -9.51925] # 735nm
|
||||
- [-1.21811, -8.51541, -9.583] # 740nm
|
||||
- [-1.46617, -8.63588, -9.645] # 745nm
|
||||
- [-1.7043, -8.75301, -9.70533] # 750nm
|
||||
- [-1.9331, -8.86694, -9.76404] # 755nm
|
||||
- [-2.15309, -8.97778, -9.82122] # 760nm
|
||||
- [-2.36479, -9.08567, -9.8769] # 765nm
|
||||
- [-2.56864, -9.19072, -9.93116] # 770nm
|
||||
- [-2.76508, -9.29305, -9.98405] # 775nm
|
||||
- [-2.95451, -9.39275, -10.0356] # 780nm
|
||||
|
||||
# Spectral density of each layer's dye at unit density, same grid and order.
|
||||
dye_density:
|
||||
- [0, 0, 0] # 380nm
|
||||
- [0, 0, 0] # 385nm
|
||||
- [0, 0, 0] # 390nm
|
||||
- [0, 0, 0] # 395nm
|
||||
- [0, 0, 0] # 400nm
|
||||
- [0.13463, 0.0557315, 0.583161] # 405nm
|
||||
- [0.128013, 0.0595558, 0.699108] # 410nm
|
||||
- [0.121487, 0.0609707, 0.822296] # 415nm
|
||||
- [0.112527, 0.0624132, 0.935903] # 420nm
|
||||
- [0.102341, 0.0628756, 1.0147] # 425nm
|
||||
- [0.0880839, 0.0641499, 1.07006] # 430nm
|
||||
- [0.0774323, 0.0588941, 1.10649] # 435nm
|
||||
- [0.0688873, 0.0674788, 1.12786] # 440nm
|
||||
- [0.0591469, 0.0755021, 1.14003] # 445nm
|
||||
- [0.0529705, 0.087447, 1.13788] # 450nm
|
||||
- [0.0486079, 0.104152, 1.12044] # 455nm
|
||||
- [0.0461916, 0.125824, 1.08581] # 460nm
|
||||
- [0.0423221, 0.15245, 1.03122] # 465nm
|
||||
- [0.0387559, 0.185448, 0.956634] # 470nm
|
||||
- [0.0373831, 0.224825, 0.870222] # 475nm
|
||||
- [0.0403377, 0.271465, 0.773301] # 480nm
|
||||
- [0.0439569, 0.326176, 0.673801] # 485nm
|
||||
- [0.0457122, 0.389224, 0.57195] # 490nm
|
||||
- [0.0495, 0.457945, 0.47455] # 495nm
|
||||
- [0.0535765, 0.54069, 0.387515] # 500nm
|
||||
- [0.0581268, 0.627548, 0.311545] # 505nm
|
||||
- [0.064248, 0.715738, 0.247134] # 510nm
|
||||
- [0.0719348, 0.801546, 0.193774] # 515nm
|
||||
- [0.0821404, 0.883435, 0.151117] # 520nm
|
||||
- [0.0920384, 0.95845, 0.114229] # 525nm
|
||||
- [0.103544, 1.02756, 0.090246] # 530nm
|
||||
- [0.119572, 1.08782, 0.0702681] # 535nm
|
||||
- [0.13682, 1.13692, 0.0551267] # 540nm
|
||||
- [0.157964, 1.17018, 0.0435376] # 545nm
|
||||
- [0.184214, 1.17917, 0.031796] # 550nm
|
||||
- [0.216843, 1.14297, 0.0280471] # 555nm
|
||||
- [0.256148, 1.07173, 0.0221872] # 560nm
|
||||
- [0.29836, 0.957004, 0.0144489] # 565nm
|
||||
- [0.34956, 0.824702, 0.00937225] # 570nm
|
||||
- [0.406378, 0.685333, 0.00691271] # 575nm
|
||||
- [0.466262, 0.549027, 0.00704693] # 580nm
|
||||
- [0.536021, 0.433989, 0.00728366] # 585nm
|
||||
- [0.608747, 0.340986, 0.00720199] # 590nm
|
||||
- [0.687167, 0.264, 0.00712033] # 595nm
|
||||
- [0.769187, 0.204049, 0.00983489] # 600nm
|
||||
- [0.853719, 0.158457, 0.00493586] # 605nm
|
||||
- [0.936366, 0.123842, 0.00485419] # 610nm
|
||||
- [1.01604, 0.0976066, 0.00477253] # 615nm
|
||||
- [1.07871, 0.0794798, 0.0046245] # 620nm
|
||||
- [1.12143, 0.0658755, 0.00359699] # 625nm
|
||||
- [1.1415, 0.0569475, 0.00452753] # 630nm
|
||||
- [1.13625, 0.0506214, 0.00444587] # 635nm
|
||||
- [1.10666, 0.0478845, 0.0043642] # 640nm
|
||||
- [1.05166, 0.0445471, 0.00428254] # 645nm
|
||||
- [0.980878, 0.0397732, 0.00807407] # 650nm
|
||||
- [0.898305, 0.0442732, 0.00578807] # 655nm
|
||||
- [0.812798, 0.0429953, 0.00684416] # 660nm
|
||||
- [0.731294, 0.0401979, 0.00748007] # 665nm
|
||||
- [0.653378, 0.0396883, 0.00754484] # 670nm
|
||||
- [0.579554, 0.0375944, 0.00817276] # 675nm
|
||||
- [0.511348, 0.0353716, 0.00832798] # 680nm
|
||||
- [0.450789, 0.0343428, 0.00839517] # 685nm
|
||||
- [0.39617, 0.0315678, 0.0086004] # 690nm
|
||||
- [0.352446, 0.0304327, 0.00851873] # 695nm
|
||||
- [0.313195, 0, 0.0141632] # 700nm
|
||||
- [0, 0, 0] # 705nm
|
||||
- [0, 0, 0] # 710nm
|
||||
- [0, 0, 0] # 715nm
|
||||
- [0, 0, 0] # 720nm
|
||||
- [0, 0, 0] # 725nm
|
||||
- [0, 0, 0] # 730nm
|
||||
- [0, 0, 0] # 735nm
|
||||
- [0, 0, 0] # 740nm
|
||||
- [0, 0, 0] # 745nm
|
||||
- [0, 0, 0] # 750nm
|
||||
- [0, 0, 0] # 755nm
|
||||
- [0, 0, 0] # 760nm
|
||||
- [0, 0, 0] # 765nm
|
||||
- [0, 0, 0] # 770nm
|
||||
- [0, 0, 0] # 775nm
|
||||
- [0, 0, 0] # 780nm
|
||||
|
||||
# The support's own density -- film base plus, for a colour negative, the
|
||||
# orange mask. Flat zero where the datasheet does not give it.
|
||||
base_density: [0.0965288, 0.0965285, 0.0965279, 0.0965263, 0.0965233, 0.0965174, 0.0965067, 0.0964884, 0.0964585, 0.0964124, 0.0963448, 0.0962504, 0.0961247, 0.0959646, 0.095769, 0.0955391, 0.0952779, 0.09499, 0.0946806, 0.0943551, 0.0940186, 0.093675, 0.0933279, 0.0929801, 0.0926342, 0.0922927, 0.0919581, 0.0916329, 0.0913197, 0.0910204, 0.0907366, 0.0904687, 0.0902163, 0.0899779, 0.0897517, 0.0895353, 0.0893266, 0.089124, 0.0889263, 0.0887335, 0.0885461, 0.0883659, 0.0881949, 0.0880359, 0.0878918, 0.087765, 0.0876572, 0.0875689, 0.0874996, 0.0874476, 0.0874103, 0.0873849, 0.0873684, 0.0873583, 0.0873524, 0.0873491, 0.0873475, 0.0873466, 0.0873462, 0.0873461, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346, 0.087346]
|
||||
|
||||
# The characteristic curves: density against log10 exposure, sampled
|
||||
# uniformly over [-3, 4].
|
||||
log_exposure_min: -3
|
||||
log_exposure_max: 4
|
||||
density_curves:
|
||||
- [-0.00082207, -0.00082089, -0.0008278]
|
||||
- [-0.00070251, -0.00068042, -0.00074299]
|
||||
- [-0.00050646, -0.00043818, -0.00059421]
|
||||
- [-0.00029511, -0.00018684, -0.00040366]
|
||||
- [-0.00012342, -3.2448e-05, -0.00017405]
|
||||
- [-5.7059e-06, -1.3791e-05, 8.2719e-05]
|
||||
- [9.1999e-05, -5.3052e-05, 0.00030861]
|
||||
- [0.00021437, -1.8094e-05, 0.00041835]
|
||||
- [0.00036418, 0.00014486, 0.00037072]
|
||||
- [0.00048967, 0.00034766, 0.00021435]
|
||||
- [0.00052986, 0.00044217, 4.9948e-05]
|
||||
- [0.00046977, 0.0003613, -5.334e-05]
|
||||
- [0.00032883, 0.00015162, -7.5506e-05]
|
||||
- [0.00015441, -7.0724e-05, -3.2627e-05]
|
||||
- [-2.3977e-06, -0.00019632, 4.8881e-05]
|
||||
- [-0.00010783, -0.00018362, 0.00014965]
|
||||
- [-0.00014853, -6.686e-05, 0.00025336]
|
||||
- [-0.00012144, 8.2629e-05, 0.00033274]
|
||||
- [-2.6361e-05, 0.00020353, 0.00034796]
|
||||
- [0.00012767, 0.00026558, 0.00026816]
|
||||
- [0.00030747, 0.00026076, 0.00010248]
|
||||
- [0.00045058, 0.00018801, -8.7828e-05]
|
||||
- [0.00048483, 5.4486e-05, -0.00021447]
|
||||
- [0.00036793, -0.00010806, -0.0002141]
|
||||
- [0.00012262, -0.00023675, -9.1985e-05]
|
||||
- [-0.00016103, -0.00026207, 7.6502e-05]
|
||||
- [-0.00036566, -0.00015576, 0.00019237]
|
||||
- [-0.00040922, 3.4806e-05, 0.0001975]
|
||||
- [-0.00029184, 0.00020223, 0.0001122]
|
||||
- [-9.5598e-05, 0.0002456, 1.8958e-05]
|
||||
- [6.4346e-05, 0.00014515, 3.3838e-06]
|
||||
- [0.00010726, -1.3323e-05, 9.4186e-05]
|
||||
- [3.073e-05, -9.0986e-05, 0.00024356]
|
||||
- [-9.5183e-05, 4.539e-06, 0.00035985]
|
||||
- [-0.00017677, 0.0002432, 0.00036739]
|
||||
- [-0.00015513, 0.00047913, 0.00025196]
|
||||
- [-3.7734e-05, 0.00054189, 6.3882e-05]
|
||||
- [0.00011276, 0.00035217, -0.00011721]
|
||||
- [0.00021999, -1.8483e-05, -0.00022712]
|
||||
- [0.00023667, -0.00038648, -0.00024212]
|
||||
- [0.00016448, -0.00056744, -0.00017543]
|
||||
- [4.5715e-05, -0.00048499, -5.8608e-05]
|
||||
- [-6.1191e-05, -0.00020712, 7.2192e-05]
|
||||
- [-0.00010547, 0.00010637, 0.00017789]
|
||||
- [-5.8446e-05, 0.00030226, 0.00021564]
|
||||
- [7.9802e-05, 0.00031088, 0.00014983]
|
||||
- [0.0002772, 0.00016015, -2.3921e-05]
|
||||
- [0.00047128, -6.5728e-05, -0.00025896]
|
||||
- [0.00058293, -0.00028098, -0.0004647]
|
||||
- [0.00054165, -0.00042321, -0.00055092]
|
||||
- [0.0003253, -0.00047619, -0.00049915]
|
||||
- [7.3888e-06, -0.00044152, -0.00034298]
|
||||
- [-0.00025808, -0.00030075, -0.00010896]
|
||||
- [-0.00033975, -6.2065e-05, 0.00016363]
|
||||
- [-0.00023732, 0.00017098, 0.00037559]
|
||||
- [-7.8829e-05, 0.00021859, 0.00040886]
|
||||
- [-4.4954e-06, -3.7905e-05, 0.00024984]
|
||||
- [-5.8369e-05, -0.00051423, 4.6701e-05]
|
||||
- [-0.0001911, -0.00093489, -1.4553e-06]
|
||||
- [-0.00034321, -0.0010368, 0.00015455]
|
||||
- [-0.00044226, -0.0007303, 0.00038475]
|
||||
- [-0.00042548, -0.00014729, 0.000506]
|
||||
- [-0.00026556, 0.00044183, 0.00041486]
|
||||
- [8.8435e-06, 0.00080062, 0.00014824]
|
||||
- [0.00030586, 0.00086561, -0.00016299]
|
||||
- [0.00049284, 0.00075457, -0.00039756]
|
||||
- [0.00045444, 0.00064466, -0.00051651]
|
||||
- [0.00016436, 0.00061974, -0.00055319]
|
||||
- [-0.00027428, 0.00060877, -0.00054402]
|
||||
- [-0.00065897, 0.00046392, -0.00047331]
|
||||
- [-0.0007874, 0.00011382, -0.00028769]
|
||||
- [-0.00057432, -0.00033589, 3.0919e-05]
|
||||
- [-0.00011044, -0.00064475, 0.00040313]
|
||||
- [0.00038362, -0.00059918, 0.00066759]
|
||||
- [0.00068867, -0.00017235, 0.00067851]
|
||||
- [0.00072362, 0.00044485, 0.00041855]
|
||||
- [0.00059903, 0.0009731, 3.9507e-05]
|
||||
- [0.00055203, 0.0012421, -0.00020708]
|
||||
- [0.0008068, 0.0013094, -0.00010653]
|
||||
- [0.0014557, 0.0014131, 0.00040832]
|
||||
- [0.0024402, 0.0017956, 0.0012452]
|
||||
- [0.0036392, 0.0025391, 0.0022398]
|
||||
- [0.0049973, 0.0035404, 0.0032739]
|
||||
- [0.0066002, 0.0046495, 0.0043434]
|
||||
- [0.0086436, 0.0058649, 0.0055424]
|
||||
- [0.011326, 0.0074311, 0.0069988]
|
||||
- [0.014747, 0.0097516, 0.0088261]
|
||||
- [0.018898, 0.01318, 0.011141]
|
||||
- [0.023789, 0.017786, 0.014089]
|
||||
- [0.029576, 0.023312, 0.017857]
|
||||
- [0.036589, 0.029431, 0.022674]
|
||||
- [0.045255, 0.036081, 0.028806]
|
||||
- [0.055938, 0.043634, 0.036488]
|
||||
- [0.06883, 0.052747, 0.045803]
|
||||
- [0.083934, 0.06403, 0.056628]
|
||||
- [0.10116, 0.077739, 0.068765]
|
||||
- [0.12047, 0.093735, 0.082227]
|
||||
- [0.14198, 0.11169, 0.097357]
|
||||
- [0.16592, 0.13141, 0.11464]
|
||||
- [0.19254, 0.15304, 0.13452]
|
||||
- [0.22197, 0.17701, 0.15718]
|
||||
- [0.25414, 0.20378, 0.18267]
|
||||
- [0.28906, 0.23351, 0.211]
|
||||
- [0.32694, 0.26602, 0.24233]
|
||||
- [0.36835, 0.30103, 0.27695]
|
||||
- [0.41397, 0.33849, 0.31507]
|
||||
- [0.46429, 0.37883, 0.35668]
|
||||
- [0.51925, 0.42272, 0.40145]
|
||||
- [0.57825, 0.47071, 0.44911]
|
||||
- [0.64052, 0.52287, 0.49971]
|
||||
- [0.70557, 0.57876, 0.55379]
|
||||
- [0.7735, 0.63774, 0.61208]
|
||||
- [0.84489, 0.69924, 0.67494]
|
||||
- [0.92031, 0.76289, 0.74196]
|
||||
- [0.99978, 0.82832, 0.81204]
|
||||
- [1.0827, 0.8952, 0.88365]
|
||||
- [1.1682, 0.96314, 0.95537]
|
||||
- [1.2549, 1.0314, 1.0266]
|
||||
- [1.3412, 1.0987, 1.0975]
|
||||
- [1.4258, 1.164, 1.1687]
|
||||
- [1.5082, 1.2274, 1.2394]
|
||||
- [1.5882, 1.2894, 1.3079]
|
||||
- [1.6656, 1.3501, 1.372]
|
||||
- [1.7392, 1.4082, 1.4306]
|
||||
- [1.8077, 1.4618, 1.4839]
|
||||
- [1.8703, 1.5099, 1.5325]
|
||||
- [1.9274, 1.5528, 1.5765]
|
||||
- [1.9797, 1.5919, 1.6158]
|
||||
- [2.0281, 1.6289, 1.6498]
|
||||
- [2.0731, 1.6645, 1.6784]
|
||||
- [2.1144, 1.6985, 1.7022]
|
||||
- [2.1518, 1.7301, 1.7223]
|
||||
- [2.1853, 1.7588, 1.7396]
|
||||
- [2.2151, 1.7848, 1.7546]
|
||||
- [2.242, 1.8088, 1.7673]
|
||||
- [2.2665, 1.8316, 1.7778]
|
||||
- [2.2889, 1.8537, 1.7862]
|
||||
- [2.3094, 1.875, 1.793]
|
||||
- [2.3279, 1.8952, 1.7987]
|
||||
- [2.3445, 1.9138, 1.8038]
|
||||
- [2.3594, 1.9309, 1.8082]
|
||||
- [2.3728, 1.9464, 1.8117]
|
||||
- [2.385, 1.9604, 1.8141]
|
||||
- [2.396, 1.9732, 1.8154]
|
||||
- [2.406, 1.9848, 1.8161]
|
||||
- [2.4146, 1.9955, 1.8166]
|
||||
- [2.4221, 2.0051, 1.8173]
|
||||
- [2.4285, 2.0137, 1.8183]
|
||||
- [2.4341, 2.0215, 1.8193]
|
||||
- [2.4391, 2.0286, 1.8202]
|
||||
- [2.4433, 2.0349, 1.8207]
|
||||
- [2.4469, 2.0405, 1.8209]
|
||||
- [2.4499, 2.0452, 1.8209]
|
||||
- [2.4523, 2.0492, 1.821]
|
||||
- [2.4543, 2.0525, 1.8212]
|
||||
- [2.456, 2.0553, 1.8213]
|
||||
- [2.4573, 2.0577, 1.8213]
|
||||
- [2.4582, 2.0598, 1.8211]
|
||||
- [2.4588, 2.0614, 1.8207]
|
||||
- [2.4593, 2.0627, 1.8204]
|
||||
- [2.4598, 2.0636, 1.8203]
|
||||
- [2.4605, 2.0645, 1.8205]
|
||||
- [2.4614, 2.0653, 1.8209]
|
||||
- [2.4622, 2.0662, 1.8213]
|
||||
- [2.4625, 2.0671, 1.8216]
|
||||
- [2.4623, 2.0678, 1.8217]
|
||||
- [2.4619, 2.0681, 1.8218]
|
||||
- [2.4616, 2.068, 1.8218]
|
||||
- [2.4618, 2.0678, 1.8219]
|
||||
- [2.4623, 2.0675, 1.822]
|
||||
- [2.4627, 2.0674, 1.8219]
|
||||
- [2.4627, 2.0674, 1.8217]
|
||||
- [2.4624, 2.0676, 1.8213]
|
||||
- [2.462, 2.0678, 1.821]
|
||||
- [2.462, 2.068, 1.8208]
|
||||
- [2.4623, 2.0681, 1.8208]
|
||||
- [2.4628, 2.068, 1.821]
|
||||
- [2.4629, 2.0678, 1.8213]
|
||||
- [2.4625, 2.0675, 1.8214]
|
||||
- [2.4618, 2.0673, 1.8214]
|
||||
- [2.4613, 2.0672, 1.8212]
|
||||
- [2.4614, 2.0674, 1.8211]
|
||||
- [2.4619, 2.0677, 1.821]
|
||||
- [2.4626, 2.0681, 1.8212]
|
||||
- [2.4627, 2.0682, 1.8214]
|
||||
- [2.4622, 2.068, 1.8216]
|
||||
- [2.4613, 2.0678, 1.8216]
|
||||
- [2.4606, 2.0676, 1.8214]
|
||||
- [2.4606, 2.0677, 1.8211]
|
||||
- [2.4614, 2.068, 1.8209]
|
||||
- [2.4624, 2.0682, 1.821]
|
||||
- [2.4631, 2.0682, 1.8212]
|
||||
- [2.463, 2.068, 1.8215]
|
||||
- [2.4623, 2.0676, 1.8216]
|
||||
- [2.4617, 2.0675, 1.8216]
|
||||
- [2.4615, 2.0677, 1.8213]
|
||||
- [2.462, 2.0682, 1.8211]
|
||||
- [2.4626, 2.0687, 1.821]
|
||||
- [2.4631, 2.0688, 1.8211]
|
||||
- [2.463, 2.0684, 1.8213]
|
||||
- [2.4625, 2.0676, 1.8215]
|
||||
- [2.4619, 2.0669, 1.8217]
|
||||
- [2.4615, 2.0665, 1.8218]
|
||||
- [2.4613, 2.0666, 1.8219]
|
||||
- [2.4613, 2.0671, 1.8219]
|
||||
- [2.4616, 2.0676, 1.8218]
|
||||
- [2.462, 2.068, 1.8216]
|
||||
- [2.4623, 2.0682, 1.8214]
|
||||
- [2.4625, 2.0683, 1.8213]
|
||||
- [2.4626, 2.0683, 1.8213]
|
||||
- [2.4626, 2.0684, 1.8215]
|
||||
- [2.4625, 2.0685, 1.8216]
|
||||
- [2.4625, 2.0686, 1.8214]
|
||||
- [2.4623, 2.0687, 1.8211]
|
||||
- [2.4622, 2.0685, 1.8208]
|
||||
- [2.462, 2.0682, 1.8205]
|
||||
- [2.462, 2.0678, 1.8205]
|
||||
- [2.4621, 2.0675, 1.8207]
|
||||
- [2.4623, 2.0673, 1.821]
|
||||
- [2.4625, 2.0674, 1.8213]
|
||||
- [2.4625, 2.0675, 1.8215]
|
||||
- [2.4624, 2.0676, 1.8216]
|
||||
- [2.4623, 2.0677, 1.8217]
|
||||
- [2.462, 2.0676, 1.8217]
|
||||
- [2.4619, 2.0676, 1.8216]
|
||||
- [2.4618, 2.0676, 1.8215]
|
||||
- [2.4619, 2.0678, 1.8213]
|
||||
- [2.4621, 2.0679, 1.8212]
|
||||
- [2.4623, 2.0681, 1.8212]
|
||||
- [2.4623, 2.0682, 1.8213]
|
||||
- [2.4623, 2.0682, 1.8215]
|
||||
- [2.4622, 2.0681, 1.8216]
|
||||
- [2.4621, 2.068, 1.8215]
|
||||
- [2.462, 2.068, 1.8212]
|
||||
- [2.462, 2.068, 1.821]
|
||||
- [2.4621, 2.068, 1.8209]
|
||||
- [2.4623, 2.0678, 1.8211]
|
||||
- [2.4625, 2.0676, 1.8213]
|
||||
- [2.4626, 2.0675, 1.8215]
|
||||
- [2.4626, 2.0674, 1.8215]
|
||||
- [2.4624, 2.0675, 1.8212]
|
||||
- [2.4621, 2.0678, 1.821]
|
||||
- [2.4618, 2.068, 1.821]
|
||||
- [2.4616, 2.0682, 1.8213]
|
||||
- [2.4616, 2.0681, 1.8218]
|
||||
- [2.4619, 2.0679, 1.8222]
|
||||
- [2.4622, 2.0676, 1.8223]
|
||||
- [2.4624, 2.0674, 1.822]
|
||||
- [2.4625, 2.0673, 1.8217]
|
||||
- [2.4623, 2.0675, 1.8215]
|
||||
- [2.4621, 2.0677, 1.8214]
|
||||
- [2.462, 2.0679, 1.8214]
|
||||
- [2.4619, 2.0681, 1.8213]
|
||||
- [2.4618, 2.0682, 1.8211]
|
||||
- [2.4618, 2.0683, 1.821]
|
||||
- [2.4617, 2.0683, 1.821]
|
||||
@@ -0,0 +1,456 @@
|
||||
//! Turning a stock into something a shader can run.
|
||||
//!
|
||||
//! # The decomposition
|
||||
//!
|
||||
//! A physically-honest film simulation looks like it needs a spectral
|
||||
//! integration per pixel, and vkdt's does exactly that. It does not have to,
|
||||
//! and the reason is worth writing down because it is what makes this cheap
|
||||
//! enough to run on a phone:
|
||||
//!
|
||||
//! 1. **Exposure is a 3×3 matrix.** A layer's exposure is
|
||||
//! `∫ S(λ)·L(λ) dλ`, and the scene spectrum `L` reconstructed from an sRGB
|
||||
//! triple is *linear* in that triple — that is what a spectral basis is. So
|
||||
//! the whole integral collapses into nine numbers, computed once, exactly.
|
||||
//! No approximation is involved.
|
||||
//!
|
||||
//! 2. **The characteristic curve is three 1D functions.** Sampled exactly, at
|
||||
//! [`crate::profile::CURVE_SAMPLES`].
|
||||
//!
|
||||
//! 3. **Everything after that is a function of three densities.** The dye
|
||||
//! transmittance, the print exposure through the negative, the paper's own
|
||||
//! curves and dyes, the viewing illuminant, the adaptation — all of it takes
|
||||
//! three numbers in and gives three numbers out. So it bakes into one small
|
||||
//! 3D lookup, and the per-pixel cost is a matrix multiply, three curve taps
|
||||
//! and one texture fetch.
|
||||
//!
|
||||
//! Splitting 2 from 3 rather than baking a single LUT over exposure is
|
||||
//! deliberate and measured: the curve carries all of the sharp shape and the
|
||||
//! dye mixing is smooth, so putting the curve in the 3D LUT would force it
|
||||
//! three times larger for the same error.
|
||||
|
||||
use crate::profile::{Profile, CURVE_SAMPLES};
|
||||
use crate::spectrum::{illuminant, Spectrum, Viewing};
|
||||
use crate::tables::{SPECTRUM, SRGB_BASIS};
|
||||
|
||||
/// The mid-grey a photographic exposure is reckoned from.
|
||||
///
|
||||
/// 18.4% rather than 18%: it is the value the upstream profiles are calibrated
|
||||
/// against, and a profile calibrated at one grey and rendered at another is
|
||||
/// off by a fraction of a stop everywhere.
|
||||
pub const MID_GREY: f32 = 0.184;
|
||||
|
||||
/// The edge length of the baked density lookup.
|
||||
///
|
||||
/// 32 holds the worst-case interpolation error to about 0.003 in linear sRGB,
|
||||
/// which is below one 8-bit code value, in 384 kB. Doubling it buys a factor
|
||||
/// of four in error for eight times the memory, and there is nothing to spend
|
||||
/// that on: the error is already under what the output can represent.
|
||||
pub const LUT_SIZE: usize = 32;
|
||||
|
||||
/// What to develop, and how.
|
||||
pub struct Recipe<'a> {
|
||||
/// The stock the picture was taken on.
|
||||
pub film: &'a Profile,
|
||||
/// The paper it is printed on. `None` views the film directly, which is
|
||||
/// what a reversal stock wants and what makes a negative come out orange
|
||||
/// and inverted — that being what a negative actually looks like.
|
||||
pub print: Option<&'a Profile>,
|
||||
/// Camera exposure, in stops.
|
||||
pub exposure_ev: f32,
|
||||
/// Enlarger exposure, in stops. Ignored without a `print`.
|
||||
pub print_exposure_ev: f32,
|
||||
}
|
||||
|
||||
impl<'a> Recipe<'a> {
|
||||
/// The straightforward reading of a stock: reversal viewed directly,
|
||||
/// negative printed on the paper its datasheet names.
|
||||
pub fn new(film: &'a Profile, print: Option<&'a Profile>) -> Self {
|
||||
Self { film, print, exposure_ev: 0.0, print_exposure_ev: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// A recipe reduced to three tables.
|
||||
///
|
||||
/// Plain `f32` with a documented layout, and no notion of a texture: what to
|
||||
/// bind this to is dr-gpu's decision, and keeping it out of here is what lets
|
||||
/// the whole model be tested on the CPU.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Baked {
|
||||
/// Linear sRGB to the three layers' log₁₀ exposure, before the log — row
|
||||
/// `l`, column `c` is layer `l`'s response to sRGB channel `c`.
|
||||
pub exposure_matrix: [[f32; 3]; 3],
|
||||
/// The characteristic curves, `CURVE_SAMPLES` samples per layer, uniform
|
||||
/// over `[curve_log_min, curve_log_max]`.
|
||||
pub curves: Vec<[f32; 3]>,
|
||||
pub curve_log_min: f32,
|
||||
pub curve_log_max: f32,
|
||||
/// Density to linear sRGB, `LUT_SIZE³` entries in x-major order, uniform
|
||||
/// over `[0, density_max]` on each axis.
|
||||
pub lut: Vec<[f32; 3]>,
|
||||
pub density_max: f32,
|
||||
pub lut_size: usize,
|
||||
}
|
||||
|
||||
impl Baked {
|
||||
/// Look a colour up the way the shader will, for tests and for previews.
|
||||
pub fn apply(&self, rgb: [f32; 3]) -> [f32; 3] {
|
||||
let mut log_exposure = [0.0f32; 3];
|
||||
for (l, slot) in log_exposure.iter_mut().enumerate() {
|
||||
let m = self.exposure_matrix[l];
|
||||
let e = m[0] * rgb[0] + m[1] * rgb[1] + m[2] * rgb[2];
|
||||
*slot = (e.max(0.0) + 1e-10).log10();
|
||||
}
|
||||
self.sample_lut(self.sample_curves(log_exposure))
|
||||
}
|
||||
|
||||
fn sample_curves(&self, log_exposure: [f32; 3]) -> [f32; 3] {
|
||||
let last = self.curves.len() - 1;
|
||||
let span = self.curve_log_max - self.curve_log_min;
|
||||
let mut out = [0.0f32; 3];
|
||||
for (c, slot) in out.iter_mut().enumerate() {
|
||||
let t = ((log_exposure[c] - self.curve_log_min) / span).clamp(0.0, 1.0) * last as f32;
|
||||
let i = (t.floor() as usize).min(last - 1);
|
||||
let f = t - i as f32;
|
||||
*slot = self.curves[i][c] * (1.0 - f) + self.curves[i + 1][c] * f;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn sample_lut(&self, density: [f32; 3]) -> [f32; 3] {
|
||||
let n = self.lut_size;
|
||||
let mut base = [0usize; 3];
|
||||
let mut frac = [0f32; 3];
|
||||
for c in 0..3 {
|
||||
let t = (density[c] / self.density_max).clamp(0.0, 1.0) * (n - 1) as f32;
|
||||
base[c] = (t.floor() as usize).min(n - 2);
|
||||
frac[c] = t - base[c] as f32;
|
||||
}
|
||||
let mut out = [0.0f32; 3];
|
||||
for dx in 0..2 {
|
||||
for dy in 0..2 {
|
||||
for dz in 0..2 {
|
||||
let w = if dx == 0 { 1.0 - frac[0] } else { frac[0] }
|
||||
* if dy == 0 { 1.0 - frac[1] } else { frac[1] }
|
||||
* if dz == 0 { 1.0 - frac[2] } else { frac[2] };
|
||||
let e = self.lut[((base[0] + dx) * n + base[1] + dy) * n + base[2] + dz];
|
||||
for c in 0..3 {
|
||||
out[c] += w * e[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Linear sRGB to the three layers' exposure, mid-grey normalised.
|
||||
///
|
||||
/// Normalised on the *green* layer alone, one shared scalar for all three.
|
||||
/// Doing it per layer is tempting and wrong: it would silently flatten the
|
||||
/// film's own channel balance, which is a large part of what distinguishes one
|
||||
/// stock from another. Where the balance genuinely has to come out — printing a
|
||||
/// negative — it is [`print_balance`]'s job, which is also where it belongs
|
||||
/// physically.
|
||||
pub fn exposure_matrix(film: &Profile) -> [[f32; 3]; 3] {
|
||||
let reference = illuminant(&film.reference_illuminant);
|
||||
let sensitivity = film.sensitivity();
|
||||
|
||||
let mut m = [[0.0f32; 3]; 3];
|
||||
let mut mid_grey = [0.0f32; 3];
|
||||
for i in 0..SPECTRUM {
|
||||
for layer in 0..3 {
|
||||
let s = sensitivity[i][layer] * reference[i];
|
||||
mid_grey[layer] += s * MID_GREY;
|
||||
for channel in 0..3 {
|
||||
m[layer][channel] += s * SRGB_BASIS[i][channel];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let scale = 1.0 / mid_grey[1];
|
||||
for row in &mut m {
|
||||
for v in row.iter_mut() {
|
||||
*v *= scale;
|
||||
}
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
/// The enlarger head's filtration, solved rather than dialled.
|
||||
///
|
||||
/// Returns the per-layer log exposure offsets that make a mid-grey scene print
|
||||
/// as a neutral mid-grey. This is also where a colour negative's orange mask
|
||||
/// goes: the mask is a fixed density, so balancing mid-grey to neutral cancels
|
||||
/// it — which is why a printed negative looks like a photograph while a scanned
|
||||
/// one looks orange.
|
||||
fn print_balance(film: &Profile, paper: &Profile, exposure_ev: f32, print_exposure_ev: f32) -> [f32; 3] {
|
||||
let matrix = exposure_matrix(film);
|
||||
let scene = MID_GREY * 2f32.powf(exposure_ev);
|
||||
let mut log_exposure = [0.0f32; 3];
|
||||
for (l, slot) in log_exposure.iter_mut().enumerate() {
|
||||
let m = matrix[l];
|
||||
*slot = ((m[0] + m[1] + m[2]) * scene + 1e-10).log10();
|
||||
}
|
||||
let mid_raw = paper_exposure(film, paper, film.density_at(log_exposure));
|
||||
|
||||
// Where on the paper's curve mid-grey belongs: the density that reflects
|
||||
// 18%, read off the average of the three curves. Averaged because the
|
||||
// point of the balance is that the three end up at the same place.
|
||||
let target_density = -MID_GREY.log10() - mean(&paper.base_density);
|
||||
let target = invert_mean_curve(paper, target_density);
|
||||
|
||||
let mut offsets = [0.0f32; 3];
|
||||
for (l, slot) in offsets.iter_mut().enumerate() {
|
||||
*slot = target - (mid_raw[l] + 1e-10).log10() + print_exposure_ev * 2f32.log10();
|
||||
}
|
||||
offsets
|
||||
}
|
||||
|
||||
/// The paper's three layer exposures, printing through a negative at these
|
||||
/// densities.
|
||||
///
|
||||
/// The one genuinely spectral step left in the chain — the negative's
|
||||
/// transmittance is `10^-D`, so this is not a matrix and cannot be made into
|
||||
/// one. It takes exactly three numbers in, which is what lets the whole
|
||||
/// negative-and-print chain still bake into a 3D lookup.
|
||||
fn paper_exposure(film: &Profile, paper: &Profile, density: [f32; 3]) -> [f32; 3] {
|
||||
let enlarger = illuminant(&paper.reference_illuminant);
|
||||
let sensitivity = paper.sensitivity();
|
||||
let transmittance = film.transmittance(density);
|
||||
|
||||
let mut raw = [0.0f32; 3];
|
||||
for i in 0..SPECTRUM {
|
||||
let light = transmittance[i] * enlarger[i];
|
||||
for layer in 0..3 {
|
||||
raw[layer] += light * sensitivity[i][layer];
|
||||
}
|
||||
}
|
||||
raw
|
||||
}
|
||||
|
||||
/// Bake a recipe into the tables a shader runs.
|
||||
pub fn bake(recipe: &Recipe) -> Baked {
|
||||
let film = recipe.film;
|
||||
let mut matrix = exposure_matrix(film);
|
||||
// Camera exposure rides in the matrix rather than in the shader: it is a
|
||||
// scalar on a linear quantity, and folding it in here costs nothing and
|
||||
// keeps the per-pixel work identical whether or not it has been moved.
|
||||
let gain = 2f32.powf(recipe.exposure_ev);
|
||||
for row in &mut matrix {
|
||||
for v in row.iter_mut() {
|
||||
*v *= gain;
|
||||
}
|
||||
}
|
||||
|
||||
let curves = film.density_curves.clone();
|
||||
let density_max = film.max_density().max(1e-3);
|
||||
|
||||
let viewing = match recipe.print {
|
||||
Some(paper) => Viewing::new(&paper.viewing_illuminant),
|
||||
None => Viewing::new(&film.viewing_illuminant),
|
||||
};
|
||||
let balance = recipe
|
||||
.print
|
||||
.map(|paper| print_balance(film, paper, recipe.exposure_ev, recipe.print_exposure_ev));
|
||||
|
||||
let n = LUT_SIZE;
|
||||
let mut lut = Vec::with_capacity(n * n * n);
|
||||
for x in 0..n {
|
||||
for y in 0..n {
|
||||
for z in 0..n {
|
||||
let density = [
|
||||
density_max * x as f32 / (n - 1) as f32,
|
||||
density_max * y as f32 / (n - 1) as f32,
|
||||
density_max * z as f32 / (n - 1) as f32,
|
||||
];
|
||||
lut.push(match recipe.print.zip(balance) {
|
||||
Some((paper, offsets)) => {
|
||||
let raw = paper_exposure(film, paper, density);
|
||||
let mut log_exposure = [0.0f32; 3];
|
||||
for (l, slot) in log_exposure.iter_mut().enumerate() {
|
||||
*slot = (raw[l] + 1e-10).log10() + offsets[l];
|
||||
}
|
||||
let paper_density = paper.density_at(log_exposure);
|
||||
viewing.to_srgb(&paper.transmittance(paper_density))
|
||||
}
|
||||
None => viewing.to_srgb(&film.transmittance(density)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Baked {
|
||||
exposure_matrix: matrix,
|
||||
curves,
|
||||
curve_log_min: film.log_exposure_min,
|
||||
curve_log_max: film.log_exposure_max,
|
||||
lut,
|
||||
density_max,
|
||||
lut_size: n,
|
||||
}
|
||||
}
|
||||
|
||||
fn mean(s: &Spectrum) -> f32 {
|
||||
s.iter().sum::<f32>() / SPECTRUM as f32
|
||||
}
|
||||
|
||||
/// The log exposure at which the paper's average curve reaches `density`.
|
||||
fn invert_mean_curve(paper: &Profile, density: f32) -> f32 {
|
||||
let curves = &paper.density_curves;
|
||||
let last = curves.len() - 1;
|
||||
let span = paper.log_exposure_max - paper.log_exposure_min;
|
||||
let at = |i: usize| (curves[i][0] + curves[i][1] + curves[i][2]) / 3.0;
|
||||
let log_at = |i: usize| paper.log_exposure_min + span * i as f32 / last as f32;
|
||||
|
||||
// A paper is negative-working, so its mean curve rises. Walk it rather
|
||||
// than binary-search: 256 samples is nothing, and a linear scan is correct
|
||||
// even where the curve is flat, which a bisection is not.
|
||||
let ascending = at(last) >= at(0);
|
||||
for i in 0..last {
|
||||
let (lo, hi) = (at(i), at(i + 1));
|
||||
let brackets = if ascending { lo <= density && density <= hi } else { hi <= density && density <= lo };
|
||||
if brackets && (hi - lo).abs() > f32::EPSILON {
|
||||
let f = (density - lo) / (hi - lo);
|
||||
return log_at(i) + (log_at(i + 1) - log_at(i)) * f;
|
||||
}
|
||||
}
|
||||
// Off the end of the curve: the nearest end is the honest answer, and it
|
||||
// keeps a badly-scaled contributed profile from producing a NaN that would
|
||||
// propagate silently through the whole LUT.
|
||||
if (density <= at(0)) == ascending { paper.log_exposure_min } else { paper.log_exposure_max }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn profile(yaml: &str) -> Profile {
|
||||
Profile::parse(yaml).unwrap()
|
||||
}
|
||||
|
||||
fn portra() -> Profile {
|
||||
profile(include_str!("../profiles/kodak_portra_400.yaml"))
|
||||
}
|
||||
|
||||
fn endura() -> Profile {
|
||||
profile(include_str!("../profiles/kodak_portra_endura.yaml"))
|
||||
}
|
||||
|
||||
fn kodachrome() -> Profile {
|
||||
profile(include_str!("../profiles/kodak_kodachrome_64.yaml"))
|
||||
}
|
||||
|
||||
fn spread(rgb: [f32; 3]) -> f32 {
|
||||
rgb.iter().cloned().fold(f32::MIN, f32::max) - rgb.iter().cloned().fold(f32::MAX, f32::min)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_exposure_matrix_is_diagonally_dominant() {
|
||||
// Each layer must respond most strongly to its own primary. A matrix
|
||||
// that failed this would mean the sensitivity table had been pasted in
|
||||
// the wrong channel order, which produces a picture that renders
|
||||
// perfectly and has its colours swapped.
|
||||
for film in [portra(), kodachrome()] {
|
||||
let m = exposure_matrix(&film);
|
||||
for layer in 0..3 {
|
||||
for channel in 0..3 {
|
||||
if channel != layer {
|
||||
assert!(
|
||||
m[layer][layer] > m[layer][channel] * 3.0,
|
||||
"{}: layer {layer} responds to channel {channel} too strongly: {m:?}",
|
||||
film.stock
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reversal_stock_renders_a_positive() {
|
||||
let film = kodachrome();
|
||||
let baked = bake(&Recipe::new(&film, None));
|
||||
let shadow = baked.apply([0.02; 3]);
|
||||
let mid = baked.apply([MID_GREY; 3]);
|
||||
let highlight = baked.apply([0.8; 3]);
|
||||
assert!(shadow[1] < mid[1] && mid[1] < highlight[1], "{shadow:?} {mid:?} {highlight:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_scanned_negative_is_inverted_and_orange() {
|
||||
// Not a defect: it is what a negative looks like, and rendering it any
|
||||
// other way would mean the print stage was silently applied.
|
||||
let film = portra();
|
||||
let baked = bake(&Recipe::new(&film, None));
|
||||
let shadow = baked.apply([0.02; 3]);
|
||||
let highlight = baked.apply([0.8; 3]);
|
||||
assert!(shadow[1] > highlight[1], "not inverted: {shadow:?} -> {highlight:?}");
|
||||
let mid = baked.apply([MID_GREY; 3]);
|
||||
assert!(mid[0] > mid[2] * 4.0, "no orange mask: {mid:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printing_a_negative_restores_the_picture() {
|
||||
// The property the whole print stage exists for: through the paper,
|
||||
// the same negative is the right way up and neutral again.
|
||||
let film = portra();
|
||||
let paper = endura();
|
||||
let baked = bake(&Recipe::new(&film, Some(&paper)));
|
||||
|
||||
let shadow = baked.apply([0.02; 3]);
|
||||
let mid = baked.apply([MID_GREY; 3]);
|
||||
let highlight = baked.apply([0.8; 3]);
|
||||
assert!(
|
||||
shadow[1] < mid[1] && mid[1] < highlight[1],
|
||||
"print is not a positive: {shadow:?} {mid:?} {highlight:?}"
|
||||
);
|
||||
for grey in [shadow, mid, highlight] {
|
||||
assert!(spread(grey) < 0.06, "print of a neutral is not neutral: {grey:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exposure_moves_the_print_the_way_it_moves_a_photograph() {
|
||||
let film = portra();
|
||||
let paper = endura();
|
||||
let brighter = bake(&Recipe { exposure_ev: 1.0, ..Recipe::new(&film, Some(&paper)) });
|
||||
let base = bake(&Recipe::new(&film, Some(&paper)));
|
||||
assert!(brighter.apply([MID_GREY; 3])[1] > base.apply([MID_GREY; 3])[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_lut_holds_its_error_under_a_code_value() {
|
||||
// The claim LUT_SIZE is chosen on. Compared against the same chain
|
||||
// evaluated exactly, so it measures interpolation error and nothing
|
||||
// else.
|
||||
let film = kodachrome();
|
||||
let baked = bake(&Recipe::new(&film, None));
|
||||
let viewing = Viewing::new(&film.viewing_illuminant);
|
||||
|
||||
let mut worst = 0.0f32;
|
||||
for i in 0..40 {
|
||||
for j in 0..40 {
|
||||
let rgb = [i as f32 / 39.0, j as f32 / 39.0, ((i + j) % 40) as f32 / 39.0];
|
||||
let mut log_exposure = [0.0f32; 3];
|
||||
for (l, slot) in log_exposure.iter_mut().enumerate() {
|
||||
let m = baked.exposure_matrix[l];
|
||||
*slot = ((m[0] * rgb[0] + m[1] * rgb[1] + m[2] * rgb[2]).max(0.0) + 1e-10).log10();
|
||||
}
|
||||
let exact = viewing.to_srgb(&film.transmittance(film.density_at(log_exposure)));
|
||||
let approx = baked.apply(rgb);
|
||||
for c in 0..3 {
|
||||
worst = worst.max((exact[c] - approx[c]).abs());
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(worst < 1.0 / 255.0, "worst LUT error {worst} exceeds one code value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_lut_is_the_size_it_says_it_is() {
|
||||
let film = kodachrome();
|
||||
let baked = bake(&Recipe::new(&film, None));
|
||||
assert_eq!(baked.lut.len(), LUT_SIZE * LUT_SIZE * LUT_SIZE);
|
||||
assert_eq!(baked.curves.len(), CURVE_SAMPLES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//! TRACES: FR-DEV-3f
|
||||
//! Spectral film simulation — what a stock would have done with this light.
|
||||
//!
|
||||
//! # What this is, and what it is not
|
||||
//!
|
||||
//! Not a look-up table someone graded by eye. Each stock here is its
|
||||
//! manufacturer's own measurements — spectral sensitivity, characteristic
|
||||
//! curves, dye densities — run through the physics: light exposes three
|
||||
//! emulsion layers, the layers develop to densities, the densities are dyes
|
||||
//! that absorb, and what is left is what reaches the eye. A colour negative
|
||||
//! comes out orange and upside down because that is what a colour negative is;
|
||||
//! it becomes a photograph when [`bake::Recipe`] prints it on paper.
|
||||
//!
|
||||
//! What that buys over a LUT is that the *parameters are physical*. Exposing a
|
||||
//! stop over moves the picture along the film's real characteristic curve,
|
||||
//! shoulder and all, rather than scaling a number someone baked at one
|
||||
//! exposure. And the data cost is the other way round from a LUT collection: a
|
||||
//! stock is about 17 kB of published measurements, where one HaldCLUT is 800 kB
|
||||
//! of one person's grade.
|
||||
//!
|
||||
//! # The shape of the crate
|
||||
//!
|
||||
//! - [`profile`] — a stock, as measured. Data, contributable as a file.
|
||||
//! - [`spectrum`] — the fixed colour science: observer, illuminants, basis.
|
||||
//! - [`bake`] — the reduction to three tables a shader can run.
|
||||
//!
|
||||
//! No wgpu dependency, deliberately: what comes out is plain `f32` with a
|
||||
//! documented layout, and every property worth asserting about the model is
|
||||
//! asserted on the CPU.
|
||||
//!
|
||||
//! # Provenance
|
||||
//!
|
||||
//! The shipped profiles are converted from **spektrafilm** by Andrea Volpato
|
||||
//! (<https://github.com/andreavolpato/spektrafilm>), licensed CC BY-SA 4.0 and
|
||||
//! modified for DarkRoom — see `profiles/LICENSE-PROFILES.txt` and
|
||||
//! `profiles/CHANGELOG.txt`. The conversion is reproducible from
|
||||
//! `tools/film-profiles/convert.py` rather than pasted, so what changed is
|
||||
//! auditable. The sRGB reflectance basis is Mallett & Yuksel (2019).
|
||||
|
||||
pub mod bake;
|
||||
pub mod profile;
|
||||
pub mod spectrum;
|
||||
pub mod tables;
|
||||
|
||||
pub use bake::{bake, Baked, Recipe};
|
||||
pub use profile::{Kind, Profile, Support};
|
||||
|
||||
/// The stocks compiled in as a floor.
|
||||
///
|
||||
/// A floor rather than the whole story: like `dr_decode::base_curve`, the point
|
||||
/// is that a stock is a file. These are here so a fresh install has something
|
||||
/// to offer, not because the list is meant to stay short.
|
||||
static BUILT_IN: &[(&str, &str)] = &[
|
||||
("kodak_portra_400", include_str!("../profiles/kodak_portra_400.yaml")),
|
||||
("kodak_kodachrome_64", include_str!("../profiles/kodak_kodachrome_64.yaml")),
|
||||
("kodak_portra_endura", include_str!("../profiles/kodak_portra_endura.yaml")),
|
||||
];
|
||||
|
||||
/// Every stock that is compiled in, parsed on first use.
|
||||
pub fn built_in() -> &'static [Profile] {
|
||||
use std::sync::OnceLock;
|
||||
static PARSED: OnceLock<Vec<Profile>> = OnceLock::new();
|
||||
PARSED.get_or_init(|| {
|
||||
BUILT_IN
|
||||
.iter()
|
||||
.filter_map(|(stock, yaml)| match Profile::parse(yaml) {
|
||||
Ok(p) => Some(p),
|
||||
Err(e) => {
|
||||
// A compiled-in profile that does not parse is a build
|
||||
// mistake, but refusing to start over one would take the
|
||||
// whole application down for a stock nobody asked for.
|
||||
log::error!("built-in film profile {stock} is malformed: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Find a compiled-in stock by its identifier.
|
||||
pub fn find(stock: &str) -> Option<&'static Profile> {
|
||||
built_in().iter().find(|p| p.stock == stock)
|
||||
}
|
||||
|
||||
/// The paper a stock should be printed on, if it names one and we have it.
|
||||
///
|
||||
/// A reversal stock names none, and needs none: it is the picture already.
|
||||
pub fn default_print(film: &Profile) -> Option<&'static Profile> {
|
||||
film.target_print.as_deref().and_then(find)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_built_in_profile_parses() {
|
||||
assert_eq!(built_in().len(), BUILT_IN.len(), "a built-in profile failed to parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negative_finds_the_paper_it_names() {
|
||||
let portra = find("kodak_portra_400").unwrap();
|
||||
assert_eq!(default_print(portra).unwrap().stock, "kodak_portra_endura");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reversal_stock_names_no_paper() {
|
||||
let k64 = find("kodak_kodachrome_64").unwrap();
|
||||
assert_eq!(k64.kind, Kind::Positive);
|
||||
assert!(default_print(k64).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
//! A film stock, as measured.
|
||||
//!
|
||||
//! # Why it is data
|
||||
//!
|
||||
//! The same argument `dr_decode::base_curve` makes for camera bodies, and for
|
||||
//! the same requirement: under the GPLv3 a stock should be contributable
|
||||
//! without a release. A profile is three tables and a handful of facts, all of
|
||||
//! them published in the manufacturer's datasheet, so adding a stock is adding
|
||||
//! a file — not a code change, not a shader, and not a new operation.
|
||||
//!
|
||||
//! # What a stock actually is
|
||||
//!
|
||||
//! Three numbers per wavelength, three times over:
|
||||
//!
|
||||
//! - **Spectral sensitivity** — how strongly each emulsion layer responds to
|
||||
//! light of each wavelength. Decides what the film *sees*.
|
||||
//! - **The characteristic curve** — density against log exposure, per layer.
|
||||
//! Decides the film's contrast, its latitude, and where it clips.
|
||||
//! - **Dye density** — the spectral absorption each layer's developed dye
|
||||
//! contributes. Decides what the film *looks* like.
|
||||
//!
|
||||
//! A print paper is the same three things; `support` is the only field that
|
||||
//! distinguishes it, and it exists so that a UI can offer papers separately
|
||||
//! rather than because the renderer treats them differently.
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::spectrum::Spectrum;
|
||||
use crate::tables::SPECTRUM;
|
||||
|
||||
/// How many samples a characteristic curve carries.
|
||||
///
|
||||
/// Enough that the toe and the shoulder survive linear interpolation, which is
|
||||
/// the resolution that matters: the 3D LUT downstream is deliberately coarse
|
||||
/// because the dye mixing is smooth, and it can only be coarse if the curve's
|
||||
/// shape is carried exactly here rather than folded into it.
|
||||
pub const CURVE_SAMPLES: usize = 256;
|
||||
|
||||
/// Which way the material works.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Kind {
|
||||
/// More light, more density: a camera negative, and also every print paper.
|
||||
Negative,
|
||||
/// More light, *less* density: a reversal stock, viewed as it comes.
|
||||
Positive,
|
||||
}
|
||||
|
||||
/// What the emulsion is coated on.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Support {
|
||||
Film,
|
||||
Paper,
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-3f
|
||||
/// One stock's measured response.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Profile {
|
||||
/// The upstream profile's version, so a mismatch is diagnosable.
|
||||
#[serde(default)]
|
||||
pub version: String,
|
||||
/// The identifier a recipe names, e.g. `kodak_portra_400`.
|
||||
pub stock: String,
|
||||
/// The stock's display name. Not a localisation key: "Kodak Portra 400" is
|
||||
/// a product name and does not translate.
|
||||
pub name: String,
|
||||
pub kind: Kind,
|
||||
pub support: Support,
|
||||
/// The light the sensitivity data was measured under.
|
||||
pub reference_illuminant: String,
|
||||
/// The light the developed result is meant to be looked at under.
|
||||
pub viewing_illuminant: String,
|
||||
/// For a negative, the paper it was designed to be printed on.
|
||||
#[serde(default)]
|
||||
pub target_print: Option<String>,
|
||||
|
||||
/// log₁₀ sensitivity per layer. `-9` marks a wavelength the datasheet does
|
||||
/// not cover, which the renderer reads as blindness rather than as data.
|
||||
#[serde(deserialize_with = "spectral_triples")]
|
||||
pub log_sensitivity: Vec<[f32; 3]>,
|
||||
/// Spectral density contributed by each layer's dye at unit density.
|
||||
#[serde(deserialize_with = "spectral_triples")]
|
||||
pub dye_density: Vec<[f32; 3]>,
|
||||
/// The support's own density — film base, plus a colour negative's orange
|
||||
/// mask.
|
||||
#[serde(deserialize_with = "spectral_scalars")]
|
||||
pub base_density: Spectrum,
|
||||
|
||||
pub log_exposure_min: f32,
|
||||
pub log_exposure_max: f32,
|
||||
/// Density against log exposure, per layer, uniformly sampled across
|
||||
/// `[log_exposure_min, log_exposure_max]`.
|
||||
pub density_curves: Vec<[f32; 3]>,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
/// Read a profile from YAML.
|
||||
pub fn parse(yaml: &str) -> Result<Self, String> {
|
||||
let profile: Profile = serde_norway::from_str(yaml).map_err(|e| e.to_string())?;
|
||||
profile.validate()?;
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
// Checked rather than assumed because a profile is a *contributed*
|
||||
// file. A short table would otherwise be caught as an index panic
|
||||
// somewhere in the baker, which names neither the file nor the field.
|
||||
if self.log_sensitivity.len() != SPECTRUM {
|
||||
return Err(format!(
|
||||
"{}: log_sensitivity has {} rows, expected {SPECTRUM}",
|
||||
self.stock,
|
||||
self.log_sensitivity.len()
|
||||
));
|
||||
}
|
||||
if self.dye_density.len() != SPECTRUM {
|
||||
return Err(format!(
|
||||
"{}: dye_density has {} rows, expected {SPECTRUM}",
|
||||
self.stock,
|
||||
self.dye_density.len()
|
||||
));
|
||||
}
|
||||
if self.density_curves.len() < 2 {
|
||||
return Err(format!("{}: density_curves needs at least two samples", self.stock));
|
||||
}
|
||||
if self.log_exposure_max <= self.log_exposure_min {
|
||||
return Err(format!(
|
||||
"{}: log exposure range [{}, {}] is empty or inverted",
|
||||
self.stock, self.log_exposure_min, self.log_exposure_max
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Linear spectral sensitivity, undoing the log the datasheet quotes.
|
||||
pub fn sensitivity(&self) -> Vec<[f32; 3]> {
|
||||
self.log_sensitivity
|
||||
.iter()
|
||||
.map(|row| row.map(|v| if v <= -8.0 { 0.0 } else { 10f32.powf(v) }))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The density each layer reaches at a given log exposure.
|
||||
///
|
||||
/// Clamped at both ends rather than extrapolated: past the shoulder a real
|
||||
/// emulsion stops responding, and a linear extrapolation of the last two
|
||||
/// samples would keep climbing and turn a blown highlight into a colour.
|
||||
pub fn density_at(&self, log_exposure: [f32; 3]) -> [f32; 3] {
|
||||
let last = self.density_curves.len() - 1;
|
||||
let span = self.log_exposure_max - self.log_exposure_min;
|
||||
let mut out = [0.0f32; 3];
|
||||
for (c, slot) in out.iter_mut().enumerate() {
|
||||
let t = ((log_exposure[c] - self.log_exposure_min) / span).clamp(0.0, 1.0)
|
||||
* last as f32;
|
||||
let i = (t.floor() as usize).min(last - 1);
|
||||
let f = t - i as f32;
|
||||
*slot = self.density_curves[i][c] * (1.0 - f) + self.density_curves[i + 1][c] * f;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The largest density any layer reaches. The 3D LUT's upper bound.
|
||||
pub fn max_density(&self) -> f32 {
|
||||
self.density_curves
|
||||
.iter()
|
||||
.flat_map(|row| row.iter())
|
||||
.fold(0.0f32, |a, &b| a.max(b))
|
||||
}
|
||||
|
||||
/// Spectral transmittance of the developed material at these densities.
|
||||
///
|
||||
/// Beer–Lambert: densities add in log space, so the layers' dyes and the
|
||||
/// support's own base compose by summing before the exponent.
|
||||
pub fn transmittance(&self, density: [f32; 3]) -> Spectrum {
|
||||
let mut out = [0.0f32; SPECTRUM];
|
||||
for (i, slot) in out.iter_mut().enumerate() {
|
||||
let dye = &self.dye_density[i];
|
||||
let total = density[0] * dye[0]
|
||||
+ density[1] * dye[1]
|
||||
+ density[2] * dye[2]
|
||||
+ self.base_density[i];
|
||||
*slot = 10f32.powf(-total);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
fn spectral_triples<'de, D>(d: D) -> Result<Vec<[f32; 3]>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Vec::<[f32; 3]>::deserialize(d)
|
||||
}
|
||||
|
||||
fn spectral_scalars<'de, D>(d: D) -> Result<Spectrum, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error as _;
|
||||
let v = Vec::<f32>::deserialize(d)?;
|
||||
v.try_into()
|
||||
.map_err(|v: Vec<f32>| D::Error::custom(format!("expected {SPECTRUM} samples, got {}", v.len())))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn portra() -> Profile {
|
||||
Profile::parse(include_str!("../profiles/kodak_portra_400.yaml")).unwrap()
|
||||
}
|
||||
|
||||
fn kodachrome() -> Profile {
|
||||
Profile::parse(include_str!("../profiles/kodak_kodachrome_64.yaml")).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shipped_profiles_load() {
|
||||
for p in [portra(), kodachrome()] {
|
||||
assert_eq!(p.log_sensitivity.len(), SPECTRUM);
|
||||
assert_eq!(p.density_curves.len(), CURVE_SAMPLES);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negative_gains_density_with_exposure_and_a_positive_loses_it() {
|
||||
// The one property that decides whether the picture comes out or comes
|
||||
// out inverted, and it is a property of the *data*, not of the code
|
||||
// that reads it — so it is asserted against the shipped files.
|
||||
let shadow = [-2.0; 3];
|
||||
let highlight = [2.0; 3];
|
||||
assert!(portra().density_at(highlight)[1] > portra().density_at(shadow)[1]);
|
||||
assert!(kodachrome().density_at(highlight)[1] < kodachrome().density_at(shadow)[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_curve_is_clamped_rather_than_extrapolated() {
|
||||
// Past the shoulder a real emulsion stops responding. Extrapolating
|
||||
// would keep climbing, which turns a blown highlight into a colour
|
||||
// cast that gets stronger the more it is overexposed.
|
||||
let p = portra();
|
||||
let shoulder = p.density_at([p.log_exposure_max; 3]);
|
||||
let far_past = p.density_at([p.log_exposure_max + 40.0; 3]);
|
||||
assert_eq!(shoulder, far_past);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_undeveloped_frame_transmits_its_base_and_nothing_else() {
|
||||
let p = portra();
|
||||
let clear = p.transmittance([0.0; 3]);
|
||||
for (i, t) in clear.iter().enumerate() {
|
||||
assert!((t - 10f32.powf(-p.base_density[i])).abs() < 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_orange_mask_is_in_the_data() {
|
||||
// Portra's base is a real orange mask: it must absorb blue far more
|
||||
// than red. If this fails the base density has been dropped or
|
||||
// zeroed, and the print balance downstream would have nothing to
|
||||
// correct — which looks like a working picture, only wrong.
|
||||
let p = portra();
|
||||
let blue = p.base_density[16]; // 460nm
|
||||
let red = p.base_density[64]; // 700nm
|
||||
assert!(blue > red + 0.2, "base density blue {blue} red {red} is not a mask");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_blind_wavelength_reads_as_zero_sensitivity() {
|
||||
let p = kodachrome();
|
||||
let sens = p.sensitivity();
|
||||
for (i, row) in sens.iter().enumerate() {
|
||||
for (c, v) in row.iter().enumerate() {
|
||||
assert!(v.is_finite() && *v >= 0.0, "sensitivity[{i}][{c}] = {v}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
//! The fixed colour science: spectra in, tristimulus out.
|
||||
//!
|
||||
//! Everything here is per-installation rather than per-stock — one observer,
|
||||
//! a handful of illuminants, one basis — and all of it is small enough to
|
||||
//! compile in. See [`crate::tables`] for the numbers themselves.
|
||||
|
||||
use crate::tables::{ILLUMINANT_D50, ILLUMINANT_D55, ILLUMINANT_D65, OBSERVER, SPECTRUM};
|
||||
|
||||
/// A spectral distribution on the crate's fixed 380–780 nm, 5 nm grid.
|
||||
pub type Spectrum = [f32; SPECTRUM];
|
||||
|
||||
/// Linear sRGB primaries from CIE XYZ, for a D65 white.
|
||||
///
|
||||
/// The adaptation to whatever white the picture is actually being viewed under
|
||||
/// happens before this is applied — see [`Viewing::to_srgb`].
|
||||
const XYZ_TO_SRGB: [[f32; 3]; 3] = [
|
||||
[3.2404542, -1.5371385, -0.4985314],
|
||||
[-0.9692660, 1.8760108, 0.0415560],
|
||||
[0.0556434, -0.2040259, 1.0572252],
|
||||
];
|
||||
|
||||
/// sRGB's own white, which is what [`XYZ_TO_SRGB`] expects to be handed.
|
||||
const D65_WHITE: [f32; 3] = [0.9504559, 1.0, 1.0890578];
|
||||
|
||||
/// Bradford cone response, and its inverse.
|
||||
///
|
||||
/// A von Kries adaptation done in XYZ — dividing each channel by the white —
|
||||
/// is the obvious thing and it is wrong: XYZ axes are not cone responses, so
|
||||
/// the result drifts in hue. Bradford is the transform that makes "the same
|
||||
/// colour, seen under a different light" mean what a viewer means by it, and
|
||||
/// it matters here because a print is specified under D50 while sRGB is D65.
|
||||
const BRADFORD: [[f32; 3]; 3] = [
|
||||
[0.8951, 0.2664, -0.1614],
|
||||
[-0.7502, 1.7135, 0.0367],
|
||||
[0.0389, -0.0685, 1.0296],
|
||||
];
|
||||
|
||||
const BRADFORD_INV: [[f32; 3]; 3] = [
|
||||
[0.9869929, -0.1470543, 0.1599627],
|
||||
[0.4323053, 0.5183603, 0.0492912],
|
||||
[-0.0085287, 0.0400428, 0.9684867],
|
||||
];
|
||||
|
||||
/// Named illuminants a profile may ask for.
|
||||
///
|
||||
/// The daylight ones are tabulated because the CIE defines them that way. The
|
||||
/// tungsten ones are computed from Planck's law, which costs no data at all,
|
||||
/// and the approximation is unusually safe here: an enlarger's lamp colour is
|
||||
/// cancelled by the filtration solved in [`crate::bake::print_balance`], the
|
||||
/// same way a darkroom worker dials it out on the colour head.
|
||||
pub fn illuminant(name: &str) -> Spectrum {
|
||||
match name {
|
||||
"D50" => ILLUMINANT_D50,
|
||||
"D55" => ILLUMINANT_D55,
|
||||
"D65" => ILLUMINANT_D65,
|
||||
// The tungsten-halogen enlarger source, with and without the heat
|
||||
// filter that a real head carries. Both land on the same blackbody:
|
||||
// the filter's effect is a colour shift, and a colour shift ahead of
|
||||
// the balance step is by construction invisible.
|
||||
"TH-KG3" | "TH-KG3-L" | "T" => blackbody(3400.0),
|
||||
other => {
|
||||
if let Some(kelvin) = other.strip_prefix("BB").and_then(|k| k.parse::<f32>().ok()) {
|
||||
return blackbody(kelvin);
|
||||
}
|
||||
log::warn!("unknown illuminant {other:?}, falling back to D55");
|
||||
ILLUMINANT_D55
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Planck's law on the grid, normalised to unit mean.
|
||||
///
|
||||
/// Normalised because only the shape matters: absolute level is set by the
|
||||
/// exposure controls, and leaving it in would make the choice of units a
|
||||
/// visible parameter.
|
||||
pub fn blackbody(kelvin: f32) -> Spectrum {
|
||||
const H: f64 = 6.626_070_15e-34;
|
||||
const C: f64 = 2.997_924_58e8;
|
||||
const KB: f64 = 1.380_649e-23;
|
||||
|
||||
let mut out = [0.0f32; SPECTRUM];
|
||||
let mut total = 0.0f64;
|
||||
for (i, slot) in out.iter_mut().enumerate() {
|
||||
let lambda = f64::from(crate::tables::LAMBDA_MIN + crate::tables::LAMBDA_STEP * i as f32)
|
||||
* 1e-9;
|
||||
let radiance = (2.0 * H * C * C)
|
||||
/ (lambda.powi(5) * ((H * C / (lambda * KB * f64::from(kelvin))).exp() - 1.0));
|
||||
*slot = radiance as f32;
|
||||
total += radiance;
|
||||
}
|
||||
let mean = (total / SPECTRUM as f64) as f32;
|
||||
for slot in &mut out {
|
||||
*slot /= mean;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// How a developed image is looked at: an illuminant, and the adaptation it
|
||||
/// implies.
|
||||
///
|
||||
/// Built once per bake rather than per sample, because the white point and the
|
||||
/// adaptation matrix depend only on the illuminant and computing them inside
|
||||
/// the LUT loop would be the same work 32 768 times.
|
||||
pub struct Viewing {
|
||||
illuminant: Spectrum,
|
||||
/// The illuminant's own Y, which normalises the integral so that a clear
|
||||
/// frame comes out at exactly 1.0 rather than at whatever the tabulated
|
||||
/// units happen to give.
|
||||
normalisation: f32,
|
||||
/// XYZ under this illuminant to linear sRGB, adaptation folded in.
|
||||
matrix: [[f32; 3]; 3],
|
||||
}
|
||||
|
||||
impl Viewing {
|
||||
pub fn new(illuminant_name: &str) -> Self {
|
||||
let illuminant = illuminant(illuminant_name);
|
||||
|
||||
let mut white = [0.0f32; 3];
|
||||
let mut normalisation = 0.0f32;
|
||||
for i in 0..SPECTRUM {
|
||||
normalisation += illuminant[i] * OBSERVER[i][1];
|
||||
for c in 0..3 {
|
||||
white[c] += illuminant[i] * OBSERVER[i][c];
|
||||
}
|
||||
}
|
||||
for c in &mut white {
|
||||
*c /= normalisation;
|
||||
}
|
||||
|
||||
Self {
|
||||
illuminant,
|
||||
normalisation,
|
||||
matrix: mat3_mul(XYZ_TO_SRGB, bradford_adaptation(white, D65_WHITE)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Integrate a transmittance (or reflectance) against the illuminant and
|
||||
/// the observer, and convert to linear sRGB.
|
||||
pub fn to_srgb(&self, transmittance: &Spectrum) -> [f32; 3] {
|
||||
let mut xyz = [0.0f32; 3];
|
||||
for i in 0..SPECTRUM {
|
||||
let light = transmittance[i] * self.illuminant[i];
|
||||
for c in 0..3 {
|
||||
xyz[c] += light * OBSERVER[i][c];
|
||||
}
|
||||
}
|
||||
for c in &mut xyz {
|
||||
*c /= self.normalisation;
|
||||
}
|
||||
mat3_apply(self.matrix, xyz)
|
||||
}
|
||||
}
|
||||
|
||||
/// The Bradford transform taking `from` white to `to` white.
|
||||
fn bradford_adaptation(from: [f32; 3], to: [f32; 3]) -> [[f32; 3]; 3] {
|
||||
let src = mat3_apply(BRADFORD, from);
|
||||
let dst = mat3_apply(BRADFORD, to);
|
||||
let scale = [
|
||||
[dst[0] / src[0], 0.0, 0.0],
|
||||
[0.0, dst[1] / src[1], 0.0],
|
||||
[0.0, 0.0, dst[2] / src[2]],
|
||||
];
|
||||
mat3_mul(BRADFORD_INV, mat3_mul(scale, BRADFORD))
|
||||
}
|
||||
|
||||
pub(crate) fn mat3_apply(m: [[f32; 3]; 3], v: [f32; 3]) -> [f32; 3] {
|
||||
[
|
||||
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
|
||||
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
|
||||
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn mat3_mul(a: [[f32; 3]; 3], b: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
|
||||
let mut out = [[0.0f32; 3]; 3];
|
||||
for (r, row) in out.iter_mut().enumerate() {
|
||||
for (c, slot) in row.iter_mut().enumerate() {
|
||||
*slot = (0..3).map(|k| a[r][k] * b[k][c]).sum();
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_clear_frame_is_white_under_every_viewing_illuminant() {
|
||||
// The property the whole viewing step exists to hold: an unexposed
|
||||
// transparency on a light table is white, whatever the light table is.
|
||||
// Normalising on luminance alone would leave the illuminant's own
|
||||
// colour in the result, and a D50 print would render warm.
|
||||
for name in ["D50", "D55", "D65", "TH-KG3"] {
|
||||
let rgb = Viewing::new(name).to_srgb(&[1.0; SPECTRUM]);
|
||||
for c in rgb {
|
||||
assert!(
|
||||
(c - 1.0).abs() < 0.01,
|
||||
"{name}: clear frame gave {rgb:?}, which is not neutral white"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapting_a_white_to_itself_changes_nothing() {
|
||||
let m = bradford_adaptation(D65_WHITE, D65_WHITE);
|
||||
for r in 0..3 {
|
||||
for c in 0..3 {
|
||||
let expected = if r == c { 1.0 } else { 0.0 };
|
||||
assert!((m[r][c] - expected).abs() < 1e-5, "{m:?} is not the identity");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hotter_blackbody_is_bluer() {
|
||||
// Cheap, but it is the one property that catches Planck's law written
|
||||
// with a sign or a reciprocal wrong, which otherwise produces a
|
||||
// plausible-looking curve pointing the wrong way.
|
||||
let cool = blackbody(2800.0);
|
||||
let hot = blackbody(9000.0);
|
||||
let blue = 10; // 430nm
|
||||
let red = 70; // 730nm
|
||||
assert!(hot[blue] / hot[red] > cool[blue] / cool[red]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_observer_integrates_to_a_plausible_white() {
|
||||
// Guards the generated table against a transposed or misaligned paste:
|
||||
// D65 through the 1931 observer must land on sRGB's own white.
|
||||
let v = Viewing::new("D65");
|
||||
let rgb = v.to_srgb(&[1.0; SPECTRUM]);
|
||||
assert!(rgb.iter().all(|c| (c - 1.0).abs() < 0.01), "{rgb:?}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
//! Generated by tools/film-profiles/convert.py. Do not edit.
|
||||
//!
|
||||
//! The fixed colour science: the observer, the illuminants and the spectral
|
||||
//! basis. None of it is per-stock, all of it is published data, and together it
|
||||
//! is under 6 kB of source -- which is the point. A film simulation's data cost
|
||||
//! is dominated by whatever it uses to turn a pixel back into a spectrum, and a
|
||||
//! basis is three curves where a coefficient table is megabytes.
|
||||
|
||||
/// The lowest wavelength sampled, in nanometres.
|
||||
pub const LAMBDA_MIN: f32 = 380.0;
|
||||
/// The spacing between samples, in nanometres.
|
||||
pub const LAMBDA_STEP: f32 = 5.0;
|
||||
/// How many wavelengths every spectral table carries.
|
||||
///
|
||||
/// The profiles, the observer and the basis all arrive on this grid already, so
|
||||
/// nothing in this crate resamples anything.
|
||||
pub const SPECTRUM: usize = 81;
|
||||
|
||||
|
||||
/// CIE 1931 2-degree standard observer, x-bar/y-bar/z-bar per wavelength.
|
||||
pub static OBSERVER: [[f32; 3]; SPECTRUM] = [
|
||||
[0.001368, 3.9e-05, 0.006450001], // 380nm
|
||||
[0.002236, 6.4e-05, 0.01054999], // 385nm
|
||||
[0.004243, 0.00012, 0.02005001], // 390nm
|
||||
[0.00765, 0.000217, 0.03621], // 395nm
|
||||
[0.01431, 0.000396, 0.06785001], // 400nm
|
||||
[0.02319, 0.00064, 0.1102], // 405nm
|
||||
[0.04351, 0.00121, 0.2074], // 410nm
|
||||
[0.07763, 0.00218, 0.3713], // 415nm
|
||||
[0.13438, 0.004, 0.6456], // 420nm
|
||||
[0.21477, 0.0073, 1.0390501], // 425nm
|
||||
[0.2839, 0.0116, 1.3856], // 430nm
|
||||
[0.3285, 0.01684, 1.62296], // 435nm
|
||||
[0.34828, 0.023, 1.74706], // 440nm
|
||||
[0.34806, 0.0298, 1.7826], // 445nm
|
||||
[0.3362, 0.038, 1.77211], // 450nm
|
||||
[0.3187, 0.048, 1.7441], // 455nm
|
||||
[0.2908, 0.06, 1.6692], // 460nm
|
||||
[0.2511, 0.0739, 1.5281], // 465nm
|
||||
[0.19536, 0.09098, 1.28764], // 470nm
|
||||
[0.1421, 0.1126, 1.0419], // 475nm
|
||||
[0.09564, 0.13902, 0.8129501], // 480nm
|
||||
[0.05795001, 0.1693, 0.6162], // 485nm
|
||||
[0.03201, 0.20802, 0.46518], // 490nm
|
||||
[0.0147, 0.2586, 0.3533], // 495nm
|
||||
[0.0049, 0.323, 0.272], // 500nm
|
||||
[0.0024, 0.4073, 0.2123], // 505nm
|
||||
[0.0093, 0.503, 0.1582], // 510nm
|
||||
[0.0291, 0.6082, 0.1117], // 515nm
|
||||
[0.06327, 0.71, 0.07824999], // 520nm
|
||||
[0.1096, 0.7932, 0.05725001], // 525nm
|
||||
[0.1655, 0.862, 0.04216], // 530nm
|
||||
[0.2257499, 0.9148501, 0.02984], // 535nm
|
||||
[0.2904, 0.954, 0.0203], // 540nm
|
||||
[0.3597, 0.9803, 0.0134], // 545nm
|
||||
[0.4334499, 0.9949501, 0.008749999], // 550nm
|
||||
[0.5120501, 1.0, 0.005749999], // 555nm
|
||||
[0.5945, 0.995, 0.0039], // 560nm
|
||||
[0.6784, 0.9786, 0.002749999], // 565nm
|
||||
[0.7621, 0.952, 0.0021], // 570nm
|
||||
[0.8425, 0.9154, 0.0018], // 575nm
|
||||
[0.9163, 0.87, 0.001650001], // 580nm
|
||||
[0.9786, 0.8163, 0.0014], // 585nm
|
||||
[1.0263, 0.757, 0.0011], // 590nm
|
||||
[1.0567, 0.6949, 0.001], // 595nm
|
||||
[1.0622, 0.631, 0.0008], // 600nm
|
||||
[1.0456, 0.5668, 0.0006], // 605nm
|
||||
[1.0026, 0.503, 0.00034], // 610nm
|
||||
[0.9384, 0.4412, 0.00024], // 615nm
|
||||
[0.8544499, 0.381, 0.00019], // 620nm
|
||||
[0.7514, 0.321, 0.0001], // 625nm
|
||||
[0.6424, 0.265, 4.999999e-05], // 630nm
|
||||
[0.5419, 0.217, 3e-05], // 635nm
|
||||
[0.4479, 0.175, 2e-05], // 640nm
|
||||
[0.3608, 0.1382, 1e-05], // 645nm
|
||||
[0.2835, 0.107, 2.1175824e-22], // 650nm
|
||||
[0.2187, 0.0816, 0.0], // 655nm
|
||||
[0.1649, 0.061, 0.0], // 660nm
|
||||
[0.1212, 0.04458, 0.0], // 665nm
|
||||
[0.0874, 0.032, 0.0], // 670nm
|
||||
[0.0636, 0.0232, 0.0], // 675nm
|
||||
[0.04677, 0.017, 0.0], // 680nm
|
||||
[0.0329, 0.01192, 0.0], // 685nm
|
||||
[0.0227, 0.00821, 0.0], // 690nm
|
||||
[0.01584, 0.005723, 0.0], // 695nm
|
||||
[0.01135916, 0.004102, 0.0], // 700nm
|
||||
[0.008110916, 0.002929, 0.0], // 705nm
|
||||
[0.005790346, 0.002091, 0.0], // 710nm
|
||||
[0.004109457, 0.001484, 0.0], // 715nm
|
||||
[0.002899327, 0.001047, 0.0], // 720nm
|
||||
[0.00204919, 0.00074, 0.0], // 725nm
|
||||
[0.001439971, 0.00052, 0.0], // 730nm
|
||||
[0.0009999493, 0.0003611, 0.0], // 735nm
|
||||
[0.0006900786, 0.0002492, 0.0], // 740nm
|
||||
[0.0004760213, 0.0001719, 0.0], // 745nm
|
||||
[0.0003323011, 0.00012, 0.0], // 750nm
|
||||
[0.0002348261, 8.48e-05, 0.0], // 755nm
|
||||
[0.0001661505, 6e-05, 0.0], // 760nm
|
||||
[0.000117413, 4.24e-05, 0.0], // 765nm
|
||||
[8.307527e-05, 3e-05, 0.0], // 770nm
|
||||
[5.870652e-05, 2.12e-05, 0.0], // 775nm
|
||||
[4.150994e-05, 1.499e-05, 0.0], // 780nm
|
||||
];
|
||||
|
||||
/// Mallett & Yuksel (2019) sRGB reflectance basis: the three smooth,
|
||||
/// non-negative spectra that reconstruct any sRGB colour exactly.
|
||||
///
|
||||
/// This is what makes the exposure step a 3x3 matrix rather than a
|
||||
/// per-pixel spectral integration -- see [`crate::bake`].
|
||||
pub static SRGB_BASIS: [[f32; 3]; SPECTRUM] = [
|
||||
[0.32745741, 0.33186171, 0.34068079], // 380nm
|
||||
[0.32375058, 0.32968819, 0.34656119], // 385nm
|
||||
[0.31343946, 0.32786002, 0.35870049], // 390nm
|
||||
[0.28887938, 0.31917358, 0.39194703], // 395nm
|
||||
[0.23920568, 0.29432258, 0.46647173], // 400nm
|
||||
[0.18970204, 0.25869706, 0.5516009], // 405nm
|
||||
[0.12174607, 0.18889432, 0.68935961], // 410nm
|
||||
[0.074578271, 0.12538838, 0.80003335], // 415nm
|
||||
[0.044433159, 0.07868706, 0.87687978], // 420nm
|
||||
[0.028928632, 0.053143271, 0.9179281], // 425nm
|
||||
[0.022316653, 0.042288146, 0.9353952], // 430nm
|
||||
[0.016911307, 0.033318346, 0.94977035], // 435nm
|
||||
[0.014181107, 0.029755948, 0.95606294], // 440nm
|
||||
[0.013053143, 0.030331251, 0.95661561], // 445nm
|
||||
[0.011986164, 0.030988572, 0.95702526], // 450nm
|
||||
[0.011288715, 0.031686355, 0.95702493], // 455nm
|
||||
[0.010906066, 0.034669962, 0.95442397], // 460nm
|
||||
[0.010400713, 0.034551957, 0.95504733], // 465nm
|
||||
[0.01063736, 0.040684806, 0.94867783], // 470nm
|
||||
[0.010907663, 0.054460037, 0.9346323], // 475nm
|
||||
[0.011032712, 0.080905287, 0.908062], // 480nm
|
||||
[0.011310657, 0.1463483, 0.84234104], // 485nm
|
||||
[0.011154642, 0.37967964, 0.60916572], // 490nm
|
||||
[0.01014877, 0.76674427, 0.22310696], // 495nm
|
||||
[0.0089185821, 0.87621475, 0.11486667], // 500nm
|
||||
[0.0076855763, 0.91849166, 0.073822768], // 505nm
|
||||
[0.0067057083, 0.94065556, 0.052638729], // 510nm
|
||||
[0.005995806, 0.95373188, 0.040272309], // 515nm
|
||||
[0.0055372566, 0.96164328, 0.032819463], // 520nm
|
||||
[0.0051937842, 0.96720002, 0.027606196], // 525nm
|
||||
[0.0050253623, 0.97098975, 0.023984891], // 530nm
|
||||
[0.0051363628, 0.9728523, 0.022011333], // 535nm
|
||||
[0.0054332003, 0.97311659, 0.021450205], // 540nm
|
||||
[0.0058199859, 0.97335107, 0.020828945], // 545nm
|
||||
[0.0064005728, 0.97335112, 0.020248311], // 550nm
|
||||
[0.0074495287, 0.97226108, 0.020289391], // 555nm
|
||||
[0.0085836358, 0.97335102, 0.018065342], // 560nm
|
||||
[0.010395762, 0.9731485, 0.016455742], // 565nm
|
||||
[0.013565434, 0.97106131, 0.01537326], // 570nm
|
||||
[0.019384516, 0.96637131, 0.014244178], // 575nm
|
||||
[0.032084071, 0.95494197, 0.012973962], // 580nm
|
||||
[0.074356038, 0.91357899, 0.012064974], // 585nm
|
||||
[0.62439372, 0.3643488, 0.011257478], // 590nm
|
||||
[0.91831003, 0.071507243, 0.010182725], // 595nm
|
||||
[0.94925303, 0.041230434, 0.0095165354], // 600nm
|
||||
[0.95818783, 0.032423874, 0.0093882927], // 605nm
|
||||
[0.95818775, 0.03192463, 0.0098876191], // 610nm
|
||||
[0.95818763, 0.031276033, 0.010536342], // 615nm
|
||||
[0.95567906, 0.03263037, 0.011690569], // 620nm
|
||||
[0.95800615, 0.029530872, 0.012462973], // 625nm
|
||||
[0.95410157, 0.031561761, 0.014336665], // 630nm
|
||||
[0.94760761, 0.035674218, 0.016718175], // 635nm
|
||||
[0.93868133, 0.041403005, 0.019915666], // 640nm
|
||||
[0.92446668, 0.05060426, 0.024929056], // 645nm
|
||||
[0.90460603, 0.0634343, 0.031959674], // 650nm
|
||||
[0.8804122, 0.078918245, 0.040669554], // 655nm
|
||||
[0.84778787, 0.099542743, 0.052669382], // 660nm
|
||||
[0.80577913, 0.12559576, 0.068625111], // 665nm
|
||||
[0.75253185, 0.15759091, 0.089877232], // 670nm
|
||||
[0.6864394, 0.19539824, 0.11816236], // 675nm
|
||||
[0.61869457, 0.23147447, 0.14983095], // 680nm
|
||||
[0.54026444, 0.26885214, 0.19088341], // 685nm
|
||||
[0.47296442, 0.29602916, 0.2310064], // 690nm
|
||||
[0.4327016, 0.30975499, 0.25754339], // 695nm
|
||||
[0.40535805, 0.31781588, 0.27682604], // 700nm
|
||||
[0.38549183, 0.32299035, 0.29151777], // 705nm
|
||||
[0.37098358, 0.32635385, 0.30266251], // 710nm
|
||||
[0.3576087, 0.3291439, 0.3132473], // 715nm
|
||||
[0.3487128, 0.33080873, 0.32047833], // 720nm
|
||||
[0.34488012, 0.33148269, 0.32363699], // 725nm
|
||||
[0.34191788, 0.33198455, 0.32609731], // 730nm
|
||||
[0.33953109, 0.33234117, 0.32812737], // 735nm
|
||||
[0.3371695, 0.33291201, 0.32991798], // 740nm
|
||||
[0.33617202, 0.33291928, 0.3309079], // 745nm
|
||||
[0.33516744, 0.33302767, 0.33180363], // 750nm
|
||||
[0.33442163, 0.3331797, 0.33239663], // 755nm
|
||||
[0.33400876, 0.33324703, 0.33274078], // 760nm
|
||||
[0.33391579, 0.33325935, 0.33282086], // 765nm
|
||||
[0.33381845, 0.33327505, 0.33290173], // 770nm
|
||||
[0.33367277, 0.33329433, 0.33302597], // 775nm
|
||||
[0.33356951, 0.33330942, 0.33311108], // 780nm
|
||||
];
|
||||
|
||||
/// CIE standard illuminant D50, normalised to unit mean.
|
||||
pub static ILLUMINANT_D50: [f32; SPECTRUM] = [
|
||||
0.28500907, 0.31632887, 0.34766031, 0.46076543, 0.57388219, 0.61580487,
|
||||
0.65773919, 0.67822334, 0.69871914, 0.68582343, 0.67292773, 0.77189179,
|
||||
0.8708675, 0.94315561, 1.0154437, 1.0350317, 1.054608, 1.0590075,
|
||||
1.0634069, 1.0851713, 1.1069474, 1.0886397, 1.0703319, 1.0922127,
|
||||
1.1141052, 1.1192844, 1.124452, 1.1274548, 1.1304576, 1.1593798,
|
||||
1.188302, 1.1804808, 1.1726596, 1.1817494, 1.1908393, 1.1773616,
|
||||
1.1638724, 1.1506973, 1.1375107, 1.1444008, 1.1512793, 1.1197383,
|
||||
1.088209, 1.1125805, 1.1369636, 1.1461582, 1.1553645, 1.1540376,
|
||||
1.1527225, 1.1334022, 1.1140819, 1.1323314, 1.1505693, 1.1320055,
|
||||
1.1134418, 1.1281298, 1.1428063, 1.1708207, 1.1988235, 1.1763025,
|
||||
1.1537816, 1.0853925, 1.0170033, 1.0415727, 1.0661536, 1.0736257,
|
||||
1.0811094, 0.98780176, 0.89448247, 0.95068587, 1.0068776, 1.0422011,
|
||||
1.077513, 0.9940052, 0.91049735, 0.7909793, 0.67146125, 0.81828375,
|
||||
0.96511788, 0.93806949, 0.91100946,
|
||||
];
|
||||
|
||||
/// CIE standard illuminant D55, normalised to unit mean.
|
||||
pub static ILLUMINANT_D55: [f32; SPECTRUM] = [
|
||||
0.37928266, 0.41130471, 0.44333841, 0.57639697, 0.70945552, 0.75371138,
|
||||
0.79797887, 0.81556713, 0.83316704, 0.81185393, 0.79052917, 0.89349794,
|
||||
0.99645507, 1.0685542, 1.1406533, 1.1550288, 1.1694044, 1.1662034,
|
||||
1.1630023, 1.1794499, 1.1958974, 1.1687759, 1.1416427, 1.1567865,
|
||||
1.1719303, 1.1720235, 1.1721049, 1.1679843, 1.1638637, 1.1884361,
|
||||
1.2130201, 1.2007514, 1.1884826, 1.1935228, 1.198563, 1.181289,
|
||||
1.164015, 1.1478119, 1.1316089, 1.1347051, 1.1378131, 1.1010418,
|
||||
1.0642822, 1.0816726, 1.0990514, 1.1032535, 1.1074439, 1.1020894,
|
||||
1.096735, 1.0747816, 1.0528283, 1.0637817, 1.0747351, 1.0545045,
|
||||
1.0342739, 1.0427945, 1.0513035, 1.072442, 1.0935921, 1.0703467,
|
||||
1.0471014, 0.98728263, 0.92745226, 0.94585534, 0.96427006, 0.97593349,
|
||||
0.98759692, 0.90256562, 0.81754596, 0.87031076, 0.92307556, 0.95620343,
|
||||
0.9893313, 0.91301847, 0.83669401, 0.72561205, 0.61451846, 0.74916008,
|
||||
0.8838017, 0.85988119, 0.83597232,
|
||||
];
|
||||
|
||||
/// CIE standard illuminant D65, normalised to unit mean.
|
||||
pub static ILLUMINANT_D65: [f32; SPECTRUM] = [
|
||||
0.57072795, 0.59740886, 0.62409091, 0.78458177, 0.94507378, 0.99492846,
|
||||
1.0447843, 1.055895, 1.0670056, 1.0284649, 0.98992529, 1.0937493,
|
||||
1.1975745, 1.2669063, 1.3362495, 1.3408404, 1.3454313, 1.3285751,
|
||||
1.3117304, 1.3177945, 1.3238586, 1.2832485, 1.2426385, 1.2457333,
|
||||
1.2488396, 1.2399776, 1.2311155, 1.2139168, 1.196718, 1.2132658,
|
||||
1.2298251, 1.2110732, 1.1923213, 1.1902656, 1.1882214, 1.1651185,
|
||||
1.1420155, 1.1210835, 1.1001515, 1.0970326, 1.0939138, 1.0533585,
|
||||
1.0128033, 1.020344, 1.0278847, 1.0255596, 1.0232356, 1.0123842,
|
||||
1.0015327, 0.97635016, 0.95116871, 0.95351327, 0.95585783, 0.93488814,
|
||||
0.91391845, 0.91499081, 0.91606316, 0.92784419, 0.93962522, 0.91682146,
|
||||
0.89401769, 0.8451223, 0.79622805, 0.80700753, 0.81778702, 0.83343149,
|
||||
0.8490771, 0.77630216, 0.70352722, 0.7508158, 0.79810438, 0.82780478,
|
||||
0.85750517, 0.79187126, 0.72623849, 0.62817019, 0.53010303, 0.64651553,
|
||||
0.76292802, 0.74338471, 0.72384139,
|
||||
];
|
||||
@@ -0,0 +1,64 @@
|
||||
//! Cross-check against the model this was ported from.
|
||||
//!
|
||||
//! The expected values were produced by `tools/film-profiles/proto.py` running
|
||||
//! against spektrafilm's own colour-science stack — a different language, a
|
||||
//! different observer table, a different chromatic adaptation implementation.
|
||||
//! Agreement to three decimal places is therefore evidence about the *model*,
|
||||
//! not about one implementation of it: a transposed matrix, a mispasted
|
||||
//! observer row or an illuminant normalised differently would all show up here
|
||||
//! and in none of the unit tests, every one of which would still pass.
|
||||
|
||||
use dr_film::bake::{bake, Recipe, MID_GREY};
|
||||
use dr_film::find;
|
||||
|
||||
/// Slack allowed against the reference.
|
||||
///
|
||||
/// The baked lookup's own interpolation error is about 0.003, and the two
|
||||
/// implementations differ in float width throughout, so anything under 0.01 is
|
||||
/// agreement. A real error — a swapped channel, a dropped illuminant — moves
|
||||
/// these numbers by tenths, not by thousandths.
|
||||
const TOLERANCE: f32 = 0.01;
|
||||
|
||||
fn assert_close(label: &str, input: f32, got: [f32; 3], want: [f32; 3]) {
|
||||
for c in 0..3 {
|
||||
assert!(
|
||||
(got[c] - want[c]).abs() < TOLERANCE,
|
||||
"{label} at {input}: channel {c} was {:.4}, reference says {:.4}\n got {got:?}\n want {want:?}",
|
||||
got[c],
|
||||
want[c]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reversal_stock_matches_the_reference() {
|
||||
let film = find("kodak_kodachrome_64").unwrap();
|
||||
let baked = bake(&Recipe::new(film, None));
|
||||
|
||||
for (input, want) in [
|
||||
(0.02f32, [0.0130f32, 0.0005, 0.0386]),
|
||||
(0.09, [0.0827, 0.0670, 0.0996]),
|
||||
(MID_GREY, [0.1965, 0.1794, 0.2055]),
|
||||
(0.40, [0.4021, 0.3772, 0.3937]),
|
||||
(0.80, [0.5743, 0.5481, 0.5450]),
|
||||
] {
|
||||
assert_close("Kodachrome 64", input, baked.apply([input; 3]), want);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_printed_negative_matches_the_reference() {
|
||||
let film = find("kodak_portra_400").unwrap();
|
||||
let paper = dr_film::default_print(film).unwrap();
|
||||
let baked = bake(&Recipe::new(film, Some(paper)));
|
||||
|
||||
for (input, want) in [
|
||||
(0.02f32, [0.0078f32, 0.0061, 0.0107]),
|
||||
(0.09, [0.0435, 0.0495, 0.0584]),
|
||||
(MID_GREY, [0.1412, 0.1468, 0.1616]),
|
||||
(0.40, [0.3567, 0.3578, 0.3721]),
|
||||
(0.80, [0.5960, 0.5846, 0.5815]),
|
||||
] {
|
||||
assert_close("Portra 400 on Endura", input, baked.apply([input; 3]), want);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user