Exemplo n.º 1
0
def main():
    try:
        import sklearn
        import joblib

        if sklearn.__version__ < "0.20":
            gs.fatal(
                "Package python3-scikit-learn 0.20 or newer is not installed")

    except ImportError:
        gs.fatal("Package python3-scikit-learn 0.20 or newer is not installed")

    # parser options
    group = options["group"]
    output = options["output"]
    model_load = options["load_model"]
    probability = flags["p"]
    prob_only = flags["z"]
    chunksize = int(options["chunksize"])

    # remove @ from output in case overwriting result
    if "@" in output:
        output = output.split("@")[0]

    # check probabilities=True if prob_only=True
    if prob_only is True and probability is False:
        gs.fatal("Need to set probabilities=True if prob_only=True")

    # reload fitted model and training data
    estimator, y, class_labels = joblib.load(model_load)

    # define RasterStack
    stack = RasterStack(group=group)

    # perform raster prediction
    region = Region()
    row_incr = math.ceil(chunksize / region.cols)

    # do not read by increments if increment > n_rows
    if row_incr >= region.rows:
        row_incr = None

    # prediction
    if prob_only is False:
        gs.message("Predicting classification/regression raster...")
        stack.predict(
            estimator=estimator,
            output=output,
            height=row_incr,
            overwrite=gs.overwrite(),
        )

    if probability is True:
        gs.message("Predicting class probabilities...")
        stack.predict_proba(
            estimator=estimator,
            output=output,
            class_labels=np.unique(y),
            overwrite=gs.overwrite(),
            height=row_incr,
        )

    # assign categories for classification map
    if class_labels is not None and prob_only is False:
        rules = []

        for val, lab in class_labels.items():
            rules.append(",".join([str(val), lab]))

        rules = "\n".join(rules)
        rules_file = string_to_rules(rules)
        r.category(map=output, rules=rules_file, separator="comma")
Exemplo n.º 2
0
def main():

    elevation = options['elevation']
    slope = options['slope']
    flat_thres = float(options['flat_thres'])
    curv_thres = float(options['curv_thres'])
    filter_size = int(options['filter_size'])
    counting_size = int(options['counting_size'])
    nclasses = int(options['classes'])
    texture = options['texture']
    convexity = options['convexity']
    concavity = options['concavity']
    features = options['features']

    # remove mapset from output name in case of overwriting existing map
    texture = texture.split('@')[0]
    convexity = convexity.split('@')[0]
    concavity = concavity.split('@')[0]
    features = features.split('@')[0]

    # store current region settings
    global current_reg
    current_reg = parse_key_val(g.region(flags='pg', stdout_=PIPE).outputs.stdout)
    del current_reg['projection']
    del current_reg['zone']
    del current_reg['cells']

    # check for existing mask and backup if found
    global mask_test
    mask_test = gs.list_grouped(
        type='rast', pattern='MASK')[gs.gisenv()['MAPSET']]
    if mask_test:
        global original_mask
        original_mask = temp_map('tmp_original_mask')
        g.copy(raster=['MASK', original_mask])

    # error checking
    if flat_thres < 0:
        gs.fatal('Parameter thres cannot be negative')

    if filter_size % 2 == 0 or counting_size % 2 == 0:
        gs.fatal(
            'Filter or counting windows require an odd-numbered window size')

    if filter_size >= counting_size:
        gs.fatal(
            'Filter size needs to be smaller than the counting window size')
    
    if features != '' and slope == '':
        gs.fatal('Need to supply a slope raster in order to produce the terrain classification')
                
    # Terrain Surface Texture -------------------------------------------------
    # smooth the dem
    gs.message("Calculating terrain surface texture...")
    gs.message(
        "1. Smoothing input DEM with a {n}x{n} median filter...".format(
            n=filter_size))
    filtered_dem = temp_map('tmp_filtered_dem')
    gs.run_command("r.neighbors", input = elevation, method = "median",
                    size = filter_size, output = filtered_dem, flags='c',
                    quiet=True)

    # extract the pits and peaks based on the threshold
    pitpeaks = temp_map('tmp_pitpeaks')
    gs.message("2. Extracting pits and peaks with difference > thres...")
    r.mapcalc(expression='{x} = if ( abs({dem}-{median})>{thres}, 1, 0)'.format(
                x=pitpeaks, dem=elevation, thres=flat_thres, median=filtered_dem),
                quiet=True)

    # calculate density of pits and peaks
    gs.message("3. Using resampling filter to create terrain texture...")
    window_radius = (counting_size-1)/2
    y_radius = float(current_reg['ewres'])*window_radius
    x_radius = float(current_reg['nsres'])*window_radius
    resample = temp_map('tmp_density')
    r.resamp_filter(input=pitpeaks, output=resample, filter=['bartlett','gauss'],
                    radius=[x_radius,y_radius], quiet=True)

    # convert to percentage
    gs.message("4. Converting to percentage...")
    r.mask(raster=elevation, overwrite=True, quiet=True)
    r.mapcalc(expression='{x} = float({y} * 100)'.format(x=texture, y=resample),
               quiet=True)
    r.mask(flags='r', quiet=True)
    r.colors(map=texture, color='haxby', quiet=True)

    # Terrain convexity/concavity ---------------------------------------------
    # surface curvature using lacplacian filter
    gs.message("Calculating terrain convexity and concavity...")
    gs.message("1. Calculating terrain curvature using laplacian filter...")
    
    # grow the map to remove border effects and run laplacian filter
    dem_grown = temp_map('tmp_elevation_grown')
    laplacian = temp_map('tmp_laplacian')
    g.region(n=float(current_reg['n']) + (float(current_reg['nsres']) * filter_size),
             s=float(current_reg['s']) - (float(current_reg['nsres']) * filter_size),
             w=float(current_reg['w']) - (float(current_reg['ewres']) * filter_size),
             e=float(current_reg['e']) + (float(current_reg['ewres']) * filter_size))

    r.grow(input=elevation, output=dem_grown, radius=filter_size, quiet=True)
    r.mfilter(
        input=dem_grown, output=laplacian,
        filter=string_to_rules(laplacian_matrix(filter_size)), quiet=True)

    # extract convex and concave pixels
    gs.message("2. Extracting convexities and concavities...")
    convexities = temp_map('tmp_convexities')
    concavities = temp_map('tmp_concavities')

    r.mapcalc(
        expression='{x} = if({laplacian}>{thres}, 1, 0)'\
        .format(x=convexities, laplacian=laplacian, thres=curv_thres),
        quiet=True)
    r.mapcalc(
        expression='{x} = if({laplacian}<-{thres}, 1, 0)'\
        .format(x=concavities, laplacian=laplacian, thres=curv_thres),
        quiet=True)

    # calculate density of convexities and concavities
    gs.message("3. Using resampling filter to create surface convexity/concavity...")
    resample_convex = temp_map('tmp_convex')
    resample_concav = temp_map('tmp_concav')
    r.resamp_filter(input=convexities, output=resample_convex,
                    filter=['bartlett','gauss'], radius=[x_radius,y_radius],
                    quiet=True)
    r.resamp_filter(input=concavities, output=resample_concav,
                    filter=['bartlett','gauss'], radius=[x_radius,y_radius],
                    quiet=True)

    # convert to percentages
    gs.message("4. Converting to percentages...")
    g.region(**current_reg)
    r.mask(raster=elevation, overwrite=True, quiet=True)
    r.mapcalc(expression='{x} = float({y} * 100)'.format(x=convexity, y=resample_convex),
               quiet=True)
    r.mapcalc(expression='{x} = float({y} * 100)'.format(x=concavity, y=resample_concav),
               quiet=True)
    r.mask(flags='r', quiet=True)

    # set colors
    r.colors_stddev(map=convexity, quiet=True)
    r.colors_stddev(map=concavity, quiet=True)

    # Terrain classification Flowchart-----------------------------------------
    if features != '':
        gs.message("Performing terrain surface classification...")
        # level 1 produces classes 1 thru 8
        # level 2 produces classes 5 thru 12
        # level 3 produces classes 9 thru 16
        if nclasses == 8: levels = 1
        if nclasses == 12: levels = 2
        if nclasses == 16: levels = 3

        classif = []
        for level in range(levels):
            # mask previous classes x:x+4
            if level != 0:
                min_cla = (4*(level+1))-4
                clf_msk = temp_map('tmp_clf_mask')
                rules = '1:{0}:1'.format(min_cla)
                r.recode(
                    input=classif[level-1], output=clf_msk,
                    rules=string_to_rules(rules), overwrite=True)
                r.mask(raster=clf_msk, flags='i', quiet=True, overwrite=True)

            # image statistics
            smean = r.univar(
                map=slope, flags='g', stdout_=PIPE).outputs.stdout.split(os.linesep)
            smean = [i for i in smean if i.startswith('mean=') is True][0].split('=')[1]

            cmean = r.univar(
                map=convexity, flags='g', stdout_=PIPE).outputs.stdout.split(os.linesep)
            cmean = [i for i in cmean if i.startswith('mean=') is True][0].split('=')[1]

            tmean = r.univar(
                map=texture, flags='g', stdout_=PIPE).outputs.stdout.split(os.linesep)
            tmean = [i for i in tmean if i.startswith('mean=') is True][0].split('=')[1]
            classif.append(temp_map('tmp_classes'))
            
            if level != 0:
                r.mask(flags='r', quiet=True)

            classification(level+1, slope, smean, texture, tmean,
                            convexity, cmean, classif[level])

        # combine decision trees
        merged = []
        for level in range(0, levels):
            if level > 0:
                min_cla = (4*(level+1))-4
                merged.append(temp_map('tmp_merged'))
                r.mapcalc(
                    expression='{x} = if({a}>{min}, {b}, {a})'.format(
                        x=merged[level], min=min_cla, a=merged[level-1],  b=classif[level]))
            else:
                merged.append(classif[level])
        g.rename(raster=[merged[-1], features], quiet=True)
        del TMP_RAST[-1]

    # Write metadata ----------------------------------------------------------
    history = 'r.terrain.texture '
    for key,val in options.iteritems():
        history += key + '=' + str(val) + ' '

    r.support(map=texture,
              title=texture,
              description='generated by r.terrain.texture',
              history=history)
    r.support(map=convexity,
              title=convexity,
              description='generated by r.terrain.texture',
              history=history)
    r.support(map=concavity,
              title=concavity,
              description='generated by r.terrain.texture',
              history=history)

    if features != '':
        r.support(map=features,
                  title=features,
                  description='generated by r.terrain.texture',
                  history=history)
        
        # write color and category rules to tempfiles                
        r.category(
            map=features,
            rules=string_to_rules(categories(nclasses)),
            separator='pipe')
        r.colors(
            map=features, rules=string_to_rules(colors(nclasses)), quiet=True)

    return 0
