Skip to content

Commit aaeca8f

Browse files
committed
Add a Python demo
1 parent 2462ca6 commit aaeca8f

File tree

5 files changed

+232
-1
lines changed

5 files changed

+232
-1
lines changed

SEPythonWrapper/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,11 @@ elements_depends_on_subdirs(SEImplementation)
6565
# elements_install_scripts()
6666
#===============================================================================
6767
elements_install_python_modules()
68+
elements_install_scripts()
6869

6970
#===============================================================================
7071
# Add the elements_install_conf_files macro
7172
# Examples:
7273
# elements_install_conf_files()
7374
#===============================================================================
75+
elements_install_conf_files()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
auto-kron-factor = 2.5
2+
auto-kron-min-radius = 3.5
3+
background-cell-size = 64
4+
smoothing-box-size = 3
5+
detection-threshold = 1.4953
6+
segmentation-algorithm = LUTZ
7+
segmentation-use-filtering = true
8+
segmentation-filter = /home/aalvarez/Work/Projects/SourceXtractor-litmus/tests/../data/sim12/default.conv
9+
detection-image-interpolation = 1
10+
detection-image-interpolation-gap = 5
11+
use-cleaning = false
12+
cleaning-minimum-area = 10
13+
detection-minimum-area = 5
14+
grouping-algorithm = SPLIT
15+
magnitude-zero-point = 32.19
16+
tile-memory-limit = 512
17+
tile-size = 256
18+
model-fitting-iterations = 1000
19+
thread-count = 4
20+
partition-multithreshold = true
21+
partition-threshold-count = 32
22+
partition-minimum-area = 3
23+
partition-minimum-contrast = 0.005
24+
psf-fwhm = 3
25+
psf-pixel-sampling = 1
26+
weight-use-symmetry = 1
27+
output-properties = SourceIDs,PixelCentroid,WorldCentroid,IsophotalFlux,SourceFlags
28+
detection-image = /home/aalvarez/Work/Projects/SourceXtractor-litmus/tests/../data/sim12/img/sim12.fits.gz
29+
weight-image = /home/aalvarez/Work/Projects/SourceXtractor-litmus/tests/../data/sim12/img/sim12.weight.fits.gz
30+
weight-type = weight
31+
weight-absolute = True
32+
python-config-file = /home/aalvarez/Work/Projects/SourceXtractor-litmus/tests/../data/sim12/sim12_multi_modelfitting.py
33+
# Custom filter!
34+
snr = 10

SEPythonWrapper/python/sourcextractor/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
# You should have received a copy of the GNU Lesser General Public License
1616
# along with this library; if not, write to the Free Software Foundation, Inc.,
1717
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18-
from _SEPythonConfig import Flags
1918

