196 Commits

Author SHA1 Message Date
Jeremy Reizenstein
502f15aca7 avoid recalculating normals for simple move
Summary: If offset_verts_ is used to move meshes with a single vector, the normals won't change so don't need to recalculate. I am planning to allow user-specified vertex normals. This change means that user-specified vertex normals won't get overwritten when they don't need to be.

Reviewed By: nikhilaravi

Differential Revision: D27765256

fbshipit-source-id: f6e4d308ac9ac023030325cb75a18d39b966cf88
2021-05-04 05:36:51 -07:00
Jeremy Reizenstein
e9f4e0d086 PLY color scaling
Summary: When a PLY file contains colors in byte format, these are now scaled from 0..255 to [0,1], as they should be

Reviewed By: gkioxari

Differential Revision: D27765254

fbshipit-source-id: 526b5f5149d5e8cbffd7412b411be52c935fa4ad
2021-05-04 05:36:51 -07:00
Jeremy Reizenstein
6c3fe952d1 PLY TexturesVertex loading
Summary:
Include TexturesVertex colors when loading and saving Meshes to PLY files.

A couple of other improvements to the internals of ply_io, including using `None` instead of empty tensors for some missing data.

Reviewed By: gkioxari

Differential Revision: D27765260

fbshipit-source-id: b9857dc777c244b9d7d6643b608596d31435ecda
2021-05-04 05:36:51 -07:00
JudyYe
eb04a488c5 TexturesVertex._num_verts_per_mesh deep copy (#623)
Summary:
When a list of Meshes is `join_batched()`, the `num_verts_per_mesh` in the list would be unexpectedly modified.

Also some cleanup around `_num_verts_per_mesh`.

Pull Request resolved: https://github.com/facebookresearch/pytorch3d/pull/623

Test Plan: A modification to an existing test checks this.

Reviewed By: nikhilaravi

Differential Revision: D27682104

Pulled By: bottler

fbshipit-source-id: 9d00913dfb4869bd6c7d3f5cc9156b7b6f1aecc9
2021-04-20 03:11:34 -07:00
Jeremy Reizenstein
c18ee9d40a lint fixes
Summary: Lint after recent changes.

Reviewed By: nikhilaravi

Differential Revision: D27682328

fbshipit-source-id: 285d159010d886e4e97902995adbdff875fd3c19
2021-04-12 19:10:18 -07:00
Jeremy Reizenstein
124bb5e391 spelling
Summary: Collection of spelling things, mostly in docs / tutorials.

Reviewed By: gkioxari

Differential Revision: D26101323

fbshipit-source-id: 652f62bc9d71a4ff872efa21141225e43191353a
2021-04-09 09:58:54 -07:00
Rong Rong (AI Infra)
c2e62a5087 Allow tests to be run on GPU with remote execution
Summary: Test path special case

Reviewed By: bottler

Differential Revision: D27566817

fbshipit-source-id: c7b3ac839908c071f1378a37b7013b91ca4e8b18
2021-04-08 20:03:04 -07:00
Rong Rong (AI Infra)
dd8343922e Get rid of duplicate test data directory initialization
Summary: Simplify finding the data directories in the tests.

Reviewed By: nikhilaravi

Differential Revision: D27634293

fbshipit-source-id: dc308a7c86c41e6fae56a2ab58187c9f0335b575
2021-04-08 20:03:04 -07:00
Rong Rong (AI Infra)
1216b5765a Extract finding directories for test data
Summary: Make common functions for finding directories where test data is found, instead of lots of tests using their own `__file__`  while trying to get ./tests/data and the tutorials data.

Reviewed By: nikhilaravi

Differential Revision: D27633701

fbshipit-source-id: 1467bb6018cea16eba3cab097d713116d51071e9
2021-04-08 20:03:04 -07:00
Jeremy Reizenstein
24ee279005 Fix flake exceptions
Summary: flake8 no longer respects the black fmt:off message, so include specific line-length exclusion.

Reviewed By: nikhilaravi

Differential Revision: D27624641

fbshipit-source-id: adcdb6f55b382fbf252eede3f3ddeda0621da883
2021-04-08 09:38:43 -07:00
Jeremy Reizenstein
cc08c6b288 CI fixes
Summary:
Update `main` build to latest CircleCI image - Ubuntu 2020.04.

Avoid torch.logical_or and logical_and for PyTorch 1.4 compatibility.

Also speed up the test run with Pytorch 1.4.0 (which has no ninja) by not setting NVCC_FLAGS for it.

Reviewed By: theschnitz

Differential Revision: D27262327

fbshipit-source-id: ddc359d134b1dc755f8b20bd3f33bb080cb3a0e1
2021-03-23 14:25:25 -07:00
Jeremy Reizenstein
ff9c6612b4 Use old style isfinite
Summary: Avoid using the newish member function isfinite. We use torch.isfinite instead for torch 1.4.0 compatibility.

Reviewed By: nikhilaravi

Differential Revision: D26946672

fbshipit-source-id: 853c3716f40061152f1ea54a39eb60b565de7c2c
2021-03-11 03:13:48 -08:00
Nikhila Ravi
13429640d3 Bug fix for case where aspect ratio is a float
Summary:
- Fix the calculation of the non square NDC range when the H and W are not integer multiples.
- Add test for this case

Reviewed By: gkioxari

Differential Revision: D26613213

fbshipit-source-id: df6763cac602e9f1d516b41b432c4d2cfbaa356d
2021-02-24 10:07:17 -08:00
Jeremy Reizenstein
0345f860d4 Loading/saving meshes to OFF files.
Summary: Implements the ascii OFF file format. This was discussed in https://github.com/facebookresearch/pytorch3d/issues/216

Reviewed By: theschnitz

Differential Revision: D25788834

fbshipit-source-id: c141d1f4ba3bad24e3c1f280a20aee782bfd74d6
2021-02-12 07:05:55 -08:00
Jeremy Reizenstein
5ac2f42184 test & compilation fixes
Summary:
Fixes mostly related to the "main" build on circleci.
-Avoid error to do with tuple copy from initializer_list which is `explicit` on old compiler.
-Add better reporting to copyright test.
-Move to PackedTensorAccessor64 from the deprecated PackedTensorAccessor
-Avoid some warnings about mismatched comparisons.

The "main" build is the only one that runs the test_build stuff. In that area
-Fix my bad copyright fix D26275931 (3463f418b8) / 965c9c
-Add test that all tutorials are valid json.

Reviewed By: nikhilaravi

Differential Revision: D26366466

fbshipit-source-id: c4ab8b7e6647987069f7cb7144aa6ab7c24bcdac
2021-02-11 11:06:08 -08:00
Shubham Goel
e13e63a811 bugfix in cotcurv laplacian loss. closes #551 (#553)
Summary: Pull Request resolved: https://github.com/facebookresearch/pytorch3d/pull/553

Reviewed By: theschnitz

Differential Revision: D26257591

Pulled By: gkioxari

fbshipit-source-id: 899a3f733a77361e8572b0900a34b55764ff08f2
2021-02-10 21:23:17 -08:00
Nikhila Ravi
340662e98e CUDA/C++ Rasterizer updates to handle clipped faces
Summary:
- Updated the C++/CUDA mesh rasterization kernels to handle the clipped faces. In particular this required careful handling of the distance calculation for faces which are cut into a quadrilateral by the image plane and then split into two sub triangles i.e. both sub triangles can't be part of the top K faces.
- Updated `rasterize_meshes.py` to use the utils functions to clip the meshes and convert the fragments back to in terms of the unclipped mesh
- Added end to end tests

Reviewed By: jcjohnson

Differential Revision: D26169685

fbshipit-source-id: d64cd0d656109b965f44a35c301b7c81f451cfa0
2021-02-08 14:32:39 -08:00
Nikhila Ravi
838b73d3b6 Updates to cameras and rasterizer to infer camera type correctly
Summary: Small update to the cameras and rasterizer to correctly infer the type of camera (perspective vs orthographic).

Reviewed By: jcjohnson

Differential Revision: D26267225

fbshipit-source-id: a58ed3bc2ab25553d2a4307c734204c1d41b5176
2021-02-08 14:32:39 -08:00
Nikhila Ravi
23279c5f1d Utils for clipping mesh faces partially behind the image plane
Summary:
Instead of culling faces behind the camera, partially clip them if they intersect with the image plane.

This diff implements the utils functions for clipping.

There are 4 cases for the mesh faces which are all handled:

```
Case 1: the triangle is completely in front of the clipping plane (it is left
        unchanged)
Case 2: the triangle is completely behind the clipping plane (it is culled)
Case 3: the triangle has exactly two vertices behind the clipping plane (it is
        clipped into a smaller triangle)
Case 4: the triangle has exactly one vertex behind the clipping plane (it is clipped
        into a smaller quadrilateral and divided into two triangular faces)
```

Reviewed By: jcjohnson

Differential Revision: D23108673

fbshipit-source-id: 550a8b6a982d06065dff10aba10d47e8b144ae52
2021-02-05 18:30:15 -08:00
Jeremy Reizenstein
cd9786e787 Disable random-dependent tests
Summary:
These two tests fail (with non-small differences) when the seed is changed or if certain environmental changes are made. We disable them pending investigation.

A small change to the tolerance at the failing assertion doesn't help. The change in common_testing helps diagnose this.

Reviewed By: shapovalov

Differential Revision: D26233419

fbshipit-source-id: 357afc1786825256c9bade101fb15707e4dea5ed
2021-02-03 18:24:27 -08:00
Jeremy Reizenstein
e42b0c4f70 Mesh normal consistency when no faces intersect
Summary: Corner case where there's nothing to do in this function.

Reviewed By: nikhilaravi

Differential Revision: D26073476

fbshipit-source-id: eb061683ffe35c1ffa8384c422a1557a636d52cd
2021-01-27 17:32:16 -08:00
Jeremy Reizenstein
7f62eacdb2 Mesh normal consistency when many faces intersect
Summary: We were double counting some pairs in some cases. Specifically if four or more faces share an edge, then some of them were getting double counted. This is a minimal tweak to avoid that.

Reviewed By: nikhilaravi

Differential Revision: D26073477

fbshipit-source-id: a40032acf3044bb98dd91cb29904614ef64d5599
2021-01-27 17:32:16 -08:00
Jeremy Reizenstein
d173a2f8da textures device consistency
Summary: Ensure that `mesh2 = mesh.to(device)` doesn't change the device of `mesh.textures`.

Reviewed By: nikhilaravi

Differential Revision: D25978610

fbshipit-source-id: 0558cd62132965d8693ebeea05e42b8c1d16cfbf
2021-01-25 06:09:56 -08:00
Jeremy Reizenstein
cf9bb7c48c (eye, at, up) extraction function
Summary:
Plotly viewing from a specific camera location requires converting that location in to an (eye, at, up) specification. There may be other reasons to want to do this as well. I create a separate utility function for it.

I envisage more such utility functions for manipulating camera information, so I create a separate camera_utils.py file for such things.

Reviewed By: nikhilaravi

Differential Revision: D25981184

fbshipit-source-id: 0947bf98b212676c021f2fddf775bf436dee3487
2021-01-22 07:33:31 -08:00
Jeremy Reizenstein
ddebdfbcd7 Allow single offset in offset_verts
Summary:
It is common when trying things out to want to move a whole mesh or point cloud by the same amount. Here we allow the offset functions to broadcast.

Also add a sanity check to join_meshes_as_scene which it is easy to call wrongly.

Reviewed By: nikhilaravi

Differential Revision: D25980593

fbshipit-source-id: cdf1568e1317e3b81ad94ed4e608ba7eef81290b
2021-01-22 07:33:31 -08:00
Jeremy Reizenstein
d60c52df4a devices for transform3d
Summary: Make `to` on Transform3D carry its member _transforms.

Reviewed By: nikhilaravi

Differential Revision: D25978611

fbshipit-source-id: 12b39e7a657f28d59ca60800bf9f4193a2c08197
2021-01-21 04:58:40 -08:00
Jeremy Reizenstein
4711665edb lint
Summary: Fix recent lint.

Reviewed By: nikhilaravi

Differential Revision: D25900168

fbshipit-source-id: 6b6e8d35b68c8415ef305dc4719f43eda9316c8f
2021-01-20 13:08:35 -08:00
Jeremy Reizenstein
95707fba1c PLY pointcloud loading
Summary:
Allow PLY files to not contain faces. Allow loading pointclouds with color, at least encoded according to the way of some cloudcompare examples.

TODO: Allow vertex normals to be read, and allow vertex colors to be written. Make the return type of load_ply something more user friendly, like a dict.

Noticed in https://github.com/facebookresearch/pytorch3d/issues/209

Reviewed By: nikhilaravi

Differential Revision: D22573314

fbshipit-source-id: 72ba1f7c6417f5dfc83f2ebf359eff017057635c
2021-01-07 15:40:11 -08:00
Jeremy Reizenstein
3b9fbfc08c Read heterogenous nonlist PLY properties as arrays
Summary:
In the original implementation, I had considered PLY properties where there are mixed types of elements in a property to be rare and basically unimportant, so the implementation is very naive.

If we want to support pointcloud PLY files, we need to handle at least the subcase where there are no lists efficiently because this seems to be very common there.

Reviewed By: nikhilaravi, gkioxari

Differential Revision: D22573315

fbshipit-source-id: db6f29446d4e555a2e2b37d38c8e4450d061465b
2021-01-07 15:40:11 -08:00
Jeremy Reizenstein
89532a876e add existing mesh formats to pluggable
Summary: We already have code for obj and ply formats. Here we actually make it available in `IO.load_mesh` and `IO.save_mesh`.

Reviewed By: theschnitz, nikhilaravi

Differential Revision: D25400650

fbshipit-source-id: f26d6d7fc46c48634a948eea4d255afad13b807b
2021-01-07 15:40:11 -08:00
David Novotny
b466c381da Implicit/Volume renderer
Summary: Implements the `ImplicitRenderer` and `VolumeRenderer`.

Reviewed By: gkioxari

Differential Revision: D24418791

fbshipit-source-id: 127f21186d8e210895db1dcd0681f09f230d81a4
2021-01-06 06:23:48 -08:00
generatedunixname89002005287564
e6a32bfc37 Daily arc lint --take BLACK
Reviewed By: zertosh

Differential Revision: D25800514

fbshipit-source-id: 191b2753b8fcfbe2386c761241aaeb58939a973e
2021-01-06 04:23:26 -08:00
David Novotny
e6bc960fb5 Raysampling
Summary: Implements 3 basic raysamplers.

Reviewed By: nikhilaravi

Differential Revision: D24110643

fbshipit-source-id: eb67d0e56773c7871ebdcb23e7e520302dc1b3c9
2021-01-06 04:01:29 -08:00
David Novotny
1af1a36bd6 Raymarching
Summary: Implements two basic raymarchers.

Reviewed By: gkioxari

Differential Revision: D24064250

fbshipit-source-id: 18071bd039995336b7410caa403ea29fafb5c66f
2021-01-05 03:59:20 -08:00
David Novotny
aa9bcaf04c Point clouds to volumes
Summary:
Conversion from point clouds to volumes

```
Benchmark                                                        Avg Time(μs)      Peak Time(μs) Iterations
--------------------------------------------------------------------------------
ADD_POINTS_TO_VOLUMES_10_trilinear_[25, 25, 25]_1000                 43219           44067             12
ADD_POINTS_TO_VOLUMES_10_trilinear_[25, 25, 25]_10000                43274           45313             12
ADD_POINTS_TO_VOLUMES_10_trilinear_[25, 25, 25]_100000               46281           47100             11
ADD_POINTS_TO_VOLUMES_10_trilinear_[101, 111, 121]_1000              51224           51912             10
ADD_POINTS_TO_VOLUMES_10_trilinear_[101, 111, 121]_10000             52092           54487             10
ADD_POINTS_TO_VOLUMES_10_trilinear_[101, 111, 121]_100000            59262           60514              9
ADD_POINTS_TO_VOLUMES_10_nearest_[25, 25, 25]_1000                   15998           17237             32
ADD_POINTS_TO_VOLUMES_10_nearest_[25, 25, 25]_10000                  15964           16994             32
ADD_POINTS_TO_VOLUMES_10_nearest_[25, 25, 25]_100000                 16881           19286             30
ADD_POINTS_TO_VOLUMES_10_nearest_[101, 111, 121]_1000                19150           25277             27
ADD_POINTS_TO_VOLUMES_10_nearest_[101, 111, 121]_10000               18746           19999             27
ADD_POINTS_TO_VOLUMES_10_nearest_[101, 111, 121]_100000              22321           24568             23
ADD_POINTS_TO_VOLUMES_100_trilinear_[25, 25, 25]_1000                49693           50288             11
ADD_POINTS_TO_VOLUMES_100_trilinear_[25, 25, 25]_10000               51429           52449             10
ADD_POINTS_TO_VOLUMES_100_trilinear_[25, 25, 25]_100000             237076          237377              3
ADD_POINTS_TO_VOLUMES_100_trilinear_[101, 111, 121]_1000             81875           82597              7
ADD_POINTS_TO_VOLUMES_100_trilinear_[101, 111, 121]_10000           106671          107045              5
ADD_POINTS_TO_VOLUMES_100_trilinear_[101, 111, 121]_100000          483740          484607              2
ADD_POINTS_TO_VOLUMES_100_nearest_[25, 25, 25]_1000                  16667           18143             31
ADD_POINTS_TO_VOLUMES_100_nearest_[25, 25, 25]_10000                 17682           18922             29
ADD_POINTS_TO_VOLUMES_100_nearest_[25, 25, 25]_100000                65463           67116              8
ADD_POINTS_TO_VOLUMES_100_nearest_[101, 111, 121]_1000               48058           48826             11
ADD_POINTS_TO_VOLUMES_100_nearest_[101, 111, 121]_10000              53529           53998             10
ADD_POINTS_TO_VOLUMES_100_nearest_[101, 111, 121]_100000            123684          123901              5
--------------------------------------------------------------------------------
```

Output with `DEBUG=True`
{F338561209}

Reviewed By: nikhilaravi

Differential Revision: D22017500

fbshipit-source-id: ed3e8ed13940c593841d93211623dd533974012f
2021-01-05 03:39:24 -08:00
David Novotny
03ee1dbf82 Volumes data structure.
Summary: Implemented a data structure for volumes.

Reviewed By: gkioxari

Differential Revision: D20342920

fbshipit-source-id: ccc23eaa183ed8a4e9cd7674b4dcf31e8a65c3c6
2021-01-05 03:39:24 -08:00
David Novotny
1e4a2e8624 __getitem__ for Transform3D
Summary: Implements the `__getitem__` method for `Transform3D`

Reviewed By: nikhilaravi

Differential Revision: D23813975

fbshipit-source-id: 5da752ed8ea029ad0af58bb7a7856f0995519b7a
2021-01-05 03:39:24 -08:00
David Novotny
b4dea43963 Support for multi-dimensional list_to_padded/padded_to_list.
Summary: Extends `list_to_padded`/`padded_to_list` to work for tensors with an arbitrary number of input dimensions.

Reviewed By: nikhilaravi, gkioxari

Differential Revision: D23813969

fbshipit-source-id: 52c212a2ecdb3c4dfb6ac47217715e07998f37f1
2021-01-04 09:42:52 -08:00
Jeremy Reizenstein
25c065e9da PathManager passing
Summary:
Make no internal functions inside pytorch3d/io interpret str paths except using a PathManager from iopath which they have been given. This means we no longer use any global PathManager object and we no longer use fvcore's deprecated file_io.

To preserve the APIs, various top level functions create their own default-initialized PathManager object if they are not provided one.

Reviewed By: theschnitz

Differential Revision: D25372969

fbshipit-source-id: c176ee31439645fa54a157d6f1aef18b09501569
2020-12-24 10:16:03 -08:00
Jeremy Reizenstein
b95621573b lint
Summary: Allowing usort, isort and black to coexist without fighting means we can't have imports commented as deprecated from the same module as other imports.

Reviewed By: nikhilaravi

Differential Revision: D25372970

fbshipit-source-id: 637f5a0025c0df9fbec47cba73ce5387f4f8b467
2020-12-24 10:16:03 -08:00
Nikhila Ravi
ebac66daeb Classic Marching Cubes algorithm implementation
Summary:
Defines a function to run marching cubes algorithm on a single or batch of 3D scalar fields. Returns a mesh's faces and vertices.

UPDATES (12/18)
- Input data is now specified as a (B, D, H, W) tensor as opposed to a (B, W, H, D) tensor. This will now be compatible with the Volumes datastructure.
- Add an option to return output vertices in local coordinates instead of world coordinates.
Also added a small fix to remove the dype for device in Transforms3D - if passing in a torch.device instead of str it causes a pyre error.

Reviewed By: jcjohnson

Differential Revision: D24599019

fbshipit-source-id: 90554a200319fed8736a12371cc349e7108aacd0
2020-12-18 07:25:50 -08:00
Nikhila Ravi
01759d8ffb Texture Atlas sampling bug fix
Summary: Fixes the index out of bound errors for texture sampling from a texture atlas: when barycentric coordinates are 1.0, the integer index into the (R, R) per face texture map is R (max can only be R-1).

Reviewed By: gkioxari

Differential Revision: D25543803

fbshipit-source-id: 82d0935b981352b49c1d95d5a17f9cc88bad0a82
2020-12-17 04:10:56 -08:00
Nikhila Ravi
3d769a66cb Non Square image rasterization for pointclouds
Summary:
Similar to non square image rasterization for meshes, apply the same updates to the pointcloud rasterizer.

Main API Change:
- PointRasterizationSettings now accepts a tuple/list of (H, W) for the image size.

Reviewed By: jcjohnson

Differential Revision: D25465206

fbshipit-source-id: 7370d83c431af1b972158cecae19d82364623380
2020-12-15 14:15:32 -08:00
Evgeniy Zheltonozhskiy
569e5229a9 Add check for verts and faces being on same device and also checks for pointclouds/features/normals being on the same device (#384)
Summary: Pull Request resolved: https://github.com/facebookresearch/pytorch3d/pull/384

Test Plan: `test_meshes` and `test_points`

Reviewed By: gkioxari

Differential Revision: D24730524

Pulled By: nikhilaravi

fbshipit-source-id: acbd35be5d9f1b13b4d56f3db14f6e8c2c0f7596
2020-12-14 16:18:19 -08:00
Nikhila Ravi
19340462e4 Return self in the to method for the renderer classes
Summary: Add `return self` to the `to` function for the renderer classes.

Reviewed By: bottler

Differential Revision: D25534487

fbshipit-source-id: e8dbd35524f0bd40e835439e93184b5a1f1532ca
2020-12-14 15:28:04 -08:00
Nikhila Ravi
d07307a451 Non square image rasterization for meshes
Summary:
There are a couple of options for supporting non square images:
1) NDC stays at [-1, 1] in both directions with the distance calculations all modified by (W/H). There are a lot of distance based calculations (e.g. triangle areas for barycentric coordinates etc) so this requires changes in many places.
2) NDC is scaled by (W/H) so the smallest side has [-1, 1]. In this case none of the distance calculations need to be updated and only the pixel to NDC calculation needs to be modified.

