pycoupole 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycoupole/__init__.py +13 -0
- pycoupole/brdf.py +206 -0
- pycoupole/core/_functions.py +373 -0
- pycoupole/core/_kernels.py +55 -0
- pycoupole/core/_utils.py +13 -0
- pycoupole/io.py +104 -0
- pycoupole-0.1.0.dist-info/METADATA +91 -0
- pycoupole-0.1.0.dist-info/RECORD +11 -0
- pycoupole-0.1.0.dist-info/WHEEL +5 -0
- pycoupole-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
- pycoupole-0.1.0.dist-info/top_level.txt +1 -0
pycoupole/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Import the two end-user functions used for BRDF and uncertainties
|
|
2
|
+
from .brdf import computeBRDFDeterministic, computeBRDFDistribution
|
|
3
|
+
|
|
4
|
+
__all__ = ["computeBRDFDeterministic", "computeBRDFDistribution",
|
|
5
|
+
"computeBRDFDeterministicFromCSV", "computeBRDFDistributionFromCSV",]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Dyanmic version based on metadata (obtained from repo tag)
|
|
9
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
10
|
+
try:
|
|
11
|
+
__version__ = version("pycoupole")
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
__version__ = "?.?.?"
|
pycoupole/brdf.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import numpy as np, warp as wp
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from .core._utils import _pickDevice, _vec2ui, _vec3d
|
|
5
|
+
from .core._kernels import _sampleBRDF, _sampleBRDFDistribution
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def computeBRDFDeterministic(
|
|
9
|
+
pixelID, channelID, pixelValueLSB, exposureTimeS,
|
|
10
|
+
lightCenter, lightNormal, triangleCenter, triangleNormal,
|
|
11
|
+
device=None
|
|
12
|
+
):
|
|
13
|
+
"""Compute the deterministic (point-estimate) BRDF for a single pixel-channel sample.
|
|
14
|
+
|
|
15
|
+
Evaluates the measurement equation once with all uncertainty sources
|
|
16
|
+
disabled, i.e. every input held at its nominal (mean) value. This is the
|
|
17
|
+
central value around which `computeBRDFDistribution` propagates uncertainties.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
pixelID : tuple of int
|
|
22
|
+
Pixel coordinates (column, row) in the sensor frame.
|
|
23
|
+
channelID : int
|
|
24
|
+
Colour channel index (0, 1, 2 for R, G, B).
|
|
25
|
+
pixelValueLSB : int
|
|
26
|
+
Raw pixel value in least-significant-bit units (digital number).
|
|
27
|
+
exposureTimeS : float
|
|
28
|
+
Exposure time of the acquisition, in seconds.
|
|
29
|
+
lightCenter, triangleCenter : array_like of float, shape (3,)
|
|
30
|
+
3D positions of the light-source centre and of the measured surface
|
|
31
|
+
element, in the scene coordinate system.
|
|
32
|
+
lightNormal, triangleNormal : array_like of float, shape (3,)
|
|
33
|
+
Unit normals of the light source and of the measured surface element.
|
|
34
|
+
device : str, optional
|
|
35
|
+
Warp device string (e.g. "cuda:0", "cpu"). Defaults to the first
|
|
36
|
+
available CUDA device, falling back to CPU.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
float
|
|
41
|
+
The BRDF value, in inverse steradians (sr^-1).
|
|
42
|
+
|
|
43
|
+
See Also
|
|
44
|
+
--------
|
|
45
|
+
computeBRDFDistribution : Monte Carlo uncertainty distribution of this value.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
device = _pickDevice(device)
|
|
49
|
+
|
|
50
|
+
output = wp.empty(1, dtype=wp.float64, device=device)
|
|
51
|
+
wp.launch(
|
|
52
|
+
kernel = _sampleBRDF,
|
|
53
|
+
dim = 1,
|
|
54
|
+
inputs = [
|
|
55
|
+
_vec2ui(pixelID), int(channelID), int(pixelValueLSB), float(exposureTimeS),
|
|
56
|
+
_vec3d(lightCenter), _vec3d(lightNormal), _vec3d(triangleCenter), _vec3d(triangleNormal),
|
|
57
|
+
True, 0
|
|
58
|
+
],
|
|
59
|
+
outputs = [output],
|
|
60
|
+
device = device,
|
|
61
|
+
)
|
|
62
|
+
wp.synchronize_device(device)
|
|
63
|
+
return float(output.numpy()[0])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def computeBRDFDistribution(
|
|
67
|
+
pixelID, channelID, pixelValueLSB, exposureTimeS,
|
|
68
|
+
lightCenter, lightNormal, triangleCenter, triangleNormal,
|
|
69
|
+
nbBins: int = 1000,
|
|
70
|
+
pilotSize: int = 100_000,
|
|
71
|
+
batchSize: int = 10_000_000,
|
|
72
|
+
maxBatches: int = 10,
|
|
73
|
+
outlierCutoff: float = 10.0,
|
|
74
|
+
seed: int = 1,
|
|
75
|
+
device: str = None
|
|
76
|
+
):
|
|
77
|
+
"""Estimate the BRDF uncertainty distribution by Monte Carlo propagation.
|
|
78
|
+
|
|
79
|
+
Propagates the measurement uncertainties (calibration factor, Spectralon
|
|
80
|
+
reflectance, ...) through the BRDF measurement equation by repeated
|
|
81
|
+
stochastic sampling, and returns the resulting probability density.
|
|
82
|
+
|
|
83
|
+
The computation runs in two passes:
|
|
84
|
+
|
|
85
|
+
1. Pilot pass -- `pilotSize` samples are drawn to locate the distribution.
|
|
86
|
+
The histogram upper bound is set to ``outlierCutoff * median(pilot)``;
|
|
87
|
+
the lower bound is 0.
|
|
88
|
+
2. Sampling pass -- exactly `maxBatches` batches of `batchSize` samples each
|
|
89
|
+
are accumulated into a fixed-range histogram. Each batch uses a disjoint
|
|
90
|
+
RNG seed, so batches are independent draws.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
pixelID, channelID, pixelValueLSB, exposureTimeS :
|
|
95
|
+
Acquisition descriptors; see `computeBRDFDeterministic`.
|
|
96
|
+
lightCenter, lightNormal, triangleCenter, triangleNormal : array_like, shape (3,)
|
|
97
|
+
Measurement geometry; see `computeBRDFDeterministic`.
|
|
98
|
+
nbBins : int, default 1000
|
|
99
|
+
Number of histogram bins spanning the distribution support.
|
|
100
|
+
pilotSize : int, default 100_000
|
|
101
|
+
Number of samples in the pilot pass used to set the histogram range.
|
|
102
|
+
batchSize : int, default 10_000_000
|
|
103
|
+
Number of samples drawn per batch in the sampling pass.
|
|
104
|
+
maxBatches : int, default 10
|
|
105
|
+
Number of batches drawn. Total sample count is ``maxBatches * batchSize``.
|
|
106
|
+
outlierCutoff : float, default 10.0
|
|
107
|
+
Multiple of the pilot median used as the histogram upper bound. Samples
|
|
108
|
+
beyond this bound are counted as out-of-range and excluded from the density.
|
|
109
|
+
seed : int, default 1
|
|
110
|
+
Base RNG seed for the sampling pass; batch ``i`` uses ``seed + 1 + i``.
|
|
111
|
+
The pilot pass uses a fixed, disjoint seed independent of this value.
|
|
112
|
+
device : str, optional
|
|
113
|
+
Warp device string. Defaults to the first available CUDA device,
|
|
114
|
+
falling back to CPU.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
dict
|
|
119
|
+
pdf : numpy.ndarray, shape (nbBins,)
|
|
120
|
+
Probability density at each bin centre, in steradians (sr, the
|
|
121
|
+
inverse of the BRDF unit). Normalized so the area under the curve
|
|
122
|
+
integrates to 1 over the histogram support; equivalently, the density
|
|
123
|
+
conditioned on a sample lying in range.
|
|
124
|
+
brdfValues : numpy.ndarray, shape (nbBins,)
|
|
125
|
+
Bin-centre BRDF values, in inverse steradians (sr^-1).
|
|
126
|
+
nbTotal : int
|
|
127
|
+
Total number of samples drawn (``maxBatches * batchSize``).
|
|
128
|
+
nbAccepted : int
|
|
129
|
+
Number of samples that fell within the histogram range. The
|
|
130
|
+
out-of-range fraction is ``1 - nbAccepted / nbTotal``; a large value
|
|
131
|
+
indicates the histogram range is mis-set.
|
|
132
|
+
|
|
133
|
+
Notes
|
|
134
|
+
-----
|
|
135
|
+
The returned density is *conditional* on samples lying within the histogram
|
|
136
|
+
support. When the out-of-range fraction is negligible (the expected regime),
|
|
137
|
+
it coincides with the unconditional density. A large rejection fraction
|
|
138
|
+
signals that `outlierCutoff` should be widened or the pilot range revisited.
|
|
139
|
+
|
|
140
|
+
See Also
|
|
141
|
+
--------
|
|
142
|
+
computeBRDFDeterministic : Deterministic point estimate of the BRDF.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
device = _pickDevice(device)
|
|
146
|
+
|
|
147
|
+
# We'll start by a first pass as a pilot
|
|
148
|
+
# This allows us to see the range of the
|
|
149
|
+
# output distribution
|
|
150
|
+
pilot = wp.empty(pilotSize, dtype=wp.float64, device=device)
|
|
151
|
+
wp.launch(
|
|
152
|
+
kernel = _sampleBRDF,
|
|
153
|
+
dim = pilotSize,
|
|
154
|
+
inputs = [
|
|
155
|
+
_vec2ui(pixelID), int(channelID), int(pixelValueLSB), float(exposureTimeS),
|
|
156
|
+
_vec3d(lightCenter), _vec3d(lightNormal), _vec3d(triangleCenter), _vec3d(triangleNormal),
|
|
157
|
+
False, 0
|
|
158
|
+
],
|
|
159
|
+
outputs = [pilot],
|
|
160
|
+
device = device,
|
|
161
|
+
)
|
|
162
|
+
wp.synchronize_device(device)
|
|
163
|
+
|
|
164
|
+
histogramMin = 0.0
|
|
165
|
+
histogramMax = outlierCutoff * float(np.median(pilot.numpy()))
|
|
166
|
+
|
|
167
|
+
# The second pass will be the true Monte Carlo sampling
|
|
168
|
+
histogram = wp.zeros(nbBins, dtype=wp.int64, device=device)
|
|
169
|
+
nbAccepted = wp.zeros(1, dtype=wp.int64, device=device)
|
|
170
|
+
|
|
171
|
+
for batchID in range(maxBatches):
|
|
172
|
+
# We'll use a disjoint seed per batch,
|
|
173
|
+
# that is distinct from the pilot seed
|
|
174
|
+
batchSeed = seed + 1 + batchID
|
|
175
|
+
|
|
176
|
+
wp.launch(
|
|
177
|
+
kernel = _sampleBRDFDistribution,
|
|
178
|
+
dim = batchSize,
|
|
179
|
+
inputs = [
|
|
180
|
+
_vec2ui(pixelID), int(channelID), int(pixelValueLSB), float(exposureTimeS),
|
|
181
|
+
_vec3d(lightCenter), _vec3d(lightNormal), _vec3d(triangleCenter), _vec3d(triangleNormal),
|
|
182
|
+
batchSeed, histogramMin, histogramMax, nbBins
|
|
183
|
+
],
|
|
184
|
+
outputs = [histogram, nbAccepted],
|
|
185
|
+
device = device,
|
|
186
|
+
)
|
|
187
|
+
wp.synchronize_device(device)
|
|
188
|
+
|
|
189
|
+
nbTotal = (batchID + 1) * batchSize
|
|
190
|
+
|
|
191
|
+
counts = histogram.numpy()
|
|
192
|
+
binEdges = np.linspace(histogramMin, histogramMax, nbBins + 1)
|
|
193
|
+
brdfValues = 0.5 * (binEdges[:-1] + binEdges[1:]) # bin centers
|
|
194
|
+
binWidth = (histogramMax - histogramMin) / nbBins
|
|
195
|
+
|
|
196
|
+
# Normalize to a probability density: the area under the curve integrates to 1
|
|
197
|
+
# over the histogram support (counts.sum() == nbAccepted, the in-range samples)
|
|
198
|
+
inRange = counts.sum()
|
|
199
|
+
pdf = counts / (inRange * binWidth) if inRange > 0 else np.zeros_like(counts, dtype=np.float64)
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
"pdf": pdf,
|
|
203
|
+
"brdfValues": brdfValues,
|
|
204
|
+
"nbTotal": nbTotal,
|
|
205
|
+
"nbAccepted": int(nbAccepted.numpy()[0]),
|
|
206
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import warp as wp
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Camera properties
|
|
5
|
+
cameraDSNUE = wp.float64(2.) # DSNU (Dark Signal Non-Uniformity), in electrons
|
|
6
|
+
cameraPRNUPercent = wp.float64(0.0127) # PRNU (Photo Response Non-Uniformity), in percent
|
|
7
|
+
conversionFactorADC = wp.float64(3.33) # ADC (Analog-to-Digital) factor, in electrons/bit
|
|
8
|
+
cameraDarkIntensity = wp.float64(22.) # Dark intensity (in electrons/second)
|
|
9
|
+
cameraReadoutNoiseE = wp.float64(13.) # Readout noise (in electrons)
|
|
10
|
+
cameraSmallestTick = wp.float64(0.000019) # Smallest camera tick (in seconds)
|
|
11
|
+
|
|
12
|
+
# Flat field correction parameters (RGB channels)
|
|
13
|
+
flatFieldAlpha0 = wp.vec3d(-0.11834731, -0.12480557, -0.11442123)
|
|
14
|
+
flatFieldAlpha1 = wp.vec3d( 0.00827793, 0.00580036, 0.02310389)
|
|
15
|
+
flatFieldAlpha2 = wp.vec3d(-0.06498365, -0.06220726, -0.03930921)
|
|
16
|
+
flatFieldAlpha3 = wp.vec3d(-0.02969467, -0.01976859, -0.00969820)
|
|
17
|
+
flatFieldAlpha4 = wp.vec3d( 0.99646296, 0.99836206, 0.99823554)
|
|
18
|
+
|
|
19
|
+
flatFieldSigma0 = wp.vec3d(1.86561084e-04, 1.08971155e-04, 0.00084640)
|
|
20
|
+
flatFieldSigma1 = wp.vec3d(9.63867472e-05, 5.62999256e-05, 0.00043729)
|
|
21
|
+
flatFieldSigma2 = wp.vec3d(1.86500416e-04, 1.08935718e-04, 0.00084612)
|
|
22
|
+
flatFieldSigma3 = wp.vec3d(9.63710605e-05, 5.62907629e-05, 0.00043722)
|
|
23
|
+
flatFieldSigma4 = wp.vec3d(1.04160470e-04, 6.08405915e-05, 0.00047256)
|
|
24
|
+
|
|
25
|
+
# Uncertainty associated to the LEDs
|
|
26
|
+
uncertaintyLEDPosition = wp.float64(0.0007) # in meters (700 µm)
|
|
27
|
+
sizeLEDSide = wp.float64(0.007) # in meters (7 mm)
|
|
28
|
+
# Uncertainty is uniformly distributed between ± 40 µm (so ± 20 µm on half size)
|
|
29
|
+
# This value has been obtained by measuring the LED with a caliper (ref DIN 862)
|
|
30
|
+
uncertaintyLEDSide = wp.float64(0.000040) # in meters (40 µm)
|
|
31
|
+
|
|
32
|
+
# Uncertainty associated to the 3D scan
|
|
33
|
+
reflectanceSpatialUncertainty = wp.float64(0.00020) # in meters (200 µm)
|
|
34
|
+
geometricalSpatialUncertainty = wp.float64(0.00003) # in meters (30 µm)
|
|
35
|
+
|
|
36
|
+
# Uncertainty associated to the triangle
|
|
37
|
+
# it is supposed to be equilateral, with
|
|
38
|
+
# a length of 200 µm
|
|
39
|
+
circumscribedCircleTriangleRadius = wp.float64(0.000115470053) # R = 200 µm * sqrt(3) / 3
|
|
40
|
+
|
|
41
|
+
# Spectralon uncertainty is 0.98 ± [-0.0054 : 0.0054]
|
|
42
|
+
# More precisely, what does the LabSphere calibration
|
|
43
|
+
# certificate say is:
|
|
44
|
+
# 0.98 ± [-0.0054 : 0.0054] at 95 % confidence level
|
|
45
|
+
# This can be modelled with a gaussian distribution,
|
|
46
|
+
# with mean 0.98 and standard deviation 0.0054 / 2.
|
|
47
|
+
spectralonReflectance = wp.float64(0.98)
|
|
48
|
+
spectralonStandardDev = wp.float64(0.0054 / 2.)
|
|
49
|
+
|
|
50
|
+
# Calibration factor uncertainty (RBG channels)
|
|
51
|
+
calibrationFactorMean = wp.vec3d(12554958902.215567, 17434401087.369884, 12713082781.469217)
|
|
52
|
+
calibrationFactorStd = wp.vec3d(705685.39513860860, 702675.41479146033, 707437.30615753750)
|
|
53
|
+
|
|
54
|
+
# Color balancing (RGB)
|
|
55
|
+
colorBalance = wp.vec3d(1. / 0.9105837051966023, 1. / 0.83213626924054, 1. / 0.9071877745806616)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@wp.func
|
|
59
|
+
def _getPixelValue(
|
|
60
|
+
pixelValueLSB: wp.float64, # LSB value of the pixel (in bits)
|
|
61
|
+
exposureTimeS: wp.float64, # Pixel exposure time (in seconds)
|
|
62
|
+
noUncertainty: wp.bool,
|
|
63
|
+
rngState: wp.uint32) -> wp.float64:
|
|
64
|
+
|
|
65
|
+
if noUncertainty:
|
|
66
|
+
return pixelValueLSB
|
|
67
|
+
|
|
68
|
+
# 0. DAC (Digital-to-Analog) conversion
|
|
69
|
+
# From LSB value to electrons number
|
|
70
|
+
pixelValueElectrons = pixelValueLSB * conversionFactorADC
|
|
71
|
+
|
|
72
|
+
# 1. PRNU (Photo Response Non-Uniformity) noise
|
|
73
|
+
# Since it is given by the manufacturer as a
|
|
74
|
+
# maximum limit, we'll use a uniform distrib
|
|
75
|
+
PRNU = wp.float64(1.0) + (wp.float64(2.0) * wp.float64(wp.randf(rngState)) - wp.float64(1.0)) * cameraPRNUPercent
|
|
76
|
+
|
|
77
|
+
# 2. Shot noise (Photon counting noise). It is the fundamental
|
|
78
|
+
# limit on noise performance in light detection systems. It
|
|
79
|
+
# can be well representated using a Poisson distribution if
|
|
80
|
+
# we have computed the signal in terms of electrons number.
|
|
81
|
+
shotNoise = wp.float64(wp.poisson(rngState, wp.float32(pixelValueElectrons * PRNU)))
|
|
82
|
+
|
|
83
|
+
# 3. Dark noise (thermal noise). Somestimes called reset noise
|
|
84
|
+
# or kTC noise. This can also be represented using Poisson.
|
|
85
|
+
darkNoise = wp.float64(wp.poisson(rngState, wp.float32(cameraDarkIntensity * exposureTimeS)))
|
|
86
|
+
|
|
87
|
+
# 4. Read noise, sometimes called the output amplifier noise.
|
|
88
|
+
# More precisely, it can be separated in two parts, white
|
|
89
|
+
# noise (also known as Johnson noise), and flicker noise.
|
|
90
|
+
# We will follow here a Gaussian rule.
|
|
91
|
+
readoutNoise = wp.float64(wp.randn(rngState)) * cameraReadoutNoiseE
|
|
92
|
+
|
|
93
|
+
# 5. DSNU (Dark Signal Non-Uniformity)
|
|
94
|
+
# Can be represented with Gaussian.
|
|
95
|
+
DSNU = wp.float64(wp.randn(rngState)) * cameraDSNUE
|
|
96
|
+
|
|
97
|
+
# Quantify using ADC (Analog-to-Digital) conversion
|
|
98
|
+
value = wp.round((shotNoise + darkNoise + readoutNoise + DSNU) / conversionFactorADC)
|
|
99
|
+
|
|
100
|
+
# 6. Quantization uncertainty, using a uniform
|
|
101
|
+
# distribution of width that is equal to 1.
|
|
102
|
+
value += wp.float64(wp.randf(rngState)) - wp.float64(0.5)
|
|
103
|
+
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
@wp.func
|
|
107
|
+
def _getFlatFieldCorrection(
|
|
108
|
+
pixelID : wp.vec2ui,
|
|
109
|
+
channelID: wp.uint32,
|
|
110
|
+
noUncertainty: wp.bool,
|
|
111
|
+
rngState : wp.uint32) -> wp.float64:
|
|
112
|
+
|
|
113
|
+
x = wp.float64(2. * (wp.float32(pixelID[0]) / 4096.) - 1.)
|
|
114
|
+
y = wp.float64(2. * (wp.float32(pixelID[1]) / 3072.) - 1.)
|
|
115
|
+
|
|
116
|
+
if noUncertainty:
|
|
117
|
+
return (flatFieldAlpha0[channelID] * x * x +
|
|
118
|
+
flatFieldAlpha1[channelID] * x +
|
|
119
|
+
flatFieldAlpha2[channelID] * y * y +
|
|
120
|
+
flatFieldAlpha3[channelID] * y +
|
|
121
|
+
flatFieldAlpha4[channelID])
|
|
122
|
+
|
|
123
|
+
alpha0 = flatFieldAlpha0[channelID] + flatFieldSigma0[channelID] * wp.float64(wp.randn(rngState))
|
|
124
|
+
alpha1 = flatFieldAlpha1[channelID] + flatFieldSigma1[channelID] * wp.float64(wp.randn(rngState))
|
|
125
|
+
alpha2 = flatFieldAlpha2[channelID] + flatFieldSigma2[channelID] * wp.float64(wp.randn(rngState))
|
|
126
|
+
alpha3 = flatFieldAlpha3[channelID] + flatFieldSigma3[channelID] * wp.float64(wp.randn(rngState))
|
|
127
|
+
alpha4 = flatFieldAlpha4[channelID] + flatFieldSigma4[channelID] * wp.float64(wp.randn(rngState))
|
|
128
|
+
|
|
129
|
+
return alpha0 * x * x + alpha1 * x + alpha2 * y * y + alpha3 * y + alpha4
|
|
130
|
+
|
|
131
|
+
@wp.func
|
|
132
|
+
def _branchlessONB(normal: wp.vec3d):
|
|
133
|
+
# Branchless orthonormal tangent frame (u, v) from a normal n
|
|
134
|
+
# Duff et al., 2017, JCGT. (n, u, v) right-handed orthonormal
|
|
135
|
+
# Requires |n| = 1.
|
|
136
|
+
x, y, z = normal[0], normal[1], normal[2]
|
|
137
|
+
|
|
138
|
+
sign = wp.copysign(wp.float64(1.), z)
|
|
139
|
+
|
|
140
|
+
a = -wp.float64(1.) / (sign + z)
|
|
141
|
+
b = x * y * a
|
|
142
|
+
|
|
143
|
+
u = wp.vec3d(wp.float64(1.) + sign * x * x * a, sign * b, -sign * x)
|
|
144
|
+
v = wp.vec3d(b, sign + y * y * a, -y)
|
|
145
|
+
|
|
146
|
+
return u, v
|
|
147
|
+
|
|
148
|
+
@wp.func
|
|
149
|
+
def _getLEDNormal(
|
|
150
|
+
normal : wp.vec3d, # LED normal
|
|
151
|
+
noUncertainty: wp.bool,
|
|
152
|
+
rngState: wp.uint32) -> wp.vec3d:
|
|
153
|
+
|
|
154
|
+
if noUncertainty:
|
|
155
|
+
return normal
|
|
156
|
+
|
|
157
|
+
# Orthonormal tangent frame
|
|
158
|
+
u, v = _branchlessONB(normal)
|
|
159
|
+
|
|
160
|
+
radius = wp.float64(0.250) # 25 cm radius
|
|
161
|
+
angleInit = wp.float64(wp.randf(rngState) * wp.tau) # [0; 2]
|
|
162
|
+
angle120 = angleInit + wp.float64(2.0943951023931953)
|
|
163
|
+
angle240 = angleInit + wp.float64(4.1887902047863905)
|
|
164
|
+
|
|
165
|
+
p0 = radius * (wp.cos(angleInit) * u + wp.sin(angleInit) * v)
|
|
166
|
+
p1 = radius * (wp.cos(angle120) * u + wp.sin(angle120) * v)
|
|
167
|
+
p2 = radius * (wp.cos(angle240) * u + wp.sin(angle240) * v)
|
|
168
|
+
|
|
169
|
+
# Every vertex (LED) has its own uncertainty that we will add
|
|
170
|
+
p0 += wp.vec3d(wp.sample_unit_sphere(rngState)) * uncertaintyLEDPosition
|
|
171
|
+
p1 += wp.vec3d(wp.sample_unit_sphere(rngState)) * uncertaintyLEDPosition
|
|
172
|
+
p2 += wp.vec3d(wp.sample_unit_sphere(rngState)) * uncertaintyLEDPosition
|
|
173
|
+
|
|
174
|
+
ledNormal = wp.normalize(wp.cross(p1 - p0, p2 - p0))
|
|
175
|
+
|
|
176
|
+
# Should we flip the new normal to be sure it will
|
|
177
|
+
# point in the same direction as the original one?
|
|
178
|
+
flipFactor = wp.copysign(wp.float64(1.), wp.dot(ledNormal, normal))
|
|
179
|
+
return flipFactor * ledNormal
|
|
180
|
+
|
|
181
|
+
@wp.func
|
|
182
|
+
def _getTriangleNormal(
|
|
183
|
+
normal : wp.vec3d, # Triangle normal
|
|
184
|
+
noUncertainty: wp.bool,
|
|
185
|
+
rngState: wp.uint32) -> wp.vec3d:
|
|
186
|
+
|
|
187
|
+
if noUncertainty:
|
|
188
|
+
return normal
|
|
189
|
+
|
|
190
|
+
# Orthonormal tangent frame
|
|
191
|
+
u, v = _branchlessONB(normal)
|
|
192
|
+
|
|
193
|
+
angleInit = wp.float64(wp.randf(rngState) * wp.tau) # [0; 2]
|
|
194
|
+
angle120 = angleInit + wp.float64(2.0943951023931953)
|
|
195
|
+
angle240 = angleInit + wp.float64(4.1887902047863905)
|
|
196
|
+
|
|
197
|
+
p0 = circumscribedCircleTriangleRadius * (wp.cos(angleInit) * u + wp.sin(angleInit) * v)
|
|
198
|
+
p1 = circumscribedCircleTriangleRadius * (wp.cos(angle120) * u + wp.sin(angle120) * v)
|
|
199
|
+
p2 = circumscribedCircleTriangleRadius * (wp.cos(angle240) * u + wp.sin(angle240) * v)
|
|
200
|
+
|
|
201
|
+
# Every vertex has its own uncertainty that we will add
|
|
202
|
+
p0 += wp.vec3d(wp.sample_unit_sphere(rngState)) * geometricalSpatialUncertainty
|
|
203
|
+
p1 += wp.vec3d(wp.sample_unit_sphere(rngState)) * geometricalSpatialUncertainty
|
|
204
|
+
p2 += wp.vec3d(wp.sample_unit_sphere(rngState)) * geometricalSpatialUncertainty
|
|
205
|
+
|
|
206
|
+
triangleNormal = wp.normalize(wp.cross(p1 - p0, p2 - p0))
|
|
207
|
+
|
|
208
|
+
# Should we flip the new normal to be sure it will
|
|
209
|
+
# point in the same direction as the original one?
|
|
210
|
+
flipFactor = wp.copysign(wp.float64(1.), wp.dot(triangleNormal, normal))
|
|
211
|
+
return flipFactor * triangleNormal
|
|
212
|
+
|
|
213
|
+
@wp.func
|
|
214
|
+
def _getCalibrationFactor(
|
|
215
|
+
channelID: wp.uint32,
|
|
216
|
+
noUncertainty: wp.bool,
|
|
217
|
+
rngState: wp.uint32) -> wp.float64:
|
|
218
|
+
# See Eq. S19 of Supplemental Document
|
|
219
|
+
# DOI: 10.6084/m9.figshare.31418393.v1
|
|
220
|
+
|
|
221
|
+
# The calibration factor corresponds to the second term in teal
|
|
222
|
+
# This one is independant of the measurement configuration. The
|
|
223
|
+
# two terms composing it are the numerator and the denominator.
|
|
224
|
+
|
|
225
|
+
if noUncertainty:
|
|
226
|
+
numerator = spectralonReflectance / wp.float64(wp.pi)
|
|
227
|
+
denominator = calibrationFactorMean[channelID]
|
|
228
|
+
return colorBalance[channelID] * numerator / denominator
|
|
229
|
+
|
|
230
|
+
numerator = spectralonReflectance * (wp.float64(1.) + spectralonStandardDev * wp.float64(wp.randn(rngState))) / wp.float64(wp.pi)
|
|
231
|
+
denominator = calibrationFactorMean[channelID] + calibrationFactorStd[channelID] * wp.float64(wp.randn(rngState))
|
|
232
|
+
|
|
233
|
+
return colorBalance[channelID] * numerator / denominator
|
|
234
|
+
|
|
235
|
+
@wp.func
|
|
236
|
+
def _getEdgeContribution(
|
|
237
|
+
x : wp.vec3d,
|
|
238
|
+
p0Vertex: wp.vec3d,
|
|
239
|
+
p1Vertex: wp.vec3d) -> wp.vec3d:
|
|
240
|
+
# See Eq. 14 of Supplemental Document:
|
|
241
|
+
# DOI: 10.6084/m9.figshare.31418393.v1
|
|
242
|
+
v0 = wp.normalize(p0Vertex - x)
|
|
243
|
+
v1 = wp.normalize(p1Vertex - x)
|
|
244
|
+
|
|
245
|
+
cross = wp.normalize(wp.cross(v0, v1))
|
|
246
|
+
|
|
247
|
+
return wp.acos(wp.dot(v0, v1)) * cross
|
|
248
|
+
|
|
249
|
+
@wp.func
|
|
250
|
+
def _getGeometricVector(
|
|
251
|
+
x : wp.vec3d,
|
|
252
|
+
p0Vertex: wp.vec3d,
|
|
253
|
+
p1Vertex: wp.vec3d,
|
|
254
|
+
p2Vertex: wp.vec3d,
|
|
255
|
+
p3Vertex: wp.vec3d) -> wp.vec3d:
|
|
256
|
+
# See Eq. S14 of Supplemental Document
|
|
257
|
+
# DOI: 10.6084/m9.figshare.31418393.v1
|
|
258
|
+
return (_getEdgeContribution(x, p0Vertex, p1Vertex) +
|
|
259
|
+
_getEdgeContribution(x, p1Vertex, p2Vertex) +
|
|
260
|
+
_getEdgeContribution(x, p2Vertex, p3Vertex) +
|
|
261
|
+
_getEdgeContribution(x, p3Vertex, p0Vertex))
|
|
262
|
+
|
|
263
|
+
@wp.func
|
|
264
|
+
def _getGeometricFactor(
|
|
265
|
+
initialLightCenter: wp.vec3d,
|
|
266
|
+
initialLightNormal: wp.vec3d,
|
|
267
|
+
initialTriangleCenter: wp.vec3d,
|
|
268
|
+
initialTriangleNormal: wp.vec3d,
|
|
269
|
+
noUncertainty: wp.bool,
|
|
270
|
+
rngState: wp.uint32) -> wp.float64:
|
|
271
|
+
# See Eq. S19 of Supplemental Document
|
|
272
|
+
# DOI: 10.6084/m9.figshare.31418393.v1
|
|
273
|
+
|
|
274
|
+
triangleNormal = _getTriangleNormal(initialTriangleNormal, noUncertainty, rngState)
|
|
275
|
+
lightNormal = _getLEDNormal(initialLightNormal, noUncertainty, rngState)
|
|
276
|
+
|
|
277
|
+
if noUncertainty:
|
|
278
|
+
triangleCenter = initialTriangleCenter
|
|
279
|
+
lightCenter = initialLightCenter
|
|
280
|
+
halfSizeLED = sizeLEDSide * wp.float64(0.5)
|
|
281
|
+
|
|
282
|
+
# LED corners (deterministic)
|
|
283
|
+
u, v = _branchlessONB(lightNormal)
|
|
284
|
+
u *= halfSizeLED
|
|
285
|
+
v *= halfSizeLED
|
|
286
|
+
p0Light = lightCenter - u - v
|
|
287
|
+
p1Light = lightCenter + u - v
|
|
288
|
+
p2Light = lightCenter + u + v
|
|
289
|
+
p3Light = lightCenter - u + v
|
|
290
|
+
|
|
291
|
+
# No surface sampling — use centers
|
|
292
|
+
lightPosition = lightCenter
|
|
293
|
+
xPosition = triangleCenter
|
|
294
|
+
else:
|
|
295
|
+
triangleCenter = initialTriangleCenter + wp.vec3d(wp.sample_unit_sphere(rngState)) * reflectanceSpatialUncertainty
|
|
296
|
+
lightCenter = initialLightCenter + wp.vec3d(wp.sample_unit_sphere(rngState)) * uncertaintyLEDPosition
|
|
297
|
+
halfSizeLED = (sizeLEDSide + wp.float64(2. * wp.randf(rngState) - 1.) * uncertaintyLEDSide) * wp.float64(0.5)
|
|
298
|
+
|
|
299
|
+
# LED corners
|
|
300
|
+
u = wp.cross(lightNormal, wp.vec3d(0., 0., 1.))
|
|
301
|
+
v = wp.cross(lightNormal, u)
|
|
302
|
+
u, v = _branchlessONB(lightNormal)
|
|
303
|
+
u *= halfSizeLED
|
|
304
|
+
v *= halfSizeLED
|
|
305
|
+
p0Light = lightCenter - u - v
|
|
306
|
+
p1Light = lightCenter + u - v
|
|
307
|
+
p2Light = lightCenter + u + v
|
|
308
|
+
p3Light = lightCenter - u + v
|
|
309
|
+
|
|
310
|
+
# Uniform sample on LED square
|
|
311
|
+
uv = wp.vec2d(wp.vec2f(wp.randf(rngState), wp.randf(rngState)))
|
|
312
|
+
lightPosition = p0Light + wp.float64(2.) * u * uv[0] + wp.float64(2.) * v * uv[1]
|
|
313
|
+
|
|
314
|
+
# Uniform sample on equilateral triangle (centroid = triangleCenter)
|
|
315
|
+
a, b = _branchlessONB(triangleNormal)
|
|
316
|
+
a *= wp.float64(0.0002)
|
|
317
|
+
b *= wp.float64(0.0002)
|
|
318
|
+
T0 = triangleCenter - wp.float64(0.5) * a - wp.float64(wp.sqrt(3.) / 6.) * b
|
|
319
|
+
T1 = triangleCenter + wp.float64(0.5) * a - wp.float64(wp.sqrt(3.) / 6.) * b
|
|
320
|
+
T2 = triangleCenter + wp.float64((wp.sqrt(3.) / 3.)) * b
|
|
321
|
+
ab = wp.vec2d(wp.sample_triangle(rngState))
|
|
322
|
+
xPosition = T0 * (wp.float64(1.) - ab[0] - ab[1]) + T1 * ab[0] + T2 * ab[1]
|
|
323
|
+
|
|
324
|
+
# Compute geometric vector
|
|
325
|
+
geometricVector = _getGeometricVector(xPosition, p0Light, p1Light, p2Light, p3Light)
|
|
326
|
+
|
|
327
|
+
# Compute direction from surface to light.
|
|
328
|
+
lightDirection = wp.normalize(lightPosition - xPosition)
|
|
329
|
+
|
|
330
|
+
return wp.abs(wp.dot(geometricVector, triangleNormal) * wp.abs(wp.dot(lightNormal, lightDirection)))
|
|
331
|
+
|
|
332
|
+
@wp.func
|
|
333
|
+
def _getBRDFValue(
|
|
334
|
+
pixelID : wp.vec2ui,
|
|
335
|
+
channelID : wp.uint32,
|
|
336
|
+
pixelValueLSB: wp.uint16,
|
|
337
|
+
exposureTimeS: wp.float64,
|
|
338
|
+
|
|
339
|
+
lightCenter : wp.vec3d,
|
|
340
|
+
lightNormal : wp.vec3d,
|
|
341
|
+
triangleCenter: wp.vec3d,
|
|
342
|
+
triangleNormal: wp.vec3d,
|
|
343
|
+
|
|
344
|
+
noUncertainty: wp.bool,
|
|
345
|
+
rngState: wp.uint32) -> wp.float64:
|
|
346
|
+
|
|
347
|
+
# Adding an uncertainty over the exposure time
|
|
348
|
+
# Let's take the smallest camera tick possible
|
|
349
|
+
actualExposureS = exposureTimeS
|
|
350
|
+
if not noUncertainty:
|
|
351
|
+
actualExposureS = exposureTimeS + cameraSmallestTick * wp.float64((2. * wp.randf(rngState) - 1.))
|
|
352
|
+
actualExposureS = wp.max(actualExposureS, cameraSmallestTick)
|
|
353
|
+
|
|
354
|
+
# Compute the flat field correction with uncertainty
|
|
355
|
+
flatFieldCorrection = _getFlatFieldCorrection(pixelID, channelID, noUncertainty, rngState)
|
|
356
|
+
|
|
357
|
+
# Compute the pixel value with uncertainty
|
|
358
|
+
pixelValueLSBf = _getPixelValue(wp.float64(pixelValueLSB), actualExposureS, noUncertainty, rngState)
|
|
359
|
+
|
|
360
|
+
# f_# number (or aperture) squared
|
|
361
|
+
# is supposed without uncertainty.
|
|
362
|
+
apertureSquared = wp.float64(64.) # 8²
|
|
363
|
+
|
|
364
|
+
# Compute the uncertainty of the calibration factor
|
|
365
|
+
calibrationFactor = _getCalibrationFactor(channelID, noUncertainty, rngState)
|
|
366
|
+
|
|
367
|
+
# Compute the geometric factor
|
|
368
|
+
geometricFactor = _getGeometricFactor(lightCenter, lightNormal, triangleCenter, triangleNormal, noUncertainty, rngState)
|
|
369
|
+
|
|
370
|
+
# See Eq. S19 of Supplemental Document
|
|
371
|
+
# DOI: 10.6084/m9.figshare.31418393.v1
|
|
372
|
+
BRDF = (pixelValueLSBf / flatFieldCorrection) * apertureSquared * calibrationFactor / (actualExposureS * geometricFactor)
|
|
373
|
+
return BRDF
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import warp as wp
|
|
2
|
+
|
|
3
|
+
from ._functions import _getBRDFValue
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@wp.kernel
|
|
7
|
+
def _sampleBRDF(
|
|
8
|
+
pixelID: wp.vec2ui,
|
|
9
|
+
channelID: wp.uint32,
|
|
10
|
+
pixelValueLSB: wp.uint16,
|
|
11
|
+
exposureTimeS: wp.float64,
|
|
12
|
+
lightCenter: wp.vec3d,
|
|
13
|
+
lightNormal: wp.vec3d,
|
|
14
|
+
triangleCenter: wp.vec3d,
|
|
15
|
+
triangleNormal: wp.vec3d,
|
|
16
|
+
removeAllUncertainties: wp.bool,
|
|
17
|
+
seed: int,
|
|
18
|
+
samples: wp.array(dtype=wp.float64)
|
|
19
|
+
):
|
|
20
|
+
ID = wp.tid()
|
|
21
|
+
rngState = wp.rand_init(seed, ID)
|
|
22
|
+
|
|
23
|
+
samples[ID] = _getBRDFValue(pixelID, channelID, pixelValueLSB, exposureTimeS,
|
|
24
|
+
lightCenter, lightNormal, triangleCenter, triangleNormal,
|
|
25
|
+
removeAllUncertainties, rngState)
|
|
26
|
+
|
|
27
|
+
@wp.kernel
|
|
28
|
+
def _sampleBRDFDistribution(
|
|
29
|
+
pixelID: wp.vec2ui,
|
|
30
|
+
channelID: wp.uint32,
|
|
31
|
+
pixelValueLSB: wp.uint16,
|
|
32
|
+
exposureTimeS: wp.float64,
|
|
33
|
+
lightCenter: wp.vec3d,
|
|
34
|
+
lightNormal: wp.vec3d,
|
|
35
|
+
triangleCenter: wp.vec3d,
|
|
36
|
+
triangleNormal: wp.vec3d,
|
|
37
|
+
seed: int,
|
|
38
|
+
histogramMin: wp.float64,
|
|
39
|
+
histogramMax: wp.float64,
|
|
40
|
+
nbBins: wp.int32,
|
|
41
|
+
|
|
42
|
+
histogram: wp.array(dtype=wp.int64),
|
|
43
|
+
nbAccepted: wp.array(dtype=wp.int64),
|
|
44
|
+
):
|
|
45
|
+
ID = wp.tid()
|
|
46
|
+
rngState = wp.rand_init(seed, ID)
|
|
47
|
+
|
|
48
|
+
brdf = _getBRDFValue(pixelID, channelID, pixelValueLSB, exposureTimeS,
|
|
49
|
+
lightCenter, lightNormal, triangleCenter, triangleNormal,
|
|
50
|
+
False, rngState)
|
|
51
|
+
|
|
52
|
+
if brdf >= histogramMin and brdf < histogramMax:
|
|
53
|
+
binID = wp.int32((brdf - histogramMin) / (histogramMax - histogramMin) * wp.float64(nbBins))
|
|
54
|
+
wp.atomic_add(histogram, binID, wp.int64(1))
|
|
55
|
+
wp.atomic_add(nbAccepted, 0, wp.int64(1))
|
pycoupole/core/_utils.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import warp as wp
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _pickDevice(device):
|
|
5
|
+
if device is not None:
|
|
6
|
+
return device
|
|
7
|
+
return "cuda:0" if wp.is_cuda_available() else "cpu"
|
|
8
|
+
|
|
9
|
+
def _vec3d(vec):
|
|
10
|
+
return wp.vec3d(float(vec[0]), float(vec[1]), float(vec[2]))
|
|
11
|
+
|
|
12
|
+
def _vec2ui(vec):
|
|
13
|
+
return wp.vec2ui(int(vec[0]), int(vec[1]))
|
pycoupole/io.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .brdf import computeBRDFDeterministic, computeBRDFDistribution
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _readRows(path, rows):
|
|
8
|
+
if rows is None:
|
|
9
|
+
wanted = None
|
|
10
|
+
elif isinstance(rows, int):
|
|
11
|
+
wanted = {rows}
|
|
12
|
+
else:
|
|
13
|
+
wanted = set(rows)
|
|
14
|
+
|
|
15
|
+
selected = []
|
|
16
|
+
with Path(path).open(newline="") as f:
|
|
17
|
+
reader = csv.DictReader(f)
|
|
18
|
+
for index, record in enumerate(reader):
|
|
19
|
+
if wanted is None or index in wanted:
|
|
20
|
+
selected.append((index, record))
|
|
21
|
+
if wanted is not None and len(selected) == len(wanted):
|
|
22
|
+
break # stop early once every requested row is found
|
|
23
|
+
|
|
24
|
+
if wanted is not None and len(selected) != len(wanted):
|
|
25
|
+
found = {index for index, _ in selected}
|
|
26
|
+
missing = sorted(wanted - found)
|
|
27
|
+
raise IndexError(f"CSV row(s) not found in {path}: {missing}")
|
|
28
|
+
|
|
29
|
+
return selected
|
|
30
|
+
|
|
31
|
+
def _rowToKwargs(row):
|
|
32
|
+
return dict(
|
|
33
|
+
pixelID = (int(row["i"]), int(row["j"])),
|
|
34
|
+
channelID = int(row["color"]),
|
|
35
|
+
pixelValueLSB = int(row["valuePixel"]),
|
|
36
|
+
exposureTimeS = float(row["exposure"]),
|
|
37
|
+
lightCenter = (float(row["l.x"]), float(row["l.y"]), float(row["l.z"])),
|
|
38
|
+
lightNormal = (float(row["nl.x"]), float(row["nl.y"]), float(row["nl.z"])),
|
|
39
|
+
triangleCenter = (float(row["x.x"]), float(row["x.y"]), float(row["x.z"])),
|
|
40
|
+
triangleNormal = (float(row["nt.x"]), float(row["nt.y"]), float(row["nt.z"])),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def computeBRDFDeterministicFromCSV(path, rows=None, device=None):
|
|
45
|
+
"""Run the deterministic BRDF estimate on selected rows of a measurement CSV
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
path : str or pathlib.Path
|
|
50
|
+
Path to the measurement CSV.
|
|
51
|
+
rows : int, iterable of int, or None, default None
|
|
52
|
+
Zero-based data-row indices to process. A single int processes one row;
|
|
53
|
+
None processes every row.
|
|
54
|
+
device : str, optional
|
|
55
|
+
Warp device string. Defaults to the first available CUDA device,
|
|
56
|
+
falling back to CPU.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
list of (int, float)
|
|
61
|
+
Pairs of (row index, BRDF value in sr^-1), in increasing index order.
|
|
62
|
+
|
|
63
|
+
Notes
|
|
64
|
+
-----
|
|
65
|
+
This runs one single-thread GPU launch per row, which is convenient but not
|
|
66
|
+
efficient for very large files. A GPU parallelized version may be done soon
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
return [(index, computeBRDFDeterministic(device=device, **_rowToKwargs(record)))
|
|
70
|
+
for index, record in _readRows(path, rows)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def computeBRDFDistributionFromCSV(path, row, device=None, **kwargs):
|
|
74
|
+
"""Run the Monte Carlo BRDF distribution on a single row of a measurement CSV.
|
|
75
|
+
|
|
76
|
+
Unlike the deterministic helper, this accepts exactly one row: a single
|
|
77
|
+
distribution already saturates the GPU, and per-row results are typically
|
|
78
|
+
inspected or stored individually rather than aggregated.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
path : str or pathlib.Path
|
|
83
|
+
Path to the measurement CSV.
|
|
84
|
+
row : int
|
|
85
|
+
Zero-based data-row index to process.
|
|
86
|
+
device : str, optional
|
|
87
|
+
Warp device string. Defaults to the first available CUDA device,
|
|
88
|
+
falling back to CPU.
|
|
89
|
+
**kwargs
|
|
90
|
+
Forwarded to `computeBRDFDistribution` (e.g. nbBins, batchSize, seed).
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
dict
|
|
95
|
+
The distribution dict returned by `computeBRDFDistribution`
|
|
96
|
+
(keys: pdf, brdfValues, nbTotal, nbAccepted).
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
if not isinstance(row, int):
|
|
100
|
+
raise TypeError("computeBRDFDistributionFromCSV processes single row; got"
|
|
101
|
+
f" {type(row).__name__}. Loop in Python for multiple rows.")
|
|
102
|
+
|
|
103
|
+
(_, record), = _readRows(path, row)
|
|
104
|
+
return computeBRDFDistribution(device=device, **_rowToKwargs(record), **kwargs)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pycoupole
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GPU-accelerated Monte Carlo uncertainty quantification for SVBRDF measurements on the La Coupole setup. Companion code to the associated Optics Express paper and dataset.
|
|
5
|
+
Author-email: François Margall <francois.margall@inria.fr>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://lacoupole.gitlabpages.inria.fr
|
|
8
|
+
Project-URL: Repository, https://gitlab.inria.fr/lacoupole/pycoupole
|
|
9
|
+
Project-URL: Tracker, https://gitlab.inria.fr/lacoupole/pycoupole/-/boards
|
|
10
|
+
Project-URL: Article, https://doi.org/10.1364/OE.587877
|
|
11
|
+
Project-URL: Supplemental, https://doi.org/10.6084/m9.figshare.31418393.v1
|
|
12
|
+
Keywords: monte-carlo,nvidia,uncertainty,metrology,warp,brdf,gum,coupole
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE.txt
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: warp-lang>=1.14.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# PyCoupole
|
|
24
|
+
|
|
25
|
+
GPU-accelerated Monte Carlo uncertainty quantification for SVBRDF measurements
|
|
26
|
+
on the La Coupole setup. Companion code to the associated Optics Express paper
|
|
27
|
+
and measured dataset.
|
|
28
|
+
|
|
29
|
+
## Interpreting the output
|
|
30
|
+
|
|
31
|
+
`computeBRDFDistribution` returns a probability density, not a histogram of
|
|
32
|
+
counts. The density carries the inverse unit of the BRDF (sr), and integrates
|
|
33
|
+
to 1 over its support — so its values are not bounded by 1 and will be large
|
|
34
|
+
for a sharply peaked distribution. Recover the per-bin probability mass, then
|
|
35
|
+
the moments and credible intervals, as:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
binWidth = brdfValues[1] - brdfValues[0]
|
|
39
|
+
mass = pdf * binWidth # sums to 1
|
|
40
|
+
|
|
41
|
+
mean = float(np.sum(mass * brdfValues))
|
|
42
|
+
std = float(np.sqrt(np.sum(mass * (brdfValues - mean) ** 2)))
|
|
43
|
+
|
|
44
|
+
cdf = np.cumsum(mass)
|
|
45
|
+
median = float(np.interp(0.50, cdf, brdfValues))
|
|
46
|
+
ci95_low = float(np.interp(0.025, cdf, brdfValues))
|
|
47
|
+
ci95_up = float(np.interp(0.975, cdf, brdfValues))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For a well-conditioned acquisition the deterministic value sits near the centre
|
|
51
|
+
of the distribution and the out-of-range fraction (`1 - nbAccepted / nbTotal`)
|
|
52
|
+
is negligible.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Citation
|
|
56
|
+
If you use this software, please cite the associated paper as below (see also [CITATION.cff](./CITATION.cff)).
|
|
57
|
+
|
|
58
|
+
> **La Coupole: an SVBRDF measurement device for large and non-planar objects**.
|
|
59
|
+
> *Antoine Lucat, Pierre Mézières, François Margall, Louis De Oliveira,
|
|
60
|
+
> Marjorie Paillet, Arnaud Tizon, Pierre Bénard, Romain Pacanowski*.
|
|
61
|
+
> Optics Express, Vol. 34, Issue 7, pp. 11695-11709 (March 2026).
|
|
62
|
+
> DOI: [10.1364/OE.587877](https://doi.org/10.1364/OE.587877).
|
|
63
|
+
|
|
64
|
+
<details>
|
|
65
|
+
<summary>Associated BibTeX entry</summary>
|
|
66
|
+
|
|
67
|
+
```bibtex
|
|
68
|
+
@article{Lucat:26,
|
|
69
|
+
author = {Antoine Lucat and Pierre M\'{e}zi\`{e}res and Fran\c{c}ois Margall and Louis De Oliveira and Marjorie Paillet and Arnaud Tizon and Pierre B\'{e}nard and Romain Pacanowski},
|
|
70
|
+
journal = {Optics Express},
|
|
71
|
+
keywords = {Camera calibration; Imaging systems; Light sources; Physiology; Printed circuit boards; Spatial resolution},
|
|
72
|
+
number = {7},
|
|
73
|
+
pages = {11695--11709},
|
|
74
|
+
publisher = {Optica Publishing Group},
|
|
75
|
+
title = {La Coupole: an SVBRDF measurement device for large and non-planar objects},
|
|
76
|
+
volume = {34},
|
|
77
|
+
month = {Apr},
|
|
78
|
+
year = {2026},
|
|
79
|
+
url = {https://opg.optica.org/oe/abstract.cfm?URI=oe-34-7-11695},
|
|
80
|
+
doi = {10.1364/OE.587877},
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
</details>
|
|
85
|
+
|
|
86
|
+
> [!note]
|
|
87
|
+
> For a detailed derivation of the uncertainty estimation method, including assumptions, propagation steps, and validation, see Section 3 (pp. 14–20) of the associated Supplemental Material (DOI: [10.6084/m9.figshare.31418393.v1](https://doi.org/10.6084/m9.figshare.31418393.v1)).
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
PyCoupole is distributed under the MIT License. See [LICENSE.txt](./LICENSE.txt) for more information.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pycoupole/__init__.py,sha256=qoENccDZLPKvQ5mRVzpOzg2iH74FHqFsZEPUJHEMcSA,514
|
|
2
|
+
pycoupole/brdf.py,sha256=TVLZ4oAOk6sxmUrfd7kV66QzI3lhzfFnNKjq2EtCXQw,8204
|
|
3
|
+
pycoupole/io.py,sha256=Ws9ArBj5ojAmCdlI7v9sq89gZdZeXz9SDNOTht9zP5A,3745
|
|
4
|
+
pycoupole/core/_functions.py,sha256=Qsuslc0JBWZ5yIdDpiJdKrMzrKBpreT4TAVgKdan_rA,15466
|
|
5
|
+
pycoupole/core/_kernels.py,sha256=PEiF7AzXWjq65dOKhvQUD8max-0zN7V9wg-wJEx8nMQ,1947
|
|
6
|
+
pycoupole/core/_utils.py,sha256=ZDo2137qofd1asFBRrFJaEHSeRFvA2n80J8Ja3T00fA,300
|
|
7
|
+
pycoupole-0.1.0.dist-info/licenses/LICENSE.txt,sha256=9jZf9VltRyFXHRifPQbC-HO4PjUTawjmHEiqIc_SYjw,1074
|
|
8
|
+
pycoupole-0.1.0.dist-info/METADATA,sha256=Hfykaxdedboj_e0jI4IuwUvXDKS5vWxZz1S4YZAlsCg,3989
|
|
9
|
+
pycoupole-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
pycoupole-0.1.0.dist-info/top_level.txt,sha256=sSK5XteiSw6vOUDLQCb2_o3zZAhBESQ7QDbk5X_zBXY,10
|
|
11
|
+
pycoupole-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MARGALL François
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pycoupole
|