Skip to main content

PINGTile

PyPI - Version

Utility to tile sonar mosaics and maps.

Table of Contents

Installation

  1. Install Miniforge.
  2. Open the Miniforge prompt.
  3. Install PINGInstaller:
    pip install pinginstaller
    
  4. Install PINGTile.
    python -m pinginstaller pingtile
    

Quick Start

  1. Create a working folder with:
    • a raster mosaic directory (for example, *.tif sonar files)
    • a polygon label file (for example, *.shp)
  2. Use the script in Usage as your starter script.
  3. Update the Parameters block values for your paths, class mapping, and tile settings.
  4. Select the pingtile interpreter/environment in VS Code.
  5. Run the script with F5.

Input and Output Expectations

  • Sonar inputs:
    • GeoTIFF rasters (.tif or .tiff) discovered recursively under sonarDir.
  • Label inputs:
    • Vector polygons from inFileMask (for example, Shapefile).
    • The class field must exist and match classFieldName.
  • CRS/projection:
    • Inputs are reprojected to epsg_out for processing.
  • Outputs:
    • Tiled images in images.
    • Tiled label rasters in labels.
    • Optional plots in plots.
    • Optional COCO JSON in json/_annotations.coco.json.

Usage

  1. Copy the following script to some location on your computer:
'''
Copyright (c) 2025 Cameron S. Bodine
'''

#########
# Imports

import os, sys
from joblib import Parallel, delayed, cpu_count

# For Package
from pingtile.imglbl2tile import doImgLbl2tile
from pingtile.utils import mask_to_coco_json

import rasterio as rio
import json

############
# Parameters

map = r'Z:\tmp\pingtile_test\map\Model_Training_Substrate_Polygons_Export.shp'
sonarDir = r'Z:\tmp\pingtile_test\mosaic'

outDirTop = r'Z:\tmp\pingtile_test'
outName = 'Hudson'

classCrossWalk = {
    '0':0,
    'U':1,
    'G':2,
    'B_C':3,
    'B':4
}

windowSize_m = [
                (12,12),
                (18,18),
                (24,24),
                ]

windowStride = 3
classFieldName = 'Substrate_'
minArea_percent = 0.5
target_size = (512, 512) #(1024, 1024)
threadCnt = 0.75
epsg_out = 32616
doPlot = True
lbl2COCO = True
allowNoMapTiles = False  # Set True to also export sonar-covered tiles that have no map overlap.

if not os.path.exists(outDirTop):
    os.makedirs(outDirTop)


###############################################
# Specify multithreaded processing thread count
if threadCnt==0: # Use all threads
    threadCnt=cpu_count()
elif threadCnt<0: # Use all threads except threadCnt; i.e., (cpu_count + (-threadCnt))
    threadCnt=cpu_count()+threadCnt
    if threadCnt<0: # Make sure not negative
        threadCnt=1
elif threadCnt<1: # Use proportion of available threads
    threadCnt = int(cpu_count()*threadCnt)
    # Make even number
    if threadCnt % 2 == 1:
        threadCnt -= 1
else: # Use specified threadCnt if positive
    pass

if threadCnt>cpu_count(): # If more than total avail. threads, make cpu_count()
    threadCnt=cpu_count();
    print("\nWARNING: Specified more process threads then available, \nusing {} threads instead.".format(threadCnt))

print("\nUsing {} threads for processing.\n".format(threadCnt))


# Find all sonar files
sonarFiles = []
for root, dirs, files in os.walk(sonarDir):
    for file in files:
        if file.lower().endswith('.tif') or file.lower().endswith('.tiff'):
            sonarFiles.append(os.path.join(root, file))


for windowSize in windowSize_m:

    # windowStride_m = windowStride*windowSize[0]
    windowStride_m = windowStride
    # minArea = minArea_percent * windowSize[0]*windowSize[1]

    dirName = f"{windowSize[0]}_{windowSize[0]}"
    outDir = os.path.join(outDirTop, dirName)
    outSonDir = os.path.join(outDir, 'images')
    outMaskDir = os.path.join(outDir,'labels')
    pltDir = os.path.join(outDir,'plots')

    if not os.path.exists(outSonDir):
        os.makedirs(outSonDir)
        os.makedirs(outMaskDir)
        os.makedirs(pltDir)

    for sonarFile in sonarFiles:

        print(f"\nProcessing {os.path.basename(sonarFile)} with windowSize: {windowSize} and windowStride_m: {windowStride_m}...\n")

        doImgLbl2tile(inFileSonar=sonarFile,
                      inFileMask=map,
                      outDir=outDir,
                      outName=outName,
                      epsg_out=epsg_out,
                      classCrossWalk=classCrossWalk,
                      windowSize=windowSize,
                      windowStride_m=windowStride_m,
                      classFieldName=classFieldName,
                      minArea_percent=minArea_percent,
                      target_size=target_size,
                      threadCnt=threadCnt,
                      doPlot=doPlot,
                    allowNoMapTiles=allowNoMapTiles,
                    grayscale=False,
                      )

