Tool for digitizing an LED spectrum from an image

I created an LED spectrum digitizing tool on my website because I got tired of straining my eyes.

The issue is that manufacturers often publish 3 to 5 spectra for a single LED series on one graph, which is not always convenient. My area of expertise is designing LED modules and lamps for horticultural lighting.

Sometimes I need to check exactly what portion of a white LED’s spectrum falls within the 650-670 nm range. Since broad-spectrum LEDs with a high color rendering index have become more affordable and more options are now available, the task at hand is designing a new generation of LED modules.

Here, for example, is the spectrum from the datasheet of the Bridgelux Thrive LED series.

The tool runs on Python, using the OpenCV library, with NumPy serving as the mathematical core. Digitization results in a data array comprising wavelength and intensity level.

The data array enables the mathematical calculation of the color rendering index and the division of the spectrum into distinct bands for ePAR analysis (my primary specialization). I have also added the feature to calculate CQS and TM-30 metrics.

This is highly valuable because LED bulb manufacturers typically specify the color rendering index based on the outdated CRI standard. CRI is fundamentally unsuited for evaluating LED light sources; it allows manufacturers to tailor the phosphor blend to achieve a high CRI score, whereas in reality, the rendered colors will be visibly distorted. This specific problem is effectively resolved by using the CQS and TM-30 evaluation methods.

The Duv parameter calculation is imprecise because the spectrogram image is too blurry.

Here is what I think would be useful for the forum:

  • You can upload and digitize the spectra of various LEDs from different manufacturers, overlay them, and compare them directly on the graph.
  • When you have a spectrum graph but lack specific numerical data (such as color temperature or color rendering index), you can obtain these metrics by digitizing the graph.
  • You can overlay the actual solar spectrum onto the digitized LED spectrum.
  • You can overlay the McCree curve onto the digitized spectrum (this is also from horticulture, a photosynthesis action spectrum, so perhaps not for the forum :slight_smile: ).

Here is an example comparing the spectrum of a 4000K SunLike LED with the solar spectrum. The AM1.5G standard with a 1 nm step was used to plot the solar spectrum. I have strived to achieve maximum accuracy—at least to the extent permitted by the inherent limitations of this format.
After digitization, you can download a screenshot of the spectrum or the data array in JSON or CSV format.

Below is an example comparing the spectra of a 4000K SunLike LED and a 5000K Bridgelux Thrive LED alongside the solar spectrum.

Please share your feedback and criticism. Feel free to suggest features to add or point out anything that isn’t working well. Let me know if you find this tool useful at all. The idea came to me when I was analyzing spectrograms yet again and straining my eyes. I figured it might be helpful to others, though I’m honestly quite unsure if there’s any real demand for it.

I apologize in advance, but to prevent DDoS attacks and ensure security, the tool is only accessible to registered users. You can log in using your Google account or via standard registration. Please note that Facebook login is currently unavailable.

Thank you all!

8 Thanks

It’s a shame everyone is staying silent; I was hoping for at least some kind of comment.

I applaud your efforts, but this is simply not something that I would use… So obviously, I had nothing to comment.
But thanks for posting about your work!

1 Thank

Alright, thanks for that anyway.

Sorry, I meant to try this out but things got busy. I’m actually very grateful that you made this tool, because I regularly experiment with various spectra to see which features influence which readings.

A few suggestions:

  • When choosing the origin and axis endpoints, the mouse skips around too much and is unable to accurately select a point on the axis. I would consider increasing the magnification factor in the neighborhood around the cursor, so that one can select calibration points more accurately.

  • Color matching is very difficult. Some plots have curves that are 1-2 pixels thick, making it near-impossible to select a color directly from the curve. I also tried to select a color by hand from the palette, and it doesn’t seem to have much tolerance–I can select a color that is visually indistinguishable from black, but still get no plot at all. I would suggest making the color picker more tolerant in the following sense: once a color is picked, the image processor registers any color within a neighborhood of the selected color.

It looks like the “color tolerance” parameter is intended to address this; I tried to increase it to 100 with no success.

Another solution I propose (which I think is better than the previous idea) is to let the user select a small box containing a piece of the plot; the image processor then chooses the darkest pixel color within that box, which should agree with the color of the plot.

  • Sunlight isn’t the correct reference spectrum for low-CCT sources–anything below 5000K actually uses the blackbody radiator of equal temperature. I suggest the following sequence of events: first compute the CCT of the plot, then figure out the right reference spectrum (blackbody or daylight) of equal CCT, and then plot it with a suitable normalization (likely equal-lumens normalization).

  • Include R1-R15 values in the generated report. R9 and R12 are two of the most useful metrics for diagnosing irregularities in an LED spectrum, so excluding them severely limits the usefulness of the report.

  • Remove the 15-second anti-DDoS wait for registered users.

