Implement CPU Image Processing Library

In this exercise, you'll learn to create a new script library in which you'll implement the CPU compute code that executes the first part of the service.

Create A New Script Utility Library

  1. Return to the running Layout application.
  2. Select Desktop » Create Script... from the main menu.

    The software presents a wizard that helps you create a new script document.

    This is a picture of the create script wizard.
  3. Set Script Folder to Application Scripts Folder if it is not already set.
  4. Set Script Type to Function Library.
  5. Set Script Author to your name or company name.
  6. Set Script Description to This script contains functions for CPU image processing.

    Copy Text To Clipboard

    This script contains functions for CPU image processing.
  7. Set Library Name to LibTerrainAnalysisCpuImage.

    Copy Text To Clipboard

    LibTerrainAnalysisCpuImage
  8. Set File Name to app_service_analyze_terrain_cpu_image_processing_util.ssl.

    Copy Text To Clipboard

    app_service_analyze_terrain_cpu_image_processing_util.ssl
  9. Click OK or hit ENTER when you are finished.

    The software creates a new script and copies the path to the Windows® clipboard.

Open The Script Library

  1. Return to the text editor and select the option to open a file from disk.
  2. Select CTRL + V to paste the script command library file path (into the place in the dialog where you specify the file to open).
  3. Open the file.

    The script appears in the text editor.

    ///////////////////////////////////////////////////////////////////////////////
    //
    // $author           Scenomics LLC
    // $description      This script contains functions for CPU image processing.
    //
    // Copyright 2023 Scenomics LLC. All Rights Reserved.
    //
    ///////////////////////////////////////////////////////////////////////////////
    
    library LibTerrainAnalysisCpuImage;
    
    import library "app_service_assert_util.ssl";
    import library "app_service_console_util.ssl";
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function void MyFunction()
    {
    }
    

