# Forum

### Author Topic: Export Model Clip  (Read 3586 times)

#### SamT

• Newbie
• Posts: 38
##### Export Model Clip
« 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?
« Last Edit: July 28, 2020, 04:39:02 AM by SamT »

#### Alexey Pasumansky

• Agisoft Technical Support
• Hero Member
• Posts: 14099
##### Re: Export Model Clip
« Reply #1 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.
Best regards,
Alexey Pasumansky,
Agisoft LLC

#### SamT

• Newbie
• Posts: 38
##### Re: Export Model Clip
« Reply #2 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.

#### Alexey Pasumansky

• Agisoft Technical Support
• Hero Member
• Posts: 14099
##### Re: Export Model Clip
« Reply #3 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()
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"
print("To execute this script press {}".format(label))
Best regards,
Alexey Pasumansky,
Agisoft LLC

#### SamT

• Newbie
• Posts: 38
##### Re: Export Model Clip
« Reply #4 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.

#### Alexey Pasumansky

• Agisoft Technical Support
• Hero Member
• Posts: 14099
##### Re: Export Model Clip
« Reply #5 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()
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"
print("To execute this script press {}".format(label))
Best regards,
Alexey Pasumansky,
Agisoft LLC

#### dpitman

• Full Member
• Posts: 203
##### Re: Export Model Clip
« Reply #6 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.
« Last Edit: March 05, 2022, 07:14:56 PM by dpitman »

#### agicraft

• Newbie
• Posts: 1
##### Re: Export Model Clip
« Reply #7 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?