Skip to content

Commit 3f0887b

Browse files
authored
Add Full Width at Half Maximum analyzer (#109)
1 parent fd012aa commit 3f0887b

File tree

4 files changed

+173
-2
lines changed

4 files changed

+173
-2
lines changed

src/vstarstack/library/data.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,16 @@ def add_parameter(self, value, name : str):
126126
"""Add parameter"""
127127
self.params[name] = value
128128

129-
def get_channel(self, channel) -> Tuple[dict, np.ndarray]:
130-
"""Get channel image"""
129+
def get_channel(self, channel : str) -> Tuple[np.ndarray, dict]:
130+
"""
131+
Get channel image.
132+
133+
Parameters:
134+
channel (str) - channel name
135+
136+
Returns:
137+
tuple(image, options) - ndarray with pixel data and channel options
138+
"""
131139
return self.channels[channel]["data"], self.channels[channel]["options"]
132140

133141
def get_channels(self) -> List[str]:

src/vstarstack/library/fwhm.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from typing import Tuple
2+
from vstarstack.library.data import DataFrame
3+
import numpy as np
4+
5+
def get_profile(image : np.ndarray):
6+
y = int(image.shape[0]/2)
7+
x = int(image.shape[1]/2)
8+
profile1 = image[y,:]
9+
profile2 = image[:,x]
10+
return (profile1 + profile2)/2
11+
12+
def get_star(image : np.ndarray, x : int, y : int, r : int):
13+
w = image.shape[1]
14+
h = image.shape[0]
15+
x1 = x - r
16+
x2 = x + r + 1
17+
y1 = y - r
18+
y2 = y + r + 1
19+
if x < r or y < r:
20+
return None
21+
if x > w-r-1 or y > h-r-1:
22+
return None
23+
return image[y1:y2, x1:x2]
24+
25+
def _interpolate(v1 : float, v2 : float, v : float):
26+
if v1 > v:
27+
return 0
28+
if v2 < v:
29+
return 1
30+
return (v - v1)/(v2-v1)
31+
32+
def get_width2(profile : np.ndarray):
33+
size = profile.shape[0]
34+
part = int(size/8)
35+
left = profile[0:part]
36+
right = profile[size-part-1:size-1]
37+
background = np.median(list(left) + list(right))
38+
noback = profile - background
39+
40+
maxv = np.amax(noback)
41+
center = int(size/2)
42+
for i in range(size):
43+
if noback[i] > noback[center]:
44+
center = i
45+
x1 = center
46+
while True:
47+
val = noback[x1]
48+
if val <= maxv/2:
49+
break
50+
x1 -= 1
51+
if x1 == 0:
52+
break
53+
54+
x2 = center
55+
while True:
56+
val = noback[x2]
57+
if val <= maxv/2:
58+
break
59+
x2 += 1
60+
if x2 == size-1:
61+
break
62+
63+
vl1 = noback[x1]
64+
vl2 = noback[x1+1]
65+
66+
vr1 = noback[x2]
67+
vr2 = noback[x2-1]
68+
69+
d = _interpolate(vl1, vl2, maxv/2)
70+
x1 += d
71+
72+
d = _interpolate(vr1, vr2, maxv/2)
73+
x2 -= d
74+
75+
return x2 - x1
76+
77+
def find_fwhm(image : np.ndarray, x : int, y : int, radius : int) -> float:
78+
maxv = np.amax(image)
79+
si = get_star(image, x, y, radius*8+1)
80+
if si is None:
81+
return None
82+
if np.amax(si) == maxv:
83+
return None
84+
profile = get_profile(si)
85+
width = get_width2(profile)
86+
return width
87+
88+
def find_fwhm_df(image : DataFrame, x : int, y : int, radius : int) -> Tuple[dict,float]:
89+
channels = image.get_channels()
90+
widths = {}
91+
for channel in channels:
92+
layer, opts = image.get_channel(channel)
93+
if not opts["brightness"]:
94+
continue
95+
widths[channel] = find_fwhm(layer, x, y, radius)
96+
mean = np.median(list(widths.values()))
97+
return widths, mean

src/vstarstack/tool/analyzers/analyzers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414

1515
commands = {
1616
"measure-mag" : ("vstarstack.tool.analyzers.measure_mag", "measure star magnitude"),
17+
"measure-fwhm" : ("vstarstack.tool.analyzers.measure_fwhm", "measure full width at half maximum"),
1718
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import vstarstack.library.data
2+
import vstarstack.library.fwhm
3+
import vstarstack.library.stars.detect
4+
import vstarstack.tool.common
5+
import numpy as np
6+
import os
7+
8+
def _measure_single_fwhm(project, argv):
9+
fname = argv[0]
10+
x = int(argv[1])
11+
y = int(argv[2])
12+
if len(argv) > 3:
13+
r = int(argv[3])
14+
else:
15+
r = 4
16+
df = vstarstack.library.data.DataFrame.load(fname)
17+
channels, mean = vstarstack.library.fwhm.find_fwhm_df(df, x, y, r)
18+
print("Mean: %f" % mean)
19+
for channel in channels:
20+
print("\t%s: %f" % (channel, channels[channel]))
21+
22+
def _measure_mean_fwhm_file(fname : str):
23+
df = vstarstack.library.data.DataFrame.load(fname)
24+
vstarstack.library.stars.detect.configure_detector(thresh_coeff=1.2)
25+
chvs = {}
26+
for channel in df.get_channels():
27+
layer, opts = df.get_channel(channel)
28+
if not opts["brightness"]:
29+
continue
30+
stars = vstarstack.library.stars.detect.detect_stars(layer)
31+
values = []
32+
for star in stars:
33+
x = int(star["x"]+0.5)
34+
y = int(star["y"]+0.5)
35+
r = int(star["radius"])
36+
fwhm = vstarstack.library.fwhm.find_fwhm(layer, x, y, r)
37+
if fwhm is not None:
38+
values.append(fwhm)
39+
fwhm = np.median(values)
40+
chvs[channel] = fwhm
41+
print("Mean: %f" % np.median(list(chvs.values())))
42+
for channel in chvs:
43+
print("\t%s: %f" % (channel, chvs[channel]))
44+
45+
def _measure_mean_fwhm_dir(dirname : str):
46+
files = vstarstack.tool.common.listfiles(dirname, ".zip")
47+
for name, filename in files:
48+
print(name)
49+
_measure_mean_fwhm_file(filename)
50+
51+
def _measure_mean_fwhm(project, argv):
52+
path = argv[0]
53+
if os.path.isdir(path):
54+
_measure_mean_fwhm_dir(path)
55+
else:
56+
_measure_mean_fwhm_file(path)
57+
58+
commands = {
59+
"star" : (_measure_single_fwhm,
60+
"calculate FWHM of specific star",
61+
"image.zip x y"),
62+
"mean" : (_measure_mean_fwhm,
63+
"calculate mean FWHM of stars",
64+
"(image.zip | path/)"),
65+
}

0 commit comments

Comments
 (0)