Skip to content
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

Segmentation: Chips and Training #337

Merged
merged 23 commits into from Aug 14, 2018

Conversation

jamesmcclain
Copy link
Contributor

@jamesmcclain jamesmcclain commented Jul 31, 2018

Overview

This pull request adds the ability to generate Deeplab-compatible TFRecords and the ability to train a Deeplab model using the same. Preliminary to #321 .

Checklist

  • Ran scripts/format_code and commited any changes
  • Documentation updated if needed
  • PR has a name that won't get you publicly shamed for vagueness
  • DocStrings
  • Remote operation
  • Ensure Tensorboard
  • Hex to class mapping, use colors in debug chips
  • Add Potsdam Georeferencing Script
  • Respond to comments

Notes

Optional. Ancillary topics, caveats, alternative strategies that didn't work out, anything else.

Testing Instructions

Step 1

Prepare the test data by using the contrib/cowc/transfer_georeference.py script. Typing

transfer_georeference.py  top_potsdam_2_10_RGBIR.tif top_potsdam_2_10_label_noBoundary.tif top_potsdam_2_10_label_georeferenced.tif

will apply the georeferencing information from top_potsdam_2_10_RGBIR.tif to the ungeoreferenced label file top_potsdam_2_10_label_noBoundary.tif to produce a new file top_potsdam_2_10_label_georeferenced.tif. The same must be done for 2_11.

Step 2

Use one of the two workflow configuration files: samples/workflow-configs/segmentation/deeplab-test.json or samples/workflow-configs/segmentation/deeplab-remote-test.json.

@jamesmcclain jamesmcclain mentioned this pull request Jul 31, 2018
20 tasks
@jamesmcclain jamesmcclain force-pushed the segmentation branch 3 times, most recently from 79ab472 to f86c3af Compare August 2, 2018 11:36
@jamesmcclain
Copy link
Contributor Author

INFO:tensorflow:global step 10: loss = 3.0484 (3.197 sec/step)
INFO:tensorflow:global step 20: loss = 2.1668 (3.190 sec/step)
INFO:tensorflow:global step 30: loss = 1.0515 (3.623 sec/step)
INFO:tensorflow:global step 40: loss = 0.6545 (3.574 sec/step)
INFO:tensorflow:global step 50: loss = 0.6408 (3.289 sec/step)
INFO:tensorflow:global step 60: loss = 0.5511 (3.453 sec/step)
INFO:tensorflow:global step 70: loss = 0.2832 (3.401 sec/step)
INFO:tensorflow:global step 80: loss = 0.4439 (3.533 sec/step)
INFO:tensorflow:global step 90: loss = 0.9702 (3.548 sec/step)
INFO:tensorflow:global step 100: loss = 1.1956 (3.269 sec/step)
INFO:tensorflow:global step 110: loss = 0.4424 (3.513 sec/step)
INFO:tensorflow:global step 120: loss = 0.8231 (3.320 sec/step)
INFO:tensorflow:global step 130: loss = 0.7730 (3.300 sec/step)

@jamesmcclain
Copy link
Contributor Author

screenshot from 2018-08-02 15-14-56
screenshot from 2018-08-02 15-15-07
screenshot from 2018-08-02 15-15-14

start_sync)


def numpy_to_png(array: np.ndarray) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps these are broadly useful enough to pull into a utility package?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, will-do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -37,9 +39,17 @@ message ClassificationGeoJSONFile {
optional Options options = 2;
}

