Skip to content

Commit 9d0feb8

Browse files
authored
v1.12.3
version 1.12.3
2 parents ed104d9 + e4da0b2 commit 9d0feb8

File tree

12 files changed

+206
-46
lines changed

12 files changed

+206
-46
lines changed

pyobs/images/meta/genericoffset.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class GenericOffset:
2+
def __init__(self, dx: float, dy: float) -> None:
3+
self.dx: float = dx
4+
self.dy: float = dy

pyobs/images/processors/offsets/astrometry.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import Any
2+
from typing import Any, Tuple, Optional
33
from astropy.coordinates import SkyCoord
44
from astropy.wcs import WCS
55
import astropy.units as u
@@ -25,6 +25,9 @@ def __init__(self, **kwargs: Any):
2525
"""
2626
Offsets.__init__(self, **kwargs)
2727

28+
self._image: Optional[Image] = None
29+
self._wcs: Optional[WCS] = None
30+
2831
async def __call__(self, image: Image) -> Image:
2932
"""Processes an image and sets x/y pixel offset to reference in offset attribute.
3033
@@ -38,23 +41,27 @@ async def __call__(self, image: Image) -> Image:
3841
ValueError: If offset could not be found.
3942
"""
4043

41-
# copy image and get WCS
42-
# we make our life a little easier by only using the new WCS from astrometry
43-
img = image.copy()
44-
wcs = WCS(img.header)
44+
self._image = image.copy()
45+
self._wcs = WCS(image.header)
46+
47+
center_sky_coord, center_pixel_coord = self._get_coordinates_from_header(("CRVAL1", "CRVAL2"))
48+
teleskope_sky_coord, telescope_pixel_coord = self._get_coordinates_from_header(("TEL-RA", "TEL-DEC"))
49+
50+
offset = telescope_pixel_coord[0] - center_pixel_coord[0], telescope_pixel_coord[1] - center_pixel_coord[1]
51+
on_sky_distance = center_sky_coord.separation(teleskope_sky_coord)
4552

46-
# get x/y coordinates from CRVAL1/2, i.e. from center with good WCS
47-
center = SkyCoord(img.header["CRVAL1"] * u.deg, img.header["CRVAL2"] * u.deg, frame="icrs")
48-
x_center, y_center = wcs.world_to_pixel(center)
53+
self._image.set_meta(PixelOffsets(*offset))
54+
self._image.set_meta(OnSkyDistance(on_sky_distance))
55+
return self._image
4956

50-
# get x/y coordinates from TEL-RA/-DEC, i.e. from where the telescope thought it's pointing
51-
tel = SkyCoord(img.header["TEL-RA"] * u.deg, img.header["TEL-DEC"] * u.deg, frame="icrs")
52-
x_tel, y_tel = wcs.world_to_pixel(tel)
57+
def _get_coordinates_from_header(self, header_cards: Tuple[str, str]) -> Tuple[SkyCoord, Tuple[float, float]]:
58+
coordinates = SkyCoord(
59+
self._image.header[header_cards[0]] * u.deg, # type: ignore
60+
self._image.header[header_cards[1]] * u.deg, # type: ignore
61+
frame="icrs")
5362

54-
# calculate offsets as difference between both
55-
img.set_meta(PixelOffsets(x_tel - x_center, y_tel - y_center))
56-
img.set_meta(OnSkyDistance(center.separation(tel)))
57-
return img
63+
pixel_coordinates = self._wcs.world_to_pixel(coordinates) # type: ignore
64+
return coordinates, pixel_coordinates
5865

5966

6067
__all__ = ["AstrometryOffsets"]
Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import logging
22
from typing import Tuple, Any
33

4-
from astropy.coordinates import SkyCoord
4+
from astropy.coordinates import Angle
5+
from astropy.table import Table, Row
56
from astropy.wcs import WCS
67

78
from pyobs.images import Image
@@ -16,12 +17,11 @@ class BrightestStarOffsets(Offsets):
1617

1718
__module__ = "pyobs.images.processors.offsets"
1819

19-
def __init__(self, center: Tuple[str, str] = ("CRPIX1", "CRPIX2"), **kwargs: Any):
20+
def __init__(self, center_header_cards: Tuple[str, str] = ("CRPIX1", "CRPIX2"), **kwargs: Any):
2021
"""Initializes a new auto guiding system."""
2122
Offsets.__init__(self, **kwargs)
2223

23-
# init
24-
self._center = center
24+
self._center_header_cards = center_header_cards
2525

2626
async def __call__(self, image: Image) -> Image:
2727
"""Processes an image and sets x/y pixel offset to reference in offset attribute.
@@ -36,31 +36,33 @@ async def __call__(self, image: Image) -> Image:
3636
ValueError: If offset could not be found.
3737
"""
3838

39-
# get catalog and sort by flux
40-
cat = image.safe_catalog
41-
if cat is None or len(cat) < 1:
39+
catalog = image.safe_catalog
40+
if catalog is None or len(catalog) < 1:
4241
log.warning("No catalog found in image.")
4342
return image
44-
cat.sort("flux", reverse=True)
4543

46-
# get first X/Y coordinates
47-
x, y = cat["x"][0], cat["y"][0]
44+
star_pos = self._get_brightest_star_position(catalog)
45+
center = image.header[self._center_header_cards[0]], image.header[self._center_header_cards[1]]
4846

49-
# get center
50-
center_x, center_y = image.header[self._center[0]], image.header[self._center[1]]
47+
offset = (star_pos[0] - center[0], star_pos[1] - center[1])
48+
on_sky_distance = self._calc_on_sky_distance(image, center, star_pos)
5149

52-
# calculate offset
53-
dx, dy = x - center_x, y - center_y
50+
image.set_meta(PixelOffsets(*offset))
51+
image.set_meta(OnSkyDistance(on_sky_distance))
52+
return image
53+
54+
@staticmethod
55+
def _get_brightest_star_position(catalog: Table) -> Tuple[float, float]:
56+
brightest_star: Row = max(catalog, key=lambda row: row["flux"])
57+
return brightest_star["x"], brightest_star["y"]
5458

55-
# get distance on sky
59+
@staticmethod
60+
def _calc_on_sky_distance(image: Image, center: Tuple[float, float], star_pos: Tuple[float, float]) -> Angle:
5661
wcs = WCS(image.header)
57-
coords1 = wcs.pixel_to_world(center_x, center_y)
58-
coords2 = wcs.pixel_to_world(center_x + dx, center_y + dy)
62+
center_coordinates = wcs.pixel_to_world(*center)
63+
star_coordinates = wcs.pixel_to_world(*star_pos)
5964

60-
# set it and return image
61-
image.set_meta(PixelOffsets(dx, dy))
62-
image.set_meta(OnSkyDistance(coords1.separation(coords2)))
63-
return image
65+
return center_coordinates.separation(star_coordinates)
6466

6567

6668
__all__ = ["BrightestStarOffsets"]

pyobs/images/processors/offsets/dummyoffsets.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
from typing import Any
2+
13
from .offsets import Offsets
24
from pyobs.images import Image
35
from pyobs.object import get_class_from_string
46

57

68
class DummyOffsets(Offsets):
7-
def __init__(self, offset_class: str, offset: float = 1.0, **kwargs):
9+
def __init__(self, offset_class: str, offset: float = 1.0, **kwargs: Any) -> None:
810
super().__init__(**kwargs)
911

1012
self._offset = offset

pyobs/images/processors/offsets/fitsheader.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from pyobs.images import Image
66
from . import Offsets
7+
from ...meta.genericoffset import GenericOffset
78

89
log = logging.getLogger(__name__)
910

@@ -15,7 +16,9 @@ def __init__(self, target: Tuple[str, str], center: Tuple[str, str] = ("DET-CPX1
1516
"""Initializes new fits header offsets."""
1617
Offsets.__init__(self, **kwargs)
1718

18-
# store
19+
if len(target) != 2 or len(center) != 2:
20+
raise ValueError("Target and center must be of length 2!")
21+
1922
self.center = center
2023
self.target = target
2124

@@ -32,13 +35,14 @@ async def __call__(self, image: Image) -> Image:
3235
ValueError: If offset could not be found.
3336
"""
3437

