CosmiQ / solaris

CosmiQ Works Geospatial Machine Learning Analysis Toolkit
https://solaris.readthedocs.io
Apache License 2.0
413 stars 112 forks source link

Error in " mask_to_poly_geojson" if do_transform is False with empty mask #409

Open asdspal opened 3 years ago

asdspal commented 3 years ago

Thank you for helping us improve solaris!

Summary of the bug

I'm getting error message "AttributeError: 'NoneType' object has no attribute 'to_epsg' " when invoking mask_to_poly_geojson with do_transform=False.

Steps to reproduce the bug

crs = rasterio.crs.CRS()
polygon_gdf = gpd.GeoDataFrame({'geometry': [], 'value': []},
                                   crs=crs.to_wkt())
output_path = "empty.geojson"
save_empty_geojson(output_path, polygon_gdf.crs.to_epsg())

Paste bug-causing code here

def mask_to_poly_geojson(pred_arr, channel_scaling=None, reference_im=None,
                         output_path=None, output_type='geojson', min_area=40,
                         bg_threshold=0, do_transform=None, simplify=False,
                         tolerance=0.5, **kwargs):
    """Get polygons from an image mask.
    Arguments
    ---------
    pred_arr : :class:`numpy.ndarray`
        A 2D array of integers. Multi-channel masks are not supported, and must
        be simplified before passing to this function. Can also pass an image
        file path here.
    channel_scaling : :class:`list`-like, optional
        If `pred_arr` is a 3D array, this argument defines how each channel
        will be combined to generate a binary output. channel_scaling should
        be a `list`-like of length equal to the number of channels in
        `pred_arr`. The following operation will be performed to convert the
        multi-channel prediction to a 2D output ::
            sum(pred_arr[channel]*channel_scaling[channel])
        If not provided, no scaling will be performend and channels will be
        summed.
    reference_im : str, optional
        The path to a reference geotiff to use for georeferencing the polygons
        in the mask. Required if saving to a GeoJSON (see the ``output_type``
        argument), otherwise only required if ``do_transform=True``.
    output_path : str, optional
        Path to save the output file to. If not provided, no file is saved.
    output_type : ``'csv'`` or ``'geojson'``, optional
        If ``output_path`` is provided, this argument defines what type of file
        will be generated - a CSV (``output_type='csv'``) or a geojson
        (``output_type='geojson'``).
    min_area : int, optional
        The minimum area of a polygon to retain. Filtering is done AFTER
        any coordinate transformation, and therefore will be in destination
        units.
    bg_threshold : int, optional
        The cutoff in ``mask_arr`` that denotes background (non-object).
        Defaults to ``0``.
    simplify : bool, optional
        If ``True``, will use the Douglas-Peucker algorithm to simplify edges,
        saving memory and processing time later. Defaults to ``False``.
    tolerance : float, optional
        The tolerance value to use for simplification with the Douglas-Peucker
        algorithm. Defaults to ``0.5``. Only has an effect if
        ``simplify=True``.
    Returns
    -------
    gdf : :class:`geopandas.GeoDataFrame`
        A GeoDataFrame of polygons.
    """

    mask_arr = preds_to_binary(pred_arr, channel_scaling, bg_threshold)

    if do_transform and reference_im is None:
        raise ValueError(
            'Coordinate transformation requires a reference image.')

    if do_transform:
        with rasterio.open(reference_im) as ref:
            transform = ref.transform
            crs = ref.crs
            ref.close()
    else:
        transform = Affine(1, 0, 0, 0, 1, 0)  # identity transform
        crs = rasterio.crs.CRS()

    mask = mask_arr > bg_threshold
    mask = mask.astype('uint8')

    polygon_generator = features.shapes(mask_arr,
                                        transform=transform,
                                        mask=mask)
    polygons = []
    values = []  # pixel values for the polygon in mask_arr
    for polygon, value in polygon_generator:
        p = shape(polygon).buffer(0.0)
        if p.area >= min_area:
            polygons.append(shape(polygon).buffer(0.0))
            values.append(value)

    polygon_gdf = gpd.GeoDataFrame({'geometry': polygons, 'value': values},
                                   crs=crs.to_wkt())
    if simplify:
        polygon_gdf['geometry'] = polygon_gdf['geometry'].apply(
            lambda x: x.simplify(tolerance=tolerance)
        )
    # save output files
    if output_path is not None:
        if output_type.lower() == 'geojson':
            if len(polygon_gdf) > 0:
                polygon_gdf.to_file(output_path, driver='GeoJSON')
            else:
                save_empty_geojson(output_path, polygon_gdf.crs.to_epsg())
        elif output_type.lower() == 'csv':

        polygon_gdf.to_csv(output_path, index=False)

    return polygon_gdf

```Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

## Buggy behavior and/or error message
Please describe the buggy behavior and/or paste output here.

Paste output here



## Expected behavior
An empty geojson file should be created at the output_path

## Screenshots
If applicable, add screenshots to help explain your problem.

## Environment information
- OS: Ubuntu 18.04.3
- `solaris` version:0.4.0
- python version:3.7.8
- version of any relevant dependencies (optional - we may ask for this information later if not provided)

## Additional context
Add any other context about the problem here.
AnshMittal1811 commented 3 years ago

I'm facing the same issue. Were you able to get a solution to your problem?

asdspal commented 3 years ago

No

FJSam commented 3 years ago

Hi,

To resolve this, I updated the else statement in the buggy code i.e. :

From

else:
      save_empty_geojson(output_path, polygon_gdf.crs.to_epsg())

to:

else:
       polygon_gdf.crs = "EPSG:4326"  # set arbitary crs
       save_empty_geojson(output_path, polygon_gdf.crs)

This worked for me, the issue was due to the crs object being None, so I just set an arbitrary value. Don't think it matters what you set it to, as it's an empty geojson anyways - bit crude but worked