message SegmentationRasterFile {
optional RasterSource src = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a comment for each of these fields would be helpful.

Copy link
Contributor

@lewfish lewfish Aug 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe comments would resolve my confusion, but more generally, I don't understand the notion of source and destination in this PR. Does source correspond to where the ground truth comes from and destination where the predictions go? If so, you should use ground_truth_label_store for the ground truth and a separate prediction_label_store for the predictions to follow the convention in the rest of RV.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, will-do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe comments would resolve my confusion, but more generally, I don't understand the notion of source and destination in this PR.

The nomenclature may be poorly chosen.

In the case of rasters, the "source" refers to the given label data (read from the filesystem or some remote location) and the "destination" is where internally generated labeling information is sent (written to the filesystem or some remote location).

In the case of classes, the "source" classes are those of the source rasters; the "destination" classes are those used internally (e.g. "cars" being class of 1 [from the configuration files] instead of class 0xffff00 [as they are in the given labels]).

The use of same two words in slightly different contexts is to emphasize the connection between the two: one translates from classes in the source raster to those in the destination raster by considering source classes versus destination classes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source classes changed from integers to hex numbers, comments added to .proto file.

}
},
"src_classes": [ 6, 1 ],
"dst_classes": [ 1, 0 ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class_ids should start at 1.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing these are indices into the channel dimension. It would be good to clarify that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are not indices, please see the (rewritten) response above. All of these will be well documented.

In this particular case what you see is 0xffff00 -> 110b -> 6 (0xffff00 is the labeling for a "car" in the source raster) being mapped to 1 (the classmap maps the class 1 to the word "car").

The class 0x0000ff -> 001 -> 1 is being mapped to zero because 0x0000ff is for buildings and we don't want to consider those.

xmin = window.xmin
ymax = window.ymax
xmax = window.xmax
return np.zeros((self.channels, ymax - ymin, xmax - xmin))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chips are stored with channels in the last dimension elsewhere in RV.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also true of TF.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -17,6 +17,25 @@ message TrainConfig {
optional string export_py = 3 [default="/opt/tf-models/object_detection/export_inference_graph.py"];
}

message SegmentationOptions {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like these parameters should be in a backend config file which is referenced from the backend_config_uri field to be consistent with the rest of RV. This would involve moving the bulk of the fields in SegmentationOptions into a DeepLabBackendConfig proto or similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

args.append('--save_summaries_secs={}'.format(
soptions.save_summaries_secs))
args.append('--save_summaries_images={}'.format(
soptions.save_summaries_images))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building up the args seems like a good thing to extract to another function.

@jamesmcclain jamesmcclain force-pushed the segmentation branch 3 times, most recently from 5617426 to 4c3f895 Compare August 7, 2018 22:01
... and other miscellaneous changes.
Copy link
Contributor

@lewfish lewfish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got this to run locally, and will test remotely next.

chip_size = options.chip_size

windows = []
for i in range(100): # XXX insensitive
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be an option.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in my current branch.

@@ -0,0 +1,38 @@
#!/usr/bin/env python

import os
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had trouble running this in the Docker container, I think due to a Python version mismatch. But, I've realized there's no need to add georeferencing to the label files. We can just use ImageFile instead of GeoTiffFiles as the RasterSource to handle non-georeferenced imagery. Here is the relevant part of the workflow config I used to get it to work:

{
    "train_scenes": [
        {
            "id": "2-10",
            "raster_source": {
                "geotiff_files": {
                    "uris": [
                        "{raw}/isprs-potsdam/4_Ortho_RGBIR/top_potsdam_2_10_RGBIR.tif"
                    ]
                }
            },
            "ground_truth_label_store": {
                "segmentation_raster_file": {
                    "src": {
                        "image_file": {
                            "uri": "{raw}/isprs-potsdam/5_Labels_for_participants_no_Boundary/top_potsdam_2_10_label_noBoundary.tif"
                        }
                    },
                    "src_classes": [ "#ffff00", "#0000ff" ],
                    "dst_classes": [ 1, 0 ]
                }
            }
        }
    ],
    "test_scenes": [
        {
            "id": "2-11",
            "raster_source": {
                "geotiff_files": {
                    "uris": [
                        "{raw}/isprs-potsdam/4_Ortho_RGBIR/top_potsdam_2_11_RGBIR.tif"
                    ]
                }
            },
            "ground_truth_label_store": {
                "segmentation_raster_file": {
                    "src": {
                        "image_file": {
                            "uri": "{raw}/isprs-potsdam/5_Labels_for_participants_no_Boundary/top_potsdam_2_11_label_noBoundary.tif"
                        }
                    },
                    "src_classes": [ "#ffff00", "#0000ff" ],
                    "dst_classes": [ 1, 0 ]
                }
            }
        }
    ],

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am running in the container, as well. I am not sure how a version mis-match could occur. Edit: I see now that you are talking about the georeferencing script. I'll address that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does image_file work when there is more than one label raster for the scene? If there is only one label raster per scene, does that imply that there is only one image raster per scene?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image_file does not work if there is more than one label raster for the scene. I'm guessing that case won't come up very frequently, but it's possible. For this dataset, it's not a problem.
Re: your second question, I think it's possible for there to be a single label raster, but multiple image rasters per scene.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is going to be a "getting started" example, we should probably use image_file and avoid having to run the georeferencing script, although it could be useful for another application.

Here's what I got when I ran the script in the container after running update:
root@e820085104e2:/opt/src# ./rastervision/contrib/cowc/transfer_georeference.py \

/opt/data/raw-data/isprs-potsdam/4_Ortho_RGBIR/top_potsdam_2_10_RGBIR.tif \
/opt/data/raw-data/isprs-potsdam/5_Labels_for_participants_no_Boundary/top_potsdam_2_10_label_noBoundary.tif \
/opt/data/raw-data/isprs-potsdam/labels/2_10.tif

Traceback (most recent call last):
File "./rastervision/contrib/cowc/transfer_georeference.py", line 27, in
ul = re.search(ul_re, ullr)
File "/usr/lib/python3.5/re.py", line 173, in search
return _compile(pattern, flags).search(string)
TypeError: cannot use a string pattern on a bytes-like object

"""Constructor.

Args:
src: A source of raster label data (either an object that
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understand the difference between src and dst now. I was confused because in the other LabelStores I was just using uri for both reading and writing (but had separate readable and writable fields). But I still think it's confusing that the term dst is being used in both dst, and dst_classes which seems like a different concept. Perhaps dst_classes should be called rv_classes since they are the class ids that RV is using internally, and they are used even when there is no dst specified.

if isinstance(src, RasterSource):
self.src = src
elif isinstance(src, RasterSourceProto):
self.src = raster_source_builder.build(src)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how this is smart enough to handle different types. Seems like a good API design pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

def make_debug_images(record_path: str,
output_dir: str,
class_map: ClassMap,
p: float = 0.25) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea to only save a random subset.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

file.

"""
return join(base_uri, '{}-0.record'.format(split))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason for the -0? Just wondering because the code for OD doesn't do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for some reason deeplap wants files with names of the form *-[0-9]\+.record.

@@ -24,6 +27,40 @@ class ProtobufParseException(Exception):
pass


def numpy_to_png(array: np.ndarray) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these two functions should be in utils.misc since they don't really deal with file handling.

backend_config_uri = get_local_path(options.backend_config_uri,
self.temp_dir)
with open(backend_config_uri) as f:
be_options = json.load(f)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of just using raw JSON file for the backend options, I think we should use a protobuf file, like in the rest of RV. That way we can more easily document and validate the options.

}
},
"src_classes": [ "#ffff00", "#0000ff" ],
"dst_classes": [ 1, 0 ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are there two elements in dst_classes but only one in class_items? I think it has something to do with the fact that everything that's not in the class_map counts as background and is assigned the id 0. But if that's true, then I still don't understand why 0 needs to be mapped to a hex value at all. Perhaps there should be more documentation about converting classes to the background value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#ffff00 is for cars, #0000ff is for buildings. #ffff00 is mapped to 1 (cars) and #0000ff is mapped to 0 (no label).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we are training a model to distinguish between car and building. But, then shouldn't there be an item in class_items for building?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is only one class under consideration: cars. Let me make some changes to clarify the situation.

@lewfish
Copy link
Contributor

lewfish commented Aug 13, 2018

Do you have any idea why some of the images are zoomed out (with gray backgrounds) in Tensorboard? Also, after 2000 steps I thought I would see some labels popping up in the predictions. screen shot 2018-08-13 at 1 37 01 pm

@lewfish
Copy link
Contributor

lewfish commented Aug 13, 2018

Do you have any idea why some of the images are zoomed out (with gray backgrounds) in Tensorboard? Also, after 2000 steps I thought I would see some labels popping up in the predictions.

If you think there's a bug we should probably just make a separate issue for it.

@jamesmcclain
Copy link
Contributor Author

jamesmcclain commented Aug 13, 2018

Do you have any idea why some of the images are zoomed out (with gray backgrounds) in Tensorboard?

I do not. Changing the chip size to 513x513 seemed to reduce this, but I still see some oddly scaled images. The might be from edges or corners but I noticed that seem to be smaller in scale so I don't think that that is so.

Also, after 2000 steps I thought I would see some labels popping up in the predictions.

So did I, still working on it.

@jamesmcclain
Copy link
Contributor Author

Comments addressed.

@jamesmcclain
Copy link
Contributor Author

Do you have any idea why some of the images are zoomed out (with gray backgrounds) in Tensorboard?

Looks like augmentation.

board1

42

8

Copy link
Contributor

@lewfish lewfish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the other changes look good to me!

]
}
},
"raster_class_map": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The raster_class_map is easier to understand. But, I still don't know why there is only an item for car, and not for building. Also, class_ids are expected to go from 1 to N for object detection and classification or things will break, and that should be fixed at some point. So I'm glad your implementation doesn't depend on it (since you've chosen 127 and 255 as ids), but that could be confusing, so maybe they should be changed to 1 and 2?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this sample only detecting cars? There are several classes inside the labels (building, low veg, trees, clutter, impervious surface) - bit confused why we would include buildings but not all of these, and I can see including just the target class, in this case, cars

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class_items contains items for cars and buildings, but the raster_class_map only has an item for car. The mismatch is what's confusing me. I think it makes sense to use a (consistent) subset though.

]
}
},
"raster_class_map": [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, what do you think about switch from geotiff_files to image_file so your script doesn't need to be run, esp. for new users?

}
]
},
"make_training_chips_options": {
"segmentation_options": {
"empty_survival_probability": 0.2,
"empty_survival_probability": 0.1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also switch to using image_file on this file too?

@jamesmcclain
Copy link
Contributor Author

Are there any further changes requested?

@lossyrob
Copy link
Contributor

Looks like the Travis CI failed because of code formatting? https://travis-ci.org/azavea/raster-vision/builds/415948141#L2128

@lewfish
Copy link
Contributor

lewfish commented Aug 14, 2018

Once the build passes this is good to merge. I think you did an awesome job on this, especially considering the lack of documentation and being new to the project. Thanks!

@jamesmcclain
Copy link
Contributor Author

Failed merge with working branch, trying to resolve.

@jamesmcclain jamesmcclain merged commit f0b93ea into azavea:develop Aug 14, 2018
@jamesmcclain jamesmcclain deleted the segmentation branch August 14, 2018 15:20
@CloudNiner CloudNiner removed the review label Aug 14, 2018
@jamesmcclain jamesmcclain restored the segmentation branch August 14, 2018 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants