-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Write regions and catalogs #36
Changes from 38 commits
b0f8048
b3998b8
f904e45
4b48818
3ad1d58
9b2bfb7
cf1e8e8
d574bf3
ece367a
e8e6075
d6a9beb
e5798d2
1d182ae
dd51336
e46f763
53e7408
896ce23
015dd70
1b6a99d
64cbe40
f0daab2
975ff99
36a4e78
b36c3b3
660f5b1
83acfa1
f1aa77e
bf9d06d
337f692
76cba25
9f6b8ca
c80f3bb
3b3eca6
1fc13d9
aa79eb3
ead9bbb
2a5dbb0
e35df02
db804fd
56ffc96
19dbbec
438e8bd
7104796
53fef7e
285d453
d32db93
3d26852
866e08d
11d9211
4d0d609
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
title: Breizorro | ||
description: Image masking and cataloguing suite | ||
show_downloads: true | ||
google_analytics: | ||
theme: jekyll-theme-midnight |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,17 +6,27 @@ | |
import os.path | ||
import re | ||
import numpy as np | ||
|
||
import astropy.units as u | ||
from astropy.io import fits | ||
from astropy.coordinates import SkyCoord | ||
from astropy.wcs import WCS | ||
from astropy.coordinates import Angle | ||
from astropy.coordinates import SkyCoord | ||
|
||
import regions | ||
from regions import PixCoord | ||
from regions import PolygonSkyRegion, PolygonPixelRegion | ||
|
||
from argparse import ArgumentParser | ||
|
||
from scipy.ndimage.morphology import binary_dilation, binary_erosion, binary_fill_holes | ||
from scipy.ndimage.measurements import label, find_objects | ||
import scipy.special | ||
import scipy.ndimage | ||
|
||
from breizorro.utils import get_source_size, format_source_coordinates, deg2ra, deg2dec | ||
from breizorro.utils import get_image_data, fitsInfo, calculate_beam_area | ||
|
||
|
||
def create_logger(): | ||
"""Create a console logger""" | ||
|
@@ -31,40 +41,20 @@ def create_logger(): | |
|
||
LOGGER = create_logger() | ||
|
||
def get_image(fitsfile): | ||
""" | ||
Reads FITS file, returns tuple of image_array, header | ||
""" | ||
LOGGER.info(f"Reading {fitsfile} data") | ||
input_hdu = fits.open(fitsfile)[0] | ||
if len(input_hdu.data.shape) == 2: | ||
image = numpy.array(input_hdu.data[:, :]) | ||
elif len(input_hdu.data.shape) == 3: | ||
image = numpy.array(input_hdu.data[0, :, :]) | ||
else: | ||
image = numpy.array(input_hdu.data[0, 0, :, :]) | ||
return image, input_hdu.header | ||
|
||
|
||
def get_image_header(fitsfile): | ||
LOGGER.info(f"Reading {fitsfile} header") | ||
input_hdu = fits.open(fitsfile)[0] | ||
return input_hdu.header | ||
|
||
|
||
def flush_fits(newimage, fitsfile, header=None): | ||
LOGGER.info(f"Writing {fitsfile}") | ||
f = fits.open(fitsfile, mode='update') | ||
input_hdu = f[0] | ||
if len(input_hdu.data.shape) == 2: | ||
input_hdu.data[:, :] = newimage | ||
elif len(input_hdu.data.shape) == 3: | ||
input_hdu.data[0, :, :] = newimage | ||
else: | ||
input_hdu.data[0, 0, :, :] = newimage | ||
if header: | ||
input_hdu.header = header | ||
f.flush() | ||
with fits.open(fitsfile, mode='update') as f: | ||
input_hdu = f[0] | ||
if len(input_hdu.data.shape) == 2: | ||
input_hdu.data[:, :] = newimage | ||
elif len(input_hdu.data.shape) == 3: | ||
input_hdu.data[0, :, :] = newimage | ||
else: | ||
input_hdu.data[0, 0, :, :] = newimage | ||
if header: | ||
input_hdu.header = header | ||
f.flush() | ||
|
||
|
||
def make_noise_map(restored_image, boxsize): | ||
|
@@ -86,6 +76,7 @@ def make_noise_map(restored_image, boxsize): | |
LOGGER.info(f"Median noise value is {median_noise}") | ||
return noise | ||
|
||
|
||
def resolve_island(isl_spec, mask_image, wcs, ignore_missing=False): | ||
if re.match("^\d+$", isl_spec): | ||
return int(isl_spec) | ||
|
@@ -104,12 +95,14 @@ def resolve_island(isl_spec, mask_image, wcs, ignore_missing=False): | |
raise ValueError(f"coordinates {c} do not select a valid island") | ||
return value | ||
|
||
|
||
def add_regions(mask_image, regs, wcs): | ||
for reg in regs: | ||
if hasattr(reg, 'to_pixel'): | ||
reg = reg.to_pixel(wcs) | ||
mask_image += reg.to_mask().to_image(mask_image.shape) | ||
|
||
|
||
def remove_regions(mask_image, regs, wcs): | ||
for reg in regs: | ||
if hasattr(reg, 'to_pixel'): | ||
|
@@ -167,9 +160,18 @@ def main(): | |
parser.add_argument('--sum-peak', dest='sum_peak', default=None, | ||
help='Sum to peak ratio of flux islands to mask in original image.' | ||
'e.g. --sum-peak 100 will mask everything with a ratio above 100') | ||
parser.add_argument('-ncpu', '--ncpu', dest='ncpu', default=None, type=int, | ||
help='Number of processors to use for cataloging.') | ||
parser.add_argument('-beam', '--beam-size', dest='beam', default=None, | ||
help='Average beam size in arcesc incase beam info is missing in image header.') | ||
|
||
parser.add_argument('-o', '--outfile', dest='outfile', default='', | ||
help='Suffix for mask image (default based on input name') | ||
parser.add_argument('--save-catalog', dest='outcatalog', default='', | ||
help='Generate catalog based on region mask') | ||
parser.add_argument('--save-regions', dest='outregion', default='', | ||
help='Generate polygon regions from the mask') | ||
|
||
|
||
parser.add_argument('--gui', dest='gui', action='store_true', default=False, | ||
help='Open mask in gui.') | ||
|
@@ -192,7 +194,7 @@ def main(): | |
|
||
# first, load or generate mask | ||
if args.imagename: | ||
input_image, input_header = get_image(input_file) | ||
input_image, input_header = get_image_data(input_file) | ||
LOGGER.info(f"Generating mask using threshold {threshold}") | ||
|
||
noise_image = make_noise_map(input_image, boxsize) | ||
|
@@ -214,7 +216,7 @@ def main(): | |
out_mask_fits = args.outfile or f"{name}.mask.fits" | ||
|
||
elif args.maskname: | ||
mask_image, mask_header = get_image(args.maskname) | ||
mask_image, mask_header = get_image_data(args.maskname) | ||
LOGGER.info(f"Input mask loaded") | ||
|
||
out_mask_fits = args.outfile or f"{name}.out.{ext}" | ||
|
@@ -231,7 +233,7 @@ def load_fits_or_region(filename): | |
fits = regs = None | ||
# read as FITS or region | ||
try: | ||
fits = get_image(filename) | ||
fits = get_image_data(filename) | ||
except OSError: | ||
try: | ||
regs = regions.Regions.read(filename) | ||
|
@@ -342,40 +344,100 @@ def load_fits_or_region(filename): | |
mask_header['BUNIT'] = 'Jy/beam' | ||
mask_image = input_image * new_mask_image | ||
LOGGER.info(f"Number of extended islands found: {len(extended_islands)}") | ||
shutil.copyfile(input_file, out_mask_fits) # to provide a template | ||
flush_fits(mask_image, out_mask_fits, mask_header) | ||
LOGGER.info("Done") | ||
sys.exit(1) | ||
|
||
if args.outcatalog or args.outregion: | ||
try: | ||
from skimage.measure import find_contours | ||
except ImportError: | ||
LOGGER.info('pip install breizorro[all] to use cataloguing feature.') | ||
exit(1) | ||
contours = find_contours(mask_image, 0.5) | ||
polygon_regions = [] | ||
for contour in contours: | ||
# Convert the contour points to pixel coordinates | ||
contour_pixels = contour | ||
# Convert the pixel coordinates to Sky coordinates | ||
contour_sky = wcs.pixel_to_world(contour_pixels[:, 1], contour_pixels[:, 0]) | ||
# Create a Polygon region from the Sky coordinates | ||
polygon_region = PolygonSkyRegion(vertices=contour_sky, meta={'label': 'Region'}) | ||
# Add the polygon region to the list | ||
polygon_regions.append(polygon_region) | ||
LOGGER.info(f"Number of regions found: {len(polygon_regions)}") | ||
if args.outregion: | ||
regions.Regions(polygon_regions).write(args.outregion, format='ds9') | ||
LOGGER.info(f"Saving regions in {args.outregion}") | ||
|
||
if args.outcatalog and args.imagename: | ||
try: | ||
import warnings | ||
# Suppress FittingWarnings from Astropy | ||
# WARNING: The fit may be unsuccessful; check fit_info['message'] for more information. [astropy.modeling.fitting] | ||
warnings.resetwarnings() | ||
warnings.filterwarnings('ignore', category=UserWarning, append=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there's a context manager form ( |
||
from breizorro.catalog import multiprocess_contours | ||
except ModuleNotFoundError: | ||
LOGGER.error("Running breizorro source detector requires optional dependencies, please re-install with: pip install breizorro[catalog]") | ||
raise('Missing cataloguing dependencies') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe raise and log the same message? |
||
source_list = [] | ||
image_data, hdu_header = get_image_data(args.imagename) | ||
fitsinfo = fitsInfo(args.imagename) | ||
mean_beam = None # Use beam info from the image header by default | ||
if args.beam: | ||
mean_beam = float(args.beam) | ||
if mean_beam: | ||
LOGGER.info(f'Using user provided size: {mean_beam}') | ||
elif fitsinfo['b_size']: | ||
bmaj,bmin,_ = np.array(fitsinfo['b_size']) * 3600.0 | ||
mean_beam = 0.5 * (bmaj + bmin) | ||
else: | ||
raise('No beam information found. Specify mean beam in arcsec: --beam-size 6.5') | ||
|
||
noise = np.median(noise_image) | ||
f = open(args.outcatalog, 'w') | ||
catalog_out = f'# processing fits image: {args.imagename} \n' | ||
f.write(catalog_out) | ||
image_dimensions = fitsinfo['naxis'] | ||
pixel_size = fitsinfo['ddec'] * 3600.0 | ||
catalog_out = f'# mean beam size (arcsec): {round(mean_beam,2)} \n' | ||
f.write(catalog_out) | ||
catalog_out = f'# original image peak flux (Jy/beam): {image_data.max()} \n' | ||
f.write(catalog_out) | ||
catalog_out = f'# noise out (µJy/beam): {round(noise*1000000,2)} \n' | ||
f.write(catalog_out) | ||
limiting_flux = noise * threshold | ||
catalog_out = f'# cutt-off flux (mJy/beam): {round(limiting_flux*1000,2)} \n' | ||
f.write(catalog_out) | ||
LOGGER.info('Submitting distributed tasks. This might take a while...') | ||
source_list = multiprocess_contours(contours, image_data, fitsinfo, noise, args.ncpu) | ||
catalog_out = f"# freq0 (Hz): {fitsinfo['freq0']} \n" | ||
f.write(catalog_out) | ||
catalog_out = f'# number of sources detected: {len(source_list)} \n' | ||
f.write(catalog_out) | ||
catalog_out = '#\n#format: name ra_d dec_d i i_err emaj_s emin_s pa_d\n' | ||
f.write(catalog_out) | ||
for i in range(len(source_list)): | ||
output = 'src' + str(i) + ' ' + source_list[i][1] + '\n' | ||
f.write(output) | ||
f.close() | ||
LOGGER.info(f'Source catalog saved: {args.outcatalog}') | ||
|
||
if args.gui: | ||
try: | ||
from bokeh.models import BoxEditTool, ColumnDataSource, FreehandDrawTool | ||
from bokeh.plotting import figure, output_file, show | ||
from bokeh.io import curdoc | ||
curdoc().theme = 'caliber' | ||
from breizorro.gui import display | ||
except ModuleNotFoundError: | ||
LOGGER.error("Running breizorro gui requires optional dependencies, please re-install with: pip install breizorro[gui]") | ||
raise('Missing GUI dependencies') | ||
|
||
LOGGER.info("Loading Gui ...") | ||
d = mask_image | ||
p = figure(tooltips=[("x", "$x"), ("y", "$y"), ("value", "@image")]) | ||
p.x_range.range_padding = p.y_range.range_padding = 0 | ||
p.title.text = out_mask_fits | ||
|
||
# must give a vector of image data for image parameter | ||
p.image(image=[d], x=0, y=0, dw=10, dh=10, palette="Greys256", level="image") | ||
p.grid.grid_line_width = 0.5 | ||
src1 = ColumnDataSource({'x': [], 'y': [], 'width': [], 'height': [], 'alpha': []}) | ||
src2 = ColumnDataSource({'xs': [], 'ys': [], 'alpha': []}) | ||
renderer1 = p.rect('x', 'y', 'width', 'height', source=src1, alpha='alpha') | ||
renderer2 = p.multi_line('xs', 'ys', source=src2, alpha='alpha') | ||
draw_tool1 = BoxEditTool(renderers=[renderer1], empty_value=1) | ||
draw_tool2 = FreehandDrawTool(renderers=[renderer2]) | ||
p.add_tools(draw_tool1) | ||
p.add_tools(draw_tool2) | ||
p.toolbar.active_drag = draw_tool1 | ||
output_file("breizorro.html", title="Mask Editor") | ||
show(p) | ||
|
||
LOGGER.info(f"Enforcing that mask to binary") | ||
mask_image = mask_image!=0 | ||
mask_header['BUNIT'] = 'mask' | ||
display(args.imagename or args.maskname, mask_image, args.outcatalog, source_list) | ||
|
||
LOGGER.info(f"Enforcing that mask to binary") | ||
mask_image = mask_image!=0 | ||
mask_header['BUNIT'] = 'mask' | ||
|
||
shutil.copyfile(input_file, out_mask_fits) # to provide a template | ||
flush_fits(mask_image, out_mask_fits, mask_header) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this breaks with our current CLI convention of only single-letter options for "-". I would use "-j" for ncpu (it's a popular Unix alias) and "-B" for beam.