Agisoft Metashape

Agisoft Metashape => Bug Reports => Topic started by: SamT on July 28, 2020, 04:26:40 AM

Title: Export Model Clip
Post by: SamT on July 28, 2020, 04:26:40 AM
Export model with clip to boundary shapes seems to use the convex hull of the shape rather than the shape itself. Is this expected behavior?
Title: Re: Export Model Clip
Post by: Alexey Pasumansky on August 08, 2020, 04:06:47 PM
Hello SamT,

Currently it is expected behavior for Export Model procedure.

As a workaround it is possible to delete the polygons which do not any any vertex inside the boundary shape using Python and export the cropped model.
Title: Re: Export Model Clip
Post by: SamT on August 10, 2020, 01:21:23 AM
Thanks Alexey,

Do you have an example script for cropping to a polygon with python? I have some code that does it but it is very slow.
Title: Re: Export Model Clip
Post by: Alexey Pasumansky on August 10, 2020, 01:07:12 PM
Hello SamT,

You can try the following script to crop the mesh to the selected shapes:

Code: [Select]
import Metashape, time
from PySide2 import QtGui, QtCore, QtWidgets

def point_inside(point, poly):

x, y = point.x, point.y
inside = False

p1x, p1y = poly[0]
for i in range(len(poly) + 1):
p2x, p2y = poly[i % len(poly)]
if y >= min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
###########################

class CropMesh(QtWidgets.QDialog):

def __init__(self, parent):
QtWidgets.QDialog.__init__(self, parent)

self.btnQuit = QtWidgets.QPushButton("Close")
self.btnP1 = QtWidgets.QPushButton("Start")
self.pBar = QtWidgets.QProgressBar()
self.pBar.setTextVisible(False)

layout = QtWidgets.QGridLayout()
layout.addWidget(self.pBar,0, 0, 1, 2)
layout.addWidget(self.btnP1, 2, 0)
layout.addWidget(self.btnQuit, 2, 1)
self.setLayout(layout)

proc_height = lambda: self.estimate_height()

QtCore.QObject.connect(self.btnP1, QtCore.SIGNAL("clicked()"), proc_height)
QtCore.QObject.connect(self.btnQuit, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT("reject()"))

self.exec()


def estimate_height(self):

doc = Metashape.app.document
chunk = doc.chunk
if not chunk:
print("Empty document, script aborted.")
return 0
model = chunk.model
if not model:
print("No mesh model, script aborted.")
return 0

if not chunk.shapes:
print("No shapes, script aborted.")

shapes = [shape for shape in chunk.shapes if shape.selected and (shape.type == Metashape.Shape.Type.Polygon)]
if len(shapes) < 1:
print("No shapes selected, script aborted.")
return 0
#shape = shapes[0]


t0 = time.time()
print("Script started...")

self.pBar.setValue(0)
app.processEvents()
self.btnP1.setDisabled(True)
self.btnQuit.setDisabled(True)

T = chunk.transform.matrix
m = chunk.crs.localframe(T.mulp(chunk.region.center))
M = m* T
scale = chunk.transform.scale
m_vertices = model.vertices
faces = model.faces
s_vertices = dict()
polygons = dict()

INIT_PROG = 1
self.pBar.setValue(INIT_PROG)
app.processEvents()
SHAPES_PROG = 10
processed = 0

for shape in shapes:
s_vertices[shape] = [m.mulp(chunk.crs.unproject(v)) for v in shape.vertices] #
polygons[shape] = [[v.x, v.y] for v in s_vertices[shape]]
processed += 1
self.pBar.setValue(INIT_PROG + int(SHAPES_PROG * processed / len(shapes)))
app.processEvents()

FACE_PROG = 80
face_step = int(len(faces) / FACE_PROG)
iface = 0
processed = 0



for face in faces:

for shape in shapes:
vert_center = list()
for i in range(3):
vert_center.append(m_vertices[face.vertices[i]].coord)
vert_center[i] = M.mulp(vert_center[i])

sum_area = False
for i in range(3):
if point_inside(vert_center[i], polygons[shape]):
sum_area = True

if sum_area:
v1 = vert_center[0] - vert_center[1]
v2 = vert_center[0] - vert_center[2]
area = Metashape.Vector.cross(v2, v1).norm() / 2 #* scale
face.selected = True
iface += 1
if iface > face_step:
iface = iface - face_step
processed += 1
self.pBar.setValue(INIT_PROG + SHAPES_PROG + int(processed))
app.processEvents()

OUTPUT_PROG = 9
processed = 0

chunk.model.cropSelection()
self.pBar.setValue(100)
app.processEvents()
Metashape.app.update()

t1 = time.time()
t1 -= t0
t1 = float(t1)
print("Script finished in " + "{:.2f}".format(t1) + " seconds.")

self.btnP1.setDisabled(False)
self.btnQuit.setDisabled(False)
app.processEvents()

return 1


def crop_mesh():
global app
app = QtWidgets.QApplication.instance()
parent = app.activeWindow()

dlg = CropMesh(parent)

label = "Custom menu/Crop mesh by shape"
Metashape.app.addMenuItem(label, crop_mesh)
print("To execute this script press {}".format(label))
Title: Re: Export Model Clip
Post by: SamT on August 11, 2020, 03:13:03 AM
That is excellent thanks Alexey.

