diff options
| author | Paweł Redman <pawel.redman@gmail.com> | 2026-06-14 18:50:27 +0200 |
|---|---|---|
| committer | Paweł Redman <pawel.redman@gmail.com> | 2026-06-14 18:50:27 +0200 |
| commit | 89a67854f5af72836498a9412f759d4337fad32d (patch) | |
| tree | ac073a83152872747e4a31f632f84edd87dfab90 /site | |
| parent | 444d3196359be6bb82fdb6a0f5d5ba32fde4118a (diff) | |
Migrate remaining GSoC posts
Co-authored-by: Cursor <cursoragent@cursor.com>
Diffstat (limited to 'site')
| -rw-r--r-- | site/content/gsoc/academic_year_ends.typ | 10 | ||||
| -rw-r--r-- | site/content/gsoc/convergence_problems.typ | 49 | ||||
| -rw-r--r-- | site/content/gsoc/differentiating_code.typ | 83 | ||||
| -rw-r--r-- | site/content/gsoc/differentiating_code_addendum.typ | 52 | ||||
| -rw-r--r-- | site/content/gsoc/first.typ | 10 | ||||
| -rw-r--r-- | site/content/gsoc/first_merge.typ | 8 | ||||
| -rw-r--r-- | site/content/gsoc/june_ends.typ | 17 | ||||
| -rw-r--r-- | site/content/gsoc/mallett_merge.typ | 8 | ||||
| -rw-r--r-- | site/content/gsoc/method_comparison.typ | 84 | ||||
| -rw-r--r-- | site/content/gsoc/mid_august.typ | 57 | ||||
| -rw-r--r-- | site/content/gsoc/new_color_quality_metrics.typ | 54 | ||||
| -rw-r--r-- | site/content/gsoc/nondimensionalization.typ | 36 | ||||
| -rw-r--r-- | site/content/gsoc/plan_june.typ | 18 | ||||
| -rw-r--r-- | site/content/gsoc/progress_late_june.typ | 24 | ||||
| -rw-r--r-- | site/content/gsoc/proposal.typ | 88 | ||||
| -rw-r--r-- | site/content/gsoc/prototypes_repo.typ | 15 | ||||
| -rw-r--r-- | site/content/gsoc/scipy_minimize_value_threshold.typ | 33 | ||||
| -rw-r--r-- | site/refs.bib | 33 |
18 files changed, 679 insertions, 0 deletions
diff --git a/site/content/gsoc/academic_year_ends.typ b/site/content/gsoc/academic_year_ends.typ new file mode 100644 index 0000000..38f6fe6 --- /dev/null +++ b/site/content/gsoc/academic_year_ends.typ @@ -0,0 +1,10 @@ +#import "/common.typ": post + +#show: post.with( + title: "Academic year ends", + date: datetime(year: 2020, month: 6, day: 19), +) + +I'm finally done with all my university exams, projects and essays. I spent the first two weeks wrapping up the semester while simultaneously working on Colour. It was a little stressful, but I managed to pull it off without falling behind the schedule. + +I've been working on moving the code I've written from the prototype repository into Colour itself. This means adapting the code to use the proper style, naming conventions and making sure it fits well with the rest of the library. Most of the work is already done, so expect a fork very soon. diff --git a/site/content/gsoc/convergence_problems.typ b/site/content/gsoc/convergence_problems.typ new file mode 100644 index 0000000..320261a --- /dev/null +++ b/site/content/gsoc/convergence_problems.typ @@ -0,0 +1,49 @@ +#import "/common.typ": post, link-self, static, img + +#show: post.with( + title: "Convergence problems", + date: datetime(year: 2020, month: 6, day: 12), +) + +#figure( + img(static("gsoc/bad_convergence.png")), + caption: [Solvers tend to get attracted to spectra like this and get stuck], +) + +I've been trying to write code that find parameters to the reflectivity model I mentioned in #link-self("gsoc/nondimensionalization")[an earlier post]. The objective is to minimize the difference between the color given the model and the target. It turns out the optimization is rather difficult and all SciPy solvers (including global optimization algorithms) frequently fail. At the moment I only have a loose idea of what's going on. + +Because of the sigmoid function used in the model, very large coefficients create spectra resembling a rectangular or a step function (like on the picture). Then, it's always possible to adjust the coefficients to make the spectrum even steeper without changing its overall shape. After doing this enough times, coefficients are so large that varying them barely changes the spectrum, a situation solvers treat as having found a good minimum. I suppose it's because the gradient becomes very small, but a better-suited algorithm would look at the near-zero Hessian and notice it's moving along an asymptote (a bad minimum here). I'm also not sure why solvers are (sometimes) attracted to big numbers in the first place. + +I did manage to overcome this issue, but in a rather hacky and inelegant way described below. + += Feedback + +Because the end goal is to reproduce Jakob and Hanika's rather dense coefficient maps, consisting of almost 800,000 points per color space, coefficients from nearby, already-solved-for colors can be used as starting points. This greatly improves the chances of finding a good minimum. Coefficients for unsaturated (but not too dark) colors are easy to find, so they provide good starting points for the entire feedback scheme. + +Unfortunately there are still some edge cases where the solver will fail even if it's given a good starting point. + += A more general solution + +To make sure I'll always be able to find the coefficients, I came up with a simple algorithm. Whenever optimization fails, a color with known coefficients is used to solve for a point somewhere between the target and the color. The process is repeated, getting closer and closer to the target until it finally converges. Below is an example using #link("https://zittrig.eu/git/enneract/gsoc-prototypes/src/25cd4e2bfffb59a1663a0620a445081723e2627e")[test_bad_convergence.py]. + +#table( + columns: 4, + table.header([*XYZ*], [*Starting coeffs.*], [*Final coeffs.*], [*ΔE*]), + [0.00263241, 0.00174905, 0.00092602], [0, 0, 0], [-1.59e+8, -3.27e+7, -5.611e+7], [4.50 (diverged)], + [0.16798287, 0.16754119, 0.16712968], [2.13, -1.07, -0.296], [2.17, -1.09, -0.835], [7.36e-5], + [0.00263241, 0.00174905, 0.00092602], [2.17, -1.09, -0.835], [-1.03e+8, -2.06e+7, -3.02e+7], [4.50 (diverged)], + [0.08530764, 0.08464512, 0.0840278], [2.17, -1.09, -0.835], [2.87, -1.45, -1.41], [3.42e-5], + [0.00263241, 0.00174905, 0.00092602], [2.87, -1.45, -1.41], [-9.79e+7, -1.97e+7, -3.68e+7], [4.50 (diverged)], + [0.04397003, 0.04319709, 0.04247693], [2.87, -1.45, -1.41], [4.36, -2.25, -2.12], [4.14e-5], + [0.00263241, 0.00174905, 0.00092602], [4.36, -2.25, -2.12], [-9.76e+7, -2.00e+7, -3.60e+7], [4.50 (diverged)], + [0.02330122, 0.02247307, 0.02170148], [4.36, -2.25, -2.12], [7.35, -3.87, -2.97], [1.66e-5], + [0.00263241, 0.00174905, 0.00092602], [7.35, -3.87, -2.97], [-1.47e+8, -3.07e+7, -4.53e+7], [4.50 (diverged)], + [0.01296681, 0.01211106, 0.0113137], [7.35, -3.87, -2.97], [13.1, -6.99, -3.98], [1.26e-5], + [0.00263241, 0.00174905, 0.00092602], [13.1, -6.99, -3.98], [59.7, -23.9, -14.9], [6.41e-7], +) + +The algorithm backtracks a lot, but it does eventually converge. It also highlights a significant problem with the implementation. This particular example runs about 3 orders of magnitude slower than the authors' code. + += What next? + +Thomas and I agreed that despite the horrible performance, the code is usable enough. The next step is writing code that would allow users to sample precomputed tables. I'll come back to the problem of creating the tables themselves later. diff --git a/site/content/gsoc/differentiating_code.typ b/site/content/gsoc/differentiating_code.typ new file mode 100644 index 0000000..8d7aa03 --- /dev/null +++ b/site/content/gsoc/differentiating_code.typ @@ -0,0 +1,83 @@ +#import "/common.typ": post + +#show: post.with( + title: "Differentiating code", + date: datetime(year: 2020, month: 7, day: 13), +) + +One step in the spectral upsampling method described by Jakob and Hanika (2019) @Jakob2019 is computing an error function, which is used to find the model parameters. I wanted to provide derivatives of the error function with respect to its arguments to the optimization algorithm I used, in hopes for faster and more reliable convergence. + +How does one differentiate Python code though? Every program, no matter how complicated, is a series of simple operations and the #link("https://en.wikipedia.org/wiki/Chain_rule")[chain rule] can be used. Computer algebra systems (the one I use is #link("http://maxima.sourceforge.net/Maxima")[Maxima]) can help with the process, making this a trivial task. + += The error function + +The algorithm is as follows: + +- Given a triplet of coefficients (model parameters), $c_0$, $c_1$, $c_2$, compute a reflectance according to the model. +$ + U(lambda) = c_0 lambda^2 + c_1 lambda + c_2 +$ +$ + R(lambda) = 1 / 2 + U / (2 sqrt(1 + U^2)) +$ +- Compute the radiosity of a surface with the reflectance, lit by the given illuminant. This is done by simply multiplying the reflectance by the illuminant's spectral distribution. +$ + E(lambda) = I(lambda) R(lambda) +$ +- Compute the CIE 1931 XYZ tristimulus values describing how the reflected light would be perceived. +$ + X &= k integral overline(E)(lambda) overline(x)(lambda) dif lambda \ + Y &= k integral overline(E)(lambda) overline(y)(lambda) dif lambda \ + Z &= k integral overline(E)(lambda) overline(z)(lambda) dif lambda +$ +where $overline(x)$, $overline(y)$ and $overline(z)$ are the cmfs (color matching functions), quantizing human color vision. They are closely related to (but not the same as) spectral sensivities of the three different kinds of cone cells in a human eye. In particular $overline(y)$ is the luminosity function used in photometry, describing how bright light of different wavelengths appears. The normalization constant $k$, +$ + k = (integral I(lambda) y(lambda) dif lambda)^(-1) +$ +assures that $Y = 1$ for perfectly white surfaces and that arbitrary units can be used for the illuminant and the cmfs. +- Compute the CIE 1976 L\*a\*b\* colorspace coordinates. Start by computing the values of the intermediate lightness function for the three XYZ components. +$ + X_n = X / X_I +$ +$ + f_X = cases( + root(3, X_n) & "for" space X_n > (24 / 116)^3, + 841 X_n / 108 + 4 / 29 & "otherwise", + ) +$ +Likewise for the remaining two components. +$ + L^* &= 116 f_Y - 16 \ + a^* &= 500(f_X - f_Y) \ + b^* &= 200(f_Y - f_Z) +$ +- Compute the Euclidean distance between the coordinates above and the target color (given in the L\*a\*b\* colorspace). This is the CIE 1976 color difference (delta E) formula. +$ + Delta E = sqrt((L^* - L^*_"target")^2 + (a^* - a^*_"target")^2 + (b^* - b^*_"target")^2) +$ + += Discretization + +Because spectral data is typically discrete, that is measured at some chosen wavelengths (e.g. every 10 nanometers in the visible range), the algorithm above operates on arrays rather than continuous functions. Integrals are approximated simply as the corresponding finite sum: +$ + integral f(x) dif x approx sum_i f_i Delta x +$ +This method turned out to be both fast and accurate enough in this application. + += The gradient + +How does $Delta E$ depend on the function inputs? Since this is a scalar function of multiple variables, what we're looking for is the gradient or three derivatives of $Delta E$ with respect to each of the inputs. The answer is given by a repeated application of the chain rule. + +The easiest way to approach it is to start from the beginning of the algorithm and compute the three functions $dif R / dif c_i$. (In code the result is a 3-by-n array, where n is the number of discrete wavelengths.) Then compute the derivatives of $E$ using the chain rule. +$ + (dif E) / (dif c_i) = (dif E) / (dif R) (dif R) / (dif c_i) = I (dif R) / (dif c_i) +$ +The derivatives of $R$ have already been computed in the previous step. Doing this step by step will eventually lead to $(dif (Delta E)) / (dif L^*)$, $(dif (Delta E)) / (dif a^*)$ and $(dif (Delta E)) / (dif b^*)$ and ultimately the gradient of $Delta E$. Take a look at the #link("https://github.com/enneract/colour/blob/b8c113efb7b819fce81213e6f6fa41db90b0b61c/colour/recovery/jakob2019.py#L198-L252")[full code] to see all the formulas. + += Automatic differentation + +There is a clever trick to "automagically" compute derivatives, without having to go step-by-step, differentiating every operation and writing code manually like I did. #link("https://en.wikipedia.org/wiki/Dual_number")[Dual numbers] have the interesting property that, when used correctly, their dual parts are the derivatives of their real parts. They're defined very similarly to complex numbers, but instead of $z = a + b i$, $i^2 = -1$, one has $z = a + b epsilon$, $epsilon^2 = 0$. (Even more interestingly, this can be extended to higher derivatives as well!). See #link("https://en.wikipedia.org/wiki/Automatic_differentiation")[automatic differentiation] on Wikipedia. + +The code I wanted to differentiate was simple enough that I didn't bother looking for and learning a new Python module. I also avoided pulling additional dependencies. + +#bibliography("/refs.bib") diff --git a/site/content/gsoc/differentiating_code_addendum.typ b/site/content/gsoc/differentiating_code_addendum.typ new file mode 100644 index 0000000..b6dcfa5 --- /dev/null +++ b/site/content/gsoc/differentiating_code_addendum.typ @@ -0,0 +1,52 @@ +#import "/common.typ": post, link-self + +#show: post.with( + title: "Differentiating code – addendum", + date: datetime(year: 2020, month: 7, day: 14), +) + +This post describes how to the compute derivatives mentioned in an #link-self("gsoc/differentiating_code")[earlier post] using Maxima, a computer algebra system. Code is listed in a monospaced font and answers are written directly below, in LaTeX. *Ending lines with dollar signs in Maxima (instead of semicolons) surpresses the output,* which is very useful for definitions. + +Start by defining the reflectance model. +``` +U: c_0 * lambda^2 + c_1 * lambda + c_2$ +R: 1 / 2 + U / (2 * sqrt(1 + U^2))$ +``` +Differentiate `R` with respect to `c_0`. The expansion of `U` appears a few times in the result. Substitute `U_` for `U` to simplify it. (The underscore is there so it doesn't expand back to the full expression.) +``` +diff(R, c_0)$ +subst(U_, U, %); +``` +$ + (dif R) / (dif c_0) = lambda^2 / (2 sqrt(U^2 + 1)) - (U^2 lambda^2) / (2 (U^2 + 1)^(3 / 2)) +$ + +This can be further simplified by substituting $t_1 = sqrt(U^2 + 1)$. Unfortunately `subst` won't recognize $(U^2 + 1)^(3 / 2)$ as $t_1^3$ so I recommend doing this manually. +$ + (dif R) / (dif c_0) = lambda^2 / (2 t_1) - (U^2 lambda^2) / (2 t_1^3) +$ + +Repeating the process for the other coefficients reveals a pattern. Let $t_2 = 1 / (2 t_1) - U^2 / (2 t_1^3)$. +$ + (dif R) / (dif c_0) &= lambda^2 t_2 \ + (dif R) / (dif c_1) &= lambda t_2 \ + (dif R) / (dif c_2) &= t_2 +$ + +After skipping trivial derivatives, which can be easily done by hand, one will find themselves having to differentiate the final expression for $Delta E$. Normally an expression like $(dif L) / (dif c_0)$ (`diff(L, c_0)`) would be treated as a derivative of a constant in Maxima and reduced to zero. One can use `depends` to tell the program that the L\*a\*b\* coordinates are functions of the model parameters. The result will then contain derivatives of the coordinates themselves. + +An alternative approach is typing out the coordinates as functions explicitly. Instead of using `depends`, replace `L` with `L(c_0, c_1, c_2)` (and so on for `a` and `b`) in the code below. The downside is that the function arguments will appear in the end result, cluttering it and making a mess. + +``` +depends([L, a, b], [c_0, c_1, c_2])$ +delta_E: sqrt((L - L_target)^2 + (a - a_target)^2 + (b - b_target)^2)$ +``` +Use the same `subst` trick as before to simplify. +``` +diff(delta_E, c_0)$ +subst(delta_E_, delta_E, %); +``` +$ + ((b - b_"target") (dif b) / (dif c_0) + (a - a_"target") (dif a) / (dif c_0) + (L - L_"target") (dif L) / (dif c_0)) / (Delta E) +$ +The other derivatives look the same. Simply substitute $c_1$ or $c_2$ for $c_0$ in the expression above to get obtain them. diff --git a/site/content/gsoc/first.typ b/site/content/gsoc/first.typ new file mode 100644 index 0000000..7ff2619 --- /dev/null +++ b/site/content/gsoc/first.typ @@ -0,0 +1,10 @@ +#import "/common.typ": post, link-self + +#show: post.with( + title: "First post", + date: datetime(year: 2020, month: 5, day: 26), +) + +My #link-self("gsoc/proposal")[proposal] has been accepted and I'm participating in this year's #link("https://summerofcode.withgoogle.com/")[Google Summer of Code] as a student. I'll be working on #link("https://summerofcode.withgoogle.com/projects/#6675798750134272")[New Spectral Upsampling Methods] for #link("https://www.colour-science.org/")[Colour], an affiliated project of #link("https://numfocus.org/")[NumFOCUS]. + +I'll be documenting my progress here, on this blog. This is just a quick first post to make sure everything is working. I'll make a separate post soon, outlining the plan and the timeline for the project. diff --git a/site/content/gsoc/first_merge.typ b/site/content/gsoc/first_merge.typ new file mode 100644 index 0000000..6b78217 --- /dev/null +++ b/site/content/gsoc/first_merge.typ @@ -0,0 +1,8 @@ +#import "/common.typ": post + +#show: post.with( + title: "First merge", + date: datetime(year: 2020, month: 7, day: 23), +) + +I'm happy to announce the code I wrote in the first month of the Summer of Code was merged upstream. You can see the pull request #link("https://github.com/colour-science/colour/pull/603")[here]. diff --git a/site/content/gsoc/june_ends.typ b/site/content/gsoc/june_ends.typ new file mode 100644 index 0000000..49fab67 --- /dev/null +++ b/site/content/gsoc/june_ends.typ @@ -0,0 +1,17 @@ +#import "/common.typ": post + +#show: post.with( + title: "June ends", + date: datetime(year: 2020, month: 7, day: 1), +) + +The first coding has ended and I'm happy to say my #link("https://github.com/colour-science/colour/pull/603")[pull request] is nearly complete. I'm waiting for mentors to review the code, then I'll fix any remaining issues. + +Here's a quick summary of the new code: +- A new sub-module called `jakob2019` was created as a part of `colour.recovery`. +- The main interface is `RGB_to_sd_Jakob2019`, which turns colors (from any RGB space supported by Colour) to a spectral distribution. +- A low-level interface, `find_coefficients`, can be used if only the model parameters are needed. It's where the entire optimization algorithm takes place. +- It's also worth mentioning `error_function`, the function minimized during optimization. It also returns its own gradient (derivatives of the function value with respect to its inputs) to aid numerical algorithms. This required analytically differentiating all the intermediate steps. +- The `Jakob2019Interpolator` class can be used to work with precomputed tables. It can read and write the article's authors' `.coeff` files and also generate new ones. +- All functions and classes are documented. +- Close to 100% test coverage. Unit tests still need some improvements. diff --git a/site/content/gsoc/mallett_merge.typ b/site/content/gsoc/mallett_merge.typ new file mode 100644 index 0000000..d45fc6f --- /dev/null +++ b/site/content/gsoc/mallett_merge.typ @@ -0,0 +1,8 @@ +#import "/common.typ": post + +#show: post.with( + title: "Mallett and Yuksel (2019) merged", + date: datetime(year: 2020, month: 8, day: 20), +) + +Another #link("https://github.com/colour-science/colour/pull/617")[pull request], implementing #link("/gsoc/mid_august.html#mallett_and_yuksel_2019")[Mallett and Yuksel (2019)] spectral upsampling method, was merged upstream. diff --git a/site/content/gsoc/method_comparison.typ b/site/content/gsoc/method_comparison.typ new file mode 100644 index 0000000..f6219e7 --- /dev/null +++ b/site/content/gsoc/method_comparison.typ @@ -0,0 +1,84 @@ +#import "/common.typ": post, link-self, static, img + +#show: post.with( + title: "Comparison of spectral upsampling methods", + date: datetime(year: 2020, month: 8, day: 10), +) + +I took the dataset published alongside +Otsu et al. (2018) @Otsu2018 and #link("https://colour.readthedocs.io/en/develop/generated/colour.COLOURCHECKER_SDS.html")[colorcheckers included in Colour], about 1300 reflectance spectra in total, reconstructed them using various methods and compared them with the original data. I used #link("https://en.wikipedia.org/wiki/Illuminant_D65")[CIE Standard Illuminant D65] and two metrics: +- CIE 1976 ΔE (ΔE is defined as the Euclidean distance between colors in CIELAB color space.), quantizing the *perceived* difference between two colors. Values around 2.4 are taken as the just-noticeable difference, so deltas smaller than this should be insignificant. +- Mean squared error (MSE), the mean squared difference between the original and reconstructed spectra. For any reflectance, there infinitely many metamers, other reflectances that look exactly the same (ΔE=0) under a given illuminant. Good spectral upsampling should result in *realistic* spectra, i.e. those that look similar to those of common and natural materials. This helps spectral rendering to achieve natural results as well. See Peters et al. (2019) @Peters2019 for an interesting and more in-depth discussion of the topic. + +Below are chromaticity diagrams with error heatmaps drawn over them. The color of each square bin represents the mean error of samples lying inside the square. The numbers above the diagrams are, in order, the minimum, maximum, mean and standard deviation of the errors. + += Jakob and Hanika (2019) + +#figure( + img(static("gsoc/error_jakob.jpg")), + caption: [Errors for Jakob and Hanika (2019) with `max_error` set to the default value of 0.024.], +) + +Ending optimization as soon as the error falls below 0.024 results in a roughly uniform distribution, bounded above by the number. The process is also relatively quick. + +#figure( + img(static("gsoc/error_jakob-slow.jpg")), + caption: [Errors for Jakob and Hanika (2019) with `max_error` set to 0.], +) + +Letting SciPy's `L-BFGS-B` solver decide on its own when to stop actually makes it diverge at many points. I'll have to revisit the code and fix this issue. += Otsu et al. (2018) + +#figure( + img(static("gsoc/error_otsu-noclip.jpg")), + caption: [Errors for Otsu et al. (2018) with `clip` set to False.], +) + +The picture above shows that the method has no round-trip errors, ie. the recovered spectra are exact metamers (have the exact same color). + +#figure( + img(static("gsoc/error_otsu.jpg")), + caption: [Errors for Otsu et al. (2018) with `clip` set to the default value of True.], +) + +Clipping unphysical reflectances caused a very small color shift at a single point. It's the sample I talked about in detail in #link-self("gsoc/otsu2018_4")[a previous post]. += Mallett and Yuksel (2019) + +#figure( + img(static("gsoc/error_mallett.jpg")), + caption: [Errors for Mallett and Yuksel (2019).], +) + +Mallett and Yuksel (2019) @Mallett2019 is a method I implemented very recently and I haven't posted about it. More details will follow in a future update. += Meng et al. (2015), Smits (1999) + +#figure( + img(static("gsoc/error_meng.jpg")), + caption: [Errors for Meng et al. (2015).], +) +#figure( + img(static("gsoc/error_smits.jpg")), + caption: [Errors for Smits (1999).], +) + +These two methods had already been implemented in Colour Smits (1999) @Smits1999 is a relatively simple method, assembling a matching reflectance using pre-defined bases. Meng et al. (2015) @Meng2015 is a method similar to Jakob and Hanika (2019) in that it numerically solves an optimization problem to reconstruct a spectrum. + +Errors in Smits (1999) look like the result of an illuminant mismatch. I'll have to redo that. Producing the data for the graph for Meng et al. (2015) took me about 2 hours, so it's another thing to take a closer look at. + += Summary + +Since all methods (bugs and user errors aside) have negligible ΔE's, I'll summarize their MSEs in the table below. Mean MSEs are compared to the method with the lowest mean error. + +#table( + columns: 5, + table.header([*Method*], [*Mean*], [], [*Max*], [*Std. dev.*]), + [Otsu et al. (2018)], [0.000 486], [Ref.], [0.027 2], [0.001 44], + [Meng et al. (2015)], [0.005 59], [12 ×], [0.056 7], [0.008 51], + [Mallett and Yuksel (2019)], [0.007 17], [15 ×], [0.094 4], [0.011 2], + [Smits (1999)], [0.007 73], [16 ×], [0.066 5], [0.010 3], + [Jakob and Hanika (2019)], [0.029 2], [60 ×], [0.152], [0.032], +) + +Unsurprisingly, the data-driven method produces results that look like data the most. Jakob and Hanika's equation seems model much more artificial reflectances. + +#bibliography("/refs.bib") diff --git a/site/content/gsoc/mid_august.typ b/site/content/gsoc/mid_august.typ new file mode 100644 index 0000000..d847872 --- /dev/null +++ b/site/content/gsoc/mid_august.typ @@ -0,0 +1,57 @@ +#import "/common.typ": post, link-self + +#show: post.with( + title: "Mid-August progress report", + date: datetime(year: 2020, month: 8, day: 16), +) + +As outlined in the #link-self("gsoc/proposal")[project proposal], the original plan was to implement at least two new spectral upsampling methods in Colour. Here's an overview of work done so far: + += Jakob and Hanika (2019) + +I've already written about this in detail #link-self("gsoc/june_ends")[here]. The code was #link-self("gsoc/first_merge")[merged upstream]. + += Otsu et al. (2018) + +This is a very interesting and quite complicated method that I covered in a four-part series of blog posts. The code for it is complete: +- A new sub-module, `colour.recovery.otsu2018`. +- `XYZ_to_sd_otsu2018` provides a familiar interface for upsampling given XYZ tristimulus values. +- `Otsu2018Dataset` allows reading and writing datasets to and from files, and can be passed to the upsampling function if one wishes to use their own data. +- `Otsu2018Tree` represents kd-trees, as described in the original article. With this, one can create an optimized clustering from an arbitrary set of spectra. This should even work with emission spectra, though I haven't tested it yet. The code could also be extended to support more (or fewer) than three principal components, turning it into a more general, spectral *representation* method. +- As usual, everything is well documented and unit tested. + +The #link("https://github.com/colour-science/colour/pull/607")[pull request] is awaiting review. + +#html.elem("span", attrs: (id: "mallett_and_yuksel_2019"))[] += Mallett and Yuksel (2019) <mallett_and_yuksel_2019> + +This method seeks to create reflectivities $bold(s)_r$, $bold(s)_g$, $bold(s)_b$ that match RGB primaries. and use them for reconstruction. Given an $(r, g, b)$ triplet, the recovered spectrum is simply: +$ + bold(s) = r bold(s)_r + g bold(s)_g + b bold(s)_b +$ +For this to work, the basis reflectivites have to obey a few constraints: +- They must color-match RGB primaries, ie. have RGB values of [1, 0, 0], [0, 1, 0] and [0, 0, 1]. +- They must sum up to an all-ones spectrum (100% reflectivity at all wavelengths), so that pure white can be recovered correctly. +- None of them can have values outside [0, 1]. +- They must minimize a chosen scalar function, e.g. one quantizing "roughness," so that the spectra are smooth and noisy solutions are avoided. +The code for this is surprisingly straightforward, thanks to SciPy's excellent interface to constrained and bounded optimization. + +A quick overview of the code: +- A new sub-module, `colour.recovery.mallett2019`. +- `sRGB_to_sd_mallett2019` implements upsampling of sRGB colors. +- `RGB_to_sd_mallett2019` can work with any RGB colorspace given the correct basis. +- `spectral_primary_decomposition_Mallett2019` implements the optimization process. + +The #link("https://github.com/colour-science/colour/pull/617")[pull request] is awaiting review. + += Peters et al. (2019) + +The article @Peters2019, as its title suggests, primarily deals with the problem of *representing* spectra. To make a long story short, it's a compression algorithm that uses fancy math to store measured spectra using only a few, low-bitcount coefficients. It can be used for spectral upsampling by pre-computing a large table of those coefficients. The results aren't that great, though. The method is easily outperformed by e.g. Otsu, et al. (2018), as measured by the authors themselves. It was decided it wasn't the best fit for `colour.recovery`. + += What's next? + +There are no more spectral upsampling methods left to implement. Once the remaining pull requests are reviewed and merged, the *New Spectral Upsampling Methods* project will be concluded. + +There's still some time left though and I'm not planning on slacking off for the rest of August. Thomas proposed that I work on #link("https://github.com/colour-science/GSoC/blob/master/2020/GSoC-2020-Project-Ideas.md#new-colour-quality-metrics")[New Colour Quality Metrics] and I agreed. I'll leave the rest of the details for another post. + +#bibliography("/refs.bib") diff --git a/site/content/gsoc/new_color_quality_metrics.typ b/site/content/gsoc/new_color_quality_metrics.typ new file mode 100644 index 0000000..298b553 --- /dev/null +++ b/site/content/gsoc/new_color_quality_metrics.typ @@ -0,0 +1,54 @@ +#import "/common.typ": post, static, img + +#show: post.with( + title: "New color quality metrics", + date: datetime(year: 2020, month: 8, day: 21), +) + +I finished working on spectral upsampling methods about two weeks ago. Since then I've been working on a different part of the library, the `quality` submodule. As described in the original #link("https://github.com/colour-science/GSoC/blob/master/2020/GSoC-2020-Project-Ideas.md#new-colour-quality-metrics")[project idea], the goals are: + +- Adding support for CIE 224:2017 Colour Fidelity Index. +- Adding support for ANSI/IES TM-30-18, a standard defining the same index and a couple more. +- Adding support for creating TM-30-18 Color Rendition Reports. This part I proposed myself after seeing #link("https://github.com/colour-science/colour/issues/551")[an open issue] for it. + +The first two are already complete, see the #link("https://github.com/colour-science/colour/pull/621")[pull request]. The last goal is being worked on in a #link("https://github.com/enneract/TM-30-18")[separate repository], since I'm not yet sure how exactly I want to implement the reports (just Matplotlib, HTML, something else?). + += Color rendering + +There are infinitely many light spectra that humans perceive as white (see #link("https://en.wikipedia.org/wiki/Metamerism_(color)")[metamerism]). In principle it's enough to combine just two monochromatic sources. Just for fun I searched for a spectrum like that and found this: +#table( + columns: 12, + table.header([*λ [nm]*], [...], [449], [450], [451], [...], [567], [568], [569], [570], [...], []), + [*s [a.u.]*], [...], [0.000000], [0.619473], [0.000000], [...], [0.000000], [0.166711], [0.833289], [0.000000], [...], [], +) + +This combination of deep blue and orange laser light, showing up as two very narrow peaks on a plot, will match the standard CIE D65 daylight illluminant perfectly. Of course, if you tried to light up a room with it, you'd notice that it's not "proper" white. Because there would be no light other than blue and orange to reflect off surfaces, everything would look blue, orange, or something in between. + +Fluorescent lamps are infamous for having very peaky emission spectra as well. #link("https://commons.wikimedia.org/wiki/File:Fluorescent_lighting_spectrum_peaks_labeled_with_colored_peaks_added.png")[This Wikimedia Commons image] shows that the peaks are emission lines of various rare elements inside the lamp. Just like in case of my theoretical lamp, objects lit with such lamps don't have quite the right colors. The same problem applies to LEDs as well. The effect a light source has on colors of things is called *color rendering*. + +Below is a chart showing how colors are affected by various daylight (6500 K) illuminants. D65 is the reference, Kinoton is some sort of a projector, FL1 is a standard fluorescent lamp, a LED, a mercury lamp (a bit too blue) and my made up spectrum. + +#figure( + img(static("gsoc/color_rendering.png")), + caption: [A visualization of differences in color rendering. The names refer to spectral distributions from Colour's `SDS_ILLUMINANTS` and `SDS_LIGHT_SOURCES`, with the exception of my artificial 'Twin peaks' lamp.], +) + += Color rendering indexes + +#figure( + img(static("gsoc/cri97.jpg")), + caption: [A high-CRI LED lamp.], +) + +The most common way to quantify how good the color rendition of a lamp is the CIE color rendering index (CRI). Colors of a set of *test color samples* are compared between a test illuminant and a reference (a black body or a daylight model, depending on the temperature), much like in my chart above. For each sample a *special CRI* (SCRI) is computed based on the color difference. The arithmetic mean of eight (There are 14, but it seems that nobody cares about the last 5.) SCRIs is called the CRI or $R_a$. + +A perfect lamp has CRI equal to 100. The worse the color rendering, the lower the number. For example, FL1 has a CRI of 76 and my made-up lamp has a CRI of -7. Negative numbers are possible, but rare. Manufacturers sometimes specify $R_9$, the SCRI of a very saturated red sample, and it does tend to be very low or negative. + +Color fidelity index (CFI) is an updated version of CRI, using many more test color samples (99) and better, more accurate colorimetry. Even the problem of negative indexes was fixed. TM-30-18 adds a few more metrics, notably the gamut area index, quantifying how intense colors can get when a given illuminant is used. This is quite useful, since washed out colors (caused by a low gamut area lamp) can look worse than inaccurate colors (caused by a low color fidelity lamp). + +TM-30-18 also details how to create a *color rendition report*, which presents all the information in a nice and organized way. Work on that is still in progress, so I'll leave the details for another post. + +#figure( + img(static("gsoc/cri97_report.jpg")), + caption: [A (partial) TM-30-18 color rendition report for the high-CRI OSRAM LED mentioned above. Work in progress. Information is accurate but the layout is still a bit messy and needs polishing.], +) diff --git a/site/content/gsoc/nondimensionalization.typ b/site/content/gsoc/nondimensionalization.typ new file mode 100644 index 0000000..aba870a --- /dev/null +++ b/site/content/gsoc/nondimensionalization.typ @@ -0,0 +1,36 @@ +#import "/common.typ": post + +#show: post.with( + title: "Nondimensionalization", + date: datetime(year: 2020, month: 6, day: 8), +) + +Here's a simple but interesting bit of math that came up while working on an implementation of Jakob and Hanika (2019) @Jakob2019. The reflectivity model they proposed is: +$ + R(lambda) = sigma(c_0 lambda^2 + c_1 lambda + c_2) +$ +where $c_0, c_1, c_2$ are model parameters, $lambda in [360 "nm", 830 "nm"]$ and $sigma: RR -> [0, 1]$ is a sigmoid function. Reflectivities tend to be rather smooth and the values of the polynomial not too large. If wavelengths are expressed numerically in nanometers, $c_1$ and especially $c_0$ will tend to be very small. This makes it harder to find minima of the function, as solvers tend get confused, unless special care is taken to change the default tolerances properly. + +A more straightforward solution is to rescale $lambda$ to a more convenient range, a process a physicist would call *nondimensionalization*. Following the authors of the article, let +$ + lambda' = (lambda - 360 "nm") / (830 "nm" - 360 "nm") +$ +so that $lambda' in [0, 1]$ (and no longer has a physical unit) and +$ + R(lambda') = sigma(c'_0 lambda'^2 + c'_1 lambda + c'_2) +$ +This makes solvers far more likely to converge, without having to fine tune their settings. + += Recovering the units + +The interesting part is that having found the primed parameters, one can recover the unprimed ones. Start by equating $R(lambda)$ and $R(lambda')$, +$ + sigma(c_0 lambda^2 + c_1 lambda + c_2) = sigma(c'_0 lambda'^2 + c'_1 lambda + c'_2) +$ +Because $sigma$ is invertible, its inverse can be applied to both sides of the equation, resulting in an equality between the arguments. Then, replace $lambda'$ with its definition and expand the right hand side, +$ + c_0 lambda^2 + c_1 lambda + c_2 = c'_0 / (220900 "nm"^2) lambda^2 + (-36 c'_0 / (11045 "nm") + c'_1 / (470 "nm")) lambda + 1296 / 2209 c'_0 - 36 / 47 c'_1 + c'_2 +$ +Because the equation must hold for all $lambda$, equate the two quadratic polynomials' coefficients to get the final answers. + +#bibliography("/refs.bib") diff --git a/site/content/gsoc/plan_june.typ b/site/content/gsoc/plan_june.typ new file mode 100644 index 0000000..0a7344b --- /dev/null +++ b/site/content/gsoc/plan_june.typ @@ -0,0 +1,18 @@ +#import "/common.typ": post + +#show: post.with( + title: "The plan for June", + date: datetime(year: 2020, month: 6, day: 1), +) + +This month I'll be working on implementing Jakob and Hanika (2019) @Jakob2019 in Colour's reflectance recovery submodule. Here are the goals to be met before the first evaluations start on June 24: + +- Write code that computes the spectrum for a single color +- Extend it to the entire sRGB gamut (using feedback to get a smooth map) +- Match the published tables +- Write code that samples the tables correctly (i.e. giving the same results as the reference code) +- Other color spaces + +The academic year here hasn't ended yet and I've still got some minor exams and projects left. I expect to be rather busy for a week or two, but hopefully things go smoothly and I won't get too overworked. At least there haven't been any pandemic-related delays. + +#bibliography("/refs.bib") diff --git a/site/content/gsoc/progress_late_june.typ b/site/content/gsoc/progress_late_june.typ new file mode 100644 index 0000000..808cd32 --- /dev/null +++ b/site/content/gsoc/progress_late_june.typ @@ -0,0 +1,24 @@ +#import "/common.typ": post + +#show: post.with( + title: "Late June progress update", + date: datetime(year: 2020, month: 6, day: 25), +) + +The first evaluation is just in a few days. Here's a quick summary of what I've done so far: + +- I wrote and tested a decent prototype of the implementation, in my #link("https://zittrig.eu/git/enneract/gsoc-prototypes/commits/master")[personal repository]. +- I #link("https://github.com/enneract/colour/tree/feature/jakob2019")[forked] the main #link("https://github.com/colour-science/colour")[repository] and began working on integrating my code into the codebase. + - I learned the basics of Sphinx and the NumPy docstring style, used in Colour. + - I learned how to use Poetry to run unit tests (using Nosetests), and how to write my own tests. + - I learned how to ensure a consistent code style using Flake8. + - Code is now well integrated into the Colour codebase, including relevant tests. Not everything is done yet, though. +- I opened a #link("https://github.com/colour-science/colour/pull/603")[pull request], and I'm hoping to get it merged into the development branch (and eventually into master) before moving on to working on the next major goal. + +What's left to do? +- Improve the optimization process. It still has convergence problems and fails for many colors. This is despite me having written code computing the exact gradient (using analytic differentiation). An investigation into new algorithms is needed. +- The code for looking up pre-computed tables isn't documented and has no unit tests. Because the tables are rather large (too big for inclusion in the main repository), testing can be challenging. An idea is to generate and use smaller tables for this purpose. +- The code for generating new tables is almost complete. +- There are still some minor stylistic issues to be discussed and fixed. + +At the moment I'm still working on the solver. Hopefully the next blog entry is titled "no convergence problems." diff --git a/site/content/gsoc/proposal.typ b/site/content/gsoc/proposal.typ new file mode 100644 index 0000000..467633b --- /dev/null +++ b/site/content/gsoc/proposal.typ @@ -0,0 +1,88 @@ +#import "/common.typ": post + +#show: post.with( + title: "New Spectral Upsampling Methods", + date: datetime(year: 2020, month: 5, day: 26), +) + +_This is the project proposal I wrote for Google Summer of Code 2020. The project can be found on their website #link("https://summerofcode.withgoogle.com/projects/#6675798750134272")[here]._ + +*New Spectral Upsampling Methods* +for Colour Science for Python, participating under NumFOCUS + += Personal information + +*Name:* Paweł Ryszard Redman \ +*Country of residence:* Poland \ +*University:* Wrocław University of Science and Technology (Polish: Politechnika Wrocławska) \ +(Contact details redacted) + +I am a first year student pursuing a Master’s degree in Optics with a specialization in Optical and Photonic Engineering at the Wrocław University of Science and Technology. I received my Engineer’s degree in February 2020 at the same university, in the same field and specialization. + +If selected, I will be willing and capable of committing to full-time work on this project during the summer. I am aware of the potential that the summer holidays start later this year due to the ongoing situation, but I have been keeping up with my studies despite classes being cancelled and I am confident that any such extraordinary situation would not affect my work. + += Abstract + +Traditional rendering is based on the idea that light can be decomposed into three basic components: red, green and blue (RGB). In some areas this is a vast oversimplification of the physical reality; light exists on a continuous spectrum of wavelengths. Spectral rendering achieves better accuracy by taking that into account. The major drawback is that it cannot use widely available color data for surfaces such as photographs. The process of gathering usable spectral data is significantly more time- and resource-intensive. + +Spectral upsampling is the process of extrapolating spectral information from RGB color data. This is possible because, in practice, spectral reflectivities, which describe how a surface interacts with incident light on a wavelength-by-wavelength basis, tend to fit relatively simple patterns. The goal would be to add support for several newly published algorithms to #link("https://github.com/colour-science/colour")[Colour], a color science library for Python. With the rising popularity of spectral rendering, the library would greatly benefit from the work. + += Technical Details + +Colour is a Python library, targeting both Python 2.7+ and Python 3.5+. Like many scientific projects, it relies on #link("https://www.scipy.org/")[SciPy] for array operations, numerical algorithms (most importantly function minimization) and plotting. There is a number other dependencies, but they would not be relevant to this project in particular. There is also an extensive list of #link("https://www.colour-science.org/contributing/")[rules and guidelines] for contributors regarding code style, documentation, unit testing and so on that I will have to follow in order to have my work accepted upstream. + +*How does spectral upsampling work?* + +Colour already has a submodule called colour.recovery, which deals with reflectance recovery. Currently two methods are implemented, Smith (1999) @Smits1999 and Meng et al. (2015) @Meng2015. Because I can use the existing code and tests as reference, integrating new code into the codebase will be relatively simple. The priority would be to extend the submodule by adding support for Otsu et al. (2018) @Otsu2018 and Jakob and Hanika (2019) @Jakob2019, with implementations of Mallett and Yuksel (2019) @Mallett2019 and Peters et al. (2019) @Peters2019 being secondary goals. A tertiary goal that I thought up myself and is not a part of the original #link("https://github.com/colour-science/GSoC/blob/master/2020/GSoC-2020-Project-Ideas.md#new-spectral-upsampling-methods")[project proposal], but was deemed a good idea by one of the mentors, is implementing the interpolation scheme described in Meng et al.’s article, which currently is missing from Colour. + +Given spectral distribution of the illumination, the process of converting the spectral reflectance of a surface to tristimulus values, which quantify the color perception of the surface, is straightforward. If the reflectance is represented as a vector with a suitable N-dimensional basis in the function space, the conversion is done by a simple linear map φ : ℝᴺ→ℝ³. Unless N = 3, which is rarely enough in real world scenarios, this mapping is non-invertible. (The original text contains an error and reads "invertible.") In other words, for each color, there are infinitely many reflectances, which are then called metamers. Worse still, two reflectances that produce exactly the same color under one illuminant can look very different under another. + +Spectral upsampling solves this underdetermined problem by imposing additional constraints. The specifics vary from method to method, but, as an example, several articles propose formulating this as a multivariate numerical optimization problem. A solver, such as SciPy’s SLSQP or #link("http://ceres-solver.org/")[Ceres], is tasked with finding a spectrum that matches the desired color as closely as possible, has a desired shape, varies smoothly between nearby colors, minimizes unwanted color shifts when changing illuminants and so on, all at the same time. This is usually very computationally expensive. All methods listed in this proposal also rely on performing all the heavy work ahead of time to achieve good performance in a spectral renderer. + +The most important part of an implementation is writing code that can exactly recreate published precomputed tables. Unit tests can be used to automatically test the validity and accuracy of the results. This is by far the most challenging task, as the methods can be rather contrived. The rest of the work is implementing interpolation, which is relatively simple, documenting the code and integrating it well into the existing codebase. + +== Schedule of Deliverables + +=== Community Bonding Period + +To get to know the community and the codebase, I will use this time to try and get a minor pull request merged in. Implementing the missing part of Meng, et. al. is one possibility, but I will have to discuss this topic in further detail. In the meantime I will set up a blog and start posting there, as per NumFOCUS’s requirements. + +=== Phase 1 + +Implement Jakob and Hanika (2019) @Jakob2019: + +- Write code that minimizes the authors’ cost function at a single point. +- Extend the solver to cover the entire sRGB gamut without discontinuities. Explore libraries other than SciPy if necessary (the authors used Ceres). +- Create coefficient maps as described in the article matching the published values. +- Write code that can sample the maps and compute the final spectral data. +- Extend the method to other color spaces. + +The optimistic time frame for completion of all the points above is 2 weeks. Looking at the article, the method seems straightforward enough to do this. Each sub-goal, excluding the third, should take a day or two at most. I know from experience that trying to exactly match published results tends to be a tedious process and I expect to spend most of the time doing that. + +Once the code is finished, it will have to go through review. I assume that a week should be enough to get the code polished enough to be worth merging in. Assuming no unexpected delays or problems, I should be left with a bit over a week before the first evaluation to work on the next goal. + +=== Phase 2 + +Implement Otsu et al. (2018) @Otsu2018. + +- Implement the principal component analysis (PCA) the authors used. +- Implement their clustering algorithm. +- Make sure the results match the published values. + +The time frame here is harder to estimate. The method looks considerably more complicated than the previous. The article is also slightly vague (especially about PCA) and analysis of the included source code could take a while. I have to be conservative and assume I will need the entire four weeks or so to get this done, including the review (which I expect to take less time than the first one). + +=== Final Week + +The last few weeks will be the time to consider other spectral upsampling methods, discussing the topic with the mentors and the community. The specifics will depend on how fast I complete the goals of the first two phases and what is decided on. The hard goal would be to fully implement at least one more method. For example, Mallet and Yuksel’s is similar to Otsu, et. al.’s and I would not expect to have problems adding support for it before the final week. + +By the final week, I expect to have created several good and reviewed commits, worthy of being merged into Colour. The final week will be the time to make sure everything is ready for the final evaluation. + += Development Experience + +I have about a decade of experience as an amateur C programmer. I published my first open source project, a modification of the first-person shooter Tremulous, on GitHub back in 2013. Since then I’ve worked on many projects, including some minor paid work as a freelancer. I showcase the more interesting ones on my webpage. I started using Python a few years ago as a replacement for MATLAB, but I’ve also embraced is as a general-purpose language, with it pretty much replacing C as my language of choice in most cases. While I could use experience working with large Python codebases, which is partly why I am applying for this project, I think I am more than qualified as far as programming skills go. + += Why this project? + +I’ve been interested in computational physics for a number of years now, so to me picking a NumFOCUS project was a natural choice. Expecting to find an interesting numerical problem to tackle in e.g. SciPy, I was surprised to find out that Colour Science was participating. As a student of Optics, having written his Engineer’s thesis on the topic of color rendering, I thought it was the perfect project to contribute to for someone with my background. Specifically, I have been considering spectral rendering as a potential topic for my upcoming Master’s thesis, so taking on this project would provide me a lot of valuable experience. + +#bibliography("/refs.bib") diff --git a/site/content/gsoc/prototypes_repo.typ b/site/content/gsoc/prototypes_repo.typ new file mode 100644 index 0000000..d892158 --- /dev/null +++ b/site/content/gsoc/prototypes_repo.typ @@ -0,0 +1,15 @@ +#import "/common.typ": post, static, img + +#show: post.with( + title: "A repository for prototypes", + date: datetime(year: 2020, month: 6, day: 1), +) + +#figure( + img(static("gsoc/jakob_hanika_prototype.png")), + caption: [An example output from the script], +) + +I've just created a #link("https://zittrig.eu/git/enneract/gsoc-prototypes")[Git repository for prototypes]. It's a place for experiments and early code that's not yet ready for inclusion in Colour. + +The first script there successfully finds the coefficients described in Jakob and Hanika for a few test colors. It doesn't work too well yet. There are convergence problems with methods that find local minima and I had to resort to using a (very slow) global optimizer. I'll talk about these issues in more detail in a future post, after working on the feedback mechanism proposed in the article. diff --git a/site/content/gsoc/scipy_minimize_value_threshold.typ b/site/content/gsoc/scipy_minimize_value_threshold.typ new file mode 100644 index 0000000..c133827 --- /dev/null +++ b/site/content/gsoc/scipy_minimize_value_threshold.typ @@ -0,0 +1,33 @@ +#import "/common.typ": post + +#show: post.with( + title: "Function value threshold in SciPy's minimize", + date: datetime(year: 2020, month: 7, day: 13), +) + +I was minimizing a color difference function using `scipy.optimize.minimize`. The problem I had was that the solver wouldn't stop until the *gradient* (the rate of change) was small enough. This is sensible in most applications, but for me this meant wasting time refining the solution until the color difference was orders of magnitude smaller than any perceptible error. In practice most iterations were unnecessary and avoiding them would save a lot of time. + +So how does one tell SciPy to finish as soon as the function *value* is small enough? Here's what I've come up with: +``` +class StopMinimizationEarly(Exception): + def __init__(self, x, fun): + self.x = x + self.fun = fun + +def error_function(x, ...): + ... + if fun <= acceptable_fun: + raise StopMinimizationEarly(x, fun) + return fun +``` + +Simply throw an exception in the minimized function and then catch it like this: +``` +try: + result = scipy.optimize.minimize(error_function, ...) + x, fun = result.x, result.fun +except StopMinimizationEarly as error: + x, fun = error.x, error.fun +``` + +As far as I know, you can't directly tell SciPy to stop once a function value threshold is reached. This could be a decent idea for a feature request, but I'm not sure how many people would actually ever need this. For the time being, this is the best approach to the problem, as far as I know. diff --git a/site/refs.bib b/site/refs.bib index 8d79bfc..5949863 100644 --- a/site/refs.bib +++ b/site/refs.bib @@ -29,3 +29,36 @@ publisher = {The Eurographics Association}, doi = {10.2312/SR.20191216}, } + +@article{Smits1999, + author = {Brian Smits}, + title = {An {RGB}-to-Spectrum Conversion for Reflectances}, + journal = {Journal of Graphics Tools}, + volume = {4}, + number = {4}, + pages = {11--22}, + year = {1999}, + doi = {10.1080/10867651.1999.10487511}, +} + +@article{Meng2015, + author = {Johannes Meng and Florian Simon and Johannes Hanika and Carsten Dachsbacher}, + title = {Physically Meaningful Rendering using Tristimulus Colours}, + journal = {Computer Graphics Forum}, + volume = {34}, + number = {4}, + pages = {31--40}, + year = {2015}, + doi = {10.1111/cgf.12676}, +} + +@article{Peters2019, + author = {Christoph Peters and Sebastian Merzbach and Johannes Hanika and Carsten Dachsbacher}, + title = {Using Moments to Represent Bounded Signals for Spectral Rendering}, + journal = {ACM Transactions on Graphics}, + volume = {38}, + number = {4}, + pages = {136:1--136:14}, + year = {2019}, + doi = {10.1145/3306346.3322964}, +} |