35-
# get values from header
3638
target = [image.header[x] for x in self.target]
3739
center = [image.header[x] for x in self.center]
3840

39-
# calculate offset
40-
image.meta["offsets"] = np.subtract(target, center)
41-
return image
41+
offset = np.subtract(target, center)
42+
43+
output_image = image.copy()
44+
output_image.set_meta(GenericOffset(*offset))
45+
return output_image
4246

4347

4448
__all__ = ["FitsHeaderOffsets"]

pyobs/images/processors/offsets/nstar.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,15 @@ async def __call__(self, image: Image) -> Image:
8686
self.ref_boxes = await self._boxes_from_ref(image, star_box_size)
8787

8888
# reset and finish
89-
image.meta["offsets"] = (0, 0)
89+
image.set_meta(PixelOffsets(0.0, 0.0))
9090
return image
9191

9292
except ValueError as e:
9393
# didn't work
9494
log.warning(f"Could not initialize reference image info due to exception '{e}'. Resetting...")
9595
await self.reset()
96-
if "offsets" in image.meta:
97-
del image.meta["offsets"]
96+
if PixelOffsets in image.meta:
97+
del image.meta[PixelOffsets]
9898
self.offset = None, None
9999
return image
100100

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22
name = "pyobs-core"
33
packages = [{ include = "pyobs" }]
4-
version = "1.12.2"
4+
version = "1.12.3"
55
description = "robotic telescope software"
66
authors = ["Tim-Oliver Husser <[email protected]>"]
77
license = "MIT"

