/
nloscapturemeter.py
194 lines (154 loc) · 7.99 KB
/
nloscapturemeter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
from __future__ import annotations # Delayed parsing of type annotations
import drjit as dr
import mitsuba as mi
from mitsuba import Log, LogLevel, is_spectral, Transform4f, ScalarVector2u, ScalarVector2f
from drjit.scalar import Array3f as ScalarArray3f # type: ignore
from typing import Tuple
from .nlossensor import NLOSSensor
from ..utils import indent
class NLOSCaptureMeter(NLOSSensor):
"""
`nlos_capture_meter` plugin
===========================
Attaches to a geometry (sensor should be child of the geometry).
Measures uniformly-spaced points on such geometry.
It is recommended to use a `rectangle` shape, the UV coordinates work better.
The `nlos_capture_meter` should have a `film` children, which acts as the
storage for the transient image. It is recommended to use `transient_hdr_film`.
<shape type="rectangle">
<sensor type="nlos_capture_meter">
<film type="transient_hdr_film">
...
</film>
</sensor>
</shape>
The `nlos_capture_meter` plugin accepts the following parameters:
* `account_first_and_last_bounces` (boolean): if `True`, the first and last bounces are accounted
in the computations of the optical path length of the temporal dimension.
This makes sense if you think of a NLOS setup.
If `False`, the first and last bounces are not accounted (useful!)
* `sensor_origin` (point): position of the sensor (NLOS setup) in the world coordinate system
* `original_film_{width|height}` (integer): special for confocal captures, you can ignore
if you use one illumination point or an exhaustive pattern.
If you want to simulate a confocal NLOS setup with NxM, you should use a 1x1 film instead of NxM,
and point the laser to the point that you want to capture. Then you should repeat the capture
NxM times.
We strongly recommend using TAL (see https://github.com/diegoroyo/tal), and
set `scan_type: confocal` in the tal render YAML configuration file, which will
handle all this automatically.
See also the parameters for `transient_hdr_film`.
"""
# TODO(diego): we assume the rays start in a vacuum
# this is reasonable for NLOS scenes, but this can be changed
# in the future if we want to support other media
IOR_BASE = 1
def __init__(self, props: mi.Properties):
super().__init__(props)
self.needs_sample_3: bool = False
self.account_first_and_last_bounces: bool = \
props.get('account_first_and_last_bounces', True)
self.world_transform: Transform4f = \
Transform4f.translate(
props.get('sensor_origin', ScalarArray3f(0)))
# Distance between the laser origin and the focusing point
# Should be provided by the user if needed
# see mitransient.nlos.focus_emitter_at_relay_wall
self.laser_bounce_opl = mi.Float(0)
self.laser_target = mi.Point3f(0)
# Get the film size. Depends on if the capture is confocal or not
self.original_film_width = props.get('original_film_width', None)
self.original_film_height = props.get('original_film_height', None)
if self.original_film_width is None or self.original_film_height is None:
self.film_size: ScalarVector2f = ScalarVector2f(self.film().size())
self.is_confocal = False
else:
self.film_size: ScalarVector2f = ScalarVector2f(
self.original_film_width, self.original_film_height)
self.is_confocal = True
if self.film().size().x != 1 or self.film().size().y != 1:
Log(LogLevel.Error,
f"Confocal configuration requires a film with size [1,1] instead of {self.film().size()}")
dr.make_opaque(self.laser_bounce_opl,
self.laser_target, self.film_size)
def _sensor_origin(self) -> ScalarArray3f:
return self.world_transform.translation()
def _pixel_to_sample(self, pixel: mi.Point2f) -> mi.Point2f:
return pixel / self.film_size
def _sample_direction(self,
time: mi.Float,
sample: mi.Point2f,
active: mi.Mask) -> Tuple[mi.Float, mi.Vector3f]:
origin = self._sensor_origin()
if self.is_confocal:
# Confocal sample always center of the pixel
target = self.laser_target
else:
# instead of continuous samples over the whole shape,
# discretize samples so they only land on the center of the film's
# "pixels"
grid_sample = self._pixel_to_sample(
dr.floor(sample * self.film_size) + 0.5)
target = self.shape().sample_position(
time, grid_sample, active
).p # sampled position of PositionSample3f
direction = target - origin
distance = dr.norm(direction)
direction /= distance
return distance, direction
def sample_ray_differential(
self, time: mi.Float,
sample1: mi.Float, sample2: mi.Point2f, sample3: mi.Point2f,
active: mi.Bool = True) -> Tuple[mi.RayDifferential3f, mi.Color3f]:
origin = self._sensor_origin()
sensor_distance, direction = self._sample_direction(
time, sample2, active)
if is_spectral:
wav_sample = mi.sample_shifted(sample1)
wavelengths, wav_weight = mi.sample_rgb_spectrum(wav_sample)
else:
wavelengths = []
wav_weight = 1.0
if not self.account_first_and_last_bounces:
time -= self.laser_bounce_opl + sensor_distance * self.IOR_BASE
# NOTE: removed * dr.pi because there is no need to account for this
return (
mi.RayDifferential3f(origin, direction, time, wavelengths),
mi.unpolarized_spectrum(wav_weight) # * dr.pi
)
def pdf_direction(self,
it: mi.Interaction3f,
ds: mi.DirectionSample3f,
active: mi.Bool = True) -> mi.Float:
# NOTE(diego): this could be used in sample_ray_differential
# but other sensors do not do it (e.g. for a thin lens camera,
# vignetting is not accounted for using this function)
return self.shape().pdf_direction(it, ds, active)
def eval(self, si: mi.SurfaceInteraction3f, active: mi.Bool = True) -> mi.Spectrum:
return dr.pi / self.shape().surface_area()
def bbox(self) -> mi.BoundingBox3f:
return self.shape().bbox()
def traverse(self, callback: mi.TraversalCallback):
super().traverse(callback)
callback.put_parameter(
"needs_sample_3", self.needs_sample_3, mi.ParamFlags.NonDifferentiable)
callback.put_parameter("account_first_and_last_bounces",
self.account_first_and_last_bounces, mi.ParamFlags.NonDifferentiable)
callback.put_parameter(
"is_confocal", self.is_confocal, mi.ParamFlags.NonDifferentiable)
callback.put_parameter(
"laser_bounce_opl", self.laser_bounce_opl, mi.ParamFlags.NonDifferentiable)
callback.put_parameter(
"laser_target", self.laser_target, mi.ParamFlags.NonDifferentiable)
def parameters_changed(self, keys):
super().parameters_changed(keys)
def to_string(self):
string = f"{type(self).__name__}[\n"
string += f" laser_bounce_opl = {self.laser_bounce_opl}, \n"
string += f" account_first_and_last_bounces = {self.account_first_and_last_bounces}, \n"
string += f" is_confocal = {self.is_confocal}, \n"
if self.is_confocal:
string += f" laser_target = {self.laser_target}, \n"
string += f" film = { indent(self.film()) }, \n"
string += f"]"
return string
mi.register_sensor('nlos_capture_meter', lambda props: NLOSCaptureMeter(props))