I was re-projecting the vertices to the shape CRS which was very time consuming.
Title: Re: Export Model Clip
Post by: Alexey Pasumansky on March 05, 2022, 05:18:42 PM
Updated script for version 1.8.x

Code: [Select]
import Metashape, time
from PySide2 import QtGui, QtCore, QtWidgets


def flatten_list(input_list):
if not input_list:
return []
output_list = []
for i in input_list:
if not isinstance(i, list):
output_list.append(i)
else:
output_list += flatten_list(i)
return output_list

def point_inside(point, poly):

x, y = point.x, point.y
inside = False

p1x, p1y = poly[0]
for i in range(len(poly) + 1):
p2x, p2y = poly[i % len(poly)]
if y >= min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
###########################

class CropMesh(QtWidgets.QDialog):

def __init__(self, parent):
QtWidgets.QDialog.__init__(self, parent)

self.btnQuit = QtWidgets.QPushButton("Close")
self.btnP1 = QtWidgets.QPushButton("Start")
self.pBar = QtWidgets.QProgressBar()
self.pBar.setTextVisible(False)

layout = QtWidgets.QGridLayout()
layout.addWidget(self.pBar,0, 0, 1, 2)
layout.addWidget(self.btnP1, 2, 0)
layout.addWidget(self.btnQuit, 2, 1)
self.setLayout(layout)

proc_height = lambda: self.estimate_height()

QtCore.QObject.connect(self.btnP1, QtCore.SIGNAL("clicked()"), proc_height)
QtCore.QObject.connect(self.btnQuit, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT("reject()"))

self.exec()


def estimate_height(self):

doc = Metashape.app.document
chunk = doc.chunk
if not chunk:
print("Empty document, script aborted.")
return 0
model = chunk.model
if not model:
print("No mesh model, script aborted.")
return 0

if not chunk.shapes:
print("No shapes, script aborted.")

shapes = [shape for shape in chunk.shapes if shape.selected and (shape.geometry.type == Metashape.Geometry.Type.PolygonType)]
if len(shapes) < 1:
print("No shapes selected, script aborted.")
return 0
#shape = shapes[0]


t0 = time.time()
print("Script started...")

self.pBar.setValue(0)
app.processEvents()
self.btnP1.setDisabled(True)
self.btnQuit.setDisabled(True)

T = chunk.transform.matrix
m = chunk.crs.localframe(T.mulp(chunk.region.center))
M = m* T
scale = chunk.transform.scale
m_vertices = model.vertices
faces = model.faces
s_vertices = dict()
polygons = dict()

INIT_PROG = 1
self.pBar.setValue(INIT_PROG)
app.processEvents()
SHAPES_PROG = 10
processed = 0

for shape in shapes:
vertices = flatten_list(shape.geometry.coordinates)
s_vertices[shape] = [m.mulp(chunk.crs.unproject(v)) for v in vertices] #
polygons[shape] = [[v.x, v.y] for v in s_vertices[shape]]
processed += 1
self.pBar.setValue(INIT_PROG + int(SHAPES_PROG * processed / len(shapes)))
app.processEvents()

FACE_PROG = 80
face_step = int(len(faces) / FACE_PROG)
iface = 0
processed = 0

for face in faces:

for shape in shapes:
vert_center = list()
for i in range(3):
vert_center.append(m_vertices[face.vertices[i]].coord)
vert_center[i] = M.mulp(vert_center[i])

sum_area = False
for i in range(3):
if point_inside(vert_center[i], polygons[shape]):
sum_area = True

if sum_area:
v1 = vert_center[0] - vert_center[1]
v2 = vert_center[0] - vert_center[2]
area = Metashape.Vector.cross(v2, v1).norm() / 2 #* scale
face.selected = True
iface += 1
if iface > face_step:
iface = iface - face_step
processed += 1
self.pBar.setValue(INIT_PROG + SHAPES_PROG + int(processed))
app.processEvents()

OUTPUT_PROG = 9
processed = 0

chunk.model.cropSelection()
self.pBar.setValue(100)
app.processEvents()
Metashape.app.update()

t1 = time.time()
t1 -= t0
t1 = float(t1)
print("Script finished in " + "{:.2f}".format(t1) + " seconds.")

self.btnP1.setDisabled(False)
self.btnQuit.setDisabled(False)
app.processEvents()

return 1


def crop_mesh():
global app
app = QtWidgets.QApplication.instance()
parent = app.activeWindow()

dlg = CropMesh(parent)

label = "Custom menu/Crop mesh by shape"
Metashape.app.addMenuItem(label, crop_mesh)
print("To execute this script press {}".format(label))
Title: Re: Export Model Clip
Post by: dpitman on March 05, 2022, 06:35:15 PM
Hi Alexey,

Do all legacy scripts that work with 1.7.X need amended to work with 1.8.X  ?

EDIT: Disregard. I see that the scripts in the GIT repository have already been amended. 
Title: Re: Export Model Clip
Post by: agicraft on October 28, 2022, 01:48:43 PM
Would it be possible to get the script updated to work with the 2.0 pre-release?