# Convert masks to COCO format
if lbl2COCO:
    

    for windowSize in windowSize_m:

        dirName = f"{windowSize[0]}_{windowSize[0]}"
        outDir = os.path.join(outDirTop, dirName)
        outSonDir = os.path.join(outDir, 'images')
        outMaskDir = os.path.join(outDir,'labels')
        pltDir = os.path.join(outDir,'plots')
        outJsonDir = os.path.join(outDir,'json')

        if not os.path.exists(outJsonDir):
            os.makedirs(outJsonDir)

        print(f"\nConverting to COCO format for windowSize: {windowSize}...\n")

        # Get the mask files
        maskFiles = []
        for root, dirs, files in os.walk(outMaskDir):
            for file in files:
                if file.lower().endswith(('.tif', '.tiff', '.png', '.jpg', '.jpeg')):
                    maskFiles.append(os.path.join(root, file))

        maskFiles=maskFiles[:10] # Debug limit to 10 files

        # Build categories list / lookup from classCrossWalk
        # categories_info passed to mask_to_coco_json should map id -> name
        categories_info = {v: str(k) for k, v in classCrossWalk.items()}
        # COCO categories (exclude background id 0 if present)
        categories = [{"id": v, "name": str(k)} for k, v in classCrossWalk.items() if v != 0]

        coco = {
            "info": {"description": outName or ""},
            "licenses": [],
            "images": [],
            "annotations": [],
            "categories": categories
        }

        annotation_id = 1
        image_id = 1

        for mask_path in maskFiles:
            base = os.path.splitext(os.path.basename(mask_path))[0]

            # try to find corresponding image filename in images folder (same base name)
            matched_image = None
            for ext in ('.png', '.jpg', '.jpeg', '.tif', '.tiff'):
                candidate = os.path.join(outSonDir, base + ext)
                if os.path.exists(candidate):
                    matched_image = os.path.basename(candidate)
                    break
            if matched_image is None:
                # fallback to mask basename (acceptable as file_name in COCO)
                matched_image = os.path.basename(mask_path)

            # read mask to get width/height
            try:
                with rio.open(mask_path) as src:
                    width, height = src.width, src.height
            except Exception as e:
                print(f"Skipping {mask_path}: cannot read ({e})")
                continue

            image_info = {
                "id": image_id,
                "file_name": matched_image,
                "width": width,
                "height": height
            }

            # mask_to_coco_json should return (annotations_list, next_annotation_id)
            anns, annotation_id = mask_to_coco_json(mask_path, image_info, categories_info, annotation_id)

            if anns:
                coco["images"].append(image_info)
                coco["annotations"].extend(anns)
                image_id += 1

        out_json = os.path.join(outJsonDir, f"_annotations.coco.json")
        with open(out_json, "w") as f:
            json.dump(coco, f)
  1. Open the file with Visual Studio Code.
  2. Update the Parameters as necessary:
############
# Parameters

map = r'Z:\tmp\pingtile_test\map\Model_Training_Substrate_Polygons_Export.shp'
sonarDir = r'Z:\tmp\pingtile_test\mosaic'

outDirTop = r'Z:\tmp\pingtile_test'
outName = 'Hudson'

classCrossWalk = {
    '0':0,
    'U':1,
    'G':2,
    'B_C':3,
    'B':4
}

windowSize_m = [
                (12,12),
                (18,18),
                (24,24),
                ]

windowStride = 3
classFieldName = 'Substrate_'
minArea_percent = 0.5
target_size = (512, 512) #(1024, 1024)
threadCnt = 0.75
epsg_out = 32616
doPlot = True
lbl2COCO = True
  1. Ensure the pingtile environment is selected as the Interpreter see this.
  2. Run the script in debug mode by pressing F5.

Parameter Reference