19+
from _SEPythonConfig import Flags
20+
from SOURCEXTRACTORPLUSPLUS_VERSION import SOURCEXTRACTORPLUSPLUS_VERSION_STRING as __version__
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright © 2022 Université de Genève, LMU Munich - Faculty of Physics, IAP-CNRS/Sorbonne Université
2+
#
3+
# This library is free software; you can redistribute it and/or modify it under
4+
# the terms of the GNU Lesser General Public License as published by the Free
5+
# Software Foundation; either version 3.0 of the License, or (at your option)
6+
# any later version.
7+
#
8+
# This library is distributed in the hope that it will be useful, but WITHOUT
9+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10+
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
11+
# details.
12+
#
13+
# You should have received a copy of the GNU Lesser General Public License
14+
# along with this library; if not, write to the Free Software Foundation, Inc.,
15+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16+
17+
from _SEPythonModule import *
18+
19+
20+
class Pipeline:
21+
"""
22+
Wrap a set of pipeline stages and chains them
23+
24+
:param stages:
25+
List of pipeline stages
26+
"""
27+
28+
def __init__(self, stages):
29+
if len(stages) < 1:
30+
raise ValueError('Expecting at least one stage')
31+
self.__first = stages[0]
32+
self.__last = stages[-1]
33+
for a, b in zip(stages[:-1], stages[1:]):
34+
a.set_next_stage(b)
35+
36+
def __call__(self):
37+
"""
38+
Trigger the execution of the pipeline
39+
:return:
40+
The last chain of the pipeline
41+
"""
42+
# TODO: Return the value generated by the last stage?
43+
self.__first()
44+
return self.__last
45+
46+
47+
class DefaultPipeline(Pipeline):
48+
"""
49+
Default implementation of the sourcextractor++ pipeline, equivalent to running
50+
the CLI manually
51+
"""
52+
53+
def __init__(self):
54+
super().__init__([Segmentation(), Partition(), Grouping(), Deblending(), Output()])
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright © 2022 Université de Genève, LMU Munich - Faculty of Physics, IAP-CNRS/Sorbonne Université
4+
#
5+
# This library is free software; you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the Free
7+
# Software Foundation; either version 3.0 of the License, or (at your option)
8+
# any later version.
9+
#
10+
# This library is distributed in the hope that it will be useful, but WITHOUT
11+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12+
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
13+
# details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with this library; if not, write to the Free Software Foundation, Inc.,
17+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18+
import itertools
19+
import os.path
20+
from argparse import ArgumentParser
21+
from configparser import ConfigParser
22+
from typing import Any, Dict
23+
24+
import h5py
25+
import numpy as np
26+
from sourcextractor import __version__ as seversion
27+
from sourcextractor import pipeline
28+
29+
30+
class SNRFilter:
31+
"""
32+
Drop sources with a signal-to-noise below the configured limit.
33+
It expect sources, so it must be inserted into the pipeline *before* the partitioning
34+
35+
:param snr: float
36+
Signal-to-noise ratio cut
37+
"""
38+
39+
def __init__(self, snr: float):
40+
self.__snr = snr
41+
self.__next = None
42+
self.__dropped = []
43+
44+
@property
45+
def dropped(self):
46+
return self.__dropped
47+
48+
def set_next_stage(self, stage):
49+
self.__next = stage
50+
51+
def __call__(self, obj):
52+
"""
53+
Apply the SNR filter
54+
"""
55+
if isinstance(obj, pipeline.Source):
56+
if obj.isophotal_flux / obj.isophotal_flux_err < self.__snr:
57+
self.__dropped.append(obj)
58+
return
59+
self.__next(obj)
60+
61+
62+
class StoreStamps:
63+
"""
64+
Store the detection stamps into the HDF5 file
65+
66+
:param hd5: h5py.File
67+
Output HDF5 file
68+
"""
69+
70+
def __init__(self, hd5: h5py.File):
71+
self.__hd5 = hd5
72+
self.__next = None
73+
74+
def set_next_stage(self, stage):
75+
self.__next = stage
76+
77+
def __store_stamp(self, source):
78+
stamp = source.detection_filtered_stamp
79+
dataset = self.__hd5.create_dataset(f'sources/{source.source_id}', data=stamp)
80+
dataset.attrs.create('CLASS', 'IMAGE', dtype='S6')
81+
dataset.attrs.create('IMAGE_VERSION', '1.2', dtype='S4')
82+
dataset.attrs.create('IMAGE_SUBCLASS', 'IMAGE_GRAYSCALE', dtype='S16')
83+
dataset.attrs.create('IMAGE_MINMAXRANGE', [np.min(stamp), np.max(stamp)])
84+
85+
def __call__(self, obj):
86+
"""
87+
Supports being called with a single Source, or with a Group of sources
88+
"""
89+
match type(obj):
90+
case pipeline.Source:
91+
self.__store_stamp(obj)
92+
case pipeline.Group:
93+
[self.__store_stamp(source) for source in obj]
94+
case _:
95+
print(f'Unknown {type(obj)}')
96+
self.__next(obj)
97+
98+
99+
def run_sourcextractor(config: Dict[str, Any], output_path: str, stamps: bool):
100+
"""
101+
Setup the sourcextractor++ pipeline, run it, and write the output to an HDF5 file
102+
"""
103+
output = h5py.File(output_path, 'w')
104+
105+
snr_filter = SNRFilter(float(config.pop('snr', 5)))
106+
with pipeline.Context(config):
107+
stages = [pipeline.Segmentation(), pipeline.Partition(), snr_filter, pipeline.Grouping(), pipeline.Deblending()]
108+
if stamps:
109+
stages.append(StoreStamps(output))
110+
stages.append(pipeline.NumpyOutput())
111+
pipe = pipeline.Pipeline(stages)
112+
result = pipe().to_numpy()
113+
print(f'Dropped {len(snr_filter.dropped)} sources')
114+
115+
output.create_dataset(os.path.basename(config['detection-image']), data=result)
116+
output.close()
117+
print(f'{output_path} created!')
118+
119+
120+
def parse_config_file(path: str) -> Dict[str, Any]:
121+
"""
122+
Parse a sourcextractor++ (like) config file into a dictionary
123+
"""
124+
parser = ConfigParser()
125+
with open(path, 'rt') as fd:
126+
parser.read_file(itertools.chain(['[main]'], fd))
127+
return {k: v for k, v in parser.items('main')}
128+
129+
130+
# Entry point
131+
if __name__ == '__main__':
132+
print(f'Running sourcextractor++ {seversion}')
133+
134+
parser = ArgumentParser()
135+
parser.add_argument('--output-file', type=str, metavar='HDF5', default='output.h5', help='Output file')
136+
parser.add_argument('--with-stamps', action='store_true', default=False, help='Store source stamps')
137+
parser.add_argument('config_file', type=str, metavar='CONFIGURATION', help='Configuration file')
138+
139+
args = parser.parse_args()
140+
run_sourcextractor(parse_config_file(args.config_file), args.output_file, args.with_stamps)

0 commit comments

Comments
 (0)