Skip to content

Commit

Permalink
Merge pull request #31 from hotosm/feature/yolo_v8_v2
Browse files Browse the repository at this point in the history
Feature : Yolo v8 Seg Integration
  • Loading branch information
kshitijrajsharma authored Nov 21, 2024
2 parents 403b2ed + da44aa2 commit ec672e8
Show file tree
Hide file tree
Showing 40 changed files with 994 additions and 138 deletions.
19 changes: 10 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.8
python-version: 3.9.20

- name: Remove solaris
run: sudo rm -rf ./docker ./weights
Expand Down Expand Up @@ -46,19 +46,20 @@ jobs:
- name: Install tensorflow
run: pip install tensorflow==2.9.2

- name : Install pytorch and ultralytics
run: pip install torch==1.12.1+cu113 torchvision==0.13.1+cu113 torchaudio==0.12.1 --extra-index-url https://download.pytorch.org/whl/cu113 ultralytics==8.1.6
- name: Install pytorch and ultralytics
run: pip install torch==2.5.1 torchvision==0.20.1 torchaudio==2.5.1 --extra-index-url https://download.pytorch.org/whl/cu113 ultralytics==8.3.26

- name: Install fair utilities
run: pip install -e .

- name: Run test ramp
run: |
pip uninstall -y gdal
pip install numpy
pip install numpy==1.26.4
pip install GDAL==$(gdal-config --version) --global-option=build_ext --global-option="-I/usr/include/gdal"
python test_app.py
python test_ramp.py
- name : Run test yolo
run : |
python test_yolo.py
- name: Run test yolo
run: |
python test_yolo_v1.py
python test_yolo_v2.py
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
## v3.0.0 (2024-11-15)

### Feat

- **yololib**: adds lib required for packaging yolo code

### Fix

- **bundlelib**: bundles lib itself with new pandas version

## v2.0.0 (2024-11-15)

### Feat

- **yololib**: adds lib required for packaging yolo code
- replace FastSAM with YOLO including training
- add FastSAM inference

### Fix

- **postprocessing/utils**: resolve OAM-x-y-z.mask.tif
- **predict**: support both .png and .tif in inference

## v1.3.0 (2024-10-04)

