Skip to content

Size COPC octree from union bbox when merging multiple inputs - #296

Open
agrigoriev wants to merge 10 commits into
enhance-copc-writingfrom
copc-multifile-merge
Open

Size COPC octree from union bbox when merging multiple inputs#296
agrigoriev wants to merge 10 commits into
enhance-copc-writingfrom
copc-multifile-merge

Conversation

@agrigoriev

@agrigoriev agrigoriev commented Apr 26, 2026

Copy link
Copy Markdown
Collaborator

This pull request improves the robustness and correctness of merged COPC (Cloud Optimized Point Cloud) file writing by ensuring accurate 3D bounding boxes and point counts are used throughout the pipeline. It enforces strict validation of input metadata, updates the handling of 3D bounds in both reading and writing, and adds safeguards to prevent malformed outputs when merging multiple files. A new test verifies the correctness of the merged COPC output.

Improvements to 3D bounding box and point count handling:

  • Enforces that all features in a VPC (Virtual Point Cloud) file must have a consistent 3D bounding box (proj:bbox with 6 elements); mixed 2D/3D bboxes now cause a clear error, preventing silent creation of malformed merged outputs. [1] [2] [3] [4]
  • Updates the reading of point counts in read_vpc to use 64-bit integers, preventing silent truncation for large datasets.
  • Ensures the union 3D bounding box and total point count are properly tracked and exposed in FileCollection, and used when merging files into a single COPC output. [1] [2] [3] [4] [5]

COPC writing and validation enhancements:

  • In LASRlaswriter, captures the union bounding box and total point count from the input collection and uses them to size the COPC octree and compute the correct auto max_depth for merged outputs; refuses to write merged COPC files if the input lacks a valid 3D bounding box. [1] [2] [3] [4]
  • Updates the VPC writing logic to always emit 6-element 3D proj:bbox arrays, aligning with the stricter requirements for merged COPC outputs. [1] [2] [3]

Testing improvements:

  • Adds a new test to verify that merging multiple input files into a single COPC output produces the correct union bounding box and point count, ensuring the octree is sized to the union of all input files.

@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from 513aeb4 to 8abccea Compare April 27, 2026 02:53
@agrigoriev agrigoriev changed the title Copc multifile merge write_copc: size COPC octree from union bbox when merging multiple inputs Apr 27, 2026
@agrigoriev agrigoriev changed the title write_copc: size COPC octree from union bbox when merging multiple inputs Size COPC octree from union bbox when merging multiple inputs Apr 27, 2026
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from 8abccea to b614726 Compare April 27, 2026 10:33
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from b614726 to 14bb4d5 Compare April 27, 2026 10:48
@agrigoriev
agrigoriev force-pushed the enhance-copc-writing branch from c67fb7f to 215a71d Compare April 27, 2026 10:48
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from 14bb4d5 to 25f15f0 Compare April 27, 2026 12:39
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from 25f15f0 to dde7712 Compare April 27, 2026 14:11
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added a commit that referenced this pull request Apr 27, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from dde7712 to 3f24668 Compare April 27, 2026 14:14
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from 3f24668 to 9878a91 Compare April 28, 2026 00:46
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from 5caf976 to bc00dd5 Compare April 28, 2026 03:16
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from bc00dd5 to 9c239a5 Compare April 28, 2026 05:48
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added a commit that referenced this pull request Apr 28, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from 9c239a5 to ed249b5 Compare April 28, 2026 11:27
agrigoriev added a commit that referenced this pull request Apr 29, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 29, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added a commit that referenced this pull request Apr 29, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from ed249b5 to 69f40d2 Compare April 29, 2026 02:11
agrigoriev added a commit that referenced this pull request Apr 29, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 29, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added a commit that referenced this pull request Apr 29, 2026
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
@agrigoriev
agrigoriev force-pushed the copc-multifile-merge branch from 69f40d2 to e93fc9f Compare April 29, 2026 04:43
agrigoriev added a commit that referenced this pull request Apr 29, 2026
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
agrigoriev added a commit that referenced this pull request Apr 29, 2026
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
agrigoriev added 10 commits May 1, 2026 10:33
Fix 5 from the maintainer review on #296. The catalog union-bbox
override in set_header was applied to every initialization,
including per-file COPC mode (`*.copc.laz` template with a `*`
placeholder) where each output file should use its own tile's
bbox. Per-file outputs were being inflated to cover the whole
input set.

Gate the override on (catalog_bbox_valid && merged &&
path_has_copc_suffix(template_filename)) so:

  - single-file COPC (merged) keeps the union override (correct)
  - per-file COPC (`*` placeholder) uses each tile's own bbox
  - non-COPC writes are untouched (they ignore bbox at open
    anyway, but explicit gate keeps it clear)

