Skip to content

Commit 67491e4

Browse files
authored
Merge pull request #20 from FugroRoames/perspective
The perspective transformation
2 parents 9b92f28 + afd1738 commit 67491e4

File tree

7 files changed

+120
-4
lines changed

7 files changed

+120
-4
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,27 @@ defined by a composition of a translation and a linear transformation. An
170170
(and controllable) if you construct it from a composition of a linear map
171171
and a translation, e.g. `Translation(v) ∘ LinearMap(v)` (or any combination of
172172
`LinearMap`, `Translation` and `AffineMap`).
173+
174+
#### Perspective transformations
175+
176+
The perspective transformation maps real-space coordinates to those on a virtual
177+
"screen" of one lesser dimension. For instance, this process is used to render
178+
3D scenes to 2D images in computer generated graphics and games. It is an ideal
179+
model of how a pinhole camera operates and is a good approximation of the modern
180+
photography process.
181+
182+
The `PerspectiveMap()` command creates a `Transformation` to perform the
183+
projective mapping. It can be applied individually, but is particularly
184+
powerful when composed with an `AffineMap` containing the position and
185+
orientation of the camera in your scene. For example, to transfer `points` in 3D
186+
space to 2D `screen_points` giving their projected locations on a virtual camera
187+
image, you might use the following code:
188+
189+
```julia
190+
cam_transform = PerspectiveMap() inv(AffineMap(cam_rotation, cam_position))
191+
screen_points = map(cam_transform, points)
192+
```
193+
194+
There is also a `cameramap()` convenience function that can create a composed
195+
transformation that includes the intrinsic scaling (e.g. focal length and pixel
196+
size) and offset (defining which pixel is labeled `(0,0)`) of an imaging system.

src/CoordinateTransformations.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ export SphericalFromCartesian, CartesianFromSpherical,
2828
# Common transformations
2929
export AbstractAffineMap
3030
export AffineMap, LinearMap, Translation
31+
export PerspectiveMap, cameramap
3132

3233
include("core.jl")
3334
include("coordinatesystems.jl")
3435
include("affine.jl")
36+
include("perspective.jl")
3537

3638
# Deprecations
3739
export transform

src/affine.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ abstract AbstractAffineMap <: Transformation
88
Construct the `Translation` transformation for translating Cartesian points by
99
an offset `v = (dx, dy, ...)`
1010
"""
11-
immutable Translation{V <: AbstractVector} <: AbstractAffineMap
11+
immutable Translation{V} <: AbstractAffineMap
1212
v::V
1313
end
1414
Translation(x::Tuple) = Translation(SVector(x))
@@ -39,9 +39,9 @@ end
3939
LinearMap(M)
4040
4141
A general linear transformation, constructed using `LinearMap(M)`
42-
for any `AbstractMatrix` `M`.
42+
for any matrix-like object `M`.
4343
"""
44-
immutable LinearMap{M <: AbstractMatrix} <: AbstractAffineMap
44+
immutable LinearMap{M} <: AbstractAffineMap
4545
m::M
4646
end
4747
Base.show(io::IO, trans::LinearMap) = print(io, "LinearMap($(trans.m))") # TODO make this output more petite
@@ -95,7 +95,7 @@ converted into an affine approximation by linearizing about a point `x` using
9595
9696
For transformations which are already affine, `x` may be omitted.
9797
"""
98-
immutable AffineMap{M <: AbstractMatrix, V <: AbstractVector} <: AbstractAffineMap
98+
immutable AffineMap{M, V} <: AbstractAffineMap
9999
m::M
100100
v::V
101101
end

src/core.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ Base.show(io::IO, trans::ComposedTransformation) = print(io, "($(trans.t1) ∘ $
3737
trans.t1(trans.t2(x))
3838
end
3939

40+
function Base.:(==)(trans1::ComposedTransformation, trans2::ComposedTransformation)
41+
(trans1.t1 == trans2.t1) && (trans1.t2 == trans2.t2)
42+
end
43+
44+
function Base.isapprox(trans1::ComposedTransformation, trans2::ComposedTransformation; kwargs...)
45+
isapprox(trans1.t1, trans2.t1; kwargs...) && isapprox(trans1.t2, trans2.t2; kwargs...)
46+
end
47+
48+
4049
"""
4150
compose(trans1, trans2)
4251
trans1 ∘ trans2