3 Thanks

Thank you for your interest, and especially for the detailed response!

  1. When calibrating the axes and selecting the plot colour, doesn’t a “magnifying glass” appear on the screen to make it easier to select the points? (I’ve attached a screenshot

    )
    I checked it in Safari, Chrome, and DuckDuckGo, and it works in all of them.
    The idea of selecting the spectrum colour by defining an arbitrary area on the screen is not a bad one. I’ll think about it.

  2. The “color tolerance” setting is correct — this mode expands the acceptable color range and often helps when the digitization starts following an incorrect path, for example when a single graph contains spectra for several LEDs plotted in different colors.

For black plots, I have implemented a filter that ignores the graph’s grid lines. I didn’t encounter any problems with black plots during my testing, but I’ll test a few more variations.

  1. I really like the idea of using the blackbody temperature! But that will just give me an excuse to add another checkbox.
    As for adding R1–R15 — thank you! I don’t know why I didn’t think of that before. The Python library already calculates these values “under the hood”; I just need to expose them in the output. In the CQS standard, these are VS1–VS15. I’ve already done that; the function works.

  2. Unfortunately, I can’t remove the delay, as that would immediately create two potential vectors of vulnerability. My server only has 2 CPU cores and 4 GB of RAM. That’s enough to run the website and the tool smoothly, but far too little to handle, for example, 100 people using the tool simultaneously (although there is no real-world demand anywhere near that level).

I’ll think about it. Perhaps I’ll reduce the delay to 10 seconds.

Thanks again for the help in creating the tool.

1 Thank

I don’t know if you already do this, but if based solely on the image, could you cross-reference the camera model/metadata from some database as to get the most accurate info/baseline?

1 Thank

I didn’t quite understand your question. Please clarify what you mean.

each sensor/camera system has a “reference” from which the specs or autosettings are drawn from…For example, a smartphone might be able to take RAW photos, of which you can glean the AWB, CCT, Saturation, etc. values from to have your program estimate a deviation. I’m not saying I know how to do this, it’s more of a suggestion so you can do the heavy lifting and figure it out, lol.

Because each phone is different, we can’t rely on the image on the screen…but the file should be quite the “same” across any given device.

This is about getting values from manufacturer provided datasheets, not about analyzing photos of an illuminated surface.

3 Thanks

talk about completely missing the mark, lol…my bad

1 Thank

Thank you so much for the thoughtful response! I’ll write in more detail later but for now just wanted to address Point 1.

I do get a magnifying glass, but it looks very different from the one in your plot:

In particular, there are way fewer pixels within the disk, and the curve is only 1 pixel thin at the darkest part. Here’s the plot I used; see if you can reproduce the issue.

(Additionally, the selection of the 3 calibrating points is not quite exact. I’m not sure how much this affects the accuracy of the digitized spectrum; perhaps it’s not significant enough to worth the effort to fix.)

1 Thank

I switched to Chrome to try again–same issue, the magnified plot is still too thin. Also, this time it looks like some gridlines between 350-400nm are picked up as part of the plot:

It looks like the above example somehow slipped through–it’s difficult when both the plot and gridlines are black. Since gridlines are thinner than the plot, I wonder if it’s possible to determine whether a pixel is a grid-point or a plot-point by computing the average brightness in a (say, 5x5 pixel) neighborhood. This average should be noticeably darker for a plot-point, compared to a pixel-point. I’m not well-versed in computer vision, maybe there already exist techniques to handle this sort of difficulty.

I was worried that this suggestion introduces too much new computation, but happy to hear that it’s already done and just not showing.

Understood! I missed that you’re actually hosting rather than outsourcing the computation.

1 Thank
  1. I’m not sure yet why your zoom looks different.

  2. My black plot is digitized correctly. I’ve improved the algorithm for gray spectra, which is the most challenging case. I’ve attached a screenshot. The digitization of black plots has also been improved; black-and-white plots now use a different algorithm.

  3. I’ve fixed a bug with navigating through reports containing digitized spectra.

  4. I’ve added the option to select the color of the desired plot by selecting an area on the screen. It works well with bright colors; for black and gray plots, the eyedropper works better.

  5. Reduced the delay between digitizations to 10 seconds.

  6. I’m planning to add a comparison with blackbody radiation. I really like this idea.

