-- ---------------------------------------------------------------------------------------------------------- -- ---------------------------------------------------------------------------------------------------------- --@author Mark Bassett --@version 1.00 --Last modified 7/23/2023 --@shortcut ctrl + p -- ---------------------------------------------------------------------------------------------------------- -- ---------------------------------------------------------------------------------------------------------- -- Turn this information into Help accessible from the UI local strHelpText = string.format( [[ -- Help Text Purpose The primary purpose of the script is to get the products of photogrammetry applications into Octane, although it can be used to populate an Octane Scatter Node with any set of points in a pre existing ascii file. Photogrammetry applications will create 3D point cloud data from a series of overlapping 2D imates, other 3d data can then be derived from the point cloud and the photographic images such as textured meshes. These products can be very useful in creating a context for a subject model. Description/Instructions Makes a file suitable for use in an Octane Scatter node from a simple list of xyz locations. Colored point clouds are supported if RGB(A) color data is present by generating an Instance Color Texture from these values and assigning a unique instance id to each point. The user must supply a suitably formatted file as described below. The file will be analyzed and loaded into the script's UI and the user given the chance to check it. The script will identify lines containing non point related data and alert the user. The user should check for the presence of a Header at this time and remove it with a text editor if necessary, or let the script decide which lines belong to a Header and remove them automatically. The user can select what sort of output they desire and then they can process the file. Processing will create a .cvs file that can be read by an Octane Scatter Node, and optionally an Instance Color texture that can be imported into Octane. The three output options are as follows. Write a space seperated .csv file suitable for input into an Octane scatter node from an input file that contains only the three cordinates for each point. This format has utility beyound point clouds and can be used to scatter any object within an Octane Scene when the original locations are not formatted appropriately for a Scatter Node. Write a space seperated .csv file suitable for input into an Octane Scatter Node from an input file that contains only the three cordinates for each point, appending an instance id to each point record. The assignment of an instance id potentially makes the data useful for other purposes, such as the assignment of color to points based on photographic images, or making the scatter data useful for further manipuation by other scripts. Write a space seperated .csv file suitable for input into an Octane Scatter Node from an input file that contains the three cordinates for each point and it's rgb(a) color data, appending an instance id to each point record and creating a seperate Instance Color Texture image to store the colors. This is the primary method for creating colored point clouds. This option is set as the default. If this option is selected, a preview of the Instance Color Texture is displayed. As part of the processing step, the user can optionally have the script create a Node Graph of the all the nodes required to display the point cloud. The script will create a scatter node and load the newly created .csv file to locate each point. It will create a Vectron Sphere object, connect a Diffuse Material to it, and import the Instance Color Texture and connect it to the material so that each point is appropriately colored. A float value defaulting to 0.01 is also connected to the shere so that its radius can be controlled. The user should set this to the desired value which will be based on the size of the point cloud's geographic extents, Larger extents will require larger points. The sphere is connected to the scatter node, and the scatter node is connected to a placement node. A ground Plane and Geometry Group are also included. Due to differences in various authoring applications, the whole point cloud might need to be rotated. Use the placement node to align with Octanes Y axes being up, or instead, try out the 'Y axis up' feature which attempts to do this by swapping the Y and Z point ordinates and negatively scaling the Z ordinate as it writes the Scatter Node matrices. The graph must be manually connected to the geometry pin of a Render Target so that it will appear in the viewport. Remember to adjust the Sphere radius to get the point cloud to display as desired, they can be difficult to see at times, and have a direct relationship with the Ray epsilon value which must be smaller than the Radius value. If you can't see your point cloud, check this numeric relationship. Point Clouds can have large geographic extents, particularly if they contain landforms, and adjusting the Ray epsilon value might required anyway. The script has been successfuly used with files in excess of 30 million points, however files of this size take several minutes (about 3 million points per minute) to process and some time beyond this to return control of Octane to the user. There is a points thinning capability that will reduce the size of the point cloud by ignoring every Nth line in the input file. User Supplied Data. ASCII files with the following extensions are made accessable via the UI. *.txt; *.pts; *.xyz; *.pcd *.ply; *.csv However these files may need to be edited in order to work with the script. Notepad++ is a good free editor given its ability to manipulate columns of data in an unstructured text file. The script supports space seperated files without a Header with some combination of the following x y z r g b a id where x,y,z are 3 dimensional cartesian coordinates, r,g,b (and optionally a) are pixel channel values between 0 and 255 specifying the points color. The script only supports a certain number of values per line in the original file. 3 is the minimum containing the X,Y,Z ordinates of a point, 7 is the maximum containing X,Y,Z and R,G,B,A specifying the red, green, blue, and alpha channel values of each point. The data must be organised in the correct order i.e. X,Y,Z,R,G,B,A within each line. Some files store additional data in each line. You can examine the data in the text box that will display the first few lines of the file. In this way you can check for headers and validate the lines found in the body of the file. The Script has a feature whereby the user can tell it to ignore certain values\columns within a line.(see discussion of .ply files below). The script will present the data from the body of the file as illustrated below. | 1| 2| 3| 4| 5| 6| 7| 8| 9| 10| To have the script ignore a given column, place an x before the Column Id in the text editor in which it's displayed. | 1| 2| 3| x4| x5| x6| 7| 8| 9| 10| This is how you would tell the script to ignore the vector normals contained in the .ply file below. The script will not let you proceed until the number of remaining columns lies within the range of 3 to 7. The script will also attempt to remove Header lines prior to processing. If Header lines are detected, it first backs up the original input file by appending "orig_" to the front of its becoming . it then rewrites the file's body to the original file i.e. without the Header. For large files this can take a few minutes, but if you don't delete the new file you can reuse it without having to do this again. Here is an example of the files produced by the script after a Header is found and the file contains X,Y,Z,R,G,B,A data. Original filename opened by script: DroneMapper3D.ply Backup filename with Header in tact: orig_DroneMapper3D.ply New filename with Header removed: DroneMapper3D.ply CSV filename with scatter node data: Scatter_DroneMapper3D.cvs Image Color Texture filename: imgICT_DroneMapper3D.png Making Point Clouds You can use third party applications such as those listed below to convert photographs you have taken, or other point cloud file formats to one of the following *.txt; *.pts; *.xyz; *.pcd *.ply; *.csv supported formats. Additionall lumalabs, https://lumalabs.ai/ produces a point cloud as a by product of its nerf creation process making it quite easy to do with a mobile phone. Autodesk ReCap pro Agisoft Metashape Meshroom (free) Pix4D 3DF Zephyr Regard3D (free) PhotoModeler WebODM (free) RealityCapture COLMAP (free) MeshLab (free) About Each Supported Format. .txt Simple ascii text of no particular content or format. Points stored in .txt files are organized however the original author sees fit. To be of use to this script, expect one point definition per line, x,y,z being the first of three substrings in that line, if six or seven substrings are present the next three or four need to be r,g,b or r,g,b,a values specifying red, green, blue and alpha channels of the point's color. This script can handle all these conditions as illustrated by the sample lines below. If your .txt file contains point data organized differently, you can reorganize it using a tool such as the column editor in Notepad++. Headers are not supported in .txt files and need to be removed, all lines of data need to be numeric. e.g. three substrings specifying the spatial location of the point. 2.1623 -1.8567 0.7161 1.6836 0.1857 1.6998 2.1676 -1.8555 0.7122 e.g. six substrings specifying the spatial location of the point & r,g,b color values. 2.1623 -1.8567 0.7161 250 248 252 1.6836 0.1857 1.6998 50 41 54 2.1676 -1.8555 0.7122 251 248 253 e.g. seven substrings specifying the spatial location of the point & r,g,b color values + an alpha value If an alpha value is detected the actual value will be set to 255, i.e. solid during processing. 2.1623 -1.8567 0.7161 250 248 252 252 1.6836 0.1857 1.6998 50 41 54 60 2.1676 -1.8555 0.7122 251 248 253 252 .xyz & .pts Similar comments to those for .txt files although the extension is more descriptive of what the data represents. As always, watch out for Header information at the top of the file. The .xyz file extension is used by Matterport in their MatterPak package and contain six substrings per line as explained above. They work well with this script, thus you can easily build a point cloud from a Matterport scan. .pcd (point cloud data) Comes in binary and ascii versions, the script can only use the ascii version however some of the third party applications mentioned could be used to convert between the two. This file format has a relatively strict Header as follows. The order of Header entries is important. Again the Header needs to be removed prior to processing by the script. The FIELDS definition will tell you if color data is present. See Wikipedia for more info. # .PCD v.7 - Point Cloud Data file format VERSION .7 FIELDS x y z rgb SIZE 4 4 4 4 TYPE F F F F COUNT 1 1 1 1 WIDTH 213 HEIGHT 1 VIEWPOINT 0 0 0 1 0 0 0 POINTS 213 DATA ascii 0.93773 0.33763 0 4.2108e+06 0.90805 0.35641 0 4.2108e+06 0.81915 0.32 0 4.2108e+06 0.97192 0.278 0 4.2108e+06 .ply The .ply file format is designed to store more than just a point cloud. It is more akin to the .obj format than anything discussed so far. Thus point clouds are a special case and you should validate the contents of the .ply files you have before attempting to use them. They come in binary and ascii versions, the script will only work with the ascii version. If you have a binary version you need to convert it with a third party app. Below is a sample of the first few lines of a .ply file containing a point cloud. It contains point location and rgba data that we can use, however there are three additional substrings in each line, defined in the Header as floating point numbers, nx,ny,nz all of which are less than 1. The script cannot process these and currently they need to be removed using an ascii text editor such as Notepad++, or using the built in Column Selection feature. ply format ascii 1.0 comment VCGLIB generated element vertex 2026712 property float x property float y property float z property float nx property float ny property float nz property uchar red property uchar green property uchar blue property uchar alpha element face 0 property list uchar int vertex_indices end_header -177714.5 -929289.9 2028.838 0.03822402 -0.003634897 0.09233478 84 75 73 255 -177715.1 -929291.8 2024.964 -0.02729818 0.01025391 0.09565389 18 17 17 255 -177715.3 -929291.6 2024.841 -0.07316666 0.004162515 0.06803906 19 18 18 255 User Interface All UI controls have tool tips. The script is heavily commented and well populated with print statements to aid in debugging. It should be relatively easy to modify to your specific purposes and run using the Octane Script Editor which will show you whats going on under the hood. There are several variables that can be customized by the user to alter the scripts start up configuration, they are: Variable name: Default value: ------------------------------- -------------- local flgFormExpanded = true local numGetHeaderLines = 20 local numDefaultBitMapDimension = 300 local flgCommaDelimited = true local flgIgnoreAlpha = true local numStepSize = 1 local flgYup = false local numPointRadius = 0.035 local numCubeSide = 0.025 Author Mark Bassett Last modified 8/06/2023 version 1.00 shortcut ctrl + p ]] ) -- Actual Script starts here: print("Current Project Directory "..octane.file.getParentDirectory(octane.project.getCurrentProject())) print(" ") -- SCRIPT WIDE VARIABLES ------------------------------------------------- -- Set Paths and File Names, -- User definable: -- Set this to true or false to change the -- forms size at startup. As GUI builds the Form already expanded -- it is initially true, the resetForm Function is called prior to -- showing wndMain, since it is in the expanded state this call -- this will collapse it before opening the window. local flgFormExpanded = true -- User definable: -- Edit this line to change the number of lines -- to include in the Header test at startup. local numGetHeaderLines = 25 -- User definable: -- Edit this line to change the height of the default ICT image preview. -- The actual texture file will be square based on number of input lines. -- The bitmap displays only a portion of the top left corner of the ITC. local numDefaultBitMapDimension = 250 -- User definable: -- Edit this flag to change the internal format of the Scatter Node -- matrices in the resulting .cvs file, false = space delimited. local flgCommaDelimited = false -- User definable: -- Replaces alpha value in file with 255 -- Currently not implemented, but set to true -- for performance reasons as the test is in a loop. local flgIgnoreAlpha = true -- User definable: -- Skips every X line in the file to reduce the Point Cloud's size. -- The resulting Point Cloud will be 1/X the original size. The number -- must be an interger value no less than 1. Larger numbers mean a smaller -- Point Clouds. Minimum reduction is 50% i.e. interger value of 2. local numStepSize = 1 -- User definable: -- Swaps the Y and Z oordinates in the output file, no changes are made -- to the input file. Some applications, such as Octane & Unity, represent -- points with X & Z on the ground plane and Y up. If the data was exported -- from an application that represnts points with X and Y on the ground -- plane and Z is vertical, such as Unreal & 3DS Max, checking this box -- will swap the Y & Z ordinates so they come into Octane correctly. -- To avoid confusion, coordinate this value with the initial settings of -- the chkYup control instantiated below. local flgYup = true -- User definable: -- Controls the size of the point in the resulting point cloud. -- Does not get set at file load, thus is persistent from one session -- to the next. Generally user higher numbers for clouds of larger geographic extent. -- Dito for cube side if you manually change to a cube instead of shere representation -- in the node graph. local numPointRadius = 0.015 local numPointSide = 0.025 -- These most of these get reassigned at file open -- by the reset_Variables function local print = print local strInputPath = "" local strInputFilePath = "" local strInputFileName = "" local strInputFileExtension = "" local strOutputPath = "" local strOutputFileName = "" local strOutputFilePath = "" local strImagePath = "" local tblAllPixels = {} local numBitMapDimension = "" local numImageSize = "" local numImageRows = "" local numImageColumns = "" local tblSampleLineSubStrings = {} local tblLineSubStrings ={} local numLineSubStrings = "" local fhInputFile = nil local fhOutputFile = nil local flgHasHeader = false local tblBOF ={} local numBodyLines = 0 local numHeaderLines = 0 local numLastHeaderLineid = 0 local numDataDeficientLines = 0 local numPercentOfOriginal = 0 local flgDataColumnEdits = false local flgColor = false -- make a couple of return values available for general use local rv1 = nil local rv2 = nil local k1 = 1 -- stores X ordinate local k2 = 2 -- stores Y ordinate local k3 = 3 -- stores Z ordinate local k4 = 4 -- stores Red channel value local k5 = 5 -- stores Green channel value local k6 = 6 -- stores Blue channel value local k7 = 7 -- stores Alpha channel value -- Get location of user selection --local Location = {} --Location.node = octane.project.getSelection()[1] -- GUI CONTROLS --------------------------------------------- -- Warnings ------------------------------------------------------------- function showWarning(text,msg) local ret = octane.gui.showDialog { type = octane.gui.dialogType.BUTTON_DIALOG, buttons = 2, buttonTexts = {"Ok"}, icon = octane.gui.dialogIcon.WARNING, title = "WARNING:\n"..tostring(text), text = tostring(msg), } end -- Helper -- use 'msg' and 'text' to display -- where you are, and value of variables repectively function showDebug(text,msg) local ret = octane.gui.showDialog { type = octane.gui.dialogType.BUTTON_DIALOG, buttons = 2, buttonTexts = {"Dismiss"}, icon = octane.gui.dialogIcon.WARNING, title = "Debug Info:\n\n"..tostring(text), text = tostring(msg), } end -- Helper labels for lines and padding columns ------------------------------------------------------------- local lblBlank = octane.gui.create { type = octane.gui.componentType.LABEL, text = "", width = 10, height = 6 } local lblSpace = octane.gui.create { type = octane.gui.componentType.LABEL, text = "", width = 1, height = 6 } local lblLine = octane.gui.create { type = octane.gui.componentType.LABEL, text = "", width = 300, height = 0 } -- Controls for source file selection ------------------------------------------------------------- local btnFileOpen = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Open...", width = 60, height = 20, tooltip = "Load Points File containing xyz point locations and optionally RGB color data.", } local lblInputPath = octane.gui.create { type = octane.gui.componentType.LABEL, text = " " .. octane.file.getParentDirectory(octane.project.getCurrentProject()), --text ="", width = 335, tooltip = "Source file directory. Processed files will also be saved here." } local txtOutputFileName = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, text = "", width = 140, tooltip = "Optional output file name, else it will be autogenerated based on the selected file's name and content." } -- Controls for file info ------------------------------------------------------------- local lblInputFileExtension = octane.gui.create { type = octane.gui.componentType.LABEL, text = " File Type: ", width = 60, height = 12, tooltip = "The type of input file." } local nbxHeaderLength = octane.gui.create { type = octane.gui.componentType.NUMERIC_BOX, width = 90, height = 16, x = 325, maxValue = 150, minValue = 10, step = 5, value = 30, tooltip = "Number of lines to preview from the beginning of file. Type or click on the arrows to set the desired value.", } local lblInputFileDescription = octane.gui.create { type = octane.gui.componentType.LABEL, text = "File Contents:", width = 578, height = 12, tooltip = "Description of the input file's content, i.e. Number of Lines, Words, Characters." } local lblInputLineDescription = octane.gui.create { type = octane.gui.componentType.LABEL, text = "Line Contents:", width = 535, height = 12, tooltip = "Description of the contents of each line in the body of the file, i.e. Points found. ID's found. Colors found." } local lblFileContent = octane.gui.create { type = octane.gui.componentType.LABEL, text = "Beginning of File", width = 260, height = 12, tooltip = "There should be sufficient lines specified to include the complete Header, otherwise automatic Header removal will fail. Please verify." } local txtFileContent = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, multiline = true, text = "", width = 577, height = 136, tooltip = "These lines will be analysed for the presence of a Header. The user can change the number of lines analysed with the numeric control below this text box.." } local lblColumnSelector = octane.gui.create { type = octane.gui.componentType.LABEL, text = "Column Selector:", width = 90, height = 14, tooltip = "List of the data columns found in the body of the file. Insert an 'x' before the column Id to ignore that column's contents during processing." } local btnResetColumns = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Reset", width = 60, height = 17, tooltip = "Repopulates the column selector with the unedited list.", } local txtColumnSelector = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, multiline = false, text = "", width = 575, padding = 0, x = 0, y = 0, height = 24, --tooltip = "List of the data columns found in the body of the file. Insert an 'x' before the column Id to ignore that column's contents during processing.", } -- Controls for output file content ------------------------------------------------------------- local chkPointsIdsAndColors = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "Points, Instance ID's & Color Map", width = 185, checked = true, enable = true, tooltip = "File will be formated for an Octane Scatter Node containing the Point locations, auto generated Instance ID's and corresponding RGB color values for each point." } local chkPointsAndIds = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "Points & Instance ID's", width = 135, checked = false, enable = true, tooltip = "File will be formated for an Octane Scatter Node with the addition of an Instance ID matching the files line number. It will be possible to assign multiple colors to the points within Octane." } local chkPointsOnly = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "Points Only", width = 105, checked = false, enable = true, tooltip = "File will be formated for an Octane Scatter Node by padding point locations with unit transformation values only. It will not be possible to assign multiple colors to the points within Octane." } local chkYup = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "Y axis up", width = 75, checked = false, enable = true, tooltip = "If the point cloud is not oriented correctly inside Octane, this will effectively rotate the data around the X axis." } local nbxStepSize = octane.gui.create { type = octane.gui.componentType.NUMERIC_BOX, width = 40, height = 19, x = 0, maxValue = 100, minValue = 1, step = 1, value = 1, tooltip = "The number of lines to increment between each write to the Scatter Node matrix file if you want to thin the point cloud. For example, if set to 10, it would create a Point Cloud using every tenth line in the original file, thus producing a Point Cloud that is only 10% of the original size, if set to 2, the cloud would be 50% of its original size." } -- Controls for file processing ------------------------------------------------------------- local btnProcessInputFile = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Process File", width = 85, height = 20, enable = false, tooltip = "Process the selected points file and generate a new .csv file containing the results formatted for an Octane Scatter Node.", } local pbrloadInputFile = octane.gui.create { type = octane.gui.componentType.PROGRESS_BAR, progress = 50, width = 375, height = 20, text = "Awaiting Input File selection" } local chkCreateOctaneNodes = octane.gui.create { type = octane.gui.componentType.CHECK_BOX, text = "Create Point Cloud.", width = 110, checked = true, enable = false, tooltip = "Check to create and configure all the nodes required to model a point cloud represented by the input file." } local txtOutputFilesPath = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, text = "", width = 440, height = 20, tooltip = "Location of Output Files. Copy and Paste to File Explorer for fast navigation." } -- Controls for Bitmap display ------------------------------------------------------------- local lblInstanceColorMap = octane.gui.create { type = octane.gui.componentType.LABEL, text = "Instance Color Map (from source file - 500 x 500 pixels )", width = 578, height = 12, } local picInstanceColors = octane.gui.create { type = octane.gui.componentType.BITMAP, width = numDefaultBitMapDimension, height = numDefaultBitMapDimension, opacity = 1, backgroundColour = { 0, 0, 0, 0 }, } -- Controls for Form (window) ------------------------------------------------------------- local btnMainClose = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Close", x = 10, y = 0, width = 60, height = 20, tooltip = "Close main window.", } local btnResizeForm = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Hide Preview <", x = 515, y = 0, width = 90, height = 17, tooltip = "Resize the main form.", inset = { 0 }, padding = { 0 }, } -- Arrange all the controls on the form ------------------------------------------------------------- -- Arrange Input file controls in a group local grpInputFileLocation = octane.gui.create { type = octane.gui.componentType.GROUP, width = 100, text = "Input File Location", rows = 1, cols = 2, children = {btnFileOpen, lblInputPath}, border = true, inset = { 5 }, padding = { 5 }, } -- Arrange Output file name in a group local grpOutputFileName = octane.gui.create { type = octane.gui.componentType.GROUP, width = 100, text = "Output File Name", rows = 1, cols = 1, children = {txtOutputFileName}, border = true, inset = { 5 }, padding = { 5 }, } -- Arrange File Processing Controls in a group local grpFileManagement = octane.gui.create { type = octane.gui.componentType.GROUP, width = 500, text = "Processing settings", rows = 1, cols = 3, children = {lblColumnSelector, btnResetColumns, nbxHeaderLength}, border = false, inset = { 0 }, padding = { 0 }, } -- Arrange Input file content controls in a group local grpInputFileContent = octane.gui.create { type = octane.gui.componentType.GROUP, width = 800, text = "Input File Content", rows = 9, cols = 1, children = {lblInputFileExtension, lblInputFileDescription, lblInputLineDescription, lblLine, lblFileContent, txtFileContent, lblLine, grpFileManagement,txtColumnSelector}, border = true, inset = { 5 }, padding = { 5 }, } -- Arrange Output file content controls in a group local grpOutputFileContent = octane.gui.create { type = octane.gui.componentType.GROUP, width = grpInputFileContent:getProperties().width, text = "Output File Content", rows = 1, cols = 5, children = {chkPointsIdsAndColors, chkPointsAndIds, chkPointsOnly, chkYup, nbxStepSize}, border = true, inset = { 5 }, padding = { 5 }, } -- Arrange the file process button & progress bar in a group local grpProcessInputFile = octane.gui.create { type = octane.gui.componentType.GROUP, width = grpInputFileContent:getProperties().width, text = "Image Source Location", rows = 1, cols = 3, children = {btnProcessInputFile, pbrloadInputFile, chkCreateOctaneNodes}, border = false, inset = { 5 }, padding = { 5 }, } -- Controls for Help form ------------------------------------------------------------- local btnHelpOpen = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Help...", x = 30, y = 0, width = 60, height = 20, tooltip = "Load a Form with information about the script.", } local lblHelp = octane.gui.create { type = octane.gui.componentType.LABEL, text = "Help Text", --x = 10, --y = 10, width = 650, height = 12, } local txtHelp = octane.gui.create { type = octane.gui.componentType.TEXT_EDITOR, multiline = true, text = "", width = lblHelp:getProperties().width, --x = 10, height = 355, tooltip = "" } local btnHelpClose = octane.gui.create { type = octane.gui.componentType.BUTTON, text = "Close", width = 60, height = 20, x = lblHelp:getProperties().width - 60, y = 10, tooltip = "Close the Help window.", } -- Group the help controls local grpHelp = octane.gui.create { type = octane.gui.componentType.GROUP, width = grpInputFileContent:getProperties().width, rows = 2, cols = 1, children = {txtHelp, btnHelpClose}, border = false, inset = { 5 }, padding = { 5 }, } -- Window that holds Help controls local wndHelp = octane.gui.create { type = octane.gui.componentType.WINDOW, text = "Points to Scatter Help", children = {grpHelp}, width = lblHelp:getProperties().width + 18, height = txtHelp:getProperties().height + 40, padding = { 12 }, inset = { 12 }, } -- Arrange the bitmap controls in a group local grpImagePreview = octane.gui.create { type = octane.gui.componentType.GROUP, width = grpInputFileContent:getProperties().width, text = "Instance Colors", rows = 2, cols = 1, children = {lblInstanceColorMap , picInstanceColors,}, border = true, inset = { 5 }, padding = { 5 }, } -- Group the groups local grpInput = octane.gui.create { type = octane.gui.componentType.GROUP, width = grpImagePreview:getProperties().width, rows = 1, cols = 2, children = {grpInputFileLocation, grpOutputFileName,}, border = false, inset = { 0 }, padding = { 1 }, } local grpEnd = octane.gui.create { type = octane.gui.componentType.GROUP, width = grpImagePreview:getProperties().width, x = 0, y = 0, rows = 1, cols = 3, children = {txtOutputFilesPath, btnHelpOpen, btnMainClose}, border = false, inset = { 0 }, padding = { 1 }, } -- Group the groups local grpAll = octane.gui.create { type = octane.gui.componentType.GROUP, width = grpImagePreview:getProperties().width, rows = 7, cols = 1, children = {grpInput, grpInputFileContent, grpOutputFileContent, grpProcessInputFile, btnResizeForm, grpImagePreview, grpEnd}, border = false, inset = { 5 }, padding = { 5 }, } local wndMain = octane.gui.create { type = octane.gui.componentType.WINDOW, children = {grpAll}, width = grpAll:getProperties().width, height = grpAll:getProperties().height -20, text = "Points to Scatter", } -- ************************************************************************************************************** -- ALL FUNCTIONS -- ************************************************************************************************************** function reset_Variables() -- Called in case user loaded a different -- file without closing the script print("@reset_Variables") print(" Inputs: ", "None") strInputPath = "" strInputFilePath = "" strInputFileName = "" strInputFileExtension = "" strOutputPath = "" strOutputFileName = "" strOutputFilePath = "" strImagePath = "" tblBOF = {} tblAllPixels = {} numBitMapDimension = "" numImageSize = "" numImageRows = "" numImageColumns = "" tblSampleLineSubStrings = {} tblLineSubStrings ={} numLineSubStrings = "" fhInputFile = nil fhOutputFile = nil flgHasHeader = false flgColor = false numBodyLines = 0 numHeaderLines = 0 numLastHeaderLineid = 0 numDataDeficientLines = 0 numPercentOfOriginal = 0 flgDataColumnEdits = false numGetHeaderLines = nbxHeaderLength.value pbrloadInputFile:updateProperties{progress = .0, text = ""} if numStepSize < 1 then numStepSize = 1 end print(" ") end -- Helper function function pause() -- Function doesn't work, useful if it did print("@pause") print(" Inputs: ", "None") print(" ") -- Should stop and require a return to continue s = io.input():read() s = io.stdin:read'*l' end -- Helper function function stop() print("@stop") print(" Inputs: ", "None") do return end print(" Deliberate error triggered") -- Cause a deleberate crash, because there is no 'Print' function -- with an uppercase p. Print(" ") -- Cause a deleberate crash, because you -- cant add an alpha to a numeric z = 7 + "a" end -- Helper function function resizeForm() print("@resizeForm") print(" Inputs: ", "None") if flgFormExpanded == true then print(" Contracting main form") print(" wndMain:",wndMain:getProperties().height) print(" grpImagePreview:",grpImagePreview:getProperties().height) print(" ") -- Half Size wndMain.height = grpAll:getProperties().height - grpImagePreview:getProperties().height - 42 btnResizeForm.text = "Show Preview >" flgFormExpanded = false else print(" Expanding main form") print(" wndMain:",wndMain:getProperties().height) print(" grpImagePreview:",grpImagePreview:getProperties().height) print(" ") -- Full Size wndMain.height = grpAll:getProperties().height btnResizeForm.text = "Hide Preview <" flgFormExpanded = true end end -- Helper function function is_Binary(strFullFilePath) print("@is_Binary") print(" Inputs: ", strFullFilePath) --print(" ") -- Update status bar -- This method is slow if its an ascii file, fast if not octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .01, text = "Performing Binary/ASCII test"} octane.gui.updateStatus("Performing Binary/ASCII test", .01) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() local now = os.clock() local input, err = io.open(strFullFilePath, "rb") assert(input, err) local is_Binary = false local chunk_size = 2^12 local find = string.find local read = input.read repeat local chunk = read(input, chunk_size) if not chunk then break end if find(chunk, "[^\f\n\r\t\032-\128]") then is_Binary = true break end until false input:close() --Calculate time to execute local tte = os.clock() - now --if is_Binary then --print "File is binary..." --else --print "File is ASCII..." --end print(string.format(" Time to execute %.3f seconds: ", tte)) print(" Returning ", is_Binary) print(" ") return is_Binary end -- Not sorted out yet function bin2ascii() -- https://github.com/squeek502/lua-arbitrary-binary-string/commit/2f13911b7352134662ae72903e02b0ce1c86108e local abs = {} local escapes = { ['\a'] = '\\a', ['\b'] = '\\b', ['\f'] = '\\f', ['\n'] = '\\n', ['\r'] = '\\r', ['\t'] = '\\t', ['\v'] = '\\v', ['\\'] = '\\\\', ['"'] = '\\"', } abs.ESCAPE_MINIMAL = '[\r\n\\"]' abs.ESCAPE_CONTROL_CHARS = '[%c\r\n\\"]' abs.ESCAPE_ALL_NONASCII = '[%c\r\n\\"\128-\255]' function abs.encode(data, escape_pattern) if not escape_pattern then escape_pattern = abs.ESCAPE_MINIMAL end return data:gsub(escape_pattern, function(char) return escapes[char] or string.format('\\%03d', string.byte(char)) end) end function abs.decode(encoded) return abs.loadchunk(abs.getchunk(encoded)) end function abs.quotedstring(encoded) return '"' .. encoded .. '"' end function abs.getchunk(encoded) return "return " .. abs.quotedstring(encoded) end function abs.loadchunk(chunk) local loadfn = loadstring or load return assert(loadfn(chunk))() end return abs end -- Helper Function to print a table -- Print the entries in a table identified by their key function tprint (tblInput, strRecordDescription) -- s is a programer's note that can be used to -- describe each entry in the table if strRecordDescription == nil then strRecordDescription = "Table entry" end print("@tprint") print(" Inputs: ", tblInput, strRecordDescription) print(" ") -- key/value pairs for k, v in pairs(tblInput) do -- Put in quotes local kformat = '["' .. tostring(k) ..'"]' -- Remove quotes from non string types if type(k) ~= 'string' then kformat = '[' .. k .. ']' end local vformat = '"'.. tostring(v) ..'"' if type(v) == 'table' then tprint(v, (strRecordDescription or '')..kformat) else if type(v) ~= 'string' then vformat = tostring(v) end -- print(type(tblInput)..(strRecordDescription or '') ..kformat..' = '..vformat) -- print(" Table entry "..(strRecordDescription or '') ..kformat..' = '..vformat) print(" " ..(strRecordDescription or '') ..kformat..' = '..vformat) end end end -- Helper function -- doesn't work, scope issue? function stopScript() print("@stopScript") print(" Inputs: ", "None") local flgRunning = wndMain:closeWindow() -- Return true if close failed if not flgRunning then return end end -- Helper function function removeDuplicates(tblInput) print("@removeDuplicates") print(" Inputs: ", tblInput) --Remove duplicates local tblExists = {} for key,value in ipairs(tblInput) do -- if tblExists[value] == true then -- If value previously not encounterd, remove it table.remove(tblInput, key) else -- Add 'true' to table of values previously not encounterd tblExists[value] = true end end print(" Returning ", tblInput) print(" ") return tblInput end -- Helper function function format_Integer(Int,digits) print("@format_Integer") print(" Inputs: ", Int,digits) local numInteger = tostring(Int):reverse():gsub("%d%d%d", "%1,"):reverse():gsub("^,", "") --return tostring(Int):reverse():gsub("%d%d%d", "%1,"):reverse():gsub("^,", "") print(" Returning", numInteger ) print(" ") return numInteger end -- Helper Function -- Return true if string is numeric. (a bit pointless, use tonumber directly) function is_Numeric(str) print("@is_Numeric") print(" Inputs: ", str) local isNumber = tonumber(str) if isNumber then print(" Returning ", isNumber) return isNumber else print(" Returning ", "nil") print(" ") return nil end end -- Remove a sequencial set of lines from a file, if startline is not supplied -- they will be removed from the beginning, file is backed up first. -- Returns file exists as boolean, and the new file path and name function removeLinesFromFile(strFilePath, strNumberOfLines, strStartLine) -- Update status bar octane.gui.dispatchGuiEvents(200) pbrloadInputFile:updateProperties{progress = .55, text = "Removing Header, rewriting " ..numBodyLines.. " lines to the file..."} octane.gui.updateStatus("Removing Header, rewriting " ..numBodyLines.. " lines to the file...", .55) octane.gui.dispatchGuiEvents(200) octane.changemanager.update() print("Entering removeLinesFromFile") print(" Inputs: ", strFilePath, strNumberOfLines, strStartLine) --print(" ") --print("Check inputs: ", strFilePath, strNumberOfLines, strStartLine) -- Open existing file to read local fh = io.open(strFilePath, "r") if fh == nil then return nil end local content = {} local i = 1 -- Load the Lines to keep if strStartLine == nil or strStartline == 0 then print(" Removing " ..strNumberOfLines.. " lines from beginning of file") for line in fh:lines() do if i > strNumberOfLines then content[#content+1] = line end i = i + 1 end else print(" Removing " ..strNumberOfLines.. " lines from file, beginning @ line " ..strStartLine) for line in fh:lines() do if i < startline or i > startline + strNumberOfLines then content[#content+1] = line end i = i + 1 end end fh:close() -- Backup and rename the existing input file. local strFileName = octane.file.getFileNameWithoutExtension(strFilePath) local strExtension = octane.file.getFileExtension(strFilePath) local strPath = octane.file.getParentDirectory(strFilePath) local strBackupFilePath = strPath..[[\orig_]]..strFileName..strExtension print(" Copying input file from", strFilePath .." to ".. strBackupFilePath) local rv = octane.file.copy(strFilePath, strBackupFilePath) print(" File successfully copied =", rv) -- Overwrite the new Lines into the existing inputfile fh = io.open(strFilePath, "w+") print(" Repopulating input file ", strFilePath) for i = 1, #content do fh:write(string.format("%s\n", content[i])) end fh:close() local exists = octane.file.exists(strBackupFilePath) print(" Returning ", exists, strBackupFilePath) print("Exiting removeLinesFromFile") print(" ") -- Update status bar octane.gui.dispatchGuiEvents(200) pbrloadInputFile:updateProperties{progress = .55, text = "Removing Header complete"} octane.gui.updateStatus("Removing Header complete", .55) octane.gui.dispatchGuiEvents(200) octane.changemanager.update() -- Boolean, String return exists, strBackupFilePath -- Todo, perhaps restore original file at end of script end -- Remove lines at a given increment (every Nth, i.e. numStepSize) from a file. function thinFile(strInputFilePath, numStepSize) -- Update status bar octane.gui.dispatchGuiEvents(200) pbrloadInputFile:updateProperties{progress = .65, text = "Thinning file to " ..100/numStepSize..("%").. " of original size..."} octane.gui.updateStatus("Thinning file to " ..100/numStepSize..("%").. " of original size...", .65) octane.gui.dispatchGuiEvents(200) octane.changemanager.update() print("Entering thinFile") print(" Inputs: ", strInputFilePath, numStepSize) print(" ") -- Open existing file to read local fh = io.open(strInputFilePath, "r") if fh == nil then return nil end local content = {} local i = 1 local numNextLineToGet = 1 -- Load the Lines to keep print(" Retaining every " ..numStepSize .. "th line from beginning of file") for line in fh:lines() do if i == numNextLineToGet then -- Goto one content[#content+1] = line numNextLineToGet = numNextLineToGet + numStepSize end i = i + 1 end fh:close() -- Save thinned file. local strFilePrefix = " - ( " ..100/numStepSize..("%").. " )" local strFileName = octane.file.getFileNameWithoutExtension(strInputFilePath) local strExtension = octane.file.getFileExtension(strInputFilePath) local strPath = octane.file.getParentDirectory(strInputFilePath) local strNewFilePath = strPath .."\\".. strFileName..strFilePrefix..strExtension print(" Writing new input file from", strInputFilePath .." to ".. strNewFilePath) -- Overwrite the new Lines into the existing inputfile fh = io.open(strNewFilePath, "w+") for i = 1, #content do fh:write(string.format("%s\n", content[i])) end fh:close() local exists = octane.file.exists(strNewFilePath) print(" Returning ", exists, strNewFilePath) print("Exiting removeNthLinesFromFile") print(" ") -- Update status bar octane.gui.dispatchGuiEvents(200) pbrloadInputFile:updateProperties{progress = .65, text = "Thinning file complete"} octane.gui.updateStatus("Thinning file to complete", .65) octane.gui.dispatchGuiEvents(200) octane.changemanager.update() -- Boolean, String return exists, strNewFilePath end -- Return true if a file exists and is readable. function file_Exists(strFilePath) print("@file_Exists") print(" Inputs: ", strFilePath) local file = io.open(strFilePath, "rb") if file then file:close() end print(" Returning ", file) print(" ") return file ~= nil end -- Get a count of the Lines, Words and Characters in a file. function getFileStatistics(strFilePath) print("@getFileStatistics") print(" Inputs: ", strFilePath) local BUFSIZE = 2^13 -- 8K local f = io.input(strFilePath) -- open input file local cc, lc, wc = 0, 0, 0 -- char, line, and word counts while true do local lines, rest = f:read(BUFSIZE, "*line") if not lines then break end if rest then lines = lines .. rest .. '\n' end cc = cc + string.len(lines) -- Count words in the chunk local _,t = string.gsub(lines, "%S+", "") wc = wc + t -- Count newlines in the chunk _,t = string.gsub(lines, "\n", "\n") lc = lc + t end print(" Returning " .." char count ".. cc .." word count ".. wc .." line count ".. lc ) print(" ") return cc, wc, lc end -- Get the number of lines in a file. function getNumberOfLines(strFilePath) print("@getNumberOfLines") print(" Inputs: ", strFilePath) local numOfLines = 0 for line in io.lines(strFilePath) do numOfLines = numOfLines + 1 end print(" Returning", numOfLines ) return numOfLines end -- Get the corners of a bounding box that encloses the cloud of points function getMaxMinXYZ(strFilePath,numSubStrings) print("Entering getMaxMinXYZ") print(" Inputs: ", strFilePath) print(" ") -- Need to get a sample line to test. -- Then extract substrings if numSubStrings ~= nil then local numSubStrings = getNumberOfSubStrings(strInput) else numSubStrings = 6 end local patRead = [["*number"]] for i = 1, numSubStrings - 1, 1 do patRead = [["*number", ]] .. patRead end patRead = tostring(patRead) print("patRead =",patRead) local fh = io.input(strFilePath) if fh == nil then return nil end local tblX = {} local tblY = {} local tblZ = {} local tblXYZ = {} local i = 1 --while true do for i = 1,30,1 do i = i+1 -- Up to 13 columns of numbers, need to automate this by getting the number of substrings in a line local X, Y, Z = io.read("*number", "*number", "*number", "*number", "*number", "*number") --, "*number", "*number", "*number", "*number, *number", "*number" if not X then break end tblX[#tblX + 1] = X tblY[#tblY + 1] = Y tblZ[#tblZ + 1] = Z tblXYZ[#tblXYZ + 1] = X,Y,Z end table.sort(tblX) -- automatically sorts lowest to highest table.sort(tblY) -- automatically sorts lowest to highest table.sort(tblZ) -- automatically sorts lowest to highest local Xmax = tblX[#tblX] local Xmin = tblX[1] local Ymax = tblY[#tblY] local Ymin = tblY[1] local Zmax = tblZ[#tblZ] local Zmin = tblZ[1] print("Number of lines read:",#tblX, i) print(" ") print("Xmax =",tblX[#tblX]) print("Xmin =",tblX[1]) print("Ymax =",tblY[#tblY]) print("Ymin =",tblY[1]) print("Zmax =",tblZ[#tblZ]) print("Zmin =",tblZ[1]) print(" ") tprint(tblX) --tprint(tblY) --tprint(tblZ) tprint(tblXYZ) -- Do some math -- Calculate the 8 corners of the bounding box --Upper right front = Xmax,Ymin,Zmax --Upper right rear = Xmax,Ymax,Zmax --Upper left front = Xmin,Ymin,Zmax --Upper left rear = Xmin,Ymax,Zmax --Lower right front = Xmax,Ymin,Zmin --Lower right rear = Xmax,Ymax,Zmin --Lower left front = Xmin,Ymin,Zmin --Lower left rear = Xmin,Ymax,Zmin --CL left rght (Ymax - Ymin)/2, (Zmax - Zmin)/2 --CL up down -- Calculate the long direction of the bounding box -- Calculate the angle between the side of the bounding box and the x axis -- Calculate the angle between the side of the bounding box and the y axis print(" Returning ", Xmin, Xmax, Ymin, Ymax, Zmin, Zmax) print("Exiting getMaxMinXYZ") print(" ") return Xmin, Xmax, Ymin, Ymax, Zmin, Zmax --print(math.max(n1, n2, n3)) --end end -- Find the length of a file in characters function getLengthOfFile(strFilePath) print("@getLengthOfFile") print(" Inputs: ", strFilePath) local file = assert(io.open(strFilePath, "rb"), "Failed to open file.") local numFileLength = assert(file:seek("end"),"Failed to find Eof.") file:close() print(" Returning ", numFileLength) return tonumber(numFileLength) end -- Create a table of specified number of lines, -- or all of the lines in a file. function getFileLinesAsTable(strFilePath, numToGet) print("@getFileLinesAsTable") print(" Inputs: ", strFilePath, numToGet) --if not file_exists(file) then return {} end local tblReturn = {} -- Get all lines if numToGet == nil then for line in io.lines(strFilePath) do tblReturn[#tblReturn + 1] = line end return tblReturn end -- Get some lines if numToGet ~= nil then local count = 0 for line in io.lines(strFilePath) do count = count + 1 if count > numToGet then break end tblReturn[#tblReturn + 1] = line end print(" Returning ", tblReturn) return tblReturn end end -- Split script's column list into SubStrings. -- Return unselected subStrings, number of records in the string function getNumberOfSubStrings(strInput, strSep) print("@getNumberOfSubStrings") print(" Inputs: ", strInput, strSep) -- Default to a space seperator if one was not supplied if strSep == nil then strSep = " " end local strReturn = "" local numRecordCount = 0 -- Build string of everything Not (i.e. ^) a seperator -- Count the number of substrings for str in string.gmatch(strInput, "([^"..strSep.."]+)") do --if tonumber(str) ~= nil then numRecordCount = numRecordCount + 1 strReturn = strReturn .." ".. str --end end --strReturn = strReturn .."|" print(" Returning ", strReturn, numRecordCount) print(" ") return tonumber(numRecordCount), strReturn end -- Split script's column list into SubStrings. -- Return unselected subStrings, number of records in the string function getColumnIdsToUse(strInput, strSep) print("@getColumnIdsToUse") print(" Inputs: ", strInput, strSep) -- Function is specific to this script, seperator between column id's if strSep == nil then strSep = "|" end local strReturn = "" local numRecordCount = 0 -- Build string of everything Not i.e.(^) a seperator for str in string.gmatch(strInput, "([^"..strSep.."]+)") do -- Supports up to 99 columns of numeric content -- User should be supplying a 2 character string if tagging -- a column hence testing for less than 3 if string.len(str) < 3 then if tonumber(str) ~= nil then numRecordCount = numRecordCount + 1 strReturn = strReturn .."|".. str end end end strReturn = strReturn .."|" print(" Returning ", strReturn, numRecordCount) print(" ") return strReturn, tonumber(numRecordCount) end -- Split script's column list into SubStrings. -- Return selected subStrings, number of records in the string function getColumnIdsToIgnore(strInput, strSep) print("@getColumnIdsToIgnore") print(" Inputs: ", strInput, strSep) --print(" ") if strSep == nil then strSep = "|" end local strReturn = "" local numRecordCount = 0 -- Build string of everything Not i.e.(^) a seperator for str in string.gmatch(strInput, "([^"..strSep.."]+)") do -- Get strings with extra non numeric characters if string.len(str) > 1 then if tonumber(str) == nil then numRecordCount = numRecordCount + 1 strReturn = strReturn .."|".. str end end end strReturn = strReturn .."|" print(" Returning ", strReturn, numRecordCount) print(" ") return strReturn, tonumber(numRecordCount) end -- Remove all occurances of a substring from a string. -- Return a string. function removeSubStrings(strInput, strToRemove) print("@removeSubStrings") print(" Inputs: ", strInput, strToRemove) strReturn = string.gsub(strInput, "strToRemove", "") print(" Returning ", strReturn) print(" ") return strReturn end -- Swap all occurances of a substring with a substring. Return a string. function substituteSubStrings(strInput, strToReplace, strReplaceWith) print("@substituteSubStrings") print(" Inputs: ", strInput, strToReplace, strReplaceWith) strReturn = string.gsub(strInput, "strToReplace", "strReplaceWith") print(" Returning", strReturn) return strReturn end -- Split any delimited string into SubStrings. -- Return a table. function split_StringAsTable(strInput, strSep) print("@split_StringAsTable") print(" Inputs: ",strInput, strSep) if strSep == nil then strSep = "%s" end local tblReturn = {} -- Build table of everything not i.e.(^) a seperator for str in string.gmatch(strInput, "([^"..strSep.."]+)") do table.insert(tblReturn, str) end print(" Returning ", tblReturn) print(" ") return tblReturn end -- Split a 'space' delimited string into SubStrings. -- Return a table and a String. function getSubStringsBySpace(strInput, flgTable) print("@getSubStringsBySpace") print(" Inputs: ",strInput) local tblReturn ={} -- insert everything that is not a space for str in string.gmatch(strInput, "%S+") do table.insert(tblReturn, str) end print(" Returning ", tblReturn, #tblReturn) print(" ") return tblReturn, #tblReturn end -- Get selected and unselected column Ids from the ColumnId SubStrings. -- Returns two strings followed by two tables. function getColumnIds(strInput, flgTable) -- User has flagged columns to ignore with an alpha character print("Entering function getColumnIds") print(" Inputs: ", strInput, flgTable) -- Strip any spaces out of the string strInput = string.gsub(strInput, "%s+", "") print(" Compressed Input string: ",strInput) print(" ") -- Get unselected column ids in a seperate string. Used to remap the columns in the output file local strColumnIdsToUse, numUnselectedColumnIds = getColumnIdsToUse(strInput, "|") --strColumnIdsToUse = strColumnIdsToUse .."|" -- Get selected column ids in a seperate string. Used for or validation purposes only local strColumnIdsToIgnore, numSelectedColumnIds = getColumnIdsToIgnore(strInput, "|") --strColumnIdsToIgnore = strColumnIdsToIgnore .."|" local tblColumnIdsToIgnore = {} local tblColumnIdsToUse = {} -- Convert strings to tables if flgTable == true then strSep = "|" -- UnselectedColumnIds, Insert everything that is not a "|" for str in string.gmatch(strColumnIdsToUse, "([^"..strSep.."]+)") do table.insert(tblColumnIdsToUse, str) end -- SelectedColumnIds, Insert everything that is not a "|" for str in string.gmatch(strColumnIdsToIgnore, "([^"..strSep.."]+)") do table.insert(tblColumnIdsToIgnore, str) end end -- Validate results -- Number of unselected columns should not exceed 7 i.e. X,Y,X,R,G,B,A --[[ -- Character in front of id for str in string.gmatch(strColumnIdsToIgnore, "%w%d") do -- Remove non numeric characters str = str:gsub("%D+", "") table.insert(tblReturn, str) end -- Character in back of id for str in string.gmatch(strColumnIdsToIgnore, "%d%w") do -- Remove non numeric characters str = str:gsub("%D+", "") table.insert(tblReturn, str) end -- Remove duplicates removeDuplicates(tblReturn) -- Sort, columns arrive in numerical order table.sort(tblReturn) --]] print("Check return values before exiting function") print(" ",numUnselectedColumnIds.. " column ids not selected by user thus retained for use: ", strColumnIdsToUse) print(" ",numSelectedColumnIds.. " column ids selected by user to be ignored: ", strColumnIdsToIgnore) print(" ") print(" ","Table of column id's to be used ") tprint(tblColumnIdsToUse) print(" ") print(" ","Table of column id's to be ignored ") tprint(tblColumnIdsToIgnore) print(" ") print(" Returning ", strColumnIdsToUse, strColumnIdsToIgnore, tblColumnIdsToUse, tblColumnIdsToIgnore) print(" ") print("Exiting function getColumnIds") print(" ") return strColumnIdsToUse, strColumnIdsToIgnore, tblColumnIdsToUse, tblColumnIdsToIgnore end -- A MAIN FUNCTION: load the selected file, analyse its content, and inform the user. function loadInputFile(strInputFilePath) local now = os.clock() print("---------------------------------------------------------------------------------") print("Entering function: loadInputFile") print("---------------------------------------------------------------------------------") print(" Inputs: ", strInputFilePath) --print(" ") -- Update status bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .07, text = "Loading Input File..."} octane.gui.updateStatus("Loading Input File...", .07) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() -- Get Paths and File Names strInputPath = octane.file.getParentDirectory(strInputFilePath) if txtOutputFileName.text ~="" then strInputFileName = octane.file.getFileNameWithoutExtension(txtOutputFileName.text) else strInputFileName = octane.file.getFileNameWithoutExtension(strInputFilePath) end strInputFileExtension = octane.file.getFileExtension(strInputFilePath) -- Set Paths and File Names -- Get Settings print(" Settings") print(" StepSize: ", numStepSize) print(" flgYup: ", flgYup) print(" ") -- Initial output file name, revised during processing based on user input strOutputPath = octane.file.getParentDirectory(strInputFilePath) strOutputFileName = "Scatter_"..[[_]]..strInputFileName ..".csv" strOutputFilePath = strInputPath..[[\]]..strOutputFileName -- Get bounding box, not used by script --local Xmin, Ymin, zmin, Xmax, Ymax, Zmax = getMaxMinXYZ(strInputFilePath) --print(Xmin, Ymin, zmin, Xmax, Ymax, Zmax) --print(" ") -- Get File info print(" File Statistics") print(" File Exists: ", file_Exists(strInputFilePath)) print(" File Name: ", strInputFileName) print(" ") -- Update status bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .15, text = "Analyzing lines from Input File..."} octane.gui.updateStatus("Analyzing lines from Input File...", .15) octane.gui.dispatchGuiEvents(15) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() local charCount, wordCount, lineCount = getFileStatistics(strInputFilePath) print(" Character Count: ", charCount) print(" Word Count: ", wordCount) print(" Line Count: ", lineCount) print(" ") -- Do prior to reformatting lineCount local numImageRows = math.floor(math.sqrt(lineCount)) local numImageColumns = math.floor(lineCount/numImageRows) local imageSize = {numImageColumns, numImageRows} numBitMapDimension = math.floor(math.sqrt(lineCount)) --lblInstanceColorMap.text = "Instance Color Map ("..numBitMapDimension .." x ".. numBitMapDimension .." pixels" .." - dimensions derived from source file)" lblInstanceColorMap.text = "Instance Color Map ("..numImageColumns .." x ".. numImageRows .." pixels" .." - dimensions derived from source file)" -- Reformat integer strings to include commas charCount = format_Integer(charCount) wordCount = format_Integer(wordCount) lineCount = format_Integer(lineCount) print(" Character Count: ", charCount) print(" Word Count: ", wordCount) print(" Line Count: ", lineCount) print(" ") -- Do again after reformatting used in naming Node Graph numBodyLines = lineCount print(" Instance Color Texture dimensions: ", numImageColumns.." x "..numImageRows) print(" ") lblInputFileExtension.text = " File Type: "..strInputFileExtension lblInputFileDescription.text = (" File Contents: Line Count = "..lineCount..[[ ]].."Word Count = ".. wordCount..[[ ]].."Character Count = "..charCount) print(" File content description for UI:", lblInputFileDescription.text) print(" ") -- Get the existing input File; not actually needed, so don't --fhInputFile = assert( io.input(strInputFilePath) ) -- Create new output file for Scatter Node Lines later -- fhOutputFile = assert( io.output( strOutputFilePath ) ) --print(" Input File Path = ", strInputFilePath) --print(" Input File handle: ",fhInputFile) --print(" ") -- Set immediately prior to use in processFile function --print("Output File Path = ", strOutputFilePath) --print(fhOutputFile) --print(" ") -- Get Line info print(" Line Statistics ------------------------------------------------------------------------------------------------------------------") print(" ") local count = 0 -- Check for a Header, test first 'value of numGetHeaderLines' lines in file tblBOF = getFileLinesAsTable(strInputFilePath, numGetHeaderLines) print(" The Top " .. numGetHeaderLines .. " Lines of: ", strInputFilePath) print(" ") tprint(tblBOF) print(" ") --[[ --Alternative Implementation, superceeded ------------------------------------ -- Get the first 20 lines of the file to show user, alternative to function call --for line in io.lines(file) do --count = count + 1 --if count > 20 then break end --table.insert(tblLines, line) --end -- Setup multiline format -- Works with non multiline textbox --[[ --e1 --e2 --e3 --e4 --e5 --e6 --e7 --e8 --e9 --]] --local strLineEntityList = "" --for i = 1, #tblBOF do -- Substitute multiline placeholder entries with actual values. --strLineEntityList = strLineEntityList:gsub("e"..i,tbHeader[i]) --end --]] ----------------------------------------------------------------------------- local strLineEntityList = "" for i = 1, #tblBOF do strLineEntityList = strLineEntityList .."line "..string.format("%03d",i)..": ".. tblBOF[i] .. "\n" end lblFileContent.text = "Beginning of File: First " .. #tblBOF .. " lines." print(" ") print(" Line entity list - content for populating the beginning of file (BOF) textbox.") print(" ") print("", strLineEntityList) print(" ") -- Update the UI, populate textbox txtFileContent.text = strLineEntityList -- Check if data is valid for header line identification. -- Spilt the last line in the BOF list and check it. tblSampleLineSubStrings, numLineSubStrings = getSubStringsBySpace(tblBOF[#tblBOF]) print(" Sample Substring Statistics -------------------------------------------------------------------------------------") --print(" ") print (" Representative Sample Data taken from line " ..#tblBOF) print(" Number of substrings i.e. values, in line " ..#tblBOF .." =", numLineSubStrings) print(" Table of substrings found in line " ..#tblBOF) tprint(tblSampleLineSubStrings) print(" ") print(" ") print(" Length of BOF table =",#tblBOF) print(" ") print(" Begin Data Deficiency and Numeric testing on all lines in the BOF table..") print(" Looping through BOF table looking for lines that do not contain enough values.") -- Check that there are at least 3 values in a Line record, as min valid data is a numeric x, y & z. -- Find the number of lines with insufficient data, count them as potential Header lines. for i = 1, #tblBOF do local tblBOF_SubStrings, numHeaderSubStrings = getSubStringsBySpace(tblBOF[i]) local flgIsDirty = false print(" Deficiency Test Results for line " ..i.. " - the number of substrings found in Line "..i.." =", numHeaderSubStrings) print(" Table of substrings found in line " ..i.. " of BOF table:") tprint(tblBOF_SubStrings) -- Make sure there are at least 3 substrings, one for each number. if numHeaderSubStrings < 3 and strInputFileExtension ~= ".cvs" then print(" Data is deficient @ Line "..i) flgHasHeader = true numDataDeficientLines = numDataDeficientLines + 1 numLastHeaderLineid = i flgIsDirty = true end print(" ") if flgIsDirty == false then print(" Line "..i.." passed the Deficiency Test.") end print(" Total number of lines with insufficient data found so far =", numDataDeficientLines) print(" ") end print(" End Deficiency testing.-------------------------------------------------------------------------------") print(" ") print(" Begin Numeric testing.--------------------------------------------------------------------------------") print(" ") print(" ") -- Find the number of non mumeric lines, count them as potential Header lines. for i = 1, #tblBOF do local tblBOF_SubStrings, numHeaderSubStrings = getSubStringsBySpace(tblBOF[i]) local flgIsDirty = false print(" Numeric Test for line " ..i.. " - number of substrings in Line "..i.." =", numHeaderSubStrings) print(" Table of substrings found in line " ..i.. " of BOF table:") tprint(tblBOF_SubStrings) -- If false, check line for non numeric substrings indicative of a Header. for k = 1, numHeaderSubStrings do if tonumber(tblBOF_SubStrings[k]) == nil then print(" Data is not numeric @ Line "..i) flgHasHeader = true numHeaderLines = numHeaderLines + 1 numLastHeaderLineid = i flgIsDirty = true do break end end end if flgIsDirty == false then print(" Line "..i.." passed the Numeric Test.") end print(" Total number of lines with non numeric data found so far =", numHeaderLines) print(" ") end print(" End of numeric testing. ------------------------------------------------------------------------------") print(" ") -- Keep for debugging --if numDataDeficientLines > 0 then --showWarning("Insufficient data found in ".. numDataDeficientLines .." Line(s) of the file's first "..#tblBOF ,"Please validate file's suitability for Octane Scatter, \nthe script will continue.") --end -- Keep for debugging --if numHeaderLines > 0 then --showWarning("Non point data found in ".. numHeaderLines .." Line(s) of the file's first "..#tblBOF ,"Please validate file's suitability for Octane Scatter, \nthe script will continue.") --end local numHeaderLength = numDataDeficientLines + numHeaderLines print(" End of testing for deficient and non Numeric data. " .. numLastHeaderLineid .. " possible Header Line(s) identified.") print(" The line number of last 'Header Record' identified within the defined BOF = " .. numLastHeaderLineid) print(" The length of a line of 'typical' body text =", numLineSubStrings) print(" Preparing results for display in GUI") print(" ") if numLastHeaderLineid == 0 then lblFileContent.text = "Beginning of File: First " ..#tblBOF.. " lines of " ..lineCount.. ". " ..numLastHeaderLineid.. " Header Lines identified." else lblFileContent.text = "Beginning of File: First " ..#tblBOF.. " lines of " ..lineCount.. ". " ..numLastHeaderLineid.. " Header Line(s) identified, ending at line " ..string.format("%03d",numLastHeaderLineid).. "." end -- Inform user based on line contents. -- numLineSubStrings with values 3 thru 7 can be processed, -- 1,12,13 are possible cvs files already written by this script. numLastHeaderLineid with -- values > 0 mean a head was detected, the script will backup the input file to -- and attempt to strip the header saving it in . if numLineSubStrings == 3 then lblInputLineDescription.text = ("Line Contents: Lines contain "..numLineSubStrings.." entries. Script is assuming these are point ordinates only") print(" lblInputLineDescription.text:", lblInputLineDescription.text) if chkPointsIdsAndColors.checked == true then chkPointsAndIds.checked = true chkPointsIdsAndColors.checked = false end if chkPointsAndIds.checked == true then chkPointsIdsAndColors.checked = false chkPointsOnly.checked = false end if chkPointsOnly.checked == true then chkPointsIdsAndColors.checked = false chkPointsAndIds.checked = false end elseif numLineSubStrings == 6 then lblInputLineDescription.text = ("Line Contents: Lines contain "..numLineSubStrings.." entries. Script is assuming these are point ordinates & RGB colors") print(" lblInputLineDescription.text:", lblInputLineDescription.text) if chkPointsIdsAndColors.checked == true then chkPointsAndIds.checked = false chkPointsOnly.checked = false end if chkPointsAndIds.checked == true then chkPointsIdsAndColors.checked = false chkPointsOnly.checked = false end if chkPointsOnly.checked == true then chkPointsIdsAndColors.checked = false chkPointsAndIds.checked = false end elseif numLineSubStrings == 7 then lblInputLineDescription.text = ("Line Contents: Lines contain "..numLineSubStrings.." entries. Script is assuming these are point ordinates & RGBA colors") print(" lblInputLineDescription.text:", lblInputLineDescription.text) if chkPointsIdsAndColors.checked == true then chkPointsAndIds.checked = false chkPointsOnly.checked = false end if chkPointsAndIds.checked == true then chkPointsIdsAndColors.checked = false chkPointsOnly.checked = false end if chkPointsOnly.checked == true then chkPointsIdsAndColors.checked = false chkPointsAndIds.checked = false end elseif numLineSubStrings == 1 and strInputFileExtension == ".cvs" then -- Only true if comma seperated lblInputLineDescription.text = ("Line Contents: Lines contain "..numLineSubStrings.." entry. Script is assuming file has already been processed.") print(" lblInputLineDescription.text:", lblInputLineDescription.text) showWarning("File to appears to already be of a suitable format.","Please validate for Octane Scatter, \\nnthe script will now terminate.") wndMain:closeWindow() flgRunning = false if not flgRunning then return flgRunning end elseif numLineSubStrings == 12 and strInputFileExtension == ".cvs" then -- Only true if space seperated and no Id lblInputLineDescription.text = ("Line Contents: Lines contain "..numLineSubStrings.." entries. Script is assuming file has already been processed, please review.") print(" lblInputLineDescription.text:", lblInputLineDescription.text) showWarning("File to appears to be of a suitable format.","Please validate for Octane Scatter, \\nnthe script will now terminate.") wndMain:closeWindow() flgRunning = false if not flgRunning then return flgRunning end elseif numLineSubStrings == 13 and strInputFileExtension == ".cvs" then -- Only true if space seperated and with Id lblInputLineDescription.text = ("Line Contents: Lines contain "..numLineSubStrings.." entries. Script is assuming file has already been processed, please review.") print(" lblInputLineDescription.text:", lblInputLineDescription.text) showWarning("File already appears to be of a suitable format.","Please validate its suitability for Octane Scatter, \\nnthe script will now terminate.") wndMain:closeWindow() flgRunning = false if not flgRunning then return flgRunning end else lblInputLineDescription.text = ("Line Contents: Lines contain " ..numLineSubStrings.." entries. Unanticipated data") print(" lblInputLineDescription.text:", lblInputLineDescription.text) end -- Populate column selector txtColumnSelector.text = "" for i = 1, numLineSubStrings do txtColumnSelector.text = txtColumnSelector.text.. "| " ..i.. "" end txtColumnSelector.text = txtColumnSelector.text.. "|" txtOutputFilesPath.text = strOutputPath print(" ") local strFileSize = octane.file.getFileSize(strInputFilePath) strFileSize = format_Integer(strFileSize) -- Calculate total time elapsed (tte) local tte = os.clock() - now local strElapsedTime = (string.format("%.3f seconds", tte)) print("File processing complete: Input File was opened and analysed in:", strElapsedTime) -- Update status bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .25, text = "Analysed "..lineCount.. " lines, completed in " ..strElapsedTime} octane.gui.updateStatus("Analysis Complete", .25) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() -- Warnings -- Header found, isufficient lines were tested to find end of header, need to rerun if numLastHeaderLineid >= #tblBOF then -- Need to bail as insufficient numer of lines were tested, if more header lines are axtually present script will fail print(" The number of lines containing possible Header material equals or exceeds the number of lines tested.") print(" The length of the BOF needs to be increased and tested again.") showWarning("The " ..numLastHeaderLineid.. " Header Lines identified, equal or exceed the "..#tblBOF.. " Lines tested.","You should increase the length of the BOF by editing this script and re import. \n\nThe script will return to the main form.") return -- To GUI callback for btnFileOpen end -- Possible Header found, need to strip file of header lines. if numLastHeaderLineid > 0 then print(" Possible " ..numLastHeaderLineid.. "lines of Header found") lblInputLineDescription.text = ("Line Contents: Non Header lines contain " ..numLineSubStrings.." entries. Header lines detected.") print(" lblInputLineDescription.text:", lblInputLineDescription.text) btnProcessInputFile:updateProperties{ enable = false } chkCreateOctaneNodes:updateProperties{ enable = false } showWarning(numLastHeaderLineid.." Header Line(s) detected.","The script will attempt to remove these lines during processing, or you can exit and edit the input file manually. \n\nThe script will return to the main form.") end -- The number of values found within a line is out of range. if numLineSubStrings < 3 or numLineSubStrings > 7 then print(" Number of values found in a line of 'typical' body text is out of range.") showWarning("The number of values in each non header line are outside the permitted range.","A minimum of 3 values, (3 X,Y,Z points) are required and a maximum of 7 are permitted, (3 X,Y,Z points followed by 4 R,G,B,A pixel channels) in the order shown here. Use the Column Selector to ignore unusable data. \n\nThe script will return to the main form.") return -- To GUI callback for btnFileOpen end print("---------------------------------------------------------------------------------") print("Exiting Function: loadInputFile") print("---------------------------------------------------------------------------------") print(" ") print(" ") ::EndInputLoadFile:: end -- loadInputFile -- A MAIN FUNCTION: generate a new output file for an Octane Scatter Node based on user input -- and optionally generate an image file for an Instance Color Texture Node. function processInputFile() local now = os.clock() print("---------------------------------------------------------------------------------") print("Entering function: processFile") print("---------------------------------------------------------------------------------") print(" ") -- Update status bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .30, text ="Preparing data for write"} octane.gui.updateStatus("Preparing data for write", .30) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() -- numStepSize must be an integer to be suitable to -- use as an increment, getting back a decimal from the nbx numStepSize = string.gsub(numStepSize,"%.?0+$", "") numStepSize = tonumber(numStepSize) -- Build a string to append to the output file name based on user's choices local strFilePrefix = nil if numStepSize >= 1 and flgYup == true then strFilePrefix = " - ( " ..100/numStepSize..("%") .. " Scatter - Y up )" numPercentOfOriginal = 100/numStepSize..("%") elseif numStepSize >= 1 and flgYup == false then strFilePrefix = " - ( " ..100/numStepSize..("%") .. " Scatter - Z up )" numPercentOfOriginal = 100/numStepSize..("%") end -- Revise the output file name, based based on user's choices strOutputPath = octane.file.getParentDirectory(strInputFilePath) strOutputFileName = strInputFileName ..strFilePrefix.. ".csv" strOutputFilePath = strInputPath ..[[\]].. strOutputFileName -- Validate data based on user's choices local flgPassed, numLineSubStrings, numWarningId = validateDataForProcessing() print("flgPassed =", flgPassed) print("numLineSubStrings =", numLineSubStrings) print("numWarningId ", numWarningId) print(" ") -- Process the return values from the valadate function if numWarningId == 1 then return numWarningId end -- Exit function if numWarningId == 3 then goto continue end if numWarningId == 4 then return numWarningId end -- Exit function if numWarningId == 5 then return numWarningId end -- Exit function ::continue:: print("Beginning column remapping process") print(" ") -- Get the text string of the user's edits local strColumnsToProcess = txtColumnSelector.text -- Get the column Ids's of the unselected and selected columns local strColumnIdsToUse, strColumnIdsToIgnore, tblColumnIdsToUse, tblColumnIdsToIgnore = getColumnIds(strColumnsToProcess, true) -- Get back the list of 'columns to keep' and map it to k1 thru k7 print(" Retrieved Column Id's formatted in both strings and tables for use in remapping indicies:") print(" strColumnIdsToUse ",strColumnIdsToUse) print(" strColumnIdsToIgnore ",strColumnIdsToIgnore) print(" ") print(" Table tblColumnIdsToUse") tprint(tblColumnIdsToUse,"IdToUse") print(" ") print(" Table tblColumnIdsToIgnore") tprint(tblColumnIdsToIgnore, "IdToIgnore") print(" ") print(" Remapping indicies") print(" flgYup =", flgYup, ", if true indicies must be remapped.") print(" New column indicies:") -- Remap table entry keys for tblLineSubStrings, to exclude columns -- These keys are used by createInstanceColorMap, so needed to be global variables if flgYup == false then k1 = tonumber(tblColumnIdsToUse[1]) print(" k1 =",k1) k2 = tonumber(tblColumnIdsToUse[2]) print(" k2 =",k2) k3 = tonumber(tblColumnIdsToUse[3]) print(" k3 =",k3) k4 = tonumber(tblColumnIdsToUse[4]) print(" k4 =",k4) k5 = tonumber(tblColumnIdsToUse[5]) print(" k5 =",k5) k6 = tonumber(tblColumnIdsToUse[6]) print(" k6 =",k6) k7 = tonumber(tblColumnIdsToUse[7]) print(" k7 =",k7) else --flg true k1 = tonumber(tblColumnIdsToUse[1]) print(" k1 =",k1) k2 = tonumber(tblColumnIdsToUse[3]) --Swap if Y up print(" k2 =",k2) k3 = tonumber(tblColumnIdsToUse[2]) --Swap if Y up print(" k3 =",k3) k4 = tonumber(tblColumnIdsToUse[4]) print(" k4 =",k4) k5 = tonumber(tblColumnIdsToUse[5]) print(" k5 =",k5) k6 = tonumber(tblColumnIdsToUse[6]) print(" k6 =",k6) k7 = tonumber(tblColumnIdsToUse[7]) print(" k7 =",k7) end print(" ") print(" Number of substrings in a line =", numLineSubStrings) print(" ") print("Ending column remapping process") print(" ") -- End of common processing. ----------------------------------------------------------------------------- -- Begining processing based on user's selected output file format -- Create new output file for Scatter Node Lines fhOutputFile = assert( io.output(strOutputFilePath)) print("Output file: ", fhOutputFile, "opened for write as", strOutputFilePath) print(" ") -- Generate output file and optionally an Instance Color Texture image. -- Write a space or comma seperated .csv file suitable for input into an Octane scatter node -- from an input file that contains only the three cordinates for each point. if chkPointsOnly.checked == true then print("@chkPointsOnly.checked") print(" Begin process of writing matricies formatted for an Octane Scatter Node without an id value.") print(" ") print(" Settings for file of points only Point") print(" Input file path = ", strInputFilePath) print(" Number of subStrings in body line = ",numLineSubStrings) print(" File's line number of the last Header line detected = " , numLastHeaderLineid) print(" flgCommaDelimited ", flgCommaDelimited ) print(" flgYup ", flgYup ) print(" numStepSize ", numStepSize) print(" numPercentOfOriginal ", numPercentOfOriginal ) print(" ") -- Update status bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .45, text = "Writing Scatter Node transformation matrices without id"} octane.gui.updateStatus("Writing Scatter Node transformation matrices without id", .45) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() flgColor = false lblInstanceColorMap.text = "Selected output format does not include color data." -- Limit to data that only contains point entries since this is what the user specified if numLineSubStrings == 3 then -- Add header to file without an instance ID column io.write("M00, M01, M02, M03, M10, M11, M12, M13, M20, M21, M22, M23", '\n') -- Loop thru the file reformatting each line for an Octane Scatter Node -- Preparing input file if headers need to be remove, could/should be moved to commom section of function print(" Begin file preparation, header removal and point thinning as requested") if numLastHeaderLineid <= 0 then print(" No Header records were found, header removal process will be skipped") end if numLastHeaderLineid > 0 then -- This function backs up the original file and -- writes a new headerless file to original file name print(" Number of last Header Line is > 0") print(" Removing Header: ", strInputFilePath) removeLinesFromFile(strInputFilePath, numLastHeaderLineid) print(" Header removed: ", strInputFilePath) end -- Preparing input file if thinning was specified if numStepSize <= 1 then print(" File thinning was not specified, thinning process will be skipped") end if numStepSize > 1 then -- This function backs up the original file and -- writes a new thinned file to original file name print(" Number of steps specified is > 0") print(" Thinning file: ", strInputFilePath) thinFile(strInputFilePath, numStepSize) print(" File thinned: ", strInputFilePath ..to.. numPercentOfOriginal) end print(" End file preparation") print(" ") octane.gui.dispatchGuiEvents(200) print(" Writing the file...") -- Loop thru the file reformatting each line for an Octane Scatter Node including an Id local count = 0 -- Write line with specified value seperator if flgCommaDelimited == false then -- Space seperated for line in io.lines(strInputFilePath), numStepSize do count = count+1 local tblLineSubStrings ={} for str in string.gmatch(line, "%S+") do table.insert(tblLineSubStrings, str) end -- Space seperated strNewLine = "1 0 0 " .. tblLineSubStrings[k1] .. " 0 1 0 " .. tblLineSubStrings[k2] .." 0 0 1 " .. tblLineSubStrings[k3] --.. " ".. count io.write(strNewLine, '\n') end else -- Comma seperated for line in io.lines(strInputFilePath), numStepSize do count = count+1 local tblLineSubStrings ={} for str in string.gmatch(line, "%S+") do table.insert(tblLineSubStrings, str) end -- Comma seperated strNewLine = "1,0,0," ..tblLineSubStrings[k1].. ",0,1,0," ..tblLineSubStrings[k2]..",0,0,1," ..tblLineSubStrings[k3] --.. ",".. count io.write(strNewLine, '\n') end end -- Number of lines actually output since some may have been headerlines which were removed. print(" Writing complete") numBodyLines = count print(" numBodyLines ", numBodyLines) print(" length pixel table =", #tblAllPixels) io.close(fhOutputFile) print(" ") print(".cvs file closed, contains points only.") print(" ") else -- data mismatch warning end if chkCreateOctaneNodes.checked == true then createPointCloudNodes(flgColor) end end -- btnProcessInputFile -- Write a space seperated .csv file suitable for input into an Octane scatter node -- from an input file that contains only the three cordinates for each point, -- appending an instance id to each point record. if chkPointsAndIds.checked == true then print("@chkPointsAndIds.checked") print(" Begin process of writing matricies formatted for an Octane Scatter Node with an appended id value.") print(" ") print(" Settings for file of points with an Id") print(" Input file path = ", strInputFilePath) print(" Number of subStrings in body line = ",numLineSubStrings) print(" File's line number of the last Header line detected = " , numLastHeaderLineid) print(" flgCommaDelimited ", flgCommaDelimited ) print(" flgYup ", flgYup ) print(" numStepSize ", numStepSize) print(" numPercentOfOriginal ", numPercentOfOriginal ) print(" ") -- Update status bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .45, text = "Writing Scatter Node transformation matrices with id"} octane.gui.updateStatus("Writing Scatter Node transformation matrices with id", .45) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() flgColor = false lblInstanceColorMap.text = "Selected output format does not include color data." -- Limit to data that only contains point entries if numLineSubStrings == 3 then -- Add header to file with an instance ID column io.write("M00, M01, M02, M03, M10, M11, M12, M13, M20, M21, M22, M23, ID", '\n') -- Loop thru the file reformatting each line for an Octane Scatter Node including an Id print("Number of last Header Line =", numLastHeaderLineid) print("Input file path = ", strInputFilePath) print(" ") -- Preparing input file if headers need to be remove print(" Begin file preparation") if numLastHeaderLineid > 0 then -- This function backs up the original file and -- writes a new headerless file to original file name print(" Removing Header: ", strInputFilePath) removeLinesFromFile(strInputFilePath, numLastHeaderLineid) print(" Header removed: ", strInputFilePath) end -- Preparing input file if thinng was specified if numStepSize > 1 then -- This function backs up the original file and -- writes a new thinned file to original file name print(" Thinning file: ", strInputFilePath) thinFile(strInputFilePath, numStepSize) print(" File thinned:", strInputFilePath ..to.. numPercentOfOriginal) end print(" End file preparation") -- Loop thru the file reformatting each line for an Octane Scatter Node including an Id local count = 0 -- Write line with specified value seperator if flgCommaDelimited == false then -- Space seperated for line in io.lines(strInputFilePath), numStepSize do count = count+1 local tblLineSubStrings ={} for str in string.gmatch(line, "%S+") do table.insert(tblLineSubStrings, str) end -- Space seperated strNewLine = "1 0 0 " .. tblLineSubStrings[k1] .. " 0 1 0 " .. tblLineSubStrings[k2] .." 0 0 1 " .. tblLineSubStrings[k3] .. " ".. count io.write(strNewLine, '\n') end numBodyLines = count print(" numBodyLines ", numBodyLines) print(" length pixel table =", #tblAllPixels) else -- Comma seperated for line in io.lines(strInputFilePath), numStepSize do count = count+1 local tblLineSubStrings ={} for str in string.gmatch(line, "%S+") do table.insert(tblLineSubStrings, str) end -- Comma seperated strNewLine = "1,0,0," ..tblLineSubStrings[k1].. ",0,1,0," ..tblLineSubStrings[k2]..",0,0,1," ..tblLineSubStrings[k3].. ",".. count io.write(strNewLine, '\n') end end print(" Writing complete") numBodyLines = count print(" numBodyLines ", numBodyLines) print(" length pixel table =", #tblAllPixels) io.close(fhOutputFile) print(".cvs file closed, containts points with a unique id for each.") else -- data mismatch warning end if chkCreateOctaneNodes.checked == true then createPointCloudNodes(flgColor) end end -- chkPointsAndIds.checked -- Write a space seperated .csv file suitable for input into an Octane scatter node -- from an input file that contains the three cordinates for each point and -- it's rgb color data, appending an instance id to each point record and -- creating a seperate Instance Color Texture image to store the colors. if chkPointsIdsAndColors.checked == true then print("@chkPointsIdsAndColors.checked") print(" ") print("Writing matricies with id formatted for Octane Scatter Node") print("Settings for Points with Id's and color map file write") print(" Input file path = ", strInputFilePath) print(" Number of subStrings in line =",numLineSubStrings) print(" Number of last Header Line in file is", numLastHeaderLineid) print(" flgCommaDelimited ", flgCommaDelimited ) print(" flgYup ", flgYup ) print(" numStepSize ", numStepSize) print(" numPercentOfOriginal ", numPercentOfOriginal ) print(" ") -- Update status bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .45, text = "Preparing data for write"} octane.gui.updateStatus("Preparing data for write", .45) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() flgColor = true lblInstanceColorMap.text = "Instance Color Map ("..numImageColumns .." x ".. numImageRows .." pixels" .." - dimensions derived from source file)" -- Limit to data that contains point and RGB color entries if numLineSubStrings >= 6 then --or numLineSubStrings == 7 then -- Add Scatter Node header to file with an instance ID column. io.write("M00 M01 M02 M03 M10 M11 M12 M13 M20 M21 M22 M23 ID", '\n') -- Loop thru the file reformatting each line for an Octane Scatter Node including an Id -- Note, color data is not included in the output, but is stored in a table for latter image creation -- Preparing input file if headers need to be remove -- Called 3x - move to its own function -- this involves header remove, file thinning and remaping indices for spatial transformation (Y up) print(" Begin file preparation...") if numLastHeaderLineid <= 0 then print(" No Header records were found, header removal process will be skipped") end if numLastHeaderLineid > 0 then -- This function backs up the original file appending the prefix of "Orig_" and -- writes a new headerless file to original file name, so a reload should not be required print(" Removing Header from: ", strInputFilePath) rv1 = removeLinesFromFile(strInputFilePath, numLastHeaderLineid) print(" Header removed and file saved as: ", strInputFilePath) end if numStepSize <= 1 then print(" File thinning was not specified, thinning process will be skipped") end -- Preparing input file if thinning was specified if numStepSize > 1 then -- This function backs up the original file and -- writes a new thinned file with a new file name, so the new input file needs to reloaded print(" Thinning file: ", strInputFilePath) rv1, strInputFilePath = thinFile(strInputFilePath, numStepSize) print(" Input file thinned and renamed to", strInputFilePath) end print(" End file preparation:") print(" ") --[[ Start Data test ------------------------------------------------------------ -- Keep for debugging additional supported file formats in the future -- Used for testing a few lines of body text, instead of mil+ in whole file. print("Count =", count) print("Values of K before loop:") print("k1 =",k1) print("k2 =",k2) print("k3 =",k3) print("k4 =",k4) print("k5 =",k5) print("k6 =",k6) print("k7 =",k7) print(" ") for line in io.lines(strInputFilePath) do count = count + 1 if count > 1 then break end local tblLineSubStrings ={} local strNewLine = "" local tblLinePixel = {} -- Get the line substrings, things that are not spaces for str in string.gmatch(line, "%S+") do print(line) print(str) print(" ") table.insert(tblLineSubStrings, str) end print("Count =", count) print("Table of substrings retrieved from line") tprint(tblLineSubStrings) print(" ") print("Count =", count) print("Values of K in loop:") print("k1 =",k1) print("k2 =",k2) print("k3 =",k3) print("k4 =",k4) print("k5 =",k5) print("k6 =",k6) print("k7 =",k7) print(" ") print("Count =", count) print("by index") print(tblLineSubStrings[k1]) print(tblLineSubStrings[k2]) print(tblLineSubStrings[k3]) print(tblLineSubStrings[k4]) print(tblLineSubStrings[k5]) print(tblLineSubStrings[k6]) print(tblLineSubStrings[k7]) print(" ") --print("Count =", count) --print("hard coded") --print(tblLineSubStrings[1]) --print(tblLineSubStrings[2]) --print(tblLineSubStrings[3]) --print(tblLineSubStrings[7]) --print(tblLineSubStrings[8]) --print(tblLineSubStrings[9]) --print(tblLineSubStrings[10]) --print(" ") -- Space seperated strNewLine = "1 0 0 " .. tblLineSubStrings[k1] .. " 0 1 0 " .. tblLineSubStrings[k2] .." 0 0 1 " .. tblLineSubStrings[k3] .. " ".. count print(strNewLine) print(" ") stop() end -- End Data test ---------------------------------------------------------- --]] -- Update status bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .65, text = "Writing Scatter Node transformation matrices with id"} octane.gui.updateStatus("Writing Scatter Node transformation matrices with id", .65) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() print(" Writing the file...") -- Write line with specified value seperator if flgCommaDelimited == false then -- i.e. Space seperated if flgYup == false then print("Output will be space delimited, Z up") print("Begin Looping") print("...") local count = 0 for line in io.lines(strInputFilePath), numStepSize do local tblLineSubStrings ={} for str in string.gmatch(line, "%S+") do table.insert(tblLineSubStrings, str) end -- Space seperated, flgYup is false strNewLine = "1 0 0 " .. tblLineSubStrings[k1] .. " 0 1 0 " .. tblLineSubStrings[k2] .." 0 0 1 " .. tblLineSubStrings[k3] .. " ".. count io.write(strNewLine, '\n') -- Process the color data for subsequent use. -- Build a table of RGBA pixels, where each RGBA entry is a table containing a single pixel's channels --if tblLineSubStrings[k7] == nil then tblLineSubStrings[k7] = 255 end tblLinePixel = {tblLineSubStrings[k4], tblLineSubStrings[k5], tblLineSubStrings[k6], 255} --tblLinePixel = {tblLineSubStrings[k4], tblLineSubStrings[k5], tblLineSubStrings[k6], tblLineSubStrings[k7]} table.insert(tblAllPixels, tblLinePixel) count = count+1 end numBodyLines = count print(" numBodyLines ", numBodyLines) print(" length pixel table =", #tblAllPixels) else -- flgYup = true -- Debug check the Yup index swap --print(" k1 =",k1) --print(" k2 =",k2) --Swap --print(" k3 =",k3) --Swap --print(" k4 =",k4) --print(" k5 =",k5) --print(" k6 =",k6) --print(" k7 =",k7) print("Output will be space delimited, y up") print("Begin Looping") print("...") local count = 0 for line in io.lines(strInputFilePath), numStepSize do local tblLineSubStrings ={} for str in string.gmatch(line, "%S+") do table.insert(tblLineSubStrings, str) end -- Space seperated, flgYup is true -- If y is up we need to multipy Z ordinate by -1 strNewLine = "1 0 0 " .. tblLineSubStrings[k1] .. " 0 1 0 " .. tblLineSubStrings[k2] .." 0 0 1 " .. -1 * tblLineSubStrings[k3] .. " ".. count io.write(strNewLine, '\n') -- Process the color data for subsequent use. -- Build a table of RGBA pixels, where each RGBA entry is a table containing a single pixel's channels --if tblLineSubStrings[k7] == nil then tblLineSubStrings[k7] = 255 end tblLinePixel = {tblLineSubStrings[k4], tblLineSubStrings[k5], tblLineSubStrings[k6], 255} --tblLinePixel = {tblLineSubStrings[k4], tblLineSubStrings[k5], tblLineSubStrings[k6], tblLineSubStrings[k7]} table.insert(tblAllPixels, tblLinePixel) count = count+1 end numBodyLines = count print(" numBodyLines ", numBodyLines) print(" length pixel table =", #tblAllPixels) end -- flgYup else -- flgCommaDelimited = true if flgYup == false then print("Output will be comma delimited, Z up") print("Begin Looping") print("...") local count = 0 local pbrCount = 0 for i = 1, #tblAllPixels, #tblAllPixels/10 do pbrCount = pbrCount + #tblAllPixels/10 for line in io.lines(strInputFilePath), numStepSize do local tblLineSubStrings ={} for str in string.gmatch(line, "%S+") do table.insert(tblLineSubStrings, str) end -- Comma seperated, flgYup is false strNewLine = "1,0,0," ..tblLineSubStrings[k1].. ",0,1,0," ..tblLineSubStrings[k2]..",0,0,1," ..tblLineSubStrings[k3].. ",".. count io.write(strNewLine, '\n') tblLinePixel = {tblLineSubStrings[k4], tblLineSubStrings[k5], tblLineSubStrings[k6], 255} table.insert(tblAllPixels, tblLinePixel) count = count+1 end local inc = .45 + .3*(#tblAllPixels/pbrCount) octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = inc, text = "Instance Color Map Created"} octane.gui.updateStatus("Instance Color Map Created", inc) octane.changemanager.update() octane.gui.dispatchGuiEvents(100) end numBodyLines = count print("numBodyLines ", numBodyLines) print("length pixel table =", #tblAllPixels) else -- flgYup = true print("Output will be comma delimited, Y up") print("Begin Looping") print("...") local count = 0 --local cnt = 0 --print("Begin inner test Loop") --print(numStepSize) --for line = 1, numBodyLines, numStepSize do --cnt = cnt+1 --print(cnt) --end -- Increment must be an integer for line in io.lines(strInputFilePath), numStepSize do local tblLineSubStrings = {} for str in string.gmatch(line, "%S+") do table.insert(tblLineSubStrings, str) end -- Comma seperated, flgYup is true --strNewLine = "1,0,0," ..tblLineSubStrings[k1].. ",0,1,0," ..tblLineSubStrings[k2]..",0,0,1," ..tblLineSubStrings[k3].. ",".. count -- If y is up we need to multipy Z ordinate by -1 strNewLine = "1,0,0," .. tblLineSubStrings[k1] .. ",0,1,0," .. tblLineSubStrings[k2] ..",0,0,1," .. -1 * tblLineSubStrings[k3] .. ",".. count io.write(strNewLine, '\n') -- Process the color data for subsequent use. -- Build a table of RGBA pixels, where each RGBA entry is a table containing a single pixel's channel values --if flgIgnoreAlpha = true then tblLinePixel = {tblLineSubStrings[k4], tblLineSubStrings[k5], tblLineSubStrings[k6], 255} --else --k7 == nil then k7 = 7 --tblLinePixel = {tblLineSubStrings[k4], tblLineSubStrings[k5], tblLineSubStrings[k6], tblLineSubStrings[k7]} --end table.insert(tblAllPixels, tblLinePixel) count = count+1 end numBodyLines = count print(" numBodyLines ", numBodyLines) print(" length pixel table =", #tblAllPixels) end -- flgYup end -- flgCommaDelimited io.close(fhOutputFile) print("End Looping") print("Length of pixel table is: "..#tblAllPixels.. " for image of " ..numImageColumns.. " x " ..numImageRows.. " pixels") print(" ") print("Write complete.") print(" ") octane.gui.dispatchGuiEvents(100) btnProcessInputFile:updateProperties{ enable = false } chkCreateOctaneNodes:updateProperties{ enable = false } pbrloadInputFile:updateProperties{progress = .75, text = "File write complete"} octane.gui.updateStatus("File write complete", .75) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() else -- data mismatch warning end -- numLineSubStrings txtOutputFilesPath.text = strOutputPath print("Creating Instance Color Texture") createInstanceColorMap(tblAllPixels) octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .90, text = "Instance Color Map Created"} octane.gui.updateStatus("Instance Color Map Created", .90) octane.changemanager.update() octane.gui.dispatchGuiEvents(100) if chkCreateOctaneNodes.checked == true then createPointCloudNodes(flgColor) end end -- chkPointsIdsAndColors.checked local strFileSize = octane.file.getFileSize(strInputFilePath) strFileSize = format_Integer(strFileSize) local tte = os.clock() - now local strElapsedTime = (string.format("%.3f seconds", tte)) print("Processing Complete: File processed in:", strElapsedTime) octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = 1, text = "Processed "..strFileSize.. " bytes, completed in " ..strElapsedTime} octane.gui.updateStatus("Point Cloud Node Graph Created, Processing Complete.", 1) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() print("---------------------------------------------------------------------------------") print("Exiting function: processFile") print("---------------------------------------------------------------------------------") print(" ") print(" ") ::EndProcessFile:: end -- processFile -- SECONDARY FUNCTION: check that the data (file contents plus per user specifications) -- can be processed. Throws warnings, returns a boolean, numLineSubStrings and warning Id. function validateDataForProcessing() print("Entering function: validateDataForProcessing") print(" Inputs: ", "None") local flgPassed = true local numWarningId = 0 local tblCase = {} -- Strip any spaces so we can get the correct # of substrings local strStrippedColumnSelectorText = string.gsub(txtColumnSelector.text, "%s+", "") print(" strStrippedColumnSelectorText =", strStrippedColumnSelectorText) local str, strLength = getColumnIdsToUse(strStrippedColumnSelectorText) print(" Returned", str, strLength) local numLength = tonumber(strLength) print(" ") if numLength < 3 or numLength > 7 then print("@Outside the permitted range Warning - numlength =",numLength) showWarning("\n\nInvalid Data:\n\nThe number of values in each line is outside the permitted range.","A minimum of 3 values, (each containing an X,Y & Z ordinate) are required and a maximum of 7 are permitted, the X,Y & Z ordinates followed by 3 or 4 (R,G,B & optionally A) pixel channel values in the range 0-255, and in the order shown here. Use the Column Selector to ignore unusable data.\n\n\nThe script will return to the main form and await further input.") numWarningId = 1 flgPassed = false print(" Returning ", numWarningId) return flgPassed, numLength, numWarningId end -- If number of data values are in range if numLength == 3 then print("@Not enough values in each line to assign colors Warning, numlength =", numLength) if chkPointsIdsAndColors.checked == true then showWarning("\n\nThere are not enough values in each line to assign colors to points, but there are the correct number of values for creating a momochromatic file of points.","The current output option will be changed, a file will be output containing Points & optionally Instances Id's.\n\n\nThe script will continue running.") else -- Do nothing end if chkPointsIdsAndColors.checked == true then chkPointsAndIds.checked = true chkPointsIdsAndColors.checked = false end if chkPointsAndIds.checked == true then chkPointsIdsAndColors.checked = false chkPointsOnly.checked = false end if chkPointsOnly.checked == true then chkPointsIdsAndColors.checked = false chkPointsAndIds.checked = false end numWarningId = 3 flgPassed = true print(" Returning ", numWarningId) return flgPassed, numLength, numWarningId end if numLength == 4 then print("@insufficent values in each line to assign colors + 1 Warning, numlength =", numLength) showWarning("\n\nInvalid Data:\n\nThere are insufficent values in each line to assign colors to points and 1 too many values for creating a simple file of points alone.","A file will be output containing Points & optionally Instance Id's. You must first use the Column Selector to exclude 1 unusable data value.\n\n\nThe script will return to the main form so you can comment out a value using the column selector and then reprocess the file.") if chkPointsAndIds.checked == false then chkPointsAndIds.checked = true end if chkPointsIdsAndColors.checked == true then chkPointsIdsAndColors.checked = false end --if chkPointsOnly.checked = true then do nothing numWarningId = 4 flgPassed = false print(" Returning ", numWarningId) return flgPassed, numLength, numWarningIdv end if numLength == 5 then print("@insufficent values in each line to assign colors + 2 Warning, numlength =",numLength) showWarning("\\nnInvalid Data:\n\nThere are insufficent values in each line to assign colors to points and 2 too many values for creating a simple file of points alone.","A file containing Points & optionally Instances Id's. Additionally, you must first use the Column Selector to exclude 2 unusable data values.\n\n\nThe script will return to the main form so you can comment out the values using the column selector and reprocess the file.") if chkPointsAndIds.checked == false then chkPointsAndIds.checked = true end if chkPointsIdsAndColors.checked == true then chkPointsIdsAndColors.checked = false end --if chkPointsOnly.checked = true then do nothing numWarningId = 5 flgPassed = false print(" Returning ", numWarningId) return flgPassed, numLength, numWarningId end -- If the are 6 or 7 substrings we are good to go. -- End in range print(" Returning", flgPassed, numLength, numWarningId ) print(" ") print("Exiting function: validateDataForProcessing") print(" ") return flgPassed, numLength, numWarningId end -- SECONDARY FUNCTION: create the Octane Instance Color texture (ICT).png file --function createInstanceColorMap(tblPixels, numImageColumns, numImageRows) function createInstanceColorMap(tblPixels) print(" ") print("Entering function: createInstanceColorMap") print(" Inputs: ", tblPixels, numImageColumns, numImageRows) print(" Length of pixel table is: "..#tblPixels.. " for image of " ..numImageColumns.. " x " ..numImageRows.. " pixels") print(" ") -- Revise based on actual number of lines written to .csv file --numBitMapDimension = math.floor(math.sqrt(#tblAllPixels)) numImageColumns = math.floor(math.sqrt(#tblAllPixels)) numImageRows = math.floor(#tblAllPixels/numImageColumns) imageSize = {numImageColumns, numImageRows} tprint(imageSize) print(" ") numImageRows = math.floor(math.sqrt(#tblAllPixels)) numImageColumns = math.floor(#tblAllPixels/numImageRows) imageSize = {numImageColumns, numImageRows} tprint(imageSize) print(" ") numImageRows = math.sqrt(#tblAllPixels) numImageColumns = math.sqrt(#tblAllPixels) imageSize = {numImageColumns, numImageRows} tprint(imageSize) print(" ") numImageRows = math.ceil(math.sqrt(#tblAllPixels)) numImageColumns = math.ceil(math.sqrt(#tblAllPixels)) imageSize = {numImageColumns, numImageRows} tprint(imageSize) print(" ") numImageRows = math.floor(math.sqrt(#tblAllPixels)) numImageColumns = math.floor(math.sqrt(#tblAllPixels)) imageSize = {numImageColumns, numImageRows} tprint(imageSize) print(" ") -- Update in case file was thinned lblInstanceColorMap.text = "Instance Color Map (from source file - " ..numImageColumns.. " x " ..numImageRows.. " pixels )" -- Update status bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .75, text = "Generating Instance Color Map..."} octane.gui.updateStatus("Generating Instance Color Map", .75) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() -- Set up the writable image properties, type attribute options are; 0 = color, 1 = greyscale -- Others are read only FYI, based on an all white .png file @ 500x500 pixels PROPS_IMAGE = {type = 0,size ={numImageColumns,numImageRows}} --PROPS_IMAGE = {type = 0,size ={numImageRows,numImageColumns}} -- PROPS_IMAGE contents, for information only --[[ --PROPS_IMAGE["type"] = 0 --PROPS_IMAGE["size"][1] = numImageColumns --PROPS_IMAGE["size"][2] = numImageRows --PROPS_IMAGE["sourceInfo"] = "24-bit RGB" --PROPS_IMAGE["isHdr"] = false --PROPS_IMAGE["hasTransparency"] = true --PROPS_IMAGE["bytesPerChannel"] = 1.0 --PROPS_IMAGE["isCompressed"] = false --PROPS_IMAGE["bytesPerPixel"] = 4.0 --PROPS_IMAGE["byteSize"] = 1000000 --PROPS_IMAGE["hasColour"] = true --PROPS_IMAGE["channelCount"] = 4 --]] -- Check file names and paths print(" Check file names and paths based on input") print(" Input File's Directory: ", strInputPath) print(" Input Path with Filename: ", strInputFilePath) print(" Input Filename: ", strInputFileName) print(" ") print(" Output File's Directory: ", strOutputPath) print(" Output Path with Filename: ", strOutputFilePath) print(" Output Filename: ", strOutputFileName) print(" ") print(" File Path: Is absolute = ", octane.file.isAbsolute(strInputFilePath)) print(" Image Dimensions = ", numImageSize.." x ".. numImageSize) print(" ") -- Set up File names and Path's for Instance Image Map print(" Modified file names and paths for output") strOutputFileName = "imgICT_" .. strInputFileName.. " (" ..numPercentOfOriginal.." )" print(" Output Filename: ", strOutputFileName) print(" Output File extension: ", strOutputFileExtension) strOutputFileName = strOutputFileName.. ".png" print(" Output Filename: ",strOutputFileName) print(" ") print(" Image Settings for: ", strOutputFileName) tprint(PROPS_IMAGE) print(" ") print(" Creating Image: Octane Instance Color Texture:", strOutputFileName) -- Create a blank Image to fill with rgba values from input file local imgInstanceColors = octane.image.create(PROPS_IMAGE) print(" Blank RGB image created:", PROPS_IMAGE["size"][1] .. " x " .. PROPS_IMAGE["size"][2] .. " pixels") -- Create a table to hold one pixels RGBA values, initially set all the pixels to white local pixNewValue = {255, 255, 255, 255} print(" ") --print(" Check if we got some color data: Row 25 from table of pixels") --print(tblPixels[25]) --tprint(tblPixels[25]) --print(" ") -- Fill image with white, done to show in UI, but not worth it. --octane.image.fill(imgInstanceColors , nil, pixNewValue, false) -- Alternative method that enables bitmap image to be addressed directly, but no apparent benefit. --picInstanceColors:updateProperties{image = imgInstanceColors} --picInstanceColors:updateProperties{} --octane.changemanager.update() -- Begin processing pixels by row (moving left), starting at the bottom left corner if imgInstanceColors ~= nil then -- Process Image print(" Processing Image...") print(" ") local c = 1 -- Write image y from bottom, x from left per Instance Color Texture specification for y = numImageColumns, 1, -1 do for x = 1, numImageRows, 1 do octane.image.setPixel(imgInstanceColors, x, y, tblPixels[c]) --octane.image.setPixel(imgInstanceColors, y, x, tblPixels[c]) -- Alternative method that addresses bitmap image directly, but no apparent benefit. --picInstanceColors.image.setPixel(imgInstanceColors, x, y, tblPixels[c]) --picInstanceColors:updateProperties{} -- too slow --octane.changemanager.update() c = c + 1 end end print(" Processing Complete") print(" Saving Instance Color Map to", strOutputPath .. [[\]] .. strOutputFileName) octane.image.save(imgInstanceColors , strOutputPath .. [[\]] .. strOutputFileName) -- Load the saved file back into the bitmap. strImagePath = strOutputPath .. [[\]] .. strOutputFileName picInstanceColors:updateProperties{image = octane.image.load(strImagePath,0)} octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{ progress = .90, text = "Image creation complete"} octane.gui.updateStatus("Image creation complete", .90) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() end print("Exiting function: createInstanceColorMap") print(" ") end -- createInstanceColorMap() -- SECONDARY FUNCTION: build a graph of the pointcloud in Octane function createPointCloudNodes(flgColor) print("Entering function: createPointCloudNodes") print(" ") -- Update progress bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = 90, text = "Creating Point Cloud Node Graph..."} octane.gui.updateStatus("Creating Point Cloud Node Graph", .90) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() print(" Creating point cloud related nodes") print(" Scatter File Name = ", strOutputFilePath) -- Create the Nodes if flgColor then nodeInstanceColorTexture = octane.node.create{ type = octane.NT_TEX_INSTANCE_COLOR, name = "Point Colors_ICT", position = { 500, 600 } } octane.node.setAttribute(nodeInstanceColorTexture, octane.A_FILENAME, strImagePath, true) end nodePointMaterial = octane.node.create{ type = octane.NT_MAT_DIFFUSE, name = "Point Material_Diffuse", position = { 600, 650} } nodePointRadius = octane.node.create{ type = octane.NT_FLOAT, name = "Point_Radius", position = { 900, 650} } nodePointSide = octane.node.create{ type = octane.NT_FLOAT, name = "Point_Side_Length", position = { 1100, 650} } octane.node.setAttribute(nodePointRadius , octane.A_VALUE , numPointRadius, true) -- Edit to change default radius of point octane.node.setAttribute(nodePointSide , octane.A_VALUE , numPointSide, true) -- Edit to change default radius of point nodePointGeoSphere = octane.node.create{ type = octane.NT_GEO_SDF_SPHERE, name = "Point Geometry_Sphere", position = { 700, 700 } } nodePointGeoBox = octane.node.create{ type = octane.NT_GEO_SDF_BOX, name = "Point Geometry_Box", position = { 1050, 700 } } nodePointCloudScatter = octane.node.create{ type = octane.NT_GEO_SCATTER, name = "Point Cloud_Scatter", position = { 800, 750 } } octane.node.setAttribute(nodePointCloudScatter, octane.A_FILENAME, strOutputFilePath, true) nodePointCloudPlacement = octane.node.create{ type = octane.NT_GEO_PLACEMENT, name = "Point Cloud_Placement", position = { 900, 800} } nodeGroundPlane = octane.node.create{ type = octane. NT_GEO_PLANE , name = "Ground Plane", position = { 1100, 800} } nodeGroundPlanePlacement = octane.node.create{ type = octane.NT_GEO_PLACEMENT, name = "Ground Plane_Placement", position = { 1100, 850} } nodeGroup = octane.node.create{ type = octane. NT_GEO_GROUP, name = "Point Cloud with Plane", position = { 1000, 900} } octane.node.setAttribute(nodeGroup, octane.A_PIN_COUNT, 2, true) nodeGeometryOut = octane.node.create{ type = octane.NT_OUT_GEOMETRY, name = "Geometry Out", position = { 100, 100} } print(" Connecting point cloud related nodes") -- Connect the Nodes if flgColor then nodePointMaterial:connectToIx(1, nodeInstanceColorTexture) end nodePointGeoSphere:connectToIx(1, nodePointMaterial) nodePointGeoSphere:connectToIx(4, nodePointRadius) nodePointCloudScatter:connectToIx(1, nodePointGeoSphere) nodePointCloudPlacement:connectToIx(2, nodePointCloudScatter) nodePointGeoBox:connectToIx(1, nodePointMaterial) nodePointGeoBox:connectToIx(4, nodePointSide) nodePointGeoBox:connectToIx(5, nodePointSide) nodePointGeoBox:connectToIx(6, nodePointSide) print(" Connecting ground plane related nodes") -- Connect Ground Plane Nodes nodeGroundPlanePlacement:connectToIx(2, nodeGroundPlane) nodeGroup:connectToIx(1, nodePointCloudPlacement) nodeGroup:connectToIx(2, nodeGroundPlanePlacement) -- Connect Geometry Output nodeGeometryOut:connectToIx(1, nodeGroup) print(" Creating point cloud graph") if flgColor then -- Group for colored Point Cloud tblItems = { nodeInstanceColorTexture, nodePointMaterial, nodePointRadius , nodePointSide, nodePointGeoSphere, nodePointGeoBox, nodePointCloudScatter, nodePointCloudPlacement, nodeGroundPlane, nodeGroundPlanePlacement, nodeGroup, nodeGeometryOut, } else -- Group for monochromatic Point Cloud tblItems = { nodePointMaterial, nodePointRadius, nodePointSide, nodePointGeoSphere, nodePointGeoBox, nodePointCloudScatter, nodePointCloudPlacement, nodeGroundPlane, nodeGroundPlanePlacement, nodeGroup, nodeGeometryOut, } end print("Naming graph based on user specified options") local strOrientation = "" if flgYup == true then strOrientation = "Y " else strOrientation = "Z " end graphPointCloud = octane.nodegraph.group(octane.project.getSceneGraph(), tblItems, true) numBodyLines = format_Integer(numBodyLines) --print(" numBodyLines = ", numBodyLines) graphPointCloud.name = "Point Cloud_"..strOrientation..strInputFileName.. " (" ..numPercentOfOriginal.. " " ..numBodyLines.. " pts)" graphPointCloud.position = { 0, 0} -- Update progress bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = 1, text = "Point Cloud Node Graph Created, Processing Complete."} octane.gui.updateStatus("Point Cloud Node Graph Created, Processing Complete.", 1) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() -- Cleanup the resulting Geometry Out node --nodesOut = octane.nodegraph.getOutputNodes(graphPointCloud) --octane.node.destroy(nodesOut[1]) --print(" ") print("Exiting function: createPointCloudNodes") print(" ") end -- Callback handling the GUI elements function guiCallback(component, event) if component == btnFileOpen then -- Update status bar octane.gui.dispatchGuiEvents(100) pbrloadInputFile:updateProperties{progress = .04, text = "Loading Input File..."} octane.gui.updateStatus("Loading Input File...", .04) octane.gui.dispatchGuiEvents(100) octane.changemanager.update() print("Entering function: guiCallback", component) print(" ") -- In the case that the user loads a different input file after running the script -- reset all of the variables. reset_Variables() -- choose an input file, get a return value local rv = octane.gui.showDialog { type = octane.gui.dialogType.FILE_DIALOG, path = octane.file.getParentDirectory(octane.project.getCurrentProject()), title = "Select Input File", wildcards = "*.txt; *.pts; *.xyz; *.pcd; *.ply; *.asc; *.csv;", save = false, } -- if a file is selected if rv.result ~= "" then strInputFilePath = rv.result lblInputPath.text = strInputFilePath lblInputPath.tooltip = "Source file directory. Processed files will also be saved here. Current Source File = "..strInputFilePath -- If a .ply file, check to see if its binary local strFileExtension = octane.file.getFileExtension(strInputFilePath) if strFileExtension == ".ply" then local flgBinary = is_Binary(strInputFilePath) print("Is Binary: = ", flgBinary) print(" ") -- Warn user if necessary if flgBinary == true then -- Need to bail showWarning("The input file is Binary, input files must be an ASCII text file.","The file needs to be opened & saved as a text file in a third party application.\nthe script will now terminate.") wndMain:closeWindow() end end -- Analyse the points file and inform user of contents. local rv1 = loadInputFile(strInputFilePath) -- Note: a return called inside loadInputFile comes back here. btnProcessInputFile:updateProperties{ enable = true } chkCreateOctaneNodes:updateProperties { enable = true } print("Function returned before completion") print("flgRunning ",flgRunning) print("Exiting function: guiCallback", component) print("Waiting for user to continue.") end elseif component == btnProcessInputFile then print("Entering function: guiCallback", component) print(" ") local rv1 = processInputFile() -- Note: a return called inside processInputFile comes back here. print("Data Valadation numWarningId =", rv1) --print("Function returned before completion") print("flgRunning ",flgRunning) print("Exiting function: guiCallback", component) print("Waiting for user action.") print(" ") elseif component == chkPointsIdsAndColors then -- don't change if an alternative has not been clicked, true at startup if chkPointsIdsAndColors.checked == false and chkPointsOnly.checked == false and chkPointsAndIds.checked == false then chkPointsIdsAndColors.checked = true elseif chkPointsIdsAndColors.checked == true then chkPointsOnly.checked = false chkPointsAndIds.checked = false --chkPointsIdsAndColors.checked = true end --showDebug("@guiCallback for chkPointsIdsAndColors " .." event type ".. event, "Value of 'checked' property = ".. tostring(chkPointsIdsAndColors.checked)) elseif component == chkPointsAndIds then -- don't change if an alternative has not been clicked if chkPointsAndIds.checked == false and chkPointsOnly.checked == false and chkPointsIdsAndColors.checked == false then chkPointsAndIds.checked = true elseif chkPointsAndIds.checked == true then chkPointsOnly.checked = false --chkPointsAndIds.checked = false chkPointsIdsAndColors.checked = false end --showDebug("@guiCallback for chkPointsAndIds " .." event type ".. event, "Value of 'checked' property = ".. tostring(chkPointsAndIds.checked)) elseif component == chkPointsOnly then -- Don't change if an alternative has not been clicked if chkPointsOnly.checked == false and chkPointsAndIds.checked == false and chkPointsIdsAndColors.checked == false then chkPointsOnly.checked = true elseif chkPointsOnly.checked == true then --chkPointsOnly.checked = false chkPointsAndIds.checked = false chkPointsIdsAndColors.checked = false end --showDebug("@guiCallback for chkPointsOnly " .." event type ".. event, "Value of 'checked' property = ".. tostring(chkPointsOnly.checked)) elseif component == chkYup then flgYup = chkYup.checked print("flgYup =", flgYup) elseif component == nbxStepSize then if nbxStepSize.value == 3 then nbxStepSize.value = 4 numStepSize = nbxStepSize.value elseif nbxStepSize.value == 6 then nbxStepSize.value = 8 numStepSize = nbxStepSize.value elseif nbxStepSize.value == 7 then nbxStepSize.value = 8 numStepSize = nbxStepSize.value elseif nbxStepSize.value == 9 then nbxStepSize.value = 10 numStepSize = nbxStepSize.value elseif nbxStepSize.value == 49 then nbxStepSize.value = 50 numStepSize = nbxStepSize.value elseif nbxStepSize.value == 11 then nbxStepSize.value = 50 numStepSize = nbxStepSize.value elseif nbxStepSize.value == 51 then nbxStepSize.value = 100 numStepSize = nbxStepSize.value elseif nbxStepSize.value == 100 then nbxStepSize.value = 1 numStepSize = nbxStepSize.value elseif nbxStepSize.value == 99 then nbxStepSize.value = 1 numStepSize = nbxStepSize.value else numStepSize = nbxStepSize.value end print("numStepSize = ", numStepSize) elseif component == txtColumnSelector then flgDataColumnEdits = true elseif component == nbxHeaderLength then --Reread the header with the new number of lines. numGetHeaderLines = nbxHeaderLength.value tblBOF = getFileLinesAsTable(strInputFilePath, numGetHeaderLines) loadInputFile(strInputFilePath) elseif component == btnResetColumns then txtColumnSelector.text = "" for i = 1, numLineSubStrings do txtColumnSelector.text = txtColumnSelector.text.. "| " ..i end txtColumnSelector.text = txtColumnSelector.text.. "|" elseif component == btnResizeForm then resizeForm() elseif component == btnHelpOpen then txtHelp.text = strHelpText wndHelp:showWindow() elseif component == btnHelpClose then wndHelp:closeWindow() elseif component == btnMainClose then print("Entering function: guiCallback", component) print(" ") print("Shutting down script") flgRunning = false wndMain:closeWindow() end -- components end -- function, guiCallback -- Hookup the relevant GUI controls to a callback btnFileOpen:updateProperties { callback = guiCallback } btnProcessInputFile:updateProperties { callback = guiCallback } chkPointsOnly:updateProperties { callback = guiCallback } chkPointsAndIds:updateProperties { callback = guiCallback } chkPointsIdsAndColors:updateProperties { callback = guiCallback } nbxHeaderLength:updateProperties { callback = guiCallback } btnResetColumns:updateProperties { callback = guiCallback } txtColumnSelector:updateProperties { callback = guiCallback } btnHelpOpen:updateProperties { callback = guiCallback } btnHelpClose:updateProperties { callback = guiCallback } btnMainClose:updateProperties { callback = guiCallback } btnResizeForm:updateProperties { callback = guiCallback } nbxStepSize:updateProperties { callback = guiCallback } chkYup:updateProperties { callback = guiCallback } -- Opens form with height set base -- on value of user definable flag resizeForm() local flgRunning = wndMain:showWindow() --if not flgRunning then return end ::exit::