Forum

Author Topic: ToolBox for BoundingBox  (Read 2517 times)

Iluvathar

  • Newbie
  • *
  • Posts: 37
    • View Profile
ToolBox for BoundingBox
« 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 !

Code: [Select]
"""
/********************************************************************************************************************
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.")

« Last Edit: November 19, 2024, 03:45:28 PM by Iluvathar »