If possible, please test the digitization again and let me know how it performs.

1 Thank

Fantastic updates!

I really wish some more folks could test this out. Another issue has come up, which is that the location of the zoom sometimes doesn’t correspond to what the magnified disk actually displays:


At the location of the cursor there’s absolutely nothing on the plot, yet the magnifying glass indicates a portion of the plot.

Maybe there is some systematic issue with how the magnifying is done.

Thank you very much for implementing this! The box selection works perfectly on my end, even for the black plot. Also I noticed that auto-calibration is more precise than what I can manage by hand.

A somewhat unrelated question, out of curiosity: how do you handle multiple plots on the same axes? Sometimes one plot can be partially covered by another, as your own example demonstrates:


How does the digitizer trace the yellow plot to the left of this section?

1 Thank

When multiple lines overlap and the original color is completely lost, turning into gray or black, the algorithm needs to continuously calculate the “color distance” between each pixel and the reference color in order to follow the line. At the point where the lines merge, this distance suddenly exceeds the allowed threshold (color_tolerance), and the algorithm recognizes: “Signal lost.”

As soon as the signal is lost, the algorithm switches to an inertia mode. Instead of searching for a similar color across the entire screen, it applies a spatial penalty. This is a mathematical rule that essentially says: “If you can’t see your color, stay on your current trajectory.”

The algorithm uses “momentum” to remember the direction and speed at which the plot was moving before the lines merged. While inside the merged region, it simply projects a “virtual” line through the color clutter, ignoring everything around it.

The algorithm continues scanning the columns step by step. Suppose that after 30 columns the plots separate again, and the gray color we are looking for emerges from the clutter. The algorithm detects a pixel with an ideal color match. However, it must first check whether this pixel is at a reasonable distance from the predicted trajectory. If it is, the spatial penalty is sharply reduced (a kind of magnetic attraction effect), and the tracker “snaps” back onto the detected line.

The algorithm also includes a check for cases where it has been flying blind for the last 30 columns and may have accumulated some noise or drifted slightly away from the actual curve. It takes:

  • Point A — the last reliable point before the lines merged
  • Point B — the first reliable point after the lines separated again

It then draws a perfectly smooth interpolation line between them. All the noisy and corrupted pixels recorded while the algorithm was inside the merged region are discarded and replaced with this clean mathematical curve.

The success of digitization in complex areas is based on a balance between:

  1. Color strictness: Don’t trust noisy pixels in the merged region.
  2. Inertia: Don’t jump around when the line is lost; continue along the current trajectory.
  3. Retrospective correction: Once the line is detected again, go back and smooth the gap.

Which browser and operating system are you using? I agree! At least five people need to test the tool.

Last updated:

  1. I’ve added the ability to set the start and end points of the spectrogram in nanometers, so you can crop out the tails caused by inaccurate spectrograms in datasheets, where the graph doesn’t actually start at 0 on the X-axis but instead has an empty area before the plotted spectrum begins.

  2. A checkbox has been added to enable the projection of a blackbody spectrum. Its color temperature corresponds to the temperature of the first digitized plot, or to the currently active plot if multiple spectra have been digitized.

I checked a set of spectrograms from various screenshots. It works well.

If you don’t mind, please rate the tool from 1 to 5 stars and leave a review at the bottom of the article.
I look forward to further suggestions for improving the tool, as well as bug reports if any are found.
We also still need to figure out what’s wrong with the zoom.

1 Thank

Thank you very much for this detailed explanation of the curve-tracking algorithm! Staying in the tangential trajectory and interpolating between known points both sound like great ideas. I imagine that the “momentum” might be thrown off if the spectrum curve is very jagged like Brownian motion (or the solar spectrum with Fraunhofer lines).

What does the interpolation look like–is it simply linear or does it take more information into account at/near the two known/reliable points, or maybe even try to track another curve that covers the target?

I’ve tried both Opera and Chrome, on a PC running Windows 11 Pro. In any case, I’m quite happy with the box selection method and won’t complain about a bug in manual selection.

Excellent! A quick question: how are you normalizing that blackbody spectrum? In the below plot, the blackbody curve evidently has both more power and more brightness than the recovered curve, suggesting that the normalization isn’t quite right.

Additionally: I think it would be nice to allow the user to change the aspect ratio of the plot–in particular, to make it flatter to match the aspect ratio usually seen in datasheets and color reports.

I see that they have been added as VS values 1-15! For some reason it seems to be giving estimates that are too optimistic. I tried to recover this spectrum:

