A Streamlit web application for estimating the number of shipping containers in high-resolution aerial imagery using image tiling, Segment Anything Model segmentation, LiDAR point-cloud elevation information, and DSM raster elevation statistics.
The app allows a user to:
- Load an RGB GeoTIFF image
- Load a corresponding LAS point cloud
- Load a corresponding DSM GeoTIFF
- Subset a large image using image tiles
- Segment likely container stacks using Segment Anything
- Estimate stack heights using LiDAR and DSM elevation information
- Estimate the number of containers in each stack
- View segmentation results and a 3D visualisation of detected container stacks
The app has two main tabs:
-
Subset image Used to select the input RGB TIFF, LAS file, DSM TIFF, and image tile.

-
Count containers Used to run segmentation, calculate elevation-based container counts, and visualise the results.

.
├── app.py
├── scripts/
│ ├── subset_image.py
│ └── count_containers.py
├── data/
│ └── containers.geojson
├── models/
│ └── sam_vit_b_01ec64.pth
├── requirements.txt
├── README.md
└── .gitignore
The app expects three input files:
RGB GeoTIFF image: .tif or .tiff
LAS point cloud: .las
DSM GeoTIFF: .tif or .tiff
The RGB image, LAS point cloud, and DSM should cover the same area and should ideally be in the same coordinate reference system.
Large aerial GeoTIFFs are impractical to process in one pass. The app converts the RGB TIFF to a JPEG preview and divides it into a configurable grid of tiles. The user selects one tile, and its pixel coordinates are mapped back to the original TIFF's geospatial reference frame using rasterio's Window and window_bounds giving an exact bounding box in the image's CRS for all downstream steps.
The selected tile is passed to the Segment Anything Model (ViT-B), running in automatic mask generation mode. Rather than requiring manual prompts, SAM samples a dense grid of points across the image (points_per_side=32) and generates candidate segmentation masks for every distinct region it finds. Each mask is filtered by predicted IOU (≥ 0.80), stability score (≥ 0.80), and minimum region area (≥ 500 px) to discard noise and poorly-defined segments.
SAM is well-suited to this task because it generalises to arbitrary shapes without domain-specific fine-tuning shipping containers form visually distinct rectangular regions in aerial imagery that the model segments reliably.
Each binary mask is converted to a contour using OpenCV, and cv2.minAreaRect fits a tight rotated bounding box around it. Rotated boxes are used rather than axis-aligned boxes because containers are rarely perfectly north-aligned in aerial imagery.
The four corner points of each box are then converted from pixel coordinates to real-world coordinates using the tile's affine transform, producing a GeoJSON polygon collection in EPSG:32631 with per-polygon area and aspect ratio attributes.
Two elevation sources are used together because they capture different things:
- LiDAR (LAS): The point cloud is filtered to the tile's bounding box, and a spatial join assigns each LiDAR return to the polygon it falls within. Per-polygon statistics are computed including mean, median, max, and standard deviation of the Z values.
- DSM (Digital Surface Model): Zonal statistics from the DSM raster give the local ground elevation directly beneath each polygon, used as the reference elevation for height calculations.
Stack height is calculated as: elev = mean_z (LiDAR) − dsm_mean (DSM)
The number of containers in a stack is estimated by dividing elev by the standard container height of approximately 2.6 metres.
The final GeoDataFrame is reprojected to WGS84 and rendered as an extruded PolygonLayer in pydeck. Each polygon is extruded to its calculated stack height, and the fill colour is sampled directly from the mean RGB value of the corresponding pixels in the source image so containers appear in their actual colours.
This app uses the Segment Anything ViT-B checkpoint: ViT-B SAM model.
The model checkpoint is not included in this repository because it is a large file.
Download the checkpoint from here, then place it in the models/ folder so that the final path is:
models/sam_vit_b_01ec64.pth
Make sure the filename matches exactly.
SAM has no concept of what a shipping container is. It segments regions based purely on visual boundaries contrast, texture, and edge information without any semantic understanding. This is both a strength and a limitation. It means SAM generalises well to aerial imagery without retraining, but it also means it will confidently segment shadows, road markings, and rooftops alongside containers. The elevation-based filtering steps downstream do the semantic heavy lifting of deciding which segments are actually containers.
Several filtering steps and fixed assumptions are applied during the container counting process. These are currently hardcoded and were tuned for the original dataset. If you are applying the app to a different site, these are the values most likely to need adjustment.
| Parameter | Value | Purpose |
|---|---|---|
pred_iou_thresh |
0.80 | Removes masks where SAM has low confidence in boundary quality |
stability_score_thresh |
0.80 | Removes masks that are inconsistent across threshold perturbations |
min_mask_region_area |
500 px | Removes very small segments likely to be noise |
After elevation statistics are added, a second round of filtering removes non-container detections:
std_z < 8.5- removes polygons with high vertical variance within their footprint, typically narrow tall structures such as floodlight poles rather than flat-topped container stacks.mean_z > 50- removes ground-level detections by requiring a minimum absolute elevation. This value reflects the site's ground elevation and will need to be updated for different locations.area_pxbetween 2,000 and 15,000 - removes segments that are too small (noise) or too large (buildings, open ground) to plausibly be individual container stacks.elev >= 2.6- removes any polygon whose calculated above-ground height is less than 2.6 metres, filtering out detections sitting at or near ground level.
git clone https://github.com/YOUR-USERNAME/container-counter-app.git
cd container-counter-appconda create -n container-counter python=3.11 -yActivate the environment:
conda activate container-counterInstall the required Python packages:
pip install -r requirements.txtDepending on your operating system, some geospatial libraries may install more reliably through Conda. If rasterio, geopandas, or related packages fail with pip, try:
conda install -c conda-forge rasterio geopandas shapely fiona pyproj -y
pip install -r requirements.txtFrom the project root folder, run:
streamlit run app.pyThis will start the Streamlit app and open it in your browser.
If it does not open automatically, copy the local URL shown in the terminal, usually something like:
http://localhost:8501
Run:
streamlit run app.pyIn the Subset image tab, enter paths to:
RGB TIFF file
LAS file
DSM TIFF file
Example:
data/rgb_image.tif
data/point_cloud.las
data/dsm.tif
Currently, the main available subset method is:
Tiles
The app converts the RGB TIFF into a JPEG preview, divides it into rows and columns, and allows the user to select one tile.
Choose the number of tile rows and tile columns.
For example:
Tile rows: 4
Tile columns: 4
The app will generate image tiles and display them in a selectable grid.
Click one of the displayed tiles.
The selected tile is mapped back to the original TIFF pixel coordinates using metadata generated during tiling.
Click:
Start container count
The app will:
- Generate segmentation masks using Segment Anything
- Convert masks to rotated bounding boxes
- Convert bounding boxes to GeoJSON polygons
- Filter the LAS point cloud to the selected image bounds
- Crop the DSM to the same selected image bounds
- Add LiDAR and DSM elevation statistics to each detected polygon
- Estimate the number of container stacks and containers
- Display a 3D visualisation
The app writes temporary and generated outputs to the outputs/ folder.
Typical generated outputs include:
outputs/preview_image.jpg
outputs/preview_tiles/
outputs/preview_tiles/user_cropped_aoi_image.tif
outputs/preview_tiles/container_segements.geojson
outputs/preview_tiles/filtered_las.las
outputs/preview_tiles/cropped_dsm.tif
These outputs are generated automatically when the app runs.
The RGB TIFF, DSM TIFF, and LAS point cloud should be in the same coordinate reference system. If they are not, the point-cloud filtering and DSM-based elevation statistics may not align correctly with the selected image tile.
The app expects the SAM checkpoint at:
models/sam_vit_b_01ec64.pth
If the checkpoint is missing, the segmentation step will fail.
The app uses PyTorch and Segment Anything. If CUDA is available, the model can run on GPU. If CUDA is not available, it will run on CPU, but segmentation may be slower.
- The BBOX subset option is not yet implemented.
- The app currently relies on file paths entered by the user rather than file upload widgets.
- Results depend on the quality and alignment of the RGB TIFF, LAS point cloud, and DSM.
- The container count estimate assumes a standard container height of approximately 2.6 metres.
- Some filtering thresholds are currently hard-coded and adjustments are not yer implemented for different sites or datasets.
- Implementing the BBOX drawing workflow
- Adding CRS validation between RGB TIFF, DSM, and LAS inputs
- Adding user controls for container height and filtering thresholds
- Saving final results as GeoJSON, CSV, or GeoPackage
- A download button for outputs
- Better error messages when files are missing or misaligned
MIT License
Developed by Mary Muthee, Pedro Alfonso and Jedidiah Chibinga.