From 27cc5767714339693fbe2e45e68fd6ea3f84b24c Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Tue, 27 Aug 2024 16:17:53 +0200 Subject: [PATCH 01/24] Move VERS samplers into torchgeo samplers, implement pre-chipping everywhere --- torchgeo/datasets/geo.py | 2 + torchgeo/samplers/single.py | 269 ++++++++++++++++++++++++++++-------- 2 files changed, 217 insertions(+), 54 deletions(-) diff --git a/torchgeo/datasets/geo.py b/torchgeo/datasets/geo.py index 8233480443a..6b57cee0620 100644 --- a/torchgeo/datasets/geo.py +++ b/torchgeo/datasets/geo.py @@ -982,6 +982,7 @@ def __init__( if not isinstance(ds, GeoDataset): raise ValueError('IntersectionDataset only supports GeoDatasets') + self.return_as_ts = dataset1.return_as_ts or dataset2.return_as_ts self.crs = dataset1.crs self.res = dataset1.res @@ -1142,6 +1143,7 @@ def __init__( if not isinstance(ds, GeoDataset): raise ValueError('UnionDataset only supports GeoDatasets') + self.return_as_ts = dataset1.return_as_ts and dataset2.return_as_ts self.crs = dataset1.crs self.res = dataset1.res diff --git a/torchgeo/samplers/single.py b/torchgeo/samplers/single.py index 094142cb647..50fd4d37629 100644 --- a/torchgeo/samplers/single.py +++ b/torchgeo/samplers/single.py @@ -13,7 +13,46 @@ from ..datasets import BoundingBox, GeoDataset from .constants import Units from .utils import _to_tuple, get_random_bounding_box, tile_to_chips +from geopandas import GeoDataFrame +from tqdm import tqdm +from shapely.geometry import box +import re +import pandas as pd +def _get_regex_groups_as_df(dataset, hits): + """ + Extracts the regex metadata from a list of hits. + + Args: + dataset (GeoDataset): The dataset to sample from. + hits (list): A list of hits. + + Returns: + pandas.DataFrame: A DataFrame containing the extracted file metadata. + """ + has_filename_regex = bool(getattr(dataset, "filename_regex", None)) + if has_filename_regex: + filename_regex = re.compile(dataset.filename_regex, re.VERBOSE) + file_metadata = [] + for hit in hits: + if has_filename_regex: + match = re.match(filename_regex, str(hit.object)) + if match: + meta = match.groupdict() + else: + meta = {} + meta.update( + { + "minx": hit.bounds[0], + "maxx": hit.bounds[1], + "miny": hit.bounds[2], + "maxy": hit.bounds[3], + "mint": hit.bounds[4], + "maxt": hit.bounds[5], + } + ) + file_metadata.append(meta) + return pd.DataFrame(file_metadata) class GeoSampler(Sampler[BoundingBox], abc.ABC): """Abstract base class for sampling from :class:`~torchgeo.datasets.GeoDataset`. @@ -44,14 +83,80 @@ def __init__(self, dataset: GeoDataset, roi: BoundingBox | None = None) -> None: self.res = dataset.res self.roi = roi + self.dataset = dataset + + @abc.abstractmethod + def get_chips(self) -> GeoDataFrame: + """Determines the way to get the extend of the chips (samples) of the dataset. + Should return a GeoDataFrame with the extend of the chips with the columns + geometry, minx, miny, maxx, maxy, mint, maxt, fid. Each row is a chip.""" + raise NotImplementedError + + + def filter_chips( + self, + filter_by: str | GeoDataFrame, + predicate: str = "intersects", + action: str = "keep", + ) -> None: + """Filter the default set of chips in the sampler down to a specific subset by + specifying files supported by geopandas such as shapefiles, geodatabases or + feather files. + + Args: + filter_by: The file or geodataframe for which the geometries will be used during filtering + predicate: Predicate as used in Geopandas sindex.query_bulk + action: What to do with the chips that satisfy the condition by the predicacte. + Can either be "drop" or "keep". + """ + prefilter_leng = len(self.chips) + filtering_gdf = load_file(filter_by).to_crs(self.dataset.crs) + self.chips = filter_tiles( + self.chips, filtering_gdf, predicate, action + ).reset_index(drop=True) + self.chips.fid = self.chips.index + print(f"Filter step reduced chips from {prefilter_leng} to {len(self.chips)}") + assert not self.chips.empty, "No chips left after filtering!" + + def set_worker_split(self, total_workers: int, worker_num: int) -> None: + """Splits the chips in n equal parts for the number of workers and keeps the set of + chips for the specific worker id, convenient if you want to split the chips across + multiple dataloaders for multi-gpu inference. + + Args: + total_workers: The total number of parts to split the chips + worker_num: The id of the worker (which part to keep), starts from 0 + + """ + self.chips = np.array_split(self.chips, total_workers)[worker_num] + + def save(self, + path: str, + driver: str = None) -> None: + """Save the chips as a shapefile or feather file""" + if path.endswith(".feather"): + self.chips.to_feather(path) + else: + self.chips.to_file(path, driver=driver) - @abc.abstractmethod def __iter__(self) -> Iterator[BoundingBox]: """Return the index of a dataset. Returns: (minx, maxx, miny, maxy, mint, maxt) coordinates to index a dataset """ + for _, chip in self.chips.iterrows(): + yield BoundingBox( + chip.minx, chip.maxx, chip.miny, chip.maxy, chip.mint, chip.maxt + ) + + def __len__(self) -> int: + """Return the number of samples over the ROI. + + Returns: + number of patches that will be sampled + """ + return len(self.chips) class RandomGeoSampler(GeoSampler): @@ -129,22 +234,40 @@ def __init__( if torch.sum(self.areas) == 0: self.areas += 1 - def __iter__(self) -> Iterator[BoundingBox]: - """Return the index of a dataset. + self.chips = self.get_chips() + - Returns: - (minx, maxx, miny, maxy, mint, maxt) coordinates to index a dataset - """ - for _ in range(len(self)): + def get_chips(self) -> GeoDataFrame: + chips = [] + for _ in tqdm(range(len(self))): # Choose a random tile, weighted by area idx = torch.multinomial(self.areas, 1) hit = self.hits[idx] bounds = BoundingBox(*hit.bounds) # Choose a random index within that tile - bounding_box = get_random_bounding_box(bounds, self.size, self.res) + bbox = get_random_bounding_box(bounds, self.size, self.res) + minx, maxx, miny, maxy, mint, maxt = tuple(bbox) + chip = { + "geometry": box(minx, miny, maxx, maxy), + "minx": minx, + "miny": miny, + "maxx": maxx, + "maxy": maxy, + "mint": mint, + "maxt": maxt, + } + chips.append(chip) + + if chips: + print("creating geodataframe... ") + chips_gdf = GeoDataFrame(chips, crs=self.dataset.crs) + chips_gdf["fid"] = chips_gdf.index - yield bounding_box + else: + warnings.warn("Sampler has no chips, check your inputs") + chips_gdf = GeoDataFrame() + return chips_gdf def __len__(self) -> int: """Return the number of samples in a single epoch. @@ -206,33 +329,38 @@ def __init__( self.size = (self.size[0] * self.res, self.size[1] * self.res) self.stride = (self.stride[0] * self.res, self.stride[1] * self.res) - self.hits = [] - for hit in self.index.intersection(tuple(self.roi), objects=True): - bounds = BoundingBox(*hit.bounds) - if ( - bounds.maxx - bounds.minx >= self.size[1] - and bounds.maxy - bounds.miny >= self.size[0] - ): - self.hits.append(hit) + hits = self.index.intersection(tuple(self.roi), objects=True) + df_path = _get_regex_groups_as_df(self.dataset, hits) - self.length = 0 - for hit in self.hits: - bounds = BoundingBox(*hit.bounds) - rows, cols = tile_to_chips(bounds, self.size, self.stride) - self.length += rows * cols + # Filter out tiles smaller than the chip size + self.df_path = df_path[ + (df_path.maxx - df_path.minx >= self.size[1]) + & (df_path.maxy - df_path.miny >= self.size[0]) + ] - def __iter__(self) -> Iterator[BoundingBox]: - """Return the index of a dataset. + # Filter out hits in the index that share the same extent + if self.dataset.return_as_ts: + self.df_path.drop_duplicates( + subset=["minx", "maxx", "miny", "maxy"], inplace=True + ) + else: + self.df_path.drop_duplicates( + subset=["minx", "maxx", "miny", "maxy", "mint", "maxt"], inplace=True + ) + + self.chips = self.get_chips() - Returns: - (minx, maxx, miny, maxy, mint, maxt) coordinates to index a dataset - """ - # For each tile... - for hit in self.hits: - bounds = BoundingBox(*hit.bounds) + + def get_chips(self) -> GeoDataFrame: + print("generating samples... ") + optional_keys = ["tile", "date"] + self.length = 0 + chips = [] + for _, row in tqdm(self.df_path.iterrows(), total=len(self.df_path)): + bounds = BoundingBox( + row.minx, row.maxx, row.miny, row.maxy, row.mint, row.maxt + ) rows, cols = tile_to_chips(bounds, self.size, self.stride) - mint = bounds.mint - maxt = bounds.maxt # For each row... for i in range(rows): @@ -244,15 +372,37 @@ def __iter__(self) -> Iterator[BoundingBox]: minx = bounds.minx + j * self.stride[1] maxx = minx + self.size[1] - yield BoundingBox(minx, maxx, miny, maxy, mint, maxt) - - def __len__(self) -> int: - """Return the number of samples over the ROI. + if self.dataset.return_as_ts: + mint = self.dataset.bounds.mint + maxt = self.dataset.bounds.maxt + else: + mint = bounds.mint + maxt = bounds.maxt + + chip = { + "geometry": box(minx, miny, maxx, maxy), + "minx": minx, + "miny": miny, + "maxx": maxx, + "maxy": maxy, + "mint": mint, + "maxt": maxt, + } + for key in optional_keys: + if key in row.keys(): + chip[key] = row[key] + + chips.append(chip) + + if chips: + print("creating geodataframe... ") + chips_gdf = GeoDataFrame(chips, crs=self.dataset.crs) + chips_gdf["fid"] = chips_gdf.index - Returns: - number of patches that will be sampled - """ - return self.length + else: + warnings.warn("Sampler has no chips, check your inputs") + chips_gdf = GeoDataFrame() + return chips_gdf class PreChippedGeoSampler(GeoSampler): @@ -287,25 +437,36 @@ def __init__( self.hits = [] for hit in self.index.intersection(tuple(self.roi), objects=True): - self.hits.append(hit) + self.hits.append(hit)\ + + self.chips = get_chips(self) - def __iter__(self) -> Iterator[BoundingBox]: - """Return the index of a dataset. + def get_chips(self) -> GeoDataFrame: - Returns: - (minx, maxx, miny, maxy, mint, maxt) coordinates to index a dataset - """ generator: Callable[[int], Iterable[int]] = range if self.shuffle: generator = torch.randperm + chips = [] for idx in generator(len(self)): - yield BoundingBox(*self.hits[idx].bounds) - - def __len__(self) -> int: - """Return the number of samples over the ROI. + minx, maxx, miny, maxy, mint, maxt = self.hits[idx].bounds + chip = { + "geometry": box(minx, miny, maxx, maxy), + "minx": minx, + "miny": miny, + "maxx": maxx, + "maxy": maxy, + "mint": mint, + "maxt": maxt, + } + chips.append(chip) + + if chips: + print("creating geodataframe... ") + chips_gdf = GeoDataFrame(chips, crs=self.dataset.crs) + chips_gdf["fid"] = chips_gdf.index + else: + warnings.warn("Sampler has no chips, check your inputs") + chips_gdf = GeoDataFrame() + return chips_gdf - Returns: - number of patches that will be sampled - """ - return len(self.hits) From 99a16aebacbf1f05f782d15360de86b282b4296a Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Tue, 17 Sep 2024 12:16:05 +0200 Subject: [PATCH 02/24] revert return_as_ts --- torchgeo/datasets/geo.py | 2 -- torchgeo/samplers/single.py | 17 +---------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/torchgeo/datasets/geo.py b/torchgeo/datasets/geo.py index 6b57cee0620..8233480443a 100644 --- a/torchgeo/datasets/geo.py +++ b/torchgeo/datasets/geo.py @@ -982,7 +982,6 @@ def __init__( if not isinstance(ds, GeoDataset): raise ValueError('IntersectionDataset only supports GeoDatasets') - self.return_as_ts = dataset1.return_as_ts or dataset2.return_as_ts self.crs = dataset1.crs self.res = dataset1.res @@ -1143,7 +1142,6 @@ def __init__( if not isinstance(ds, GeoDataset): raise ValueError('UnionDataset only supports GeoDatasets') - self.return_as_ts = dataset1.return_as_ts and dataset2.return_as_ts self.crs = dataset1.crs self.res = dataset1.res diff --git a/torchgeo/samplers/single.py b/torchgeo/samplers/single.py index 50fd4d37629..1833288cab3 100644 --- a/torchgeo/samplers/single.py +++ b/torchgeo/samplers/single.py @@ -338,15 +338,7 @@ def __init__( & (df_path.maxy - df_path.miny >= self.size[0]) ] - # Filter out hits in the index that share the same extent - if self.dataset.return_as_ts: - self.df_path.drop_duplicates( - subset=["minx", "maxx", "miny", "maxy"], inplace=True - ) - else: - self.df_path.drop_duplicates( - subset=["minx", "maxx", "miny", "maxy", "mint", "maxt"], inplace=True - ) + self.chips = self.get_chips() @@ -372,13 +364,6 @@ def get_chips(self) -> GeoDataFrame: minx = bounds.minx + j * self.stride[1] maxx = minx + self.size[1] - if self.dataset.return_as_ts: - mint = self.dataset.bounds.mint - maxt = self.dataset.bounds.maxt - else: - mint = bounds.mint - maxt = bounds.maxt - chip = { "geometry": box(minx, miny, maxx, maxy), "minx": minx, From a158e0b6aa10c22246d015e76a96da3f0715ba07 Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Tue, 17 Sep 2024 15:22:04 +0200 Subject: [PATCH 03/24] Pass ruff and tests 100% --- tests/data/samplers/filtering_4x4.feather | Bin 0 -> 5490 bytes .../samplers/filtering_4x4/filtering_4x4.cpg | 1 + .../samplers/filtering_4x4/filtering_4x4.dbf | Bin 0 -> 78 bytes .../samplers/filtering_4x4/filtering_4x4.prj | 1 + .../samplers/filtering_4x4/filtering_4x4.shp | Bin 0 -> 236 bytes .../samplers/filtering_4x4/filtering_4x4.shx | Bin 0 -> 108 bytes tests/samplers/test_single.py | 96 +++++- torchgeo/samplers/single.py | 293 ++++++++++-------- 8 files changed, 246 insertions(+), 145 deletions(-) create mode 100644 tests/data/samplers/filtering_4x4.feather create mode 100644 tests/data/samplers/filtering_4x4/filtering_4x4.cpg create mode 100644 tests/data/samplers/filtering_4x4/filtering_4x4.dbf create mode 100644 tests/data/samplers/filtering_4x4/filtering_4x4.prj create mode 100644 tests/data/samplers/filtering_4x4/filtering_4x4.shp create mode 100644 tests/data/samplers/filtering_4x4/filtering_4x4.shx diff --git a/tests/data/samplers/filtering_4x4.feather b/tests/data/samplers/filtering_4x4.feather new file mode 100644 index 0000000000000000000000000000000000000000..305d37e4fa6244002c3a17f577da8866f1a340c8 GIT binary patch literal 5490 zcmeHL&2HO95MC#;;{9<$d<^lp+(9+v`FLXq{YtoVXQ-lq zP@_&Sjq^d`Y;lKh1F6->`I0*pni2T?5{`vTdTAIXeH?iE)pN5~*?mv4-mx4AV2-sO zW?L&OLzSMmsp_v-RJL;3bT}eTp?;il3h=GB>Zoddx ze1$*X=IyvEi7g?!B9on7A;Q#7U_+tvfzhHQzAMvE>ZrFbkE9}PmP$;d5HVO5e7--Q zn5xv06QfAyp;1tDyGou$1R;BqonHx!(_y5@+663u@_^<^q_HWKeI=zsVqHf)P5L*J zw_cqhFGXf}UZs8rwF{$)Tpy<@(j&0n**n;UI23h=fmMHfvoMh{rIq?pvOIxHLY$yj zK2E3IWTS@fyvs$*+e!-Tn}O?Q;QNN_P#{<|LT`&#%uSXwX5i@&KN4{!IVsSOOjMtW zI5(oN5wk0L&&ECod=nR!YQ8G%{vn1>J%9ZcERDt$mK7DfO!G{OBor#-ut^+C6Nsg* zYt36QZo$;oGxn44Ba8$0HfCO(X1znvN_UhCeWO>srWqA=kweXOn<22sv+L6=U*rCL zglE;b|B>6qczifhhS?TM%PbNmgx}9Eg=KJmu4Wa$K6vM?$WuNpwVsLIdnu|QXcU{l z=&fdR4Mv|7whq#&xzWyE8jt#vw+;#2?WSkA=1rFjZeFXH(d*D0M=CF}rsonK!uKet z=v2|Mb0?0viYhG4!D)!=Vk%bUT+{Vd-Bo8irqWDRyl!ea>NOtJ{v~?sNyh$MK;4d# zP@WZ4yU0R65)53FNcz=EmgI3vGSN*{NaYBdOs7MddS)ukbPQc>z3&*}))bQqSWr2N zWE^&if>*$-WX62pPsz=sX`+|pgW+g`NV}-#un)OPP)$aYFx^Xh`{w1-fDCad%#{>d z?M|j-nYa=3uSnD#Q4j+@gFMj5vcKMk!;w%b{Rz}69`x2-az4sq$-9E|UHUk1F1e09 zq^CjeW%BAh(~&ibK5n4~UvqlUzhS`trN&s-%*XbHztbADFzW14(HYhaMRvEwJ}3Bh z25Vn+f;3EI**(Bv4`@KHa+EyKC*Cw37YJT_2bGPWRiy_M^uVY&UqAZc*)O#G8OuYw zFv>}R|AIh&FF*hgTBP?UPUD%wK3iss_wTSh0@{K87&F+dPMRzcr#h|B=fYZ$>xr3J zQrtAojQs&LjjDanU(fsKc^^IRqbE6^Qu(}(E}PkTA6@c9b{MY&~mj?Nx literal 0 HcmV?d00001 diff --git a/tests/data/samplers/filtering_4x4/filtering_4x4.cpg b/tests/data/samplers/filtering_4x4/filtering_4x4.cpg new file mode 100644 index 00000000000..57decb48120 --- /dev/null +++ b/tests/data/samplers/filtering_4x4/filtering_4x4.cpg @@ -0,0 +1 @@ +ISO-8859-1 diff --git a/tests/data/samplers/filtering_4x4/filtering_4x4.dbf b/tests/data/samplers/filtering_4x4/filtering_4x4.dbf new file mode 100644 index 0000000000000000000000000000000000000000..499d67bcec48f8473adebc8ab148bcb05ea6894d GIT binary patch literal 78 mcmZRsVV7WJU|?`$-~p1Dz|GSICg=xZaKm^|npXh<45R>p9Rtt+ literal 0 HcmV?d00001 diff --git a/tests/data/samplers/filtering_4x4/filtering_4x4.prj b/tests/data/samplers/filtering_4x4/filtering_4x4.prj new file mode 100644 index 00000000000..42fd4b91b78 --- /dev/null +++ b/tests/data/samplers/filtering_4x4/filtering_4x4.prj @@ -0,0 +1 @@ +PROJCS["NAD_1983_BC_Environment_Albers",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Albers"],PARAMETER["False_Easting",1000000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-126.0],PARAMETER["Standard_Parallel_1",50.0],PARAMETER["Standard_Parallel_2",58.5],PARAMETER["Latitude_Of_Origin",45.0],UNIT["Meter",1.0]] diff --git a/tests/data/samplers/filtering_4x4/filtering_4x4.shp b/tests/data/samplers/filtering_4x4/filtering_4x4.shp new file mode 100644 index 0000000000000000000000000000000000000000..65606c26dd6675aa22232af31613c0d39433b9db GIT binary patch literal 236 zcmZQzQ0HR64$59IGcd4Xmjj9lI6$OeG){#e2}U4xAjT|^Lfq;=M!^8gUR*Rx9fAe` DP2vP( literal 0 HcmV?d00001 diff --git a/tests/data/samplers/filtering_4x4/filtering_4x4.shx b/tests/data/samplers/filtering_4x4/filtering_4x4.shx new file mode 100644 index 0000000000000000000000000000000000000000..b2028e759e5a7509214f94701bfbbb3e3bb83d69 GIT binary patch literal 108 lcmZQzQ0HR64$NLKGcd4Xmjj9lI6$OeG){#e2_qnO005390%`yN literal 0 HcmV?d00001 diff --git a/tests/samplers/test_single.py b/tests/samplers/test_single.py index 1416368098a..764a100c727 100644 --- a/tests/samplers/test_single.py +++ b/tests/samplers/test_single.py @@ -2,12 +2,15 @@ # Licensed under the MIT License. import math -from collections.abc import Iterator +import os from itertools import product +import geopandas as gpd import pytest from _pytest.fixtures import SubRequest +from geopandas import GeoDataFrame from rasterio.crs import CRS +from shapely.geometry import box from torch.utils.data import DataLoader from torchgeo.datasets import BoundingBox, GeoDataset, stack_samples @@ -23,14 +26,23 @@ class CustomGeoSampler(GeoSampler): def __init__(self) -> None: - pass - - def __iter__(self) -> Iterator[BoundingBox]: - for i in range(len(self)): - yield BoundingBox(i, i, i, i, i, i) - - def __len__(self) -> int: - return 2 + self.chips = self.get_chips() + + def get_chips(self) -> GeoDataFrame: + chips = [] + for i in range(2): + chips.append( + { + 'geometry': box(i, i, i, i), + 'minx': i, + 'miny': i, + 'maxx': i, + 'maxy': i, + 'mint': i, + 'maxt': i, + } + ) + return GeoDataFrame(chips, crs=CRS.from_epsg(3005)) class CustomGeoDataset(GeoDataset): @@ -64,6 +76,64 @@ def test_abstract(self, dataset: CustomGeoDataset) -> None: with pytest.raises(TypeError, match="Can't instantiate abstract class"): GeoSampler(dataset) # type: ignore[abstract] + @pytest.mark.parametrize( + 'filtering_file', ['filtering_4x4', 'filtering_4x4.feather'] + ) + def test_filtering_from_path(self, filtering_file: str) -> None: + datadir = os.path.join('tests', 'data', 'samplers') + ds = CustomGeoDataset() + ds.index.insert(0, (0, 10, 0, 10, 0, 10)) + sampler = GridGeoSampler( + ds, 5, 5, units=Units.CRS, roi=BoundingBox(0, 10, 0, 10, 0, 10) + ) + iterator = iter(sampler) + + assert len(sampler) == 4 + filtering_path = os.path.join(datadir, filtering_file) + sampler.filter_chips(filtering_path, 'intersects', 'drop') + assert len(sampler) == 3 + assert next(iterator) == BoundingBox(5, 10, 0, 5, 0, 10) + + def test_filtering_from_gdf(self) -> None: + datadir = os.path.join('tests', 'data', 'samplers') + ds = CustomGeoDataset() + ds.index.insert(0, (0, 10, 0, 10, 0, 10)) + sampler = GridGeoSampler( + ds, 5, 5, units=Units.CRS, roi=BoundingBox(0, 10, 0, 10, 0, 10) + ) + iterator = iter(sampler) + + # Dropping first chip + assert len(sampler) == 4 + filtering_gdf = gpd.read_file(os.path.join(datadir, 'filtering_4x4')) + sampler.filter_chips(filtering_gdf, 'intersects', 'drop') + assert len(sampler) == 3 + assert next(iterator) == BoundingBox(5, 10, 0, 5, 0, 10) + + # Keeping only first chip + sampler = GridGeoSampler(ds, 5, 5, units=Units.CRS) + iterator = iter(sampler) + sampler.filter_chips(filtering_gdf, 'intersects', 'keep') + assert len(sampler) == 1 + assert next(iterator) == BoundingBox(0, 5, 0, 5, 0, 10) + + def test_set_worker_split(self) -> None: + ds = CustomGeoDataset() + ds.index.insert(0, (0, 10, 0, 10, 0, 10)) + sampler = GridGeoSampler( + ds, 5, 5, units=Units.CRS, roi=BoundingBox(0, 10, 0, 10, 0, 10) + ) + assert len(sampler) == 4 + sampler.set_worker_split(total_workers=4, worker_num=1) + assert len(sampler) == 1 + + def test_save_chips(self, tmpdir_factory: pytest.TempdirFactory) -> None: + ds = CustomGeoDataset() + ds.index.insert(0, (0, 10, 0, 10, 0, 10)) + sampler = GridGeoSampler(ds, 5, 5, units=Units.CRS) + sampler.save(str(tmpdir_factory.mktemp('out').join('chips'))) + sampler.save(str(tmpdir_factory.mktemp('out').join('chips.feather'))) + @pytest.mark.slow @pytest.mark.parametrize('num_workers', [0, 1, 2]) def test_dataloader( @@ -115,6 +185,10 @@ def test_roi(self, dataset: CustomGeoDataset) -> None: for query in sampler: assert query in roi + def test_empty(self, dataset: CustomGeoDataset) -> None: + sampler = RandomGeoSampler(dataset, 5, length=0) + assert len(sampler) == 0 + def test_small_area(self) -> None: ds = CustomGeoDataset(res=1) ds.index.insert(0, (0, 10, 0, 10, 0, 10)) @@ -267,11 +341,11 @@ def dataset(self) -> CustomGeoDataset: def sampler(self, dataset: CustomGeoDataset) -> PreChippedGeoSampler: return PreChippedGeoSampler(dataset, shuffle=True) - def test_iter(self, sampler: GridGeoSampler) -> None: + def test_iter(self, sampler: PreChippedGeoSampler) -> None: for _ in sampler: continue - def test_len(self, sampler: GridGeoSampler) -> None: + def test_len(self, sampler: PreChippedGeoSampler) -> None: assert len(sampler) == 2 def test_roi(self, dataset: CustomGeoDataset) -> None: diff --git a/torchgeo/samplers/single.py b/torchgeo/samplers/single.py index 1833288cab3..1a7f1d70f31 100644 --- a/torchgeo/samplers/single.py +++ b/torchgeo/samplers/single.py @@ -6,53 +6,42 @@ import abc from collections.abc import Callable, Iterable, Iterator +import geopandas as gpd +import numpy as np import torch +from geopandas import GeoDataFrame from rtree.index import Index, Property +from shapely.geometry import box from torch.utils.data import Sampler +from tqdm import tqdm from ..datasets import BoundingBox, GeoDataset from .constants import Units from .utils import _to_tuple, get_random_bounding_box, tile_to_chips -from geopandas import GeoDataFrame -from tqdm import tqdm -from shapely.geometry import box -import re -import pandas as pd -def _get_regex_groups_as_df(dataset, hits): - """ - Extracts the regex metadata from a list of hits. - Args: - dataset (GeoDataset): The dataset to sample from. - hits (list): A list of hits. +def load_file(path: str | GeoDataFrame) -> GeoDataFrame: + """Load a file from the given path. + + Parameters: + path (str or GeoDataFrame): The path to the file or a GeoDataFrame object. Returns: - pandas.DataFrame: A DataFrame containing the extracted file metadata. + GeoDataFrame: The loaded file as a GeoDataFrame. + + Raises: + None + """ - has_filename_regex = bool(getattr(dataset, "filename_regex", None)) - if has_filename_regex: - filename_regex = re.compile(dataset.filename_regex, re.VERBOSE) - file_metadata = [] - for hit in hits: - if has_filename_regex: - match = re.match(filename_regex, str(hit.object)) - if match: - meta = match.groupdict() - else: - meta = {} - meta.update( - { - "minx": hit.bounds[0], - "maxx": hit.bounds[1], - "miny": hit.bounds[2], - "maxy": hit.bounds[3], - "mint": hit.bounds[4], - "maxt": hit.bounds[5], - } - ) - file_metadata.append(meta) - return pd.DataFrame(file_metadata) + if isinstance(path, GeoDataFrame): + return path + if path.endswith('.feather'): + print(f'Reading feather file: {path}') + return gpd.read_feather(path) + else: + print(f'Reading shapefile: {path}') + return gpd.read_file(path) + class GeoSampler(Sampler[BoundingBox], abc.ABC): """Abstract base class for sampling from :class:`~torchgeo.datasets.GeoDataset`. @@ -84,24 +73,44 @@ def __init__(self, dataset: GeoDataset, roi: BoundingBox | None = None) -> None: self.res = dataset.res self.roi = roi self.dataset = dataset - - @abc.abstractmethod + self.chips: GeoDataFrame = GeoDataFrame() + + @staticmethod + def __save_as_gpd_or_feather( + path: str, gdf: GeoDataFrame, driver: str = 'ESRI Shapefile' + ) -> None: + """Save a GeoDataFrame as a file supported by any geopandas driver or as a feather file. + + Parameters: + path (str): The path to save the file. + gdf (GeoDataFrame): The GeoDataFrame to be saved. + driver (str, optional): The driver to be used for saving the file. Defaults to 'ESRI Shapefile'. + + Returns: + None + """ + if path.endswith('.feather'): + gdf.to_feather(path) + else: + gdf.to_file(path, driver=driver) + + @abc.abstractmethod def get_chips(self) -> GeoDataFrame: - """Determines the way to get the extend of the chips (samples) of the dataset. - Should return a GeoDataFrame with the extend of the chips with the columns - geometry, minx, miny, maxx, maxy, mint, maxt, fid. Each row is a chip.""" - raise NotImplementedError + """Determines the way to get the extent of the chips (samples) of the dataset. + Should return a GeoDataFrame with the extend of the chips with the columns + geometry, minx, miny, maxx, maxy, mint, maxt, fid. Each row is a chip. It is + expected that every sampler calls this method to get the chips as one of the + last steps in the __init__ method. + """ def filter_chips( self, filter_by: str | GeoDataFrame, - predicate: str = "intersects", - action: str = "keep", + predicate: str = 'intersects', + action: str = 'keep', ) -> None: - """Filter the default set of chips in the sampler down to a specific subset by - specifying files supported by geopandas such as shapefiles, geodatabases or - feather files. + """Filter the default set of chips in the sampler down to a specific subset. Args: filter_by: The file or geodataframe for which the geometries will be used during filtering @@ -111,33 +120,57 @@ def filter_chips( """ prefilter_leng = len(self.chips) filtering_gdf = load_file(filter_by).to_crs(self.dataset.crs) - self.chips = filter_tiles( - self.chips, filtering_gdf, predicate, action - ).reset_index(drop=True) + + if action == 'keep': + self.chips = self.chips.iloc[ + list( + set( + self.chips.sindex.query_bulk( + filtering_gdf.geometry, predicate=predicate + )[1] + ) + ) + ].reset_index(drop=True) + elif action == 'drop': + self.chips = self.chips.drop( + index=list( + set( + self.chips.sindex.query_bulk( + filtering_gdf.geometry, predicate=predicate + )[1] + ) + ) + ).reset_index(drop=True) + self.chips.fid = self.chips.index - print(f"Filter step reduced chips from {prefilter_leng} to {len(self.chips)}") - assert not self.chips.empty, "No chips left after filtering!" + print(f'Filter step reduced chips from {prefilter_leng} to {len(self.chips)}') + assert not self.chips.empty, 'No chips left after filtering!' def set_worker_split(self, total_workers: int, worker_num: int) -> None: - """Splits the chips in n equal parts for the number of workers and keeps the set of + """Split the chips for multi-worker inference. + + Splits the chips in n equal parts for the number of workers and keeps the set of chips for the specific worker id, convenient if you want to split the chips across - multiple dataloaders for multi-gpu inference. + multiple dataloaders for multi-worker inference. Args: - total_workers: The total number of parts to split the chips - worker_num: The id of the worker (which part to keep), starts from 0 + total_workers (int): The total number of parts to split the chips + worker_num (int): The id of the worker (which part to keep), starts from 0 """ self.chips = np.array_split(self.chips, total_workers)[worker_num] - def save(self, - path: str, - driver: str = None) -> None: - """Save the chips as a shapefile or feather file""" - if path.endswith(".feather"): - self.chips.to_feather(path) - else: - self.chips.to_file(path, driver=driver) + def save(self, path: str, driver: str = 'ESRI Shapefile') -> None: + """Save the chips as a file format supported by GeoPandas or feather file. + + Parameters: + - path (str): The path to save the file. + - driver (str): The driver to use for saving the file. Defaults to 'ESRI Shapefile'. + + Returns: + - None + """ + self.__save_as_gpd_or_feather(path, self.chips, driver) def __iter__(self) -> Iterator[BoundingBox]: """Return the index of a dataset. @@ -235,11 +268,15 @@ def __init__( self.areas += 1 self.chips = self.get_chips() - def get_chips(self) -> GeoDataFrame: + """Generate chips from the dataset. + + Returns: + GeoDataFrame: A GeoDataFrame containing the generated chips. + """ chips = [] - for _ in tqdm(range(len(self))): + for _ in tqdm(range(self.length)): # Choose a random tile, weighted by area idx = torch.multinomial(self.areas, 1) hit = self.hits[idx] @@ -249,34 +286,25 @@ def get_chips(self) -> GeoDataFrame: bbox = get_random_bounding_box(bounds, self.size, self.res) minx, maxx, miny, maxy, mint, maxt = tuple(bbox) chip = { - "geometry": box(minx, miny, maxx, maxy), - "minx": minx, - "miny": miny, - "maxx": maxx, - "maxy": maxy, - "mint": mint, - "maxt": maxt, + 'geometry': box(minx, miny, maxx, maxy), + 'minx': minx, + 'miny': miny, + 'maxx': maxx, + 'maxy': maxy, + 'mint': mint, + 'maxt': maxt, } chips.append(chip) - + if chips: - print("creating geodataframe... ") + print('creating geodataframe... ') chips_gdf = GeoDataFrame(chips, crs=self.dataset.crs) - chips_gdf["fid"] = chips_gdf.index + chips_gdf['fid'] = chips_gdf.index else: - warnings.warn("Sampler has no chips, check your inputs") chips_gdf = GeoDataFrame() return chips_gdf - def __len__(self) -> int: - """Return the number of samples in a single epoch. - - Returns: - length of the epoch - """ - return self.length - class GridGeoSampler(GeoSampler): """Samples elements in a grid-like fashion. @@ -329,29 +357,28 @@ def __init__( self.size = (self.size[0] * self.res, self.size[1] * self.res) self.stride = (self.stride[0] * self.res, self.stride[1] * self.res) - hits = self.index.intersection(tuple(self.roi), objects=True) - df_path = _get_regex_groups_as_df(self.dataset, hits) - - # Filter out tiles smaller than the chip size - self.df_path = df_path[ - (df_path.maxx - df_path.minx >= self.size[1]) - & (df_path.maxy - df_path.miny >= self.size[0]) - ] - + self.hits = [] + for hit in self.index.intersection(tuple(self.roi), objects=True): + bounds = BoundingBox(*hit.bounds) + if ( + bounds.maxx - bounds.minx >= self.size[1] + and bounds.maxy - bounds.miny >= self.size[0] + ): + self.hits.append(hit) - self.chips = self.get_chips() - def get_chips(self) -> GeoDataFrame: - print("generating samples... ") - optional_keys = ["tile", "date"] + """Generates chips from the given hits. + + Returns: + GeoDataFrame: A GeoDataFrame containing the generated chips. + """ + print('generating samples... ') self.length = 0 chips = [] - for _, row in tqdm(self.df_path.iterrows(), total=len(self.df_path)): - bounds = BoundingBox( - row.minx, row.maxx, row.miny, row.maxy, row.mint, row.maxt - ) + for hit in self.hits: + bounds = BoundingBox(*hit.bounds) rows, cols = tile_to_chips(bounds, self.size, self.stride) # For each row... @@ -365,27 +392,23 @@ def get_chips(self) -> GeoDataFrame: maxx = minx + self.size[1] chip = { - "geometry": box(minx, miny, maxx, maxy), - "minx": minx, - "miny": miny, - "maxx": maxx, - "maxy": maxy, - "mint": mint, - "maxt": maxt, + 'geometry': box(minx, miny, maxx, maxy), + 'minx': minx, + 'miny': miny, + 'maxx': maxx, + 'maxy': maxy, + 'mint': bounds.mint, + 'maxt': bounds.maxt, } - for key in optional_keys: - if key in row.keys(): - chip[key] = row[key] - + self.length += 1 chips.append(chip) if chips: - print("creating geodataframe... ") + print('creating geodataframe... ') chips_gdf = GeoDataFrame(chips, crs=self.dataset.crs) - chips_gdf["fid"] = chips_gdf.index + chips_gdf['fid'] = chips_gdf.index else: - warnings.warn("Sampler has no chips, check your inputs") chips_gdf = GeoDataFrame() return chips_gdf @@ -422,36 +445,38 @@ def __init__( self.hits = [] for hit in self.index.intersection(tuple(self.roi), objects=True): - self.hits.append(hit)\ - - self.chips = get_chips(self) + self.hits.append(hit) + + self.length = len(self.hits) + self.chips = self.get_chips() def get_chips(self) -> GeoDataFrame: + """Generate chips from the hits and return them as a GeoDataFrame. + Returns: + GeoDataFrame: A GeoDataFrame containing the generated chips. + """ generator: Callable[[int], Iterable[int]] = range if self.shuffle: generator = torch.randperm chips = [] - for idx in generator(len(self)): + for idx in generator(self.length): minx, maxx, miny, maxy, mint, maxt = self.hits[idx].bounds chip = { - "geometry": box(minx, miny, maxx, maxy), - "minx": minx, - "miny": miny, - "maxx": maxx, - "maxy": maxy, - "mint": mint, - "maxt": maxt, + 'geometry': box(minx, miny, maxx, maxy), + 'minx': minx, + 'miny': miny, + 'maxx': maxx, + 'maxy': maxy, + 'mint': mint, + 'maxt': maxt, } + print('generating chip') + self.length += 1 chips.append(chip) - if chips: - print("creating geodataframe... ") - chips_gdf = GeoDataFrame(chips, crs=self.dataset.crs) - chips_gdf["fid"] = chips_gdf.index - else: - warnings.warn("Sampler has no chips, check your inputs") - chips_gdf = GeoDataFrame() + print('creating geodataframe... ') + chips_gdf = GeoDataFrame(chips, crs=self.dataset.crs) + chips_gdf['fid'] = chips_gdf.index return chips_gdf - From 77901fc98b357f67a9f89e63f52c95ff0026fec8 Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Tue, 17 Sep 2024 15:36:05 +0200 Subject: [PATCH 04/24] run prettier on landcoverai --- tests/conf/landcoverai100.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/conf/landcoverai100.yaml b/tests/conf/landcoverai100.yaml index 1610bb03990..f6461851fa3 100644 --- a/tests/conf/landcoverai100.yaml +++ b/tests/conf/landcoverai100.yaml @@ -1,9 +1,9 @@ model: class_path: SemanticSegmentationTask init_args: - loss: "ce" - model: "unet" - backbone: "resnet18" + loss: 'ce' + model: 'unet' + backbone: 'resnet18' in_channels: 3 num_classes: 5 num_filters: 1 @@ -13,4 +13,4 @@ data: init_args: batch_size: 1 dict_kwargs: - root: "tests/data/landcoverai" + root: 'tests/data/landcoverai' From 60539addc820ba2c5c6601f2d978c388a385a213 Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 08:22:25 +0200 Subject: [PATCH 05/24] add refresh_samples function --- torchgeo/samplers/single.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/torchgeo/samplers/single.py b/torchgeo/samplers/single.py index 1a7f1d70f31..ae82a82f167 100644 --- a/torchgeo/samplers/single.py +++ b/torchgeo/samplers/single.py @@ -269,6 +269,14 @@ def __init__( self.chips = self.get_chips() + def refresh_samples(self) -> None: + """Refresh the samples in the sampler. + + This method is useful when you want to refresh the random samples in the sampler + without creating a new sampler instance. + """ + self.chips = self.get_chips() + def get_chips(self) -> GeoDataFrame: """Generate chips from the dataset. From bc3300a36ff44365dd9934999469ccefdc22f4d3 Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 09:25:30 +0200 Subject: [PATCH 06/24] Add dependencies, add test for refresh --- pyproject.toml | 4 ++++ requirements/min-reqs.old | 2 ++ requirements/required.txt | 2 ++ tests/samplers/test_single.py | 8 ++++++++ 4 files changed, 16 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ca5159fde52..b3f1eba68ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,8 @@ dependencies = [ "einops>=0.3", # fiona 1.8.21+ required for Python 3.10 wheels "fiona>=1.8.21", + # geopandas 0.13.2 is the last version to support pandas 1.3, but has feather support + "geopandas=0.13.2", # kornia 0.7.3+ required for instance segmentation support in AugmentationSequential "kornia>=0.7.3", # lightly 1.4.5+ required for LARS optimizer @@ -58,6 +60,8 @@ dependencies = [ "pandas>=1.3.3", # pillow 8.4+ required for Python 3.10 wheels "pillow>=8.4", + # pyarrow 12.0+ required for feather support + "pyarrow>=17.0.0", # pyproj 3.3+ required for Python 3.10 wheels "pyproj>=3.3", # rasterio 1.3+ required for Python 3.10 wheels diff --git a/requirements/min-reqs.old b/requirements/min-reqs.old index a6e91f70fe9..24e15ba962a 100644 --- a/requirements/min-reqs.old +++ b/requirements/min-reqs.old @@ -4,6 +4,7 @@ setuptools==61.0.0 # install einops==0.3.0 fiona==1.8.21 +geopandas==0.13.2 kornia==0.7.3 lightly==1.4.5 lightning[pytorch-extra]==2.0.0 @@ -11,6 +12,7 @@ matplotlib==3.5.0 numpy==1.21.2 pandas==1.3.3 pillow==8.4.0 +pyarrow==17.0.0 pyproj==3.3.0 rasterio==1.3.0.post1 rtree==1.0.0 diff --git a/requirements/required.txt b/requirements/required.txt index 2fbcd75f732..27ae0610102 100644 --- a/requirements/required.txt +++ b/requirements/required.txt @@ -4,6 +4,7 @@ setuptools==75.1.0 # install einops==0.8.0 fiona==1.10.1 +geopandas==0.14.4 kornia==0.7.3 lightly==1.5.12 lightning[pytorch-extra]==2.4.0 @@ -11,6 +12,7 @@ matplotlib==3.9.2 numpy==2.1.1 pandas==2.2.2 pillow==10.4.0 +pyarrow==17.0.0 pyproj==3.6.1 rasterio==1.3.11 rtree==1.3.0 diff --git a/tests/samplers/test_single.py b/tests/samplers/test_single.py index 764a100c727..00f5889a22f 100644 --- a/tests/samplers/test_single.py +++ b/tests/samplers/test_single.py @@ -189,6 +189,14 @@ def test_empty(self, dataset: CustomGeoDataset) -> None: sampler = RandomGeoSampler(dataset, 5, length=0) assert len(sampler) == 0 + def test_refresh_samples(self, dataset: CustomGeoDataset) -> None: + sampler = RandomGeoSampler(dataset, 5, length=1) + samples = list(sampler) + assert len(sampler) == 1 + sampler.refresh_samples() + assert list(sampler) != samples + assert len(sampler) == 1 + def test_small_area(self) -> None: ds = CustomGeoDataset(res=1) ds.index.insert(0, (0, 10, 0, 10, 0, 10)) From 83411f40176a32a3c91de39568fd09b2986f25de Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 09:30:19 +0200 Subject: [PATCH 07/24] fix typo --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b3f1eba68ed..6d144cd6179 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ # fiona 1.8.21+ required for Python 3.10 wheels "fiona>=1.8.21", # geopandas 0.13.2 is the last version to support pandas 1.3, but has feather support - "geopandas=0.13.2", + "geopandas==0.13.2", # kornia 0.7.3+ required for instance segmentation support in AugmentationSequential "kornia>=0.7.3", # lightly 1.4.5+ required for LARS optimizer From 6fba6cc9918dce492da616199c4134a0bac7a41a Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 11:22:58 +0000 Subject: [PATCH 08/24] fix datamodules failing test, better test for resampling --- tests/datamodules/test_geo.py | 3 ++- tests/samplers/test_single.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/datamodules/test_geo.py b/tests/datamodules/test_geo.py index 8e5fd13d292..d1924984ea7 100644 --- a/tests/datamodules/test_geo.py +++ b/tests/datamodules/test_geo.py @@ -11,6 +11,7 @@ from matplotlib.figure import Figure from rasterio.crs import CRS from torch import Tensor +from geopandas import GeoDataFrame from torchgeo.datamodules import ( GeoDataModule, @@ -182,7 +183,7 @@ def test_zero_length_sampler(self) -> None: dm = CustomGeoDataModule() dm.dataset = CustomGeoDataset() dm.sampler = RandomGeoSampler(dm.dataset, 1, 1) - dm.sampler.length = 0 + dm.sampler.chips = GeoDataFrame() msg = r'CustomGeoDataModule\.sampler has length 0.' with pytest.raises(MisconfigurationException, match=msg): dm.train_dataloader() diff --git a/tests/samplers/test_single.py b/tests/samplers/test_single.py index 00f5889a22f..6fdbf4712fc 100644 --- a/tests/samplers/test_single.py +++ b/tests/samplers/test_single.py @@ -190,6 +190,7 @@ def test_empty(self, dataset: CustomGeoDataset) -> None: assert len(sampler) == 0 def test_refresh_samples(self, dataset: CustomGeoDataset) -> None: + dataset.index.insert(0, (0, 100, 200, 300, 400, 500)) sampler = RandomGeoSampler(dataset, 5, length=1) samples = list(sampler) assert len(sampler) == 1 From c9df4e404ee6a3a984abd9f07d6e0aa2eb763834 Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 13:34:07 +0200 Subject: [PATCH 09/24] ruff --- tests/datamodules/test_geo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/datamodules/test_geo.py b/tests/datamodules/test_geo.py index d1924984ea7..80e71c52a43 100644 --- a/tests/datamodules/test_geo.py +++ b/tests/datamodules/test_geo.py @@ -7,11 +7,11 @@ import pytest import torch from _pytest.fixtures import SubRequest +from geopandas import GeoDataFrame from lightning.pytorch import Trainer from matplotlib.figure import Figure from rasterio.crs import CRS from torch import Tensor -from geopandas import GeoDataFrame from torchgeo.datamodules import ( GeoDataModule, From eaf22dce635c607c80d182409121a0bcbd68302d Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 14:30:04 +0200 Subject: [PATCH 10/24] Documentation updates, try to add geopandas --- docs/conf.py | 1 + torchgeo/samplers/single.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4078970c2e4..5bd7536ac5c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -122,6 +122,7 @@ 'torch': ('https://pytorch.org/docs/stable', None), 'torchmetrics': ('https://lightning.ai/docs/torchmetrics/stable/', None), 'torchvision': ('https://pytorch.org/vision/stable', None), + 'geopandas': ('https://geopandas.org/en/stable/', None), } # nbsphinx diff --git a/torchgeo/samplers/single.py b/torchgeo/samplers/single.py index ae82a82f167..36f525e610a 100644 --- a/torchgeo/samplers/single.py +++ b/torchgeo/samplers/single.py @@ -24,10 +24,10 @@ def load_file(path: str | GeoDataFrame) -> GeoDataFrame: """Load a file from the given path. Parameters: - path (str or GeoDataFrame): The path to the file or a GeoDataFrame object. + path (str or :class:`GeoDataFrame`): The path to the file or a :class:`GeoDataFrame` object. Returns: - GeoDataFrame: The loaded file as a GeoDataFrame. + :class:`GeoDataFrame`: The loaded file as a :class:`GeoDataFrame`. Raises: None @@ -79,11 +79,11 @@ def __init__(self, dataset: GeoDataset, roi: BoundingBox | None = None) -> None: def __save_as_gpd_or_feather( path: str, gdf: GeoDataFrame, driver: str = 'ESRI Shapefile' ) -> None: - """Save a GeoDataFrame as a file supported by any geopandas driver or as a feather file. + """Save a :class:`GeoDataFrame` as a file supported by any geopandas driver or as a feather file. Parameters: path (str): The path to save the file. - gdf (GeoDataFrame): The GeoDataFrame to be saved. + gdf (:class:`GeoDataFrame`): The :class:`GeoDataFrame` to be saved. driver (str, optional): The driver to be used for saving the file. Defaults to 'ESRI Shapefile'. Returns: @@ -98,10 +98,10 @@ def __save_as_gpd_or_feather( def get_chips(self) -> GeoDataFrame: """Determines the way to get the extent of the chips (samples) of the dataset. - Should return a GeoDataFrame with the extend of the chips with the columns + Should return a :class:`GeoDataFrame` with the extend of the chips with the columns geometry, minx, miny, maxx, maxy, mint, maxt, fid. Each row is a chip. It is expected that every sampler calls this method to get the chips as one of the - last steps in the __init__ method. + last steps in the `__init__` method. """ def filter_chips( @@ -113,10 +113,10 @@ def filter_chips( """Filter the default set of chips in the sampler down to a specific subset. Args: - filter_by: The file or geodataframe for which the geometries will be used during filtering + filter_by: The file or :class:`GeoDataFrame` for which the geometries will be used during filtering predicate: Predicate as used in Geopandas sindex.query_bulk action: What to do with the chips that satisfy the condition by the predicacte. - Can either be "drop" or "keep". + Can either be ``'drop'``or ``'keep'``. """ prefilter_leng = len(self.chips) filtering_gdf = load_file(filter_by).to_crs(self.dataset.crs) @@ -281,7 +281,7 @@ def get_chips(self) -> GeoDataFrame: """Generate chips from the dataset. Returns: - GeoDataFrame: A GeoDataFrame containing the generated chips. + :class:`GeoDataFrame`: A :class:`GeoDataFrame` containing the generated chips. """ chips = [] for _ in tqdm(range(self.length)): @@ -380,7 +380,7 @@ def get_chips(self) -> GeoDataFrame: """Generates chips from the given hits. Returns: - GeoDataFrame: A GeoDataFrame containing the generated chips. + :class:`GeoDataFrame`: A :class:`GeoDataFrame` containing the generated chips. """ print('generating samples... ') self.length = 0 @@ -459,10 +459,10 @@ def __init__( self.chips = self.get_chips() def get_chips(self) -> GeoDataFrame: - """Generate chips from the hits and return them as a GeoDataFrame. + """Generate chips from the hits and return them as a :class:`GeoDataFrame`. Returns: - GeoDataFrame: A GeoDataFrame containing the generated chips. + :class:`GeoDataFrame`: A :class:`GeoDataFrame` containing the generated chips. """ generator: Callable[[int], Iterable[int]] = range if self.shuffle: From 5a554fffd4fdde678a9cbc87bef865a7aabd756c Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 14:53:35 +0200 Subject: [PATCH 11/24] add GeoDataFrame to nitpick ignore --- docs/conf.py | 1 + torchgeo/samplers/single.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 5bd7536ac5c..dfec675d3f4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -67,6 +67,7 @@ ('py:class', 'torchvision.models._api.WeightsEnum'), ('py:class', 'torchvision.models.resnet.ResNet'), ('py:class', 'torchvision.models.swin_transformer.SwinTransformer'), + ('py:class', 'geopandas.GeoDataFrame'), ] diff --git a/torchgeo/samplers/single.py b/torchgeo/samplers/single.py index 36f525e610a..da544a264d1 100644 --- a/torchgeo/samplers/single.py +++ b/torchgeo/samplers/single.py @@ -24,10 +24,10 @@ def load_file(path: str | GeoDataFrame) -> GeoDataFrame: """Load a file from the given path. Parameters: - path (str or :class:`GeoDataFrame`): The path to the file or a :class:`GeoDataFrame` object. + path (str or GeoDataFrame): The path to the file or a GeoDataFrame object. Returns: - :class:`GeoDataFrame`: The loaded file as a :class:`GeoDataFrame`. + GeoDataFrame: The loaded file as a GeoDataFrame. Raises: None @@ -79,11 +79,11 @@ def __init__(self, dataset: GeoDataset, roi: BoundingBox | None = None) -> None: def __save_as_gpd_or_feather( path: str, gdf: GeoDataFrame, driver: str = 'ESRI Shapefile' ) -> None: - """Save a :class:`GeoDataFrame` as a file supported by any geopandas driver or as a feather file. + """Save a GeoDataFrame as a file supported by any geopandas driver or as a feather file. Parameters: path (str): The path to save the file. - gdf (:class:`GeoDataFrame`): The :class:`GeoDataFrame` to be saved. + gdf (GeoDataFrame): The GeoDataFrame to be saved. driver (str, optional): The driver to be used for saving the file. Defaults to 'ESRI Shapefile'. Returns: @@ -98,10 +98,10 @@ def __save_as_gpd_or_feather( def get_chips(self) -> GeoDataFrame: """Determines the way to get the extent of the chips (samples) of the dataset. - Should return a :class:`GeoDataFrame` with the extend of the chips with the columns + Should return a GeoDataFrame with the extend of the chips with the columns geometry, minx, miny, maxx, maxy, mint, maxt, fid. Each row is a chip. It is expected that every sampler calls this method to get the chips as one of the - last steps in the `__init__` method. + last steps in the ``__init__`` method. """ def filter_chips( @@ -113,10 +113,10 @@ def filter_chips( """Filter the default set of chips in the sampler down to a specific subset. Args: - filter_by: The file or :class:`GeoDataFrame` for which the geometries will be used during filtering + filter_by: The file or geodataframe for which the geometries will be used during filtering predicate: Predicate as used in Geopandas sindex.query_bulk action: What to do with the chips that satisfy the condition by the predicacte. - Can either be ``'drop'``or ``'keep'``. + Can either be ``'drop'`` or ``'keep'``. """ prefilter_leng = len(self.chips) filtering_gdf = load_file(filter_by).to_crs(self.dataset.crs) @@ -272,7 +272,7 @@ def __init__( def refresh_samples(self) -> None: """Refresh the samples in the sampler. - This method is useful when you want to refresh the random samples in the sampler + This method is useful when you want to refresh the samples in the sampler without creating a new sampler instance. """ self.chips = self.get_chips() @@ -281,7 +281,7 @@ def get_chips(self) -> GeoDataFrame: """Generate chips from the dataset. Returns: - :class:`GeoDataFrame`: A :class:`GeoDataFrame` containing the generated chips. + GeoDataFrame: A GeoDataFrame containing the generated chips. """ chips = [] for _ in tqdm(range(self.length)): @@ -380,7 +380,7 @@ def get_chips(self) -> GeoDataFrame: """Generates chips from the given hits. Returns: - :class:`GeoDataFrame`: A :class:`GeoDataFrame` containing the generated chips. + GeoDataFrame: A GeoDataFrame containing the generated chips. """ print('generating samples... ') self.length = 0 @@ -459,10 +459,10 @@ def __init__( self.chips = self.get_chips() def get_chips(self) -> GeoDataFrame: - """Generate chips from the hits and return them as a :class:`GeoDataFrame`. + """Generate chips from the hits and return them as a GeoDataFrame. Returns: - :class:`GeoDataFrame`: A :class:`GeoDataFrame` containing the generated chips. + GeoDataFrame: A GeoDataFrame containing the generated chips. """ generator: Callable[[int], Iterable[int]] = range if self.shuffle: From 9e0627fa33ff166c27f0dd3e03403e65c9c226ef Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 15:43:22 +0200 Subject: [PATCH 12/24] remove explicit GeoDataFrame return value --- torchgeo/samplers/single.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/torchgeo/samplers/single.py b/torchgeo/samplers/single.py index da544a264d1..05bdf32f314 100644 --- a/torchgeo/samplers/single.py +++ b/torchgeo/samplers/single.py @@ -281,7 +281,7 @@ def get_chips(self) -> GeoDataFrame: """Generate chips from the dataset. Returns: - GeoDataFrame: A GeoDataFrame containing the generated chips. + A GeoDataFrame containing the generated chips. """ chips = [] for _ in tqdm(range(self.length)): @@ -380,7 +380,7 @@ def get_chips(self) -> GeoDataFrame: """Generates chips from the given hits. Returns: - GeoDataFrame: A GeoDataFrame containing the generated chips. + A GeoDataFrame containing the generated chips. """ print('generating samples... ') self.length = 0 @@ -462,7 +462,7 @@ def get_chips(self) -> GeoDataFrame: """Generate chips from the hits and return them as a GeoDataFrame. Returns: - GeoDataFrame: A GeoDataFrame containing the generated chips. + A GeoDataFrame containing the generated chips. """ generator: Callable[[int], Iterable[int]] = range if self.shuffle: From fb282fefd61644a8264496121a79840770987c20 Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 17:01:26 +0200 Subject: [PATCH 13/24] automatically shuffle every __iter__. Add tutorial notebook. --- docs/tutorials/visualizing_samples.ipynb | 374 +++++++++++++++++++++++ torchgeo/samplers/single.py | 18 +- 2 files changed, 388 insertions(+), 4 deletions(-) create mode 100644 docs/tutorials/visualizing_samples.ipynb diff --git a/docs/tutorials/visualizing_samples.ipynb b/docs/tutorials/visualizing_samples.ipynb new file mode 100644 index 00000000000..80be70dc7a5 --- /dev/null +++ b/docs/tutorials/visualizing_samples.ipynb @@ -0,0 +1,374 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualizing Samples\n", + "\n", + "This tutorial shows how to visualize and save the extent of your samples before and during training. In this particular example, we compare a vanilla RandomGeoSampler with one bounded by multiple ROI's and show how easy it is to gain insight on the distribution of your samples." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import tempfile\n", + "\n", + "from torch.utils.data import DataLoader\n", + "\n", + "from torchgeo.datasets import NAIP, stack_samples\n", + "from torchgeo.datasets.utils import download_url\n", + "from torchgeo.samplers import RandomGeoSampler" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "\n", + "def run_epochs(dataset, sampler):\n", + " dataloader = DataLoader(\n", + " naip, sampler=sampler, batch_size=1, collate_fn=stack_samples, num_workers=0\n", + " )\n", + " fig, ax = plt.subplots()\n", + " num_epochs = 5\n", + " for epoch in range(num_epochs):\n", + " color = plt.cm.viridis(epoch / num_epochs)\n", + " sampler.chips.to_file(f'naip_chips_epoch_{epoch}')\n", + " ax = sampler.chips.plot(ax=ax, color=color)\n", + " for sample in dataloader:\n", + " pass\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Generate dataset" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using downloaded and verified file: C:\\Users\\SIEGER~1.FAL\\AppData\\Local\\Temp\\naip\\m_3807511_ne_18_060_20181104.tif\n", + "Using downloaded and verified file: C:\\Users\\SIEGER~1.FAL\\AppData\\Local\\Temp\\naip\\m_3807512_sw_18_060_20180815.tif\n" + ] + } + ], + "source": [ + "naip_root = os.path.join(tempfile.gettempdir(), 'naip')\n", + "naip_url = (\n", + " 'https://naipeuwest.blob.core.windows.net/naip/v002/de/2018/de_060cm_2018/38075/'\n", + ")\n", + "tiles = ['m_3807511_ne_18_060_20181104.tif', 'm_3807512_sw_18_060_20180815.tif']\n", + "for tile in tiles:\n", + " download_url(naip_url + tile, naip_root)\n", + "\n", + "naip = NAIP(naip_root)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First we create the default sampler for our dataset (3 samples) and run it for 5 epochs and plot its results. Each color displays a different epoch, so we can see how the RandomGeoSampler has distributed it's samples for every epoch." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "generating samples... \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 3/3 [00:00<00:00, 823.92it/s]" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "sampler = RandomGeoSampler(naip, size=1000, length=3)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "generating samples... \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 3/3 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "run_epochs(naip, sampler)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we split our dataset by two bounding boxes and re-inspect the samples." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from torchgeo.datasets import roi_split\n", + "from torchgeo.datasets.utils import BoundingBox\n", + "\n", + "rois = [\n", + " BoundingBox(440854, 442938, 4299766, 4301731, 0, np.inf),\n", + " BoundingBox(449070, 451194, 4289463, 4291746, 0, np.inf),\n", + "]\n", + "datasets = roi_split(naip, rois)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "combined = datasets[0] | datasets[1]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "generating samples... \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 3/3 [00:00<00:00, 2997.36it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "generating samples... \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 3/3 [00:00" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sampler = RandomGeoSampler(combined, size=1000, length=3)\n", + "run_epochs(combined, sampler)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cca", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/torchgeo/samplers/single.py b/torchgeo/samplers/single.py index 05bdf32f314..fcb4ed536f8 100644 --- a/torchgeo/samplers/single.py +++ b/torchgeo/samplers/single.py @@ -269,6 +269,18 @@ def __init__( self.chips = self.get_chips() + def __iter__(self) -> Iterator[BoundingBox]: + """Return the index of a dataset. + + Returns: + (minx, maxx, miny, maxy, mint, maxt) coordinates to index a dataset + """ + self.refresh_samples() + for _, chip in self.chips.iterrows(): + yield BoundingBox( + chip.minx, chip.maxx, chip.miny, chip.maxy, chip.mint, chip.maxt + ) + def refresh_samples(self) -> None: """Refresh the samples in the sampler. @@ -284,6 +296,7 @@ def get_chips(self) -> GeoDataFrame: A GeoDataFrame containing the generated chips. """ chips = [] + print('generating samples... ') for _ in tqdm(range(self.length)): # Choose a random tile, weighted by area idx = torch.multinomial(self.areas, 1) @@ -305,7 +318,6 @@ def get_chips(self) -> GeoDataFrame: chips.append(chip) if chips: - print('creating geodataframe... ') chips_gdf = GeoDataFrame(chips, crs=self.dataset.crs) chips_gdf['fid'] = chips_gdf.index @@ -412,7 +424,6 @@ def get_chips(self) -> GeoDataFrame: chips.append(chip) if chips: - print('creating geodataframe... ') chips_gdf = GeoDataFrame(chips, crs=self.dataset.crs) chips_gdf['fid'] = chips_gdf.index @@ -468,6 +479,7 @@ def get_chips(self) -> GeoDataFrame: if self.shuffle: generator = torch.randperm + print('generating samples... ') chips = [] for idx in generator(self.length): minx, maxx, miny, maxy, mint, maxt = self.hits[idx].bounds @@ -480,11 +492,9 @@ def get_chips(self) -> GeoDataFrame: 'mint': mint, 'maxt': maxt, } - print('generating chip') self.length += 1 chips.append(chip) - print('creating geodataframe... ') chips_gdf = GeoDataFrame(chips, crs=self.dataset.crs) chips_gdf['fid'] = chips_gdf.index return chips_gdf From c29451902bd2d68cb1070421f226cdea47f38c6a Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 17:26:31 +0200 Subject: [PATCH 14/24] add notebook to docs, change some notebook cells. --- docs/index.rst | 1 + docs/tutorials/visualizing_samples.ipynb | 49 ++++++++---------------- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index ced959493a8..60deae2c855 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,6 +29,7 @@ torchgeo :caption: Tutorials tutorials/getting_started + tutorials/visualizing_samples tutorials/custom_raster_dataset tutorials/transforms tutorials/indices diff --git a/docs/tutorials/visualizing_samples.ipynb b/docs/tutorials/visualizing_samples.ipynb index 80be70dc7a5..79b6e436d63 100644 --- a/docs/tutorials/visualizing_samples.ipynb +++ b/docs/tutorials/visualizing_samples.ipynb @@ -18,31 +18,23 @@ "import os\n", "import tempfile\n", "\n", + "import matplotlib.pyplot as plt\n", "from torch.utils.data import DataLoader\n", "\n", "from torchgeo.datasets import NAIP, stack_samples\n", "from torchgeo.datasets.utils import download_url\n", - "from torchgeo.samplers import RandomGeoSampler" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", + "from torchgeo.samplers import RandomGeoSampler\n", "\n", "\n", "def run_epochs(dataset, sampler):\n", " dataloader = DataLoader(\n", - " naip, sampler=sampler, batch_size=1, collate_fn=stack_samples, num_workers=0\n", + " dataset, sampler=sampler, batch_size=1, collate_fn=stack_samples, num_workers=0\n", " )\n", " fig, ax = plt.subplots()\n", " num_epochs = 5\n", " for epoch in range(num_epochs):\n", " color = plt.cm.viridis(epoch / num_epochs)\n", - " sampler.chips.to_file(f'naip_chips_epoch_{epoch}')\n", + " # sampler.chips.to_file(f'naip_chips_epoch_{epoch}') # Optional: save chips to file for display in GIS software\n", " ax = sampler.chips.plot(ax=ax, color=color)\n", " for sample in dataloader:\n", " pass\n", @@ -58,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -91,7 +83,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -105,14 +97,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 3/3 [00:00<00:00, 823.92it/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n" + "100%|██████████| 3/3 [00:00<00:00, 998.72it/s]\n" ] } ], @@ -122,7 +107,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -197,7 +182,7 @@ }, { "data": { - "image/png": "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", + "image/png": "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bN+2bZvGjh2rxMREBQUFqX///nruueea7L9+/XoNGTJEDodDffv21RtvvNFkzEsvvaRevXopMDBQqamp2rhxo9fz3377rSZNmqSuXbsqNDRUt9xyi2pqatp62qfVmjU52d///nedd9558vPz06FDh7ye64hr0pb1eOONN5SSkqLAwEB1795dkyZN8np++/btGj58uAIDA5WYmKi5c+c2OcbSpUuVnJyswMBADRo0SO+9916TOcyYMUNxcXEKCgqS0+lUeXl5m875x7R2TYqLi5WWlqbIyEhFRUVp5MiR2rZtm9eYjrgmza3HiBEj5Ofn5/W47777vParqKhQRkaGgoOD1b17dz3yyCP67rvvvMacVd8zBme1jRs3ml69epmUlBSTnZ3d5Pmnn37ajB492kgyy5cv92x/9dVXzUMPPWTWr19v9uzZY9566y0TFBRkXnjhBc+YvXv3muDgYDNlyhSzY8cO88ILL5iAgADz/vvve8YUFBSYLl26mNdee8189tln5p577jGRkZGmpqbGM+a+++4ziYmJZs2aNWbTpk3miiuuMMOGDftJ1sOY1q/JycaMGeMZc/DgQc/2jrgmbVmPp556ysTHx5tFixaZL774wmzbts2sXLnS87zL5TI9evQw48aNM2VlZWbJkiUmKCjIzJ8/3zPmo48+MgEBAWbu3Llmx44dZtq0aaZz586mtLTUMyYvL89ERESYFStWmG3btpl//dd/Nb179zbHjx/3+XoY0/o1OXz4sImOjjZ33XWX+fzzz01ZWZm55ZZbTI8ePUxdXV2HXZNTrce1115r7rnnHlNVVeV5uFwuz/PfffedGThwoHE6nWbLli3mvffeM926dTM5OTmeMWfb9wxRO4sdPnzYXHDBBaawsNBce+21Tb45t2zZYhISEkxVVdVpf4CfcP/995vrrrvO8/Wjjz5qBgwY4DXm9ttvNyNHjvR8ffnll5tJkyZ5vm5oaDDx8fEmNzfXGGPMoUOHTOfOnc3SpUs9Y3bu3GkkmaKiopae8o/yxZq8/PLL5tprrzVr1qxpErWOtiZtWY8DBw6YoKAgs3r16lMe/+WXXzZRUVHG7XZ7tk2dOtX069fP8/Vtt91mMjIyvPZLTU019957rzHGmMbGRhMbG2t+//vfe54/dOiQcTgcZsmSJa057dNqy5oUFxcbSaaiosKzbfv27UaSKS8vN8Z0vDU53Xo0tz4ne++994y/v7+prq72bJs3b54JDw/3nP/Z9j3Drx/PYpMmTVJGRoacTmeT544dO6Y77rhDL730kmJjY8/oeC6XS9HR0Z6vi4qKmhx75MiRKioqkiTV1dWppKTEa4y/v7+cTqdnTElJierr673GJCcnKykpyTPGl9q6Jjt27NBvf/tbLVy4sNkPTu1oa9KW9SgsLFRjY6O++eYb9e/fX+edd55uu+02VVZWesYUFRXpmmuuUZcuXTzbRo4cqV27dungwYOeMadbsy+//FLV1dVeYyIiIpSamnrW/TfSr18/de3aVa+++qrq6up0/Phxvfrqq+rfv7969eolqeOtyenWQ5IWLVqkbt26aeDAgcrJydGxY8c8zxUVFWnQoEHq0aOH13nU1tbqs88+84w5m75nzpkPNO5oCgoKtHnzZhUXFzf7/MMPP6xhw4ZpzJgxZ3S8jz/+WH/605/017/+1bOturra6z9WSerRo4dqa2t1/PhxHTx4UA0NDc2O+fzzzz3H6NKliyIjI5uMqa6uPqO5nam2ronb7dbYsWP1+9//XklJSdq7d2+TMR1pTdq6Hnv37lVjY6N+97vf6bnnnlNERISmTZumG264Qdu3b1eXLl1UXV2t3r17NzkP6fvzjIqKOuWanTjXE/97ujG+0tY1CQsL0/r165WZmalZs2ZJki644AKtWrVKnTp9/+OyI63Jj63HHXfcoZ49eyo+Pl7bt2/X1KlTtWvXLi1btswzz+bmePI5nG3fM0TtLFRZWans7GwVFhYqMDCwyfPvvPOO1q5dqy1btpzR8crKyjRmzBjNnDlT6enpvp7uz8IXa5KTk6P+/ftr/PjxP+VUfxa+WI/GxkbV19fr+eef9/x3sWTJEsXGxmrdunUaOXLkTzb/n4Iv1uT48eP69a9/rauuukpLlixRQ0OD8vPzlZGRoeLiYgUFBf2Up+BTP7YekjRx4kTP/x80aJDi4uKUlpamPXv2qE+fPj/XVH2KXz+ehUpKSrR//34NGTJEnTp1UqdOnfQ///M/ev7559WpUycVFhZqz549ioyM9DwvSbfccotGjBjhdawdO3YoLS1NEydO1LRp07yei42NbXJ3UU1NjcLDwxUUFKRu3bopICCg2TEnfnUTGxururq6JncQnjzGF3yxJmvXrtXSpUs9z6elpUmSunXrppkzZ3aoNfHFesTFxUmSLrroIs9xY2Ji1K1bN1VUVJx2PU48d7oxJz9/8n7NjfEFX6zJ4sWL9dVXX+n111/XZZddpiuuuEKLFy/Wl19+qZUrV3aoNfmx9WhoaGiyT2pqqiTpiy++OO15nHwOZ933TIvegcPPora21pSWlno9hg4dasaPH29KS0tNVVVVk+clmeeee87s3bvXc5yysjLTvXt388gjjzT7Oo8++qgZOHCg17axY8c2eYP3gQce8Hzd0NBgEhISmrzB+/bbb3vGfP755z6/KcIXa/LFF194Pf/aa68ZSebjjz/23IXVUdbEF+uxa9cuI8nrRpG///3vxt/f36xatcoY88+bIk7c+WeMMTk5OU1uivjFL37hNb8rr7yyyU0R+fn5nuddLpfPb4rwxZo8//zzJjY21jQ2NnqOW19fb0JCQsyiRYs61Jr82Ho0Z8OGDUaS2bZtmzHmnzeKnHyX4vz58014eLj59ttvjTFn3/cMUesgfuwuJf3gLq7S0lITExNjxo8f73W77v79+z1jTtyK+8gjj5idO3eal156qdlbcR0Oh3njjTfMjh07zMSJE01kZKTX3VD33XefSUpKMmvXrjWbNm0yV155pbnyyit9ev7Naema/NC6detOeUt/R1yT1qzHmDFjzIABA8xHH31kSktLzS9+8Qtz0UUXeX5gHzp0yPTo0cPceeedpqyszBQUFJjg4OAmt6936tTJ5Ofnm507d5qZM2c2e/t6ZGSkWblypdm+fbsZM2bMT3pL/wktXZOdO3cah8Nh/uM//sPs2LHDlJWVmfHjx5uIiAizb98+Y0zHXpOT1+OLL74wv/3tb82mTZvMl19+aVauXGnOP/98c80113jGn7ilPz093WzdutW8//77JiYmptlb+s+W7xmi1kG09Jtz5syZRlKTR8+ePb32W7dunRk8eLDp0qWLOf/8883rr7/e5NgvvPCCSUpKMl26dDGXX365+eSTT7yeP378uLn//vtNVFSUCQ4ONjfddJOpqqpqw9memZ8iaie2d8Q1ac16uFwu86tf/cpERkaa6Ohoc9NNN3ndzm6MMdu2bTNXX321cTgcJiEhweTl5TU59p///Gdz4YUXmi5dupgBAwaYv/71r17PNzY2munTp5sePXoYh8Nh0tLSzK5du1p9rmeqNWvywQcfmKuuuspERESYqKgoc/311ze5Wuioa3LyelRUVJhrrrnGREdHG4fDYfr27WseeeQRr79TM8aYr776yowePdoEBQWZbt26mf/8z/809fX1XmPOpu8Z/ukZAIA1uFEEAGANogYAsAZRAwBYg6gBAKxB1AAA1iBqAABrEDUAgDWIGgDAGkQNAGANogYAsAZRAwBYg6gBAKzx/+T4+LOlcW8bAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -219,7 +204,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -237,7 +222,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -246,7 +231,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -260,7 +245,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 3/3 [00:00<00:00, 2997.36it/s]\n" + "100%|██████████| 3/3 [00:00<00:00, 1743.27it/s]\n" ] }, { @@ -288,7 +273,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 3/3 [00:00" ] From 26da49877f830ef3df45fec5b09f7f46af77a809 Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 21:08:19 +0200 Subject: [PATCH 15/24] add debug statement --- docs/tutorials/visualizing_samples.ipynb | 223 ++--------------------- 1 file changed, 12 insertions(+), 211 deletions(-) diff --git a/docs/tutorials/visualizing_samples.ipynb b/docs/tutorials/visualizing_samples.ipynb index 79b6e436d63..fb4423c9218 100644 --- a/docs/tutorials/visualizing_samples.ipynb +++ b/docs/tutorials/visualizing_samples.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -34,6 +34,7 @@ " num_epochs = 5\n", " for epoch in range(num_epochs):\n", " color = plt.cm.viridis(epoch / num_epochs)\n", + " print(sampler.chips)\n", " # sampler.chips.to_file(f'naip_chips_epoch_{epoch}') # Optional: save chips to file for display in GIS software\n", " ax = sampler.chips.plot(ax=ax, color=color)\n", " for sample in dataloader:\n", @@ -50,18 +51,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using downloaded and verified file: C:\\Users\\SIEGER~1.FAL\\AppData\\Local\\Temp\\naip\\m_3807511_ne_18_060_20181104.tif\n", - "Using downloaded and verified file: C:\\Users\\SIEGER~1.FAL\\AppData\\Local\\Temp\\naip\\m_3807512_sw_18_060_20180815.tif\n" - ] - } - ], + "outputs": [], "source": [ "naip_root = os.path.join(tempfile.gettempdir(), 'naip')\n", "naip_url = (\n", @@ -83,114 +75,18 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "generating samples... \n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 3/3 [00:00<00:00, 998.72it/s]\n" - ] - } - ], + "outputs": [], "source": [ "sampler = RandomGeoSampler(naip, size=1000, length=3)" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "generating samples... \n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 3/3 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "run_epochs(naip, sampler)" ] @@ -204,7 +100,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -222,7 +118,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -231,104 +127,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "generating samples... \n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 3/3 [00:00<00:00, 1743.27it/s]\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "generating samples... \n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 3/3 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "sampler = RandomGeoSampler(combined, size=1000, length=3)\n", "run_epochs(combined, sampler)" From 989e479f1e03ca55c5e0d4a2c215ef6dbb4c35fb Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 21:35:26 +0200 Subject: [PATCH 16/24] Installing torchgeo as part of workflow to avoid installing master --- .github/workflows/tutorials.yaml | 2 +- docs/tutorials/visualizing_samples.ipynb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tutorials.yaml b/.github/workflows/tutorials.yaml index 67accad5e2c..ab45b96d3ab 100644 --- a/.github/workflows/tutorials.yaml +++ b/.github/workflows/tutorials.yaml @@ -33,7 +33,7 @@ jobs: - name: Install pip dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - pip install -r requirements/required.txt -r requirements/docs.txt -r requirements/tests.txt planetary_computer pystac + pip install -r requirements/required.txt -r requirements/docs.txt -r requirements/tests.txt planetary_computer pystac -e . pip cache purge - name: List pip dependencies run: pip list diff --git a/docs/tutorials/visualizing_samples.ipynb b/docs/tutorials/visualizing_samples.ipynb index fb4423c9218..d1d5c9924d3 100644 --- a/docs/tutorials/visualizing_samples.ipynb +++ b/docs/tutorials/visualizing_samples.ipynb @@ -34,7 +34,6 @@ " num_epochs = 5\n", " for epoch in range(num_epochs):\n", " color = plt.cm.viridis(epoch / num_epochs)\n", - " print(sampler.chips)\n", " # sampler.chips.to_file(f'naip_chips_epoch_{epoch}') # Optional: save chips to file for display in GIS software\n", " ax = sampler.chips.plot(ax=ax, color=color)\n", " for sample in dataloader:\n", From aa49753536b4621438b61417499b196d4d3fa6ae Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Wed, 18 Sep 2024 21:38:24 +0200 Subject: [PATCH 17/24] remove required.txt from workflow --- .github/workflows/tutorials.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tutorials.yaml b/.github/workflows/tutorials.yaml index ab45b96d3ab..e1e4ea4498f 100644 --- a/.github/workflows/tutorials.yaml +++ b/.github/workflows/tutorials.yaml @@ -33,7 +33,7 @@ jobs: - name: Install pip dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - pip install -r requirements/required.txt -r requirements/docs.txt -r requirements/tests.txt planetary_computer pystac -e . + pip install -r requirements/docs.txt -r requirements/tests.txt planetary_computer pystac -e . pip cache purge - name: List pip dependencies run: pip list From 7a2e0796d89b979713b87f7a3e9e0a0fab261b8e Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Fri, 20 Sep 2024 13:39:34 +0200 Subject: [PATCH 18/24] restore workflow --- .github/workflows/tutorials.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tutorials.yaml b/.github/workflows/tutorials.yaml index e1e4ea4498f..67accad5e2c 100644 --- a/.github/workflows/tutorials.yaml +++ b/.github/workflows/tutorials.yaml @@ -33,7 +33,7 @@ jobs: - name: Install pip dependencies if: steps.cache.outputs.cache-hit != 'true' run: | - pip install -r requirements/docs.txt -r requirements/tests.txt planetary_computer pystac -e . + pip install -r requirements/required.txt -r requirements/docs.txt -r requirements/tests.txt planetary_computer pystac pip cache purge - name: List pip dependencies run: pip list From 20b643afe4be30fcb370400d209fc6e524faca60 Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Fri, 20 Sep 2024 13:44:01 +0200 Subject: [PATCH 19/24] allow later versions of geopandas --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6d144cd6179..07929979939 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ # fiona 1.8.21+ required for Python 3.10 wheels "fiona>=1.8.21", # geopandas 0.13.2 is the last version to support pandas 1.3, but has feather support - "geopandas==0.13.2", + "geopandas>=0.13.2", # kornia 0.7.3+ required for instance segmentation support in AugmentationSequential "kornia>=0.7.3", # lightly 1.4.5+ required for LARS optimizer From 494fbd76672b4a29e0a6f8736b604369346d8bdd Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Fri, 20 Sep 2024 19:43:38 +0200 Subject: [PATCH 20/24] Add random generator --- torchgeo/datamodules/agrifieldnet.py | 6 +++++- torchgeo/samplers/batch.py | 7 ++++++- torchgeo/samplers/single.py | 20 +++++++++++++++++--- torchgeo/samplers/utils.py | 10 +++++++--- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/torchgeo/datamodules/agrifieldnet.py b/torchgeo/datamodules/agrifieldnet.py index bed6365d4a2..c5b92b6b01a 100644 --- a/torchgeo/datamodules/agrifieldnet.py +++ b/torchgeo/datamodules/agrifieldnet.py @@ -74,7 +74,11 @@ def setup(self, stage: str) -> None: if stage in ['fit']: self.train_batch_sampler = RandomBatchGeoSampler( - self.train_dataset, self.patch_size, self.batch_size, self.length + self.train_dataset, + self.patch_size, + self.batch_size, + self.length, + generator=generator, ) if stage in ['fit', 'validate']: self.val_sampler = GridGeoSampler( diff --git a/torchgeo/samplers/batch.py b/torchgeo/samplers/batch.py index 22726f74b2c..396ad0f0c7b 100644 --- a/torchgeo/samplers/batch.py +++ b/torchgeo/samplers/batch.py @@ -70,6 +70,7 @@ def __init__( length: int | None = None, roi: BoundingBox | None = None, units: Units = Units.PIXELS, + generator: torch.Generator | None = None, ) -> None: """Initialize a new Sampler instance. @@ -97,9 +98,11 @@ def __init__( roi: region of interest to sample from (minx, maxx, miny, maxy, mint, maxt) (defaults to the bounds of ``dataset.index``) units: defines if ``size`` is in pixel or CRS units + generator: random number generator """ super().__init__(dataset, roi) self.size = _to_tuple(size) + self.generator = generator if units == Units.PIXELS: self.size = (self.size[0] * self.res, self.size[1] * self.res) @@ -144,7 +147,9 @@ def __iter__(self) -> Iterator[list[BoundingBox]]: # Choose random indices within that tile batch = [] for _ in range(self.batch_size): - bounding_box = get_random_bounding_box(bounds, self.size, self.res) + bounding_box = get_random_bounding_box( + bounds, self.size, self.res, self.generator + ) batch.append(bounding_box) yield batch diff --git a/torchgeo/samplers/single.py b/torchgeo/samplers/single.py index 094142cb647..ea943db3d53 100644 --- a/torchgeo/samplers/single.py +++ b/torchgeo/samplers/single.py @@ -5,6 +5,7 @@ import abc from collections.abc import Callable, Iterable, Iterator +from functools import partial import torch from rtree.index import Index, Property @@ -72,6 +73,7 @@ def __init__( length: int | None = None, roi: BoundingBox | None = None, units: Units = Units.PIXELS, + generator: torch.Generator | None = None, ) -> None: """Initialize a new Sampler instance. @@ -98,6 +100,8 @@ def __init__( roi: region of interest to sample from (minx, maxx, miny, maxy, mint, maxt) (defaults to the bounds of ``dataset.index``) units: defines if ``size`` is in pixel or CRS units + generator: The random generator used for sampling. + """ super().__init__(dataset, roi) self.size = _to_tuple(size) @@ -105,6 +109,7 @@ def __init__( if units == Units.PIXELS: self.size = (self.size[0] * self.res, self.size[1] * self.res) + self.generator = generator self.length = 0 self.hits = [] areas = [] @@ -142,7 +147,9 @@ def __iter__(self) -> Iterator[BoundingBox]: bounds = BoundingBox(*hit.bounds) # Choose a random index within that tile - bounding_box = get_random_bounding_box(bounds, self.size, self.res) + bounding_box = get_random_bounding_box( + bounds, self.size, self.res, self.generator + ) yield bounding_box @@ -270,7 +277,11 @@ class PreChippedGeoSampler(GeoSampler): """ def __init__( - self, dataset: GeoDataset, roi: BoundingBox | None = None, shuffle: bool = False + self, + dataset: GeoDataset, + roi: BoundingBox | None = None, + shuffle: bool = False, + generator: torch.Generator | None = None, ) -> None: """Initialize a new Sampler instance. @@ -281,9 +292,12 @@ def __init__( roi: region of interest to sample from (minx, maxx, miny, maxy, mint, maxt) (defaults to the bounds of ``dataset.index``) shuffle: if True, reshuffle data at every epoch + generator: The random number generator used in combination with shuffle. + """ super().__init__(dataset, roi) self.shuffle = shuffle + self.generator = generator self.hits = [] for hit in self.index.intersection(tuple(self.roi), objects=True): @@ -297,7 +311,7 @@ def __iter__(self) -> Iterator[BoundingBox]: """ generator: Callable[[int], Iterable[int]] = range if self.shuffle: - generator = torch.randperm + generator = partial(torch.randperm, generator=self.generator) for idx in generator(len(self)): yield BoundingBox(*self.hits[idx].bounds) diff --git a/torchgeo/samplers/utils.py b/torchgeo/samplers/utils.py index a1fca673a3a..258f74a5425 100644 --- a/torchgeo/samplers/utils.py +++ b/torchgeo/samplers/utils.py @@ -35,7 +35,10 @@ def _to_tuple(value: tuple[float, float] | float) -> tuple[float, float]: def get_random_bounding_box( - bounds: BoundingBox, size: tuple[float, float] | float, res: float + bounds: BoundingBox, + size: tuple[float, float] | float, + res: float, + generator: torch.Generator | None = None, ) -> BoundingBox: """Returns a random bounding box within a given bounding box. @@ -50,6 +53,7 @@ def get_random_bounding_box( bounds: the larger bounding box to sample from size: the size of the bounding box to sample res: the resolution of the image + generator: random number generator Returns: randomly sampled bounding box from the extent of the input @@ -64,8 +68,8 @@ def get_random_bounding_box( miny = bounds.miny # Use an integer multiple of res to avoid resampling - minx += int(torch.rand(1).item() * width) * res - miny += int(torch.rand(1).item() * height) * res + minx += int(torch.rand(1, generator=generator).item() * width) * res + miny += int(torch.rand(1, generator=generator).item() * height) * res maxx = minx + t_size[1] maxy = miny + t_size[0] From 5a9e107fd1b5556f177d9607e426e021ab57a75a Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Fri, 20 Sep 2024 20:21:19 +0200 Subject: [PATCH 21/24] Add tests for seed --- tests/samplers/test_batch.py | 16 ++++++++++++++++ tests/samplers/test_single.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/tests/samplers/test_batch.py b/tests/samplers/test_batch.py index 59c8aaa00be..20ad33a58c9 100644 --- a/tests/samplers/test_batch.py +++ b/tests/samplers/test_batch.py @@ -6,6 +6,7 @@ from itertools import product import pytest +import torch from _pytest.fixtures import SubRequest from rasterio.crs import CRS from torch.utils.data import DataLoader @@ -144,6 +145,21 @@ def test_weighted_sampling(self) -> None: for bbox in batch: assert bbox == BoundingBox(0, 10, 0, 10, 0, 10) + def test_random_seed(self) -> None: + ds = CustomGeoDataset() + ds.index.insert(0, (0, 10, 0, 10, 0, 10)) + generator = torch.manual_seed(0) + sampler = RandomBatchGeoSampler(ds, 1, 1, generator=generator) + for bbox in sampler: + sample1 = bbox + break + + sampler = RandomBatchGeoSampler(ds, 1, 1, generator=generator) + for bbox in sampler: + sample2 = bbox + break + assert sample1 == sample2 + @pytest.mark.slow @pytest.mark.parametrize('num_workers', [0, 1, 2]) def test_dataloader( diff --git a/tests/samplers/test_single.py b/tests/samplers/test_single.py index 1416368098a..15f1025f672 100644 --- a/tests/samplers/test_single.py +++ b/tests/samplers/test_single.py @@ -6,6 +6,7 @@ from itertools import product import pytest +import torch from _pytest.fixtures import SubRequest from rasterio.crs import CRS from torch.utils.data import DataLoader @@ -139,6 +140,21 @@ def test_weighted_sampling(self) -> None: for bbox in sampler: assert bbox == BoundingBox(0, 10, 0, 10, 0, 10) + def test_random_seed(self) -> None: + ds = CustomGeoDataset() + ds.index.insert(0, (0, 10, 0, 10, 0, 10)) + generator = torch.manual_seed(0) + sampler = RandomGeoSampler(ds, 1, 1, generator=generator) + for bbox in sampler: + sample1 = bbox + break + + sampler = RandomGeoSampler(ds, 1, 1, generator=generator) + for bbox in sampler: + sample2 = bbox + break + assert sample1 == sample2 + @pytest.mark.slow @pytest.mark.parametrize('num_workers', [0, 1, 2]) def test_dataloader( @@ -288,6 +304,22 @@ def test_point_data(self) -> None: for _ in sampler: continue + def test_shuffle_seed(self) -> None: + ds = CustomGeoDataset() + ds.index.insert(0, (0, 10, 0, 10, 0, 10)) + ds.index.insert(1, (0, 11, 0, 11, 0, 11)) + generator = torch.manual_seed(0) + sampler = PreChippedGeoSampler(ds, shuffle=True, generator=generator) + for bbox in sampler: + sample1 = bbox + break + + sampler = PreChippedGeoSampler(ds, shuffle=True, generator=generator) + for bbox in sampler: + sample2 = bbox + break + assert sample1 == sample2 + @pytest.mark.slow @pytest.mark.parametrize('num_workers', [0, 1, 2]) def test_dataloader( From 46e1f11d440ecf1363393d7e616666cbd7f3e9f5 Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Fri, 20 Sep 2024 18:49:44 +0000 Subject: [PATCH 22/24] pass generator every sampler --- tests/samplers/test_batch.py | 5 ++--- tests/samplers/test_single.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/samplers/test_batch.py b/tests/samplers/test_batch.py index 20ad33a58c9..16b99e16a93 100644 --- a/tests/samplers/test_batch.py +++ b/tests/samplers/test_batch.py @@ -148,13 +148,12 @@ def test_weighted_sampling(self) -> None: def test_random_seed(self) -> None: ds = CustomGeoDataset() ds.index.insert(0, (0, 10, 0, 10, 0, 10)) - generator = torch.manual_seed(0) - sampler = RandomBatchGeoSampler(ds, 1, 1, generator=generator) + sampler = RandomBatchGeoSampler(ds, 1, 1, generator=torch.manual_seed(0)) for bbox in sampler: sample1 = bbox break - sampler = RandomBatchGeoSampler(ds, 1, 1, generator=generator) + sampler = RandomBatchGeoSampler(ds, 1, 1, generator=torch.manual_seed(0)) for bbox in sampler: sample2 = bbox break diff --git a/tests/samplers/test_single.py b/tests/samplers/test_single.py index 15f1025f672..abbf22d2727 100644 --- a/tests/samplers/test_single.py +++ b/tests/samplers/test_single.py @@ -308,15 +308,18 @@ def test_shuffle_seed(self) -> None: ds = CustomGeoDataset() ds.index.insert(0, (0, 10, 0, 10, 0, 10)) ds.index.insert(1, (0, 11, 0, 11, 0, 11)) - generator = torch.manual_seed(0) - sampler = PreChippedGeoSampler(ds, shuffle=True, generator=generator) - for bbox in sampler: + generator = torch.manual_seed(2) + sampler1 = PreChippedGeoSampler(ds, shuffle=True, generator=generator) + for bbox in sampler1: sample1 = bbox + print(sample1) break - sampler = PreChippedGeoSampler(ds, shuffle=True, generator=generator) - for bbox in sampler: + generator = torch.manual_seed(2) + sampler2 = PreChippedGeoSampler(ds, shuffle=True, generator=generator) + for bbox in sampler2: sample2 = bbox + print(sample2) break assert sample1 == sample2 From c51a63bdb188217cc32343f66b88566905bac6d8 Mon Sep 17 00:00:00 2001 From: Sieger Falkena Date: Mon, 23 Sep 2024 13:21:12 +0200 Subject: [PATCH 23/24] Revert "Merge branch 'vers_working_branch' into geosampler_prechipping" This reverts commit c7f3e4cf8b3e1ae6274d6f19f7417157a0515955, reversing changes made to d8cb4b207874fe69ab295e7e341edddd3b765644. --- tests/samplers/test_batch.py | 15 ------------ tests/samplers/test_single.py | 35 ---------------------------- torchgeo/datamodules/agrifieldnet.py | 6 +---- torchgeo/samplers/batch.py | 7 +----- torchgeo/samplers/single.py | 18 +++----------- torchgeo/samplers/utils.py | 10 +++----- 6 files changed, 8 insertions(+), 83 deletions(-) diff --git a/tests/samplers/test_batch.py b/tests/samplers/test_batch.py index 16b99e16a93..59c8aaa00be 100644 --- a/tests/samplers/test_batch.py +++ b/tests/samplers/test_batch.py @@ -6,7 +6,6 @@ from itertools import product import pytest -import torch from _pytest.fixtures import SubRequest from rasterio.crs import CRS from torch.utils.data import DataLoader @@ -145,20 +144,6 @@ def test_weighted_sampling(self) -> None: for bbox in batch: assert bbox == BoundingBox(0, 10, 0, 10, 0, 10) - def test_random_seed(self) -> None: - ds = CustomGeoDataset() - ds.index.insert(0, (0, 10, 0, 10, 0, 10)) - sampler = RandomBatchGeoSampler(ds, 1, 1, generator=torch.manual_seed(0)) - for bbox in sampler: - sample1 = bbox - break - - sampler = RandomBatchGeoSampler(ds, 1, 1, generator=torch.manual_seed(0)) - for bbox in sampler: - sample2 = bbox - break - assert sample1 == sample2 - @pytest.mark.slow @pytest.mark.parametrize('num_workers', [0, 1, 2]) def test_dataloader( diff --git a/tests/samplers/test_single.py b/tests/samplers/test_single.py index 77db86f2b57..6fdbf4712fc 100644 --- a/tests/samplers/test_single.py +++ b/tests/samplers/test_single.py @@ -7,7 +7,6 @@ import geopandas as gpd import pytest -import torch from _pytest.fixtures import SubRequest from geopandas import GeoDataFrame from rasterio.crs import CRS @@ -223,21 +222,6 @@ def test_weighted_sampling(self) -> None: for bbox in sampler: assert bbox == BoundingBox(0, 10, 0, 10, 0, 10) - def test_random_seed(self) -> None: - ds = CustomGeoDataset() - ds.index.insert(0, (0, 10, 0, 10, 0, 10)) - generator = torch.manual_seed(0) - sampler = RandomGeoSampler(ds, 1, 1, generator=generator) - for bbox in sampler: - sample1 = bbox - break - - sampler = RandomGeoSampler(ds, 1, 1, generator=generator) - for bbox in sampler: - sample2 = bbox - break - assert sample1 == sample2 - @pytest.mark.slow @pytest.mark.parametrize('num_workers', [0, 1, 2]) def test_dataloader( @@ -387,25 +371,6 @@ def test_point_data(self) -> None: for _ in sampler: continue - def test_shuffle_seed(self) -> None: - ds = CustomGeoDataset() - ds.index.insert(0, (0, 10, 0, 10, 0, 10)) - ds.index.insert(1, (0, 11, 0, 11, 0, 11)) - generator = torch.manual_seed(2) - sampler1 = PreChippedGeoSampler(ds, shuffle=True, generator=generator) - for bbox in sampler1: - sample1 = bbox - print(sample1) - break - - generator = torch.manual_seed(2) - sampler2 = PreChippedGeoSampler(ds, shuffle=True, generator=generator) - for bbox in sampler2: - sample2 = bbox - print(sample2) - break - assert sample1 == sample2 - @pytest.mark.slow @pytest.mark.parametrize('num_workers', [0, 1, 2]) def test_dataloader( diff --git a/torchgeo/datamodules/agrifieldnet.py b/torchgeo/datamodules/agrifieldnet.py index c5b92b6b01a..bed6365d4a2 100644 --- a/torchgeo/datamodules/agrifieldnet.py +++ b/torchgeo/datamodules/agrifieldnet.py @@ -74,11 +74,7 @@ def setup(self, stage: str) -> None: if stage in ['fit']: self.train_batch_sampler = RandomBatchGeoSampler( - self.train_dataset, - self.patch_size, - self.batch_size, - self.length, - generator=generator, + self.train_dataset, self.patch_size, self.batch_size, self.length ) if stage in ['fit', 'validate']: self.val_sampler = GridGeoSampler( diff --git a/torchgeo/samplers/batch.py b/torchgeo/samplers/batch.py index 396ad0f0c7b..22726f74b2c 100644 --- a/torchgeo/samplers/batch.py +++ b/torchgeo/samplers/batch.py @@ -70,7 +70,6 @@ def __init__( length: int | None = None, roi: BoundingBox | None = None, units: Units = Units.PIXELS, - generator: torch.Generator | None = None, ) -> None: """Initialize a new Sampler instance. @@ -98,11 +97,9 @@ def __init__( roi: region of interest to sample from (minx, maxx, miny, maxy, mint, maxt) (defaults to the bounds of ``dataset.index``) units: defines if ``size`` is in pixel or CRS units - generator: random number generator """ super().__init__(dataset, roi) self.size = _to_tuple(size) - self.generator = generator if units == Units.PIXELS: self.size = (self.size[0] * self.res, self.size[1] * self.res) @@ -147,9 +144,7 @@ def __iter__(self) -> Iterator[list[BoundingBox]]: # Choose random indices within that tile batch = [] for _ in range(self.batch_size): - bounding_box = get_random_bounding_box( - bounds, self.size, self.res, self.generator - ) + bounding_box = get_random_bounding_box(bounds, self.size, self.res) batch.append(bounding_box) yield batch diff --git a/torchgeo/samplers/single.py b/torchgeo/samplers/single.py index 452dfaaadad..fcb4ed536f8 100644 --- a/torchgeo/samplers/single.py +++ b/torchgeo/samplers/single.py @@ -5,7 +5,6 @@ import abc from collections.abc import Callable, Iterable, Iterator -from functools import partial import geopandas as gpd import numpy as np @@ -211,7 +210,6 @@ def __init__( length: int | None = None, roi: BoundingBox | None = None, units: Units = Units.PIXELS, - generator: torch.Generator | None = None, ) -> None: """Initialize a new Sampler instance. @@ -238,8 +236,6 @@ def __init__( roi: region of interest to sample from (minx, maxx, miny, maxy, mint, maxt) (defaults to the bounds of ``dataset.index``) units: defines if ``size`` is in pixel or CRS units - generator: The random generator used for sampling. - """ super().__init__(dataset, roi) self.size = _to_tuple(size) @@ -247,7 +243,6 @@ def __init__( if units == Units.PIXELS: self.size = (self.size[0] * self.res, self.size[1] * self.res) - self.generator = generator self.length = 0 self.hits = [] areas = [] @@ -309,7 +304,7 @@ def get_chips(self) -> GeoDataFrame: bounds = BoundingBox(*hit.bounds) # Choose a random index within that tile - bbox = get_random_bounding_box(bounds, self.size, self.res, self.generator) + bbox = get_random_bounding_box(bounds, self.size, self.res) minx, maxx, miny, maxy, mint, maxt = tuple(bbox) chip = { 'geometry': box(minx, miny, maxx, maxy), @@ -452,11 +447,7 @@ class PreChippedGeoSampler(GeoSampler): """ def __init__( - self, - dataset: GeoDataset, - roi: BoundingBox | None = None, - shuffle: bool = False, - generator: torch.Generator | None = None, + self, dataset: GeoDataset, roi: BoundingBox | None = None, shuffle: bool = False ) -> None: """Initialize a new Sampler instance. @@ -467,12 +458,9 @@ def __init__( roi: region of interest to sample from (minx, maxx, miny, maxy, mint, maxt) (defaults to the bounds of ``dataset.index``) shuffle: if True, reshuffle data at every epoch - generator: The random number generator used in combination with shuffle. - """ super().__init__(dataset, roi) self.shuffle = shuffle - self.generator = generator self.hits = [] for hit in self.index.intersection(tuple(self.roi), objects=True): @@ -489,7 +477,7 @@ def get_chips(self) -> GeoDataFrame: """ generator: Callable[[int], Iterable[int]] = range if self.shuffle: - generator = partial(torch.randperm, generator=self.generator) + generator = torch.randperm print('generating samples... ') chips = [] diff --git a/torchgeo/samplers/utils.py b/torchgeo/samplers/utils.py index 258f74a5425..a1fca673a3a 100644 --- a/torchgeo/samplers/utils.py +++ b/torchgeo/samplers/utils.py @@ -35,10 +35,7 @@ def _to_tuple(value: tuple[float, float] | float) -> tuple[float, float]: def get_random_bounding_box( - bounds: BoundingBox, - size: tuple[float, float] | float, - res: float, - generator: torch.Generator | None = None, + bounds: BoundingBox, size: tuple[float, float] | float, res: float ) -> BoundingBox: """Returns a random bounding box within a given bounding box. @@ -53,7 +50,6 @@ def get_random_bounding_box( bounds: the larger bounding box to sample from size: the size of the bounding box to sample res: the resolution of the image - generator: random number generator Returns: randomly sampled bounding box from the extent of the input @@ -68,8 +64,8 @@ def get_random_bounding_box( miny = bounds.miny # Use an integer multiple of res to avoid resampling - minx += int(torch.rand(1, generator=generator).item() * width) * res - miny += int(torch.rand(1, generator=generator).item() * height) * res + minx += int(torch.rand(1).item() * width) * res + miny += int(torch.rand(1).item() * height) * res maxx = minx + t_size[1] maxy = miny + t_size[0] From e932902e0e17a822c4c4c9f4acf5fa31cd96749c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:11:12 +0000 Subject: [PATCH 24/24] Bump ruff from 0.6.6 to 0.6.7 in /requirements (#2313) --- requirements/style.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/style.txt b/requirements/style.txt index a88e62af3cc..648a2033db5 100644 --- a/requirements/style.txt +++ b/requirements/style.txt @@ -1,3 +1,3 @@ # style mypy==1.11.2 -ruff==0.6.6 +ruff==0.6.7