src/perspective.jl

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""
2+
PerspectiveMap()
3+
4+
Construct a perspective transformation. The persepective transformation takes,
5+
e.g., a point in 3D space and "projects" it onto a 2D virtual screen of an ideal
6+
pinhole camera (at distance `1` away from the camera). The camera is oriented
7+
towards the positive-Z axis (or in general, along the final dimension) and the
8+
sign of the `x` and `y` components is preserved for objects in front of the
9+
camera (objects behind the camera are also projected and therefore inverted - it
10+
is up to the user to cull these as necessary).
11+
12+
This transformation is designed to be used in composition with other coordinate
13+
transformations, defining e.g. the position and orientation of the camera. For
14+
example:
15+
16+
cam_transform = PerspectiveMap() ∘ inv(AffineMap(cam_rotation, cam_position))
17+
screen_points = map(cam_transform, points)
18+
19+
(see also `cameramap`)
20+
"""
21+
immutable PerspectiveMap <: Transformation
22+
end
23+
24+
function (::PerspectiveMap)(v::AbstractVector)
25+
scale = 1/v[end]
26+
return [v[i] * scale for i in 1:length(v)-1]
27+
end
28+
29+
@inline function (::PerspectiveMap)(v::StaticVector)
30+
return pop(v) * inv(v[end])
31+
end
32+
33+
Base.isapprox(::PerspectiveMap, ::PerspectiveMap; kwargs...) = true
34+
35+
"""
36+
cameramap()
37+
cameramap(scale)
38+
cameramap(scale, offset)
39+
40+
Create a transformation that takes points in real space (e.g. 3D) and projects
41+
them through a perspective transformation onto the focal plane of an ideal
42+
(pinhole) camera with the given properties.
43+
44+
The `scale` sets the scale of the screen. For a standard digital camera, this
45+
would be `scale = focal_length / pixel_size`. Non-square pixels are supported
46+
by providing a pair of scales in a tuple, `scale = (scale_x, scale_y)`. Positive
47+
scales represent a camera looking in the +z axis with a virtual screen in front
48+
of the camera (the x,y coordinates are not inverted compared to 3D space). Note
49+
that points behind the camera (with negative z component) will be projected
50+
(and inverted) onto the image coordinates and it is up to the user to cull
51+
such points as necessary.
52+
53+
The `offset = (offset_x, offset_y)` is used to define the origin in the imaging
54+
plane. For instance, you may wish to have the point (0,0) represent the top-left
55+
corner of your imaging sensor. This measurement is in the units after applying
56+
`scale` (e.g. pixels).
57+
58+
(see also `PerspectiveMap`)
59+
"""
60+
cameramap() = PerspectiveMap()
61+
cameramap(scale::Number) =
62+
LinearMap(UniformScaling(scale)) PerspectiveMap()
63+
cameramap(scale::Tuple{Number, Number}) =
64+
LinearMap(@SMatrix([scale[1] 0; 0 scale[2]])) PerspectiveMap()
65+
cameramap(scale::Number, offset::Tuple{Number,Number}) =
66+
AffineMap(UniformScaling(scale), SVector(-offset[1], -offset[2])) PerspectiveMap()
67+
cameramap(scale::Tuple{Number, Number}, offset::Tuple{Number,Number}) =
68+
AffineMap(@SMatrix([scale[1] 0; 0 scale[2]]), SVector(-offset[1], -offset[2])) PerspectiveMap()
69+

test/perspective.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@testset "Perspective transformation" begin
2+
@test PerspectiveMap()([2.0, -1.0, 0.5]) [4.0, -2.0]
3+
4+
@test cameramap(2.0) LinearMap(UniformScaling(2.0)) PerspectiveMap()
5+
@test cameramap((1.1,2.2)) LinearMap([1.1 0; 0 2.2]) PerspectiveMap()
6+
@test cameramap(1.1, (3.3,4.4)) Translation([-3.3,-4.4]) LinearMap(UniformScaling(1.1)) PerspectiveMap()
7+
@test cameramap((1.1,2.2), (3.3,4.4)) Translation([-3.3,-4.4]) LinearMap([1.1 0; 0 2.2]) PerspectiveMap()
8+
end

test/runtests.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ using Base.Test
33
using ForwardDiff: Dual, partials
44
using StaticArrays
55

6+
# See https://github.com/JuliaLang/julia/issues/18858
7+
Base.isapprox(a::UniformScaling, b::UniformScaling; kwargs...) = isapprox(a.λ, b.λ; kwargs...)
8+
69
@testset "CoordinateTransformations" begin
710

811
include("core.jl")
912
include("coordinatesystems.jl")
1013
include("affine.jl")
14+
include("perspective.jl")
1115

1216
end

0 commit comments

Comments
 (0)