Verified: per-file COPC writes of bcts_1+bcts_2 produce two
outputs with their own per-file bbox in the COPC info VLR
(file 1 center_y ~629279, not the union 629429). Merge mode
unchanged.
Two follow-up fixes for #296:

Issue 1 — auto max_depth used file-1's count, not the merged total.
LASRlaswriter::set_header overrode bbox for merged COPC but left
header.number_of_point_records as the per-tile count. LASio::init
saves whatever count it sees, and COPCwriter's compute_max_depth
heuristic divides total points by max_points_per_octant (100000)
to pick depth — so a 1.35M-point merge picked depth from file-1's
531k count alone, ending up too shallow for the merged data set.

  - FileCollection::get_total_points() sums header counts across
    every input.
  - LASRlaswriter::process(FileCollection*&) captures the total.
  - set_header overrides h.number_of_point_records with the catalog
    total when writing merged COPC. Verified: bcts_1+bcts_2 merge
    now produces 88 hierarchy entries vs 40 with file-1's count
    alone, and depth-3 reads now return all 1.35M points.

Issue 4 — VPC z bounds were dropped, catalog_bbox_valid only
checked x/y. read_vpc parsed the 6-element STAC proj:bbox using
indices [0,1,3,4] (skipping z at [2] and [5]); FileCollection's
zmin/zmax stayed at numeric_limits defaults. Combined with the
write-side check that ignored z, merged COPC from a VPC could
get bogus z bounds and either fail late (degenerate-bbox check)
or silently clamp.

  - read_vpc reads min_z/max_z from 6-element bbox and updates
    headers + catalog z accumulators.
  - catalog_bbox_valid now requires zmin ≤ zmax in addition to
    x/y validity.
Issue 10 from the latest review on #296.

The lasR-emitted VPC was a closed loop bug: write_vpc wrote
properties.proj:bbox as 4 elements (no z) and a 6D top-level "bbox"
field, but read_vpc only inspects properties.proj:bbox. Round-tripping
a lasR-generated VPC into a merged COPC write therefore lost z bounds
on the catalog side, the writer's catalog_bbox_valid check (which now
requires z) returned false, and the union bbox/count override was
silently skipped — falling back to the per-tile bbox of file 1.

Switch the proj:bbox write to the 6-element form
[minx, miny, minz, maxx, maxy, maxz]. The reader already handles both
the 4D and 6D forms (post-#296), so this stays compatible with VPCs
emitted by other tools.

Separately, pc:count was parsed as `int`. The STAC pointcloud extension
puts no upper bound on the field, and a catalog over ~2.1B points
would silently truncate, mis-sizing the auto max_depth heuristic. Read
as uint64_t.
Issue 15 — write_vpc reused the EPSG:4979-transformed z values for
properties.proj:bbox. The OGR transform mutates z in-place when called
with a 3D source/target, so by the time proj:bbox was written zmin/zmax
no longer carried source-CRS values while x/y still did — making the
proj:bbox internally inconsistent if the vertical transform shifted z.
Use a local copy of zmin/zmax for the WGS-84 top-level "bbox" and keep
the originals for proj:bbox.

Issue 16 — read_vpc accepted a mix of 4D and 6D proj:bbox in the same
VPC. The catalog's z-bounds accumulator only ran on 6D features, but
total point count covered every feature; on a merged COPC write that
sized the auto max_depth from the full count but the octree z extent
from only the 3D-bbox subset, producing a skewed octree. Track the
ratio of 3D-vs-2D entries and reject loudly if mixed, with a message
pointing the user at the proj:bbox shape they need.
If a merged COPC write reaches set_header without a valid 3D catalog
bbox — typically a legacy VPC whose proj:bbox is 4-element so the
reader can't accumulate z bounds — set_header used to silently fall
back to file-1's per-tile bbox/count. The result was a merged COPC
sized from a single tile: octree bbox under-covers the data (clamping
at close), auto max_depth is computed from one file's count (too
shallow), and PR review chain caught both downstream.

Add an explicit check: merged && copc-suffix && !catalog_bbox_valid
is now a stage-level error with a message naming the likely cause
(legacy 4D VPC) and the two ways out (regenerate the VPC with 6D
proj:bbox, or use a '*' placeholder for per-tile COPC). The user
sees the failure at the start of writing rather than as a clamp
warning at close.

Also: explicit <algorithm> include now that path_has_copc_suffix
uses std::equal — relying on transitive includes for std::equal,
std::exception_ptr, etc. is fragile across compilers. Pair with the
<exception> include added to writelas.cpp on PR 1.
The "fail fast on invalid catalog bbox" guard added for the new
streaming COPC writer assumes the writer needs an accurate union
bbox at open() to size its octree. LASlib's LASwriterCOPC (now the
default) inventories at close instead, so that guard is unnecessary
on the old path and would only refuse work the old writer can handle.

Restrict the guard to experimental_writer = TRUE and update the
multi-file merge test to opt in.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant