Skip to content

Commit d2c2be9

Browse files
committed
Add demo with live preview
1 parent 1f43d9e commit d2c2be9

File tree

1 file changed

+123
-0
lines changed

1 file changed

+123
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright © 2022 Université de Genève, LMU Munich - Faculty of Physics, IAP-CNRS/Sorbonne Université
5+
#
6+
# This library is free software; you can redistribute it and/or modify it under
7+
# the terms of the GNU Lesser General Public License as published by the Free
8+
# Software Foundation; either version 3.0 of the License, or (at your option)
9+
# any later version.
10+
#
11+
# This library is distributed in the hope that it will be useful, but WITHOUT
12+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13+
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
14+
# details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with this library; if not, write to the Free Software Foundation, Inc.,
18+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19+
import itertools
20+
import threading
21+
from argparse import ArgumentParser
22+
from configparser import ConfigParser
23+
from typing import Any, Dict
24+
25+
import matplotlib.pyplot as plt
26+
from astropy.io import fits
27+
from matplotlib.animation import FuncAnimation
28+
from matplotlib.colors import SymLogNorm
29+
from sourcextractor import __version__ as seversion
30+
from sourcextractor import pipeline
31+
32+
33+
class LivePreview:
34+
"""
35+
Display sources on a matplotlib figure as they are being detected
36+
"""
37+
38+
def __init__(self, image):
39+
self.__next_stage = None
40+
self.__figure, self.__ax = plt.subplots()
41+
self.__height, self.__width = fits.open(image)[0].data.shape
42+
self.__ani = FuncAnimation(self.__figure, self.__update, interval=1000, init_func=self.__init_fig, blit=False)
43+
self.__artists = []
44+
self.__norm = SymLogNorm(10)
45+
46+
def set_next_stage(self, next_stage):
47+
self.__next_stage = next_stage
48+
49+
def __init_fig(self):
50+
self.__ax.set_xlim(0, self.__width)
51+
self.__ax.set_ylim(0, self.__height)
52+
return []
53+
54+
def __update(self, frame: int):
55+
artists = self.__artists
56+
self.__artists = []
57+
return artists
58+
59+
def __add_stamp(self, source):
60+
stamp = source.detection_filtered_stamp
61+
pos_x, pos_y = source.pixel_centroid_x, source.pixel_centroid_y
62+
left, right = pos_x - stamp.shape[1] // 2, pos_x + stamp.shape[1] // 2
63+
bottom, top = pos_y - stamp.shape[0] // 2, pos_y + stamp.shape[1] // 2
64+
self.__artists.append(
65+
self.__ax.imshow(stamp, origin='lower', extent=(left, right, bottom, top), cmap='Greys_r', norm=self.__norm)
66+
)
67+
68+
def show(self):
69+
self.__figure.show()
70+
71+
def stop(self):
72+
self.__ani.event_source.stop()
73+
74+
def __call__(self, obj):
75+
if isinstance(obj, pipeline.Source):
76+
self.__add_stamp(obj)
77+
elif isinstance(obj, pipeline.Group):
78+
[self.__add_stamp(source) for source in obj]
79+
self.__next_stage(obj)
80+
81+
82+
def run_sourcextractor(config: Dict[str, Any], output_path: str):
83+
"""
84+
Setup the sourcextractor++ pipeline
85+
"""
86+
config['output-catalog-filename'] = output_path
87+
live = LivePreview(config['detection-image'])
88+
with pipeline.Context(config):
89+
stages = [pipeline.Segmentation(), pipeline.Partition(),
90+
pipeline.Grouping(), pipeline.Deblending(), live, pipeline.Measurement(), pipeline.FitsOutput()]
91+
pipe = pipeline.Pipeline(stages)
92+
93+
# UI must run on the main thread, so sourcextractor++ must run on another
94+
sourcex_thread = threading.Thread(target=lambda: pipe().get())
95+
sourcex_thread.start()
96+
while sourcex_thread.is_alive():
97+
live.show()
98+
plt.pause(0.05)
99+
sourcex_thread.join()
100+
print(f'Done!')
101+
live.stop()
102+
plt.show(block=True)
103+
104+
105+
def parse_config_file(path: str) -> Dict[str, Any]:
106+
"""
107+
Parse a sourcextractor++ (like) config file into a dictionary
108+
"""
109+
parser = ConfigParser()
110+
with open(path, 'rt') as fd:
111+
parser.read_file(itertools.chain(['[main]'], fd))
112+
return {k: v for k, v in parser.items('main')}
113+
114+
115+
if __name__ == '__main__':
116+
print(f'Running sourcextractor++ {seversion}')
117+
118+
parser = ArgumentParser()
119+
parser.add_argument('--output-file', type=str, metavar='FITS', default='output.fits', help='Output file')
120+
parser.add_argument('config_file', type=str, metavar='CONFIGURATION', help='Configuration file')
121+
122+
args = parser.parse_args()
123+
run_sourcextractor(parse_config_file(args.config_file), args.output_file)

0 commit comments

Comments
 (0)