Exemplo n.º 3
0
def compute_supply(
    base,
    recreation_spectrum,
    highest_spectrum,
    base_reclassification_rules,
    reclassified_base,
    reclassified_base_title,
    flow,
    flow_map_name,
    aggregation,
    ns_resolution,
    ew_resolution,
    print_only=False,
    flow_column_name=None,
    vector=None,
    supply_filename=None,
    use_filename=None,
):
    """
     Algorithmic description of the "Contribution of Ecosysten Types"

     # FIXME
     '''
     1   B ← {0, .., m-1}     :  Set of aggregational boundaries
     2   T ← {0, .., n-1}     :  Set of land cover types
     3   WE ← 0               :  Set of weighted extents
     4   R ← 0                :  Set of fractions
     5   F ← 0
     6   MASK ← HQR           : High Quality Recreation
     7   foreach {b} ⊆ B do   : for each aggregational boundary 'b'
     8      RB ← 0
     9      foreach {t} ⊆ T do  : for each Land Type
     10         WEt ← Et * Wt   : Weighted Extent = Extent(t) * Weight(t)
     11         WE ← WE⋃{WEt}   : Add to set of Weighted Extents
     12     S ← ∑t∈WEt
     13     foreach t ← T do
     14        Rt ← WEt / ∑WE
     15        R ← R⋃{Rt}
     16     RB ← RB⋃{R}
     '''
     # FIXME

    Parameters
    ----------
    recreation_spectrum:
        Map scoring access to and quality of recreation

    highest_spectrum :
        Expected is a map of areas with highest recreational value (category 9
        as per the report ... )

    base :
        Base land types map for final zonal statistics. Specifically to
        ESTIMAP's recrceation mapping algorithm

    base_reclassification_rules :
        Reclassification rules for the input base map

    reclassified_base :
        Name for the reclassified base cover map

    reclassified_base_title :
        Title for the reclassified base map

    ecosystem_types :

    flow :
        Map of visits, derived from the mobility function, depicting the
        number of people living inside zones 0, 1, 2, 3. Used as a cover map
        for zonal statistics.

    flow_map_name :
        A name for the 'flow' map. This is required when the 'flow' input
        option is not defined by the user, yet some of the requested outputs
        required first the production of the 'flow' map. An example is the
        request for a supply table without requesting the 'flow' map itself.

    aggregation :

    ns_resolution :

    ew_resolution :

    statistics_filename :

    supply_filename :
        Name for CSV output file of the supply table

    use_filename :
        Name for CSV output file of the use table

    flow_column_name :
        Name for column to populate with 'flow' values

    vector :
        If 'vector' is given, a vector map of the 'flow' along with appropriate
        attributes will be produced.

    ? :
        Land cover class percentages in ROS9 (this is: relative percentage)

    output :
        Supply table (distribution of flow for each land cover class)

    Returns
    -------
    This function produces a map to base the production of a supply table in
    form of CSV.

    Examples
    --------
    """
    # Inputs
    flow_in_base = flow + "_" + base
    base_scores = base + ".scores"

    # Define lists and dictionaries to hold intermediate data
    statistics_dictionary = {}
    weighted_extents = {}
    flows = []

    # MASK areas of high quality recreation
    r.mask(raster=highest_spectrum, overwrite=True, quiet=True)

    # Reclassify land cover map to MAES ecosystem types
    r.reclass(
        input=base,
        rules=base_reclassification_rules,
        output=reclassified_base,
        quiet=True,
    )
    # add to "remove_at_exit" after the reclassified maps!

    # Discard areas out of MASK
    copy_equation = EQUATION.format(result=reclassified_base,
                                    expression=reclassified_base)
    r.mapcalc(copy_equation, overwrite=True)

    # Count flow within each land cover category
    r.stats_zonal(
        base=base,
        flags="r",
        cover=flow_map_name,
        method="sum",
        output=flow_in_base,
        overwrite=True,
        quiet=True,
    )

    # Set colors for "flow" map
    r.colors(map=flow_in_base, color=MOBILITY_COLORS, quiet=True)

    # Parse aggregation raster categories and labels
    categories = grass.parse_command("r.category",
                                     map=aggregation,
                                     delimiter="\t")

    for category in categories:

        # Intermediate names

        cells = highest_spectrum + ".cells" + "." + category
        remove_map_at_exit(cells)

        extent = highest_spectrum + ".extent" + "." + category
        remove_map_at_exit(extent)

        weighted = highest_spectrum + ".weighted" + "." + category
        remove_map_at_exit(weighted)

        fractions = base + ".fractions" + "." + category
        remove_map_at_exit(fractions)

        flow_category = "_flow_" + category
        flow = base + flow_category
        remove_map_at_exit(flow)

        flow_in_reclassified_base = reclassified_base + "_flow"
        flow_in_category = reclassified_base + flow_category
        flows.append(flow_in_category)  # add to list for patching
        remove_map_at_exit(flow_in_category)

        # Output names

        msg = "Processing aggregation raster category: {r}"
        msg = msg.format(r=category)
        grass.debug(_(msg))
        # g.message(_(msg))

        # First, set region to extent of the aggregation map
        # and resolution to the one of the population map
        # Note the `-a` flag to g.region: ?
        # To safely modify the region: grass.use_temp_region()  # FIXME
        g.region(
            raster=aggregation,
            nsres=ns_resolution,
            ewres=ew_resolution,
            flags="a",
            quiet=True,
        )

        msg = "|! Computational resolution matched to {raster}"
        msg = msg.format(raster=aggregation)
        grass.debug(_(msg))

        # Build MASK for current category & high quality recreation areas
        msg = "Setting category '{c}' of '{a}' as a MASK"
        grass.verbose(_(msg.format(c=category, a=aggregation)))

        masking = "if( {spectrum} == {highest_quality_category} && "
        masking += "{aggregation} == {category}, "
        masking += "1, null() )"
        masking = masking.format(
            spectrum=recreation_spectrum,
            highest_quality_category=HIGHEST_RECREATION_CATEGORY,
            aggregation=aggregation,
            category=category,
        )
        masking_equation = EQUATION.format(result="MASK", expression=masking)
        grass.mapcalc(masking_equation, overwrite=True)

        # zoom to MASK
        g.region(zoom="MASK",
                 nsres=ns_resolution,
                 ewres=ew_resolution,
                 quiet=True)

        # Count number of cells within each land category
        r.stats_zonal(
            flags="r",
            base=base,
            cover=highest_spectrum,
            method="count",
            output=cells,
            overwrite=True,
            quiet=True,
        )
        cells_categories = grass.parse_command("r.category",
                                               map=cells,
                                               delimiter="\t")
        grass.debug(_("Cells: {c}".format(c=cells_categories)))

        # Build cell category and label rules for `r.category`
        cells_rules = "\n".join([
            "{0}:{1}".format(key, value)
            for key, value in cells_categories.items()
        ])

        # Discard areas out of MASK
        copy_equation = EQUATION.format(result=cells, expression=cells)
        r.mapcalc(copy_equation, overwrite=True)

        # Reassign cell category labels
        r.category(map=cells, rules="-", stdin=cells_rules, separator=":")

        # Compute extent of each land category
        extent_expression = "@{cells} * area()"
        extent_expression = extent_expression.format(cells=cells)
        extent_equation = EQUATION.format(result=extent,
                                          expression=extent_expression)
        r.mapcalc(extent_equation, overwrite=True)

        # Write extent figures as labels
        r.stats_zonal(
            flags="r",
            base=base,
            cover=extent,
            method="average",
            output=extent,
            overwrite=True,
            verbose=False,
            quiet=True,
        )

        # Write land suitability scores as an ASCII file
        temporary_reclassified_base_map = temporary_filename(
            filename=reclassified_base)
        suitability_scores_as_labels = string_to_file(
            SUITABILITY_SCORES_LABELS,
            filename=temporary_reclassified_base_map)
        remove_files_at_exit(suitability_scores_as_labels)

        # Write scores as raster category labels
        r.reclass(
            input=base,
            output=base_scores,
            rules=suitability_scores_as_labels,
            overwrite=True,
            quiet=True,
            verbose=False,
        )
        remove_map_at_exit(base_scores)

        # Compute weighted extents
        weighted_expression = "@{extent} * float(@{scores})"
        weighted_expression = weighted_expression.format(extent=extent,
                                                         scores=base_scores)
        weighted_equation = EQUATION.format(result=weighted,
                                            expression=weighted_expression)
        r.mapcalc(weighted_equation, overwrite=True)

        # Write weighted extent figures as labels
        r.stats_zonal(
            flags="r",
            base=base,
            cover=weighted,
            method="average",
            output=weighted,
            overwrite=True,
            verbose=False,
            quiet=True,
        )

        # Get weighted extents in a dictionary
        weighted_extents = grass.parse_command("r.category",
                                               map=weighted,
                                               delimiter="\t")

        # Compute the sum of all weighted extents and add to dictionary
        category_sum = sum([
            float(x) if not math.isnan(float(x)) else 0
            for x in weighted_extents.values()
        ])
        weighted_extents["sum"] = category_sum

        # Create a map to hold fractions of each weighted extent to the sum
        # See also:
        # https://grasswiki.osgeo.org/wiki/LANDSAT#Hint:_Minimal_disk_space_copies
        r.reclass(
            input=base,
            output=fractions,
            rules="-",
            stdin="*=*",
            verbose=False,
            quiet=True,
        )

        # Compute weighted fractions of land types
        fraction_category_label = {
            key: float(value) / weighted_extents["sum"]
            for (key, value) in weighted_extents.iteritems()
            if key is not "sum"
        }

        # Build fraction category and label rules for `r.category`
        fraction_rules = "\n".join([
            "{0}:{1}".format(key, value)
            for key, value in fraction_category_label.items()
        ])

        # Set rules
        r.category(map=fractions,
                   rules="-",
                   stdin=fraction_rules,
                   separator=":")

        # Assert that sum of fractions is ~1
        fraction_categories = grass.parse_command("r.category",
                                                  map=fractions,
                                                  delimiter="\t")

        fractions_sum = sum([
            float(x) if not math.isnan(float(x)) else 0
            for x in fraction_categories.values()
        ])
        msg = "Fractions: {f}".format(f=fraction_categories)
        grass.debug(_(msg))

        # g.message(_("Sum: {:.17g}".format(fractions_sum)))
        assert abs(fractions_sum - 1) < 1.0e-6, "Sum of fractions is != 1"

        # Compute flow
        flow_expression = "@{fractions} * @{flow}"
        flow_expression = flow_expression.format(fractions=fractions,
                                                 flow=flow_in_base)
        flow_equation = EQUATION.format(result=flow,
                                        expression=flow_expression)
        r.mapcalc(flow_equation, overwrite=True)

        # Write flow figures as raster category labels
        r.stats_zonal(
            base=reclassified_base,
            flags="r",
            cover=flow,
            method="sum",
            output=flow_in_category,
            overwrite=True,
            verbose=False,
            quiet=True,
        )

        # Parse flow categories and labels
        flow_categories = grass.parse_command("r.category",
                                              map=flow_in_category,
                                              delimiter="\t")
        grass.debug(_("Flow: {c}".format(c=flow_categories)))

        # Build flow category and label rules for `r.category`
        flow_rules = "\n".join([
            "{0}:{1}".format(key, value)
            for key, value in flow_categories.items()
        ])

        # Discard areas out of MASK

        # Check here again!
        # Output patch of all flow maps?

        copy_equation = EQUATION.format(result=flow_in_category,
                                        expression=flow_in_category)
        r.mapcalc(copy_equation, overwrite=True)

        # Reassign cell category labels
        r.category(map=flow_in_category,
                   rules="-",
                   stdin=flow_rules,
                   separator=":")

        # Update title
        reclassified_base_title += " " + category
        r.support(flow_in_category, title=reclassified_base_title)

        # debugging
        # r.report(
        #     flags='hn',
        #     map=(flow_in_category),
        #     units=('k','c','p'),
        # )

        if print_only:
            r.stats(
                input=(flow_in_category),
                output="-",
                flags="nacpl",
                separator=COMMA,
                quiet=True,
            )

        if not print_only:

            if flow_column_name:
                flow_column_prefix = flow_column_name + category
            else:
                flow_column_name = "flow"
                flow_column_prefix = flow_column_name + category

            # Produce vector map(s)
            if vector:

                # The following is wrong

                # update_vector(vector=vector,
                #         raster=flow_in_category,
                #         methods=METHODS,
                #         column_prefix=flow_column_prefix)

                # What can be done?

                # Maybe update columns of an existing map from the columns of
                # the following vectorised raster map(s)
                # ?

                raster_to_vector(raster=flow_in_category,
                                 vector=flow_in_category,
                                 type="area")

            # get statistics
            dictionary = get_raster_statistics(
                map_one=aggregation,  # reclassified_base
                map_two=flow_in_category,
                separator="|",
                flags="nlcap",
            )

            # merge 'dictionary' with global 'statistics_dictionary'
            statistics_dictionary = merge_two_dictionaries(
                statistics_dictionary, dictionary)

        # It is important to remove the MASK!
        r.mask(flags="r", quiet=True)

    # FIXME

    # Add "reclassified_base" map to "remove_at_exit" here, so as to be after
    # all reclassified maps that derive from it

    # remove the map 'reclassified_base'
    # g.remove(flags='f', type='raster', name=reclassified_base, quiet=True)
    # remove_map_at_exit(reclassified_base)

    if not print_only:
        r.patch(flags="",
                input=flows,
                output=flow_in_reclassified_base,
                quiet=True)

        if vector:
            # Patch all flow vector maps in one
            v.patch(
                flags="e",
                input=flows,
                output=flow_in_reclassified_base,
                overwrite=True,
                quiet=True,
            )

        # export to csv
        if supply_filename:
            supply_filename += CSV_EXTENSION
            nested_dictionary_to_csv(supply_filename, statistics_dictionary)

        if use_filename:
            use_filename += CSV_EXTENSION
            uses = compile_use_table(statistics_dictionary)
            dictionary_to_csv(use_filename, uses)

    # Maybe return list of flow maps?  Requires unique flow map names
    return flows
