Documentation
¶
Overview ¶
Package structured_light is a standard-library-only implementation of the structured-light pattern generation and decoding routines from OpenCV's contrib "structured_light" module, built on top of the root cv package (github.com/malcolmston/opencv).
It depends only on the Go standard library and the root cv package: no cgo, no third-party code, and it imports no sibling cv/* subpackage. The root package supplies the github.com/malcolmston/opencv.Mat container (8-bit, row-major, channel-interleaved); everything specific to structured light is implemented here from scratch.
What structured light is ¶
A structured-light scanner projects a known sequence of patterns onto a scene and observes them with a camera. Because the projected pattern is known, each camera pixel can be labelled with the projector pixel (column and/or row) that illuminated it. That per-pixel camera→projector correspondence is the raw ingredient for triangulating 3-D geometry. This package produces the patterns and recovers the correspondence; it does not perform projector-camera calibration or triangulation (see the deferred list).
Gray-code patterns ¶
GrayCodePattern encodes the projector column and row indices in binary reflected Gray code, one bit per projected image. Gray code is used because adjacent codes differ in exactly one bit, so a decoding error never produces a wildly wrong coordinate. For each bit a pattern and its photometric inverse are projected; comparing the two makes bit decisions independent of surface albedo and ambient light. A fully-lit (white) and fully-dark (black) reference pair is also projected to build a shadow mask and to gauge the per-pixel contrast used for the robust-bit test.
The pattern vector produced by GrayCodePattern.Generate is laid out column-bits first, then row-bits, with each bit immediately followed by its inverse:
[col0 col0inv col1 col1inv ... row0 row0inv row1 row1inv ...]
GrayCodePattern.Decode consumes a captured stack in that exact order, together with the white/black references, and returns a Decoded map giving, for every camera pixel, the projector column and row it corresponds to plus a validity mask.
Sinusoidal (phase-shifting) patterns ¶
SinusoidalPattern implements N-step phase-shifting profilometry. It projects N sinusoidal fringe patterns, each phase-shifted by a fixed amount, and recovers a wrapped phase map with SinusoidalPattern.ComputeWrappedPhase. The wrapped phase lies in (-π, π]; UnwrapPhaseMap removes the 2π discontinuities along the fringe direction to yield a continuous absolute phase that is proportional to the projector coordinate. NStepWrappedPhase is a stack-only, arbitrary-step generalization of the same estimator.
Temporal phase unwrapping ¶
A single fringe frequency wraps every period, so a spatial unwrap fails wherever the surface is discontinuous. The temporal routines resolve the ambiguity per-pixel instead, from a set of frequencies: MultiFrequencyUnwrap performs hierarchical ratio unwrapping and HeterodyneUnwrap the two-/three-frequency beat method. Both take FrequencyPhase levels and recover an absolute phase whose range far exceeds 2π. CombineGrayAndPhase fuses a coarse Gray-code fringe order with a fine phase-shift wrap (the Gray-code-plus-phase-shift hybrid), and QualityGuidedUnwrap follows a PhaseGradientQuality map to unwrap 2-D fields along their most reliable path first.
Fourier-transform profilometry ¶
FTPWrappedPhase (and the fixed-band FTPWrappedPhaseBand) recover phase from a single fringe image by band-pass filtering one sideband in the frequency domain — a one-shot alternative to multi-image phase shifting, implemented with a dependency-free DFT.
Quality maps and masking ¶
ComputeDataModulation, ComputeAmplitude and ComputeBackground turn a phase-shift stack into per-pixel confidence and signal maps. ShadowMask, OverexposureMask and ModulationMask (combined with CombineMasks) reject unlit, saturated and low-contrast pixels before decoding.
Binary vs. Gray encoding ¶
CodePattern generates and decodes column/row codes under a selectable Encoding — reflected Gray (robust, the default) or natural binary — so the two schemes can be compared through an identical pipeline.
Triangulation and stereo ¶
Given a decoded correspondence and calibrated CameraMatrix projection matrices (built with NewPinhole), TriangulatePoint and Triangulate reconstruct 3-D world points into a PointCloud by the linear DLT method. StereoDecode converts two cameras' decodings into projector-referenced StereoMatch correspondences that TriangulateStereo reconstructs. Calibration itself (recovering the intrinsics/pose) is still out of scope; the projection matrices are supplied by the caller.
Pattern export ¶
WritePatternPNG, EncodePatternPNG, SavePatternPNG and SavePatternStack serialize generated pattern stacks to PNG for projection.
Determinism ¶
Everything here is deterministic: pattern generation is a pure function of the GrayCodeParams / Params dimensions, and decoding is a pure function of its inputs. No randomness, no clocks, no goroutines. Identical inputs always yield identical outputs, which is what makes the tests in this package exact.
Visualization ¶
Decoded coordinate maps and phase maps are plain Go slices. CoordMapToMat and PhaseMapToMat scale them into a single-channel github.com/malcolmston/opencv.Mat suitable for saving or display, and MaskToMat renders a boolean mask.
Deferred ¶
The following parts of the OpenCV module remain intentionally NOT implemented:
- Projector-camera calibration. Triangulation is provided (TriangulatePoint, Triangulate, TriangulateStereo) but the projection matrices must be supplied by the caller; this package does not recover intrinsics, distortion or pose from calibration captures.
- Marker-based absolute-phase disambiguation of cv::structured_light::SinusoidalPattern. Absolute phase is instead obtained by temporal unwrapping (MultiFrequencyUnwrap, HeterodyneUnwrap), Gray-code fusion (CombineGrayAndPhase) or FTP (FTPWrappedPhase).
- Lens-distortion modelling; the pinhole CameraMatrix is linear only.
- Reading/writing real projector-camera captures; the "capture" step in the tests is simulated by sampling the generated patterns at a known mapping.
Index ¶
- Constants
- func CombineGrayAndPhase(fringeOrder []int, wrapped []float64) []float64
- func CombineMasks(masks ...[]bool) []bool
- func ComputeAmplitude(captured []*cv.Mat, shift float64) []float64
- func ComputeBackground(captured []*cv.Mat, shift float64) []float64
- func ComputeDataModulation(captured []*cv.Mat, shift float64) []float64
- func CoordMapToMat(coord []int, rows, cols, maxVal int) *cv.Mat
- func EncodePatternPNG(m *cv.Mat) ([]byte, error)
- func FTPWrappedPhase(img *cv.Mat, horizontal bool) []float64
- func FTPWrappedPhaseBand(img *cv.Mat, horizontal bool, carrier, band int) []float64
- func HeterodyneUnwrap(levels []FrequencyPhase, rows, cols int, horizontal bool) ([]float64, error)
- func MaskToMat(mask []bool, rows, cols int) *cv.Mat
- func ModulationMask(mod []float64, minMod float64) []bool
- func MultiFrequencyUnwrap(levels []FrequencyPhase, rows, cols int, horizontal bool) ([]float64, error)
- func NStepWrappedPhase(captured []*cv.Mat, shift float64) []float64
- func OverexposureMask(images []*cv.Mat, sat uint8) []bool
- func PhaseGradientQuality(wrapped []float64, rows, cols int) []float64
- func PhaseMapToMat(phase []float64, rows, cols int) *cv.Mat
- func PhaseToCoord(abs []float64, freq float64, extent int) []float64
- func QualityGuidedUnwrap(wrapped, quality []float64, rows, cols int) []float64
- func SavePatternPNG(path string, m *cv.Mat) error
- func SavePatternStack(dir, prefix string, stack []*cv.Mat) ([]string, error)
- func ShadowMask(white, black *cv.Mat, thresh int) []bool
- func TriangulatePoint(cam, proj CameraMatrix, camX, camY, projCol, projRow float64) [3]float64
- func UnwrapPhaseMap(wrapped []float64, rows, cols int, horizontal bool) []float64
- func WrapPhase(a float64) float64
- func WritePatternPNG(w io.Writer, m *cv.Mat) error
- type CameraMatrix
- type CodePattern
- func (c *CodePattern) Decode(captured []*cv.Mat, white, black *cv.Mat) (*Decoded, error)
- func (c *CodePattern) Generate() []*cv.Mat
- func (c *CodePattern) NumColBits() int
- func (c *CodePattern) NumRowBits() int
- func (c *CodePattern) NumberOfPatternImages() int
- func (c *CodePattern) ReferenceImages() (white, black *cv.Mat)
- type Decoded
- type Encoding
- type FrequencyPhase
- type GrayCodeParams
- type GrayCodePattern
- func (g *GrayCodePattern) Decode(captured []*cv.Mat, white, black *cv.Mat) (*Decoded, error)
- func (g *GrayCodePattern) Generate() []*cv.Mat
- func (g *GrayCodePattern) NumColBits() int
- func (g *GrayCodePattern) NumRowBits() int
- func (g *GrayCodePattern) NumberOfPatternImages() int
- func (g *GrayCodePattern) ReferenceImages() (white, black *cv.Mat)
- type Params
- type PointCloud
- type SinusoidalPattern
- type StereoMatch
Examples ¶
Constants ¶
const ( // DefaultWhiteThreshold is the minimum contrast between a pattern sample // and its inverse for a bit to be considered reliable. DefaultWhiteThreshold = 5 // DefaultBlackThreshold is the minimum contrast between the white and black // reference for a camera pixel to be considered lit (not in shadow). DefaultBlackThreshold = 40 )
Default thresholds used by NewGrayCodePattern; see the GrayCodePattern fields for their meaning. They match the defaults of OpenCV's cv::structured_light::GrayCodePattern.
Variables ¶
This section is empty.
Functions ¶
func CombineGrayAndPhase ¶ added in v0.4.0
CombineGrayAndPhase fuses a coarse integer fringe order (recovered from a Gray-code or binary stack) with a fine wrapped phase (recovered from a phase-shift stack) into a single continuous absolute phase, the standard Gray-code-plus-phase-shift hybrid. For fringe order m and wrapped phase w the absolute phase is w + 2π·k where k = round((2π·m - w)/2π); the rounding makes the fusion robust to half-fringe misregistration between the two stacks. The slices must share a length; it panics otherwise.
func CombineMasks ¶ added in v0.4.0
CombineMasks returns the element-wise logical AND of one or more boolean masks of equal length; a pixel is valid only where every input marks it valid. With no arguments it returns nil. It panics if the masks differ in length.
func ComputeAmplitude ¶ added in v0.4.0
ComputeAmplitude returns the per-pixel modulation amplitude B = (2/N)·√(S²+C²) of an N-step phase-shifted stack, where S and C are the sine/cosine correlations of the captured intensities with the projected phase. It is the contrast of the recovered fringe and, together with ComputeBackground, characterizes signal strength. shift zero selects the canonical 2π/N step. The result is a row-major []float64. It panics on an inconsistent stack.
func ComputeBackground ¶ added in v0.4.0
ComputeBackground returns the per-pixel background (DC / bias) intensity of an N-step phase-shifted stack, i.e. the mean of the captured images. shift zero selects the canonical 2π/N step (the background does not depend on the step, but the argument keeps the quality-metric signatures uniform). The result is a row-major []float64. It panics on an inconsistent stack.
func ComputeDataModulation ¶ added in v0.4.0
ComputeDataModulation returns the per-pixel data modulation (fringe visibility) γ = B/A of an N-step phase-shifted stack, the ratio of the modulation amplitude B to the background A. It lies in [0,1] for a physical capture and is the standard confidence measure for phase-shifting profilometry: near 1 where fringes are crisp, near 0 in shadow or on saturated/textureless surfaces. Pixels with zero background yield 0. shift zero selects the canonical 2π/N step. The result is a row-major []float64. It panics on an inconsistent stack.
func CoordMapToMat ¶
CoordMapToMat renders a decoded coordinate map (one int per camera pixel, -1 for invalid) as a single-channel github.com/malcolmston/opencv.Mat of size rows×cols. Valid coordinates are linearly scaled from [0, maxVal] to [1, 255]; invalid pixels are 0. maxVal must be positive. It panics on a size mismatch or non-positive maxVal.
func EncodePatternPNG ¶ added in v0.4.0
EncodePatternPNG returns the PNG encoding of a pattern image as a byte slice, convenient for embedding a generated pattern in memory or a test. It returns any encoding error.
func FTPWrappedPhase ¶ added in v0.4.0
FTPWrappedPhase decodes a single fringe image into a wrapped phase map using Fourier-transform profilometry — a one-shot alternative to multi-image phase shifting. The carrier fringe frequency is detected automatically from a central line and a proportional sideband is retained. When horizontal is false the fringes are vertical and each image row is transformed along x; when true each column is transformed along y. img may be single- or multi-channel (reduced to luma). The result is a row-major []float64 in (-π, π]; unwrap it (e.g. with UnwrapPhaseMap or QualityGuidedUnwrap) to recover absolute phase.
func FTPWrappedPhaseBand ¶ added in v0.4.0
FTPWrappedPhaseBand is FTPWrappedPhase with an explicit carrier bin and sideband half-width instead of automatic detection, for callers that know the projected fringe frequency or need a tighter band to reject harmonics. carrier and band are in cycles-per-line; band must be at least 1. Orientation and the output convention match FTPWrappedPhase. It panics if carrier<1 or band<1.
func HeterodyneUnwrap ¶ added in v0.4.0
func HeterodyneUnwrap(levels []FrequencyPhase, rows, cols int, horizontal bool) ([]float64, error)
HeterodyneUnwrap unwraps two or three frequencies with the heterodyne (beat) method. Rather than scaling frequency ratios directly, it synthesizes a low-frequency beat by wrapping the difference of two higher-frequency phases; a beat spanning about one fringe over the whole field is unambiguous and guides the finer levels. The levels must be ordered by strictly increasing Frequency and share the length rows*cols. The returned slice is the absolute phase at the highest frequency.
For two levels the beat is wrap(φ_hi − φ_lo) at frequency f_hi−f_lo; it is spatially unwrapped and used to resolve the high frequency. For three levels the beat between the two lowest frequencies resolves the middle frequency, which in turn resolves the highest. It returns an error unless exactly two or three valid, increasing levels are supplied.
func MaskToMat ¶
MaskToMat renders a boolean validity mask as a single-channel github.com/malcolmston/opencv.Mat of size rows×cols, with 255 for true and 0 for false. It panics on a size mismatch.
func ModulationMask ¶ added in v0.4.0
ModulationMask thresholds a data-modulation (or amplitude) map: a pixel is valid (true) when its value is at least minMod. Combine it with ShadowMask and the negation of OverexposureMask via CombineMasks to form the final decoding mask. The result has the length of mod.
func MultiFrequencyUnwrap ¶ added in v0.4.0
func MultiFrequencyUnwrap(levels []FrequencyPhase, rows, cols int, horizontal bool) ([]float64, error)
MultiFrequencyUnwrap performs hierarchical (temporal) phase unwrapping across a set of frequencies, resolving a fine, high-frequency phase whose absolute range far exceeds 2π. The levels must be ordered by strictly increasing Frequency and share the length rows*cols.
The lowest-frequency map is unwrapped spatially with UnwrapPhaseMap to form an unambiguous reference; every higher frequency is then unwrapped per-pixel against the running absolute phase via the fringe-order formula (see FrequencyPhase). Because the higher levels are unwrapped point-by-point, the method tolerates noise and true 2π-per-pixel gradients that defeat a purely spatial unwrap. The returned slice is the absolute phase at the highest frequency.
It returns an error if fewer than two levels are given, the frequencies are not strictly increasing or non-positive, or any map has the wrong length.
Example ¶
ExampleMultiFrequencyUnwrap recovers a fine phase ramp spanning many fringes from coarse and fine wrapped phase maps.
package main
import (
"fmt"
"math"
sl "github.com/malcolmston/opencv/structured_light"
)
func main() {
rows, cols := 1, 100
freqs := []float64{1, 8}
var levels []sl.FrequencyPhase
for _, f := range freqs {
w := make([]float64, rows*cols)
for x := 0; x < cols; x++ {
w[x] = sl.WrapPhase(2 * math.Pi * f * float64(x) / float64(cols))
}
levels = append(levels, sl.FrequencyPhase{Frequency: f, Wrapped: w})
}
abs, err := sl.MultiFrequencyUnwrap(levels, rows, cols, false)
if err != nil {
panic(err)
}
// The absolute phase at freq 8 rises to almost 8·2π ≈ 50 rad, well past 2π.
fmt.Printf("start=%.2f end=%.2f fringes=%.0f\n", abs[0], abs[cols-1], math.Round((abs[cols-1]-abs[0])/(2*math.Pi)))
}
Output: start=0.00 end=49.76 fringes=8
func NStepWrappedPhase ¶ added in v0.4.0
NStepWrappedPhase computes the wrapped phase of an N-step phase-shifted stack with an arbitrary, caller-supplied uniform phase step, generalizing SinusoidalPattern.ComputeWrappedPhase to any N≥3 and any step. The estimator is the standard least-squares solution
φ = atan2( -Σ Iᵢ·sin(i·shift), Σ Iᵢ·cos(i·shift) )
If shift is zero the canonical step 2π/N is used. captured must hold at least three images of identical size; each may be single- or multi-channel (reduced to luma). The result is a row-major []float64 in (-π, π]. It panics on an inconsistent stack.
func OverexposureMask ¶ added in v0.4.0
OverexposureMask flags pixels that are saturated (sample ≥ sat) in any image of a stack; such pixels clip the sinusoid and corrupt the recovered phase, so they are excluded from decoding. Every image must share the size of the first. The result is row-major of length Rows*Cols, true where overexposed. It panics on an empty stack or a size mismatch.
func PhaseGradientQuality ¶ added in v0.4.0
PhaseGradientQuality builds a quality map for a wrapped phase field, suitable for QualityGuidedUnwrap. Each pixel's quality is 1/(1+g), where g is the largest wrapped phase difference to its 4-connected neighbours; smooth regions score near 1 and residue-prone discontinuities score lower. The input is a row-major wrapped phase of length rows*cols; the output is a new slice of the same length. It panics on a size mismatch.
func PhaseMapToMat ¶
PhaseMapToMat renders a phase map (one float per pixel) as a single-channel github.com/malcolmston/opencv.Mat of size rows×cols, min-max normalized to the full 0..255 range. A constant map maps to all zeros. It panics on a size mismatch.
func PhaseToCoord ¶ added in v0.4.0
PhaseToCoord converts an absolute (unwrapped) phase map into projector coordinates. For a fringe pattern carrying freq full periods across an image of the given extent (Width for vertical fringes, Height for horizontal), the absolute phase φ at a pixel corresponds to the projector coordinate φ·extent/(2π·freq). The result is a new []float64 of the same length. It panics if freq is not positive or extent is not positive.
func QualityGuidedUnwrap ¶ added in v0.4.0
QualityGuidedUnwrap unwraps a 2-D wrapped phase map by flood-filling outward from the highest-quality pixel, always extending the unwrapped region through its best-quality frontier pixel next. Following high-quality (smooth) paths first and only crossing residues last is what lets this method recover fields whose absolute range far exceeds 2π where a naive line-by-line unwrap would propagate a single bad step across a whole row.
wrapped and quality are row-major of length rows*cols; higher quality means a more trustworthy pixel (see PhaseGradientQuality). Each pixel is unwrapped relative to the neighbour it was reached from using the nearest-2π rule. The result is a new continuous absolute phase map, fixed by setting the seed pixel equal to its wrapped value; add a constant to align it to a reference. It panics on a size mismatch.
func SavePatternPNG ¶ added in v0.4.0
SavePatternPNG writes a single pattern image to path as a PNG file, creating or truncating it. It returns any file or encoding error.
func SavePatternStack ¶ added in v0.4.0
SavePatternStack writes an entire pattern stack to dir as zero-padded PNG files named "<prefix>NN.png" (for example projector patterns from GrayCodePattern.Generate or SinusoidalPattern.Generate). The directory is created if necessary. It returns the paths written, in stack order, and any error.
func ShadowMask ¶ added in v0.4.0
ShadowMask builds a boolean lit/shadow mask from an all-white and an all-black reference capture: a pixel is lit (true) when white−black exceeds thresh. This is the robust ambient-light rejection used before decoding a Gray-code or phase stack. The references must be single- or multi-channel of identical size. The result is row-major of length Rows*Cols. It panics on a size mismatch.
func TriangulatePoint ¶ added in v0.4.0
func TriangulatePoint(cam, proj CameraMatrix, camX, camY, projCol, projRow float64) [3]float64
TriangulatePoint reconstructs the world point observed at camera pixel (camX, camY) and projector pixel (projCol, projRow) from the two projection matrices, using the linear (DLT) method: each 2-D observation contributes two rows to a homogeneous system A·X=0, and X is the unit null vector of A found as the smallest-eigenvalue eigenvector of AᵀA. The homogeneous result is dehomogenized to Euclidean (X, Y, Z). Coordinates may be fractional, so this works directly with sub-pixel phase-derived correspondences.
Example ¶
ExampleTriangulatePoint reconstructs a 3-D point from its camera and projector projections under a synthetic calibration.
package main
import (
"fmt"
sl "github.com/malcolmston/opencv/structured_light"
)
func main() {
k := [3][3]float64{{800, 0, 320}, {0, 800, 240}, {0, 0, 1}}
id := [3][3]float64{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}
cam := sl.NewPinhole(k, id, [3]float64{0, 0, 0})
proj := sl.NewPinhole(k, id, [3]float64{-0.2, 0, 0})
world := [3]float64{0.1, -0.05, 1.2}
uc, vc := cam.Project(world)
up, vp := proj.Project(world)
got := sl.TriangulatePoint(cam, proj, uc, vc, up, vp)
fmt.Printf("%.3f %.3f %.3f\n", got[0], got[1], got[2])
}
Output: 0.100 -0.050 1.200
func UnwrapPhaseMap ¶
UnwrapPhaseMap removes the 2π discontinuities of a wrapped phase map by a simple line-by-line spatial unwrap along the fringe direction. For vertical fringes (horizontal == false) each image row is unwrapped left-to-right; for horizontal fringes each image column is unwrapped top-to-bottom. The input is a row-major []float64 of length rows*cols in (-π, π]; the output is a new slice of continuous absolute phase.
This is exact for a clean monotonic phase ramp but is not quality-guided; see the package Deferred list.
func WrapPhase ¶ added in v0.4.0
WrapPhase brings an arbitrary phase angle into the canonical interval (-π, π]. It is the inverse notion of phase unwrapping: whereas unwrapping removes 2π jumps, WrapPhase reintroduces the principal-value wrap. It is used throughout the multi-frequency and hybrid routines to compare phases modulo 2π.
func WritePatternPNG ¶ added in v0.4.0
WritePatternPNG encodes a single pattern image as a PNG and writes it to w. The Mat is rendered through its standard-library image form (grayscale for a single channel, RGB otherwise). It returns any encoding or write error.
Types ¶
type CameraMatrix ¶ added in v0.4.0
type CameraMatrix struct {
// P is the row-major 3×4 projection matrix.
P [3][4]float64
}
CameraMatrix is a 3×4 pinhole projection matrix P mapping a homogeneous world point [X Y Z 1]ᵀ to homogeneous image coordinates P·[X Y Z 1]ᵀ. Build one from intrinsics and a pose with NewPinhole.
func NewPinhole ¶ added in v0.4.0
func NewPinhole(k, r [3][3]float64, t [3]float64) CameraMatrix
NewPinhole assembles a CameraMatrix as P = K·[R|t], where K is the 3×3 intrinsic matrix, R the 3×3 world-to-camera rotation, and t the translation. The projector in a structured-light rig is modelled as a second pinhole "camera" with its own K, R, t.
func (CameraMatrix) Project ¶ added in v0.4.0
func (c CameraMatrix) Project(x [3]float64) (u, v float64)
Project maps a world point to its pixel coordinates (u, v) under this camera, performing the perspective divide. The result is undefined for points on the camera plane (zero homogeneous depth).
type CodePattern ¶ added in v0.4.0
type CodePattern struct {
// Width and Height are the encoded projector resolution.
Width, Height int
// Encoding selects the coordinate-to-bit mapping.
Encoding Encoding
// WhiteThreshold is the minimum pattern/inverse contrast for a trusted bit.
WhiteThreshold int
// BlackThreshold is the minimum white/black contrast for a lit pixel.
BlackThreshold int
// contains filtered or unexported fields
}
CodePattern generates and decodes a column/row-encoding binary pattern set under a selectable Encoding (reflected Gray or natural binary), letting a caller compare the two schemes with an otherwise identical pipeline. The image layout, reference pair and decoded output match GrayCodePattern; only the coordinate-to-bits mapping changes. Construct it with NewCodePattern.
Example ¶
ExampleCodePattern shows selecting the natural-binary encoding instead of the default reflected Gray code.
package main
import (
"fmt"
sl "github.com/malcolmston/opencv/structured_light"
)
func main() {
c := sl.NewCodePattern(sl.GrayCodeParams{Width: 32, Height: 16}, sl.EncodingBinary)
fmt.Printf("encoding=%s images=%d\n", c.Encoding, c.NumberOfPatternImages())
}
Output: encoding=binary images=18
func NewCodePattern ¶ added in v0.4.0
func NewCodePattern(p GrayCodeParams, enc Encoding) *CodePattern
NewCodePattern returns a CodePattern for the given resolution and encoding with the default robust-bit and shadow thresholds. It panics if either dimension is smaller than 2.
func (*CodePattern) Decode ¶ added in v0.4.0
Decode recovers the camera→projector correspondence from a captured stack, mirroring GrayCodePattern.Decode but honouring the pattern's Encoding when converting recovered code words back to coordinates. captured must hold exactly CodePattern.NumberOfPatternImages images in generation order; white and black are the captured references. It returns an error on inconsistent sizes.
func (*CodePattern) Generate ¶ added in v0.4.0
func (c *CodePattern) Generate() []*cv.Mat
Generate returns the pattern stack, column bits first then row bits, each bit immediately followed by its photometric inverse — the same order and 0/255 convention as GrayCodePattern.Generate, but using the selected encoding.
func (*CodePattern) NumColBits ¶ added in v0.4.0
func (c *CodePattern) NumColBits() int
NumColBits returns the number of code bits used to encode a projector column.
func (*CodePattern) NumRowBits ¶ added in v0.4.0
func (c *CodePattern) NumRowBits() int
NumRowBits returns the number of code bits used to encode a projector row.
func (*CodePattern) NumberOfPatternImages ¶ added in v0.4.0
func (c *CodePattern) NumberOfPatternImages() int
NumberOfPatternImages returns 2*(NumColBits+NumRowBits), the number of images CodePattern.Generate produces (a pattern and its inverse per bit), excluding the white/black reference pair.
func (*CodePattern) ReferenceImages ¶ added in v0.4.0
func (c *CodePattern) ReferenceImages() (white, black *cv.Mat)
ReferenceImages returns the all-white and all-black reference patterns, as in GrayCodePattern.ReferenceImages.
type Decoded ¶
type Decoded struct {
// Rows and Cols are the camera image dimensions.
Rows, Cols int
// Col holds, for each camera pixel, the decoded projector column, or -1 if
// the pixel is invalid (in shadow or with an unreliable bit).
Col []int
// Row holds, for each camera pixel, the decoded projector row, or -1 if the
// pixel is invalid.
Row []int
// Mask reports which camera pixels decoded to a valid projector coordinate.
Mask []bool
}
Decoded holds the result of GrayCodePattern.Decode. All slices are indexed in row-major order (index = y*Cols + x) over the camera image.
type Encoding ¶ added in v0.4.0
type Encoding int
Encoding selects how a CodePattern maps a projector coordinate to the bit sequence projected for it.
const ( // EncodingGray uses binary reflected Gray code: adjacent coordinates differ // in exactly one projected bit, so a single mis-decoded bit can only move the // result to an adjacent coordinate. This is the robust default and matches // [GrayCodePattern]. EncodingGray Encoding = iota // EncodingBinary uses the natural binary representation of the coordinate. // It is simpler and occasionally requested for teaching or for comparison, // but a single bad bit can cause a large coordinate error, so it is less // robust than Gray code in practice. EncodingBinary )
type FrequencyPhase ¶ added in v0.4.0
type FrequencyPhase struct {
// Frequency is the number of sinusoid periods the pattern spanned.
Frequency float64
// Wrapped is the row-major wrapped phase map for that frequency.
Wrapped []float64
}
FrequencyPhase pairs a wrapped phase map with the number of full fringe periods (Frequency) its projected pattern spanned across the varying image direction. It is the unit of input to the temporal phase-unwrapping routines MultiFrequencyUnwrap and HeterodyneUnwrap. Wrapped is a row-major []float64 in (-π, π]; Frequency must be positive.
type GrayCodeParams ¶
type GrayCodeParams struct {
// Width is the number of projector columns to encode.
Width int
// Height is the number of projector rows to encode.
Height int
}
GrayCodeParams configures the projector resolution a GrayCodePattern encodes. Width and Height are the projector's column and row counts; both must be at least 2.
type GrayCodePattern ¶
type GrayCodePattern struct {
// Width and Height are the encoded projector resolution.
Width, Height int
// WhiteThreshold is the minimum absolute difference between a pattern
// sample and its inverse for the corresponding bit to be trusted during
// decoding. Pixels with lower contrast are marked invalid.
WhiteThreshold int
// BlackThreshold is the minimum difference between the white and black
// reference samples for a camera pixel to be treated as lit. Pixels below
// it are considered shadow and marked invalid.
BlackThreshold int
// contains filtered or unexported fields
}
GrayCodePattern generates and decodes a binary reflected Gray-code pattern set for a projector of the configured resolution. Construct it with NewGrayCodePattern. The zero value is not usable.
Example ¶
ExampleGrayCodePattern shows generating a Gray-code stack, simulating a capture with an identity camera→projector mapping, and decoding it back.
package main
import (
"fmt"
cv "github.com/malcolmston/opencv"
sl "github.com/malcolmston/opencv/structured_light"
)
func main() {
g := sl.NewGrayCodePattern(sl.GrayCodeParams{Width: 32, Height: 16})
fmt.Println("pattern images:", g.NumberOfPatternImages())
patterns := g.Generate()
// Simulate a capture: the camera sees the projector directly (identity),
// so a pixel's decoded coordinate equals its own position.
rows, cols := 16, 32
captured := make([]*cv.Mat, len(patterns))
for i, p := range patterns {
captured[i] = p.Clone()
}
white, black := g.ReferenceImages()
dec, err := g.Decode(captured, white, black)
if err != nil {
panic(err)
}
col, row, ok := dec.At(7, 20)
fmt.Printf("pixel (20,7) -> col=%d row=%d valid=%v\n", col, row, ok)
_ = rows
_ = cols
}
Output: pattern images: 18 pixel (20,7) -> col=20 row=7 valid=true
func NewGrayCodePattern ¶
func NewGrayCodePattern(p GrayCodeParams) *GrayCodePattern
NewGrayCodePattern returns a GrayCodePattern for the given projector resolution with the default robust-bit and shadow thresholds. It panics if either dimension is smaller than 2.
func (*GrayCodePattern) Decode ¶
Decode recovers the camera→projector correspondence from a captured pattern stack. captured must contain exactly GrayCodePattern.NumberOfPatternImages images in the order produced by GrayCodePattern.Generate; white and black are the captured references. Every image must share the camera resolution and be single-channel or convertible to grayscale.
For each camera pixel Decode:
- marks the pixel lit only if white-black exceeds BlackThreshold (shadow masking);
- decides each Gray-code bit by comparing the pattern with its inverse, marking the pixel invalid if their contrast is below WhiteThreshold (the robust-bit test);
- converts the recovered Gray codes to binary projector column and row, marking the pixel invalid if either falls outside [0,Width)/[0,Height).
It returns an error if the stack size or image dimensions are inconsistent.
func (*GrayCodePattern) Generate ¶
func (g *GrayCodePattern) Generate() []*cv.Mat
Generate returns the full stack of projection pattern images, each a single-channel github.com/malcolmston/opencv.Mat of size Height×Width whose samples are 0 or 255. The layout is column bits first then row bits, each bit immediately followed by its photometric inverse:
[col0 col0inv col1 col1inv ... row0 row0inv row1 row1inv ...]
GrayCodePattern.Decode expects a captured stack in exactly this order.
func (*GrayCodePattern) NumColBits ¶
func (g *GrayCodePattern) NumColBits() int
NumColBits returns the number of Gray-code bits used to encode a projector column (before inverses are added).
func (*GrayCodePattern) NumRowBits ¶
func (g *GrayCodePattern) NumRowBits() int
NumRowBits returns the number of Gray-code bits used to encode a projector row (before inverses are added).
func (*GrayCodePattern) NumberOfPatternImages ¶
func (g *GrayCodePattern) NumberOfPatternImages() int
NumberOfPatternImages returns the number of pattern images GrayCodePattern.Generate produces, namely 2*(NumColBits+NumRowBits): a pattern and an inverse for every column and row bit. It does not count the white/black reference pair.
func (*GrayCodePattern) ReferenceImages ¶
func (g *GrayCodePattern) ReferenceImages() (white, black *cv.Mat)
ReferenceImages returns the fully-lit (white, all 255) and fully-dark (black, all 0) reference patterns of size Height×Width. The captured versions of these two images are passed to GrayCodePattern.Decode to build the shadow mask and to measure per-pixel contrast.
type Params ¶
type Params struct {
// Width and Height are the projected image dimensions in pixels.
Width, Height int
// NumOfPatternImages is the number of phase-shifted fringe images (N). It
// must be at least 3; three is the minimum for an unambiguous three-unknown
// (offset, amplitude, phase) solution.
NumOfPatternImages int
// Shift is the phase step, in radians, between consecutive pattern images.
// If zero, the canonical uniform step 2π/N is used.
Shift float64
// Frequency is the number of full sinusoid periods (fringes) across the
// varying direction of the image. It must be at least 1.
Frequency int
// Horizontal selects the fringe orientation. When false (the default) the
// fringes are vertical and the phase varies along x (columns); when true the
// fringes are horizontal and the phase varies along y (rows).
Horizontal bool
}
Params configures a SinusoidalPattern (N-step phase-shifting profilometry).
type PointCloud ¶ added in v0.4.0
type PointCloud struct {
// Points holds the reconstructed world coordinates.
Points [][3]float64
// PixelX and PixelY are the source camera pixel for each point.
PixelX []int
PixelY []int
}
PointCloud is the result of triangulating a decoded correspondence field. The three slices are parallel: Points[i] is the reconstructed 3-D world point seen at camera pixel (PixelX[i], PixelY[i]).
func Triangulate ¶ added in v0.4.0
func Triangulate(dec *Decoded, cam, proj CameraMatrix) *PointCloud
Triangulate reconstructs a PointCloud from a Gray-code/phase decoding by triangulating every valid camera pixel against its decoded projector pixel. The camera pixel position (x, y) and the decoded projector coordinate (Col, Row) supply the two views for TriangulatePoint. Pixels that are invalid in dec.Mask are skipped. cam and proj are the calibrated projection matrices of the camera and projector.
func TriangulateStereo ¶ added in v0.4.0
func TriangulateStereo(matches []StereoMatch, leftCam, rightCam CameraMatrix) *PointCloud
TriangulateStereo reconstructs world points from [StereoMatch]es using two calibrated camera projection matrices. Each match's left and right pixels are the two views passed to TriangulatePoint. The returned PointCloud records each point together with its left-camera pixel.
func (*PointCloud) Len ¶ added in v0.4.0
func (pc *PointCloud) Len() int
Len returns the number of reconstructed points.
type SinusoidalPattern ¶
type SinusoidalPattern struct {
// contains filtered or unexported fields
}
SinusoidalPattern generates phase-shifted sinusoidal fringe patterns and decodes them into a wrapped phase map. Construct it with NewSinusoidalPattern. The zero value is not usable.
Example ¶
ExampleSinusoidalPattern shows generating fringe patterns and recovering the unwrapped absolute phase, which is proportional to the projector column.
package main
import (
"fmt"
sl "github.com/malcolmston/opencv/structured_light"
)
func main() {
s := sl.NewSinusoidalPattern(sl.Params{
Width: 64,
Height: 2,
NumOfPatternImages: 4,
Frequency: 2,
})
patterns := s.Generate()
wrapped := s.ComputeWrappedPhase(patterns)
abs := sl.UnwrapPhaseMap(wrapped, 2, 64, false)
// Absolute phase rises monotonically across the row.
p0 := abs[0]
if p0 > -0.005 && p0 < 0.005 {
p0 = 0
}
fmt.Printf("phase[0]=%.2f increasing=%v\n", p0, abs[10] > abs[0] && abs[63] > abs[10])
}
Output: phase[0]=0.00 increasing=true
func NewSinusoidalPattern ¶
func NewSinusoidalPattern(params Params) *SinusoidalPattern
NewSinusoidalPattern validates params and returns a SinusoidalPattern. It panics if the dimensions are non-positive, NumOfPatternImages < 3, or Frequency < 1.
func (*SinusoidalPattern) ComputeWrappedPhase ¶
func (s *SinusoidalPattern) ComputeWrappedPhase(captured []*cv.Mat) []float64
ComputeWrappedPhase recovers the wrapped phase map from a captured stack of N phase-shifted images using the standard N-step estimator
φ = atan2( -Σ Iᵢ·sin(i·Shift), Σ Iᵢ·cos(i·Shift) )
The result is a row-major []float64 of length Rows*Cols with values in (-π, π]. captured must hold exactly NumOfPatternImages images of identical size; each may be single-channel or convertible to grayscale. It panics if the stack size or dimensions are inconsistent.
func (*SinusoidalPattern) Generate ¶
func (s *SinusoidalPattern) Generate() []*cv.Mat
Generate returns NumOfPatternImages single-channel fringe images of size Height×Width. Image i has intensity 127.5·(1 + cos(φ + i·Shift)), where φ is the reference phase along the varying direction. Samples are rounded to the 0..255 range.
func (*SinusoidalPattern) Params ¶
func (s *SinusoidalPattern) Params() Params
Params returns a copy of the configuration the pattern was built with.
func (*SinusoidalPattern) PhaseShift ¶
func (s *SinusoidalPattern) PhaseShift() float64
PhaseShift returns the effective phase step in radians between consecutive pattern images.
type StereoMatch ¶ added in v0.4.0
type StereoMatch struct {
// LeftX, LeftY is the pixel in the left camera.
LeftX, LeftY int
// RightX, RightY is the pixel in the right camera.
RightX, RightY int
// Col, Row is the shared projector coordinate.
Col, Row int
}
StereoMatch is one camera-to-camera correspondence recovered by decoding the same projector patterns in two cameras: the left and right camera pixels that both saw the same projector pixel. Such matches are the input to stereo triangulation, and unlike raw block matching they are dense and unambiguous because the projector coordinate is a global label.
func StereoDecode ¶ added in v0.4.0
func StereoDecode(left, right *Decoded) []StereoMatch
StereoDecode converts two independently-decoded correspondence fields (from the left and right cameras viewing the same projector sequence) into a list of left↔right pixel matches that share a projector coordinate. It builds an index from projector (Col,Row) to the first valid right-camera pixel — scanning the right field in row-major order so the choice is deterministic — then emits a match for every valid left-camera pixel whose projector coordinate is present. The matches are returned in left-camera row-major order.
This is the projector-as-common-reference form of two-camera structured light: the two cameras need not be rectified and no window search is performed.