1
Bug Reports / Re: Deleted tie points from all cameras re-appear, when realigning any one camera
« on: January 24, 2025, 10:54:42 AM »
Do you have the keep key points option enabled ?
This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.
"""
/********************************************************************************************************************
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.")