From ba48f759ca94d17bb0c4e9b63fbe87316c06f540 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Tue, 8 Aug 2023 10:15:37 -0400 Subject: [PATCH 01/34] Photogrammetry updates with standardized naming and alignment cutoff parameter. --- server/recipes/photogrammetry.json | 9 ++++++++- server/scripts/MetashapeGenerateMesh.py | 18 ++++++++++++++++-- server/scripts/MetashapeGenerateTexture.py | 5 +++-- source/server/tasks/PhotogrammetryTask.ts | 4 ++++ source/server/tools/MetashapeTool.ts | 5 +++++ 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index 592c268..e411f23 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -38,13 +38,19 @@ "optimizeMarkers": { "type": "boolean", "default": false + }, + "alignmentLimit": { + "type": "number", + "default": 50, + "minimum": 0, + "maximum": 100 } }, "required": [ "sourceImageFolder" ], "advanced": [ - + "alignmentLimit" ], "additionalProperties": false }, @@ -109,6 +115,7 @@ "scalebarFile": "scalebarCSV", "generatePointCloud": "generatePointCloud", "optimizeMarkers": "optimizeMarkers", + "alignmentLimit": "alignmentLimit", "tool": "tool", "timeout": 86400 }, diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 635f032..10ea30e 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -19,6 +19,7 @@ def convert(s): parser.add_argument("-i", "--input", required=True, help="Input filepath") parser.add_argument("-c", "--cameras", required=True, help="Cameras filepath") parser.add_argument("-o", "--output", required=True, help="Output filename") +parser.add_argument("-al", "--align_limit", required=False, help="Alignment threshold (%)") parser.add_argument("-sb", required=False, help="Scalebar definition file") parser.add_argument("-optm", required=False, default="False", help="Optimize markers") parser.add_argument("-bdc", required=False, default="False", help="Build dense cloud") @@ -29,7 +30,8 @@ def convert(s): imagePath = args.input camerasPath = args.cameras -name = os.path.basename(os.path.normpath(imagePath)) +name = os.path.basename(os.path.normpath(args.output)) +name = os.path.splitext(name)[0]; # Grab images from directory (include subdirectories) imageFiles=[] @@ -65,6 +67,17 @@ def convert(s): # align the matched image pairs chunk.alignCameras() +# save post-alignment +doc.save(imagePath+"\\..\\"+name+"-align.psx") + +aligned = [camera for camera in chunk.cameras if camera.transform and camera.type==Metashape.Camera.Type.Regular] +success_ratio = len(aligned) / len(chunk.cameras) * 100 +print("ALIGNMENT SUCCESS: "+str(success_ratio)) + +# exit out if alignment is less than requirement +if success_ratio < int(args.align_limit): + sys.exit("Error: Image alignment does not meet minimum threshold") + # optimize cameras chunk.optimizeCameras\ ( @@ -227,5 +240,6 @@ def convert(s): ) chunk.exportCameras(camerasPath) +chunk.exportReport(imagePath+"\\..\\"+name+"-report.pdf") -doc.save(imagePath+"\\..\\"+name+".psx") +doc.save(imagePath+"\\..\\"+name+"-mesh.psx") diff --git a/server/scripts/MetashapeGenerateTexture.py b/server/scripts/MetashapeGenerateTexture.py index 109117d..de983bf 100644 --- a/server/scripts/MetashapeGenerateTexture.py +++ b/server/scripts/MetashapeGenerateTexture.py @@ -29,7 +29,8 @@ def convert(s): imagePath = args.input modelPath = args.model camerasPath = args.cameras -name = os.path.basename(os.path.normpath(imagePath)) +name = os.path.basename(os.path.normpath(args.output)) +name = os.path.splitext(name)[0]; # Grab images from directory (include subdirectories) imageFiles=[] @@ -92,4 +93,4 @@ def convert(s): format=Metashape.ModelFormatOBJ, ) -doc.save(imagePath+"\\..\\"+name+"-final.psx") +doc.save(imagePath+"\\..\\"+name+".psx") diff --git a/source/server/tasks/PhotogrammetryTask.ts b/source/server/tasks/PhotogrammetryTask.ts index 740c243..2dbb68b 100644 --- a/source/server/tasks/PhotogrammetryTask.ts +++ b/source/server/tasks/PhotogrammetryTask.ts @@ -42,6 +42,8 @@ export interface IPhotogrammetryTaskParameters extends ITaskParameters generatePointCloud: boolean; /** Flag to enable discarding high-error markers */ optimizeMarkers: boolean; + /** Percent success required to pass alignment stage */ + alignmentLimit?: number; /** Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup, see [[IToolConfiguration]]). */ timeout?: number; /** Tool to use for photogrammetry ("Metashape" or "RealityCapture" or "Meshroom", default: "Metashape"). */ @@ -69,6 +71,7 @@ export default class PhotogrammetryTask extends ToolTask scalebarFile: { type: "string", minLength: 1 }, generatePointCloud: { type: "boolean", default: false}, optimizeMarkers: { type: "boolean", default: false}, + alignmentLimit: { type: "number", default: 50}, timeout: { type: "integer", default: 0 }, tool: { type: "string", enum: [ "Metashape", "RealityCapture", "Meshroom" ], default: "Metashape" } }, @@ -94,6 +97,7 @@ export default class PhotogrammetryTask extends ToolTask scalebarFile: params.scalebarFile, generatePointCloud: params.generatePointCloud, optimizeMarkers: params.optimizeMarkers, + alignmentLimit: params.alignmentLimit, mode: "full", timeout: params.timeout }; diff --git a/source/server/tools/MetashapeTool.ts b/source/server/tools/MetashapeTool.ts index 1f3e190..f791afa 100644 --- a/source/server/tools/MetashapeTool.ts +++ b/source/server/tools/MetashapeTool.ts @@ -30,6 +30,7 @@ export interface IMetashapeToolSettings extends IToolSettings scalebarFile?: string; generatePointCloud?: boolean; optimizeMarkers?: boolean; + alignmentLimit?: number; } export type MetashapeInstance = ToolInstance; @@ -82,6 +83,10 @@ export default class MetashapeTool extends Tool Date: Tue, 8 Aug 2023 11:18:41 -0400 Subject: [PATCH 02/34] WIP adding folder zip from absolute folder path --- server/recipes/si-nas-zip.json | 74 +++++++++++++++++++++++++++++ source/server/tasks/ZipTask.ts | 10 +++- source/server/tools/SevenZipTool.ts | 10 ++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 server/recipes/si-nas-zip.json diff --git a/server/recipes/si-nas-zip.json b/server/recipes/si-nas-zip.json new file mode 100644 index 0000000..6b637a3 --- /dev/null +++ b/server/recipes/si-nas-zip.json @@ -0,0 +1,74 @@ +{ + "id": "e965d8a9-6003-461a-bc92-c01aa67f9e94", + "name": "si-nas-zip", + "description": "Zips files directly from storage folder", + "version": "1", + "start": "log", + + "parameterSchema": { + "type": "object", + "properties": { + "sourceFolderPath": { + "type": "string", + "minLength": 1 + }, + "filetype": { + "type": "string", + "minLength": 1 + }, + "recursive": { + "type": "boolean", + "default": false + } + }, + "required": [ + "sourceFolderPath" + ], + "advanced": [ + ], + "additionalProperties": false + }, + + "steps": { + "log": { + "task": "Log", + "description": "Enable logging services", + "pre": { + }, + "parameters": { + "logToConsole": true, + "reportFile": "'zip-report.json'" + }, + "success": "'zip-files'", + "failure": "$failure" + }, + "zip-files": { + "task": "Zip", + "description": "Zip files direct from storage", + "pre": { + "deliverables": { + "fileZip": "zippedFiles.zip" + } + }, + "parameters": { + "inputFile1": "sourceFolderPath", + "fileFilter": "filetype", + "recursive": "recursive", + "operation": "'nas-zip'" + }, + "success": "'delivery'", + "failure": "$failure" + }, + "delivery": { + "task": "Delivery", + "description": "Send result files back to client", + "parameters": { + "method": "transportMethod", + "path": "$firstTrue(deliveryPath, pickupPath, $currentDir)", + "files": "deliverables" + }, + "success": "$success", + "failure": "$failure" + } + } +} diff --git a/source/server/tasks/ZipTask.ts b/source/server/tasks/ZipTask.ts index 3d14d9f..7d0d961 100644 --- a/source/server/tasks/ZipTask.ts +++ b/source/server/tasks/ZipTask.ts @@ -43,6 +43,10 @@ export interface IZipTaskParameters extends ITaskParameters outputFile?: string; /** Degree of compression */ compressionLevel?, + /** Flag to recurse sub-directories */ + recursive?, + /** Filetype to filter for */ + fileFilter?, /** Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup, see [[IToolConfiguration]]). */ timeout?: number; /** Default tool is 7Zip. Specify another tool if needed. */ @@ -71,9 +75,11 @@ export default class ZipTask extends ToolTask inputFile6: { type: "string" }, inputFile7: { type: "string" }, inputFile8: { type: "string" }, - operation: { type: "string", enum: [ "zip", "unzip" ] }, + operation: { type: "string", enum: [ "zip", "unzip", "nas-zip" ] }, outputFile: { type: "string", minLength: 1, default: "CookArchive.zip" }, compressionLevel: { type: "integer", minimum: 0, default: 5 }, + fileFilter: { type: "string" }, + recursive: { type: "boolean", default: false }, timeout: { type: "integer", minimum: 0, default: 0 }, tool: { type: "string", enum: [ "SevenZip" ], default: "SevenZip" } }, @@ -101,6 +107,8 @@ export default class ZipTask extends ToolTask inputFile7: params.inputFile7, inputFile8: params.inputFile8, compressionLevel: params.compressionLevel, + recursive: params.recursive, + fileFilter: params.fileFilter, operation: params.operation, outputFile: params.outputFile, timeout: params.timeout diff --git a/source/server/tools/SevenZipTool.ts b/source/server/tools/SevenZipTool.ts index a831400..e24dcb2 100644 --- a/source/server/tools/SevenZipTool.ts +++ b/source/server/tools/SevenZipTool.ts @@ -32,6 +32,8 @@ export interface ISevenZipToolSettings extends IToolSettings inputFile7: string; inputFile8: string; compressionLevel: number; + fileFilter: string; + recursive: boolean; operation: string; outputFile: string; } @@ -76,6 +78,14 @@ export default class SevenZipTool extends Tool Date: Thu, 10 Aug 2023 10:21:27 -0400 Subject: [PATCH 03/34] Added convert to jpg option --- server/recipes/photogrammetry.json | 40 ++++++- source/server/tasks/BatchConvertImageTask.ts | 84 +++++++++++++ source/server/tasks/FileOperationTask.ts | 2 +- source/server/tools/ImageMagickTool.ts | 120 ++++++++++++------- 4 files changed, 196 insertions(+), 50 deletions(-) create mode 100644 source/server/tasks/BatchConvertImageTask.ts diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index e411f23..0379d77 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -44,13 +44,17 @@ "default": 50, "minimum": 0, "maximum": 100 + }, + "convertToJpg": { + "type": "boolean", + "default": false } }, "required": [ "sourceImageFolder" ], "advanced": [ - "alignmentLimit" + "alignmentLimit", "convertToJpg" ], "additionalProperties": false }, @@ -61,7 +65,8 @@ "description": "Enable logging services", "pre": { "outputFileBaseName": "$baseName($firstTrue(outputFileBaseName, sourceImageFolder))", - "baseMeshName": "$firstTrue(outputFileBaseName, sourceImageFolder)" + "baseMeshName": "$firstTrue(outputFileBaseName, sourceImageFolder)", + "sourceFolderBaseName": "$baseName(sourceImageFolder)" }, "parameters": { "logToConsole": true, @@ -95,6 +100,33 @@ "inputFile1": "sourceImageFolder", "operation": "'unzip'" }, + "success": "'make-convert-folder'", + "failure": "$failure" + }, + "make-convert-folder": { + "task": "FileOperation", + "skip": "$not(convertToJpg)", + "description": "Create folder for converted images", + "pre": { + "sourceFolderConverted": "sourceFolderBaseName & '_converted'" + }, + "parameters": { + "operation": "'CreateFolder'", + "name": "sourceFolderConverted" + }, + "success": "'convert-to-jpg'", + "failure": "$failure" + }, + "convert-to-jpg": { + "task": "BatchConvertImage", + "skip": "$not(convertToJpg)", + "description": "Convert images to .jpg", + "parameters": { + "inputImageFolder": "sourceFolderBaseName", + "outputImageFolder": "sourceFolderConverted", + "filetype": "jpg", + "quality": "85" + }, "success": "'photogrammetry'", "failure": "$failure" }, @@ -109,7 +141,7 @@ } }, "parameters": { - "inputImageFolder": "sourceImageFolder", + "inputImageFolder": "convertToJpg ? sourceFolderConverted : sourceFolderBaseName", "camerasFile": "camerasFile", "outputFile": "deliverables.meshFile", "scalebarFile": "scalebarCSV", @@ -161,7 +193,7 @@ } }, "parameters": { - "inputImageFolder": "sourceImageFolder", + "inputImageFolder": "convertToJpg ? sourceFolderConverted : sourceFolderBaseName", "inputModelFile": "deliverables.cleanedMeshFile", "camerasFile": "camerasFile", "outputFile": "deliverables.finalMeshFile", diff --git a/source/server/tasks/BatchConvertImageTask.ts b/source/server/tasks/BatchConvertImageTask.ts new file mode 100644 index 0000000..8bce7a1 --- /dev/null +++ b/source/server/tasks/BatchConvertImageTask.ts @@ -0,0 +1,84 @@ +/** + * 3D Foundation Project + * Copyright 2023 Smithsonian Institution + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Job from "../app/Job"; + +import { IImageMagickToolSettings } from "../tools/ImageMagickTool"; + +import Task, { ITaskParameters } from "../app/Task"; +import ToolTask from "../app/ToolTask"; + +//////////////////////////////////////////////////////////////////////////////// + +/** Parameters for [[BatchConvertImageTask]]. */ +export interface IBatchConvertImageTaskParameters extends ITaskParameters +{ + /** Input image folder path. */ + inputImageFolder: string; + /** Output image folder path. */ + outputImageFolder: string; + /** Compression quality for JPEG images (0 - 100, default: 70). */ + quality?: number; + /** Filetype to convert images to. */ + filetype?: string; +} + +/** + * Converts folders image files between different formats. + * + * Parameters: [[IBatchConvertImageTaskParameters]]. + * Tool: [[ImageMagickTool]]. + */ +export default class BatchConvertImageTask extends ToolTask +{ + static readonly taskName = "BatchConvertImage"; + + static readonly description = "Converts folders image files between different formats. "; + + static readonly parameterSchema = { + type: "object", + properties: { + inputImageFolder: { type: "string", minLength: 1 }, + outputImageFolder: { type: "string", minLength: 1 }, + quality: { type: "integer", minimum: 0, maximum: 100, default: 70 }, + filetype: { type: "string", default: "jpg" } + }, + required: [ + "inputImageFolder", + "outputImageFolder", + "filetype" + ], + additionalParameters: false + }; + + static readonly parameterValidator = + Task.jsonValidator.compile(BatchConvertImageTask.parameterSchema); + + constructor(params: IBatchConvertImageTaskParameters, context: Job) + { + super(params, context); + + const settings: IImageMagickToolSettings = { + inputImageFolder: params.inputImageFolder, + outputImageFolder: params.outputImageFolder, + quality: params.quality, + batchConvertType: params.filetype + }; + + this.addTool("ImageMagick", settings); + } +} \ No newline at end of file diff --git a/source/server/tasks/FileOperationTask.ts b/source/server/tasks/FileOperationTask.ts index 647b0b2..e7af9c2 100644 --- a/source/server/tasks/FileOperationTask.ts +++ b/source/server/tasks/FileOperationTask.ts @@ -55,7 +55,7 @@ export default class FileOperationTask extends Task properties: { operation: { type: "string", enum: [ "DeleteFile", "RenameFile", "CreateFolder", "DeleteFolder" ]}, name: { type: "string", minLength: 1 }, - newName: { type: "string", minLength: 1, default: "" } + newName: { type: "string", minLength: 1 } }, required: [ "operation", diff --git a/source/server/tools/ImageMagickTool.ts b/source/server/tools/ImageMagickTool.ts index 93ba918..4ca22ee 100644 --- a/source/server/tools/ImageMagickTool.ts +++ b/source/server/tools/ImageMagickTool.ts @@ -30,7 +30,7 @@ export interface IImageMagickToolSettings extends IToolSettings /** Name of the image file for the blue channel (optional, only required if combining individual channels). */ blueChannelInputFile?: string; /** Name of the output image file. */ - outputImageFile: string; + outputImageFile?: string; /** The compression quality for JPEG images (0 - 100). */ quality?: number; /** Automatic stretching of the final image. */ @@ -45,6 +45,12 @@ export interface IImageMagickToolSettings extends IToolSettings channelNormalize?: boolean; /** Gamma correction of the individual channels (1.0 = unchanged). */ channelGamma?: number[]; + /** Name of the RGB input image folder (for batch conversion). */ + inputImageFolder?: string; + /** Name of the output image folder. (for batch conversion) */ + outputImageFolder?: string; + /** Filetype to batch convert folders of images to. */ + batchConvertType?: string; } export type ImageMagickInstance = ToolInstance; @@ -59,63 +65,87 @@ export default class ImageMagickTool extends Tool { const settings = instance.settings; + let operation = ""; - const outputImagePath = instance.getFilePath(settings.outputImageFile); - if (!outputImagePath) { - throw new Error("ImageMagickTool: missing output map file"); - } - - let operation = "convert"; - - if (settings.channelCombine) { - const redImagePath = instance.getFilePath(settings.redChannelInputFile); - const greenImagePath = instance.getFilePath(settings.greenChannelInputFile); - const blueImagePath = instance.getFilePath(settings.blueChannelInputFile); + if(settings.inputImageFolder) { // batch conversion + const outputImagePath = instance.getFilePath(settings.outputImageFolder); + if (!outputImagePath) { + throw new Error("ImageMagickTool: missing output map folder"); + } - if (!redImagePath || !greenImagePath || !blueImagePath) { - throw new Error("ImageMagickTool.run - missing input map file"); + const inputImagePath = instance.getFilePath(settings.inputImageFolder); + if (!inputImagePath) { + throw new Error("ImageMagickTool: missing input map folder"); } - let channelGamma = [ 1.0, 1.0, 1.0 ]; - if (Array.isArray(settings.channelGamma) && settings.channelGamma.length === 3) { - channelGamma = settings.channelGamma; + if (!settings.batchConvertType) { + throw new Error("ImageMagickTool: missing filetype to convert to"); } - const channelAutoLevel = settings.channelNormalize ? "-auto-level" : ""; + operation = "mogrify"; - operation += [ - ` ( "${redImagePath}" ${channelAutoLevel} -gamma ${channelGamma[0]} )`, - ` ( "${greenImagePath}" ${channelAutoLevel} -gamma ${channelGamma[1]} )`, - ` ( "${blueImagePath}" ${channelAutoLevel} -gamma ${channelGamma[2]} ) -combine`, - ].join(""); - } - else { - const inputImagePath = instance.getFilePath(settings.inputImageFile); - operation += ` "${inputImagePath}"`; - } + let quality = settings.quality || 70; - let resize = settings.resize || 1.0; - if (resize <= 2.0 && resize !== 1.0) { - operation += ` -resize ${Math.round(resize * 100)}%`; - } - else if (resize !== 1.0) { - operation += ` -resize ${Math.round(resize)}`; + operation += ` -path "${outputImagePath}" -quality ${quality} -format ${settings.batchConvertType} "${inputImagePath}\\*.*"`; } + else { // single image conversion + const outputImagePath = instance.getFilePath(settings.outputImageFile); + if (!outputImagePath) { + throw new Error("ImageMagickTool: missing output map file"); + } - if (settings.normalize === true) { - operation += " -auto-level"; - } + operation = "convert"; - const gamma = settings.gamma || 1.0; - if (gamma !== 1.0) { - operation += ` -gamma ${gamma}`; - } + if (settings.channelCombine) { + const redImagePath = instance.getFilePath(settings.redChannelInputFile); + const greenImagePath = instance.getFilePath(settings.greenChannelInputFile); + const blueImagePath = instance.getFilePath(settings.blueChannelInputFile); + + if (!redImagePath || !greenImagePath || !blueImagePath) { + throw new Error("ImageMagickTool.run - missing input map file"); + } + + let channelGamma = [ 1.0, 1.0, 1.0 ]; + if (Array.isArray(settings.channelGamma) && settings.channelGamma.length === 3) { + channelGamma = settings.channelGamma; + } - let quality = settings.quality || 70; - if (outputImagePath.toLowerCase().endsWith("png")) { - quality = 100; + const channelAutoLevel = settings.channelNormalize ? "-auto-level" : ""; + + operation += [ + ` ( "${redImagePath}" ${channelAutoLevel} -gamma ${channelGamma[0]} )`, + ` ( "${greenImagePath}" ${channelAutoLevel} -gamma ${channelGamma[1]} )`, + ` ( "${blueImagePath}" ${channelAutoLevel} -gamma ${channelGamma[2]} ) -combine`, + ].join(""); + } + else { + const inputImagePath = instance.getFilePath(settings.inputImageFile); + operation += ` "${inputImagePath}"`; + } + + let resize = settings.resize || 1.0; + if (resize <= 2.0 && resize !== 1.0) { + operation += ` -resize ${Math.round(resize * 100)}%`; + } + else if (resize !== 1.0) { + operation += ` -resize ${Math.round(resize)}`; + } + + if (settings.normalize === true) { + operation += " -auto-level"; + } + + const gamma = settings.gamma || 1.0; + if (gamma !== 1.0) { + operation += ` -gamma ${gamma}`; + } + + let quality = settings.quality || 70; + if (outputImagePath.toLowerCase().endsWith("png")) { + quality = 100; + } + operation += ` -quality ${quality} "${outputImagePath}"`; } - operation += ` -quality ${quality} "${outputImagePath}"`; const command = `"${this.configuration.executable}" ${operation}`; From 9364cc530d627215f79700cce392a58a6c554c8a Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 11 Aug 2023 13:53:23 -0400 Subject: [PATCH 04/34] Save fix and added params for tiepointLimit and keypointLimit. --- server/recipes/photogrammetry.json | 16 +++++++++++++++- server/scripts/MetashapeGenerateMesh.py | 9 ++++++--- source/server/tasks/PhotogrammetryTask.ts | 8 ++++++++ source/server/tools/MetashapeTool.ts | 4 +++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index 0379d77..f6a9d28 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -48,13 +48,25 @@ "convertToJpg": { "type": "boolean", "default": false + }, + "tiepointLimit": { + "type": "number", + "default": 4000, + "minimum": 1000, + "maximum": 50000 + }, + "keypointLimit": { + "type": "number", + "default": 40000, + "minimum": 1000, + "maximum": 120000 } }, "required": [ "sourceImageFolder" ], "advanced": [ - "alignmentLimit", "convertToJpg" + "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit" ], "additionalProperties": false }, @@ -148,6 +160,8 @@ "generatePointCloud": "generatePointCloud", "optimizeMarkers": "optimizeMarkers", "alignmentLimit": "alignmentLimit", + "tiepointLimit": "tiepointLimit", + "keypointLimit": "keypointLimit", "tool": "tool", "timeout": 86400 }, diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 10ea30e..453a45d 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -23,6 +23,8 @@ def convert(s): parser.add_argument("-sb", required=False, help="Scalebar definition file") parser.add_argument("-optm", required=False, default="False", help="Optimize markers") parser.add_argument("-bdc", required=False, default="False", help="Build dense cloud") +parser.add_argument("-tp", required=False, default=4000, help="Tiepoint limit") +parser.add_argument("-kp", required=False, default=40000, help="Keypoint limit") args = parser.parse_args() doc = Metashape.app.document @@ -57,8 +59,8 @@ def convert(s): #reference_preselection_mode=Metashape.ReferencePreselectionSource, filter_mask=False, mask_tiepoints=False, - keypoint_limit=40000, - tiepoint_limit=4000, + keypoint_limit=args.kp, + tiepoint_limit=args.tp, keep_keypoints=False, guided_matching=False, reset_matches=False @@ -69,6 +71,7 @@ def convert(s): # save post-alignment doc.save(imagePath+"\\..\\"+name+"-align.psx") +chunk = doc.chunks[0] aligned = [camera for camera in chunk.cameras if camera.transform and camera.type==Metashape.Camera.Type.Regular] success_ratio = len(aligned) / len(chunk.cameras) * 100 @@ -242,4 +245,4 @@ def convert(s): chunk.exportCameras(camerasPath) chunk.exportReport(imagePath+"\\..\\"+name+"-report.pdf") -doc.save(imagePath+"\\..\\"+name+"-mesh.psx") +doc.save(imagePath+"\\..\\"+name+"-mesh.psx", [chunk]) diff --git a/source/server/tasks/PhotogrammetryTask.ts b/source/server/tasks/PhotogrammetryTask.ts index 2dbb68b..d8eadef 100644 --- a/source/server/tasks/PhotogrammetryTask.ts +++ b/source/server/tasks/PhotogrammetryTask.ts @@ -44,6 +44,10 @@ export interface IPhotogrammetryTaskParameters extends ITaskParameters optimizeMarkers: boolean; /** Percent success required to pass alignment stage */ alignmentLimit?: number; + /** Max number of tiepoints */ + tiepointLimit?: number; + /** Max number of keypoints */ + keypointLimit?: number; /** Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup, see [[IToolConfiguration]]). */ timeout?: number; /** Tool to use for photogrammetry ("Metashape" or "RealityCapture" or "Meshroom", default: "Metashape"). */ @@ -72,6 +76,8 @@ export default class PhotogrammetryTask extends ToolTask generatePointCloud: { type: "boolean", default: false}, optimizeMarkers: { type: "boolean", default: false}, alignmentLimit: { type: "number", default: 50}, + tiepointLimit: { type: "integer", default: 4000}, + keypointLimit: { type: "integer", default: 40000}, timeout: { type: "integer", default: 0 }, tool: { type: "string", enum: [ "Metashape", "RealityCapture", "Meshroom" ], default: "Metashape" } }, @@ -98,6 +104,8 @@ export default class PhotogrammetryTask extends ToolTask generatePointCloud: params.generatePointCloud, optimizeMarkers: params.optimizeMarkers, alignmentLimit: params.alignmentLimit, + tiepointLimit: params.tiepointLimit, + keypointLimit: params.keypointLimit, mode: "full", timeout: params.timeout }; diff --git a/source/server/tools/MetashapeTool.ts b/source/server/tools/MetashapeTool.ts index f791afa..5eaa7c9 100644 --- a/source/server/tools/MetashapeTool.ts +++ b/source/server/tools/MetashapeTool.ts @@ -31,6 +31,8 @@ export interface IMetashapeToolSettings extends IToolSettings generatePointCloud?: boolean; optimizeMarkers?: boolean; alignmentLimit?: number; + tiepointLimit?: number; + keypointLimit?: number; } export type MetashapeInstance = ToolInstance; @@ -82,7 +84,7 @@ export default class MetashapeTool extends Tool Date: Mon, 14 Aug 2023 12:47:08 -0400 Subject: [PATCH 05/34] Changing matchPhotos downscale to 1 --- server/scripts/MetashapeGenerateMesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 453a45d..cfc0218 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -53,7 +53,7 @@ def convert(s): chunk.matchPhotos\ ( - downscale=0, + downscale=1, generic_preselection=True, reference_preselection=False, #reference_preselection_mode=Metashape.ReferencePreselectionSource, From 21e4b35e5358bfc196f16818ec5906d9295b88af Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Mon, 21 Aug 2023 08:25:11 -0400 Subject: [PATCH 06/34] WIP Metashape camera filtering --- server/recipes/photogrammetry.json | 23 ++- server/scripts/MetashapeGenerateMesh.py | 172 +++++++++++++++++++++- source/server/tasks/PhotogrammetryTask.ts | 16 +- source/server/tools/MetashapeTool.ts | 12 ++ 4 files changed, 211 insertions(+), 12 deletions(-) diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index f6a9d28..1a7afc1 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -51,22 +51,36 @@ }, "tiepointLimit": { "type": "number", - "default": 4000, + "default": 25000, "minimum": 1000, "maximum": 50000 }, "keypointLimit": { "type": "number", - "default": 40000, + "default": 75000, "minimum": 1000, "maximum": 120000 + }, + "turntableGroups": { + "type": "boolean", + "default": false + }, + "genericPreselection": { + "type": "boolean", + "default": true + }, + "depthMaxNeighbors": { + "type": "number", + "default": 16, + "minimum": 4, + "maximum": 256 } }, "required": [ "sourceImageFolder" ], "advanced": [ - "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit" + "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit", "turntableGroups", "genericPreselection", "depthMaxNeighbors" ], "additionalProperties": false }, @@ -162,6 +176,9 @@ "alignmentLimit": "alignmentLimit", "tiepointLimit": "tiepointLimit", "keypointLimit": "keypointLimit", + "turntableGroups": "turntableGroups", + "genericPreselection": "genericPreselection", + "depthMaxNeighbors": "depthMaxNeighbors", "tool": "tool", "timeout": 86400 }, diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index cfc0218..ea07c32 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -2,8 +2,10 @@ import csv import sys import os +import math import argparse from os import walk, path +from statistics import mean, pstdev, variance def convert(s): if s.lower() == "true": @@ -11,6 +13,42 @@ def convert(s): else: return False +def mag(x): + return math.sqrt(sum(i**2 for i in x)) + +def findLowProjectionCameras(chunk, cameras, limit): + point_cloud = chunk.point_cloud + projections = point_cloud.projections + points = point_cloud.points + npoints = len(points) + tracks = point_cloud.tracks + point_ids = [-1] * len(point_cloud.tracks) + + for point_id in range(0, npoints): + point_ids[points[point_id].track_id] = point_id + + for camera in cameras: + nprojections = 0 + + if not camera.transform: + camera.enabled = False + print(camera, "NO ALIGNMENT") + continue + + for proj in projections[camera]: + track_id = proj.track_id + point_id = point_ids[track_id] + if point_id < 0: + continue + if not points[point_id].valid: + continue + + nprojections += 1 + + if nprojections <= limit: + camera.enabled = False + print(camera, nprojections, len(projections[camera])) + #get args argv = sys.argv @@ -23,8 +61,11 @@ def convert(s): parser.add_argument("-sb", required=False, help="Scalebar definition file") parser.add_argument("-optm", required=False, default="False", help="Optimize markers") parser.add_argument("-bdc", required=False, default="False", help="Build dense cloud") -parser.add_argument("-tp", required=False, default=4000, help="Tiepoint limit") -parser.add_argument("-kp", required=False, default=40000, help="Keypoint limit") +parser.add_argument("-tp", required=False, default=25000, help="Tiepoint limit") +parser.add_argument("-kp", required=False, default=75000, help="Keypoint limit") +parser.add_argument("-gp", required=False, default="True", help="Generic preselection") +parser.add_argument("-dmn", required=False, default=16, help="Depth map max neighbors") +parser.add_argument("-ttg", required=False, default="False", help="Process turntable groups") args = parser.parse_args() doc = Metashape.app.document @@ -32,6 +73,8 @@ def convert(s): imagePath = args.input camerasPath = args.cameras +processGroups = convert(args.ttg) +genericPreselection = convert(args.gp) name = os.path.basename(os.path.normpath(args.output)) name = os.path.splitext(name)[0]; @@ -51,10 +94,32 @@ def convert(s): # Add photos chunk.addPhotos(imageFiles) +# Sort into camera groups (if needed) +camera_groups = {} +camera_refs = dict() +if processGroups == True: + for photo in chunk.cameras: + name = str(photo.label) + # Remove the sequence number from the base name (CaptureOne Pro formatting) + base_name_without_sequence_number = name[0:name.rfind("_")] + #print(name + " --> " + base_name_without_sequence_number) + + # If this naming pattern doesn't have a camera group yet, create one + if base_name_without_sequence_number not in camera_groups: + camera_group = chunk.addCameraGroup() + camera_group.label = base_name_without_sequence_number + camera_groups[base_name_without_sequence_number] = camera_group + camera_refs[base_name_without_sequence_number] = [] + + # Add the camera to the appropriate camera group + photo.group = camera_groups[base_name_without_sequence_number] + + camera_refs[base_name_without_sequence_number].append(photo) + chunk.matchPhotos\ ( downscale=1, - generic_preselection=True, + generic_preselection=genericPreselection, reference_preselection=False, #reference_preselection_mode=Metashape.ReferencePreselectionSource, filter_mask=False, @@ -69,6 +134,102 @@ def convert(s): # align the matched image pairs chunk.alignCameras() +# evaluate alignment based on groups +if processGroups == True: + + findLowProjectionCameras(chunk, chunk.cameras, 100) + + good_cameras = [] + bad_cameras = [] + for camera in chunk.cameras: + if camera.enabled == False: + bad_cameras.append(camera) + else: + good_cameras.append(camera) + print(len(bad_cameras)) + for camera in bad_cameras: + camera.transform = None + + chunk.optimizeCameras( adaptive_fitting=True ) + + # Try to realign flagged cameras + for camera in bad_cameras: + camera.enabled = True + chunk.alignCameras([camera]) + + findLowProjectionCameras(chunk, bad_cameras, 100) + + # Try to realign again for good measure + for camera in bad_cameras: + if camera.enabled == False: + camera.transform = None + camera.enabled = True + chunk.alignCameras([camera]) + + findLowProjectionCameras(chunk, bad_cameras, 20) + + bad_cameras = [] + for camera in chunk.cameras: + if camera.enabled == False: + bad_cameras.append(camera) + camera.transform = None + print(len(bad_cameras)) + + # compute overall mean deviation + tot_avg = [0,0,0] + tot_dev = [] + tot_count = 0 + for camera in chunk.cameras: + if camera.center != None: + tot_count += 1 + for i, bi in enumerate(camera.center): tot_avg[i] += bi + else: + camera.enabled = False + tot_avg[0] /= tot_count + tot_avg[1] /= tot_count + tot_avg[2] /= tot_count + for camera in chunk.cameras: + if camera.center != None: + camera_err = [0,0,0] + camera_err[0] = camera.center[0] - tot_avg[0] + camera_err[1] = camera.center[1] - tot_avg[1] + camera_err[2] = camera.center[2] - tot_avg[2] + tot_dev.append(mag(camera_err)) + avg_dev = mean(tot_dev) + #print("AVG DEV: "+str(avg_dev)) + + # Identify cameras that are too tightly clustered within a group + for group in camera_refs.keys(): + camera_count = 0 + + pos_avg = [0,0,0] + for camera in camera_refs[group]: + if camera.center != None: + camera_count += 1 + for i, bi in enumerate(camera.center): pos_avg[i] += bi + pos_avg[0] /= camera_count + pos_avg[1] /= camera_count + pos_avg[2] /= camera_count + + loc_dev_arr = [] + for camera in camera_refs[group]: + if camera.center != None: + camera_err = [0,0,0] + camera_err[0] = camera.center[0] - pos_avg[0] + camera_err[1] = camera.center[1] - pos_avg[1] + camera_err[2] = camera.center[2] - pos_avg[2] + loc_dev_arr.append(mag(camera_err)) + if mag(camera_err) < avg_dev * 0.5: + if camera.enabled != False: + camera.enabled = False + bad_cameras.append(camera) + #grp_variance = variance(loc_dev_arr) + #std_dev = pstdev(loc_dev_arr) + #print(group, grp_variance, std_dev) + + #chunk.remove(bad_cameras) + + # save post-alignment doc.save(imagePath+"\\..\\"+name+"-align.psx") chunk = doc.chunks[0] @@ -82,10 +243,7 @@ def convert(s): sys.exit("Error: Image alignment does not meet minimum threshold") # optimize cameras -chunk.optimizeCameras\ -( - adaptive_fitting=True -) +chunk.optimizeCameras( adaptive_fitting=True ) if args.sb != None: ## Detect markers diff --git a/source/server/tasks/PhotogrammetryTask.ts b/source/server/tasks/PhotogrammetryTask.ts index d8eadef..1f98be6 100644 --- a/source/server/tasks/PhotogrammetryTask.ts +++ b/source/server/tasks/PhotogrammetryTask.ts @@ -48,6 +48,12 @@ export interface IPhotogrammetryTaskParameters extends ITaskParameters tiepointLimit?: number; /** Max number of keypoints */ keypointLimit?: number; + /** Flag to process images as SI-formatted turntable groups */ + turntableGroups: boolean; + /** Max neighbors value to use for depth map generation in Metashape */ + depthMaxNeighbors?: number; + /** Flag = true to use generic preselection in Metashape */ + genericPreselection?: boolean; /** Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup, see [[IToolConfiguration]]). */ timeout?: number; /** Tool to use for photogrammetry ("Metashape" or "RealityCapture" or "Meshroom", default: "Metashape"). */ @@ -76,8 +82,11 @@ export default class PhotogrammetryTask extends ToolTask generatePointCloud: { type: "boolean", default: false}, optimizeMarkers: { type: "boolean", default: false}, alignmentLimit: { type: "number", default: 50}, - tiepointLimit: { type: "integer", default: 4000}, - keypointLimit: { type: "integer", default: 40000}, + tiepointLimit: { type: "integer", default: 25000}, + keypointLimit: { type: "integer", default: 75000}, + turntableGroups: { type: "boolean", default: false}, + depthMaxNeighbors: { type: "integer", default: 16}, + genericPreselection: { type: "boolean", default: true}, timeout: { type: "integer", default: 0 }, tool: { type: "string", enum: [ "Metashape", "RealityCapture", "Meshroom" ], default: "Metashape" } }, @@ -106,6 +115,9 @@ export default class PhotogrammetryTask extends ToolTask alignmentLimit: params.alignmentLimit, tiepointLimit: params.tiepointLimit, keypointLimit: params.keypointLimit, + turntableGroups: params.turntableGroups, + depthMaxNeighbors: params.depthMaxNeighbors, + genericPreselection: params.genericPreselection, mode: "full", timeout: params.timeout }; diff --git a/source/server/tools/MetashapeTool.ts b/source/server/tools/MetashapeTool.ts index 5eaa7c9..f295407 100644 --- a/source/server/tools/MetashapeTool.ts +++ b/source/server/tools/MetashapeTool.ts @@ -33,6 +33,9 @@ export interface IMetashapeToolSettings extends IToolSettings alignmentLimit?: number; tiepointLimit?: number; keypointLimit?: number; + turntableGroups?: boolean; + depthMaxNeighbors?: number; + genericPreselection?: boolean; } export type MetashapeInstance = ToolInstance; @@ -89,6 +92,15 @@ export default class MetashapeTool extends Tool Date: Mon, 21 Aug 2023 08:40:20 -0400 Subject: [PATCH 07/34] Add output filename for zip --- server/recipes/si-nas-zip.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/recipes/si-nas-zip.json b/server/recipes/si-nas-zip.json index 6b637a3..ec4038d 100644 --- a/server/recipes/si-nas-zip.json +++ b/server/recipes/si-nas-zip.json @@ -12,6 +12,10 @@ "type": "string", "minLength": 1 }, + "outputFileBaseName": { + "type": "string", + "minLength": 1 + }, "filetype": { "type": "string", "minLength": 1 @@ -47,13 +51,14 @@ "description": "Zip files direct from storage", "pre": { "deliverables": { - "fileZip": "zippedFiles.zip" + "fileZip": "$firstTrue(outputFileBaseName, 'zippedFiles.zip')" } }, "parameters": { "inputFile1": "sourceFolderPath", "fileFilter": "filetype", "recursive": "recursive", + "outputFile": "deliverables.fileZip", "operation": "'nas-zip'" }, "success": "'delivery'", From f78810d86492654bebe67f921b753eb07da36315 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Mon, 21 Aug 2023 13:25:48 -0400 Subject: [PATCH 08/34] Exposed parameters for mesh quality, generic preselection, max neighbors --- server/recipes/photogrammetry.json | 20 +++++++++++++++++++- server/scripts/MetashapeGenerateMesh.py | 11 +++++++---- source/server/tasks/PhotogrammetryTask.ts | 10 +++++++++- source/server/tools/MetashapeTool.ts | 19 +++++++++++++++---- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index 1a7afc1..e587db1 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -74,13 +74,29 @@ "default": 16, "minimum": 4, "maximum": 256 + }, + "meshQuality": { + "type": "string", + "enum": [ + "Low", + "Medium", + "High", + "Highest", + "Custom" + ], + "default": "High" + }, + "customFaceCount": { + "type": "number", + "default": 3000000 } }, "required": [ "sourceImageFolder" ], "advanced": [ - "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit", "turntableGroups", "genericPreselection", "depthMaxNeighbors" + "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit", "turntableGroups", "genericPreselection", "depthMaxNeighbors", + "meshQuality", "customFaceCount" ], "additionalProperties": false }, @@ -179,6 +195,8 @@ "turntableGroups": "turntableGroups", "genericPreselection": "genericPreselection", "depthMaxNeighbors": "depthMaxNeighbors", + "meshQuality": "meshQuality", + "customFaceCount": "customFaceCount", "tool": "tool", "timeout": 86400 }, diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index ea07c32..d855aff 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -66,6 +66,8 @@ def findLowProjectionCameras(chunk, cameras, limit): parser.add_argument("-gp", required=False, default="True", help="Generic preselection") parser.add_argument("-dmn", required=False, default=16, help="Depth map max neighbors") parser.add_argument("-ttg", required=False, default="False", help="Process turntable groups") +parser.add_argument("-mq", required=False, default=2, help="Model resolution quality") +parser.add_argument("-cfc", required=False, default=3000000, help="Custom model face count") args = parser.parse_args() doc = Metashape.app.document @@ -139,6 +141,7 @@ def findLowProjectionCameras(chunk, cameras, limit): findLowProjectionCameras(chunk, chunk.cameras, 100) + # Sort cameras and reset bad ones good_cameras = [] bad_cameras = [] for camera in chunk.cameras: @@ -316,7 +319,7 @@ def findLowProjectionCameras(chunk, cameras, limit): downscale=1, filter_mode=Metashape.MildFiltering, reuse_depth=False, - max_neighbors=16, + max_neighbors=args.dmn, subdivide_task=True, workitem_size_cameras=20, max_workgroup_size=100 @@ -339,14 +342,14 @@ def findLowProjectionCameras(chunk, cameras, limit): max_workgroup_size=100 ) -modelQuality = [Metashape.FaceCount.HighFaceCount] +modelQuality = [Metashape.FaceCount.LowFaceCount, Metashape.FaceCount.MediumFaceCount, Metashape.FaceCount.HighFaceCount, Metashape.FaceCount.CustomFaceCount] chunk.buildModel\ ( surface_type=Metashape.Arbitrary, interpolation=Metashape.DisabledInterpolation, - face_count=modelQuality[0], - face_count_custom=200000, + face_count = modelQuality[3] if int(args.mq) < 0 else modelQuality[int(args.mq)], + face_count_custom = 0 if int(args.mq) < 0 else args.cfc, source_data = Metashape.DenseCloudData if denseCloudFlag == True else Metashape.DepthMapsData, vertex_colors=False, vertex_confidence=True, diff --git a/source/server/tasks/PhotogrammetryTask.ts b/source/server/tasks/PhotogrammetryTask.ts index 1f98be6..41a7a5e 100644 --- a/source/server/tasks/PhotogrammetryTask.ts +++ b/source/server/tasks/PhotogrammetryTask.ts @@ -49,11 +49,15 @@ export interface IPhotogrammetryTaskParameters extends ITaskParameters /** Max number of keypoints */ keypointLimit?: number; /** Flag to process images as SI-formatted turntable groups */ - turntableGroups: boolean; + turntableGroups?: boolean; /** Max neighbors value to use for depth map generation in Metashape */ depthMaxNeighbors?: number; /** Flag = true to use generic preselection in Metashape */ genericPreselection?: boolean; + /** Preset for mesh quality ("Low", "Medium", "High", "Highest", "Custom") */ + meshQuality?: string; + /** If meshQuality is custom, this defines the goal face count */ + customFaceCount?: number; /** Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup, see [[IToolConfiguration]]). */ timeout?: number; /** Tool to use for photogrammetry ("Metashape" or "RealityCapture" or "Meshroom", default: "Metashape"). */ @@ -87,6 +91,8 @@ export default class PhotogrammetryTask extends ToolTask turntableGroups: { type: "boolean", default: false}, depthMaxNeighbors: { type: "integer", default: 16}, genericPreselection: { type: "boolean", default: true}, + meshQuality: { type: "string", enum: [ "Low", "Medium", "High", "Highest", "Custom" ], default: "High"}, + customFaceCount: { type: "integer", default: 3000000}, timeout: { type: "integer", default: 0 }, tool: { type: "string", enum: [ "Metashape", "RealityCapture", "Meshroom" ], default: "Metashape" } }, @@ -118,6 +124,8 @@ export default class PhotogrammetryTask extends ToolTask turntableGroups: params.turntableGroups, depthMaxNeighbors: params.depthMaxNeighbors, genericPreselection: params.genericPreselection, + meshQuality: params.meshQuality, + customFaceCount: params.customFaceCount, mode: "full", timeout: params.timeout }; diff --git a/source/server/tools/MetashapeTool.ts b/source/server/tools/MetashapeTool.ts index f295407..41d9008 100644 --- a/source/server/tools/MetashapeTool.ts +++ b/source/server/tools/MetashapeTool.ts @@ -36,6 +36,8 @@ export interface IMetashapeToolSettings extends IToolSettings turntableGroups?: boolean; depthMaxNeighbors?: number; genericPreselection?: boolean; + meshQuality?: string; + customFaceCount?: number; } export type MetashapeInstance = ToolInstance; @@ -89,18 +91,27 @@ export default class MetashapeTool extends Tool e == settings.meshQuality); + operation += ` -mq ${qualityIdx} `; + + if(qualityIdx == 3) { + operation += ` -cfc ${settings.customFaceCount} `; + } + } } else if(settings.mode === "texture") { const inputModelPath = instance.getFilePath(settings.inputModelFile); From 1620d37b1b041ebae0bca1d2e01b5d7d76b47207 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 25 Aug 2023 13:36:04 -0400 Subject: [PATCH 09/34] WIP centering/axis-aligning for geometry cleanup --- server/recipes/clean.json | 25 +++++++ server/recipes/photogrammetry.json | 25 +++++++ server/scripts/MetashapeGenerateMesh.py | 92 ++++++++++++++++++++++++- source/server/tasks/CleanupMeshTask.ts | 38 +++++++++- source/server/tools/MeshlabTool.ts | 25 ++++++- 5 files changed, 200 insertions(+), 5 deletions(-) diff --git a/server/recipes/clean.json b/server/recipes/clean.json index e61fed2..c691c2a 100644 --- a/server/recipes/clean.json +++ b/server/recipes/clean.json @@ -30,6 +30,10 @@ "keepLargestComponent": { "type": "boolean", "default": true + }, + "isTurntable": { + "type": "boolean", + "default": false } }, "required": [ @@ -68,6 +72,25 @@ "highPolyMeshFile": "sourceMeshFile" } }, + "success": "'inspect'", + "failure": "$failure" + }, + "inspect": { + "task": "InspectMesh", + "description": "Validate mesh and inspect topology", + "pre": { + "deliverables": { + "inspectionReport": "outputFileBaseName & '-inspection.json'" + } + }, + "parameters": { + "meshFile": "sourceMeshFile", + "reportFile": "deliverables.inspectionReport", + "tool": "'Blender'" + }, + "post": { + "sceneSize": "$result.inspection.scene.geometry.size" + }, "success": "'clean-mesh'", "failure": "$failure" }, @@ -83,6 +106,8 @@ "inputMeshFile": "sourceMeshFile", "outputMeshFile": "deliverables.cleanedMeshFile", "keepLargestComponent": "keepLargestComponent", + "isTurntable": "isTurntable", + "sceneSize": "sceneSize", "timeout": 1200 }, "success": "'delivery'", diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index e587db1..ca49981 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -65,6 +65,10 @@ "type": "boolean", "default": false }, + "isTurntable": { + "type": "boolean", + "default": false + }, "genericPreselection": { "type": "boolean", "default": true @@ -212,6 +216,25 @@ "name": "'texturedMesh.obj'", "newName": "deliverables.meshFile" }, + "success": "'inspect'", + "failure": "$failure" + }, + "inspect": { + "task": "InspectMesh", + "description": "Validate mesh and inspect topology", + "pre": { + "deliverables": { + "inspectionReport": "outputFileBaseName & '-inspection.json'" + } + }, + "parameters": { + "meshFile": "deliverables.meshFile", + "reportFile": "deliverables.inspectionReport", + "tool": "'Blender'" + }, + "post": { + "sceneSize": "$result.inspection.scene.geometry.size" + }, "success": "'cleanup'", "failure": "$failure" }, @@ -226,6 +249,8 @@ "parameters": { "inputMeshFile": "deliverables.meshFile", "outputMeshFile": "deliverables.cleanedMeshFile", + "isTurntable": "isTurntable", + "sceneSize": "sceneSize", "timeout": 1200 }, "success": "'texture'", diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index d855aff..fc71cf7 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -49,6 +49,58 @@ def findLowProjectionCameras(chunk, cameras, limit): camera.enabled = False print(camera, nprojections, len(projections[camera])) +def matrixFromAxisAngle(axis, angle): + + c = math.cos(angle) + s = math.sin(angle) + t = 1.0 - c + + m00 = c + axis[0]*axis[0]*t + m11 = c + axis[1]*axis[1]*t + m22 = c + axis[2]*axis[2]*t + + tmp1 = axis[0]*axis[1]*t + tmp2 = axis[2]*s + m10 = tmp1 + tmp2 + m01 = tmp1 - tmp2 + tmp1 = axis[0]*axis[2]*t + tmp2 = axis[1]*s + m20 = tmp1 - tmp2 + m02 = tmp1 + tmp2 + tmp1 = axis[1]*axis[2]*t + tmp2 = axis[0]*s + m21 = tmp1 + tmp2 + m12 = tmp1 - tmp2 + + return Metashape.Matrix([[m00, m01, m02],[m10, m11, m12],[m20, m21, m22]]) + +def model_to_origin(chunk): + model = chunk.model + if not model: + print("No model in chunk, script aborted") + return 0 + vertices = model.vertices + T = chunk.transform.matrix + + minx = vertices[0].coord[0] + maxx = vertices[0].coord[0] + miny = vertices[0].coord[1] + maxy = vertices[0].coord[1] + minz = vertices[0].coord[2] + maxz = vertices[0].coord[2] + for i in range(0, len(vertices)): + minx = min(minx,vertices[i].coord[0]) + maxx = max(maxx,vertices[i].coord[0]) + miny = min(miny,vertices[i].coord[1]) + maxy = max(maxy,vertices[i].coord[1]) + minz = min(minz,vertices[i].coord[2]) + maxz = max(maxz,vertices[i].coord[2]) + print(minx,maxx,miny,maxy,minz,maxz) + avg = Metashape.Vector([(minx+maxx)/2.0,(miny+maxy)/2.0,(minz+maxz)/2.0]) + #chunk.region.center = avg + chunk.transform.translation = chunk.transform.translation - T.mulp(avg) + + #get args argv = sys.argv @@ -202,6 +254,7 @@ def findLowProjectionCameras(chunk, cameras, limit): #print("AVG DEV: "+str(avg_dev)) # Identify cameras that are too tightly clustered within a group + local_centers = [] for group in camera_refs.keys(): camera_count = 0 @@ -213,6 +266,7 @@ def findLowProjectionCameras(chunk, cameras, limit): pos_avg[0] /= camera_count pos_avg[1] /= camera_count pos_avg[2] /= camera_count + local_centers.append(pos_avg) loc_dev_arr = [] for camera in camera_refs[group]: @@ -222,7 +276,7 @@ def findLowProjectionCameras(chunk, cameras, limit): camera_err[1] = camera.center[1] - pos_avg[1] camera_err[2] = camera.center[2] - pos_avg[2] loc_dev_arr.append(mag(camera_err)) - if mag(camera_err) < avg_dev * 0.5: + if mag(camera_err) < avg_dev * 0.1: if camera.enabled != False: camera.enabled = False bad_cameras.append(camera) @@ -233,6 +287,37 @@ def findLowProjectionCameras(chunk, cameras, limit): #chunk.remove(bad_cameras) + # calculate center points + chunk_ctr = chunk.region.center + max_dist = 0 + max_idx = -1 + for i, pos in enumerate(local_centers): + dist = mag([chunk_ctr[0] - pos[0], chunk_ctr[1] - pos[1], chunk_ctr[2] - pos[2]]) + print(dist, max_idx) + if dist > max_dist: + max_dist = dist + max_idx = i + + # add line shape for up axis + #chunk.shapes = Metashape.Shapes() + #chunk.shapes.crs = chunk.crs + #new_shape = chunk.shapes.addShape() + #new_shape.geometry.type = Metashape.Geometry.Type.LineStringType + #new_shape.geometry = Metashape.Geometry.LineString([Metashape.Vector(chunk_ctr), Metashape.Vector(local_centers[max_idx])]) + + curr_dir = Metashape.Vector(local_centers[max_idx]) - Metashape.Vector(chunk_ctr) + curr_dir = curr_dir.normalized() + angle = math.acos(sum( [curr_dir[i]*[0,0,1][i] for i in range(len([0,0,1]))] )) + axis = Metashape.Vector.cross(curr_dir,[0,0,1]).normalized() + + rot_offset = matrixFromAxisAngle(axis, angle) + + R = chunk.region.rot*(rot_offset*chunk.region.rot.inv()) #Bounding box rotation matrix + C = chunk.region.center #Bounding box center vector + T = Metashape.Matrix( [[R[0,0], R[0,1], R[0,2], C[0]], [R[1,0], R[1,1], R[1,2], C[1]], [R[2,0], R[2,1], R[2,2], C[2]], [0, 0, 0, 1]]) + + chunk.transform.matrix = Metashape.Matrix.Rotation(rot_offset)*Metashape.Matrix.Translation(C).inv() #T.inv() + # save post-alignment doc.save(imagePath+"\\..\\"+name+"-align.psx") chunk = doc.chunks[0] @@ -240,7 +325,7 @@ def findLowProjectionCameras(chunk, cameras, limit): aligned = [camera for camera in chunk.cameras if camera.transform and camera.type==Metashape.Camera.Type.Regular] success_ratio = len(aligned) / len(chunk.cameras) * 100 print("ALIGNMENT SUCCESS: "+str(success_ratio)) - +#sys.exit(1) # exit out if alignment is less than requirement if success_ratio < int(args.align_limit): sys.exit("Error: Image alignment does not meet minimum threshold") @@ -381,6 +466,9 @@ def findLowProjectionCameras(chunk, cameras, limit): chunk.updateTransform() +# Move model to center +model_to_origin(chunk) + chunk.exportModel\ ( path=imagePath+"\\..\\"+args.output, diff --git a/source/server/tasks/CleanupMeshTask.ts b/source/server/tasks/CleanupMeshTask.ts index 3441255..bb54b1b 100644 --- a/source/server/tasks/CleanupMeshTask.ts +++ b/source/server/tasks/CleanupMeshTask.ts @@ -37,6 +37,10 @@ export interface ICleanupMeshTaskParameters extends ITaskParameters computeVertexNormals?: boolean; /** Meshlab only: Removes everything but the largest connected component. */ keepLargestComponent?: boolean; + /** Flag to enable optimizations for turntable captures. */ + isTurntable?: boolean; + /** String containing scene dimensions */ + sceneSize?: number[]; /** Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup, see [[IToolConfiguration]]). */ timeout?: number; } @@ -66,7 +70,9 @@ export default class CleanupMeshTask extends ToolTask preserveTexCoords: { type: "boolean", default: true }, computeVertexNormals: { type: "boolean", default: true }, keepLargestComponent: { type: "boolean", default: true }, - timeout: { type: "integer", default: 0 } + isTurntable: { type: "boolean", default: false }, + timeout: { type: "integer", default: 0 }, + sceneSize: { type: "array" } }, required: [ "inputMeshFile", @@ -113,6 +119,36 @@ export default class CleanupMeshTask extends ToolTask timeout: params.timeout }; + if(params.isTurntable) { + settings.filters.unshift( + /*{ + name: "CenterScene", + params: { + "traslMethod": 'Center on Scene BBox' + } + },*/ + { + name: "ConditionalFaceSelect", + params: { + "condSelect": 'abs(x0)<'+params.sceneSize[0]*0.02+' && abs(y0)<'+params.sceneSize[1]*0.02 //+' && abs(z0)<'+params.sceneSize[2]*0.1 + } + }, + { + name: "SelectConnectedFaces" + }, + { + name: "InvertSelection", + params: { + "InvFaces": true, + "InvVerts": false + } + }, + { + name: "DeleteSelected" + } + ); + } + this.addTool("Meshlab", settings); } } \ No newline at end of file diff --git a/source/server/tools/MeshlabTool.ts b/source/server/tools/MeshlabTool.ts index 5cab43f..f25b01c 100644 --- a/source/server/tools/MeshlabTool.ts +++ b/source/server/tools/MeshlabTool.ts @@ -58,7 +58,11 @@ export default class MeshlabTool extends Tool "ComputeFaceNormals": { name: "Re-Compute Face Normals" }, "ComputeVertexNormals": { name: "Re-Compute Vertex Normals" }, "SelectSmallComponents": { name: "Select small disconnected component"}, - "DeleteSelected": { name: "Delete Selected Faces and Vertices"} + "DeleteSelected": { name: "Delete Selected Faces and Vertices"}, + "CenterScene": { name: "Transform: Translate, Center, set Origin"}, + "ConditionalFaceSelect": { name: "Conditional Face Selection"}, + "SelectConnectedFaces": { name: "Select Connected Faces" }, + "InvertSelection": { name: "Invert Selection" } /*"MeshReport": { name: "Generate JSON Report", type: "xml" }*/ }; @@ -158,6 +162,19 @@ export default class MeshlabTool extends Tool filterSteps.forEach(filterDef => { const filterType = filterDef.type === "xml" ? "xmlfilter" : "filter"; + if(filterDef.name === "Transform: Translate, Center, set Origin") { + scriptLines.push(``); + scriptLines.push(``); + scriptLines.push(``); + scriptLines.push(``); + scriptLines.push(``); + scriptLines.push(``); + scriptLines.push(``); + scriptLines.push(``); + scriptLines.push(``); + return; + } + if (filter.params) { scriptLines.push(`<${filterType} name="${filterDef.name}">`); for (const paramName in filter.params) { @@ -188,12 +205,16 @@ export default class MeshlabTool extends Tool private getParameter(name: string, value: string | number | boolean, type?: string) { if (typeof value === "string") { - const parsedValue = parseFloat(value) || 0; + const parsedValue = parseFloat(value) || null; if (value.indexOf("%") > -1) { return ``; } + if (parsedValue == null) { + return ``; + } + value = parsedValue; } From 5e5f419fd3cc1492621e646b055b7812c3326031 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Wed, 30 Aug 2023 09:30:25 -0400 Subject: [PATCH 10/34] Script update for Metashape 2.0.2 --- server/scripts/MetashapeGenerateMesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index fc71cf7..f01a463 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -17,7 +17,7 @@ def mag(x): return math.sqrt(sum(i**2 for i in x)) def findLowProjectionCameras(chunk, cameras, limit): - point_cloud = chunk.point_cloud + point_cloud = chunk.tie_points projections = point_cloud.projections points = point_cloud.points npoints = len(points) From 124090cf3210d65b77f7118ba513b509f13095e8 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Thu, 31 Aug 2023 09:37:39 -0400 Subject: [PATCH 11/34] Renaming --- server/recipes/{si-nas-zip.json => si-path-zip.json} | 4 ++-- source/server/tasks/ZipTask.ts | 2 +- source/server/tools/SevenZipTool.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename server/recipes/{si-nas-zip.json => si-path-zip.json} (95%) diff --git a/server/recipes/si-nas-zip.json b/server/recipes/si-path-zip.json similarity index 95% rename from server/recipes/si-nas-zip.json rename to server/recipes/si-path-zip.json index ec4038d..75013c5 100644 --- a/server/recipes/si-nas-zip.json +++ b/server/recipes/si-path-zip.json @@ -1,7 +1,7 @@ { "id": "e965d8a9-6003-461a-bc92-c01aa67f9e94", "name": "si-nas-zip", - "description": "Zips files directly from storage folder", + "description": "Zips files directly from filesystem path", "version": "1", "start": "log", @@ -59,7 +59,7 @@ "fileFilter": "filetype", "recursive": "recursive", "outputFile": "deliverables.fileZip", - "operation": "'nas-zip'" + "operation": "'path-zip'" }, "success": "'delivery'", "failure": "$failure" diff --git a/source/server/tasks/ZipTask.ts b/source/server/tasks/ZipTask.ts index 7d0d961..159e3e0 100644 --- a/source/server/tasks/ZipTask.ts +++ b/source/server/tasks/ZipTask.ts @@ -75,7 +75,7 @@ export default class ZipTask extends ToolTask inputFile6: { type: "string" }, inputFile7: { type: "string" }, inputFile8: { type: "string" }, - operation: { type: "string", enum: [ "zip", "unzip", "nas-zip" ] }, + operation: { type: "string", enum: [ "zip", "unzip", "path-zip" ] }, outputFile: { type: "string", minLength: 1, default: "CookArchive.zip" }, compressionLevel: { type: "integer", minimum: 0, default: 5 }, fileFilter: { type: "string" }, diff --git a/source/server/tools/SevenZipTool.ts b/source/server/tools/SevenZipTool.ts index e24dcb2..d3016e5 100644 --- a/source/server/tools/SevenZipTool.ts +++ b/source/server/tools/SevenZipTool.ts @@ -78,7 +78,7 @@ export default class SevenZipTool extends Tool Date: Thu, 31 Aug 2023 13:03:00 -0400 Subject: [PATCH 12/34] Adding custom SI recipe combining zip and photogrammetry --- server/recipes/si-zip-photogrammetry.json | 296 ++++++++++++++++++++++ server/scripts/MetashapeGenerateMesh.py | 5 +- 2 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 server/recipes/si-zip-photogrammetry.json diff --git a/server/recipes/si-zip-photogrammetry.json b/server/recipes/si-zip-photogrammetry.json new file mode 100644 index 0000000..0dbf4b1 --- /dev/null +++ b/server/recipes/si-zip-photogrammetry.json @@ -0,0 +1,296 @@ +{ + "id": "7310026c-68cb-4470-9841-f026e3bd9069", + "name": "si-zip-photogrammetry", + "description": "Zip an image folder and process through photogrammetry pipeline", + "version": "1", + "start": "log", + + "parameterSchema": { + "type": "object", + "properties": { + "sourceFolderPath": { + "type": "string", + "minLength": 1 + }, + "filetype": { + "type": "string", + "minLength": 1 + }, + "outputFileBaseName": { + "type": "string", + "minLength": 1 + }, + "tool": { + "type": "string", + "enum": [ + "Metashape", + "RealityCapture", + "Meshroom" + ], + "default": "Metashape" + }, + "scalebarCSV": { + "type": "string", + "minLength": 1, + "format": "file" + }, + "generatePointCloud": { + "type": "boolean", + "default": false + }, + "optimizeMarkers": { + "type": "boolean", + "default": false + }, + "alignmentLimit": { + "type": "number", + "default": 50, + "minimum": 0, + "maximum": 100 + }, + "convertToJpg": { + "type": "boolean", + "default": false + }, + "tiepointLimit": { + "type": "number", + "default": 25000, + "minimum": 1000, + "maximum": 50000 + }, + "keypointLimit": { + "type": "number", + "default": 75000, + "minimum": 1000, + "maximum": 120000 + }, + "turntableGroups": { + "type": "boolean", + "default": false + }, + "isTurntable": { + "type": "boolean", + "default": false + }, + "genericPreselection": { + "type": "boolean", + "default": true + }, + "depthMaxNeighbors": { + "type": "number", + "default": 16, + "minimum": 4, + "maximum": 256 + }, + "meshQuality": { + "type": "string", + "enum": [ + "Low", + "Medium", + "High", + "Highest", + "Custom" + ], + "default": "High" + }, + "customFaceCount": { + "type": "number", + "default": 3000000 + } + }, + "required": [ + "sourceFolderPath" + ], + "advanced": [ + "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit", "turntableGroups", "genericPreselection", "depthMaxNeighbors", + "meshQuality", "customFaceCount" + ], + "additionalProperties": false + }, + + "steps": { + "log": { + "task": "Log", + "description": "Enable logging services", + "pre": { + "outputFileBaseName": "$baseName($firstTrue(outputFileBaseName, sourceFolderPath))", + "baseMeshName": "$firstTrue(outputFileBaseName, sourceFolderPath)", + "sourceFolderBaseName": "$baseName(sourceFolderPath)" + }, + "parameters": { + "logToConsole": true, + "reportFile": "outputFileBaseName & '-report.json'" + }, + "success": "'zip-files'", + "failure": "$failure" + }, + "zip-files": { + "task": "Zip", + "description": "Zip files direct from storage", + "pre": { + "deliverables": { + "fileZip": "sourceFolderBaseName & '.zip'" + } + }, + "parameters": { + "inputFile1": "sourceFolderPath", + "fileFilter": "filetype", + "recursive": false, + "outputFile": "deliverables.fileZip", + "operation": "'path-zip'" + }, + "success": "'unzip'", + "failure": "$failure" + }, + "unzip": { + "task": "Zip", + "description": "Unzip image folder", + "parameters": { + "inputFile1": "deliverables.fileZip", + "operation": "'unzip'" + }, + "success": "'make-convert-folder'", + "failure": "$failure" + }, + "make-convert-folder": { + "task": "FileOperation", + "skip": "$not(convertToJpg)", + "description": "Create folder for converted images", + "pre": { + "sourceFolderConverted": "sourceFolderBaseName & '_converted'" + }, + "parameters": { + "operation": "'CreateFolder'", + "name": "sourceFolderConverted" + }, + "success": "'convert-to-jpg'", + "failure": "$failure" + }, + "convert-to-jpg": { + "task": "BatchConvertImage", + "skip": "$not(convertToJpg)", + "description": "Convert images to .jpg", + "parameters": { + "inputImageFolder": "sourceFolderBaseName", + "outputImageFolder": "sourceFolderConverted", + "filetype": "jpg", + "quality": "85" + }, + "success": "'photogrammetry'", + "failure": "$failure" + }, + "photogrammetry": { + "task": "Photogrammetry", + "description": "Create mesh and texture from image set.", + "pre": { + "camerasFile": "baseMeshName & '-cameras.xml'", + "deliverables": { + "meshFile": "baseMeshName & '-' & $lowercase(tool) & '.obj'", + "textureFile": "baseMeshName & '-texture-' & '.png'" + } + }, + "parameters": { + "inputImageFolder": "convertToJpg ? sourceFolderConverted : sourceFolderBaseName", + "camerasFile": "camerasFile", + "outputFile": "deliverables.meshFile", + "scalebarFile": "scalebarCSV", + "generatePointCloud": "generatePointCloud", + "optimizeMarkers": "optimizeMarkers", + "alignmentLimit": "alignmentLimit", + "tiepointLimit": "tiepointLimit", + "keypointLimit": "keypointLimit", + "turntableGroups": "turntableGroups", + "genericPreselection": "genericPreselection", + "depthMaxNeighbors": "depthMaxNeighbors", + "meshQuality": "meshQuality", + "customFaceCount": "customFaceCount", + "tool": "tool", + "timeout": 86400 + }, + "success": "'rename'", + "failure": "$failure" + }, + "rename": { + "task": "FileOperation", + "description": "Rename Meshroom Model", + "skip": "$not(tool = 'Meshroom')", + "parameters": { + "operation": "'RenameFile'", + "name": "'texturedMesh.obj'", + "newName": "deliverables.meshFile" + }, + "success": "'inspect'", + "failure": "$failure" + }, + "inspect": { + "task": "InspectMesh", + "description": "Validate mesh and inspect topology", + "pre": { + "deliverables": { + "inspectionReport": "outputFileBaseName & '-inspection.json'" + } + }, + "parameters": { + "meshFile": "deliverables.meshFile", + "reportFile": "deliverables.inspectionReport", + "tool": "'Blender'" + }, + "post": { + "sceneSize": "$result.inspection.scene.geometry.size" + }, + "success": "'cleanup'", + "failure": "$failure" + }, + "cleanup": { + "task": "CleanupMesh", + "description": "Cleanup common issues with mesh.", + "pre": { + "deliverables": { + "cleanedMeshFile": "baseMeshName & '-cleaned' & '.obj'" + } + }, + "parameters": { + "inputMeshFile": "deliverables.meshFile", + "outputMeshFile": "deliverables.cleanedMeshFile", + "isTurntable": "isTurntable", + "sceneSize": "sceneSize", + "timeout": 1200 + }, + "success": "'texture'", + "failure": "$failure" + }, + "texture": { + "task": "PhotogrammetryTex", + "skip": "$not(tool = 'Metashape')", + "description": "Create and map texture from model and image set.", + "pre": { + "deliverables": { + "finalMeshFile": "baseMeshName & '-' & $lowercase(tool) & '-final.obj'", + "textureFile": "baseMeshName & '-texture-final' & '.png'" + } + }, + "parameters": { + "inputImageFolder": "convertToJpg ? sourceFolderConverted : sourceFolderBaseName", + "inputModelFile": "deliverables.cleanedMeshFile", + "camerasFile": "camerasFile", + "outputFile": "deliverables.finalMeshFile", + "scalebarFile": "scalebarCSV", + "tool": "tool", + "timeout": 86400 + }, + "success": "'delivery'", + "failure": "$failure" + }, + "delivery": { + "task": "Delivery", + "description": "Send result files back to client", + "parameters": { + "method": "transportMethod", + "path": "$firstTrue(deliveryPath, pickupPath, $currentDir)", + "files": "deliverables" + }, + "success": "$success", + "failure": "$failure" + } + } +} \ No newline at end of file diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index f01a463..409fb34 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -466,8 +466,9 @@ def model_to_origin(chunk): chunk.updateTransform() -# Move model to center -model_to_origin(chunk) +if processGroups == True: + # Move model to center + model_to_origin(chunk) chunk.exportModel\ ( From 8f686cde243445f17a172b4bd91c909b79eb75ab Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Thu, 31 Aug 2023 13:55:13 -0400 Subject: [PATCH 13/34] Add missing validation option --- source/server/tasks/ZipTask.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/server/tasks/ZipTask.ts b/source/server/tasks/ZipTask.ts index 159e3e0..05b0ad0 100644 --- a/source/server/tasks/ZipTask.ts +++ b/source/server/tasks/ZipTask.ts @@ -38,7 +38,7 @@ export interface IZipTaskParameters extends ITaskParameters inputFile7?: string; inputFile8?: string; /** The type of zip operation we want to do. */ - operation: "zip" | "unzip"; + operation: "zip" | "unzip" | "path-zip"; /** Name to give generated zip file. */ outputFile?: string; /** Degree of compression */ From f03509d70f1f2ca994affac8f32261cafd295eb8 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Thu, 7 Sep 2023 11:37:57 -0400 Subject: [PATCH 14/34] Alignment-only image set support --- server/recipes/photogrammetry.json | 29 ++++++-- server/recipes/si-zip-photogrammetry.json | 34 +++++++++ server/scripts/MetashapeGenerateMesh.py | 90 +++++++++++++---------- source/server/tasks/PhotogrammetryTask.ts | 4 + source/server/tools/MetashapeTool.ts | 5 ++ 5 files changed, 118 insertions(+), 44 deletions(-) diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index ca49981..c1d94a7 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -31,6 +31,11 @@ "minLength": 1, "format": "file" }, + "alignImageFolder": { + "type": "string", + "minLength": 1, + "format": "file" + }, "generatePointCloud": { "type": "boolean", "default": false @@ -65,7 +70,7 @@ "type": "boolean", "default": false }, - "isTurntable": { + "findTurntableCenter": { "type": "boolean", "default": false }, @@ -128,7 +133,8 @@ "method": "transportMethod", "path": "$firstTrue(pickupPath, $currentDir)", "files": { - "sourceImageFolder": "sourceImageFolder" + "sourceImageFolder": "sourceImageFolder", + "alignImageFolder": "alignImageFolder" } }, "success": "'unzip'", @@ -137,13 +143,21 @@ "unzip": { "task": "Zip", "description": "Unzip image folder", + "parameters": { + "inputFile1": "sourceImageFolder", + "operation": "'unzip'" + }, + "success": "alignImageFolder ? 'unzip-align' : 'make-convert-folder'", + "failure": "$failure" + }, + "unzip-align": { + "task": "Zip", + "description": "Unzip alignment image folder", "pre": { - "deliverables": { - "objZipLow": "scaleToMeters ? baseMeshMapNameLow & '-obj_std.zip' : baseMeshMapNameLow & '-obj.zip'" - } + "alignFolderBaseName": "$baseName(alignImageFolder)" }, "parameters": { - "inputFile1": "sourceImageFolder", + "inputFile1": "alignImageFolder", "operation": "'unzip'" }, "success": "'make-convert-folder'", @@ -188,6 +202,7 @@ }, "parameters": { "inputImageFolder": "convertToJpg ? sourceFolderConverted : sourceFolderBaseName", + "alignImageFolder": "alignFolderBaseName", "camerasFile": "camerasFile", "outputFile": "deliverables.meshFile", "scalebarFile": "scalebarCSV", @@ -249,7 +264,7 @@ "parameters": { "inputMeshFile": "deliverables.meshFile", "outputMeshFile": "deliverables.cleanedMeshFile", - "isTurntable": "isTurntable", + "isTurntable": "findTurntableCenter", "sceneSize": "sceneSize", "timeout": 1200 }, diff --git a/server/recipes/si-zip-photogrammetry.json b/server/recipes/si-zip-photogrammetry.json index 0dbf4b1..d291f75 100644 --- a/server/recipes/si-zip-photogrammetry.json +++ b/server/recipes/si-zip-photogrammetry.json @@ -20,6 +20,10 @@ "type": "string", "minLength": 1 }, + "alignFolderPath": { + "type": "string", + "minLength": 1 + }, "tool": { "type": "string", "enum": [ @@ -149,6 +153,35 @@ "inputFile1": "deliverables.fileZip", "operation": "'unzip'" }, + "success": "alignFolderPath ? 'zip-align' : 'make-convert-folder'", + "failure": "$failure" + }, + "zip-align": { + "task": "Zip", + "description": "Zip alignment files direct from storage", + "pre": { + "alignFolderBaseName": "$baseName(alignFolderPath)", + "deliverables": { + "alignFileZip": "alignFolderBaseName & '.zip'" + } + }, + "parameters": { + "inputFile1": "alignFolderPath", + "fileFilter": "filetype", + "recursive": false, + "outputFile": "deliverables.alignFileZip", + "operation": "'path-zip'" + }, + "success": "'unzip-align'", + "failure": "$failure" + }, + "unzip-align": { + "task": "Zip", + "description": "Unzip alignment image folder", + "parameters": { + "inputFile1": "deliverables.alignFileZip", + "operation": "'unzip'" + }, "success": "'make-convert-folder'", "failure": "$failure" }, @@ -191,6 +224,7 @@ }, "parameters": { "inputImageFolder": "convertToJpg ? sourceFolderConverted : sourceFolderBaseName", + "alignImageFolder": "alignFolderBaseName", "camerasFile": "camerasFile", "outputFile": "deliverables.meshFile", "scalebarFile": "scalebarCSV", diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 409fb34..e750cbf 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -109,6 +109,7 @@ def model_to_origin(chunk): parser.add_argument("-i", "--input", required=True, help="Input filepath") parser.add_argument("-c", "--cameras", required=True, help="Cameras filepath") parser.add_argument("-o", "--output", required=True, help="Output filename") +parser.add_argument("-ai", "--align_input", required=False, help="Alignment input filepath") parser.add_argument("-al", "--align_limit", required=False, help="Alignment threshold (%)") parser.add_argument("-sb", required=False, help="Scalebar definition file") parser.add_argument("-optm", required=False, default="False", help="Optimize markers") @@ -129,8 +130,8 @@ def model_to_origin(chunk): camerasPath = args.cameras processGroups = convert(args.ttg) genericPreselection = convert(args.gp) -name = os.path.basename(os.path.normpath(args.output)) -name = os.path.splitext(name)[0]; +basename = os.path.basename(os.path.normpath(args.output)) +basename = os.path.splitext(basename)[0]; # Grab images from directory (include subdirectories) imageFiles=[] @@ -145,30 +146,48 @@ def model_to_origin(chunk): # set 'Marker Projection Accuracy' to 0.1 chunk.marker_projection_accuracy = 0.1 +# Add optional alignment images +camera_groups = {} +alignImages=[] +alignCameras=[] +if args.align_input != None: + alignPath = args.align_input + camera_group = chunk.addCameraGroup() + camera_group.label = "alignment_images" + camera_groups["alignment_images"] = camera_group + for r, d, f in walk(alignPath): + for i, file in enumerate(f): + alignImages.append(os.path.join(r, file)) + chunk.addPhotos(alignImages) + for photo in chunk.cameras: + if photo.group == None: + photo.group = camera_group + alignCameras.append(photo) + # Add photos chunk.addPhotos(imageFiles) # Sort into camera groups (if needed) -camera_groups = {} camera_refs = dict() if processGroups == True: for photo in chunk.cameras: - name = str(photo.label) - # Remove the sequence number from the base name (CaptureOne Pro formatting) - base_name_without_sequence_number = name[0:name.rfind("_")] - #print(name + " --> " + base_name_without_sequence_number) + if photo.group == None: + name = str(photo.label) + # Remove the sequence number from the base name (CaptureOne Pro formatting) + base_name_without_sequence_number = name[0:name.rfind("_")] + #print(name + " --> " + base_name_without_sequence_number) - # If this naming pattern doesn't have a camera group yet, create one - if base_name_without_sequence_number not in camera_groups: - camera_group = chunk.addCameraGroup() - camera_group.label = base_name_without_sequence_number - camera_groups[base_name_without_sequence_number] = camera_group - camera_refs[base_name_without_sequence_number] = [] + # If this naming pattern doesn't have a camera group yet, create one + if base_name_without_sequence_number not in camera_groups: + camera_group = chunk.addCameraGroup() + camera_group.label = base_name_without_sequence_number + camera_groups[base_name_without_sequence_number] = camera_group + camera_refs[base_name_without_sequence_number] = [] - # Add the camera to the appropriate camera group - photo.group = camera_groups[base_name_without_sequence_number] + # Add the camera to the appropriate camera group + photo.group = camera_groups[base_name_without_sequence_number] - camera_refs[base_name_without_sequence_number].append(photo) + camera_refs[base_name_without_sequence_number].append(photo) chunk.matchPhotos\ ( @@ -228,7 +247,7 @@ def model_to_origin(chunk): if camera.enabled == False: bad_cameras.append(camera) camera.transform = None - print(len(bad_cameras)) + print("FLAGGED BAD CAMERAS: ", len(bad_cameras)) # compute overall mean deviation tot_avg = [0,0,0] @@ -253,7 +272,7 @@ def model_to_origin(chunk): avg_dev = mean(tot_dev) #print("AVG DEV: "+str(avg_dev)) - # Identify cameras that are too tightly clustered within a group + # Identify cameras that are too tightly clustered within a group (currently disabled) local_centers = [] for group in camera_refs.keys(): camera_count = 0 @@ -276,13 +295,10 @@ def model_to_origin(chunk): camera_err[1] = camera.center[1] - pos_avg[1] camera_err[2] = camera.center[2] - pos_avg[2] loc_dev_arr.append(mag(camera_err)) - if mag(camera_err) < avg_dev * 0.1: - if camera.enabled != False: - camera.enabled = False - bad_cameras.append(camera) - #grp_variance = variance(loc_dev_arr) - #std_dev = pstdev(loc_dev_arr) - #print(group, grp_variance, std_dev) + # if mag(camera_err) < avg_dev * 0.1: + # if camera.enabled != False: + # camera.enabled = False + # bad_cameras.append(camera) #chunk.remove(bad_cameras) @@ -297,14 +313,8 @@ def model_to_origin(chunk): if dist > max_dist: max_dist = dist max_idx = i - - # add line shape for up axis - #chunk.shapes = Metashape.Shapes() - #chunk.shapes.crs = chunk.crs - #new_shape = chunk.shapes.addShape() - #new_shape.geometry.type = Metashape.Geometry.Type.LineStringType - #new_shape.geometry = Metashape.Geometry.LineString([Metashape.Vector(chunk_ctr), Metashape.Vector(local_centers[max_idx])]) + # calculate rotation offset to up vector curr_dir = Metashape.Vector(local_centers[max_idx]) - Metashape.Vector(chunk_ctr) curr_dir = curr_dir.normalized() angle = math.acos(sum( [curr_dir[i]*[0,0,1][i] for i in range(len([0,0,1]))] )) @@ -313,23 +323,29 @@ def model_to_origin(chunk): rot_offset = matrixFromAxisAngle(axis, angle) R = chunk.region.rot*(rot_offset*chunk.region.rot.inv()) #Bounding box rotation matrix - C = chunk.region.center #Bounding box center vector + C = chunk.region.center #Bounding box center vector T = Metashape.Matrix( [[R[0,0], R[0,1], R[0,2], C[0]], [R[1,0], R[1,1], R[1,2], C[1]], [R[2,0], R[2,1], R[2,2], C[2]], [0, 0, 0, 1]]) chunk.transform.matrix = Metashape.Matrix.Rotation(rot_offset)*Metashape.Matrix.Translation(C).inv() #T.inv() +# disable alignment-only cameras +if args.align_input != None: + for camera in alignCameras: + camera.enabled = False + # save post-alignment -doc.save(imagePath+"\\..\\"+name+"-align.psx") +doc.save(imagePath+"\\..\\"+basename+"-align.psx") chunk = doc.chunks[0] aligned = [camera for camera in chunk.cameras if camera.transform and camera.type==Metashape.Camera.Type.Regular] success_ratio = len(aligned) / len(chunk.cameras) * 100 print("ALIGNMENT SUCCESS: "+str(success_ratio)) -#sys.exit(1) + # exit out if alignment is less than requirement if success_ratio < int(args.align_limit): sys.exit("Error: Image alignment does not meet minimum threshold") +#sys.exit(1) # optimize cameras chunk.optimizeCameras( adaptive_fitting=True ) @@ -493,6 +509,6 @@ def model_to_origin(chunk): ) chunk.exportCameras(camerasPath) -chunk.exportReport(imagePath+"\\..\\"+name+"-report.pdf") +chunk.exportReport(imagePath+"\\..\\"+basename+"-report.pdf") -doc.save(imagePath+"\\..\\"+name+"-mesh.psx", [chunk]) +doc.save(imagePath+"\\..\\"+basename+"-mesh.psx", [chunk]) diff --git a/source/server/tasks/PhotogrammetryTask.ts b/source/server/tasks/PhotogrammetryTask.ts index 41a7a5e..c9c45d9 100644 --- a/source/server/tasks/PhotogrammetryTask.ts +++ b/source/server/tasks/PhotogrammetryTask.ts @@ -32,6 +32,8 @@ export interface IPhotogrammetryTaskParameters extends ITaskParameters { /** Input image folder. */ inputImageFolder: string; + /** Alignment image folder. */ + alignImageFolder?: string; /** Base name used for output files */ outputFile: string; /** Name used for saved camera position file */ @@ -80,6 +82,7 @@ export default class PhotogrammetryTask extends ToolTask type: "object", properties: { inputImageFolder: { type: "string", minLength: 1 }, + alignImageFolder: { type: "string", minLength: 1 }, outputFile: { type: "string", minLength: 1 }, camerasFile: { type: "string", minLength: 1 }, scalebarFile: { type: "string", minLength: 1 }, @@ -113,6 +116,7 @@ export default class PhotogrammetryTask extends ToolTask if (params.tool === "Metashape") { const toolOptions: IMetashapeToolSettings = { imageInputFolder: params.inputImageFolder, + alignImageFolder: params.alignImageFolder, outputFile: params.outputFile, camerasFile: params.camerasFile, scalebarFile: params.scalebarFile, diff --git a/source/server/tools/MetashapeTool.ts b/source/server/tools/MetashapeTool.ts index 41d9008..113f1a0 100644 --- a/source/server/tools/MetashapeTool.ts +++ b/source/server/tools/MetashapeTool.ts @@ -23,6 +23,7 @@ import Tool, { IToolMessageEvent, IToolSettings, IToolSetup, ToolInstance } from export interface IMetashapeToolSettings extends IToolSettings { imageInputFolder: string; + alignImageFolder?: string; outputFile: string; mode: string; inputModelFile?: string; @@ -94,6 +95,10 @@ export default class MetashapeTool extends Tool Date: Fri, 8 Sep 2023 11:18:32 -0400 Subject: [PATCH 15/34] Added support for generating screenshot task --- server/recipes/photogrammetry.json | 10 +++++ server/recipes/si-zip-photogrammetry.json | 14 +++++- server/scripts/BlenderScreenshot.py | 52 +++++++++++++++++++++++ source/server/tools/BlenderTool.ts | 3 ++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 server/scripts/BlenderScreenshot.py diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index c1d94a7..5239339 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -290,6 +290,16 @@ "tool": "tool", "timeout": 86400 }, + "success": "'screenshot'", + "failure": "$failure" + }, + "screenshot": { + "task": "Screenshot", + "description": "Generate screenshot of result geometry.", + "parameters": { + "inputMeshFile": "deliverables.finalMeshFile", + "timeout": 1200 + }, "success": "'delivery'", "failure": "$failure" }, diff --git a/server/recipes/si-zip-photogrammetry.json b/server/recipes/si-zip-photogrammetry.json index d291f75..cbdcd5d 100644 --- a/server/recipes/si-zip-photogrammetry.json +++ b/server/recipes/si-zip-photogrammetry.json @@ -72,7 +72,7 @@ "type": "boolean", "default": false }, - "isTurntable": { + "findTurntableCenter": { "type": "boolean", "default": false }, @@ -286,7 +286,7 @@ "parameters": { "inputMeshFile": "deliverables.meshFile", "outputMeshFile": "deliverables.cleanedMeshFile", - "isTurntable": "isTurntable", + "isTurntable": "findTurntableCenter", "sceneSize": "sceneSize", "timeout": 1200 }, @@ -312,6 +312,16 @@ "tool": "tool", "timeout": 86400 }, + "success": "'screenshot'", + "failure": "$failure" + }, + "screenshot": { + "task": "Screenshot", + "description": "Generate screenshot of result geometry.", + "parameters": { + "inputMeshFile": "deliverables.finalMeshFile", + "timeout": 1200 + }, "success": "'delivery'", "failure": "$failure" }, diff --git a/server/scripts/BlenderScreenshot.py b/server/scripts/BlenderScreenshot.py new file mode 100644 index 0000000..21a1e84 --- /dev/null +++ b/server/scripts/BlenderScreenshot.py @@ -0,0 +1,52 @@ +import bpy +import json +import os +import sys + +# get rid of default mesh objects +for ob in bpy.context.scene.objects: + if ob.type == 'MESH': + ob.select_set(True) + +#bpy.ops.object.select_all(action='SELECT') +bpy.ops.object.delete(use_global=False) +bpy.ops.outliner.orphans_purge() +bpy.ops.outliner.orphans_purge() +bpy.ops.outliner.orphans_purge() + +#get args +argv = sys.argv +argv = argv[argv.index("--") + 1:] + +#get import file extension +filename, file_extension = os.path.splitext(argv[0]) +file_extension = file_extension.lower() + +#import scene +if file_extension == '.obj': + bpy.ops.wm.obj_import(filepath=argv[0]) +elif file_extension == '.ply': + bpy.ops.import_mesh.ply(filepath=argv[0]) +elif file_extension == '.stl': + bpy.ops.import_mesh.stl(filepath=argv[0]) +elif file_extension == '.x3d': + bpy.ops.import_scene.x3d(filepath=argv[0]) +elif file_extension == '.dae': + bpy.ops.wm.collada_import(filepath=argv[0]) +elif file_extension == '.fbx': + bpy.ops.import_scene.fbx(filepath=argv[0]) +elif file_extension == '.glb' or file_extension == '.gltf': + bpy.ops.import_scene.gltf(filepath=argv[0]) +else: + print("Error: Unsupported file type: " + file_extension) + sys.exit(1) + +if len(bpy.data.objects) > 0: + bpy.context.scene.camera = bpy.context.scene.objects.get('Camera') + dir = os.path.dirname(filename) + save_file = os.path.join(dir, "preview.png") + print("Writing preview image: " + save_file) + bpy.ops.view3d.camera_to_view_selected() + +bpy.context.scene.render.filepath = save_file +bpy.ops.render.render(write_still = True) diff --git a/source/server/tools/BlenderTool.ts b/source/server/tools/BlenderTool.ts index aed87b9..58e4d54 100644 --- a/source/server/tools/BlenderTool.ts +++ b/source/server/tools/BlenderTool.ts @@ -95,6 +95,9 @@ export default class BlenderTool extends Tool else if(settings.mode === "merge") { operation += ` --python "${instance.getFilePath("../../scripts/BlenderMergeTextures.py")}" -- "${inputFilePath}" "${instance.getFilePath(settings.outputFile2)}" "${instance.getFilePath(settings.outputFile)}"`; } + else if(settings.mode === "screenshot") { + operation += ` --python "${instance.getFilePath("../../scripts/BlenderScreenshot.py")}" -- "${inputFilePath}"`; + } const command = `"${this.configuration.executable}" ${operation}`; From 71f996df6eb320682f22e4b30ddcd6242e6b6ccd Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 8 Sep 2023 11:23:28 -0400 Subject: [PATCH 16/34] Adding missing file --- source/server/tasks/ScreenshotTask.ts | 75 +++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 source/server/tasks/ScreenshotTask.ts diff --git a/source/server/tasks/ScreenshotTask.ts b/source/server/tasks/ScreenshotTask.ts new file mode 100644 index 0000000..f1ebaad --- /dev/null +++ b/source/server/tasks/ScreenshotTask.ts @@ -0,0 +1,75 @@ +/** + * 3D Foundation Project + * Copyright 2023 Smithsonian Institution + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Job from "../app/Job"; + +import { IBlenderToolSettings } from "../tools/BlenderTool"; + +import Task, { ITaskParameters } from "../app/Task"; +import ToolTask from "../app/ToolTask"; + +//////////////////////////////////////////////////////////////////////////////// + +/** Parameters for [[ScreenshotTask]]. */ +export interface IScreenshotTaskParameters extends ITaskParameters +{ + /** Input mesh file name. */ + inputMeshFile: string; + /** Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup, see [[IToolConfiguration]]). */ + timeout?: number; +} + +/** + * Merges a multi-mesh model file into one .obj and texture + * + * Parameters: [[IScreenshotTaskParameters]]. + * Tool: [[BlenderTool]]. + */ +export default class ScreenshotTask extends ToolTask +{ + static readonly taskName = "Screenshot"; + + static readonly description = "Generates a screenshot of the provided geometry"; + + static readonly parameterSchema = { + type: "object", + properties: { + inputMeshFile: { type: "string", minLength: 1 }, + timeout: { type: "integer", default: 0 } + }, + required: [ + "inputMeshFile" + ], + additionalProperties: false + }; + + static readonly parameterValidator = + Task.jsonValidator.compile(ScreenshotTask.parameterSchema); + + constructor(params: IScreenshotTaskParameters, context: Job) + { + super(params, context); + + const settings: IBlenderToolSettings = { + inputMeshFile: params.inputMeshFile, + mode: "screenshot", + timeout: params.timeout + }; + + this.addTool("Blender", settings); + } +} \ No newline at end of file From ab16337c30a2eae1036a3d2ca47218de92fd18a6 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 8 Sep 2023 15:52:28 -0400 Subject: [PATCH 17/34] Update to axis alignment and centering --- server/scripts/MetashapeGenerateMesh.py | 62 ++++++++++++++++++------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index e750cbf..750fb64 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -74,7 +74,7 @@ def matrixFromAxisAngle(axis, angle): return Metashape.Matrix([[m00, m01, m02],[m10, m11, m12],[m20, m21, m22]]) -def model_to_origin(chunk): +def center_of_geometry_to_origin(chunk): model = chunk.model if not model: print("No model in chunk, script aborted") @@ -100,6 +100,29 @@ def model_to_origin(chunk): #chunk.region.center = avg chunk.transform.translation = chunk.transform.translation - T.mulp(avg) +def model_to_origin(chunk, camera_refs): + model = chunk.model + if not model: + print("No model in chunk, script aborted") + return 0 + T = chunk.transform.matrix + + local_centers = [] + for group in camera_refs.keys(): + camera_count = 0 + + pos_avg = [0,0,0] + for camera in camera_refs[group]: + if camera.center != None: + camera_count += 1 + for i, bi in enumerate(camera.center): pos_avg[i] += bi + pos_avg[0] /= camera_count + pos_avg[1] /= camera_count + pos_avg[2] /= camera_count + local_centers.append(pos_avg) + + chunk.transform.translation = chunk.transform.translation - T.mulp(Metashape.Vector(local_centers[0])) + #get args argv = sys.argv @@ -302,32 +325,39 @@ def model_to_origin(chunk): #chunk.remove(bad_cameras) - - # calculate center points - chunk_ctr = chunk.region.center - max_dist = 0 - max_idx = -1 - for i, pos in enumerate(local_centers): - dist = mag([chunk_ctr[0] - pos[0], chunk_ctr[1] - pos[1], chunk_ctr[2] - pos[2]]) - print(dist, max_idx) - if dist > max_dist: - max_dist = dist - max_idx = i + # calculate near and far ring centers + if len(local_centers) > 1: + chunk_ctr = chunk.region.center + near_center = local_centers[0] + far_center = local_centers[1] + dist_near = mag([chunk_ctr[0] - near_center[0], chunk_ctr[1] - near_center[1], chunk_ctr[2] - near_center[2]]) + dist_far = mag([chunk_ctr[0] - far_center[0], chunk_ctr[1] - far_center[1], chunk_ctr[2] - far_center[2]]) + if dist_near > dist_far: + near_center = local_centers[1] + far_center = local_centers[0] + print("Info: Using first two rings for axis alignment") + else: + near_center = chunk.region.center + far_center = local_centers[0] + print("Info: Using chunk center for axis alignment") # calculate rotation offset to up vector - curr_dir = Metashape.Vector(local_centers[max_idx]) - Metashape.Vector(chunk_ctr) + curr_dir = Metashape.Vector(far_center) - Metashape.Vector(near_center) curr_dir = curr_dir.normalized() angle = math.acos(sum( [curr_dir[i]*[0,0,1][i] for i in range(len([0,0,1]))] )) axis = Metashape.Vector.cross(curr_dir,[0,0,1]).normalized() rot_offset = matrixFromAxisAngle(axis, angle) - R = chunk.region.rot*(rot_offset*chunk.region.rot.inv()) #Bounding box rotation matrix - C = chunk.region.center #Bounding box center vector + R = chunk.region.rot*(rot_offset*chunk.region.rot.inv()) # Bounding box rotation matrix + C = chunk.region.center # Bounding box center vector T = Metashape.Matrix( [[R[0,0], R[0,1], R[0,2], C[0]], [R[1,0], R[1,1], R[1,2], C[1]], [R[2,0], R[2,1], R[2,2], C[2]], [0, 0, 0, 1]]) chunk.transform.matrix = Metashape.Matrix.Rotation(rot_offset)*Metashape.Matrix.Translation(C).inv() #T.inv() + camera_ctr = chunk.transform.matrix.mulp(Metashape.Vector(near_center)) + chunk.transform.matrix = chunk.transform.matrix*Metashape.Matrix.Translation(Metashape.Vector([camera_ctr[0], camera_ctr[1], 0])).inv() + # disable alignment-only cameras if args.align_input != None: for camera in alignCameras: @@ -484,7 +514,7 @@ def model_to_origin(chunk): if processGroups == True: # Move model to center - model_to_origin(chunk) + model_to_origin(chunk, camera_refs) chunk.exportModel\ ( From cf9138a5c1f41fd85c8a6edaba359d108041f4fb Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Thu, 14 Sep 2023 13:07:08 -0400 Subject: [PATCH 18/34] Bug fix when no cameras align. Changed group delimeter to dash. --- server/scripts/MetashapeGenerateMesh.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 750fb64..36b8752 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -197,7 +197,7 @@ def model_to_origin(chunk, camera_refs): if photo.group == None: name = str(photo.label) # Remove the sequence number from the base name (CaptureOne Pro formatting) - base_name_without_sequence_number = name[0:name.rfind("_")] + base_name_without_sequence_number = name[0:name.rfind("-")] #print(name + " --> " + base_name_without_sequence_number) # If this naming pattern doesn't have a camera group yet, create one @@ -305,9 +305,12 @@ def model_to_origin(chunk, camera_refs): if camera.center != None: camera_count += 1 for i, bi in enumerate(camera.center): pos_avg[i] += bi - pos_avg[0] /= camera_count - pos_avg[1] /= camera_count - pos_avg[2] /= camera_count + if camera_count > 0: + pos_avg[0] /= camera_count + pos_avg[1] /= camera_count + pos_avg[2] /= camera_count + else: + print("ERROR - no cameras aligned!!!") local_centers.append(pos_avg) loc_dev_arr = [] From de43db9477d63a9c201ae5be3a29242fc8101dd4 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 15 Sep 2023 07:45:00 -0400 Subject: [PATCH 19/34] Formatting fix --- server/scripts/MetashapeGenerateMesh.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 36b8752..5da724a 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -133,6 +133,7 @@ def model_to_origin(chunk, camera_refs): parser.add_argument("-c", "--cameras", required=True, help="Cameras filepath") parser.add_argument("-o", "--output", required=True, help="Output filename") parser.add_argument("-ai", "--align_input", required=False, help="Alignment input filepath") +parser.add_argument("-mi", "--mask_input", required=False, help="Mask input filepath") parser.add_argument("-al", "--align_limit", required=False, help="Alignment threshold (%)") parser.add_argument("-sb", required=False, help="Scalebar definition file") parser.add_argument("-optm", required=False, default="False", help="Optimize markers") @@ -306,9 +307,9 @@ def model_to_origin(chunk, camera_refs): camera_count += 1 for i, bi in enumerate(camera.center): pos_avg[i] += bi if camera_count > 0: - pos_avg[0] /= camera_count - pos_avg[1] /= camera_count - pos_avg[2] /= camera_count + pos_avg[0] /= camera_count + pos_avg[1] /= camera_count + pos_avg[2] /= camera_count else: print("ERROR - no cameras aligned!!!") local_centers.append(pos_avg) From b4b55a927f8b984bf9879bc0ff997955123897a5 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 15 Sep 2023 07:54:30 -0400 Subject: [PATCH 20/34] Formatting fix --- server/scripts/MetashapeGenerateMesh.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 5da724a..0450abf 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -306,12 +306,12 @@ def model_to_origin(chunk, camera_refs): if camera.center != None: camera_count += 1 for i, bi in enumerate(camera.center): pos_avg[i] += bi - if camera_count > 0: - pos_avg[0] /= camera_count - pos_avg[1] /= camera_count - pos_avg[2] /= camera_count - else: - print("ERROR - no cameras aligned!!!") + if camera_count > 0: + pos_avg[0] /= camera_count + pos_avg[1] /= camera_count + pos_avg[2] /= camera_count + else: + print("ERROR - no cameras aligned!!!") local_centers.append(pos_avg) loc_dev_arr = [] From 7dad6fbd289fd95ab02fd2bc512091385328691a Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 15 Sep 2023 12:29:15 -0400 Subject: [PATCH 21/34] Photogrammetry mask image set support --- server/recipes/photogrammetry.json | 30 +++++++++++-- server/recipes/si-zip-photogrammetry.json | 44 ++++++++++++++++++- server/scripts/MetashapeGenerateMesh.py | 51 ++++++++++++++--------- source/server/tasks/PhotogrammetryTask.ts | 4 ++ source/server/tools/MetashapeTool.ts | 5 +++ 5 files changed, 109 insertions(+), 25 deletions(-) diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index 5239339..a880db7 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -36,6 +36,11 @@ "minLength": 1, "format": "file" }, + "maskImageFolder": { + "type": "string", + "minLength": 1, + "format": "file" + }, "generatePointCloud": { "type": "boolean", "default": false @@ -117,7 +122,9 @@ "pre": { "outputFileBaseName": "$baseName($firstTrue(outputFileBaseName, sourceImageFolder))", "baseMeshName": "$firstTrue(outputFileBaseName, sourceImageFolder)", - "sourceFolderBaseName": "$baseName(sourceImageFolder)" + "sourceFolderBaseName": "$baseName(sourceImageFolder)", + "doAlign": "$exists(alignImageFolder)", + "doMask": "$exists(maskImageFolder)" }, "parameters": { "logToConsole": true, @@ -134,7 +141,8 @@ "path": "$firstTrue(pickupPath, $currentDir)", "files": { "sourceImageFolder": "sourceImageFolder", - "alignImageFolder": "alignImageFolder" + "alignImageFolder": "alignImageFolder", + "maskImageFolder": "maskImageFolder" } }, "success": "'unzip'", @@ -147,11 +155,12 @@ "inputFile1": "sourceImageFolder", "operation": "'unzip'" }, - "success": "alignImageFolder ? 'unzip-align' : 'make-convert-folder'", + "success": "'unzip-align'", "failure": "$failure" }, "unzip-align": { "task": "Zip", + "skip": "$not(doAlign)", "description": "Unzip alignment image folder", "pre": { "alignFolderBaseName": "$baseName(alignImageFolder)" @@ -160,6 +169,20 @@ "inputFile1": "alignImageFolder", "operation": "'unzip'" }, + "success": "'unzip-mask'", + "failure": "$failure" + }, + "unzip-mask": { + "task": "Zip", + "skip": "$not(doMask)", + "description": "Unzip mask image folder", + "pre": { + "maskFolderBaseName": "$baseName(maskImageFolder)" + }, + "parameters": { + "inputFile1": "maskImageFolder", + "operation": "'unzip'" + }, "success": "'make-convert-folder'", "failure": "$failure" }, @@ -203,6 +226,7 @@ "parameters": { "inputImageFolder": "convertToJpg ? sourceFolderConverted : sourceFolderBaseName", "alignImageFolder": "alignFolderBaseName", + "maskImageFolder": "maskFolderBaseName", "camerasFile": "camerasFile", "outputFile": "deliverables.meshFile", "scalebarFile": "scalebarCSV", diff --git a/server/recipes/si-zip-photogrammetry.json b/server/recipes/si-zip-photogrammetry.json index cbdcd5d..c42f406 100644 --- a/server/recipes/si-zip-photogrammetry.json +++ b/server/recipes/si-zip-photogrammetry.json @@ -24,6 +24,10 @@ "type": "string", "minLength": 1 }, + "maskFolderPath": { + "type": "string", + "minLength": 1 + }, "tool": { "type": "string", "enum": [ @@ -119,7 +123,9 @@ "pre": { "outputFileBaseName": "$baseName($firstTrue(outputFileBaseName, sourceFolderPath))", "baseMeshName": "$firstTrue(outputFileBaseName, sourceFolderPath)", - "sourceFolderBaseName": "$baseName(sourceFolderPath)" + "sourceFolderBaseName": "$baseName(sourceFolderPath)", + "doAlign": "$exists(alignFolderPath)", + "doMask": "$exists(maskFolderPath)" }, "parameters": { "logToConsole": true, @@ -153,11 +159,12 @@ "inputFile1": "deliverables.fileZip", "operation": "'unzip'" }, - "success": "alignFolderPath ? 'zip-align' : 'make-convert-folder'", + "success": "'zip-align'", "failure": "$failure" }, "zip-align": { "task": "Zip", + "skip": "$not(doAlign)", "description": "Zip alignment files direct from storage", "pre": { "alignFolderBaseName": "$baseName(alignFolderPath)", @@ -177,11 +184,43 @@ }, "unzip-align": { "task": "Zip", + "skip": "$not(doAlign)", "description": "Unzip alignment image folder", "parameters": { "inputFile1": "deliverables.alignFileZip", "operation": "'unzip'" }, + "success": "'zip-mask'", + "failure": "$failure" + }, + "zip-mask": { + "task": "Zip", + "skip": "$not(doMask)", + "description": "Zip mask files direct from storage", + "pre": { + "maskFolderBaseName": "$baseName(maskFolderPath)", + "deliverables": { + "maskFileZip": "maskFolderBaseName & '.zip'" + } + }, + "parameters": { + "inputFile1": "maskFolderPath", + "fileFilter": "filetype", + "recursive": false, + "outputFile": "deliverables.maskFileZip", + "operation": "'path-zip'" + }, + "success": "'unzip-mask'", + "failure": "$failure" + }, + "unzip-mask": { + "task": "Zip", + "skip": "$not(doMask)", + "description": "Unzip mask image folder", + "parameters": { + "inputFile1": "deliverables.maskFileZip", + "operation": "'unzip'" + }, "success": "'make-convert-folder'", "failure": "$failure" }, @@ -225,6 +264,7 @@ "parameters": { "inputImageFolder": "convertToJpg ? sourceFolderConverted : sourceFolderBaseName", "alignImageFolder": "alignFolderBaseName", + "maskImageFolder": "maskFolderBaseName", "camerasFile": "camerasFile", "outputFile": "deliverables.meshFile", "scalebarFile": "scalebarCSV", diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 0450abf..3819a5d 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -101,27 +101,28 @@ def center_of_geometry_to_origin(chunk): chunk.transform.translation = chunk.transform.translation - T.mulp(avg) def model_to_origin(chunk, camera_refs): - model = chunk.model - if not model: - print("No model in chunk, script aborted") - return 0 - T = chunk.transform.matrix + model = chunk.model + if not model: + print("No model in chunk, script aborted") + return 0 + T = chunk.transform.matrix - local_centers = [] - for group in camera_refs.keys(): - camera_count = 0 + local_centers = [] + for group in camera_refs.keys(): + camera_count = 0 - pos_avg = [0,0,0] - for camera in camera_refs[group]: - if camera.center != None: - camera_count += 1 - for i, bi in enumerate(camera.center): pos_avg[i] += bi - pos_avg[0] /= camera_count - pos_avg[1] /= camera_count - pos_avg[2] /= camera_count - local_centers.append(pos_avg) + pos_avg = [0,0,0] + for camera in camera_refs[group]: + if camera.center != None: + camera_count += 1 + for i, bi in enumerate(camera.center): pos_avg[i] += bi + if camera_count > 0: + pos_avg[0] /= camera_count + pos_avg[1] /= camera_count + pos_avg[2] /= camera_count + local_centers.append(pos_avg) - chunk.transform.translation = chunk.transform.translation - T.mulp(Metashape.Vector(local_centers[0])) + chunk.transform.translation = chunk.transform.translation - T.mulp(Metashape.Vector(local_centers[0])) #get args @@ -153,6 +154,7 @@ def model_to_origin(chunk, camera_refs): imagePath = args.input camerasPath = args.cameras processGroups = convert(args.ttg) +filterMask = args.mask_input != None genericPreselection = convert(args.gp) basename = os.path.basename(os.path.normpath(args.output)) basename = os.path.splitext(basename)[0]; @@ -163,6 +165,9 @@ def model_to_origin(chunk, camera_refs): for i, file in enumerate(f): imageFiles.append(os.path.join(r, file)) +# get image extension +imageExt = os.path.splitext(imageFiles[0])[1] + # set 'Scale Bar Accuracy' to 0.0001 chunk.scalebar_accuracy = 0.0001 # set 'Tie Point Accuracy' to 0.1 @@ -213,13 +218,19 @@ def model_to_origin(chunk, camera_refs): camera_refs[base_name_without_sequence_number].append(photo) +if args.mask_input != None: + try: + chunk.generateMasks(path=args.mask_input+"\\{filename}"+imageExt, masking_mode=Metashape.MaskingMode.MaskingModeFile) + except: + print("Warning: Missing mask images!") + chunk.matchPhotos\ ( downscale=1, generic_preselection=genericPreselection, reference_preselection=False, #reference_preselection_mode=Metashape.ReferencePreselectionSource, - filter_mask=False, + filter_mask=filterMask, mask_tiepoints=False, keypoint_limit=args.kp, tiepoint_limit=args.tp, @@ -310,9 +321,9 @@ def model_to_origin(chunk, camera_refs): pos_avg[0] /= camera_count pos_avg[1] /= camera_count pos_avg[2] /= camera_count + local_centers.append(pos_avg) else: print("ERROR - no cameras aligned!!!") - local_centers.append(pos_avg) loc_dev_arr = [] for camera in camera_refs[group]: diff --git a/source/server/tasks/PhotogrammetryTask.ts b/source/server/tasks/PhotogrammetryTask.ts index c9c45d9..54355af 100644 --- a/source/server/tasks/PhotogrammetryTask.ts +++ b/source/server/tasks/PhotogrammetryTask.ts @@ -34,6 +34,8 @@ export interface IPhotogrammetryTaskParameters extends ITaskParameters inputImageFolder: string; /** Alignment image folder. */ alignImageFolder?: string; + /** Mask image folder. */ + maskImageFolder?: string; /** Base name used for output files */ outputFile: string; /** Name used for saved camera position file */ @@ -83,6 +85,7 @@ export default class PhotogrammetryTask extends ToolTask properties: { inputImageFolder: { type: "string", minLength: 1 }, alignImageFolder: { type: "string", minLength: 1 }, + maskImageFolder: { type: "string", minLength: 1 }, outputFile: { type: "string", minLength: 1 }, camerasFile: { type: "string", minLength: 1 }, scalebarFile: { type: "string", minLength: 1 }, @@ -117,6 +120,7 @@ export default class PhotogrammetryTask extends ToolTask const toolOptions: IMetashapeToolSettings = { imageInputFolder: params.inputImageFolder, alignImageFolder: params.alignImageFolder, + maskImageFolder: params.maskImageFolder, outputFile: params.outputFile, camerasFile: params.camerasFile, scalebarFile: params.scalebarFile, diff --git a/source/server/tools/MetashapeTool.ts b/source/server/tools/MetashapeTool.ts index 113f1a0..58054b4 100644 --- a/source/server/tools/MetashapeTool.ts +++ b/source/server/tools/MetashapeTool.ts @@ -24,6 +24,7 @@ export interface IMetashapeToolSettings extends IToolSettings { imageInputFolder: string; alignImageFolder?: string; + maskImageFolder?: string; outputFile: string; mode: string; inputModelFile?: string; @@ -98,6 +99,10 @@ export default class MetashapeTool extends Tool Date: Fri, 15 Sep 2023 15:32:34 -0400 Subject: [PATCH 22/34] Made photogrammetry screenshot step off by default --- server/recipes/photogrammetry.json | 7 ++++++- server/recipes/si-zip-photogrammetry.json | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index a880db7..c7775dc 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -79,6 +79,10 @@ "type": "boolean", "default": false }, + "saveScreenshot": { + "type": "boolean", + "default": false + }, "genericPreselection": { "type": "boolean", "default": true @@ -110,7 +114,7 @@ ], "advanced": [ "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit", "turntableGroups", "genericPreselection", "depthMaxNeighbors", - "meshQuality", "customFaceCount" + "meshQuality", "customFaceCount", "saveScreenshot" ], "additionalProperties": false }, @@ -319,6 +323,7 @@ }, "screenshot": { "task": "Screenshot", + "skip": "$not(saveScreenshot)", "description": "Generate screenshot of result geometry.", "parameters": { "inputMeshFile": "deliverables.finalMeshFile", diff --git a/server/recipes/si-zip-photogrammetry.json b/server/recipes/si-zip-photogrammetry.json index c42f406..17b8550 100644 --- a/server/recipes/si-zip-photogrammetry.json +++ b/server/recipes/si-zip-photogrammetry.json @@ -80,6 +80,10 @@ "type": "boolean", "default": false }, + "saveScreenshot": { + "type": "boolean", + "default": false + }, "genericPreselection": { "type": "boolean", "default": true @@ -111,7 +115,7 @@ ], "advanced": [ "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit", "turntableGroups", "genericPreselection", "depthMaxNeighbors", - "meshQuality", "customFaceCount" + "meshQuality", "customFaceCount", "saveScreenshot" ], "additionalProperties": false }, @@ -357,6 +361,7 @@ }, "screenshot": { "task": "Screenshot", + "skip": "$not(saveScreenshot)", "description": "Generate screenshot of result geometry.", "parameters": { "inputMeshFile": "deliverables.finalMeshFile", From 57607f258269ead16f475083abe394a7e876e1dc Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Thu, 5 Oct 2023 09:27:30 -0400 Subject: [PATCH 23/34] Mesh alignment improvements and generative masking support --- server/scripts/MetashapeGenerateMesh.py | 106 ++++++++++++++++++------ 1 file changed, 79 insertions(+), 27 deletions(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 3819a5d..7265030 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -100,7 +100,7 @@ def center_of_geometry_to_origin(chunk): #chunk.region.center = avg chunk.transform.translation = chunk.transform.translation - T.mulp(avg) -def model_to_origin(chunk, camera_refs): +def model_to_origin(chunk, camera_refs, name): model = chunk.model if not model: print("No model in chunk, script aborted") @@ -109,21 +109,30 @@ def model_to_origin(chunk, camera_refs): local_centers = [] for group in camera_refs.keys(): - camera_count = 0 - - pos_avg = [0,0,0] - for camera in camera_refs[group]: - if camera.center != None: - camera_count += 1 - for i, bi in enumerate(camera.center): pos_avg[i] += bi - if camera_count > 0: - pos_avg[0] /= camera_count - pos_avg[1] /= camera_count - pos_avg[2] /= camera_count - local_centers.append(pos_avg) + if group == name: + camera_count = 0 + + pos_avg = [0,0,0] + for camera in camera_refs[group]: + if camera.center != None: + camera_count += 1 + for i, bi in enumerate(camera.center): pos_avg[i] += bi + if camera_count > 0: + pos_avg[0] /= camera_count + pos_avg[1] /= camera_count + pos_avg[2] /= camera_count + local_centers.append(pos_avg) chunk.transform.translation = chunk.transform.translation - T.mulp(Metashape.Vector(local_centers[0])) +def get_background_masks(mask_path): + masks = [] + for r, d, f in walk(mask_path): + for file in f: + filename = os.path.splitext(file)[0] + delimeter = filename[filename.rfind("-"):] + masks.append({"key":delimeter,"name":file}) + return masks #get args argv = sys.argv @@ -183,7 +192,7 @@ def model_to_origin(chunk, camera_refs): alignPath = args.align_input camera_group = chunk.addCameraGroup() camera_group.label = "alignment_images" - camera_groups["alignment_images"] = camera_group + #camera_groups["alignment_images"] = camera_group for r, d, f in walk(alignPath): for i, file in enumerate(f): alignImages.append(os.path.join(r, file)) @@ -219,10 +228,29 @@ def model_to_origin(chunk, camera_refs): camera_refs[base_name_without_sequence_number].append(photo) if args.mask_input != None: + mask_count = len([name for name in os.listdir(args.mask_input+"\\") if os.path.isfile(args.mask_input+"\\"+name)]) + print("Number of masks", mask_count) try: - chunk.generateMasks(path=args.mask_input+"\\{filename}"+imageExt, masking_mode=Metashape.MaskingMode.MaskingModeFile) + if mask_count > 10: # assumes per-image mask + chunk.generateMasks(path=args.mask_input+"\\{filename}"+imageExt, masking_mode=Metashape.MaskingMode.MaskingModeFile) + else: # otherwise generate device specific masks + masks = get_background_masks(args.mask_input) + for mask in masks: + key = mask["key"] + camera_filter = list(filter(lambda x: key in x.label, chunk.cameras)) + chunk.generateMasks \ + ( + path=args.mask_input+"\\"+mask["name"], + masking_mode=Metashape.MaskingModeBackground, + mask_operation=Metashape.MaskOperationReplacement, + tolerance=30, + cameras=camera_filter, + mask_defocus=False, + fix_coverage=True, + ) + except: - print("Warning: Missing mask images!") + print("Warning: Mask generation error!") chunk.matchPhotos\ ( @@ -321,7 +349,7 @@ def model_to_origin(chunk, camera_refs): pos_avg[0] /= camera_count pos_avg[1] /= camera_count pos_avg[2] /= camera_count - local_centers.append(pos_avg) + local_centers.append({"name": group, "ctr": pos_avg}) else: print("ERROR - no cameras aligned!!!") @@ -343,17 +371,41 @@ def model_to_origin(chunk, camera_refs): # calculate near and far ring centers if len(local_centers) > 1: chunk_ctr = chunk.region.center - near_center = local_centers[0] - far_center = local_centers[1] - dist_near = mag([chunk_ctr[0] - near_center[0], chunk_ctr[1] - near_center[1], chunk_ctr[2] - near_center[2]]) - dist_far = mag([chunk_ctr[0] - far_center[0], chunk_ctr[1] - far_center[1], chunk_ctr[2] - far_center[2]]) - if dist_near > dist_far: - near_center = local_centers[1] - far_center = local_centers[0] - print("Info: Using first two rings for axis alignment") + ring_sort_arr = [] + filtered_ctrs = list(filter(lambda x: "-s01" in x["name"], local_centers)) + for center in filtered_ctrs: + aligned = [camera for camera in camera_refs[center["name"]] if camera.transform and camera.type==Metashape.Camera.Type.Regular] + success_ratio = len(aligned) / len(camera_refs[center["name"]]) * 100 + if success_ratio > 75: + dist = mag([chunk_ctr[0] - center["ctr"][0], chunk_ctr[1] - center["ctr"][1], chunk_ctr[2] - center["ctr"][2]]) + ring_sort_arr.append({"name": center["name"], "distance": dist}) + + # find far idx + direction = 0 + for idx in range(len(ring_sort_arr)-1): + if ring_sort_arr[idx+1]["distance"] > ring_sort_arr[idx]["distance"]: + direction += 1 + else: + direction -= 1 + + if direction > 0: + far_idx = len(ring_sort_arr)-1 + elif direction < 0: + far_idx = 0 + else: + far_idx = 0 + print("Warning: Could not find approriate capture ring for alignment. Using first encountered.") + + far_center = next(x for x in local_centers if x["name"] == ring_sort_arr[far_idx]["name"])["ctr"] + if far_idx > 0: + near_center = next(x for x in local_centers if x["name"] == ring_sort_arr[far_idx-1]["name"])["ctr"] + else: + near_center = next(x for x in local_centers if x["name"] == ring_sort_arr[far_idx+1]["name"])["ctr"] + align_ring_name = ring_sort_arr[far_idx]["name"] + print("Info: Using " + align_ring_name + " for axis alignment") else: near_center = chunk.region.center - far_center = local_centers[0] + far_center = local_centers[0]["ctr"] print("Info: Using chunk center for axis alignment") # calculate rotation offset to up vector @@ -529,7 +581,7 @@ def model_to_origin(chunk, camera_refs): if processGroups == True: # Move model to center - model_to_origin(chunk, camera_refs) + model_to_origin(chunk, camera_refs, align_ring_name) chunk.exportModel\ ( From 297b3e0dd6ec9ff335f4a358457b4d6b401742f1 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Thu, 5 Oct 2023 15:25:03 -0400 Subject: [PATCH 24/34] Added support for clip-to alignment generation, exposed depthMapQuality param, made selection ray smaller, updated postfix for SI final files. --- server/recipes/photogrammetry.json | 52 +++++++++++++++++- server/recipes/si-zip-photogrammetry.json | 56 ++++++++++++++++++-- server/scripts/MetashapeGenerateMesh.py | 3 +- source/server/tasks/BatchConvertImageTask.ts | 8 ++- source/server/tasks/CleanupMeshTask.ts | 2 +- source/server/tasks/PhotogrammetryTask.ts | 4 ++ source/server/tools/ImageMagickTool.ts | 8 +++ source/server/tools/MetashapeTool.ts | 6 +++ 8 files changed, 131 insertions(+), 8 deletions(-) diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index c7775dc..527c73f 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -107,6 +107,24 @@ "customFaceCount": { "type": "number", "default": 3000000 + }, + "depthMapQuality": { + "type": "string", + "enum": [ + "Low", + "Medium", + "High", + "Highest" + ], + "default": "Highest" + }, + "levelClipAlign": { + "type": "boolean", + "default": false + }, + "clipLevel": { + "type": "number", + "default": 150 } }, "required": [ @@ -114,7 +132,7 @@ ], "advanced": [ "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit", "turntableGroups", "genericPreselection", "depthMaxNeighbors", - "meshQuality", "customFaceCount", "saveScreenshot" + "meshQuality", "customFaceCount", "depthMapQuality", "saveScreenshot", "levelClipAlign", "clipLevel" ], "additionalProperties": false }, @@ -173,6 +191,37 @@ "inputFile1": "alignImageFolder", "operation": "'unzip'" }, + "success": "'make-level-clip-folder'", + "failure": "$failure" + }, + "make-level-clip-folder": { + "task": "FileOperation", + "skip": "$not(levelClipAlign)", + "description": "Create folder for converted images", + "pre": { + "sourceFolderConverted": "'clipped_alignment'" + }, + "parameters": { + "operation": "'CreateFolder'", + "name": "sourceFolderConverted" + }, + "success": "'level-align'", + "failure": "$failure" + }, + "level-align": { + "task": "BatchConvertImage", + "skip": "$not(levelClipAlign)", + "description": "Generate level-clipped alignment images", + "parameters": { + "inputImageFolder": "sourceFolderBaseName", + "outputImageFolder": "sourceFolderConverted", + "filetype": "jpg", + "quality": "85", + "level": "clipLevel" + }, + "post": { + "alignFolderBaseName": "$baseName(sourceFolderConverted)" + }, "success": "'unzip-mask'", "failure": "$failure" }, @@ -244,6 +293,7 @@ "depthMaxNeighbors": "depthMaxNeighbors", "meshQuality": "meshQuality", "customFaceCount": "customFaceCount", + "depthMapQuality": "depthMapQuality", "tool": "tool", "timeout": 86400 }, diff --git a/server/recipes/si-zip-photogrammetry.json b/server/recipes/si-zip-photogrammetry.json index 17b8550..7c7bb2b 100644 --- a/server/recipes/si-zip-photogrammetry.json +++ b/server/recipes/si-zip-photogrammetry.json @@ -108,6 +108,24 @@ "customFaceCount": { "type": "number", "default": 3000000 + }, + "depthMapQuality": { + "type": "string", + "enum": [ + "Low", + "Medium", + "High", + "Highest" + ], + "default": "Highest" + }, + "levelClipAlign": { + "type": "boolean", + "default": false + }, + "clipLevel": { + "type": "number", + "default": 150 } }, "required": [ @@ -115,7 +133,7 @@ ], "advanced": [ "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit", "turntableGroups", "genericPreselection", "depthMaxNeighbors", - "meshQuality", "customFaceCount", "saveScreenshot" + "meshQuality", "customFaceCount", "depthMapQuality", "saveScreenshot", "levelClipAlign", "clipLevel" ], "additionalProperties": false }, @@ -194,6 +212,37 @@ "inputFile1": "deliverables.alignFileZip", "operation": "'unzip'" }, + "success": "'make-level-clip-folder'", + "failure": "$failure" + }, + "make-level-clip-folder": { + "task": "FileOperation", + "skip": "$not(levelClipAlign)", + "description": "Create folder for converted images", + "pre": { + "sourceFolderConverted": "'clipped_alignment'" + }, + "parameters": { + "operation": "'CreateFolder'", + "name": "sourceFolderConverted" + }, + "success": "'level-align'", + "failure": "$failure" + }, + "level-align": { + "task": "BatchConvertImage", + "skip": "$not(levelClipAlign)", + "description": "Generate level-clipped alignment images", + "parameters": { + "inputImageFolder": "sourceFolderBaseName", + "outputImageFolder": "sourceFolderConverted", + "filetype": "jpg", + "quality": "85", + "level": "clipLevel" + }, + "post": { + "alignFolderBaseName": "$baseName(sourceFolderConverted)" + }, "success": "'zip-mask'", "failure": "$failure" }, @@ -282,6 +331,7 @@ "depthMaxNeighbors": "depthMaxNeighbors", "meshQuality": "meshQuality", "customFaceCount": "customFaceCount", + "depthMapQuality": "depthMapQuality", "tool": "tool", "timeout": 86400 }, @@ -343,8 +393,8 @@ "description": "Create and map texture from model and image set.", "pre": { "deliverables": { - "finalMeshFile": "baseMeshName & '-' & $lowercase(tool) & '-final.obj'", - "textureFile": "baseMeshName & '-texture-final' & '.png'" + "finalMeshFile": "baseMeshName & '-' & $lowercase(tool) & '-raw_clean.obj'", + "textureFile": "baseMeshName & '-raw_clean' & '.png'" } }, "parameters": { diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 7265030..4a17913 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -155,6 +155,7 @@ def get_background_masks(mask_path): parser.add_argument("-ttg", required=False, default="False", help="Process turntable groups") parser.add_argument("-mq", required=False, default=2, help="Model resolution quality") parser.add_argument("-cfc", required=False, default=3000000, help="Custom model face count") +parser.add_argument("-dmq", required=False, default=0, help="Depth map quality") args = parser.parse_args() doc = Metashape.app.document @@ -514,7 +515,7 @@ def get_background_masks(mask_path): # Ultrahigh setting loads the image data at full resolution, High downsamples x2, medium downsamples x4, low x8 chunk.buildDepthMaps\ ( - downscale=1, + downscale=pow(2,int(args.dmq)), filter_mode=Metashape.MildFiltering, reuse_depth=False, max_neighbors=args.dmn, diff --git a/source/server/tasks/BatchConvertImageTask.ts b/source/server/tasks/BatchConvertImageTask.ts index 8bce7a1..0e72458 100644 --- a/source/server/tasks/BatchConvertImageTask.ts +++ b/source/server/tasks/BatchConvertImageTask.ts @@ -35,6 +35,8 @@ export interface IBatchConvertImageTaskParameters extends ITaskParameters quality?: number; /** Filetype to convert images to. */ filetype?: string; + /** Clips image to black (value < 128) or white (value > 128) */ + level?: number; } /** @@ -55,7 +57,8 @@ export default class BatchConvertImageTask extends ToolTask inputImageFolder: { type: "string", minLength: 1 }, outputImageFolder: { type: "string", minLength: 1 }, quality: { type: "integer", minimum: 0, maximum: 100, default: 70 }, - filetype: { type: "string", default: "jpg" } + filetype: { type: "string", default: "jpg" }, + level: { type: "integer", minimum: 0, maximum: 255} }, required: [ "inputImageFolder", @@ -76,7 +79,8 @@ export default class BatchConvertImageTask extends ToolTask inputImageFolder: params.inputImageFolder, outputImageFolder: params.outputImageFolder, quality: params.quality, - batchConvertType: params.filetype + batchConvertType: params.filetype, + level: params.level }; this.addTool("ImageMagick", settings); diff --git a/source/server/tasks/CleanupMeshTask.ts b/source/server/tasks/CleanupMeshTask.ts index bb54b1b..8a2e7dd 100644 --- a/source/server/tasks/CleanupMeshTask.ts +++ b/source/server/tasks/CleanupMeshTask.ts @@ -130,7 +130,7 @@ export default class CleanupMeshTask extends ToolTask { name: "ConditionalFaceSelect", params: { - "condSelect": 'abs(x0)<'+params.sceneSize[0]*0.02+' && abs(y0)<'+params.sceneSize[1]*0.02 //+' && abs(z0)<'+params.sceneSize[2]*0.1 + "condSelect": 'abs(x0)<'+params.sceneSize[0]*0.015+' && abs(y0)<'+params.sceneSize[1]*0.015 //+' && abs(z0)<'+params.sceneSize[2]*0.1 } }, { diff --git a/source/server/tasks/PhotogrammetryTask.ts b/source/server/tasks/PhotogrammetryTask.ts index 54355af..2301d69 100644 --- a/source/server/tasks/PhotogrammetryTask.ts +++ b/source/server/tasks/PhotogrammetryTask.ts @@ -62,6 +62,8 @@ export interface IPhotogrammetryTaskParameters extends ITaskParameters meshQuality?: string; /** If meshQuality is custom, this defines the goal face count */ customFaceCount?: number; + /** Preset for depth map quality ("Low", "Medium", "High", "Highest") */ + depthMapQuality?: string; /** Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup, see [[IToolConfiguration]]). */ timeout?: number; /** Tool to use for photogrammetry ("Metashape" or "RealityCapture" or "Meshroom", default: "Metashape"). */ @@ -99,6 +101,7 @@ export default class PhotogrammetryTask extends ToolTask genericPreselection: { type: "boolean", default: true}, meshQuality: { type: "string", enum: [ "Low", "Medium", "High", "Highest", "Custom" ], default: "High"}, customFaceCount: { type: "integer", default: 3000000}, + depthMapQuality: { type: "string", enum: [ "Low", "Medium", "High", "Highest" ], default: "Highest"}, timeout: { type: "integer", default: 0 }, tool: { type: "string", enum: [ "Metashape", "RealityCapture", "Meshroom" ], default: "Metashape" } }, @@ -134,6 +137,7 @@ export default class PhotogrammetryTask extends ToolTask genericPreselection: params.genericPreselection, meshQuality: params.meshQuality, customFaceCount: params.customFaceCount, + depthMapQuality: params.depthMapQuality, mode: "full", timeout: params.timeout }; diff --git a/source/server/tools/ImageMagickTool.ts b/source/server/tools/ImageMagickTool.ts index 4ca22ee..7641753 100644 --- a/source/server/tools/ImageMagickTool.ts +++ b/source/server/tools/ImageMagickTool.ts @@ -37,6 +37,8 @@ export interface IImageMagickToolSettings extends IToolSettings normalize?: boolean; /** Gamma correction of the final image (1.0 = unchanged). */ gamma?: number; + /** Clips image to black (value < 128) or white (value > 128) */ + level?: number; /** Resizes the image. values <= 2 represent relative scale, otherwise absolute size in pixels. */ resize?: number; /** If true, expects three input images which are copied to the red, green, and blue channels. */ @@ -86,6 +88,12 @@ export default class ImageMagickTool extends Tool e == settings.depthMapQuality); + operation += ` -dmq ${qualityIdx} `; + } } else if(settings.mode === "texture") { const inputModelPath = instance.getFilePath(settings.inputModelFile); From 0134422dc259d7a4e6c6c98f5e7aa40e509c473e Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 6 Oct 2023 11:26:26 -0400 Subject: [PATCH 25/34] maskMode param added to control file vs background masking --- server/recipes/photogrammetry.json | 11 ++++++++++- server/recipes/si-zip-photogrammetry.json | 11 ++++++++++- server/scripts/MetashapeGenerateMesh.py | 13 ++++++++++--- source/server/tasks/PhotogrammetryTask.ts | 4 ++++ source/server/tools/MetashapeTool.ts | 4 ++++ 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index 527c73f..5784050 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -125,6 +125,14 @@ "clipLevel": { "type": "number", "default": 150 + }, + "maskMode": { + "type": "string", + "enum": [ + "File", + "Background" + ], + "default": "File" } }, "required": [ @@ -132,7 +140,7 @@ ], "advanced": [ "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit", "turntableGroups", "genericPreselection", "depthMaxNeighbors", - "meshQuality", "customFaceCount", "depthMapQuality", "saveScreenshot", "levelClipAlign", "clipLevel" + "meshQuality", "customFaceCount", "depthMapQuality", "saveScreenshot", "levelClipAlign", "clipLevel", "maskMode" ], "additionalProperties": false }, @@ -294,6 +302,7 @@ "meshQuality": "meshQuality", "customFaceCount": "customFaceCount", "depthMapQuality": "depthMapQuality", + "maskMode": "maskMode", "tool": "tool", "timeout": 86400 }, diff --git a/server/recipes/si-zip-photogrammetry.json b/server/recipes/si-zip-photogrammetry.json index 7c7bb2b..4b5ed3f 100644 --- a/server/recipes/si-zip-photogrammetry.json +++ b/server/recipes/si-zip-photogrammetry.json @@ -126,6 +126,14 @@ "clipLevel": { "type": "number", "default": 150 + }, + "maskMode": { + "type": "string", + "enum": [ + "File", + "Background" + ], + "default": "File" } }, "required": [ @@ -133,7 +141,7 @@ ], "advanced": [ "alignmentLimit", "convertToJpg", "tiepointLimit", "keypointLimit", "turntableGroups", "genericPreselection", "depthMaxNeighbors", - "meshQuality", "customFaceCount", "depthMapQuality", "saveScreenshot", "levelClipAlign", "clipLevel" + "meshQuality", "customFaceCount", "depthMapQuality", "saveScreenshot", "levelClipAlign", "clipLevel", "maskMode" ], "additionalProperties": false }, @@ -332,6 +340,7 @@ "meshQuality": "meshQuality", "customFaceCount": "customFaceCount", "depthMapQuality": "depthMapQuality", + "maskMode": "maskMode", "tool": "tool", "timeout": 86400 }, diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 4a17913..baf9663 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -144,6 +144,7 @@ def get_background_masks(mask_path): parser.add_argument("-o", "--output", required=True, help="Output filename") parser.add_argument("-ai", "--align_input", required=False, help="Alignment input filepath") parser.add_argument("-mi", "--mask_input", required=False, help="Mask input filepath") +parser.add_argument("-mm", "--mask_mode", required=False, help="Masking mode") parser.add_argument("-al", "--align_limit", required=False, help="Alignment threshold (%)") parser.add_argument("-sb", required=False, help="Scalebar definition file") parser.add_argument("-optm", required=False, default="False", help="Optimize markers") @@ -228,12 +229,18 @@ def get_background_masks(mask_path): camera_refs[base_name_without_sequence_number].append(photo) +# Add/generate masks if args.mask_input != None: mask_count = len([name for name in os.listdir(args.mask_input+"\\") if os.path.isfile(args.mask_input+"\\"+name)]) + # determine mask mode + if args.mask_mode == "Background": + mask_mode = Metashape.MaskingMode.MaskingModeBackground + else: + mask_mode = Metashape.MaskingMode.MaskingModeFile print("Number of masks", mask_count) try: if mask_count > 10: # assumes per-image mask - chunk.generateMasks(path=args.mask_input+"\\{filename}"+imageExt, masking_mode=Metashape.MaskingMode.MaskingModeFile) + chunk.generateMasks(path=args.mask_input+"\\{filename}"+imageExt, masking_mode=mask_mode) else: # otherwise generate device specific masks masks = get_background_masks(args.mask_input) for mask in masks: @@ -242,7 +249,7 @@ def get_background_masks(mask_path): chunk.generateMasks \ ( path=args.mask_input+"\\"+mask["name"], - masking_mode=Metashape.MaskingModeBackground, + masking_mode=mask_mode, mask_operation=Metashape.MaskOperationReplacement, tolerance=30, cameras=camera_filter, @@ -443,7 +450,7 @@ def get_background_masks(mask_path): if success_ratio < int(args.align_limit): sys.exit("Error: Image alignment does not meet minimum threshold") -#sys.exit(1) +sys.exit(1) # optimize cameras chunk.optimizeCameras( adaptive_fitting=True ) diff --git a/source/server/tasks/PhotogrammetryTask.ts b/source/server/tasks/PhotogrammetryTask.ts index 2301d69..75c0a56 100644 --- a/source/server/tasks/PhotogrammetryTask.ts +++ b/source/server/tasks/PhotogrammetryTask.ts @@ -64,6 +64,8 @@ export interface IPhotogrammetryTaskParameters extends ITaskParameters customFaceCount?: number; /** Preset for depth map quality ("Low", "Medium", "High", "Highest") */ depthMapQuality?: string; + /** Desired masking operation */ + maskMode?: "File" | "Background"; /** Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup, see [[IToolConfiguration]]). */ timeout?: number; /** Tool to use for photogrammetry ("Metashape" or "RealityCapture" or "Meshroom", default: "Metashape"). */ @@ -102,6 +104,7 @@ export default class PhotogrammetryTask extends ToolTask meshQuality: { type: "string", enum: [ "Low", "Medium", "High", "Highest", "Custom" ], default: "High"}, customFaceCount: { type: "integer", default: 3000000}, depthMapQuality: { type: "string", enum: [ "Low", "Medium", "High", "Highest" ], default: "Highest"}, + maskMode: { type: "string", enum: [ "File", "Background" ], default: "File"}, timeout: { type: "integer", default: 0 }, tool: { type: "string", enum: [ "Metashape", "RealityCapture", "Meshroom" ], default: "Metashape" } }, @@ -138,6 +141,7 @@ export default class PhotogrammetryTask extends ToolTask meshQuality: params.meshQuality, customFaceCount: params.customFaceCount, depthMapQuality: params.depthMapQuality, + maskMode: params.maskMode, mode: "full", timeout: params.timeout }; diff --git a/source/server/tools/MetashapeTool.ts b/source/server/tools/MetashapeTool.ts index 00e65e4..0ca80e8 100644 --- a/source/server/tools/MetashapeTool.ts +++ b/source/server/tools/MetashapeTool.ts @@ -41,6 +41,7 @@ export interface IMetashapeToolSettings extends IToolSettings meshQuality?: string; depthMapQuality?: string; customFaceCount?: number; + maskMode?: string; } export type MetashapeInstance = ToolInstance; @@ -105,6 +106,9 @@ export default class MetashapeTool extends Tool Date: Fri, 6 Oct 2023 12:41:52 -0400 Subject: [PATCH 26/34] Comment out early exit --- server/scripts/MetashapeGenerateMesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index baf9663..27fa316 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -450,7 +450,7 @@ def get_background_masks(mask_path): if success_ratio < int(args.align_limit): sys.exit("Error: Image alignment does not meet minimum threshold") -sys.exit(1) +#sys.exit(1) # optimize cameras chunk.optimizeCameras( adaptive_fitting=True ) From c77ab612282da1228845f1238536e1204acdbaf7 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Tue, 10 Oct 2023 15:18:24 -0400 Subject: [PATCH 27/34] Lowered selection ray size to 1cm std --- source/server/tasks/CleanupMeshTask.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/server/tasks/CleanupMeshTask.ts b/source/server/tasks/CleanupMeshTask.ts index 8a2e7dd..a6f15b9 100644 --- a/source/server/tasks/CleanupMeshTask.ts +++ b/source/server/tasks/CleanupMeshTask.ts @@ -130,7 +130,7 @@ export default class CleanupMeshTask extends ToolTask { name: "ConditionalFaceSelect", params: { - "condSelect": 'abs(x0)<'+params.sceneSize[0]*0.015+' && abs(y0)<'+params.sceneSize[1]*0.015 //+' && abs(z0)<'+params.sceneSize[2]*0.1 + "condSelect": 'abs(x0)<'+0.005+' && abs(y0)<'+0.005 //+' && abs(z0)<'+params.sceneSize[2]*0.1 } }, { From 3e55e9ba2e76bd7e44bf4de63ed6088e8b4ccf8b Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Wed, 11 Oct 2023 13:16:48 -0400 Subject: [PATCH 28/34] Adding alignment debug statements --- server/scripts/MetashapeGenerateMesh.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 27fa316..4ed531f 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -122,7 +122,8 @@ def model_to_origin(chunk, camera_refs, name): pos_avg[1] /= camera_count pos_avg[2] /= camera_count local_centers.append(pos_avg) - + print("MESH ALIGN: ", T.mulp(Metashape.Vector(local_centers[0]))) + print(chunk.transform.translation, Metashape.Vector(local_centers[0])) chunk.transform.translation = chunk.transform.translation - T.mulp(Metashape.Vector(local_centers[0])) def get_background_masks(mask_path): From 90491dde32498e2769e21ad68b4533ccc20d8c0f Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Tue, 17 Oct 2023 16:23:40 -0400 Subject: [PATCH 29/34] Zipping project files for more complete delivery package --- server/recipes/si-zip-photogrammetry.json | 79 ++++++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/server/recipes/si-zip-photogrammetry.json b/server/recipes/si-zip-photogrammetry.json index 4b5ed3f..4651688 100644 --- a/server/recipes/si-zip-photogrammetry.json +++ b/server/recipes/si-zip-photogrammetry.json @@ -134,6 +134,18 @@ "Background" ], "default": "File" + }, + "transportMethod": { + "type": "string", + "enum": [ + "none", + "local" + ], + "default": "none" + }, + "deliveryPath": { + "type": "string", + "minLength": 1 } }, "required": [ @@ -319,7 +331,8 @@ "camerasFile": "baseMeshName & '-cameras.xml'", "deliverables": { "meshFile": "baseMeshName & '-' & $lowercase(tool) & '.obj'", - "textureFile": "baseMeshName & '-texture-' & '.png'" + "textureFile": "baseMeshName & '-' & $lowercase(tool) & '.tif'", + "mtlFile": "baseMeshName & '-' & $lowercase(tool) & '.mtl'" } }, "parameters": { @@ -383,7 +396,8 @@ "description": "Cleanup common issues with mesh.", "pre": { "deliverables": { - "cleanedMeshFile": "baseMeshName & '-cleaned' & '.obj'" + "cleanedMeshFile": "baseMeshName & '-cleaned' & '.obj'", + "cleanedMtlFile": "baseMeshName & '-cleaned' & '.obj.mtl'" } }, "parameters": { @@ -403,7 +417,8 @@ "pre": { "deliverables": { "finalMeshFile": "baseMeshName & '-' & $lowercase(tool) & '-raw_clean.obj'", - "textureFile": "baseMeshName & '-raw_clean' & '.png'" + "finalTextureFile": "baseMeshName & '-' & $lowercase(tool) & '-raw_clean.tif'", + "finalMtlFile": "baseMeshName & '-' & $lowercase(tool) & '-raw_clean.mtl'" } }, "parameters": { @@ -426,6 +441,64 @@ "inputMeshFile": "deliverables.finalMeshFile", "timeout": 1200 }, + "success": "'zip-proj-align'", + "failure": "$failure" + }, + "zip-proj-align": { + "task": "Zip", + "description": "Zip photogrammetry project files for align stage", + "pre": { + "deliverables": { + "alignProjZip": "baseMeshName & '-' & $lowercase(tool) & '-align.files.zip'", + "alignProjFile": "baseMeshName & '-' & $lowercase(tool) & '-align.psx'" + } + }, + "parameters": { + "inputFile1": "$jobDir & '\\\\' & baseMeshName & '-' & $lowercase(tool) & '-align.files'", + "fileFilter": "filetype", + "recursive": true, + "outputFile": "deliverables.alignProjZip", + "operation": "'path-zip'" + }, + "success": "'zip-proj-mesh'", + "failure": "$failure" + }, + "zip-proj-mesh": { + "task": "Zip", + "description": "Zip photogrammetry project files for mesh stage", + "pre": { + "deliverables": { + "meshProjZip": "baseMeshName & '-' & $lowercase(tool) & '-mesh.files.zip'", + "meshProjFile": "baseMeshName & '-' & $lowercase(tool) & '-mesh.psx'", + "meshProjReport": "baseMeshName & '-' & $lowercase(tool) & '-report.pdf'" + } + }, + "parameters": { + "inputFile1": "$jobDir & '\\\\' & baseMeshName & '-' & $lowercase(tool) & '-mesh.files'", + "fileFilter": "filetype", + "recursive": true, + "outputFile": "deliverables.meshProjZip", + "operation": "'path-zip'" + }, + "success": "'zip-proj-final'", + "failure": "$failure" + }, + "zip-proj-final": { + "task": "Zip", + "description": "Zip photogrammetry project files for final stage", + "pre": { + "deliverables": { + "finalProjZip": "baseMeshName & '-' & $lowercase(tool) & '-raw_clean.files.zip'", + "finalProjFile": "baseMeshName & '-' & $lowercase(tool) & '-raw_clean.psx'" + } + }, + "parameters": { + "inputFile1": "$jobDir & '\\\\' & baseMeshName & '-' & $lowercase(tool) & '-raw_clean.files'", + "fileFilter": "filetype", + "recursive": true, + "outputFile": "deliverables.finalProjZip", + "operation": "'path-zip'" + }, "success": "'delivery'", "failure": "$failure" }, From 7ad29e557aecb29963cc24f369b93dceeba118b6 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 3 Nov 2023 10:51:25 -0400 Subject: [PATCH 30/34] Delete alignment only cameras before exporting --- server/scripts/MetashapeGenerateMesh.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 4ed531f..13c8224 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -195,7 +195,7 @@ def get_background_masks(mask_path): alignPath = args.align_input camera_group = chunk.addCameraGroup() camera_group.label = "alignment_images" - #camera_groups["alignment_images"] = camera_group + camera_groups["alignment_images"] = camera_group for r, d, f in walk(alignPath): for i, file in enumerate(f): alignImages.append(os.path.join(r, file)) @@ -614,6 +614,12 @@ def get_background_masks(mask_path): format=Metashape.ModelFormatOBJ, ) +# remove alignment-only cameras +if args.align_input != None: + for camera in chunk.cameras: + if camera.group != None and camera.group.label == "alignment_images": + chunk.remove(camera) + chunk.exportCameras(camerasPath) chunk.exportReport(imagePath+"\\..\\"+basename+"-report.pdf") From 1df9d9dba1619d9028a6a48f11b68f23e6ba6d5a Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Wed, 29 Nov 2023 11:41:58 -0500 Subject: [PATCH 31/34] TUrn off color save on mesh creation --- server/scripts/MetashapeGenerateMesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 13c8224..8720162 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -601,7 +601,7 @@ def get_background_masks(mask_path): save_texture=True, save_uv=True, save_normals=True, - save_colors=True, + save_colors=False, save_cameras=True, save_markers=True, save_udim=False, From 6b2f0961cb8ee0407c57c3f82fbbbade92d39707 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 15 Dec 2023 11:52:06 -0500 Subject: [PATCH 32/34] Adding permissive CORS header to /machine endpoint --- source/server/app/ApiRouter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/source/server/app/ApiRouter.ts b/source/server/app/ApiRouter.ts index 1afc486..43923ad 100644 --- a/source/server/app/ApiRouter.ts +++ b/source/server/app/ApiRouter.ts @@ -160,6 +160,7 @@ export default class ApiRouter // machine state this.router.get("/machine", (req, res) => { const state = jobManager.getState(); + res.set('Access-Control-Allow-Origin', '*'); return res.json(state); }) } From 3566035c29a26891453c4fe800d3d5f828e45bbf Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 15 Dec 2023 12:31:29 -0500 Subject: [PATCH 33/34] Adding additional permissive CORS header to /machine endpoint --- source/server/app/ApiRouter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/source/server/app/ApiRouter.ts b/source/server/app/ApiRouter.ts index 43923ad..3205217 100644 --- a/source/server/app/ApiRouter.ts +++ b/source/server/app/ApiRouter.ts @@ -161,6 +161,7 @@ export default class ApiRouter this.router.get("/machine", (req, res) => { const state = jobManager.getState(); res.set('Access-Control-Allow-Origin', '*'); + res.set('Access-Control-Allow-Private-Network', 'true'); return res.json(state); }) } From 954473026942dbfbb3bfcfa91927ed3d96913a77 Mon Sep 17 00:00:00 2001 From: Jamie Cope Date: Fri, 2 Feb 2024 14:38:03 -0500 Subject: [PATCH 34/34] Documentation update and photogrammetry cleanup --- docs/content/recipes/clean/index.md | 9 +++++ docs/content/recipes/photogrammetry/index.md | 9 +++++ .../recipes/si-zip-photogrammetry/index.md | 9 +++++ .../tasks/batch-convert-image/index.md | 23 ++++++++++++ docs/content/tasks/combine-mesh/index.md | 20 +++++++++++ docs/content/tasks/merge-mesh/index.md | 19 ++++++++++ .../content/tasks/photogrammetry-tex/index.md | 23 ++++++++++++ docs/content/tasks/photogrammetry/index.md | 36 +++++++++++++++++++ .../tasks/photogrammetry/scalebar-defs.csv | 33 +++++++++++++++++ docs/content/tasks/screenshot/index.md | 17 +++++++++ docs/content/tools/meshroom/index.md | 27 ++++++++++++++ docs/content/tools/metashape/index.md | 27 ++++++++++++++ docs/content/tools/reality-capture/index.md | 6 +--- server/recipes/photogrammetry.json | 5 --- server/recipes/si-zip-photogrammetry.json | 5 --- server/scripts/MetashapeGenerateMesh.py | 20 +---------- source/server/tasks/MergeMeshTask.ts | 3 +- source/server/tasks/PhotogrammetryTask.ts | 6 ---- source/server/tasks/PhotogrammetryTexTask.ts | 2 -- source/server/tasks/ScreenshotTask.ts | 2 +- source/server/tools/MeshroomTool.ts | 1 - source/server/tools/MetashapeTool.ts | 3 +- source/server/tools/RealityCaptureTool.ts | 1 - 23 files changed, 258 insertions(+), 48 deletions(-) create mode 100644 docs/content/recipes/clean/index.md create mode 100644 docs/content/recipes/photogrammetry/index.md create mode 100644 docs/content/recipes/si-zip-photogrammetry/index.md create mode 100644 docs/content/tasks/batch-convert-image/index.md create mode 100644 docs/content/tasks/combine-mesh/index.md create mode 100644 docs/content/tasks/merge-mesh/index.md create mode 100644 docs/content/tasks/photogrammetry-tex/index.md create mode 100644 docs/content/tasks/photogrammetry/index.md create mode 100644 docs/content/tasks/photogrammetry/scalebar-defs.csv create mode 100644 docs/content/tasks/screenshot/index.md create mode 100644 docs/content/tools/meshroom/index.md create mode 100644 docs/content/tools/metashape/index.md diff --git a/docs/content/recipes/clean/index.md b/docs/content/recipes/clean/index.md new file mode 100644 index 0000000..0e0583a --- /dev/null +++ b/docs/content/recipes/clean/index.md @@ -0,0 +1,9 @@ +--- +title: "Recipe: clean" +summary: "Cleans a mesh of common issues" +weight: 110 +--- + +The `clean` recipe fixes some common issues with unnecessary geometry in a mesh by removing unreferenced vertices, zero area faces, duplicate vertices, and duplicate faces. + +It also has options for removing extraneous geometry components (often appearing as floating triangle clusters or unneeded reconstructions in photogrammetry results) by deleting everything but the largest component or, when doing turntable capture, deleting everything but the component central to the capture volume. \ No newline at end of file diff --git a/docs/content/recipes/photogrammetry/index.md b/docs/content/recipes/photogrammetry/index.md new file mode 100644 index 0000000..a9155cb --- /dev/null +++ b/docs/content/recipes/photogrammetry/index.md @@ -0,0 +1,9 @@ +--- +title: "Recipe: photogrammetry" +summary: "Creates a mesh and texture from capture image set folders" +weight: 110 +--- + +The `photogrammetry` recipe takes zip files of capture image sets (including alignment-only and masking images) and aligns the images, generates a mesh, cleans the mesh of unnecessary geometry, and finally generates a texture mapped to the cleaned mesh. This full photogrammetry pipeline currently works with Agisoft Metashape, with limited support for the RealityCapture and Meshroom applications. + +Resulting meshes may require some manual cleanup or fixing dependent on the input and masking data available. \ No newline at end of file diff --git a/docs/content/recipes/si-zip-photogrammetry/index.md b/docs/content/recipes/si-zip-photogrammetry/index.md new file mode 100644 index 0000000..c9e4941 --- /dev/null +++ b/docs/content/recipes/si-zip-photogrammetry/index.md @@ -0,0 +1,9 @@ +--- +title: "Recipe: si-zip-photogrammetry" +summary: "Creates a mesh and texture from zipped capture image sets" +weight: 120 +--- + +The `si-zip-photogrammetry` recipe is similar to the `photogrammetry` recipe but with some steps specific to the Smithsonian workflow. It takes folders of capture image sets (including alignment-only and masking images) as input and aligns the images, generates a mesh, cleans the mesh of unnecessary geometry, and finally generates a texture mapped to the cleaned mesh. This full photogrammetry pipeline currently works with Agisoft Metashape, with limited support for the RealityCapture and Meshroom applications. + +Resulting meshes may require some manual cleanup or fixing dependent on the input and masking data available. \ No newline at end of file diff --git a/docs/content/tasks/batch-convert-image/index.md b/docs/content/tasks/batch-convert-image/index.md new file mode 100644 index 0000000..531f85e --- /dev/null +++ b/docs/content/tasks/batch-convert-image/index.md @@ -0,0 +1,23 @@ +--- +title: BatchConvertImage +summary: Converts folders of image files between different formats. +--- + + +### Description + +Converts image files between different formats. + +Optionally clip the images to black or white. + +Tool: [ImageMagick](../../tools/imageMagick) + +### Options + +| Option | Type | Required | Default | Description | +|-----------------|---------|----------|---------|---------------------------------------------------------------------------------------------------------------------| +| inputImageFolder| string | yes | | Input image folder name. | +| outputImageFolder | string | yes | | Output image folder name. | +| quality | number | no | 70 | Compression quality for JPEG images (0 - 100). | +| filetype | string | no | 'jpg' | File type to convert images to. | +| level | number | no | none | If provided, clips image to black (value < 128) or white (value > 128) | \ No newline at end of file diff --git a/docs/content/tasks/combine-mesh/index.md b/docs/content/tasks/combine-mesh/index.md new file mode 100644 index 0000000..21d103b --- /dev/null +++ b/docs/content/tasks/combine-mesh/index.md @@ -0,0 +1,20 @@ +--- +title: CombineMesh +summary: Combines two meshes into a single self contained .fbx +--- + +### Description + +Combines two meshes into a single self contained .fbx. + +Tool: [Blender](../../tools/blender) + +### Options + +| Option | Type | Required | Default | Description | +|---------------|----------|----------|--------------------|---------------------------------------------------------------| +| baseMeshFile | string | yes | | Base mesh file name. | +| inputMeshFile | string | yes | | Input mesh file name to combine with base. | +| inputMeshBasename | string | yes | | Name used for merged input mesh | +| outputMeshFile | string | yes | | Output mesh file name. | +| timeout | number | no | 0 | Maximum task execution time in seconds | \ No newline at end of file diff --git a/docs/content/tasks/merge-mesh/index.md b/docs/content/tasks/merge-mesh/index.md new file mode 100644 index 0000000..a5243fb --- /dev/null +++ b/docs/content/tasks/merge-mesh/index.md @@ -0,0 +1,19 @@ +--- +title: MergeMesh +summary: Merges a multi-mesh model file into one .obj and texture +--- + +### Description + +Merges a multi-mesh model file into one .obj and texture. + +Tool: [Blender](../../tools/blender) + +### Options + +| Option | Type | Required | Default | Description | +|---------------|----------|----------|--------------------|---------------------------------------------------------------| +| inputMeshFile | string | yes | | Input mesh file name to merge. | +| outputMeshFile | string | yes | | Output mesh file name. | +| outputTextureFile | string | yes | | Output texture file name. | +| timeout | number | no | 0 | Maximum task execution time in seconds | \ No newline at end of file diff --git a/docs/content/tasks/photogrammetry-tex/index.md b/docs/content/tasks/photogrammetry-tex/index.md new file mode 100644 index 0000000..0f222c2 --- /dev/null +++ b/docs/content/tasks/photogrammetry-tex/index.md @@ -0,0 +1,23 @@ +--- +title: PhotogrammetryTex +summary: Uses photogrammetry capture images to project a texture onto existing geometry. +--- + + +### Description + +Uses photogrammetry capture images to project a texture onto existing geometry using saved camera positions from a previous photogrammetry process. + +Tools: [Metashape](../../tools/metashape) (** Planned implementations for RealityCapture and Meshroom **) + +### Options + +| Option | Type | Required | Default | Description | +|----------------------|---------|----------|-----------|----------------------------------------------------------------------------------------------| +| inputImageFolder | string | yes | | Input image folder zip file. | +| inputModelFile | string | yes | | Metashape only: Alignment image folder. | +| outputFile | string | yes | | Base name used for output files. | +| camerasFile | string | yes | | Name used for saved camera position file. | +| scalebarFile | string | no | | CSV file with scalebar markers and distances. ([Example scalebar file](./scalebar-defs.csv)) | +| timeout | number | no | 0 | Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup). | +| tool | string | no | "Metashape" | Tool to use for decimation: "Metashape", "RealityCapture", or "Meshroom". | \ No newline at end of file diff --git a/docs/content/tasks/photogrammetry/index.md b/docs/content/tasks/photogrammetry/index.md new file mode 100644 index 0000000..5f6e6b6 --- /dev/null +++ b/docs/content/tasks/photogrammetry/index.md @@ -0,0 +1,36 @@ +--- +title: Photogrammetry +summary: Generates a mesh and texture from zipped image sets using photogrammetry techniques. +--- + + +### Description + +Generates a mesh and texture from zipped image sets using photogrammetry techniques. It includes options for masking image sets and alignment-only images. + +Tools: [Metashape](../../tools/metashape), +With limited support by: [RealityCapture](../../tools/reality-capture), [Meshroom](../../tools/meshroom) + +### Options + +| Option | Type | Required | Default | Description | +|----------------------|---------|----------|-----------|----------------------------------------------------------------------------------------------| +| inputImageFolder | string | yes | | Input image folder zip file. | +| alignImageFolder | string | yes | | Metashape only: Alignment image folder. | +| maskImageFolder | string | no | | Metashape only: Mask image folder. | +| outputFile | string | no | | Base name used for output files. | +| camerasFile | string | no | | Metashape only: Name used for saved camera position file. | +| scalebarFile | string | no | | CSV file with scalebar markers and distances. ([Example scalebar file](./scalebar-defs.csv)) | +| optimizeMarkers | boolean | no | false | Metashape only: Flag to enable discarding high-error markers. | +| alignmentLimit | number | no | 50 | Metashape only: Percent success required to pass alignment stage. | +| tiepointLimit | integer | no | 25000 | Metashape only: Max number of tiepoints. | +| keypointLimit | integer | no | 75000 | Metashape only: Max number of keypoints. | +| turntableGroups | boolean | no | false | Metashape only: Flag to process images as SI-formatted turntable groups. | +| depthMaxNeighbors | integer | no | 16 | Metashape only: Max neighbors value to use for depth map generation. | +| genericPreselection | boolean | no | true | Metashape only: Flag = true to use generic preselection. | +| meshQuality | string | no | "High" | Metashape only: Preset for mesh quality ("Low", "Medium", "High", "Highest", "Custom"). | +| customFaceCount | integer | no | 3000000 | Metashape only: If meshQuality is custom, this defines the goal face count. | +| depthMapQuality | string | no | "Highest" | Metashape only: Preset for depth map quality ("Low", "Medium", "High", "Highest"). | +| maskMode | string | no | "File" | Metashape only: Desired masking operation. "File" assumes provided image is the mask, "Background" uses the background of the image as a basis for 'smart' masking. | +| timeout | number | no | 0 | Maximum task execution time in seconds (default: 0, uses timeout defined in tool setup). | +| tool | string | no | "Metashape" | Tool to use for decimation: "Metashape", "RealityCapture", or "Meshroom". | \ No newline at end of file diff --git a/docs/content/tasks/photogrammetry/scalebar-defs.csv b/docs/content/tasks/photogrammetry/scalebar-defs.csv new file mode 100644 index 0000000..b43c10e --- /dev/null +++ b/docs/content/tasks/photogrammetry/scalebar-defs.csv @@ -0,0 +1,33 @@ +marker1,marker2,distance +4,5,0.2498 +5,6,0.24987 +7,8,0.24985 +9,10,0.49965 +10,11,0.49963 +12,13,0.24976 +13,14,0.24987 +33,34,0.50016 +34,35,0.50034 +36,37,0.24997 +37,38,0.2501 +41,42,0.49999 +42,43,0.50019 +44,45,0.25008 +45,46,0.24996 +49,50,0.50009 +50,51,0.50007 +52,53,0.25 +53,54,0.25004 +55,56,0.2501 +57,58,0.50016 +58,59,0.50013 +63,64,0.25008 +65,66,0.50021 +66,67,0.50029 +68,69,0.25008 +69,70,0.25006 +73,74,0.50026 +74,75,0.50035 +76,77,0.25014 +77,78,0.25011 +79,80,0.25007 diff --git a/docs/content/tasks/screenshot/index.md b/docs/content/tasks/screenshot/index.md new file mode 100644 index 0000000..9ebb51d --- /dev/null +++ b/docs/content/tasks/screenshot/index.md @@ -0,0 +1,17 @@ +--- +title: Screenshot +summary: Generates a screenshot of the provided geometry +--- + +### Description + +Generates a screenshot of the provided geometry + +Tool: [Blender](../../tools/blender) + +### Options + +| Option | Type | Required | Default | Description | +|---------------|----------|----------|--------------------|---------------------------------------------------------------| +| inputMeshFile | string | yes | | Input mesh file name to combine with base. | +| timeout | number | no | 0 | Maximum task execution time in seconds | \ No newline at end of file diff --git a/docs/content/tools/meshroom/index.md b/docs/content/tools/meshroom/index.md new file mode 100644 index 0000000..f88e471 --- /dev/null +++ b/docs/content/tools/meshroom/index.md @@ -0,0 +1,27 @@ +--- +title: Meshroom +summary: Photogrammetry tool +--- + +### Information + +- Developer: AliceVision +- Website: https://alicevision.org/#meshroom +- License: https://github.com/alicevision/meshroom?tab=License-1-ov-file + +### Installation + +- Windows installer: https://www.fosshub.com/Meshroom.html?dwl=Meshroom-2023.3.0-win64.zip + +### Configuration + +Example configuration for Meshroom in the `tools.json` configuration file: + +```json +"RealityCapture": { + "executable": "C:\\Program Files\\Meshroom\\Meshroom-2021.1.0\\meshroom_batch.exe", + "version": "2021.1.0", + "maxInstances": 1, + "timeout": 0 // never +} +``` \ No newline at end of file diff --git a/docs/content/tools/metashape/index.md b/docs/content/tools/metashape/index.md new file mode 100644 index 0000000..960b847 --- /dev/null +++ b/docs/content/tools/metashape/index.md @@ -0,0 +1,27 @@ +--- +title: Agisoft Metashape +summary: Photogrammetry tool +--- + +### Information + +- Developer: Agisoft LLC +- Website: https://www.agisoft.com/ +- License: Commercial/Proprietary + +### Installation + +- Windows installer: https://www.agisoft.com/downloads/installer/ + +### Configuration + +Example configuration for Agisoft Metashape in the `tools.json` configuration file: + +```json +"RealityCapture": { + "executable": "C:\\Program Files\\Agisoft\\Metashape Pro\\metashape.exe", + "version": "v1.8.3, build 14331", + "maxInstances": 1, + "timeout": 7200 +} +``` \ No newline at end of file diff --git a/docs/content/tools/reality-capture/index.md b/docs/content/tools/reality-capture/index.md index f604ad9..abbb713 100644 --- a/docs/content/tools/reality-capture/index.md +++ b/docs/content/tools/reality-capture/index.md @@ -3,10 +3,6 @@ title: Reality Capture summary: Photogrammetry tool --- -### Note - -_Reality Capture support is planned for a future release of Cook._ - ### Information - Developer: Capturing Reality s.r.o. @@ -24,7 +20,7 @@ Example configuration for Reality Capture in the `tools.json` configuration file ```json "RealityCapture": { "executable": "C:\\Program Files\\Capturing Reality\\RealityCapture\\RealityCapture.exe", - "version": "1.0.3.4658", + "version": "1.2.0.17385", "maxInstances": 1, "timeout": 0 } diff --git a/server/recipes/photogrammetry.json b/server/recipes/photogrammetry.json index 5784050..4ad7d40 100644 --- a/server/recipes/photogrammetry.json +++ b/server/recipes/photogrammetry.json @@ -41,10 +41,6 @@ "minLength": 1, "format": "file" }, - "generatePointCloud": { - "type": "boolean", - "default": false - }, "optimizeMarkers": { "type": "boolean", "default": false @@ -291,7 +287,6 @@ "camerasFile": "camerasFile", "outputFile": "deliverables.meshFile", "scalebarFile": "scalebarCSV", - "generatePointCloud": "generatePointCloud", "optimizeMarkers": "optimizeMarkers", "alignmentLimit": "alignmentLimit", "tiepointLimit": "tiepointLimit", diff --git a/server/recipes/si-zip-photogrammetry.json b/server/recipes/si-zip-photogrammetry.json index 4651688..952d173 100644 --- a/server/recipes/si-zip-photogrammetry.json +++ b/server/recipes/si-zip-photogrammetry.json @@ -42,10 +42,6 @@ "minLength": 1, "format": "file" }, - "generatePointCloud": { - "type": "boolean", - "default": false - }, "optimizeMarkers": { "type": "boolean", "default": false @@ -342,7 +338,6 @@ "camerasFile": "camerasFile", "outputFile": "deliverables.meshFile", "scalebarFile": "scalebarCSV", - "generatePointCloud": "generatePointCloud", "optimizeMarkers": "optimizeMarkers", "alignmentLimit": "alignmentLimit", "tiepointLimit": "tiepointLimit", diff --git a/server/scripts/MetashapeGenerateMesh.py b/server/scripts/MetashapeGenerateMesh.py index 8720162..c9fc6ad 100644 --- a/server/scripts/MetashapeGenerateMesh.py +++ b/server/scripts/MetashapeGenerateMesh.py @@ -149,7 +149,6 @@ def get_background_masks(mask_path): parser.add_argument("-al", "--align_limit", required=False, help="Alignment threshold (%)") parser.add_argument("-sb", required=False, help="Scalebar definition file") parser.add_argument("-optm", required=False, default="False", help="Optimize markers") -parser.add_argument("-bdc", required=False, default="False", help="Build dense cloud") parser.add_argument("-tp", required=False, default=25000, help="Tiepoint limit") parser.add_argument("-kp", required=False, default=75000, help="Keypoint limit") parser.add_argument("-gp", required=False, default="True", help="Generic preselection") @@ -532,23 +531,6 @@ def get_background_masks(mask_path): max_workgroup_size=100 ) -denseCloudFlag = convert(args.bdc); -if denseCloudFlag == True: - # build dense cloud - # the quality of the dense cloud is determined by the quality of the depth maps - # "max_neighbors" value of '-1' will evaluate ALL IMAGES in parallel. 200-300 is good when there is a lot of image overlap. - # setting this value will fix an issue where there is excessive 'fuzz' in the dense cloud. the default value is 100. - chunk.buildDenseCloud\ - ( - point_colors=True, - point_confidence=True, - keep_depth=True, - max_neighbors=300, - subdivide_task=True, - workitem_size_cameras=20, - max_workgroup_size=100 - ) - modelQuality = [Metashape.FaceCount.LowFaceCount, Metashape.FaceCount.MediumFaceCount, Metashape.FaceCount.HighFaceCount, Metashape.FaceCount.CustomFaceCount] chunk.buildModel\ @@ -557,7 +539,7 @@ def get_background_masks(mask_path): interpolation=Metashape.DisabledInterpolation, face_count = modelQuality[3] if int(args.mq) < 0 else modelQuality[int(args.mq)], face_count_custom = 0 if int(args.mq) < 0 else args.cfc, - source_data = Metashape.DenseCloudData if denseCloudFlag == True else Metashape.DepthMapsData, + source_data = Metashape.DepthMapsData, vertex_colors=False, vertex_confidence=True, volumetric_masks=False, diff --git a/source/server/tasks/MergeMeshTask.ts b/source/server/tasks/MergeMeshTask.ts index 97661f6..2e9986e 100644 --- a/source/server/tasks/MergeMeshTask.ts +++ b/source/server/tasks/MergeMeshTask.ts @@ -59,7 +59,8 @@ export default class MergeMeshTask extends ToolTask }, required: [ "inputMeshFile", - "outputMeshFile" + "outputMeshFile", + "outputTextureFile" ], additionalProperties: false }; diff --git a/source/server/tasks/PhotogrammetryTask.ts b/source/server/tasks/PhotogrammetryTask.ts index 75c0a56..40edb7f 100644 --- a/source/server/tasks/PhotogrammetryTask.ts +++ b/source/server/tasks/PhotogrammetryTask.ts @@ -42,8 +42,6 @@ export interface IPhotogrammetryTaskParameters extends ITaskParameters camerasFile: string; /** CSV file with scalebar markers and distances */ scalebarFile: string; - /** Flag to enable building a dense point cloud */ - generatePointCloud: boolean; /** Flag to enable discarding high-error markers */ optimizeMarkers: boolean; /** Percent success required to pass alignment stage */ @@ -93,7 +91,6 @@ export default class PhotogrammetryTask extends ToolTask outputFile: { type: "string", minLength: 1 }, camerasFile: { type: "string", minLength: 1 }, scalebarFile: { type: "string", minLength: 1 }, - generatePointCloud: { type: "boolean", default: false}, optimizeMarkers: { type: "boolean", default: false}, alignmentLimit: { type: "number", default: 50}, tiepointLimit: { type: "integer", default: 25000}, @@ -130,7 +127,6 @@ export default class PhotogrammetryTask extends ToolTask outputFile: params.outputFile, camerasFile: params.camerasFile, scalebarFile: params.scalebarFile, - generatePointCloud: params.generatePointCloud, optimizeMarkers: params.optimizeMarkers, alignmentLimit: params.alignmentLimit, tiepointLimit: params.tiepointLimit, @@ -153,7 +149,6 @@ export default class PhotogrammetryTask extends ToolTask imageInputFolder: params.inputImageFolder, outputFile: params.outputFile, scalebarFile: params.scalebarFile, - generatePointCloud: params.generatePointCloud, timeout: params.timeout }; @@ -164,7 +159,6 @@ export default class PhotogrammetryTask extends ToolTask imageInputFolder: params.inputImageFolder, outputFile: params.outputFile, scalebarFile: params.scalebarFile, - generatePointCloud: params.generatePointCloud, timeout: params.timeout }; diff --git a/source/server/tasks/PhotogrammetryTexTask.ts b/source/server/tasks/PhotogrammetryTexTask.ts index 9841789..a2129c3 100644 --- a/source/server/tasks/PhotogrammetryTexTask.ts +++ b/source/server/tasks/PhotogrammetryTexTask.ts @@ -103,7 +103,6 @@ export default class PhotogrammetryTexTask extends ToolTask imageInputFolder: params.inputImageFolder, outputFile: params.outputFile, scalebarFile: params.scalebarFile, - generatePointCloud: params.generatePointCloud, timeout: params.timeout }; @@ -114,7 +113,6 @@ export default class PhotogrammetryTexTask extends ToolTask imageInputFolder: params.inputImageFolder, outputFile: params.outputFile, scalebarFile: params.scalebarFile, - generatePointCloud: params.generatePointCloud, timeout: params.timeout }; diff --git a/source/server/tasks/ScreenshotTask.ts b/source/server/tasks/ScreenshotTask.ts index f1ebaad..2b1a372 100644 --- a/source/server/tasks/ScreenshotTask.ts +++ b/source/server/tasks/ScreenshotTask.ts @@ -34,7 +34,7 @@ export interface IScreenshotTaskParameters extends ITaskParameters } /** - * Merges a multi-mesh model file into one .obj and texture + * Generates a screenshot of the provided geometry * * Parameters: [[IScreenshotTaskParameters]]. * Tool: [[BlenderTool]]. diff --git a/source/server/tools/MeshroomTool.ts b/source/server/tools/MeshroomTool.ts index 60e9814..9158512 100644 --- a/source/server/tools/MeshroomTool.ts +++ b/source/server/tools/MeshroomTool.ts @@ -23,7 +23,6 @@ export interface IMeshroomToolSettings extends IToolSettings imageInputFolder: string; outputFile?: string; scalebarFile?: string; - generatePointCloud?: boolean; } //////////////////////////////////////////////////////////////////////////////// diff --git a/source/server/tools/MetashapeTool.ts b/source/server/tools/MetashapeTool.ts index 0ca80e8..1d09c66 100644 --- a/source/server/tools/MetashapeTool.ts +++ b/source/server/tools/MetashapeTool.ts @@ -30,7 +30,6 @@ export interface IMetashapeToolSettings extends IToolSettings inputModelFile?: string; camerasFile?: string; scalebarFile?: string; - generatePointCloud?: boolean; optimizeMarkers?: boolean; alignmentLimit?: number; tiepointLimit?: number; @@ -93,7 +92,7 @@ export default class MetashapeTool extends Tool