Exemplo n.º 4
0
def export_map(input_name, title, categories, colors, output_name, timestamp):
    """
    Export a raster map by renaming the (temporary) raster map name
    'input_name' to the requested output raster map name 'output_name'.
    This function is (mainly) used to export either of the intermediate
    recreation 'potential' or 'opportunity' maps.

    Parameters
    ----------
    raster :
        Input raster map name

    title :
        Title for the output raster map

    categories :
        Categories and labels for the output raster map

    colors :
        Colors for the output raster map

    output_name :
        Output raster map name

    Returns
    -------
    output_name :
        This function will return the requested 'output_name'

    Examples
    --------
    ..
    """
    finding = grass.find_file(name=input_name, element="cell")
    if not finding["file"]:
        grass.fatal("Raster map {name} not found".format(
            name=input_name))  # Maybe use 'finding'?

    # inform
    msg = "* Outputting '{raster}' map\n"
    msg = msg.format(raster=output_name)
    grass.verbose(_(msg))

    # get categories and labels
    temporary_raster_categories_map = temporary_filename("categories_of_" +
                                                         input_name)
    raster_category_labels = string_to_file(
        string=categories, filename=temporary_raster_categories_map)

    # add ascii file to removal list
    remove_files_at_exit(raster_category_labels)

    # apply categories and description
    r.category(map=input_name, rules=raster_category_labels, separator=":")

    # update meta and colors
    update_meta(input_name, title, timestamp)
    r.colors(map=input_name, rules="-", stdin=colors, quiet=True)

    # rename to requested output name
    g.rename(raster=(input_name, output_name), quiet=True)

    return output_name