-
Notifications
You must be signed in to change notification settings - Fork 387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MultiBands, and tools improvements (mostly) #138
base: master
Are you sure you want to change the base?
Changes from 62 commits
213540e
6991719
bbd86a3
9c9b307
d51393d
d8f1686
43a6050
b20e5a4
73933fd
fd4ca3b
29935f6
2e97d16
dd15457
822bae7
0b86808
1e43632
48115d2
82c2bcd
f2e6405
b0c554c
fab73f9
e68d8f5
236f471
69ef557
249107b
1407730
2fe3864
3f27682
8da0e8b
b2a76dd
4946f6f
de654a4
bf30a22
173a60d
98efc71
350d46f
7a5eae4
275f017
47364ea
9b0588c
9457880
f9e788b
c874a04
2270d49
6a55b2b
b75c3b1
13d4f7c
d260eef
7c88599
7083cb9
aad94fc
8f1faca
5ade9f2
e6a5f87
1bf590a
4e795e0
23b9b96
ce12ab5
9ce1782
3218253
f79fa97
b20561d
ccc7e8d
16a985d
bc82113
008398e
583f1ce
47d91a4
293673e
e3cfd3a
cce48b6
30a68ad
42c110b
6ead578
6aba3a7
a0fd555
d3dbd89
57f3bc4
01ea0b2
b4391d3
3612d70
0433e26
ed16e8f
53b0b4e
e02dffd
1a451d3
0fa123e
0c6fff7
24716db
bcf13ce
552ffb5
0504df5
9f3eb8f
0aa7f46
d7e8559
c628975
a59d971
9b8f94e
bb2dcf8
b28b2c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
Daniel J. Hofmann <[email protected]> https://github.com/daniel-j-h | ||
|
||
Bhargav Kowshik <[email protected]> https://github.com/bkowshik | ||
|
||
Olivier Courtin <[email protected]> https://github.com/ocourtin | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,3 +21,11 @@ | |
# Note: use `./rs weights -h` to compute these for new datasets. | ||
[weights] | ||
values = [1.6248, 5.762827] | ||
|
||
|
||
# Channels configuration let your indicate wich dataset sub-directory and bands to take as input | ||
# You could so, add several channels blocks to compose your input Tensor. Orders are meaningful. | ||
[[channels]] | ||
type = "file" | ||
sub = "images" | ||
bands = [1,2,3] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should these start at zero? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By convention raster bands are named 1-N (at the very least in GDAL and rasterio) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,9 +5,6 @@ | |
# Model specific common attributes. | ||
[common] | ||
|
||
# Use CUDA for GPU acceleration. | ||
cuda = true | ||
|
||
# Batch size for training. | ||
batch_size = 2 | ||
|
||
|
@@ -32,3 +29,6 @@ | |
|
||
# Loss function name (e.g 'Lovasz', 'mIoU' or 'CrossEntropy') | ||
loss = 'Lovasz' | ||
|
||
# Data augmentation, Flip or Rotate probability | ||
data_augmentation = 0.75 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we expose this in a configuration file? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The ideal value (on the dataset i play with) is related to the number of training epochs. With a quick train, best result is with small or even no data augmentation. So it's looks like something a power user could want to change. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,15 +20,15 @@ more-itertools==4.2.0 | |
numpy==1.14.4 | ||
opencv-contrib-python==3.4.1.15 | ||
osmium==2.14.1 | ||
Pillow==5.1.0 | ||
Pillow-simd==5.3.0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting - do you see image operations as a bottleneck and/or can your workers not keep the gpus busy for some reason? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the train tool, indeed the workers performs well, and it's easy to feed fully the GPUs. |
||
pluggy==0.6.0 | ||
py==1.5.3 | ||
pyparsing==2.2.0 | ||
pyproj==1.9.5.1 | ||
pytest==3.6.1 | ||
python-dateutil==2.7.3 | ||
pytz==2018.4 | ||
rasterio==1.0b1 | ||
rasterio==1.0.9 | ||
requests==2.18.4 | ||
Rtree==0.8.3 | ||
scipy==1.1.0 | ||
|
@@ -37,7 +37,7 @@ six==1.11.0 | |
snuggs==1.4.1 | ||
supermercado==0.0.5 | ||
toml==0.9.4 | ||
torch==0.4.0 | ||
torch==0.4.1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should rebase onto master. For example this is already updated there. Maybe rebase on master and then update the deps and the lockfile. |
||
torchvision==0.2.1 | ||
tqdm==4.23.4 | ||
urllib3==1.22 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ FROM ubuntu:16.04 | |
# See: https://github.com/skvark/opencv-python/issues/90 | ||
RUN apt-get update -qq && \ | ||
apt-get install -qq -y -o quiet=1 \ | ||
python3 python3-dev python3-tk python3-pip build-essential libboost-python-dev libexpat1-dev zlib1g-dev libbz2-dev libspatialindex-dev libsm6 | ||
python3 python3-dev python3-tk python3-pip build-essential libboost-python-dev libexpat1-dev zlib1g-dev libbz2-dev libspatialindex-dev libsm6 libwebp-dev libjpeg-turbo8-dev | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to add these new system-wide native deps to the readme installation instructions, too. For users not running the dockerized image. |
||
|
||
WORKDIR /app | ||
ADD . /app | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
""" | ||
|
||
import colorsys | ||
import numpy as np | ||
|
||
from enum import Enum, unique | ||
|
||
|
@@ -49,9 +50,10 @@ def make_palette(*colors): | |
colors: variable number of color names. | ||
""" | ||
|
||
assert 0 < len(colors) <= 256 | ||
rgbs = [Mapbox[color].value for color in colors] | ||
flattened = sum(rgbs, ()) | ||
return list(flattened) | ||
|
||
return list(sum(rgbs, ())) | ||
|
||
|
||
def color_string_to_rgb(color): | ||
|
@@ -84,12 +86,24 @@ def continuous_palette_for_color(color, bins=256): | |
r, g, b = [v / 255 for v in Mapbox[color].value] | ||
h, s, v = colorsys.rgb_to_hsv(r, g, b) | ||
|
||
palette = [] | ||
assert 0 < bins <= 256 | ||
|
||
palette = [] | ||
for i in range(bins): | ||
ns = (1 / bins) * (i + 1) | ||
palette.extend([int(v * 255) for v in colorsys.hsv_to_rgb(h, ns, v)]) | ||
|
||
assert len(palette) // 3 == bins | ||
r, g, b = [int(v * 255) for v in colorsys.hsv_to_rgb(h, (1 / bins) * (i + 1), v)] | ||
palette.extend(r, g, b) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 Nice refactor |
||
|
||
return palette | ||
|
||
|
||
def complementary_palette(palette): | ||
|
||
comp_palette = [] | ||
colors = [palette[i : i + 3] for i in range(0, len(palette), 3)] | ||
|
||
for color in colors: | ||
r, g, b = [v for v in color] | ||
h, s, v = colorsys.rgb_to_hsv(r, g, b) | ||
comp_palette.extend(map(int, colorsys.hsv_to_rgb((h + 0.5) % 1, s, v))) | ||
|
||
return comp_palette | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neat! |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,9 +5,13 @@ | |
See: http://pytorch.org/docs/0.3.1/data.html | ||
""" | ||
|
||
import os | ||
import sys | ||
import torch | ||
from PIL import Image | ||
import torch.utils.data | ||
import cv2 | ||
import numpy as np | ||
|
||
from robosat.tiles import tiles_from_slippy_map, buffer_tile_image | ||
|
||
|
@@ -17,21 +21,35 @@ class SlippyMapTiles(torch.utils.data.Dataset): | |
"""Dataset for images stored in slippy map format. | ||
""" | ||
|
||
def __init__(self, root, transform=None): | ||
def __init__(self, root, mode, transform=None): | ||
super().__init__() | ||
|
||
self.tiles = [] | ||
self.transform = transform | ||
|
||
self.tiles = [(tile, path) for tile, path in tiles_from_slippy_map(root)] | ||
self.tiles.sort(key=lambda tile: tile[0]) | ||
self.mode = mode | ||
|
||
def __len__(self): | ||
return len(self.tiles) | ||
|
||
def __getitem__(self, i): | ||
tile, path = self.tiles[i] | ||
image = Image.open(path) | ||
|
||
if self.mode == "image": | ||
image = cv2.cvtColor(cv2.imread(path), cv2.COLOR_BGR2RGB) | ||
|
||
elif self.mode == "multibands": | ||
image = cv2.imread(path, cv2.IMREAD_ANYCOLOR) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, I'm wondering how we should design the api around multi-band images. What do you think of instead of splitting the distinction into |
||
if len(image.shape) == 3 and image.shape[2] >= 3: | ||
# FIXME Look twice to find an in-place way to perform a multiband BGR2RGB | ||
g = image[:, :, 0] | ||
image[:, :, 0] = image[:, :, 2] | ||
image[:, :, 2] = g | ||
|
||
elif self.mode == "mask": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should these modes be enum values? |
||
image = np.array(Image.open(path).convert("P")) | ||
|
||
if self.transform is not None: | ||
image = self.transform(image) | ||
|
@@ -40,42 +58,49 @@ def __getitem__(self, i): | |
|
||
|
||
# Multiple Slippy Map directories. | ||
# Think: one with images, one with masks, one with rasterized traces. | ||
class SlippyMapTilesConcatenation(torch.utils.data.Dataset): | ||
"""Dataset to concate multiple input images stored in slippy map format. | ||
""" | ||
|
||
def __init__(self, inputs, target, joint_transform=None): | ||
def __init__(self, path, channels, target, joint_transform=None): | ||
super().__init__() | ||
|
||
# No transformations in the `SlippyMapTiles` instead joint transformations in getitem | ||
self.joint_transform = joint_transform | ||
assert len(channels), "Channels configuration empty" | ||
self.channels = channels | ||
self.inputs = dict() | ||
|
||
for channel in channels: | ||
for band in channel["bands"]: | ||
self.inputs[channel["sub"]] = SlippyMapTiles(os.path.join(path, channel["sub"]), mode="multibands") | ||
|
||
self.inputs = [SlippyMapTiles(inp) for inp in inputs] | ||
self.target = SlippyMapTiles(target) | ||
self.target = SlippyMapTiles(target, mode="mask") | ||
|
||
assert len(set([len(dataset) for dataset in self.inputs])) == 1, "same number of tiles in all images" | ||
assert len(self.target) == len(self.inputs[0]), "same number of tiles in images and label" | ||
# No transformations in the `SlippyMapTiles` instead joint transformations in getitem | ||
self.joint_transform = joint_transform | ||
|
||
def __len__(self): | ||
return len(self.target) | ||
|
||
def __getitem__(self, i): | ||
# at this point all transformations are applied and we expect to work with raw tensors | ||
inputs = [dataset[i] for dataset in self.inputs] | ||
|
||
images = [image for image, _ in inputs] | ||
tiles = [tile for _, tile in inputs] | ||
mask, tile = self.target[i] | ||
|
||
mask, mask_tile = self.target[i] | ||
for channel in self.channels: | ||
try: | ||
data, band_tile = self.inputs[channel["sub"]][i] | ||
assert band_tile == tile | ||
|
||
assert len(set(tiles)) == 1, "all images are for the same tile" | ||
assert tiles[0] == mask_tile, "image tile is the same as label tile" | ||
for band in channel["bands"]: | ||
data_band = data[:, :, int(band) - 1] if len(data.shape) == 3 else data_band | ||
data_band = data_band.reshape(mask.shape[0], mask.shape[1], 1) | ||
tensor = np.concatenate((tensor, data_band), axis=2) if "tensor" in locals() else data_band | ||
except: | ||
sys.exit("Unable to concatenate input Tensor") | ||
|
||
if self.joint_transform is not None: | ||
images, mask = self.joint_transform(images, mask) | ||
tensor, mask = self.joint_transform(tensor, mask) | ||
|
||
return torch.cat(images, dim=0), mask, tiles | ||
return tensor, mask, tile | ||
|
||
|
||
# Todo: once we have the SlippyMapDataset this dataset should wrap | ||
|
@@ -113,7 +138,7 @@ def __len__(self): | |
|
||
def __getitem__(self, i): | ||
tile, path = self.tiles[i] | ||
image = buffer_tile_image(tile, self.tiles, overlap=self.overlap, tile_size=self.size) | ||
image = np.array(buffer_tile_image(tile, self.tiles, overlap=self.overlap, tile_size=self.size)) | ||
|
||
if self.transform is not None: | ||
image = self.transform(image) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,16 +24,19 @@ def __init__(self, labels): | |
self.fp = 0 | ||
self.tp = 0 | ||
|
||
def add(self, actual, predicted): | ||
def add(self, label, predicted, is_prob=True): | ||
"""Adds an observation to the tracker. | ||
|
||
Args: | ||
actual: the ground truth labels. | ||
predicted: the predicted labels. | ||
label: the ground truth labels. | ||
predicted: the predicted prob or mask. | ||
is_prob: as predicted could be either a prob or a mask. | ||
""" | ||
|
||
masks = torch.argmax(predicted, 0) | ||
confusion = masks.view(-1).float() / actual.view(-1).float() | ||
if is_prob: | ||
predicted = torch.argmax(predicted, 0) | ||
|
||
confusion = predicted.view(-1).float() / label.view(-1).float() | ||
|
||
self.tn += torch.sum(torch.isnan(confusion)).item() | ||
self.fn += torch.sum(confusion == float("inf")).item() | ||
|
@@ -46,7 +49,13 @@ def get_miou(self): | |
Returns: | ||
The mean Intersection over Union score for all observations seen so far. | ||
""" | ||
return np.nanmean([self.tn / (self.tn + self.fn + self.fp), self.tp / (self.tp + self.fn + self.fp)]) | ||
|
||
try: | ||
miou = np.nanmean([self.tn / (self.tn + self.fn + self.fp), self.tp / (self.tp + self.fn + self.fp)]) | ||
except ZeroDivisionError: | ||
miou = float("NaN") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch |
||
|
||
return miou | ||
|
||
def get_fg_iou(self): | ||
"""Retrieves the foreground Intersection over Union score. | ||
|
@@ -58,7 +67,7 @@ def get_fg_iou(self): | |
try: | ||
iou = self.tp / (self.tp + self.fn + self.fp) | ||
except ZeroDivisionError: | ||
iou = float("Inf") | ||
iou = float("NaN") | ||
|
||
return iou | ||
|
||
|
@@ -74,7 +83,7 @@ def get_mcc(self): | |
(self.tp + self.fp) * (self.tp + self.fn) * (self.tn + self.fp) * (self.tn + self.fn) | ||
) | ||
except ZeroDivisionError: | ||
mcc = float("Inf") | ||
mcc = float("NaN") | ||
|
||
return mcc | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Thanks for all your work so far!