### Feat
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Initially lib was developed during Open AI Challenge with [Omdeena](https://omde

Installing all libraries could be pain so we suggest you to use docker , If you like to do it bare , You can follow `.github/build.yml`

<!-- comment -->

Clone repo

Expand Down
7 changes: 4 additions & 3 deletions hot_fair_utilities/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .georeferencing import georeference
from .inference import predict, evaluate
from .inference import evaluate, predict
from .postprocessing import polygonize, vectorize
from .preprocessing import preprocess
from .training import train
from .preprocessing import preprocess, yolo_v8_v1

# from .training import ramp, yolo_v8_v1
from .utils import bbox2tiles, tms2img
6 changes: 3 additions & 3 deletions hot_fair_utilities/georeferencing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .utils import get_bounding_box


def georeference(input_path: str, output_path: str, is_mask=False) -> None:
def georeference(input_path: str, output_path: str, is_mask=False,epsg=3857) -> None:
"""Perform georeferencing and remove the fourth band from images (if any).
CRS of the georeferenced images will be EPSG:3857 ('WGS 84 / Pseudo-Mercator').
Expand All @@ -38,7 +38,7 @@ def georeference(input_path: str, output_path: str, is_mask=False) -> None:
out_file = f"{output_path}/{filename}.tif"

# Get bounding box in EPSG:3857
x_min, y_min, x_max, y_max = get_bounding_box(filename)
x_min, y_min, x_max, y_max = get_bounding_box(filename,epsg=epsg)

# Use one band for masks and the first three bands for images
bands = [1] if is_mask else [1, 2, 3]
Expand All @@ -51,7 +51,7 @@ def georeference(input_path: str, output_path: str, is_mask=False) -> None:
format="GTiff",
bandList=bands,
outputBounds=[x_min, y_max, x_max, y_min],
outputSRS="EPSG:3857",
outputSRS=f"EPSG:{epsg}",
)
# Close dataset
_ = None
17 changes: 13 additions & 4 deletions hot_fair_utilities/inference/evaluate.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
# Patched from ramp-code.scripts.calculate_accuracy.iou created for ramp project by [email protected]

# Standard library imports
from pathlib import Path

# Third party imports
import geopandas as gpd

from ramp.utils.eval_utils import get_iou_accuracy_metrics
try:
# Third party imports
from ramp.utils.eval_utils import get_iou_accuracy_metrics
except ImportError:
print("Ramp eval metrics are not available, Possibly ramp is not installed")


def evaluate(test_path, truth_path, filter_area_m2=None, iou_threshold=0.5, verbose=False):
def evaluate(
test_path, truth_path, filter_area_m2=None, iou_threshold=0.5, verbose=False
):
"""
Calculate precision/recall/F1-score based on intersection-over-union accuracy evaluation protocol defined by RAMP.
Expand All @@ -29,9 +38,9 @@ def evaluate(test_path, truth_path, filter_area_m2=None, iou_threshold=0.5, verb
truth_df, test_df = gpd.read_file(str(truth_path)), gpd.read_file(str(test_path))
metrics = get_iou_accuracy_metrics(test_df, truth_df, filter_area_m2, iou_threshold)

n_detections = metrics['n_detections']
n_detections = metrics["n_detections"]
n_truth = metrics["n_truth"]
n_truepos = metrics['true_pos']
n_truepos = metrics["true_pos"]
n_falsepos = n_detections - n_truepos
n_falseneg = n_truth - n_truepos
agg_precision = n_truepos / n_detections
Expand Down
34 changes: 24 additions & 10 deletions hot_fair_utilities/inference/predict.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@

from ..georeferencing import georeference
from ..utils import remove_files
from .utils import open_images, save_mask, initialize_model
from .utils import initialize_model, open_images, save_mask

BATCH_SIZE = 8
IMAGE_SIZE = 256
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"


def predict(
checkpoint_path: str, input_path: str, prediction_path: str, confidence: float = 0.5, remove_images=True
checkpoint_path: str,
input_path: str,
prediction_path: str,
confidence: float = 0.5,
remove_images=True,
) -> None:
"""Predict building footprints for aerial images given a model checkpoint.
Expand All @@ -46,7 +50,7 @@ def predict(
"""
start = time.time()
print(f"Using : {checkpoint_path}")
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = initialize_model(checkpoint_path, device=device)
print(f"It took {round(time.time()-start)} sec to load model")
start = time.time()
Expand Down Expand Up @@ -74,14 +78,24 @@ def predict(
)
elif isinstance(model, YOLO):
for idx in range(0, len(image_paths), BATCH_SIZE):
batch = image_paths[idx:idx + BATCH_SIZE]
for i, r in enumerate(model(batch, stream=True, conf=confidence, verbose=False)):
if r.masks is None:
preds = np.zeros((IMAGE_SIZE, IMAGE_SIZE,), dtype=np.float32)
batch = image_paths[idx : idx + BATCH_SIZE]
for i, r in enumerate(
model.predict(batch, conf=confidence, imgsz=IMAGE_SIZE, verbose=False)
):
if hasattr(r, "masks") and r.masks is not None:
preds = (
r.masks.data.max(dim=0)[0].detach().cpu().numpy()
) # Combine masks and convert to numpy

else:
preds = r.masks.data.max(dim=0)[0] # dim=0 means to take only footprint
preds = torch.where(preds > confidence, torch.tensor(1), torch.tensor(0))
preds = preds.detach().cpu().numpy()
preds = np.zeros(
(
IMAGE_SIZE,
IMAGE_SIZE,
),
dtype=np.float32,
) # Default if no masks

save_mask(preds, str(f"{prediction_path}/{Path(batch[i]).stem}.png"))
else:
raise RuntimeError("Loaded model is not supported")
Expand Down
4 changes: 3 additions & 1 deletion hot_fair_utilities/postprocessing/building_footprint.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Standard library imports
import collections

# Third party imports
from geopandas import GeoSeries
from shapely.geometry import MultiPolygon, Polygon
from shapely.ops import unary_union
Expand Down Expand Up @@ -75,4 +77,4 @@ def extract(self, tile, mask):
self.features.append(feature)

def save(self, out):
GeoSeries(self.features).set_crs(CRS).to_file(out)
GeoSeries(self.features).set_crs(CRS).to_file(out, driver="GeoJSON")
5 changes: 3 additions & 2 deletions hot_fair_utilities/postprocessing/merge_polygons.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from shapely.geometry import MultiPolygon, Polygon
from shapely.validation import make_valid
from tqdm import tqdm
import os

from .utils import UndirectedGraph, make_index, project, union

Expand All @@ -19,7 +20,7 @@ def merge_polygons(polygons_path, new_polygons_path, distance_threshold):
new_polygons_path: Path to GeoJSON file where the merged polygons will be saved
distance_threshold: Minimum distance to define adjacent polygons, in meters
"""
gdf = read_file(polygons_path)
gdf = read_file(os.path.relpath(polygons_path))
shapes = list(gdf["geometry"])

graph = UndirectedGraph()
Expand Down Expand Up @@ -71,4 +72,4 @@ def unbuffered(shape):
features.append(feature)

gs = GeoSeries(features).set_crs(SOURCE_CRS)
gs.simplify(TOLERANCE).to_file(new_polygons_path)
gs.simplify(TOLERANCE).to_file(new_polygons_path, driver="GeoJSON")
11 changes: 6 additions & 5 deletions hot_fair_utilities/postprocessing/vectorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
AREA_THRESHOLD = 5


def vectorize(input_path: str, output_path: str , tolerance: float = 0.5, area_threshold: float = 5) -> None:
def vectorize(
input_path: str, output_path: str, tolerance: float = 0.5, area_threshold: float = 5
) -> None:
"""Polygonize raster tiles from the input path.
Note that as input, we are expecting GeoTIF images with EPSG:3857 as
Expand Down Expand Up @@ -52,15 +54,14 @@ def vectorize(input_path: str, output_path: str , tolerance: float = 0.5, area_t
polygons = [
Polygon(poly.exterior.coords)
for poly in polygons
if poly.area != max_area
and poly.area / median_area > area_threshold
if poly.area != max_area and poly.area / median_area > area_threshold
]

gs = gpd.GeoSeries(polygons, crs=kwargs["crs"]).simplify(tolerance)
gs = remove_overlapping_polygons(gs)
if gs.empty:
raise ValueError("No Features Found")
gs.to_crs("EPSG:4326").to_file(output_path)
gs.to_crs("EPSG:4326").to_file(output_path, driver="GeoJSON")


def remove_overlapping_polygons(gs: gpd.GeoSeries) -> gpd.GeoSeries:
Expand All @@ -79,4 +80,4 @@ def remove_overlapping_polygons(gs: gpd.GeoSeries) -> gpd.GeoSeries:
else:
to_remove.add(j)

return gs.drop(list(to_remove))
return gs.drop(list(to_remove))
1 change: 1 addition & 0 deletions hot_fair_utilities/preprocessing/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .preprocess import preprocess
from .yolo_v8_v1 import yolo_format
25 changes: 18 additions & 7 deletions hot_fair_utilities/preprocessing/clip_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from glob import glob
from pathlib import Path

# Third party imports
# Third-party imports
import geopandas
from osgeo import gdal
Expand All @@ -13,7 +14,12 @@


def clip_labels(
input_path: str, output_path: str, rasterize=False, rasterize_options=None
input_path: str,
output_path: str,
rasterize=False,
rasterize_options=None,
all_geojson_file=None,
epsg=3857,
) -> None:
"""Clip and rasterize the GeoJSON labels for each aerial image.
Expand Down Expand Up @@ -71,24 +77,29 @@ def clip_labels(
glob(f"{input_path}/*.png"), desc=f"Clipping labels for {Path(input_path).stem}"
):
filename = Path(path).stem
geojson_file_all_labels = f"{output_path}/labels_epsg3857.geojson"
if all_geojson_file:
geojson_file_all_labels = all_geojson_file
else:
geojson_file_all_labels = f"{output_path}/labels_epsg3857.geojson"
clipped_geojson_file = f"{output_geojson_path}/{filename}.geojson"

# Bounding box as a tuple
x_min, y_min, x_max, y_max = get_bounding_box(filename)
x_min, y_min, x_max, y_max = get_bounding_box(filename, epsg=epsg)
# Bounding box as a polygon
bounding_box_polygon = box(x_min, y_min, x_max, y_max)

# Read all labels into a GeoDataFrame, clip it and
# write to GeoJSON
gdf_all_labels = geopandas.read_file(geojson_file_all_labels)
gdf_all_labels = geopandas.read_file(os.path.relpath(geojson_file_all_labels))
gdf_clipped = gdf_all_labels.clip(bounding_box_polygon)
if len(gdf_clipped) > 0:
gdf_clipped.to_file(clipped_geojson_file)
gdf_clipped.to_file(clipped_geojson_file, driver="GeoJSON")
else:
schema = {"geometry": "Polygon", "properties": {"id": "int"}}
crs = "EPSG:3857"
gdf_clipped.to_file(clipped_geojson_file, schema=schema, crs=crs)
crs = f"EPSG:{epsg}"
gdf_clipped.to_file(
clipped_geojson_file, schema=schema, crs=crs, driver="GeoJSON"
)

# Rasterizing
if rasterize:
Expand Down
7 changes: 4 additions & 3 deletions hot_fair_utilities/preprocessing/fix_labels.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Third party imports
import geopandas
from shapely.validation import explain_validity, make_valid

import os

def remove_self_intersection(row):
"""Fix self-intersections in the polygons.
Expand Down Expand Up @@ -29,8 +29,9 @@ def fix_labels(input_path: str, output_path: str) -> None:
input_path: Path to the GeoJSON file where the input data are stored.
output_path: Path to the GeoJSON file where the output data will go.
"""
gdf = geopandas.read_file(input_path)
gdf = geopandas.read_file(os.path.relpath(input_path))
# print(gdf)
if gdf.empty:
raise ValueError("Error: gdf is empty, No Labels found : Check your labels")
gdf["geometry"] = gdf.apply(remove_self_intersection, axis=1)
gdf.to_file(output_path)
gdf.to_file(output_path, driver="GeoJSON")
4 changes: 2 additions & 2 deletions hot_fair_utilities/preprocessing/multimasks_from_polygons.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Standard library imports
from pathlib import Path

import os
# Third party imports
import geopandas as gpd
import rasterio as rio
Expand Down Expand Up @@ -88,7 +88,7 @@ def multimasks_from_polygons(
# workaround for bug in solaris
mask_shape, mask_transform = get_rasterio_shape_and_transform(chip_path)

gdf = gpd.read_file(json_path)
gdf = gpd.read_file(os.path.relpath(json_path))

# remove empty and null geometries
gdf = gdf[~gdf["geometry"].isna()]
Expand Down
Loading

0 comments on commit ec672e8

Please sign in to comment.