2
« on: November 18, 2024, 04:50:41 PM »
Hello i build a toolbox to work with the bounding box, creating markers and lines on the edge of the bounding box, aligning it between two markers, create line between two markers, hiding point cloud outside the bounding box etc, modify thikness of the bounding box and move it in Z to extract precise Slices of the point cloud
i noticed that my script is working fine when i'm in local projection, but when i'm working in georeferenced project usually i work in Lambert 93, some of my tools are not working i think i missed something with the crs, here is the code
The functionnality thas is not working is aligning the BBox between two markers and When i resize the bounding box the scale is not good
Thanks for the help !
"""
/********************************************************************************************************************
BoundingBox_ToolBox
A Metashape script
Compatibility - Metashape Professional 2.1.3
-------------------
begin : 2024-11
copyright : 2024 X. Villat - Laboratoire Régional d'Archeologie-LRA
email : xavier.villat@orange.fr
********************************************************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 3 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
import Metashape
from PySide2 import QtWidgets, QtCore
import math
# Function to place markers at the corners of the bounding box
def place_corner_markers(chunk, side="front_top"):
region = chunk.region
T = chunk.transform.matrix
size = region.size
center = region.center
R = region.rot
if side == "front_top":
corner1_local = Metashape.Vector([-0.5 * size.x, 0.5 * size.y, 0.5 * size.z])
corner2_local = Metashape.Vector([0.5 * size.x, 0.5 * size.y, 0.5 * size.z])
elif side == "back_top":
corner1_local = Metashape.Vector([-0.5 * size.x, -0.5 * size.y, 0.5 * size.z])
corner2_local = Metashape.Vector([0.5 * size.x, -0.5 * size.y, 0.5 * size.z])
elif side == "front_bottom":
corner1_local = Metashape.Vector([-0.5 * size.x, 0.5 * size.y, -0.5 * size.z])
corner2_local = Metashape.Vector([0.5 * size.x, 0.5 * size.y, -0.5 * size.z])
elif side == "back_bottom":
corner1_local = Metashape.Vector([-0.5 * size.x, -0.5 * size.y, -0.5 * size.z])
corner2_local = Metashape.Vector([0.5 * size.x, -0.5 * size.y, -0.5 * size.z])
corner1_global = chunk.crs.project(T.mulp(center + R * corner1_local))
corner2_global = chunk.crs.project(T.mulp(center + R * corner2_local))
marker_left = chunk.addMarker()
marker_left.reference.location = corner1_global
marker_left.label = f"{side} Corner 1"
marker_right = chunk.addMarker()
marker_right.reference.location = corner2_global
marker_right.label = f"{side} Corner 2"
print(f"Markers created: {marker_left.label}, {marker_right.label}")
return marker_left, marker_right
# Function to create a line between two markers
def create_line_from_markers(chunk, marker_left, marker_right):
point1 = marker_left.reference.location
point2 = marker_right.reference.location
if not chunk.shapes:
chunk.shapes = Metashape.Shapes()
chunk.shapes.crs = chunk.crs
group = chunk.shapes.addGroup()
group.label = "Bounding Box Edge"
shape = chunk.shapes.addShape()
shape.geometry = Metashape.Geometry.LineString([point1, point2])
shape.label = "Bounding Box Edge Line"
shape.group = group
print(f"Line created between {marker_left.label} and {marker_right.label}")
# Function to align the bounding box based on two markers
def align_bounding_box_to_markers(chunk, marker1, marker2):
region = chunk.region
T = chunk.transform.matrix
# Convert marker positions to local coordinates
point1_global = marker1.reference.location
point2_global = marker2.reference.location
point1 = T.inv().mulp(chunk.crs.unproject(point1_global))
point2 = T.inv().mulp(chunk.crs.unproject(point2_global))
# Adjust the Z of the second marker to maintain horizontality
point2.z = point1.z
# Calculate the new width (dimension X) of the bounding box
width = (point2 - point1).norm()
size = region.size
size.x = width
# Calculate direction and rotation angle
direction = point2 - point1
direction.normalize()
angle = math.atan2(direction.y, direction.x)
cos_a = math.cos(angle)
sin_a = math.sin(angle)
new_rot = Metashape.Matrix([[cos_a, -sin_a, 0],
[sin_a, cos_a, 0],
[0, 0, 1]])
region.rot = new_rot
# Calculate the midpoint between the two markers
mid_point = (point1 + point2) * 0.5
# Adjust the center of the bounding box to align the front top side
offset = new_rot * Metashape.Vector([0, -0.5 * size.y, -0.5 * size.z])
region.center = mid_point + offset
# Update the size and chunk.region
region.size = size
chunk.region = region
print(f"Bounding box aligned based on markers: {marker1.label}, {marker2.label}")
# Function to adjust the view orientation
def set_view_orientation(chunk, orientation):
"""Adjusts the view based on the selected orientation by directly modifying the viewport."""
if not chunk:
print("No active chunk.")
return
T = chunk.transform.matrix
region = chunk.region
r_center = region.center
r_rotate = region.rot
r_size = region.size
# Calculate the bounding box corners (useful for scale calculation)
r_vert = [
Metashape.Vector([
0.5 * r_size[0] * ((i & 2) - 1),
r_size[1] * ((i & 1) - 0.5),
0.5 * r_size[2] * ((i & 4) - 2)
]) for i in range(8)
]
r_vert = [r_center + r_rotate * v for v in r_vert]
# Calculate projected dimensions
height = T.mulv(r_vert[1] - r_vert[0]).norm()
width = T.mulv(r_vert[2] - r_vert[0]).norm()
# Get the viewport
viewport = Metashape.app.model_view.viewpoint
cx, cy = viewport.width, viewport.height
# Calculate the scale to adjust the zoom level
scale = cx / width if (width / cx > height / cy) else cy / height
# Update the point the camera is looking at (center of the bounding box)
viewport.coo = T.mulp(r_center)
# Update the zoom level
viewport.mag = scale
# Define the rotation based on the orientation
if orientation == "Front":
ym = Metashape.Matrix([[1, 0, 0],
[0, 0, -1],
[0, 1, 0]])
elif orientation == "Back":
ym = Metashape.Matrix([[-1, 0, 0],
[0, 0, 1],
[0, 1, 0]])
elif orientation == "Top":
ym = Metashape.Matrix([[1, 0, 0],
[0, -1, 0],
[0, 0, 1]])
elif orientation == "Bottom":
ym = Metashape.Matrix([[1, 0, 0],
[0, 1, 0],
[0, 0, -1]])
else:
print("Invalid orientation.")
return
# Update the camera rotation
viewport.rot = chunk.transform.rotation * r_rotate * ym
print(f"View oriented towards {orientation}.")
# Class for the toolbox
class ToolBox(QtWidgets.QWidget):
def __init__(self, chunk, parent=None):
super().__init__(parent)
self.chunk = chunk
self.setWindowTitle("Toolbox")
self.setMinimumSize(400, 600)
# Create the interface
self.create_controls()
def create_controls(self):
layout = QtWidgets.QVBoxLayout()
# Section Line and Marker Management
layout.addWidget(QtWidgets.QLabel("Line and Marker Management"))
self.side_combo = QtWidgets.QComboBox()
self.side_combo.addItems(["Front Top", "Back Top", "Front Bottom", "Back Bottom"])
layout.addWidget(self.side_combo)
self.create_polyline_button = QtWidgets.QPushButton("Create Polyline on Selected Side")
layout.addWidget(self.create_polyline_button)
layout.addWidget(QtWidgets.QLabel("Select two markers to create a line"))
self.marker_combo1 = QtWidgets.QComboBox()
self.marker_combo2 = QtWidgets.QComboBox()
layout.addWidget(self.marker_combo1)
layout.addWidget(self.marker_combo2)
self.create_line_button = QtWidgets.QPushButton("Create Polyline between Selected Markers")
layout.addWidget(self.create_line_button)
layout.addWidget(QtWidgets.QLabel("Align Bounding Box based on two markers"))
self.orient_marker_combo1 = QtWidgets.QComboBox()
self.orient_marker_combo2 = QtWidgets.QComboBox()
layout.addWidget(self.orient_marker_combo1)
layout.addWidget(self.orient_marker_combo2)
self.align_box_button = QtWidgets.QPushButton("Align Bounding Box on Markers")
layout.addWidget(self.align_box_button)
# Button to refresh markers
self.refresh_markers_button = QtWidgets.QPushButton("Refresh Markers")
layout.addWidget(self.refresh_markers_button)
self.refresh_markers_button.clicked.connect(self.load_markers)
# Load markers at startup
self.load_markers()
# Section Adjust Bounding Box in Z only
layout.addWidget(QtWidgets.QLabel("Adjust Bounding Box in Z"))
self.label_z = QtWidgets.QLabel("Z Thickness:")
self.input_z = QtWidgets.QLineEdit(str(self.chunk.region.size.z))
layout.addWidget(self.label_z)
layout.addWidget(self.input_z)
self.adjust_z_button = QtWidgets.QPushButton("Apply Z Thickness")
layout.addWidget(self.adjust_z_button)
# Section Offset Bounding Box on Z Axis
layout.addWidget(QtWidgets.QLabel("Offset Bounding Box on Z Axis"))
self.z_offset_label = QtWidgets.QLabel("Z Offset Interval:")
self.z_offset_input = QtWidgets.QLineEdit("0.1") # Default interval
layout.addWidget(self.z_offset_label)
layout.addWidget(self.z_offset_input)
self.z_increase_button = QtWidgets.QPushButton("+")
self.z_decrease_button = QtWidgets.QPushButton("-")
z_button_layout = QtWidgets.QHBoxLayout()
z_button_layout.addWidget(self.z_decrease_button)
z_button_layout.addWidget(self.z_increase_button)
layout.addLayout(z_button_layout)
# Section Point Cloud Visibility
layout.addWidget(QtWidgets.QLabel("Point Cloud Visibility"))
self.crop_button = QtWidgets.QPushButton("Hide Points Outside Bounding Box")
self.reset_button = QtWidgets.QPushButton("Reset Point Cloud Visibility")
layout.addWidget(self.crop_button)
layout.addWidget(self.reset_button)
# Section View Orientation
layout.addWidget(QtWidgets.QLabel("View Orientation"))
self.view_orientation_combo = QtWidgets.QComboBox()
self.view_orientation_combo.addItems(["Front", "Back", "Top", "Bottom"])
layout.addWidget(self.view_orientation_combo)
self.apply_view_orientation_button = QtWidgets.QPushButton("Apply Orientation")
layout.addWidget(self.apply_view_orientation_button)
self.apply_view_orientation_button.clicked.connect(self.apply_view_orientation)
# Button for front view based on bounding box
self.front_view_button = QtWidgets.QPushButton("Front View (Bounding Box)")
layout.addWidget(self.front_view_button)
self.front_view_button.clicked.connect(self.apply_front_view)
self.setLayout(layout)
# Button connections
self.create_polyline_button.clicked.connect(self.create_polyline)
self.create_line_button.clicked.connect(self.create_line_between_markers)
self.align_box_button.clicked.connect(self.align_bounding_box)
self.adjust_z_button.clicked.connect(self.apply_thickness_z)
self.z_increase_button.clicked.connect(self.increase_z)
self.z_decrease_button.clicked.connect(self.decrease_z)
self.crop_button.clicked.connect(self.apply_crop)
self.reset_button.clicked.connect(self.show_all_points)
def load_markers(self):
"""Loads markers into the comboboxes."""
# Block signals to avoid unintended triggers
self.marker_combo1.blockSignals(True)
self.marker_combo2.blockSignals(True)
self.orient_marker_combo1.blockSignals(True)
self.orient_marker_combo2.blockSignals(True)
# Clear comboboxes
self.marker_combo1.clear()
self.marker_combo2.clear()
self.orient_marker_combo1.clear()
self.orient_marker_combo2.clear()
markers = self.chunk.markers
for marker in markers:
label = marker.label if marker.label else "Unnamed Marker"
self.marker_combo1.addItem(label, marker)
self.marker_combo2.addItem(label, marker)
self.orient_marker_combo1.addItem(label, marker)
self.orient_marker_combo2.addItem(label, marker)
# Unblock signals
self.marker_combo1.blockSignals(False)
self.marker_combo2.blockSignals(False)
self.orient_marker_combo1.blockSignals(False)
self.orient_marker_combo2.blockSignals(False)
print(f"{len(markers)} markers loaded.")
def create_polyline(self):
"""Creates markers at selected corners and a polyline."""
side = self.side_combo.currentText().replace(" ", "_").lower()
marker1, marker2 = place_corner_markers(self.chunk, side)
create_line_from_markers(self.chunk, marker1, marker2)
self.load_markers() # Update marker lists
def create_line_between_markers(self):
"""Creates a line between two selected markers."""
marker1 = self.marker_combo1.currentData()
marker2 = self.marker_combo2.currentData()
if marker1 and marker2:
create_line_from_markers(self.chunk, marker1, marker2)
# No need to reload markers here as no new marker is created
def align_bounding_box(self):
"""Aligns the bounding box based on two selected markers."""
marker1 = self.orient_marker_combo1.currentData()
marker2 = self.orient_marker_combo2.currentData()
if marker1 and marker2:
align_bounding_box_to_markers(self.chunk, marker1, marker2)
Metashape.app.update()
print("Bounding box aligned.")
def apply_thickness_z(self):
"""Applies the specified Z thickness to the bounding box."""
try:
thickness_z = float(self.input_z.text())
region = self.chunk.region
size = region.size
size.z = thickness_z
region.size = size
self.chunk.region = region # Reassignment necessary
Metashape.app.update()
print(f"Bounding box Z size set to: {thickness_z}")
except ValueError:
print("Invalid value for Z thickness.")
def adjust_z_offset(self, offset):
"""Adjusts the position of the bounding box in Z by defined increments."""
try:
region = self.chunk.region
center = region.center
center.z += offset
region.center = center
self.chunk.region = region # Reassignment necessary
Metashape.app.update()
print(f"Bounding box Z center adjusted by: {offset}")
except Exception as e:
print(f"Error adjusting Z offset: {e}")
def increase_z(self):
"""Increases the Z position of the bounding box."""
try:
offset = float(self.z_offset_input.text())
self.adjust_z_offset(offset)
except ValueError:
print("Invalid value for Z offset.")
def decrease_z(self):
"""Decreases the Z position of the bounding box."""
try:
offset = -float(self.z_offset_input.text())
self.adjust_z_offset(offset)
except ValueError:
print("Invalid value for Z offset.")
def apply_crop(self):
"""Hides points outside the bounding box."""
if not self.chunk.point_cloud:
print("No active point cloud found.")
return
self.chunk.point_cloud.resetFilters()
self.chunk.point_cloud.selectPointsByRegion(self.chunk.region)
self.chunk.point_cloud.setSelectionFilter()
Metashape.app.update()
print("Points outside the bounding box have been hidden.")
def show_all_points(self):
"""Resets the point cloud visibility."""
if not self.chunk.point_cloud:
print("No active point cloud found.")
return
self.chunk.point_cloud.resetFilters()
Metashape.app.update()
print("Point cloud visibility reset.")
def apply_view_orientation(self):
"""Applies the selected view orientation."""
orientation = self.view_orientation_combo.currentText()
set_view_orientation(self.chunk, orientation)
def apply_front_view(self):
"""Applies the front view based on the bounding box."""
set_view_orientation(self.chunk, "Front")
# Function to launch the dockable panel
def run_toolbox():
doc = Metashape.app.document
chunk = doc.chunk
if chunk:
app = QtWidgets.QApplication.instance()
parent = app.activeWindow()
# Check if the dock already exists
for widget in parent.findChildren(QtWidgets.QDockWidget):
if widget.windowTitle() == "Toolbox":
widget.show()
return
dock_widget = QtWidgets.QDockWidget("Toolbox", parent)
toolbox_panel = ToolBox(chunk, parent=dock_widget)
dock_widget.setWidget(toolbox_panel)
parent.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock_widget)
else:
print("No chunk found in the document.")
# Add the tool to Metashape's menu
Metashape.app.addMenuItem("ToolBox/Advanced Tools (Dockable)", run_toolbox)
print("ToolBox loaded. Access it from the 'ToolBox' menu.")