Skip to content

photogrammetry

Code for handling and processing photogrammetry data

Dataset dataclass

Container class for photogrammetry dataset. Provides a dict like interface for accessing points by their labels.

Attributes:

Name Type Description
data_dict dict[str, NDArray[float32]]

Dict of photogrammetry points. You should genrally not touch this directly.

Source code in lat_alignment/photogrammetry.py
 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
@dataclass
class Dataset:
    """
    Container class for photogrammetry dataset.
    Provides a dict like interface for accessing points by their labels.

    Attributes
    ----------
    data_dict : dict[str, NDArray[np.float32]]
        Dict of photogrammetry points.
        You should genrally not touch this directly.
    """

    data_dict: dict[str, NDArray[np.float32]]

    def _clear_cache(self):
        self.__dict__.pop("points", None)
        self.__dict__.pop("labels", None)
        self.__dict__.pop("codes", None)
        self.__dict__.pop("code_labels", None)
        self.__dict__.pop("target", None)
        self.__dict__.pop("target_labels", None)

    def __setattr__(self, name, value):
        if name == "data_dict":
            self._clear_cache()
        return super().__setattr__(name, value)

    def __setitem__(self, key, item):
        self._clear_cache()
        self.data_dict[key] = item

    def __getitem__(self, key):
        return self.data_dict[key]

    def __repr__(self):
        return repr(self.data_dict)

    def __len__(self):
        return len(self.data_dict)

    def __delitem__(self, key):
        self._clear_cache()
        del self.data_dict[key]

    def __contains__(self, item):
        return item in self.data_dict

    def __iter__(self):
        return iter(self.data_dict)

    @cached_property
    def points(self) -> NDArray[np.float32]:
        """
        Get all points in the dataset as an array.
        This is cached.
        """
        return np.array(list(self.data_dict.values()))

    @cached_property
    def labels(self) -> NDArray[np.str_]:
        """
        Get all labels in the dataset as an array.
        This is cached.
        """
        return np.array(list(self.data_dict.keys()))

    @cached_property
    def codes(self) -> NDArray[np.float32]:
        """
        Get all coded points in the dataset as an array.
        This is cached.
        """
        msk = np.char.find(self.labels, "CODE") >= 0
        return self.points[msk]

    @cached_property
    def code_labels(self) -> NDArray[np.str_]:
        """
        Get all coded labels in the dataset as an array.
        This is cached.
        """
        msk = np.char.find(self.labels, "CODE") >= 0
        return self.labels[msk]

    @cached_property
    def targets(self) -> NDArray[np.float32]:
        """
        Get all target points in the dataset as an array.
        This is cached.
        """
        msk = np.char.find(self.labels, "TARGET") >= 0
        return self.points[msk]

    @cached_property
    def target_labels(self) -> NDArray[np.str_]:
        """
        Get all target labels in the dataset as an array.
        This is cached.
        """
        msk = np.char.find(self.labels, "TARGET") >= 0
        return self.labels[msk]

    def copy(self) -> Self:
        """
        Make a deep copy of the dataset.

        Returns
        -------
        copy : Dataset
            A deep copy of this dataset.
        """
        return deepcopy(self)

code_labels cached property

Get all coded labels in the dataset as an array. This is cached.

codes cached property

Get all coded points in the dataset as an array. This is cached.

labels cached property

Get all labels in the dataset as an array. This is cached.

points cached property

Get all points in the dataset as an array. This is cached.

target_labels cached property

Get all target labels in the dataset as an array. This is cached.

targets cached property

Get all target points in the dataset as an array. This is cached.

copy()

Make a deep copy of the dataset.

Returns:

Name Type Description
copy Dataset

A deep copy of this dataset.

Source code in lat_alignment/photogrammetry.py
125
126
127
128
129
130
131
132
133
134
def copy(self) -> Self:
    """
    Make a deep copy of the dataset.

    Returns
    -------
    copy : Dataset
        A deep copy of this dataset.
    """
    return deepcopy(self)

align_photo(dataset, reference, kill_refs, element='primary', scale=True, *, plot=True, max_dist=100.0)

Align photogrammetry data and then put it into mirror coordinates.

Parameters:

Name Type Description Default
dataset Dataset

The photogrammetry data to align.

required
reference dict

Reference dictionary. Should contain a key called coords that specifies the coordinate system that the reference points are in. The rest of the keys should be optical elements (ie: "primary") pointing to a list of reference points to use. Each point given should be a tuple with two elements. The first element is a tuple with the (x, y, z) coordinates of the point in the global coordinate system. The second is a list of nearby coded targets that can be used to identify the point.

required
kill_refs bool

If True remove reference points from the dataset.

required
element str

The element that these points belong to. Should be either: 'primary', 'secondary', 'bearing', or 'receiver'.

'primary'
plot bool

If True show a diagnostic plot of how well the reference points are aligned.

True
max_dist float

Max distance in mm that the reference poing can be from the target point used to locate it.

100

Returns:

Name Type Description
aligned Dataset

The photogrammetry data aligned to the reference points.

alignment tuple[NDArray[float32], NDArray[float32]]

The transformation that aligned the points. The first element is a rotation matrix and the second is the shift.