I decided to go with option 2 after trying option 1!

API Changes:
- Image size can now be specified optionally as a tuple

TODO:
- add a benchmark test for the non square case.

Reviewed By: jcjohnson

Differential Revision: D24404975

fbshipit-source-id: 545efb67c822d748ec35999b35762bce58db2cf4
2020-12-09 09:18:11 -08:00
Georgia Gkioxari
112959e087 taubin smoothing
Summary: Taubin Smoothing for filtering meshes and making them smoother. Taubin smoothing is an iterative approach.

Reviewed By: nikhilaravi

Differential Revision: D24751149

fbshipit-source-id: fb779e955f1a1f6750e704f1b4c6dfa37aebac1a
2020-11-30 11:38:04 -08:00
Georgia Gkioxari
5fb63b4520 move icp_data.pth to tests/data
Summary: Move icp_data.pth to tests/data

Reviewed By: bottler

Differential Revision: D25012575

fbshipit-source-id: 9252d2eeca9141c82ad3bf9d3e3331a2eab5203b
2020-11-18 14:07:12 -08:00
John Reese
90f1d5c258 apply pyfmt with usort to opted-in sources
Reviewed By: zertosh

Differential Revision: D24880203

fbshipit-source-id: 2034cdfc2712209e86d3d05c119c58f979b05c52
2020-11-10 21:27:01 -08:00
Christoph Lassner
fb2763dc78 Fix flaky CircleCI test.
Summary: This fixes issues with `pulsar.test.TestDepth` that we are encountering on CircleCI. The ID equality test is removed, which seems to give different results on different hardware (which is okay, because the exact order of spheres can slightly vary if they're close due to numerical instabilities). The depth map validity test stays in place.

Reviewed By: bottler

Differential Revision: D24840776

fbshipit-source-id: 2f38ea4880abf202c84d2987fdd71a84c5ef3b05
2020-11-10 10:14:17 -08:00