Skip to content

Commit 00f40c1

Browse files
committed
Add utilities for detection when noise is correlated.
1 parent 23fdbe9 commit 00f40c1

File tree

6 files changed

+484
-0
lines changed

6 files changed

+484
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// -*- lsst-c++ -*-
2+
/*
3+
* LSST Data Management System
4+
* Copyright 2017 LSST/AURA.
5+
*
6+
* This product includes software developed by the
7+
* LSST Project (http://www.lsst.org/).
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU General Public License as published by
11+
* the Free Software Foundation, either version 3 of the License, or
12+
* (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the LSST License Statement and
20+
* the GNU General Public License along with this program. If not,
21+
* see <http://www.lsstcorp.org/LegalNotices/>.
22+
*/
23+
24+
#ifndef LSST_MEAS_ALGORITHMS_CorrelatedNoiseDetection_h_INCLUDED
25+
#define LSST_MEAS_ALGORITHMS_CorrelatedNoiseDetection_h_INCLUDED
26+
27+
#include <string>
28+
#include <vector>
29+
30+
#include "lsst/afw/image/MaskedImage.h"
31+
32+
namespace lsst { namespace meas { namespace algorithms {
33+
34+
/**
35+
* Estimate the noise correlation kernel of an image, assuming it is stationary.
36+
*
37+
* @param[in] image The image to be measured. The empirical covariance
38+
* will be divided by the values in image's variance plane,
39+
* and its mask will be used to reject pixels containing
40+
* objects or artifacts.
41+
* @param[in] radius Distance in pixels to which the correlation is measured
42+
* on either side; the returned image will have dimensions
43+
* (2*radius + 1, 2*radius + 1).
44+
* @param[in] badBitMask A bit mask indicating pixels that should not be included
45+
* in the measurement. Should generally include at least
46+
* DETECTED.
47+
*
48+
* @return the noise correlation kernel: an image in which the central pixel
49+
* represents the fraction of the total variance/covariance in the variance,
50+
* and neighboring pixels contain the correlation at different offsets.
51+
*/
52+
afw::image::Image<float> measureCorrelationKernel(
53+
afw::image::MaskedImage<float> const & image,
54+
int radius,
55+
afw::image::MaskPixel badBitMask
56+
);
57+
58+
/**
59+
* Estimate the noise correlation kernel of an image, assuming it is stationary.
60+
*
61+
* @param[in] image The image to be measured. The empirical covariance
62+
* will be divided by the values in image's variance plane,
63+
* and its mask will be used to reject pixels containing
64+
* objects or artifacts.
65+
* @param[in] radius Distance in pixels to which the correlation is measured
66+
* on either side; the returned image will have dimensions
67+
* (2*radius + 1, 2*radius + 1).
68+
* @param[in] badMaskPlanes A list of mask planenames indicating pixels that
69+
* should not be included in the measurement.
70+
* Should generally include at least DETECTED.
71+
*
72+
* @return the noise correlation kernel: an image in which the central pixel
73+
* represents the fraction of the total variance/covariance in the variance,
74+
* and neighboring pixels contain the correlation at different offsets.
75+
*/
76+
afw::image::Image<float> measureCorrelationKernel(
77+
afw::image::MaskedImage<float> const & image,
78+
int radius,
79+
std::vector<std::string> const & badMaskPlanes
80+
);
81+
82+
83+
/**
84+
* Fit an optimal detection kernel for a PSF that corrects for correlated noise.
85+
*
86+
* This is the same as the kernel that yields the PSF when convolved with the
87+
* correlation kernel.
88+
*
89+
* The returned kernel cannot be used in detection in quite the same way as the
90+
* PSF is used in detection on images with noise correlated noise.
91+
* In the uncorrelated noise case with image @f$z@f$, PSF @f$\phi@f$, and
92+
* per-pixel variance @f$\sigma^2@f$, the significance image is
93+
* @f[
94+
* \nu = \frac{\phi \ast z}{\sigma \sqrt{\phi \cdot \phi}}
95+
* @f]
96+
* In the correlated noise case, with @f$\psi@f$ the kernel computed by this
97+
* function, the significance image is
98+
* @f[
99+
* \nu = \frac{\psi \ast z}{\sigma \sqrt{\phi \cdot \psi}}
100+
* @f]
101+
* (note the difference in the denominator).
102+
*
103+
* @param[in] psf Image of the PSF model.
104+
* @param[in] correlation Noise correlation kernel, of the sort estimated by
105+
* measureCorrelationKernel.
106+
* @param[in] radius Radius of the detection kernel image in pixels.
107+
* The returned image will have dimensions
108+
* (2*radius + 1, 2*radius + 1).
109+
*
110+
* @return an image containing the optimal detection kernel.
111+
*/
112+
afw::image::Image<double> fitGeneralDetectionKernel(
113+
afw::image::Image<double> const & psf,
114+
afw::image::Image<float> const & correlation,
115+
int radius
116+
);
117+
118+
119+
}}} // namespace lsst::meas::algorithms
120+
121+
#endif // !LSST_MEAS_ALGORITHMS_CorrelatedNoiseDetection_h_INCLUDED

