docs: def source_properties(data, segment_img, error=None, mask=None,
                        background=None, filter_kernel=None, wcs=None,
                        labels=None):
      """
      Calculate photometry and morphological properties of sources defined
      by a labeled segmentation image.
  
      Parameters
      ----------
      data : array_like or `~astropy.units.Quantity`
          The 2D array from which to calculate the source photometry and
          properties.  ``data`` should be background-subtracted.
          Non-finite ``data`` values (e.g. NaN or inf) are automatically
          masked.
  
      segment_img : `SegmentationImage` or array_like (int)
          A 2D segmentation image, either as a `SegmentationImage` object
          or an `~numpy.ndarray`, with the same shape as ``data`` where
          sources are labeled by different positive integer values.  A
          value of zero is reserved for the background.
  
      error : array_like or `~astropy.units.Quantity`, optional
          The total error array corresponding to the input ``data`` array.
          ``error`` is assumed to include *all* sources of error,
          including the Poisson error of the sources (see
          `~photutils.utils.calc_total_error`) .  ``error`` must have the
          same shape as the input ``data``.  Non-finite ``error`` values
          (e.g. NaN or inf) are not automatically masked, unless they are
          at the same position of non-finite values in the input ``data``
          array.  Such pixels can be masked using the ``mask`` keyword.
          See the Notes section below for details on the error
          propagation.
  
      mask : array_like (bool), optional
          A boolean mask with the same shape as ``data`` where a `True`
          value indicates the corresponding element of ``data`` is masked.
          Masked data are excluded from all calculations.  Non-finite
          values (e.g. NaN or inf) in the input ``data`` are automatically
          masked.
  
      background : float, array_like, or `~astropy.units.Quantity`, optional
          The background level that was *previously* present in the input
          ``data``.  ``background`` may either be a scalar value or a 2D
          image with the same shape as the input ``data``.  Inputting the
          ``background`` merely allows for its properties to be measured
          within each source segment.  The input ``background`` does *not*
          get subtracted from the input ``data``, which should already be
          background-subtracted.  Non-finite ``background`` values (e.g.
          NaN or inf) are not automatically masked, unless they are at the
          same position of non-finite values in the input ``data`` array.
          Such pixels can be masked using the ``mask`` keyword.
  
      filter_kernel : array-like (2D) or `~astropy.convolution.Kernel2D`, optional
          The 2D array of the kernel used to filter the data prior to
          calculating the source centroid and morphological parameters.
          The kernel should be the same one used in defining the source
          segments, i.e. the detection image (e.g., see
          :func:`~photutils.detect_sources`).  If `None`, then the
          unfiltered ``data`` will be used instead.
  
      wcs : `~astropy.wcs.WCS`
          The WCS transformation to use.  If `None`, then any sky-based
          properties will be set to `None`.
  
      labels : int, array-like (1D, int)
          The segmentation labels for which to calculate source
          properties.  If `None` (default), then the properties will be
          calculated for all labeled sources.
  
      Returns
      -------
      output : `SourceCatalog` instance
          A `SourceCatalog` instance containing the properties of each
          source.
  
      Notes
      -----
      `SExtractor`_'s centroid and morphological parameters are always
      calculated from a filtered "detection" image, i.e. the image used to
      define the segmentation image.  The usual downside of the filtering
      is the sources will be made more circular than they actually are.
      If you wish to reproduce `SExtractor`_ centroid and morphology
      results, then input a filtered and background-subtracted "detection"
      image into the ``filtered_data`` keyword.  If ``filtered_data`` is
      `None`, then the unfiltered ``data`` will be used for the source
      centroid and morphological parameters.
  
      Negative data values (``filtered_data`` or ``data``) within the
      source segment are set to zero when calculating morphological
      properties based on image moments.  Negative values could occur, for
      example, if the segmentation image was defined from a different
      image (e.g., different bandpass) or if the background was
      oversubtracted. Note that `~photutils.SourceProperties.source_sum`
      always includes the contribution of negative ``data`` values.
  
      The input ``error`` is assumed to include *all* sources of error,
      including the Poisson error of the sources.
      `~photutils.SourceProperties.source_sum_err` is simply the
      quadrature sum of the pixel-wise total errors over the non-masked
      pixels within the source segment:
  
      .. math:: \\Delta F = \\sqrt{\\sum_{i \\in S}
                \\sigma_{\\mathrm{tot}, i}^2}
  
      where :math:`\\Delta F` is
      `~photutils.SourceProperties.source_sum_err`, :math:`S` are the
      non-masked pixels in the source segment, and
      :math:`\\sigma_{\\mathrm{tot}, i}` is the input ``error`` array.
  
      .. _SExtractor: http://www.astromatic.net/software/sextractor
  
      See Also
      --------
      SegmentationImage, SourceProperties, detect_sources
  
      Examples
      --------
      >>> import numpy as np
      >>> from photutils import SegmentationImage, source_properties
      >>> image = np.arange(16.).reshape(4, 4)
      >>> print(image)  # doctest: +SKIP
      [[ 0.  1.  2.  3.]
       [ 4.  5.  6.  7.]
       [ 8.  9. 10. 11.]
       [12. 13. 14. 15.]]
      >>> segm = SegmentationImage([[1, 1, 0, 0],
      ...                           [1, 0, 0, 2],
      ...                           [0, 0, 2, 2],
      ...                           [0, 2, 2, 0]])
      >>> props = source_properties(image, segm)
  
      Print some properties of the first object (labeled with ``1`` in the
      segmentation image):
  
      >>> props[0].id    # id corresponds to segment label number
      1
      >>> props[0].centroid    # doctest: +FLOAT_CMP
      <Quantity [0.8, 0.2] pix>
      >>> props[0].source_sum    # doctest: +FLOAT_CMP
      5.0
      >>> props[0].area    # doctest: +FLOAT_CMP
      <Quantity 3. pix2>
      >>> props[0].max_value    # doctest: +FLOAT_CMP
      4.0
  
      Print some properties of the second object (labeled with ``2`` in
      the segmentation image):
  
      >>> props[1].id    # id corresponds to segment label number
      2
      >>> props[1].centroid    # doctest: +FLOAT_CMP
      <Quantity [2.36363636, 2.09090909] pix>
      >>> props[1].perimeter    # doctest: +FLOAT_CMP
      <Quantity 5.41421356 pix>
      >>> props[1].orientation    # doctest: +FLOAT_CMP
      <Quantity -0.74175931 rad>
      """
  
      if not isinstance(segment_img, SegmentationImage):
          segment_img = SegmentationImage(segment_img)
  
      if segment_img.shape != data.shape:
          raise ValueError('segment_img and data must have the same shape.')
  
      # filter the data once, instead of repeating for each source
      if filter_kernel is not None:
          filtered_data = filter_data(data, filter_kernel, mode='constant',
                                      fill_value=0.0, check_normalization=True)
      else:
          filtered_data = None
  
      if labels is None:
          labels = segment_img.labels
      labels = np.atleast_1d(labels)
  
      sources_props = []
      for label in labels:
          if label not in segment_img.labels:
              warnings.warn('label {} is not in the segmentation image.'
                            .format(label), AstropyUserWarning)
              continue  # skip invalid labels
  
          sources_props.append(SourceProperties(
              data, segment_img, label, filtered_data=filtered_data,
              error=error, mask=mask, background=background, wcs=wcs))
  
      if len(sources_props) == 0:
          raise ValueError('No sources are defined.')
  
      return SourceCatalog(sources_props, wcs=wcs)