tests/images/processors/offsets/__init__.py

Whitespace-only changes.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import numpy as np
2+
import pytest
3+
from astropy.io import fits
4+
from astropy.utils.data import get_pkg_data_filename
5+
6+
from pyobs.images import Image
7+
from pyobs.images.meta import PixelOffsets, OnSkyDistance
8+
from pyobs.images.processors.offsets import AstrometryOffsets
9+
10+
11+
@pytest.mark.asyncio
12+
async def test_call() -> None:
13+
filename = get_pkg_data_filename('data/j94f05bgq_flt.fits', package='astropy.wcs.tests')
14+
fits_file = fits.open(filename)
15+
header = fits_file[1].header
16+
header["TEL-RA"] = 5.63
17+
header["TEL-DEC"] = -72.05
18+
19+
image = Image(header=header)
20+
21+
offsets = AstrometryOffsets()
22+
23+
output_image = await offsets(image)
24+
pixel_offset = output_image.get_meta(PixelOffsets)
25+
26+
np.testing.assert_almost_equal(pixel_offset.dx, 128.94120449972797)
27+
np.testing.assert_almost_equal(pixel_offset.dy, -309.1795167877043)
28+
29+
on_sky_distance = output_image.get_meta(OnSkyDistance)
30+
np.testing.assert_almost_equal(on_sky_distance.distance.value, 0.004575193216279022)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import logging
2+
3+
import numpy as np
4+
import pytest
5+
from astropy.io import fits
6+
from astropy.table import QTable
7+
from astropy.utils.data import get_pkg_data_filename
8+
9+
from pyobs.images import Image
10+
from pyobs.images.meta import PixelOffsets, OnSkyDistance
11+
from pyobs.images.processors.offsets import BrightestStarOffsets
12+
13+
14+
@pytest.mark.asyncio
15+
async def test_missing_catalog(caplog: pytest.LogCaptureFixture) -> None:
16+
offsets = BrightestStarOffsets()
17+
image = Image()
18+
19+
with caplog.at_level(logging.WARNING):
20+
await offsets(image)
21+
22+
assert caplog.messages[0] == "No catalog found in image."
23+
24+
25+
@pytest.mark.asyncio
26+
async def test_empty_catalog(caplog: pytest.LogCaptureFixture) -> None:
27+
offsets = BrightestStarOffsets()
28+
image = Image(catalog=QTable())
29+
30+
with caplog.at_level(logging.WARNING):
31+
await offsets(image)
32+
33+
assert caplog.messages[0] == "No catalog found in image."
34+
35+
36+
@pytest.mark.asyncio
37+
async def test_call() -> None:
38+
fn = get_pkg_data_filename('data/j94f05bgq_flt.fits', package='astropy.wcs.tests')
39+
f = fits.open(fn)
40+
41+
catalog = QTable({"x": [2050], "y": [1020], "flux": [1]})
42+
image = Image(data=np.zeros((20, 20)), catalog=catalog, header=f[1].header)
43+
44+
offsets = BrightestStarOffsets()
45+
46+
output_image = await offsets(image)
47+
pixel_offset = output_image.get_meta(PixelOffsets)
48+
49+
assert pixel_offset.dx == 2.0
50+
assert pixel_offset.dy == -4.0
51+
52+
on_sky_distance = output_image.get_meta(OnSkyDistance)
53+
np.testing.assert_almost_equal(on_sky_distance.distance.value, 6.06585686e-05)
54+
55+
56+
@pytest.mark.asyncio
57+
async def test_ordering() -> None:
58+
catalog = QTable({"x": [2050, 2049], "y": [1020, 1021], "flux": [1, 2]})
59+
60+
assert BrightestStarOffsets._get_brightest_star_position(catalog) == (2049, 1021)

0 commit comments

Comments
 (0)