"""
This module provides functions for mapping DS9 region files given in sky
coordinates to DS9 region files specified in image coordinates of multiple
images using the WCS information from the images.
:py:func:`~drizzlepac.mapreg.MapReg` provides an automated interface for
converting a region file to the image coordinate system (CS) of multiple
images (and their extensions) using WCS information from the image(s)
header(s). This conversion does not take into account pointing errors and,
therefore, an examination and adjustment (if required) of output region
files is highly recommended. This task is designed to simplify the creation
of the exclusions and/or inclusions region files used with
:py:func:`~drizzlepac.tweakreg.TweakReg` task for sources finding.
"""
import os
import sys
import glob
import logging
import warnings
from astropy.io import fits
from astropy.wcs import WCS
from astropy.utils.exceptions import AstropyDeprecationWarning
from regions import Regions
from drizzlepac import __version__
from drizzlepac.util import count_sci_extensions
__all__ = ["MapReg"]
package_level_logger = logging.getLogger("drizzlepac")
log = logging.getLogger(f"drizzlepac.mapreg")
[docs]
def MapReg(
input_reg,
images,
img_wcs_ext="sci",
outpath="./regions",
filter=None,
refimg="",
chip_reg="",
catfname="",
append=False,
ref_wcs_ext=None,
iteractive=None,
verbose=None,
):
"""Primary interface to map DS9 region files given in sky coordinates.
Parameters
----------
input_reg : string or list of strings (Default = '')
Input region files that need to be mapped to image CS using WCS
information from ``images`` (see below). Only region files saved in
sky CS are allowed in this release. Regions specified in image-like
coordinates (e.g., image, physical) will be ignored.
This parameter can be provided in any of several forms:
* filename of a single image
* comma-separated list of filenames
* ``@-file`` filelist containing list of desired input region filenames
The ``@-file`` filelist needs to be provided as an ASCII text file
containing a list of filenames for all input region files with one
filename on each line of the file.
images : string or list of strings (Default = ``*.fits``)
FITS images onto which the region files ``input_reg`` will be mapped.
These image files must contain WCS information in their headers to
convert ``input_reg`` from sky coordinates to correct image coordinates.
This parameter can be provided in any of several forms:
* filename of a single image
* filename of an association (ASN) table
* wild-card specification for files in a directory (using ``*``, ``?``
etc.)
* comma-separated list of filenames
* ``@-file`` filelist containing list of desired input filenames (and
optional inverse variance map filenames)
The ``@-file`` filelist needs to be provided as an ASCII text file
containing a list of filenames for all input images (to which
``input_reg`` regions should be mapped) with one filename on each
line of the file.
img_wcs_ext : string or list of integers (Default = ``sci``)
Extension selection for mapping. A string specifies an EXTNAME to use
for every matching extension in each image (for example ``"SCI"``). A
list of integers specifies explicit extension numbers to process. The
associated headers must contain valid WCS information for the
transformation.
*Note:* Previously a tuple of strings was accepted to specify multiple
EXTNAMEs; this usage is no longer supported.
outpath : string (Default = ``./regions``)
The directory to which the transformed regions should be saved. If the
directory does not exist it will be created.
filter : string {None, 'fast', 'precise', 'none'} (Default = None)
Specify whether or not to remove the regions in the transformed region
files that are outside the image array. With the 'fast' method only
intersection of the bounding boxes is being checked.
chip_reg : string or list of strings (Default = '', deprecated)
Input region files in image CS associated with each extension specified
by the ``img_wcs_ext`` parameter above. These regions will be added
directly (without any transformation) to the ``input_reg`` regions
mapped to each extension of the input ``images``. These regions must be
specified in image-like coordinates. Typically, these regions should
contain "exclude" regions to exclude parts of the image specific to the
detector **chip** (e.g., vignetted regions due to used filters, or
occulting finger in ACS/HRC images) from being used for source finding.
This parameter can be provided in one of the following forms:
* filename of a single image (if ``img_wcs_ext`` specifies a single
FITS extension)
* comma-separated list of filenames (if ``img_wcs_ext`` specifies more
than one extension) or ``None`` for extensions that do not need any
chip-specific regions to be excluded/included
* '' (empty string) or None if no chip-specific region files are
provided
The number of regions ideally must be equal to the number of
extensions specified by the ``img_wcs_ext`` parameter. If the number
of chip-specific regions is less than the number of ``img_wcs_ext``
extensions then ``chip_reg`` regions will be assigned to the first
extensions from ``img_wcs_ext`` (after internal expansion described in
help for the ``img_wcs_ext`` parameter above). If the number of
``chip_reg`` entries is larger than the number of ``img_wcs_ext``
extensions then extra regions will be ignored.
catfname : string (Default = ``exclusions_cat.txt``)
The file name of the output exclusions catalog file to be created
from the supplied image and region file names. This file can be
passed as an input to TweakReg task. Verify that the created file is
correct!
append : bool (Default = False)
Specify whether or not to append the transformed regions to the
existing region files with the same name.
verbose : bool (Default = False)
Specify whether or not to print extra messages during processing.
refimg : string (Default = '', deprecated)
**Reserved for future use.** Filename of the reference image. May
contain extension specifier: [extname,extver], [extname], or
[extnumber].
ref_wcs_ext : string (Default = 'sci', deprecated)
**Reserved for future use.** Extension name and/or version of FITS
extensions in the ``refimg`` that contain WCS information that will
be used to convert ``input_reg`` from image-like CS to sky CS. NOTE:
Only extension name is allowed when ``input_reg`` is a list of region
files that contain regions in image-like CS. In this case, the number
of regions in ``input_reg`` must agree with the number of extensions
with name specified by ``ref_wcs_ext`` present in the ``refimg`` FITS
image.
iteractive : bool (Default = False, deprecated)
**Reserved for future use.** This switch controls whether the
program stops and waits for the user to examine any generated region
files before continuing on to the next image.
Notes
-----
**NOTE 1:** This task takes a region file (or multiple files) that
describe(s) what regions of sky should be used for source finding
(*include* regions) and what regions should be avoided (*exclude*
regions) and transforms/maps this region file onto a number of image
files that need to be aligned.
The idea behind this task is automate the creation of region files that
then can be passed to *exclusions* parameter of the ``TweakReg`` task.
The same thing can be achieved manually using, for example, external
FITS viewers, e.g., SAO DS9. For example, based on some image
``refimg.fits`` we can select a few small regions of sky that contain
several good (bright, not saturated) point-like sources that could be
used for image alignment of other images (say ``img1.fits``,
``img2.fits``, etc.). We can save this region file in sky coordinates
(e.g., ``fk5``) under the name ``input_reg.reg``. We can then load a
specific extension of each of the images ``img1.fits``, ``img2.fits``,
etc. one by one into DS9 and then load onto those images the previously
created include/exclude region file ``input_reg.reg``. Now we can save the
regions using *image* coordinates. To do conversion from the sky
coordinates to image coordinates, DS9 will use the WCS info from the
image onto which the region file was loaded. The
:py:func:`~drizzlepac.mapreg.MapReg` task tries to automate this process.
**NOTE 2:** :py:func:`~drizzlepac.mapreg.MapReg` does not take into account
pointing errors and thus the produced region files can be somewhat
misaligned compared to their intended position around the sources
identified in the "reference" image. Therefore, it is highly
recommended that the produced region files be loaded into DS9 and their
position be adjusted manually to include the sources of interest (or to
avoid the regions that need to be avoided). If possible, the *include*
or *exclude* regions should be large enough as to allow for most pointing
errors.
Examples
--------
Let's say that based on some image ``refimg.fits`` we have produced a
"master" reference image (``master.reg``) that includes regions around
sources that we want to use for image alignment in task
:py:func:`~drizzlepac.tweakreg.TweakReg` and excludes regions that we
want to avoid being used for image alignment (e.g, diffraction spikes,
saturated quasars, stars, etc.). We save the file ``master.reg`` in sky
CS (e.g., ``fk5``).
Also, let's assume that we have a set of images ``img1.fits``,
``img2.fits``, etc. with four FITS extensions named 'SCI' and 'DQ'. For
some of the extensions, after analyzing the ``img*.fits`` images we have
identified parts of the chips that cannot be used for image alignment.
We create region files for those extensions and save the files in image
CS as, e.g., ``img1_chip_sci2.reg`` (in our example this will be the only
chip that needs "special" treatment).
Finally, let's say we want to "replicate" the "master" region file to all
SCI extensions of the ``img*.fits`` images.
To do this we run:
>>> mapreg(input_reg='master.reg', images='img*.fits', img_wcs_ext=[2,8])
This will produce region files in the ``./regions`` subdirectory for *each*
input image::
img1_sci2_twreg.reg, img1_sci8_twreg.reg,
::
img2_sci2_twreg.reg, img2_sci8_twreg.reg,
"""
# add logging
if verbose:
default_log_level = logging.DEBUG
formatter = logging.Formatter("[%(levelname)s:%(name)s] %(message)s")
else:
default_log_level = logging.INFO
formatter = logging.Formatter("[%(levelname)-8s] %(message)s")
file_handler = logging.FileHandler("mapreg.log", mode="w", encoding="utf-8")
stream_handler = logging.StreamHandler(sys.stdout)
file_handler.setLevel(default_log_level)
stream_handler.setLevel(default_log_level)
file_handler.setFormatter(formatter)
stream_handler.setFormatter(formatter)
package_level_logger.addHandler(file_handler)
package_level_logger.addHandler(stream_handler)
package_level_logger.setLevel(default_log_level)
# deprecation warnings for removed parameters
if refimg not in ("", None):
warnings.warn(
"The 'refimg' parameter is deprecated and ignored.",
AstropyDeprecationWarning,
)
if ref_wcs_ext not in ("sci", "SCI", None, ""):
warnings.warn(
"The 'ref_wcs_ext' parameter is deprecated and ignored.",
AstropyDeprecationWarning,
)
if chip_reg not in ("", None):
warnings.warn(
"The 'chip_reg' parameter is no longer supported; please"
"make individual MapReg calls for each image/extension pair.",
AstropyDeprecationWarning,
)
if iteractive:
warnings.warn(
"The 'iteractive' parameter is deprecated and ignored.",
AstropyDeprecationWarning,
)
# log input parameters
log.info("Starting MapReg...")
log.info("Mapping region files to image coordinate systems...")
log.info(f"Using drizzlepac version: {__version__}")
log.debug(f"Input region file(s): {input_reg}")
log.debug(f"Input image file(s): {images}")
log.debug(f"Image WCS extension(s): {img_wcs_ext}")
log.debug(f"Output directory: {outpath}")
log.debug(f"Output exclusions catalog file: {catfname}")
log.debug(f"Region filtering method: {filter if filter else 'None'}")
log.debug(f"Append mode: {append}")
log.debug(f"Verbose mode: {verbose}")
# validate and normalize inputs
region_paths = tuple(_validate_input_reg(input_reg))
image_list = tuple(_validate_images(images))
ext_spec = _validate_img_wcs_ext(img_wcs_ext)
normalized_catfname = _validate_catfname(catfname)
output_dir = _validate_outpath(outpath)
filter_mode = _validate_filter(filter)
append_flag = _validate_append(append)
# log updated parameters
log.debug(f"Expanded region paths: {region_paths}")
log.debug(f"Expanded image list: {image_list}")
log.debug(f"Normalized img_wcs_ext: {ext_spec}")
log.debug(f"Normalized outpath: {output_dir}")
log.debug(f"Normalized catfname: {normalized_catfname}")
log.debug(f"Normalized filter: {filter_mode}")
log.debug(f"Normalized append: {append_flag}")
# normalize extension specification
if isinstance(ext_spec, list):
normalized_ext_spec = ext_spec
elif isinstance(ext_spec, tuple):
normalized_ext_spec = tuple(ext_spec)
else:
normalized_ext_spec = ext_spec
# read all input region files and combine into a single Regions object
sky_regions = []
for region_path in region_paths:
sky_regions.extend(Regions.read(region_path))
all_sky_regions = Regions(sky_regions)
log.debug("Total number of sky regions: %d", len(all_sky_regions))
# perform the mapping
map_region_files(
sky_regions=all_sky_regions,
images=image_list,
img_wcs_ext=normalized_ext_spec,
catfname=normalized_catfname,
outpath=output_dir,
filter=filter_mode,
append=append_flag,
)
# remove logging handlers
package_level_logger.removeHandler(file_handler)
package_level_logger.removeHandler(stream_handler)
file_handler.close()
stream_handler.close()
def _validate_input_reg(input_reg):
"""Normalize input region specifications into concrete file paths."""
log.debug(f"Validating input_reg: {input_reg}")
seen_lists = set()
def _expand_string_spec(spec, context_dir=None):
value = spec.strip()
if not value:
raise ValueError("input_reg entries must not be empty")
if value.startswith("@-"):
list_name = value[2:].strip()
if not list_name:
raise ValueError("input_reg list specifications must name a file")
if context_dir and not os.path.isabs(list_name):
list_name = os.path.join(context_dir, list_name)
list_path = os.path.normpath(os.path.expanduser(list_name))
if not os.path.isfile(list_path):
raise IOError(f"The input region list '{list_path}' does not exist.")
key = os.path.abspath(list_path)
if key in seen_lists:
raise ValueError(f"input_reg list '{list_path}' references itself")
seen_lists.add(key)
entries = []
with open(list_path, "r", encoding="utf-8") as handle:
for raw_line in handle:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
entries.extend(
_expand_string_spec(line, os.path.dirname(list_path))
)
seen_lists.remove(key)
if not entries:
raise ValueError(
f"input_reg list '{list_path}' did not yield any filenames"
)
return entries
if value.startswith("@"):
raise ValueError("input_reg list specifications must use the '@-' prefix")
if "," in value:
parts = [part.strip() for part in value.split(",") if part.strip()]
if not parts:
raise ValueError(
"input_reg comma-separated specifications must list filenames"
)
expanded = []
for part in parts:
expanded.extend(_expand_string_spec(part, context_dir))
return expanded
normalized = os.path.expanduser(value)
if context_dir and not os.path.isabs(normalized):
normalized = os.path.join(context_dir, normalized)
normalized = os.path.normpath(normalized)
if any(char in normalized for char in "*?[]"):
matches = sorted(glob.glob(normalized))
if not matches:
raise IOError(
f"The input region wildcard '{value}' did not match any files."
)
return matches
return [normalized]
def _expand_entry(entry):
if isinstance(entry, str):
return _expand_string_spec(entry)
raise TypeError("input_reg entries must be strings")
if isinstance(input_reg, str):
candidates = _expand_string_spec(input_reg)
elif isinstance(input_reg, (list, tuple)):
candidates = []
for item in input_reg:
candidates.extend(_expand_entry(item))
else:
raise TypeError("input_reg must be a string or a sequence of strings")
if not candidates:
raise ValueError("input_reg must specify at least one region file")
normalized = []
for candidate in candidates:
path = candidate.strip()
if not path:
raise ValueError("input_reg entries must not be empty")
if not os.path.isfile(path):
raise IOError(f"The input region file '{path}' does not exist.")
normalized.append(path)
return normalized
def _validate_images(images):
"""Expand image specifications into a list of existing files."""
log.debug(f"Validating images: {images}")
seen_lists = set()
def _normalize_path(path, base_dir):
expanded = os.path.expanduser(path)
if base_dir and not os.path.isabs(expanded):
expanded = os.path.join(base_dir, expanded)
return os.path.abspath(expanded)
def _expand_string_spec(spec, base_dir=None):
value = spec.strip()
if not value:
raise ValueError("images entries must not be empty")
if value.startswith("@-"):
list_name = value[2:].strip()
if not list_name:
raise ValueError("images list specifications must name a file")
list_path = _normalize_path(list_name, base_dir)
if not os.path.isfile(list_path):
raise IOError(f"The image list '{list_path}' does not exist.")
key = os.path.abspath(list_path)
if key in seen_lists:
raise ValueError(f"images list '{list_path}' references itself")
seen_lists.add(key)
collected = []
with open(list_path, "r", encoding="utf-8") as handle:
for raw_line in handle:
line = raw_line.strip()
if not line or line.startswith("#"):
continue
collected.extend(
_expand_string_spec(line, os.path.dirname(list_path))
)
seen_lists.remove(key)
if not collected:
raise ValueError(
f"images list '{list_path}' did not yield any filenames"
)
return collected
if value.startswith("@"):
raise ValueError("images list specifications must use the '@-' prefix")
if "," in value:
parts = [part.strip() for part in value.split(",") if part.strip()]
if not parts:
raise ValueError(
"images comma-separated specifications must list filenames"
)
expanded = []
for part in parts:
expanded.extend(_expand_string_spec(part, base_dir))
return expanded
normalized = _normalize_path(value, base_dir)
if any(char in value for char in "*?[]"):
matches = sorted(glob.glob(normalized))
if not matches:
raise IOError(f"The image wildcard '{value}' did not match any files.")
return matches
if not os.path.isfile(normalized):
raise IOError(f"The image file '{normalized}' does not exist.")
return [normalized]
def _expand_entry(entry):
if isinstance(entry, str):
return _expand_string_spec(entry)
raise TypeError("images entries must be strings")
if isinstance(images, str):
normalized = _expand_string_spec(images)
elif isinstance(images, (list, tuple)):
normalized = []
for item in images:
normalized.extend(_expand_entry(item))
else:
raise TypeError("images must be a string or a sequence of strings")
if not normalized:
raise ValueError("images must contain at least one file")
return normalized
def _validate_img_wcs_ext(img_wcs_ext):
log.debug(f"Validating img_wcs_ext: {img_wcs_ext}")
if img_wcs_ext in (None, ""):
return None
def _normalize_single(ext):
if not isinstance(ext, int):
raise TypeError("img_wcs_ext sequences must contain integers")
if ext < 0:
raise ValueError("img_wcs_ext integers must be non-negative")
return ext
if isinstance(img_wcs_ext, str):
value = img_wcs_ext.strip()
if not value:
raise ValueError("img_wcs_ext string must not be empty")
if any(sep in value for sep in ";,"):
raise ValueError(
"img_wcs_ext string must name a single" + " EXTNAME without separators"
)
return value
if isinstance(img_wcs_ext, (list, tuple)):
if not img_wcs_ext:
raise ValueError("img_wcs_ext sequences must not be empty")
return [_normalize_single(ext) for ext in img_wcs_ext]
raise TypeError("img_wcs_ext must be a string or a sequence of integers")
def _validate_outpath(outpath):
log.debug(f"Validating outpath: {outpath}")
if outpath in (None, ""):
return os.path.curdir + os.path.sep
if not isinstance(outpath, str):
raise TypeError("outpath must be a string")
path = outpath.strip()
if not path:
path = os.path.curdir + os.path.sep
if not os.path.isdir(path):
os.makedirs(path, exist_ok=True)
log.debug(f"Created output directory: {path}")
return path
def _validate_catfname(catfname):
log.debug(f"Validating catfname: {catfname}")
if catfname in (None, ""):
return "exclusions_cat.txt"
if not isinstance(catfname, str):
raise TypeError("catfname must be a string")
path = catfname.strip()
if not path:
path = "exclusions_cat.txt"
return path
def _validate_filter(filter_option):
log.debug(f"Validating filter_option: {filter_option}")
if filter_option is None:
return None
if isinstance(filter_option, str):
value = filter_option.strip().lower()
if not value or value == "none" or value == "":
return None
if value == "fast":
return "fast"
if value == "precise":
return "precise"
raise ValueError("filter must be None, 'fast', 'precise', or 'none'")
raise TypeError("filter must be None or a string value")
def _validate_append(append_option):
log.debug(f"Validating append_option: {append_option}")
if isinstance(append_option, bool):
return append_option
raise TypeError("append must be a boolean value")
def map_region_files(
sky_regions,
images,
img_wcs_ext=None,
outpath="./regions",
catfname="exclusions_cat.txt",
filter=None,
append=False,
):
"""Map DS9 sky-coordinate regions onto image coordinates.
Parameters
----------
sky_regions : regions.Regions
Regions defined in sky coordinates to project into each output image.
Callers must supply a populated :class:`regions.Regions` object.
images : tuple[str, ...]
Tuple of FITS image paths providing WCS transformations. Each file
must exist on disk prior to invocation.
img_wcs_ext : str | tuple[int, ...] | None, optional
Normalized extension selector from :func:`_validate_img_wcs_ext`.
``"sci"`` expands to all science extensions, a tuple selects explicit
HDU indices, and ``None`` implies automatic SCI discovery.
outpath : str, optional
Destination directory for the generated region files. The directory
must already exist and should be provided by :func:`_validate_outpath`.
catfname : str, optional
Destination path for the exclusion catalog output. Supply the value
returned by :func:`_validate_catfname`.
filter : None | {'fast', 'precise'}, optional
Filtering mode produced by :func:`_validate_filter`. ``None`` disables
filtering.
append : bool, optional
Whether to append to existing outputs. Use the result from
:func:`_validate_append`.
"""
created_region_files = []
# iterate of images and extensions
for image_fname in images:
image_label = os.path.basename(image_fname)
current_exts = img_wcs_ext
# expand "sci" to all science extensions
if current_exts is None or (
isinstance(current_exts, str) and current_exts.lower() == "sci"
):
current_exts = count_sci_extensions(image_fname, return_ind=True)
log.debug(f"Expanded 'sci' to extensions: {current_exts}")
# make sure that we are working with a list of extensions and not a single value
if not isinstance(current_exts, list):
current_exts = [current_exts]
with fits.open(image_fname, memmap=False) as hdu:
for ext in current_exts:
all_pix_region = []
data_hdu = hdu[ext]
wcs = WCS(data_hdu.header, hdu)
shape = data_hdu.data.shape if data_hdu.data is not None else None
# Project each sky region into pixel coordinates and filter if needed
for region in sky_regions:
if filter and shape is not None:
# skip regions outside the image
if not _check_if_region_in_image(
region, wcs, shape, mode=filter
):
log.debug(f"Skipping region outside image: {region}")
continue
pix_reg = region.to_pixel(wcs)
all_pix_region.append(pix_reg)
full_pix_region = Regions(all_pix_region)
# do not create empty region files
if not full_pix_region:
log.warning(
f"No overlap between image '{image_label}' and "
f"extension {ext}; skipping file creation."
)
continue
# get output region file name
extsuffix = _ext2str_suffix(ext)
basefname, _ = os.path.splitext(os.path.basename(image_fname))
regfname = basefname + extsuffix + os.extsep + "reg"
fullregfname = os.path.join(outpath, regfname)
if append:
_save_region_file(
full_pix_region,
fullregfname,
exclusion_file=False,
append=append,
)
else:
full_pix_region.write(fullregfname, format="ds9", overwrite=True)
log.debug(f"Wrote region file: {fullregfname}")
created_region_files.append(os.path.basename(fullregfname))
# write exclusion catalog entry
_save_region_file(
full_pix_region, catfname, exclusion_file=True, append=append
)
log.debug(f"Wrote exclusion catalog entry: {catfname}")
if created_region_files:
log.info(f"Output region files: {', '.join(created_region_files)}")
else:
log.info("Output region files: none")
log.info("MapReg processing complete.")
def _check_if_region_in_image(region, wcs, shape, mode="fast"):
"""
Check whether a region is within an image.
Parameters
----------
region : regions.Region
SkyRegion or PixelRegion.
wcs : astropy.wcs.WCS
WCS for the image extension.
shape : tuple
Image shape as (ny, nx).
mode : {"fast", "precise"}
- "fast": bounding-box containment (approximate, very fast)
- "precise": pixel-mask containment (exact, slower)
Returns
-------
inside : bool
True if region is fully within the image.
"""
# Convert to pixel coordinates if needed
pix = region.to_pixel(wcs) if hasattr(region, "to_pixel") else region
ny, nx = shape
if mode == "fast":
bb = pix.bounding_box
return bb.ixmin >= 0 and bb.iymin >= 0 and bb.ixmax <= nx and bb.iymax <= ny
elif mode == "precise":
mask = pix.to_mask()
img = mask.to_image((ny, nx))
return img is not None and img.all()
else:
raise ValueError("mode must be 'fast' or 'precise'")
def _ext2str_suffix(ext):
if isinstance(ext, tuple):
return "_{}{}_twreg".format(ext[0], ext[1])
elif isinstance(ext, int):
return "_extn{}_twreg".format(ext)
else:
return "_{}_twreg".format(ext) # <- we should not get here...
def _save_region_file(region, filename, exclusion_file, append):
"""Export a ds9 region file with optional appending and exclusion markers.
When ``append`` is True, new shapes append to an existing file; otherwise
the file is overwritten. When ``exclusion_file`` is True, each shape
gains the '-' prefix required for tweakreg exclusion regions. For simple
saves prefer the ``Regions.write`` method.
Parameters
----------
region : regions.Regions
The regions to be converted.
filename : str
The output exclusion catalog filename.
exclusion_file : bool
Essentially, adds a '-' before each region shape type if True.
append : bool
Whether to append to the output file if it exists.
"""
allowable_ds9_region_types = ["polygon", "circle", "ellipse", "box"]
if len(region) == 0:
raise ValueError("No regions were provided for exclusion conversion")
header_lines = []
header_captured = False
converted_lines = []
for reg in region:
reg_serial = reg.serialize(format="ds9")
shape_lines = []
candidate_header = []
saw_shape = False
for raw_line in reg_serial.splitlines():
stripped = raw_line.strip()
if not stripped:
continue
if stripped.startswith("#"):
if not header_captured and not saw_shape:
candidate_header.append(stripped)
continue
token = stripped.lstrip("-")
lowered = token.lower()
is_shape = False
for region_type in allowable_ds9_region_types:
if lowered.startswith(region_type):
is_shape = True
break
if is_shape:
if exclusion_file and not stripped.startswith("-"):
stripped = "-" + stripped
shape_lines.append(stripped)
saw_shape = True
continue
if not header_captured and not saw_shape:
candidate_header.append(stripped)
if not shape_lines:
raise ValueError(f"No DS9 shapes found in region: {reg}")
converted_lines.extend(shape_lines)
if not header_captured and candidate_header:
header_lines = candidate_header
header_captured = True
if filename:
writing_mode = "a" if append else "w"
write_header = True
if append and os.path.exists(filename):
write_header = os.path.getsize(filename) == 0
with open(filename, writing_mode, encoding="utf-8") as handle:
if write_header and header_lines:
for header_line in header_lines:
handle.write(header_line + "\n")
for entry in converted_lines:
handle.write(entry + "\n")
log.debug(f"Wrote exclusion catalog file: {filename}")