Source code in lat_alignment/photogrammetry.py
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
def align_photo(
    dataset: Dataset,
    reference: dict,
    kill_refs: bool,
    element: str = "primary",
    scale: bool = True,
    *,
    plot: bool = True,
    max_dist: float = 100.0,
) -> tuple[
    Dataset,
    tuple[NDArray[np.float32], NDArray[np.float32]],
]:
    """
    Align photogrammetry data and then put it into mirror coordinates.

    Parameters
    ----------
    dataset : Dataset
        The photogrammetry data to align.
    reference : dict
        Reference dictionary.
        Should contain a key called `coords` that specifies the
        coordinate system that the reference points are in.
        The rest of the keys should be optical elements (ie: "primary")
        pointing to a list of reference points to use.
        Each point given should be a tuple with two elements.
        The first element is a tuple with the (x, y, z) coordinates
        of the point in the global coordinate system.
        The second is a list of nearby coded targets that can be used
        to identify the point.
    kill_refs : bool
        If True remove reference points from the dataset.
    element: str, default: 'primary'
        The element that these points belong to.
        Should be either: 'primary', 'secondary', 'bearing', or 'receiver'.
    plot : bool, default: True
        If True show a diagnostic plot of how well the reference points
        are aligned.
    max_dist : float, default: 100
        Max distance in mm that the reference poing can be from the target
        point used to locate it.

    Returns
    -------
    aligned : Dataset
        The photogrammetry data aligned to the reference points.
    alignment : tuple[NDArray[np.float32], NDArray[np.float32]]
        The transformation that aligned the points.
        The first element is a rotation matrix and
        the second is the shift.
    """
    logger.info("\tAligning with reference points for %s", element)
    elements = ["primary", "secondary", "bearing", "receiver"]
    if element not in elements and element != "all":
        raise ValueError(f"Invalid element: {element}")
    if len(reference) == 0:
        raise ValueError("Invalid or empty reference")
    if element not in reference and element != "all":
        raise ValueError("Element not found in reference dict")
    if "coords" not in reference:
        raise ValueError("Reference coordinate system not specified")
    if element == "primary":
        transform = partial(
            coord_transform, cfrom=reference["coords"], cto="opt_primary"
        )
    elif element == "secondary":
        transform = partial(
            coord_transform, cfrom=reference["coords"], cto="opt_secondary"
        )
    else:
        transform = partial(
            coord_transform, cfrom=reference["coords"], cto="opt_global"
        )
    if element == "all":
        all_refs = []
        for el in elements:
            if el not in reference:
                continue
            all_refs += reference[el]
        reference["all"] = all_refs

    # Lets find the points we can use
    ref = []
    pts = []
    invars = []
    for rpoint, codes in reference[element]:
        codes = np.array(codes)
        have = np.isin(codes, dataset.code_labels)
        if np.sum(have) == 0:
            continue
        coded = dataset[codes[have][0]]
        # Find the closest point
        dist = np.linalg.norm(dataset.targets - coded, axis=-1)
        if np.min(dist) > max_dist:
            continue
        label = dataset.target_labels[np.argmin(dist)]
        ref += [rpoint]
        pts += [dataset[label]]
        invars += [label]
    if len(ref) < 4:
        raise ValueError(f"Only {len(ref)} reference points found! Can't align!")
    logger.debug(
        "\t\tFound %d reference points in measurements with labels:\n\t\t\t%s",
        len(pts),
        str(invars),
    )
    pts = np.vstack(pts)
    ref = np.vstack(ref)
    pts = np.vstack((pts, np.mean(pts, 0)))
    ref = np.vstack((ref, np.mean(ref, 0)))
    ref = transform(ref)
    logger.debug("\t\tReference points in element coords:\n%s", str(ref))
    scale_fac = 1
    if scale:
        triu_idx = np.triu_indices(len(pts), 1)
        scale_fac = np.nanmedian(make_edm(ref)[triu_idx] / make_edm(pts)[triu_idx])
        pts *= scale_fac
    logger.debug("\t\tScale factor of %f applied", scale_fac)

    rot, sft = get_rigid(pts, ref, method="mean")
    pts_t = apply_transform(pts, rot, sft)

    if plot:
        fig = plt.figure()
        ax = fig.add_subplot(projection="3d")
        ax.scatter(pts[:, 0], pts[:, 1], pts[:, 2], color="g")
        ax.scatter(pts_t[:, 0], pts_t[:, 1], pts_t[:, 2], color="b")
        ax.scatter(ref[:, 0], ref[:, 1], ref[:, 2], color="r", marker="X")
        plt.show()
    logger.info(
        "\t\tRMS of reference points after alignment: %f",
        np.sqrt(np.mean((pts_t - ref) ** 2)),
    )
    coords_transformed = apply_transform(dataset.points * scale_fac, rot, sft)
    labels = dataset.labels

    if kill_refs:
        msk = ~np.isin(dataset.labels, invars)
        labels = labels[msk]
        coords_transformed = coords_transformed[msk]

    data = {label: coord for label, coord in zip(labels, coords_transformed)}
    transformed = Dataset(data)

    logger.debug("\t\tShift is %s mm", str(sft))
    logger.debug("\t\tRotation is %s deg", str(np.rad2deg(decompose_rotation(rot))))
    scale_fac = np.eye(3) * scale_fac
    rot @= scale_fac

    if plot:
        fig = plt.figure()
        ax = fig.add_subplot(projection="3d")
        ax.scatter(
            transformed.targets[:, 0],
            transformed.targets[:, 1],
            transformed.targets[:, 2],
            marker="x",
        )
        plt.show()

    return transformed, (rot, sft)