Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions carta/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ class SmoothingMode(IntEnum):
GAUSSIAN_BLUR = 2


class Auto(str, Enum):
"""Special value for parameters to be calculated automatically."""
AUTO = "Auto"


class ContourDashMode(StrEnum):
"""Contour dash modes."""
NONE = "None"
Expand Down
105 changes: 27 additions & 78 deletions carta/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
Image objects should not be instantiated directly, and should only be created through methods on the :obj:`carta.session.Session` object.
"""
from .constants import Colormap, Scaling, SmoothingMode, ContourDashMode, Polarization, CoordinateSystem, SpatialAxis
from .util import Macro, cached, PixelValue, AngularSize, WorldCoordinate
from .validation import validate, Number, Color, Constant, Boolean, NoneOr, IterableOf, Evaluate, Attr, Attrs, OneOf, Size, Coordinate
from .util import Macro, cached
from .units import PixelValue, AngularSize, WorldCoordinate
from .validation import validate, Number, Color, Constant, Boolean, NoneOr, IterableOf, Evaluate, Attr, Attrs, OneOf, Size, Coordinate, all_optional
from .metadata import parse_header


class Image:
Expand All @@ -18,23 +20,18 @@ class Image:
The session object associated with this image.
image_id : integer
The ID identifying this image within the session. This is a unique number which is not reused, not the index of the image within the list of currently open images.
file_name : string
The file name of the image. This is not a full path.

Attributes
----------
session : :obj:`carta.session.Session`
The session object associated with this image.
image_id : integer
The ID identifying this image within the session.
file_name : string
The file name of the image.
"""

def __init__(self, session, image_id, file_name):
def __init__(self, session, image_id):
self.session = session
self.image_id = image_id
self.file_name = file_name

