-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: mbw <[email protected]>
- Loading branch information
Showing
9 changed files
with
601 additions
and
110 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,29 @@ | ||
FROM python:3.9 | ||
WORKDIR /code | ||
FROM python:3.11-buster as builder | ||
|
||
RUN pip install poetry==1.8.1 | ||
|
||
ENV POETRY_NO_INTERACTION=1 \ | ||
POETRY_VIRTUALENVS_IN_PROJECT=1 \ | ||
POETRY_VIRTUALENVS_CREATE=1 \ | ||
POETRY_CACHE_DIR=/tmp/poetry_cache | ||
|
||
WORKDIR /app | ||
|
||
COPY pyproject.toml poetry.lock ./ | ||
RUN touch README.md | ||
|
||
COPY ./requirements.txt /code/requirements.txt | ||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt | ||
COPY ./aprox.txt /code | ||
RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --no-root | ||
|
||
FROM python:3.11-slim-buster as runtime | ||
|
||
ENV VIRTUAL_ENV=/app/.venv \ | ||
PATH="/app/.venv/bin:$PATH" | ||
|
||
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV} | ||
WORKDIR /code | ||
COPY ./abridged-cie-cmf.txt /code | ||
COPY ./cie-cmf.txt /code | ||
COPY ./colour_system.py /code | ||
COPY ./main.py /code | ||
|
||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"] | ||
CMD ["hypercorn", "main:app", "--bind", "0.0.0.0:3000"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,116 +1,124 @@ | ||
# | ||
# | ||
# colour_system.py | ||
# https://scipython.com/blog/converting-a-spectrum-to-a-colour/ | ||
|
||
import numpy as np | ||
|
||
def xyz_from_xy(x, y): | ||
"""Return the vector (x, y, 1-x-y).""" | ||
return np.array((x, y, 1-x-y)) | ||
from enum import Enum | ||
|
||
class ColourSystem: | ||
"""A class representing a colour system. | ||
|
||
A colour system defined by the CIE x, y and z=1-x-y coordinates of | ||
its three primary illuminants and its "white point". | ||
class MatchSpace(Enum): | ||
FULL = 1 | ||
ABRIDGED = 2 | ||
|
||
TODO: Implement gamma correction | ||
|
||
""" | ||
# Return the vector (x, y, 1-x-y). | ||
def xyz_from_xy(x, y): | ||
|
||
# The CIE colour matching function for 380 - 780 nm in 5 nm intervals | ||
cmf = np.loadtxt('aprox.txt', usecols=(1,2,3)) | ||
return np.array((x, y, 1 - x - y)) | ||
|
||
def __init__(self, red, green, blue, white): | ||
"""Initialise the ColourSystem object. | ||
|
||
Pass vectors (ie NumPy arrays of shape (3,)) for each of the | ||
red, green, blue chromaticities and the white illuminant | ||
defining the colour system. | ||
# A colour system defined by the CIE x, y and z=1-x-y coordinates of | ||
# its three primary illuminants and its "white point". | ||
class ColourSystem: | ||
|
||
""" | ||
# Pass vectors (ie NumPy arrays of shape (3,)) for each of the | ||
# red, green, blue chromaticities and the white illuminant | ||
# defining the colour system. | ||
def __init__(self, red, green, blue, white, matchSpace: MatchSpace): | ||
# The CIE colour matching function for 380 - 780 nm in 5 nm intervals | ||
if matchSpace == MatchSpace.FULL: | ||
self.cmf = np.loadtxt("cie-cmf.txt", usecols=(1, 2, 3)) | ||
else: | ||
self.cmf = np.loadtxt("abridged-cie-cmf.txt", usecols=(1, 2, 3)) | ||
|
||
# Chromaticities | ||
self.red, self.green, self.blue = red, green, blue | ||
self.white = white | ||
|
||
# The chromaticity matrix (rgb -> xyz) and its inverse | ||
self.M = np.vstack((self.red, self.green, self.blue)).T | ||
self.M = np.vstack((self.red, self.green, self.blue)).T | ||
self.MI = np.linalg.inv(self.M) | ||
|
||
# White scaling array | ||
self.wscale = self.MI.dot(self.white) | ||
|
||
# xyz -> rgb transformation matrix | ||
self.T = self.MI / self.wscale[:, np.newaxis] | ||
|
||
# Transform from xyz to rgb representation of colour. | ||
# | ||
# The output rgb components are normalized on their maximum | ||
# value. If xyz is out the rgb gamut, it is desaturated until it | ||
# comes into gamut. | ||
# | ||
# By default, fractional rgb components are returned; if | ||
# out_fmt='html', the HTML hex string '#rrggbb' is returned. | ||
def xyz_to_rgb(self, xyz, out_fmt=None): | ||
"""Transform from xyz to rgb representation of colour. | ||
The output rgb components are normalized on their maximum | ||
value. If xyz is out the rgb gamut, it is desaturated until it | ||
comes into gamut. | ||
By default, fractional rgb components are returned; if | ||
out_fmt='html', the HTML hex string '#rrggbb' is returned. | ||
""" | ||
|
||
rgb = self.T.dot(xyz) | ||
if np.any(rgb < 0): | ||
# We're not in the RGB gamut: approximate by desaturating | ||
w = - np.min(rgb) | ||
w = -np.min(rgb) | ||
rgb += w | ||
if not np.all(rgb==0): | ||
if not np.all(rgb == 0): | ||
# Normalize the rgb vector | ||
rgb /= np.max(rgb) | ||
|
||
if out_fmt == 'html': | ||
if out_fmt == "html": | ||
return self.rgb_to_hex(rgb) | ||
return rgb | ||
|
||
# Convert from fractional rgb values to HTML-style hex string. | ||
def rgb_to_hex(self, rgb): | ||
"""Convert from fractional rgb values to HTML-style hex string.""" | ||
|
||
hex_rgb = (255 * rgb).astype(int) | ||
return '#{:02x}{:02x}{:02x}'.format(*hex_rgb) | ||
return "#{:02x}{:02x}{:02x}".format(*hex_rgb) | ||
|
||
# Convert a spectrum to an xyz point. | ||
# The spectrum must be on the same grid of points as the colour-matching | ||
# function, self.cmf: 380-780 nm in 5 nm steps. | ||
def spec_to_xyz(self, spec): | ||
"""Convert a spectrum to an xyz point. | ||
The spectrum must be on the same grid of points as the colour-matching | ||
function, self.cmf: 380-780 nm in 5 nm steps. | ||
""" | ||
XYZ = np.sum(spec[:, np.newaxis] * self.cmf, axis=0) | ||
den = np.sum(XYZ) | ||
if den == 0.: | ||
if den == 0.0: | ||
return XYZ | ||
return XYZ / den | ||
|
||
# Convert a spectrum to an rgb value. | ||
def spec_to_rgb(self, spec, out_fmt=None): | ||
"""Convert a spectrum to an rgb value.""" | ||
|
||
xyz = self.spec_to_xyz(spec) | ||
return self.xyz_to_rgb(xyz, out_fmt) | ||
|
||
|
||
illuminant_D65 = xyz_from_xy(0.3127, 0.3291) | ||
cs_hdtv = ColourSystem(red=xyz_from_xy(0.67, 0.33), | ||
green=xyz_from_xy(0.21, 0.71), | ||
blue=xyz_from_xy(0.15, 0.06), | ||
white=illuminant_D65) | ||
|
||
cs_smpte = ColourSystem(red=xyz_from_xy(0.63, 0.34), | ||
green=xyz_from_xy(0.31, 0.595), | ||
blue=xyz_from_xy(0.155, 0.070), | ||
white=illuminant_D65) | ||
|
||
cs_srgb = ColourSystem(red=xyz_from_xy(0.64, 0.33), | ||
green=xyz_from_xy(0.30, 0.60), | ||
blue=xyz_from_xy(0.15, 0.06), | ||
white=illuminant_D65) | ||
|
||
spec = np.array([ 60. ,37. ,33. ,56. ,4. ,4. ,4., 3. ]) | ||
|
||
|
||
print(cs_hdtv.spec_to_rgb(spec, out_fmt='html')) | ||
print(cs_srgb.spec_to_rgb(spec, out_fmt='html')) | ||
print(cs_smpte.spec_to_rgb(spec, out_fmt='html')) | ||
@classmethod | ||
def HDTV(cls, matchSpace: MatchSpace): | ||
illuminant_D65 = xyz_from_xy(0.3127, 0.3291) | ||
return cls( | ||
red=xyz_from_xy(0.67, 0.33), | ||
green=xyz_from_xy(0.21, 0.71), | ||
blue=xyz_from_xy(0.15, 0.06), | ||
white=illuminant_D65, | ||
matchSpace=matchSpace, | ||
) | ||
|
||
@classmethod | ||
def SMPTE(cls, matchSpace: MatchSpace): | ||
illuminant_D65 = xyz_from_xy(0.3127, 0.3291) | ||
return cls( | ||
red=xyz_from_xy(0.67, 0.33), | ||
green=xyz_from_xy(0.21, 0.71), | ||
blue=xyz_from_xy(0.15, 0.06), | ||
white=illuminant_D65, | ||
matchSpace=matchSpace, | ||
) | ||
|
||
@classmethod | ||
def SRGB(cls, matchSpace: MatchSpace): | ||
illuminant_D65 = xyz_from_xy(0.3127, 0.3291) | ||
return cls( | ||
red=xyz_from_xy(0.64, 0.33), | ||
green=xyz_from_xy(0.30, 0.60), | ||
blue=xyz_from_xy(0.15, 0.06), | ||
white=illuminant_D65, | ||
matchSpace=matchSpace, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,34 +1,82 @@ | ||
# SPDX Apache:2.0 | ||
|
||
from pydantic import BaseModel | ||
from fastapi import FastAPI | ||
from colour_system import * | ||
import numpy as np | ||
from scipy.constants import h, c, k | ||
|
||
app = FastAPI() | ||
|
||
|
||
# Spretrum and RGB are the two objects pased as JSON into and | ||
# out of the REST API | ||
# Use pydantic to handle these by extending from BaseModel | ||
class Spectrum(BaseModel): | ||
freq: list = [] | ||
|
||
|
||
class RGB(BaseModel): | ||
hdtv_hex: str | ||
srgb_hex: str | ||
hdtv_rgb: list = [] | ||
srgb_rgb: list = [] | ||
|
||
|
||
## Utility to convert text HEX values to RGB int values | ||
def hex_to_rgb(hexa): | ||
hexa = hexa.lstrip('#') | ||
hexa = hexa.lstrip("#") | ||
rgb = [] | ||
for i in (0,2,4): | ||
rgb.append(int(hexa[i:i+2], 16)) | ||
for i in (0, 2, 4): | ||
rgb.append(int(hexa[i : i + 2], 16)) | ||
return rgb | ||
|
||
|
||
# Returns the spectral radiance, B(lam, T), in W.sr-1.m-2 of a black body | ||
# at temperature T (in K) at a wavelength lam (in nm), using Planck's law. | ||
# | ||
# Algorthm here is clear by inspection so no need for comments :-) | ||
def planck(lam, T): | ||
lam_m = lam / 1.0e9 | ||
fac = h * c / lam_m / k / T | ||
B = 2 * h * c**2 / lam_m**5 / (np.exp(fac) - 1) | ||
return B | ||
|
||
|
||
@app.post("/spectrum/") | ||
def read_item(spec: Spectrum) -> RGB: | ||
spec = np.array(spec.freq) | ||
print(spec) | ||
hdtv = cs_hdtv.spec_to_rgb(spec, out_fmt='html') | ||
srgb = cs_srgb.spec_to_rgb(spec, out_fmt='html') | ||
|
||
r = RGB(hdtv_hex=hdtv, srgb_hex=srgb, hdtv_rgb=hex_to_rgb(hdtv),srgb_rgb=hex_to_rgb(srgb)) | ||
|
||
# use the abridge set of colour frequencies here | ||
hdtv = ColourSystem.HDTV(MatchSpace.ABRIDGED).spec_to_rgb(spec, out_fmt="html") | ||
srgb = ColourSystem.SRGB(MatchSpace.ABRIDGED).spec_to_rgb(spec, out_fmt="html") | ||
|
||
r = RGB( | ||
hdtv_hex=hdtv, | ||
srgb_hex=srgb, | ||
hdtv_rgb=hex_to_rgb(hdtv), | ||
srgb_rgb=hex_to_rgb(srgb), | ||
) | ||
print(r) | ||
return r | ||
|
||
|
||
@app.post("/temperature/") | ||
def colour_temp(temp: int) -> RGB: | ||
|
||
# full range of frequencies here | ||
lam = np.arange(380.0, 781.0, 5) | ||
spec = planck(lam, temp) | ||
|
||
hdtv = ColourSystem.HDTV(MatchSpace.FULL).spec_to_rgb(spec, out_fmt="html") | ||
srgb = ColourSystem.SRGB(MatchSpace.FULL).spec_to_rgb(spec, out_fmt="html") | ||
|
||
r = RGB( | ||
hdtv_hex=hdtv, | ||
srgb_hex=srgb, | ||
hdtv_rgb=hex_to_rgb(hdtv), | ||
srgb_rgb=hex_to_rgb(srgb), | ||
) | ||
print(r) | ||
return r | ||
return r |
Oops, something went wrong.