Parameter Type Description Typical Values
map str Path to vector labels file. Z:\...\labels.shp
sonarDir str Directory containing sonar rasters. Z:\...\mosaic
outDirTop str Root output directory. Z:\tmp\pingtile_test
outName str Prefix used in output naming/metadata. Hudson
classCrossWalk dict[str,int] Class name to class id mapping. {'U':1, 'G':2, ...}
windowSize_m list[tuple[int,int]] Tile window sizes to run. [(12,12), (18,18)]
windowStride int Window stride in meters. 1 to 6
classFieldName str Label field name in the map file. Substrate_
minArea_percent float Minimum non-zero label fraction required to keep a tile. 0.1 to 0.7
target_size tuple[int,int] Output tile dimensions. (512, 512)
threadCnt int or float Processing thread policy (0, <0, <1, >=1). 0.75
epsg_out int Processing CRS EPSG code. 32616
doPlot bool Enable generation of diagnostic plots. True / False
lbl2COCO bool Convert label outputs to COCO JSON. True / False

Expected Output Layout

After running for multiple window sizes, the output directory will look similar to:

outDirTop/
    12_12/
        images/
        labels/
        plots/
        json/
            _annotations.coco.json
    18_18/
        images/
        labels/
        plots/
        json/
            _annotations.coco.json

Troubleshooting

  • No tiles generated:
    • Lower minArea_percent.
    • Confirm label polygons overlap the sonar raster extent.
  • Empty or sparse labels:
    • Verify classFieldName exists in the label file.
    • Confirm classCrossWalk keys match label attribute values.
  • CRS mismatch symptoms (offset labels/images):
    • Ensure both raster and vector sources are georeferenced.
    • Set epsg_out to a projected CRS appropriate for your area.
  • Slow execution:
    • Reduce number of windowSize_m entries.
    • Increase windowStride.
    • Tune threadCnt (for example, 0.5 to 0.9).
  • GDAL/raster I/O issues:
    • Run from the pingtile conda environment.
    • Confirm rasterio and GDAL are installed from compatible channels.

Upload Dataset to Roboflow

It is possible to upload your dataset to Roboflow with the following script:

import glob
from roboflow import Roboflow

# Initialize Roboflow client
rf = Roboflow(api_key="ADD_ROBOFLOW_API_KEY_HERE") #More info: https://docs.roboflow.com/developer/authentication/find-your-roboflow-api-key

# Directory path and file extension for images
dir_name = r"Z:\tmp\pingtile_test\12_12\json"
file_extension_type = ".png"

# Annotation file path and format (e.g., .coco.json)
annotation_filename = r"Z:\tmp\pingtile_test\12_12\json\_annotations.coco.json"

# Get the upload project from Roboflow workspace
project = rf.workspace().project("ADD_PROJECT_NAME_HERE")

# Upload images
image_glob = glob.glob(dir_name + '/*' + file_extension_type)
for image_path in image_glob:
    try:
        result = project.single_upload(
            image_path=image_path,
            annotation_path=annotation_filename,
        )
        # Roboflow returns a dict; check for an error key or a successful upload URL
        if result.get("error"):
            print(f"Upload failed for {image_path}: {result['error']}")
        else:
            print(f"Uploaded {image_path} -> {result.get('image') or result}")
    except Exception as e:
        print(f"Error uploading {image_path}: {e}")

Release files for pingtile 1.1.14

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pingtile 1.1.14
File Size Uploaded
pingtile-1.1.14.tar.gz 62.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pingtile 1.1.14
File Interpreter ABI Platform
pingtile-1.1.14-py3-none-any.whl Python 3 none any Details

Total release size: 124.3 kB

Release files / pingtile-1.1.14.tar.gz

Download URL pingtile-1.1.14.tar.gz
Size 62.4 kB
Tags Source
SHA-256 checksum
How to use checksums
d4c680ea2ca67dac7cd6a338a0467ba810b5f9329e76086bb7212eb7da44d70f
BLAKE2b-256 checksum
How to use checksums
a724be04c55a07efd3b1e4ca12469cd0120919c35458a6f982b1ee29d574be93
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.14

Release files / pingtile-1.1.14-py3-none-any.whl

Download URL pingtile-1.1.14-py3-none-any.whl
Size 61.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d171efb7c9421c2c0b64920abf73028a7a6400b093fde127e84dcdd0ed5a06b5
BLAKE2b-256 checksum
How to use checksums
130a588ec3b01784628001b7a983bb83c88037c83830e2a05458d50745b791fd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.14

Release history Release notifications | RSS feed

This release

1.1.14 This release

2 release files

1.1.13

2 release files

1.1.12

2 release files

1.1.11

2 release files

1.1.10

2 release files

1.1.9

2 release files

1.1.8

2 release files

1.1.7

2 release files

1.1.6

2 release files

1.1.5

2 release files

1.1.4

2 release files

1.1.3

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page