self._base_path = f"frameMap[{image_id}]"
self._frame = Macro("", self._base_path)
Expand Down Expand Up @@ -78,7 +75,7 @@ def new(cls, session, directory, file_name, hdu, append, image_arithmetic, make_
params.append(update_directory)

image_id = session.call_action(command, *params, return_path="frameInfo.fileId")
return cls(session, image_id, file_name)
return cls(session, image_id)

@classmethod
def from_list(cls, session, image_list):
Expand All @@ -98,7 +95,7 @@ def from_list(cls, session, image_list):
list of :obj:`carta.image.Image`
A list of new image objects.
"""
return [cls(session, f["value"], f["label"].split(":")[1].strip()) for f in image_list]
return [cls(session, f["value"]) for f in image_list]

def __repr__(self):
return f"{self.session.session_id}:{self.image_id}:{self.file_name}"
Expand Down Expand Up @@ -163,6 +160,17 @@ def macro(self, target, variable):

# METADATA

@property
@cached
def file_name(self):
"""The name of the image.
Returns
-------
string
The image name.
"""
return self.get_value("frameInfo.fileInfo.name")

@property
@cached
def directory(self):
Expand All @@ -178,74 +186,15 @@ def directory(self):
@property
@cached
def header(self):
"""The header of the image.

Entries with T or F string values are automatically converted to booleans.

``HISTORY``, ``COMMENT`` and blank keyword entries are aggregated into single entries with list values and with ``'HISTORY'``, ``'COMMENT'`` and ``''`` as keys, respectively. An entry in the history list which begins with ``'>'`` will be concatenated with the previous entry.

Adjacent ``COMMENT`` entries are not concatenated automatically.

Any other header entries with no values are given values of ``None``.
"""The header of the image, parsed from the raw frontend data (see :obj:`carta.metadata.parse_header`).

Returns
-------
dict of string to string, integer, float, boolean, ``None`` or list of strings
The header of the image, with field names as keys.
"""
raw_header = self.get_value("frameInfo.fileInfoExtended.headerEntries")

header = {}

history = []
comment = []
blank = []

def header_value(raw_entry):
try:
return raw_entry["numericValue"]
except KeyError:
try:
value = raw_entry["value"]
if value == 'T':
return True
if value == 'F':
return False
return value
except KeyError:
return None

for i, raw_entry in enumerate(raw_header):
name = raw_entry["name"]

if name.startswith("HISTORY "):
line = name[8:]
if line.startswith(">") and history:
history[-1] = history[-1] + line[1:]
else:
history.append(line)
continue

if name.startswith("COMMENT "):
comment.append(name[8:])
continue

if name.startswith(" " * 8):
blank.append(name[8:])
continue

header[name] = header_value(raw_entry)

if history:
header["HISTORY"] = history

if comment:
header["COMMENT"] = comment

if blank:
header[""] = blank

return header
return parse_header(raw_header)

@property
@cached
Expand Down Expand Up @@ -607,7 +556,7 @@ def hide_raster(self):

# CONTOURS

@validate(NoneOr(IterableOf(Number())), NoneOr(Constant(SmoothingMode)), NoneOr(Number()))
@validate(*all_optional(IterableOf(Number()), Constant(SmoothingMode), Number()))
def configure_contours(self, levels=None, smoothing_mode=None, smoothing_factor=None):
"""Configure contours.

Expand All @@ -628,7 +577,7 @@ def configure_contours(self, levels=None, smoothing_mode=None, smoothing_factor=
smoothing_factor = self.macro("contourConfig", "smoothingFactor")
self.call_action("contourConfig.setContourConfiguration", levels, smoothing_mode, smoothing_factor)

@validate(NoneOr(Constant(ContourDashMode)), NoneOr(Number()))
@validate(*all_optional(Constant(ContourDashMode), Number()))
def set_contour_dash(self, dash_mode=None, thickness=None):
"""Set the contour dash style.

Expand All @@ -653,7 +602,7 @@ def set_contour_color(self, color):
Parameters
----------
color : {0}
The color.
The color. The default is green.
"""
self.call_action("contourConfig.setColor", color)
self.call_action("contourConfig.setColormapEnabled", False)
Expand All @@ -669,7 +618,7 @@ def set_contour_colormap(self, colormap, bias=None, contrast=None):
colormap : {0}
The colormap.
bias : {1}
The colormap bias.
The colormap bias. The default is :obj:`carta.constants.Colormap.VIRIDIS`.
contrast : {2}
The colormap contrast.
"""
Expand All @@ -684,7 +633,7 @@ def apply_contours(self):
"""Apply the contour configuration."""
self.call_action("applyContours")

@validate(NoneOr(IterableOf(Number())), NoneOr(Constant(SmoothingMode)), NoneOr(Number()), NoneOr(Constant(ContourDashMode)), NoneOr(Number()), NoneOr(Color()), NoneOr(Constant(Colormap)), NoneOr(Number()), NoneOr(Number()))
@validate(*all_optional(*configure_contours.VARGS, *set_contour_dash.VARGS, *set_contour_color.VARGS, *set_contour_colormap.VARGS))
def plot_contours(self, levels=None, smoothing_mode=None, smoothing_factor=None, dash_mode=None, thickness=None, color=None, colormap=None, bias=None, contrast=None):
"""Configure contour levels, scaling, dash, and colour or colourmap; and apply contours; in a single step.

Expand All @@ -703,9 +652,9 @@ def plot_contours(self, levels=None, smoothing_mode=None, smoothing_factor=None,
thickness : {4}
The dash thickness.
color : {5}
The color.
The color. The default is green.
colormap : {6}
The colormap.
The colormap. The default is :obj:`carta.constants.Colormap.VIRIDIS`.
bias : {7}
The colormap bias.
contrast : {8}
Expand Down
69 changes: 69 additions & 0 deletions carta/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""This module provides a collection of helper objects for storing and accessing file metadata."""


def parse_header(raw_header):
"""Parse raw image header entries from the frontend into a more user-friendly format.
Entries with T or F string values are automatically converted to booleans.
``HISTORY``, ``COMMENT`` and blank keyword entries are aggregated into single entries with list values and with ``'HISTORY'``, ``'COMMENT'`` and ``''`` as keys, respectively. An entry in the history list which begins with ``'>'`` will be concatenated with the previous entry.
Adjacent ``COMMENT`` entries are not concatenated automatically.
Any other header entries with no values are given values of ``None``.
Parameters
----------
raw_header : dict
The raw header entries received from the frontend.
Returns
-------
dict of string to string, integer, float, boolean, ``None`` or list of strings
The header of the image, with field names as keys.
"""
header = {}

history = []
comment = []
blank = []

def header_value(raw_entry):
try:
return raw_entry["numericValue"]
except KeyError:
try:
value = raw_entry["value"]
if value == 'T':
return True
if value == 'F':
return False
return value
except KeyError:
return None

for i, raw_entry in enumerate(raw_header):
name = raw_entry["name"]

if name.startswith("HISTORY "):
line = name[8:]
if line.startswith(">") and history:
history[-1] = history[-1] + line[1:]
else:
history.append(line)
continue

if name.startswith("COMMENT "):
comment.append(name[8:])
continue

if name.startswith(" " * 8):
blank.append(name[8:])
continue

header[name] = header_value(raw_entry)

if history:
header["HISTORY"] = history

if comment:
header["COMMENT"] = comment

if blank:
header[""] = blank

return header
24 changes: 19 additions & 5 deletions carta/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,11 @@ def open_image(self, path, hdu="", append=False, make_active=True, update_direct
Whether the image should be made active in the frontend. This only applies if an image is being appended. The default is ``True``.
update_directory : {4}
Whether the starting directory of the frontend file browser should be updated to the parent directory of the image. The default is ``False``.

Returns
-------
:obj:`carta.image.Image`
The opened image.
"""
directory, file_name = posixpath.split(path)
return Image.new(self, directory, file_name, hdu, append, False, make_active=make_active, update_directory=update_directory)
Expand All @@ -376,6 +381,11 @@ def open_complex_image(self, path, component=ComplexComponent.AMPLITUDE, append=
Whether the image should be made active in the frontend. This only applies if an image is being appended. The default is ``True``.
update_directory : {4}
Whether the starting directory of the frontend file browser should be updated to the parent directory of the image. The default is ``False``.

Returns
-------
:obj:`carta.image.Image`
The opened image.
"""
directory, file_name = posixpath.split(path)
expression = f'{component}("{file_name}")'
Expand All @@ -397,6 +407,11 @@ def open_LEL_image(self, expression, directory=".", append=False, make_active=Tr
Whether the image should be made active in the frontend. This only applies if an image is being appended. The default is ``True``.
update_directory : {4}
Whether the starting directory of the frontend file browser should be updated to the base directory of the LEL expression. The default is ``False``.

Returns
-------
:obj:`carta.image.Image`
The opened image.
"""
return Image.new(self, directory, expression, "", append, True, make_active=make_active, update_directory=update_directory)

Expand All @@ -405,7 +420,8 @@ def image_list(self):

Returns
-------
list of :obj:`carta.image.Image` objects.
list of :obj:`carta.image.Image` objects
The list of images open in this session.
"""
return Image.from_list(self, self.get_value("frameNames"))

Expand All @@ -417,10 +433,8 @@ def active_frame(self):
:obj:`carta.image.Image`
The currently active image.
"""
frame_info = self.get_value("activeFrame.frameInfo")
image_id = frame_info["fileId"]
file_name = frame_info["fileInfo"]["name"]
return Image(self, image_id, file_name)
image_id = self.get_value("activeFrame.frameInfo.fileId")
return Image(self, image_id)

def clear_spatial_reference(self):
"""Clear the spatial reference."""
Expand Down
Loading