and got a sensible looking plot, but the R12 of 97.6 is significantly higher than the ~83 displayed in the actual color report.

The CRI estimate also ended up on the high side (96.8 rather than 94), but the other attributes (CCT, Duv, Rf, Rg) are very accurate.

I think it would be most sensible to leave a review after most of the bugs are ironed out, since that way my review would be representative of a future user’s experience.

2 Thanks

Got it. Unfortunately, I don’t have access to Windows, so I’ll try asking someone else to test it. Are you still having issues with the zoom?

  1. You are absolutely right here! I normalized it based on the graph’s scale, but that was not the correct approach. I’ve fixed it — the normalization is now based on the area.

The algorithm now takes the digitized LED spectrum and calculates its area within the visible wavelength range. Physically, this area represents the total emitted energy within that wavelength range. The algorithm then calculates the same area for the generated blackbody spectrum.

  1. My digitization of this graph yields VS12 = 89.4.
    Here is the most interesting part: colorimetry calculations are performed using the Python library Colour Science. There is nothing I can do to influence this; the result depends entirely on the accuracy of the digitization. There are two main sources of error:

А. Spectrograms in datasheets can be distorted. The line width can be quite large, and the algorithm targets the center of the line. The spectrogram in the datasheet may also have been slightly smoothed by the person who compiled the document; even a minor distortion can lead to a rather significant error.

В. This point also relates to your question about the algorithm’s logic. For short gaps of up to 40 pixels, strict linear interpolation is used. For gaps larger than 40 pixels, zero interpolation is applied. However, once the algorithm finishes scanning the spectrum, a Savitzky-Golay filter is applied. This filter takes a “window” of adjacent points and fits them to a parabola using the method of least squares. Consequently, this can also introduce a certain margin of error, although it is not too large.

Overall, the digitization of the graph appears correct, and the Colour Science library calculates very accurately. I have added an option to auto-snap the spectrogram to the X-axis; this can sometimes help if the curve is “floating in the air” (hovering above the baseline). However, with your specific example, this does not really change much.

Yes, sure!

2 Thanks

Still the same zoom issue, though I’m very happy with how reliably the box selection works.

Area is not the correct normalization–it’s much more useful to normalize instead by total brightness (same lumens), or equivalently, area weighed by the luminosity function (relative sensitivity of the human eye as a function of wavelength).

To see this, imagine perturbing a spectrum by adding a huge amount of power near 400nm and 700nm. Since these wavelengths are barely visible to the human eye, they should not substantially change the distance between the test and reference spectra for the purposes of color measurement. Normalizing by area allows the added mass at 400/700 to significantly distort the plot, while normalizing by brightness mitigates this issue by assigning very little brightness to these tails.

Here’s a plot from within the last half an hour–you can see the huge offset between the test and reference spectra near the middle (peak sensitivity) of the visible spectrum. The mass of the reference spectrum beyond 700nm contributes a ton to area, but essentially nothing to brightness/color.

Understood! It looks like uncontrollable factors can cause the digitization of the same plot to yield slightly different results on different runs. It might be interesting to implement a feature that overlays the recovered plot on top of the submitted image, which lets the user decide whether the recovery is accurate enough that the color calculations can be trusted.

Very nice! Using a parabola as the model should allow the algorithm to capture more nuanced features like convexity, which a linear interpolation would not do. I gave it a particularly difficult spectrum, and the recovery was as good as one could reasonably hope for, picking up all the essential features. A human may infer that the spikes around 610nm and 630nm are actually 2 spikes in close proximity, but the digitizer doesn’t know that from the image alone.

1 Thank
  1. I’ve done some work and improved the tool. The normalization of the blackbody radiation spectrum has been updated. Screenshot below.

That’s a great idea! Now a blue line appears on the image, following the plot that has been digitized. I’ve implemented this and also added the ability to manually adjust the spectrum after the initial digitization.

I tried digitizing your image, which has so much overlap that even I have a hard time telling where the peaks are for some of the spectra. Below are two screenshots of the digitization.


To obtain accurate results, I manually corrected the spectrum. The pencil tool allows you to draw the plot in sections, while the second mode lets you drag the spectrum points — specifically, the points of the blue line after the initial digitization.

Manual adjustment clearly contributes to improving the accuracy and quality of the spectrum digitization tool! Thank you for the help. Initially, I planned it as a simple tool for quick analysis, but now it’s obvious that it has almost turned into a professional-grade tool.

Is there anything else that needs to be improved?

1 Thank