python/lsst/meas/algorithms/SConscript

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ scripts.BasicSConscript.pybind11(["binnedWcs",
44
"cr",
55
"coaddBoundedField",
66
"coaddPsf/coaddPsf",
7+
"correlatedNoiseDetection",
78
"doubleGaussianPsf",
89
"imagePsf",
910
"interp",

python/lsst/meas/algorithms/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
from .loadIndexedReferenceObjects import *
6464
from .indexerRegistry import *
6565
from .reserveSourcesTask import *
66+
from .correlatedNoiseDetection import *
6667

6768
from .version import *
6869

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* LSST Data Management System
3+
* Copyright 2017 LSST/AURA.
4+
*
5+
* This product includes software developed by the
6+
* LSST Project (http://www.lsst.org/).
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU General Public License as published by
10+
* the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU General Public License for more details.
17+
*
18+
* You should have received a copy of the LSST License Statement and
19+
* the GNU General Public License along with this program. If not,
20+
* see <https://www.lsstcorp.org/LegalNotices/>.
21+
*/
22+
#include "pybind11/pybind11.h"
23+
#include "pybind11/stl.h"
24+
25+
#include "lsst/meas/algorithms/CorrelatedNoiseDetection.h"
26+
27+
namespace py = pybind11;
28+
using namespace pybind11::literals;
29+
30+
namespace lsst {
31+
namespace meas {
32+
namespace algorithms {
33+
34+
PYBIND11_PLUGIN(correlatedNoiseDetection) {
35+
py::module::import("lsst.afw.image");
36+
37+
py::module mod("correlatedNoiseDetection");
38+
39+
mod.def(
40+
"measureCorrelationKernel",
41+
(afw::image::Image<float> (*)(
42+
afw::image::MaskedImage<float> const &,
43+
int,
44+
afw::image::MaskPixel
45+
))&measureCorrelationKernel,
46+
"image"_a, "radius"_a, "badBitMask"_a
47+
);
48+
49+
mod.def(
50+
"measureCorrelationKernel",
51+
(afw::image::Image<float> (*)(
52+
afw::image::MaskedImage<float> const &,
53+
int,
54+
std::vector<std::string> const &
55+
))&measureCorrelationKernel,
56+
"image"_a, "radius"_a, "badMaskPlanes"_a
57+
);
58+
59+
mod.def("fitGeneralDetectionKernel", &fitGeneralDetectionKernel, "psf"_a, "correlations"_a, "radius"_a);
60+
61+
return mod.ptr();
62+
}
63+
64+
} // algorithms
65+
} // meas
66+
} // lsst

src/CorrelatedNoiseDetection.cc

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// -*- LSST-C++ -*-
2+
/*
3+
* LSST Data Management System
4+
* Copyright 2017 LSST/AURA.
5+
*
6+
* This product includes software developed by the
7+
* LSST Project (http://www.lsst.org/).
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU General Public License as published by
11+
* the Free Software Foundation, either version 3 of the License, or
12+
* (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the LSST License Statement and
20+
* the GNU General Public License along with this program. If not,
21+
* see <http://www.lsstcorp.org/LegalNotices/>.
22+
*/
23+
24+
#include "lsst/pex/exceptions.h"
25+
#include "lsst/afw/math/LeastSquares.h"
26+
#include "lsst/meas/algorithms/CorrelatedNoiseDetection.h"
27+
28+
namespace lsst { namespace meas { namespace algorithms {
29+
30+
31+
afw::image::Image<float> measureCorrelationKernel(
32+
afw::image::MaskedImage<float> const & mi,
33+
int radius,
34+
afw::image::MaskPixel badBitMask
35+
) {
36+
afw::image::Image<float> result(2*radius + 1, 2*radius + 1);
37+
afw::image::Image<int> count(result.getDimensions());
38+
int const width = mi.getWidth();
39+
int const height = mi.getHeight();
40+
// iterate over pixels in the MaskedImage, skipping any that meet our bad mask criteria
41+
for (int y1 = 0; y1 < height; ++y1) {
42+
int const y2a = std::max(0, y1 - radius);
43+
int const y2b = std::min(height, y1 + radius + 1);
44+
for (int x1 = 0; x1 < width; ++x1) {
45+
if ((*mi.getMask())(x1, y1) & badBitMask) {
46+
continue;
47+
}
48+
float z = (*mi.getImage())(x1, y1) / (*mi.getVariance())(x1, y1);
49+
// iterate over neighboring pixels, with bounds set to avoid image boundaries
50+
int const x2a = std::max(0, x1 - radius);
51+
int const x2b = std::min(height, x1 + radius + 1);
52+
for (int y2 = y2a; y2 < y2b; ++y2) {
53+
auto miIter = mi.row_begin(y2) + x2a;
54+
auto outIter = result.row_begin(radius + y2 - y1) + radius + x2a - x1;
55+
auto countIter = count.row_begin(radius + y2 - y1) + radius + x2a - x1;
56+
for (int x2 = x2a; x2 < x2b; ++x2, ++miIter, ++outIter, ++countIter) {
57+
if (miIter.mask() & badBitMask) {
58+
continue;
59+
}
60+
*outIter += z*miIter.image();
61+
*countIter += 1;
62+
}
63+
}
64+
}
65+
}
66+
result.getArray().deep() /= count.getArray();
67+
result.setXY0(-radius, -radius);
68+
return result;
69+
}
70+
71+
afw::image::Image<float> measureCorrelationKernel(
72+
afw::image::MaskedImage<float> const & image,
73+
int radius,
74+
std::vector<std::string> const & badMaskPlanes
75+
) {
76+
afw::image::MaskPixel mask = 0x0;
77+
for (auto plane : badMaskPlanes) {
78+
mask |= image.getMask()->getPlaneBitMask(plane);
79+
}
80+
return measureCorrelationKernel(image, radius, mask);
81+
}
82+
83+
84+
namespace {
85+
86+
afw::geom::Extent2I getOddBoxHalfWidth(afw::geom::Box2I const & box, std::string const & name) {
87+
afw::geom::Extent2I r((box.getWidth() - 1) / 2, (box.getHeight() - 1) / 2);
88+
if (r.getX()*2 + 1 != box.getWidth()) {
89+
throw LSST_EXCEPT(
90+
pex::exceptions::InvalidParameterError,
91+
name + " image width must be an odd integer"
92+
);
93+
}
94+
if (r.getY()*2 + 1 != box.getHeight()) {
95+
throw LSST_EXCEPT(
96+
pex::exceptions::InvalidParameterError,
97+
name + " image height must be an odd integer"
98+
);
99+
}
100+
return r;
101+
}
102+
103+
} // anonymous
104+
105+
106+
afw::image::Image<double> fitGeneralDetectionKernel(
107+
afw::image::Image<double> const & psf,
108+
afw::image::Image<float> const & correlation,
109+
int radius
110+
) {
111+
auto const psfR = getOddBoxHalfWidth(psf.getBBox(), "PSF");
112+
auto const corrR = getOddBoxHalfWidth(correlation.getBBox(), "Correlation kernel");
113+
afw::geom::Extent2I const outR(radius, radius);
114+
if (psfR.getX() <= corrR.getX()) {
115+
throw LSST_EXCEPT(
116+
pex::exceptions::InvalidParameterError,
117+
"PSF image width must be greater than correlation kernel width"
118+
);
119+
}
120+
if (psfR.getY() <= corrR.getY()) {
121+
throw LSST_EXCEPT(
122+
pex::exceptions::InvalidParameterError,
123+
"PSF image height must be greater than correlation kernel height"
124+
);
125+
}
126+
if (psfR.getX() <= outR.getX()) {
127+
throw LSST_EXCEPT(
128+
pex::exceptions::InvalidParameterError,
129+
"PSF image width must be greater than output kernel width"
130+
);
131+
}
132+
if (psfR.getY() <= outR.getY()) {
133+
throw LSST_EXCEPT(
134+
pex::exceptions::InvalidParameterError,
135+
"PSF image height must be greater than output kernel height"
136+
);
137+
}
138+
139+
int const psfN = (psfR.getX()*2 + 1)*(psfR.getY()*2 + 1);
140+
int const outN = (outR.getX()*2 + 1)*(outR.getY()*2 + 1);
141+
142+
// Get locators at the center of each image, so we can use indices with
143+
// the origin at the center and make the code easier to read.
144+
auto psfL = psf.xy_at(psfR.getX(), psfR.getY());
145+
auto corrL = correlation.xy_at(corrR.getX(), corrR.getY());
146+
147+
// rhs is the PSF image, with rows and columns flattened into one dimension
148+
Eigen::VectorXd rhs = Eigen::VectorXd::Zero(psfN);
149+
150+
// matrix represents convolution with the correlated noise kernel image
151+
Eigen::MatrixXd matrix = Eigen::MatrixXd::Zero(psfN, outN);
152+
int xyN = 0;
153+
for (int y = -psfR.getY(); y <= psfR.getY(); ++y) {
154+
for (int x = -psfR.getX(); x <= psfR.getX(); ++x) {
155+
int ijN = 0;
156+
for (int i = -outR.getY(); i <= outR.getY(); ++i) {
157+
for (int j = -outR.getX(); j <= outR.getX(); ++j) {
158+
// Could move these checks into the inner loop bounds for performance,
159+
// but this is easier to read and probably fast enough.
160+
if (std::abs(y - i) <= corrR.getY() && std::abs(x - j) <= corrR.getX()) {
161+
matrix(xyN, ijN) = corrL(x - j, y - i);
162+
}
163+
++ijN;
164+
}
165+
}
166+
rhs[xyN] = psfL(x, y);
167+
++xyN;
168+
}
169+
}
170+
171+
// solve for the kernel that produces the PSF when convolved with the
172+
// noise correlation kernel
173+
auto lstsq = afw::math::LeastSquares::fromDesignMatrix(matrix, rhs);
174+
auto solution = lstsq.getSolution();
175+
176+
// copy the result from the flattened solution vector into an image
177+
afw::image::Image<double> result(outR.getX()*2 + 1, outR.getY()*2 + 1);
178+
auto outL = result.xy_at(outR.getX(), outR.getY());
179+
xyN = 0;
180+
for (int y = -outR.getY(); y <= outR.getY(); ++y) {
181+
for (int x = -outR.getX(); x <= outR.getX(); ++x) {
182+
outL(x, y) = solution[xyN];
183+
++xyN;
184+
}
185+
}
186+
187+
result.setXY0(-outR.getX(), -outR.getY());
188+
return result;
189+
}
190+
191+
192+
}}} // namespace lsst::meas::algorithms

0 commit comments

Comments
 (0)