Implement Functions

  1. Find the script imports.

    Instead of copying each function in, one-by-one, in this exercise we'll copy in the entire script source code and then review the functions in the next section.

    import library "app_service_assert_util.ssl";
    import library "app_service_console_util.ssl";
    
  2. Replace everything in the script from the imports to the bottom of the script file, including the function MyFunction, with the following:

    (Do not replace the header at the top of the script and do not replace the library declaration!)

    Copy Text To Clipboard

    import library "app_service_assert_util.ssl";
    import library "app_service_assert_util.ssl";
    import library "app_service_console_util.ssl";
    import library "type_float32_array_util.ssl";
    import library "type_image_channel_extract_util.ssl";
    import library "type_image_format_util.ssl";
    import library "type_image_util.ssl";
    import library "type_int32_array_util.ssl";
    import library "type_service_enumeration_util.ssl";
    import library "type_str_list_util.ssl";
    
    import library "app_service_workload_analyze_terrain_classes.ssl";
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function void SaveTiles( BuildInputs p_oInputs, Image p_oSrc, string p_sFilename )
    {
       auto FilePath a_oFilePath = new FilePath( p_oInputs.m_oArraysPath.GetPath() );
       a_oFilePath.RemoveEndSeparator();
       a_oFilePath.AppendPathWithFile( p_sFilename );
    
       p_oSrc.SaveFile( a_oFilePath.GetPath() );
    
       p_oInputs.m_slMessages.Add( "Generated image: " + a_oFilePath.GetPath() );
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function void SaveMap( BuildInputs p_oInputs, Image p_oSrc, string p_sFilename )
    {
       auto FilePath a_oFilePath = new FilePath( p_oInputs.m_oMapsPath.GetPath() );
       a_oFilePath.RemoveEndSeparator();
       a_oFilePath.AppendPathWithFile( p_sFilename );
    
       p_oSrc.SaveFile( a_oFilePath.GetPath() );
    
       p_oInputs.m_slMessages.Add( "Generated image: " + a_oFilePath.GetPath() );
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function void GetCellPixelCoords( Int32Vector p_viLensCoords, Int32Array p_aiCoords )
    {
       p_aiCoords.Clear();
    
       int a_nStartY = p_viLensCoords.Y;
       int a_nStartX = p_viLensCoords.X;
    
       for( int x = a_nStartY; x < a_nStartY + p_viLensCoords.Z; ++x )
       {
          for( int y = a_nStartX; y < a_nStartX + p_viLensCoords.W; ++y )
          {
             p_aiCoords.Add( x );
             p_aiCoords.Add( y );
             //Console.Out( x + "," + y );
          }
       }
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function void GetMinMaxFromSlices( Image p_oSrc, Float32Array p_afMinMax )
    {
       int a_nArraySlices = p_oSrc.Height / p_oSrc.Width;
       int a_nSliceWidth = p_oSrc.Width;
       int a_nSliceHeight = p_oSrc.Height / a_nArraySlices;
       int a_nOffset = a_nSliceWidth * a_nSliceHeight;
       int a_nOffsetBase = 0;
    
       auto Float32ArrayAlgorithms a_afAlgorithms;
       auto Float32Array a_afMinMax = new Float32Array( 2, 0.0 );
       auto Float32Iterator dst = a_afMinMax.First();
    
       auto Float32ArrayView src_view = p_oSrc.GetFloat32View();
    
       for( int i = 0; i < a_nArraySlices; ++i )
       {
          src_view.First.Position = a_nOffsetBase;
          src_view.Last.Position = a_nOffsetBase + a_nOffset;
          a_afAlgorithms.FindMinMax( src_view.First, src_view.Last, dst );
          p_afMinMax.Add( a_afMinMax.Get( 0 ) );
          p_afMinMax.Add( a_afMinMax.Get( 1 ) );
          a_nOffsetBase += a_nOffset;
       }
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function void GetMinMaxZFromNormals( Image p_oSrc, Float32Array p_afMinMax )
    {
       auto Uint8ArrayView src_view = p_oSrc.GetUint8View();
    
       int a_nArraySlices = p_oSrc.Height / p_oSrc.Width;
       int a_nSliceWidth = p_oSrc.Width;
       int a_nSliceHeight = p_oSrc.Height / a_nArraySlices;
       int a_nOffset = a_nSliceWidth * a_nSliceHeight * 4;
       int a_nOffsetBase = 0;
    
       auto Uint8Array a_auMinMax = new Uint8Array( 2, 0 );
       auto Uint8Iterator dst = a_auMinMax.First();
    
       auto Uint8Array a_auChannel = new Uint8Array(
          a_nSliceWidth * a_nSliceHeight, 0 );
       auto Uint8ArrayView channel_view = a_auChannel.GetView();
    
       auto Uint8ArrayAlgorithms a_auAlgorithms;
       int a_nRgbaChannelCount = 4; // RGBA has four channels.
       int a_nTargetChannel = 0;// Z values from normal. RGBA -> BGRA = Z = 0
    
       for( int i = 0; i < a_nArraySlices; ++i )
       {
          src_view.First.Position = a_nOffsetBase;
          src_view.Last.Position = a_nOffsetBase + a_nOffset;
    
          a_auAlgorithms.Deinterleave(
             src_view.First,
             src_view.Last,
             a_nRgbaChannelCount,
             a_nTargetChannel,
             channel_view.First );
    
          //LibUint8Array.OutRange( a_auChannel, 0, 32 );
    
          a_auAlgorithms.FindMinMax(
             channel_view.First, channel_view.Last, dst );
    
          //Console.Out( a_auMinMax.Get( 0 ) );
          //Console.Out( a_auMinMax.Get( 1 ) );
          //Console.Out( src_view.First.Position );
          //Console.Out( src_view.Last.Position );
    
          p_afMinMax.Add( ((double)a_auMinMax.Get( 0 )) / NumericLimits.uint8_max() );
          p_afMinMax.Add( ((double)a_auMinMax.Get( 1 )) / NumericLimits.uint8_max() );
    
          a_nOffsetBase += a_nOffset;
       }
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function bool ConvertFloat32ToRgba8888(
    
       BuildInputs p_oInputs,
       Image p_oSrc,
       Image p_oDst,
       FilePath p_oDstPath,
       StrList p_slMessages
    
       )
    {
       // Configure channels for our FP32 to RGBA conversion.
       // We'll map the zero-th channel from our FP32 image
       // onto the red, green, and blue channels in the RGBA
       // image so we end up with a gray scale result.
       auto Int32Array a_aiSrcChannels = new Int32Array( 4, 0 );
    
       // Unpack each slice/layer from the source Image.
       auto TypeBuffer a_apSrcSlices;
       auto TypeBuffer a_apDstSlices;
       LibImage.UnpackSlices(
          p_oSrc, p_oInputs.m_iArraySlices, a_apSrcSlices );
    
       // Declare dst pixel format.
       int a_eFormat = Enum.IPF_8888_ARGB();
       int a_nChannelCount = a_aiSrcChannels.GetCount();
       int a_nChannel = 3;
    
       // Create an alpha channel filled with pure white.
       Image a_oFirst = (Image)a_apSrcSlices.GetFirst();
       auto Uint8ArrayAlgorithms a_auAlgorithms;
       auto Uint8Array a_auAlpha = new Uint8Array(
          a_oFirst.Width * a_oFirst.Height, NumericLimits.uint8_max() );
       auto Uint8ArrayView alpha_view = a_auAlpha.GetView();
    
       // Iterate the slices, converting each from FP32 to RGBA 8888.
       for( int i = 0; i < a_apSrcSlices.GetCount(); ++i )
       {
          Image a_oSrcSlice = (Image)a_apSrcSlices.Get( i );
    
          // Get/set the min/max values for each slice.
          auto Float64Array a_afMinMax;
          a_oSrcSlice.GetMinMax( a_afMinMax );
          a_afMinMax.Add( NumericLimits.zero() );      // min for RGB color
          a_afMinMax.Add( NumericLimits.uint8_max() ); // max for RGB color
    
          //LibFloat64Array.Out( a_afMinMax );
    
          // Allocate a new Image object for each slice
          // that is the destination for our format conversion.
          Image a_oDstSlice = new Image(
             a_oSrcSlice.Width, a_oSrcSlice.Height, a_eFormat );
          // Take ownership of the slice.
          a_apDstSlices.Owns( a_oDstSlice );
    
          // From FP32 to 8888 ARGB...
          LibImageFormat.convert_to_IPF_8888_ARGB(
             a_oSrcSlice, a_oDstSlice, a_aiSrcChannels, a_afMinMax );
    
          // Interleave the pure-white alpha channel.
          auto Uint8ArrayView dst_view = a_oDstSlice.GetUint8View();
          a_auAlgorithms.Interleave(
             alpha_view.First, alpha_view.Last,
             a_nChannelCount, a_nChannel, dst_view.First );
    
          // DEBUG
          /*
          auto FilePath a_oDstPath = new FilePath(
             p_oInputs.m_oDstPath.GetFilePath() );
          a_oDstPath.RemoveEndSeparator();
          a_oDstPath.AppendPathWithFile( "test_" + i + ".png" );
          a_oDstSlice.SaveFile( a_oDstPath.GetPath() );
          Console.Out( a_oDstPath.GetPath() );
          */
       }
    
       LibImage.PackSlices( a_apDstSlices, a_eFormat, p_oSrc, p_oDst );
    
       p_oDst.SaveFile( p_oDstPath.GetPath() );
    
       return true;
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function void ComputeTerrainInfo( BuildInputs p_oInputs )
    {
       // Use the 'Lens' to determine which tiles
       // in the elevation data the user wishes to analyze.
    
       p_oInputs.m_slMessages.Add(
          "Using 'Lens' sub-range for analysis: " +
          "[" + p_oInputs.m_oLens.X + "," + p_oInputs.m_oLens.Y + "] spanning " +
          "[" + p_oInputs.m_oLens.Z + "," + p_oInputs.m_oLens.W + "] chunks." );
    
       //LibInt32Vector.Out( p_oInputs.m_oLens );
    
       // Print build message information.
       p_oInputs.m_slMessages.Add(
          "Using scale factor: " + p_oInputs.m_nScaleFactor );
    
       // Set the x/y point spacing in meters.
       p_oInputs.m_dSpacingX = Math.AbsFloat64(
          p_oInputs.m_oGdalImageInfo.GetSpacingX() ) * p_oInputs.m_nScaleFactor;
       p_oInputs.m_dSpacingY = Math.AbsFloat64(
          p_oInputs.m_oGdalImageInfo.GetSpacingY() ) * p_oInputs.m_nScaleFactor;
    
       //Console.Out( p_oInputs.m_dSpacingX );
       //Console.Out( p_oInputs.m_dSpacingY );
       //return;
    
       // Print build message information.
       p_oInputs.m_slMessages.Add(
          "Geotiff Original Width: "  +
          p_oInputs.m_oGdalImageInfo.GetRasterSizeX() );
    
       p_oInputs.m_slMessages.Add(
          "Geotiff Original Height: " +
          p_oInputs.m_oGdalImageInfo.GetRasterSizeY() );
    
       p_oInputs.m_slMessages.Add(
          "Geotiff Original Chunks X: " +
          p_oInputs.m_oGdalImageInfo.GetChunkCountX() );
    
       p_oInputs.m_slMessages.Add(
          "Geotiff Original Chunks Y: " +
          p_oInputs.m_oGdalImageInfo.GetChunkCountY() );
    
       p_oInputs.m_slMessages.Add(
          "Geotiff Original Chunks Width: " +
          p_oInputs.m_oGdalImageInfo.GetChunkDimX() );
    
       p_oInputs.m_slMessages.Add(
          "Geotiff Original Chunks Height: " +
          p_oInputs.m_oGdalImageInfo.GetChunkDimY() );
    
       // Copy the area parameters set by the lens.
       p_oInputs.m_viDstRange.X = p_oInputs.m_oLens.X;
       p_oInputs.m_viDstRange.Y = p_oInputs.m_oLens.Y;
       p_oInputs.m_viDstRange.Z = p_oInputs.m_oLens.Z;
       p_oInputs.m_viDstRange.W = p_oInputs.m_oLens.W;
    
       // Print build message information.
       p_oInputs.m_slMessages.Add(
          "Geotiff Elevation Spacing X: " + p_oInputs.m_dSpacingX );
       p_oInputs.m_slMessages.Add(
          "Geotiff Elevation Spacing Y: " + p_oInputs.m_dSpacingY );
    
       // Print build message information.
       p_oInputs.m_slMessages.Add(
          "Dst Range X Start " + p_oInputs.m_viDstRange.X );
       p_oInputs.m_slMessages.Add(
          "Dst Range Y Start " + p_oInputs.m_viDstRange.Y );
       p_oInputs.m_slMessages.Add(
          "Dst Range Z Count " + p_oInputs.m_viDstRange.Z );
       p_oInputs.m_slMessages.Add(
          "Dst Range W Count " + p_oInputs.m_viDstRange.W );
    
       // Compute the range in meters that we are analyzing.
       p_oInputs.m_dRangeX = p_oInputs.m_viDstRange.Z *
          p_oInputs.m_oGdalImageInfo.GetChunkDimX() * p_oInputs.m_dSpacingX;
       p_oInputs.m_dRangeY = p_oInputs.m_viDstRange.W *
          p_oInputs.m_oGdalImageInfo.GetChunkDimY() * p_oInputs.m_dSpacingX;
    
       p_oInputs.m_dChunkRangeX = p_oInputs.m_dRangeX / p_oInputs.m_viDstRange.Z;
       p_oInputs.m_dChunkRangeY = p_oInputs.m_dRangeY / p_oInputs.m_viDstRange.W;
    
       p_oInputs.m_slMessages.Add(
          "Analyzing area of 'Lens' sub-range for analysis. " +
          ((int)p_oInputs.m_dRangeX) + " metres x " + ((int)p_oInputs.m_dRangeY) +
          " metres requested." );
    
       p_oInputs.m_slMessages.Add(
          "Starting analysis at chunk [" +
          p_oInputs.m_viDstRange.X + "," + p_oInputs.m_viDstRange.Y + "]." );
    
       p_oInputs.m_slMessages.Add(
          "Analysis spans [" + p_oInputs.m_viDstRange.Z +
          "x" + p_oInputs.m_viDstRange.W + "] chunks." );
    
       p_oInputs.m_iArraySlices =
          p_oInputs.m_viDstRange.Z * p_oInputs.m_viDstRange.W;
    
       //LibStrList.Out( p_oInputs.m_slMessages );
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function void UnpackImages(
    
       Image p_oSrc,
       FilePath p_oOutputsPath,
       Int32Vector p_viLensCoords,
       string p_sCategory,
       TypeBuffer p_apDstImages,
       StrList p_slOutputPaths,
       StrList p_slMessages
    
       )
    {
       auto ImageFormatQuery a_oQuery;
       int a_nScalars = a_oQuery.GetScalarsPerFormat( p_oSrc );
    
       int a_nArraySlices = p_oSrc.Height / p_oSrc.Width;
       int a_nSliceWidth = p_oSrc.Width;
       int a_nSliceHeight = p_oSrc.Height / a_nArraySlices;
    
       int a_nOffset = a_nSliceWidth * a_nSliceHeight * 4;
       int a_nOffsetBase = 0;
    
       auto Uint8ArrayAlgorithms a_aiAlgorithms;
       auto Uint8ArrayView src_view = p_oSrc.GetUint8View();
    
       auto Int32Array a_aiCoords;
       GetCellPixelCoords( p_viLensCoords, a_aiCoords );
    
       // DEBUG
       // Enable if "Saved slice: ..." is enabled below.
       //p_slMessages.AddBlank();
    
       for( int i = 0; i < a_nArraySlices; ++i )
       {
          src_view.First.Position = a_nOffsetBase;
          src_view.Last.Position = a_nOffsetBase + a_nOffset;
    
          string a_sCoord = a_aiCoords.Get( i * 2 ) + "x" + a_aiCoords.Get( i * 2 + 1 );
          string a_sExt = "png";
          if( p_oSrc.PixelFormat == Enum.IPF_FP32() )
          {
             a_sExt = "image";
          }
    
          string a_sFilename = p_sCategory + "_" + a_sCoord + "." + a_sExt;
    
          // DEBUG
          //Console.Out( a_sFilename );
    
          auto FilePath a_oSlicePath = new FilePath( p_oOutputsPath.GetPath() );
          a_oSlicePath.AppendPath( a_sFilename );
    
          // DEBUG
          //p_slMessages.Add( "Saved slice: '" + a_oSlicePath.GetPath() + "'" );
    
          Image a_oDst = new Image(
             a_nSliceWidth, a_nSliceHeight, p_oSrc.PixelFormat );
          p_apDstImages.Owns( a_oDst );
    
          auto Uint8ArrayView dst_view = a_oDst.GetUint8View();
    
          // DEBUG
          /*
          if( p_oSrc.PixelFormat == Enum.IPF_FP32() )
          {
             Console.Out( src_view.First.Position );
             Console.Out( src_view.Last.Position );
             Console.Out( a_oDst.SizeInBytes() );
             Console.Out( dst_view.First.Position );
             Console.Out( dst_view.Last.Position );
          }
          */
    
          a_aiAlgorithms.Copy( src_view.First, src_view.Last, dst_view.First );
    
          // DEBUG
          //Console.Out( src_view.First.Position );
          //Console.Out( src_view.Last.Position );
          //Console.Out( dst_view.First.Position );
          //Console.Out( dst_view.Last.Position );
    
          a_oDst.SaveFile( a_oSlicePath.GetPath() );
          p_slOutputPaths.Add( a_oSlicePath.GetPath() );
    
          // DEBUG
          //Console.Out( a_oSlicePath.GetPath() );
    
          a_nOffsetBase += a_nOffset;
       }
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function bool CreateMap( BuildInputs p_oInputs, Image p_oSrc, Image p_oDst )
    {
       int a_nDimX    = p_oInputs.m_viMapInfo.X * p_oInputs.m_viMapInfo.Z;
       int a_nDimY    = p_oInputs.m_viMapInfo.Y * p_oInputs.m_viMapInfo.W;
       int a_nOffset  = p_oInputs.m_viMapInfo.Z * p_oInputs.m_viMapInfo.W * 4;
    
       // DEBUG
       //Console.Out( p_oInputs.m_viMapInfo.X );
       //Console.Out( p_oInputs.m_viMapInfo.Y );
       //Console.Out( p_oInputs.m_viMapInfo.Z );
       //Console.Out( p_oInputs.m_viMapInfo.W );
       //Console.Out( a_nDimX );
       //Console.Out( a_nDimY );
    
       bool a_bAlloc = p_oDst.AllocateBuffer(
          a_nDimX, a_nDimY, p_oSrc.PixelFormat );
       if( !( a_bAlloc ) )
       {
          p_oInputs.m_slMessages.Add(
             "Error. Source data allocation failed! Requested: " +
             a_nDimX + "x" + a_nDimX + " pixels." );
       }
    
       auto Uint8ArrayAlgorithms a_auAlgorithms;
       auto Uint8ArrayView src_view = p_oSrc.GetUint8View();
       auto Uint8ArrayView dst_view = p_oDst.GetUint8View();
    
       // DEBUG
       //Console.Out( src_view.First.GetCount() );
       //Console.Out( src_view.Last.GetCount() );
       //Console.Out( dst_view.First.GetCount() );
       //Console.Out( dst_view.Last.GetCount() );
    
       int a_nArraySlices = p_oInputs.m_iArraySlices;
    
       int a_nOffsetW = 0;
       int a_nOffsetH = 0;
    
       // DEBUG
       //Console.Out( ">> " + a_nDimX );
       //Console.Out( ">> " + a_nDimY );
       //Console.Out( ">> " + p_viMapInfo.Z );
       //Console.Out( ">> " + p_viMapInfo.W );
    
       // DEBUG
       //Console.Out( p_oSrc.Width );
       //Console.Out( p_oSrc.Height );
       //Console.Out( p_oDst.Width );
       //Console.Out( p_oDst.Height );
    
       // DEBUG
       //Console.Out( p_oSrc.PixelFormat );
       //Console.Out( p_oDst.PixelFormat );
    
       auto Int32Array a_aiFillParams;
    
       a_aiFillParams.SetCount( 7 );
       a_aiFillParams.Set( 0, a_nDimX );
       a_aiFillParams.Set( 1, a_nDimY );
       a_aiFillParams.Set( 2, 0 ); // We set index 2 in the loop below.
       a_aiFillParams.Set( 3, 0 ); // We set index 3 in the loop below.
       a_aiFillParams.Set( 4, p_oInputs.m_viMapInfo.Z );
       a_aiFillParams.Set( 5, p_oInputs.m_viMapInfo.W );
       a_aiFillParams.Set( 6, p_oSrc.GetChannelCount() );
    
       // DEBUG
       //LibInt32Array.Out( a_aiFillParams );
    
       // DEBUG
       //Console.Out( "p_oInputs.m_viMapInfo.X " + p_oInputs.m_viMapInfo.X ); // Number of chunks to write on X.
       //Console.Out( "p_oInputs.m_viMapInfo.Y " + p_oInputs.m_viMapInfo.Y ); // Number of chunks to write on Y.
       //Console.Out( "p_oInputs.m_viMapInfo.Z " + p_oInputs.m_viMapInfo.Z ); // Chunk pixel width X
       //Console.Out( "p_oInputs.m_viMapInfo.W " + p_oInputs.m_viMapInfo.W ); // Chunk pixel width Y
       //Console.Out( "a_nOffset " + a_nOffset );
    
       src_view.Last.Position = p_oInputs.m_viMapInfo.W;
    
       // DEBUG
       //Console.Out( "a_nOffsetH " + a_nOffsetH );
    
       bool a_bFailed = false;
    
       for( int h = 0; h < p_oInputs.m_viMapInfo.X; ++h )
       {
          a_nOffsetH = a_nDimY - p_oInputs.m_viMapInfo.W;
    
          for( int w = 0; w < p_oInputs.m_viMapInfo.Y; ++w )
          {
             a_aiFillParams.Set( 2, a_nOffsetW );
             a_aiFillParams.Set( 3, a_nOffsetH );
    
             // DEBUG
             //Console.Out( "Pixel Coordinates For Fill Region - " + a_nOffsetW + ", " + a_nOffsetH );
    
             ///*
             bool a_bFill = a_auAlgorithms.FillRegion(
                src_view.First,
                src_view.Last,
                dst_view.First,
                a_aiFillParams );
    
             // DEBUG
             //p_oInputs.m_slMessages.Add( "w, a_nOffsetW = " + a_nOffsetW );
             //p_oInputs.m_slMessages.Add( "h, a_nOffsetH = " + a_nOffsetH );
             //p_oInputs.m_slMessages.Add( "src_view.GetCount() = " + src_view.Last.GetCount() );
             //p_oInputs.m_slMessages.Add( "dst_view.GetCount() = " + dst_view.Last.GetCount() );
    
             if( !( a_bFill ) )
             {
                p_oInputs.m_slMessages.Add( "Call to a_auAlgorithms.FillRegion() failed!" );
    
                // DEBUG
                //p_oInputs.m_slMessages.Add( "h = " + h + " w = " + w );
                //p_oInputs.m_slMessages.Add( "p_oSrc.PixelFormat " + p_oSrc.PixelFormat );
                //p_oInputs.m_slMessages.Add( "p_oDst.PixelFormat " + p_oDst.PixelFormat );
                //return false;
    
                a_bFailed = true;
             }
             //*/
    
             // DEBUG
             //Console.Out( "a_bFill " + a_bFill );
             //Console.Out( "first " + src_view.First.Position );
             //Console.Out( "last  " + src_view.Last.Position );
    
             a_nOffsetH -= p_oInputs.m_viMapInfo.W;
             src_view.First.Position += a_nOffset;
             src_view.Last.Position = src_view.First.Position + a_nOffset;
             //LibInt32Array.Out( a_aiFillParams );
          }
    
          a_nOffsetH = 0;
          a_nOffsetW += p_oInputs.m_viMapInfo.Z;
    
          // DEBUG
          //Console.Out( "***" );
       }
    
       if( a_bFailed )
       {
         p_oInputs.m_slMessages.Add( "Failed to create overhead map!" );
       }
       else
       {
          p_oInputs.m_slMessages.Add( "Successfully created overhead map!" );
       }
    
       return !( a_bFailed );
    }
    
    ///////////////////////////////////////////////////////////////////////////////
    // function
    ///////////////////////////////////////////////////////////////////////////////
    
    function bool BuildArrayTextureFromGeotiff( BuildInputs p_oInputs, Image p_oTiles )
    {
       // Initialize raster/chunk variables from metadata.
       int a_nRasterWidth         = p_oInputs.m_oGdalImageInfo.GetRasterSizeX();
       int a_nRasterHeight        = p_oInputs.m_oGdalImageInfo.GetRasterSizeY();
       int a_nChunkDimX           = p_oInputs.m_oGdalImageInfo.GetChunkDimX();
       int a_nChunkDimY           = p_oInputs.m_oGdalImageInfo.GetChunkDimY();
       int a_nChunkCountX         = p_oInputs.m_viDstRange.Z;
       int a_nChunkCountY         = p_oInputs.m_viDstRange.W;
       int a_nChunkElements       = p_oInputs.m_oGdalImageInfo.GetChunkItemCount();
       int a_nChunkOffset         = p_oInputs.m_oGdalImageInfo.GetChunkOffsetInBytes();
       double a_dMinElevation     = p_oInputs.m_oGdalImageInfo.GetMinElevation();
       double a_dMaxElevation     = p_oInputs.m_oGdalImageInfo.GetMaxElevation();
    
       /*
       // DEBUG
       Console.Out( "a_nRasterWidth   " + a_nRasterWidth );
       Console.Out( "a_nRasterHeight  " + a_nRasterHeight );
       Console.Out( "a_nChunkDimX     " + a_nChunkDimX );
       Console.Out( "a_nChunkDimY     " + a_nChunkDimY );
       Console.Out( "a_nChunkCountX   " + a_nChunkCountX );
       Console.Out( "a_nChunkCountY   " + a_nChunkCountY );
       Console.Out( "a_nChunkElements " + a_nChunkElements );
       Console.Out( "a_nChunkOffset   " + a_nChunkOffset );
       Console.Out( "a_dMinElevation  " + a_dMinElevation );
       Console.Out( "a_dMaxElevation  " + a_dMaxElevation );
       return false;
       */
    
       int a_nTotalWidth = a_nChunkDimX;
       int a_nTotalHeight = p_oTiles.Height;
    
       // Declare objects that will do work for us.
       auto Uint8ArrayAlgorithms a_auAlgorithms;
       auto Float32ArrayAlgorithms a_afAlgorithms;
    
       auto Float32ArrayView f_tiles_view =
          p_oTiles.GetFloat32View();
    
       // Allocate storage for chunks. We can allocate
       // one object here and stream the data from the
       // geotiff into this. We only allocate once.
       auto Float32Array a_afChunkData = new Float32Array(
          a_nChunkDimX * a_nChunkDimY, 0.0 );
       auto Float32ArrayView f_chunk_view = a_afChunkData.GetView();
    
       auto Image a_oTile;
       a_oTile.AllocateBuffer(
          a_nChunkDimX, a_nChunkDimY, Enum.IPF_FP32() );
       auto Float32ArrayView tile_view =
          a_oTile.GetFloat32View();
    
       // Allocate storage for our array transform
       // operations. We need to flip the data after
       // we stream it out so that it's oriented
       // correctly for visual use.
       auto Float32Array a_afFlip = new Float32Array(
          a_nChunkDimX * a_nChunkDimY, 0 );
       auto Float32ArrayView f_flip_view = a_afFlip.GetView();
    
       // Allocate our min/max storage.
       auto Float32Array a_afMinMax = new Float32Array( 2, 0.0 );
       auto Float32Iterator min_max_dst = a_afMinMax.First();
    
       // Allocate storage for our fill parameters.
       // We'll use this to memcpy each chunk into
       // the master Image.
       auto Int32Array a_aiFillParams;
       a_aiFillParams.SetCount( 7 );
       a_aiFillParams.Set( 0, a_nTotalWidth );
       a_aiFillParams.Set( 1, a_nTotalHeight );
       // We set index 2 in the loop below.
       // We set index 3 in the loop below.
       a_aiFillParams.Set( 4, a_nChunkDimX );
       a_aiFillParams.Set( 5, a_nChunkDimY );
       a_aiFillParams.Set( 6, p_oTiles.GetChannelCount() );
       //LibInt32Array.Out( a_aiFillParams );
    
       int a_nChunkCountXCache = a_nChunkCountX;
       int a_nChunkCountYCache = a_nChunkCountY;
    
       // Set these as 1 or 2 or 3 or N to debug.
       //Console.Out( a_nChunkCountXCache );
       //Console.Out( a_nChunkCountYCache );
    
       bool a_bBadMin = a_dMinElevation < 0.0;
       bool a_bBadMax = a_dMaxElevation < 0.0;
       //Console.Alert( "a_bBadMin " + a_bBadMin );
       //Console.Alert( "a_bBadMax " + a_bBadMax );
    
       // Iterate over our chunk indices,
       // streaming data from the geotiff
       // to our local data structures.
       // The looping x/y is a little
       // funky because we're looping
       // across and down, processing
       // chunks as we go.
    
       p_oInputs.m_afMinMaxElevation.Reserve = a_nChunkCountXCache * a_nChunkCountYCache;
       //p_oInputs.m_afMinMaxElevation.Count = 1;
    
       //Console.Out( p_oInputs.m_oGdalImageInfo.GetChunkCountX() );
       //Console.Out( p_oInputs.m_oGdalImageInfo.GetChunkCountY() );
       //return false;
    
       int a_nStartRow = p_oInputs.m_viDstRange.X;
       int a_nStartCol = p_oInputs.m_viDstRange.Y;
    
       int a_nBlitPosY = 0;
    
       //Console.Out( a_nChunkCountXCache );
       //Console.Out( a_nChunkCountYCache );
    
       // Geotiff and other elevation formats are top-down.
       // This can be confusing since Simdify and OpenGL are bottom-up.
       // We extract top-down and write into the array texture bottom-up.
       // This means we follow the 'intended' layout of the data, assuming
       // that whatever chunk the user extracts first from the GEOTIFF
       // should be the first slice in the array texture.
       for( int row = a_nStartRow; row < a_nStartRow + a_nChunkCountXCache; ++row )
       {
          for( int col = a_nStartCol; col < a_nStartCol + a_nChunkCountYCache; ++col )
          {
             //Console.Out( col + ", " + row );
             // Stream out from the geotiff.
             PluginDomainGIS.LoadGeotiffChunk(
                p_oInputs.m_oSourcePath.GetPath(),
                p_oInputs.m_oGdalDataset,
                row,
                col,
                a_afChunkData );
    
             ///*
             if( a_bBadMin )
             {
                a_afAlgorithms.ReplaceIfEqual(
                   f_chunk_view.First,
                   f_chunk_view.Last,
                   a_dMinElevation, 0.0 );
             }
    
             if( a_bBadMax )
             {
                a_afAlgorithms.ReplaceIfEqual(
                   f_chunk_view.First,
                   f_chunk_view.Last,
                   a_dMaxElevation, 0.0 );
             }
             //*/
    
             // Elevation data reports
             // bad min/max values.
             // We will need to find
             // correct min/max values.
             a_afAlgorithms.FindMinMax(
                f_chunk_view.First,
                f_chunk_view.Last,
                min_max_dst );
             p_oInputs.m_afMinMaxElevation.Add(
                a_afMinMax.Get( 0 ) );
             p_oInputs.m_afMinMaxElevation.Add(
                a_afMinMax.Get( 1 ) );
    
             /*
             // DEBUG
             auto StrList a_slInfo;
             a_afAlgorithms.Print2D(
                f_chunk_view.First,
                f_chunk_view.Last,
                a_nChunkDimX,
                a_nChunkDimY,
                a_slInfo );
    
             auto FilePath a_oDumpPath =
             new FilePath( Model.Filename );
             a_oDumpPath.RemoveFileName();
             a_oDumpPath.RemoveEndSeparator();
             string a_sFile = row + "_" + col + ".txt";
             a_oDumpPath.AppendPath( a_sFile );
    
             auto TextFile a_oDoc;
             LibStrList.WriteToDisk(
             a_oDoc, a_oDumpPath, a_slInfo );
             */
    
             // Flip orientation so that the data
             // matches the human visual system.
    
             ///*
             auto Int32Array a_aiParams;
             a_aiParams.Add( Enum.ImageRotateOptions_NoRotation() );
             a_aiParams.Add( Enum.ImageFlipOptions_FlipHeight() );
             a_aiParams.Add( a_nChunkDimX );
             a_aiParams.Add( a_nChunkDimY );
             a_aiParams.Add( 0 );
             //Console.Out( "a_nChunkDimX " + a_nChunkDimX );
             //Console.Out( "a_nChunkDimY " + a_nChunkDimY );
    
             a_afAlgorithms.TransformOrientation(
                f_chunk_view.First,
                f_chunk_view.Last,
                f_flip_view.First,
                a_aiParams );
    
             int succeed = a_afAlgorithms.Copy(
                f_chunk_view.First,
                f_chunk_view.Last,
                tile_view.First );
    
             // Create params we can use to blit
             // the chunk data back into the
             // master Image.
             a_aiFillParams.Set( 2, 0 );
             a_aiFillParams.Set( 3, a_nBlitPosY );
    
             //Console.Out( 0 );
             //Console.Out( a_nBlitPosY );
             //Console.Out( "***" );
    
             // Blit chunk to master image.
             // These are pixel coordinates!
             bool a_bFill = a_afAlgorithms.FillRegion(
                f_flip_view.First,
                f_flip_view.Last,
                f_tiles_view.First,
                a_aiFillParams );
    
             //Console.Out( a_bFill + " " + a_nBlitPosY );
             // Update chunk position on col.
             a_nBlitPosY += a_nChunkDimY;
             //*/
          }
       }
    
       return true;
    }
    
  3. Save changes to the script.

Review Functions

Let's review the functions in this library.

Table 1.1. Review Script Contents

Function Description
SaveTiles This function saves an array texture to disk.
SaveMap This function saves 2D map to disk. This is a 'map' of the array texture, with each slice of the array texture positioned in the correct spot in the 2D grid.
GetCellPixelCoords This function populates an <Int32Array> with the 2D pixel coordinates of a slice of an array texture. These coordinates are the correct 2D position of the array texture in a corresponding 2D map.
GetMinMaxFromSlices This function is for debugging, and it iterates over each slice of an array texture, finding the min/max values in each slice. Note that it makes good use of iterators and algorithms to reduce the amount of boilerplate code needed for searches.
GetMinMaxZFromNormals This function finds the minimum and maximum Z values in each slice of an array texture. It uses <ArrayView> features, iterators, and algorithms to make it easy to perform the search.
ConvertFloat32ToRgba8888 This function implements a custom image conversion routine from FP32 to 8888 ARGB. Each 32-bit floating point elevation texture has only a single channel of floating point data, so the conversion process is fairly easy. We'll need to pack the RGB channels of the RGBA image with quantized pixel values that map the range of the elevation data (say 172 meters to 2404 meters ) correctly onto the range 0-255. First, we decompose the array texture into slices, and store them in a <TypeBuffer> object. Then we iterate over each slice/layer, converting each slice/layer to RGBA 8888, and then filling the alpha channel with pure white. At the end, we repack the slices/layers into an array texture.
ComputeTerrainInfo This function computes information about the terrain and the area under analysis. It sets values in the <BuildInputs> object and prints build log messages into the <BuildInputs> object's internal <StrList> data member.
UnpackImages This function unpacks an array texture into separate <Image> objects and saves them to disk.
CreateMap This function unpacks an array texture into its individual slices/layers, and then copies them into the correct 2D position in a 2D map.
BuildArrayTextureFromGeotiff This function streams data out of the source GEOTIFF, cleans the data, reorients the data to match the human visual system (north is top of the image), and then packs each chunk of the GEOTIFF into a slice/layer of a 32-bit floating point array texture.

Import CPU Image Processing Library Into Service Script

  1. Return to the running text editor.
  2. Find the script named app_service_workload_analyze_terrain_util.ssl.
  3. Find the import statements.

    import library "app_service_assert_util.ssl";
    import library "app_service_build_util.ssl";
    import library "app_service_console_util.ssl";
    import library "app_service_main_util.ssl";
    import library "type_file_path_algorithms.ssl";
    import library "type_float32_array_util.ssl";
    import library "type_image_channel_extract_util.ssl";
    import library "type_image_format_util.ssl";
    import library "type_image_util.ssl";
    import library "type_int32_array_util.ssl";
    import library "type_render3d_util.ssl";
    import library "type_service_enumeration_util.ssl";
    import library "type_str_list_util.ssl";
    import library "app_service_workload_analyze_terrain_classes.ssl";
    
  4. Copy the following import statement and paste it at the end of the existing import statements.

    Copy Text To Clipboard

    import library "app_service_analyze_terrain_cpu_image_processing_util.ssl";

Test Code Changes

  1. Return to the running Layout app.
  2. Select File » Run Service... from the main menu.

    The software displays the service selection dialog. Since we're dynamically loading this script, using the Desktop » Refresh Scripts command won't work. We need to invoke the service in order to compile the script.

    This is a picture of the service selection dialog.

    If there are script errors, you'll see the following dialog:

    This is a picture of script error dialog.

    You'll also see script compiler errors in the output window:

    D:\release6\scripts\app_service_analyze_terrain_cpu_image_processing_util.ssl(31) : error: undeclared identifier 'a'
    A runtime error ocurred:
    
    Error:       RUNTIME_STATUS_NULL_OBJECT_POINTER
    Function:    ApplicationRunService
    File:        D:\release6\scripts\app_service_file_menu_scripts.ssl
    Line:        1346
    
    Call Stack:
       ApplicationRunService()  app_service_file_menu_scripts.ssl(1346)
    
    Script execution has been stopped.
    

    If there are errors, return to the start of this tutorial and retrace your steps. Otherwise, continue to the next step.

  3. Double click the entry named Spa.Service.AnalyzeTerrain.
  4. The service runs and then displays the build log in the output window.

    --- <Executing Service 'Spa.Service.AnalyzeTerrain'> ---
    
    Executed service: Spa.Service.AnalyzeTerrain
    

    The service runs correctly.