explains conformal mapping, support up to 4 mappings

This commit is contained in:
wangziao 2025-04-30 00:38:35 -07:00
parent 34c677e747
commit 47cbc06ce9
8 changed files with 62 additions and 24 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
grid.mp4

Binary file not shown.

BIN
grid_anti.mp4 Normal file

Binary file not shown.

BIN
grid_exp.mp4 Normal file

Binary file not shown.

BIN
grid_holo.mp4 Normal file

Binary file not shown.

BIN
grid_log.mp4 Normal file

Binary file not shown.

View File

@ -1,16 +1,23 @@
from PIL import Image from PIL import Image
import imageio import imageio
import math
# Function to map (x, y) to (u, v) # Function to map (x, y) to (u, v)
def map_function(x, y, scale_s, scale_t, mapparam): def map_function(x, y, map_type):
# You can define your mapping function here. if map_type == "holo":
# For example, this function scales and shifts the coordinates: u = x / (x*x+y*y)
u = mapparam * (scale_s*scale_t) * x / (x*x+y*y) v = -y / (x*x+y*y)
v = mapparam * (scale_s*scale_t) * y / (x*x+y*y) elif map_type == "antiholo":
u = x / (x*x+y*y)
v = y / (x*x+y*y)
elif map_type == "exp":
u, v = math.exp(x)*math.cos(y), math.exp(x)*math.sin(y)
elif map_type == "log":
u, v = math.log(x*x+y*y), math.atan(y/(x+1e-9))
return u, v return u, v
def create_frames(stepsize,N_pics,w,h,wt,ht,mapparam): def create_frames(stepsize,N_pics,w,h,wt,ht,s_in,s_out,map_type):
default_color = (0, 0, 0) default_color = (0, 0, 0)
output_images = [] output_images = []
scale_s = min(w, h) scale_s = min(w, h)
@ -29,7 +36,8 @@ def create_frames(stepsize,N_pics,w,h,wt,ht,mapparam):
if realx==0 and realy==0: if realx==0 and realy==0:
output_image.putpixel((x,y), default_color) output_image.putpixel((x,y), default_color)
continue continue
realu, realv = map_function(realx, realy, scale_s, scale_t, mapparam) realu, realv = map_function(realx*s_in/scale_s, realy*s_in/scale_s, map_type)
realu, realv = realu*s_out*scale_t, realv*s_out*scale_t
u, v = realu + wt//2, realv + ht//2 u, v = realu + wt//2, realv + ht//2
# Interpolation (you can use different methods, such as bilinear) # Interpolation (you can use different methods, such as bilinear)
@ -77,7 +85,13 @@ if __name__ == "__main__":
parser.add_argument('--output_height', type=int, default=400) parser.add_argument('--output_height', type=int, default=400)
parser.add_argument('--num_frames', type=int, default=60) parser.add_argument('--num_frames', type=int, default=60)
parser.add_argument('--x_shift_per_frame', type=int, nargs="?") parser.add_argument('--x_shift_per_frame', type=int, nargs="?")
parser.add_argument('--mapping_param', type=float, default=0.05) parser.add_argument('--scale_in', type=float, default=1, help="multiply to the input to the mapping,\
default range [-0.5, 0.5] x [-0.5, 0.5]")
parser.add_argument('--scale_out', type=float, default=0.05, help="multiply to the output of the mapping,\
those out of range [-0.5, 0.5] x [-0.5, 0.5] is discarded")
parser.add_argument('--map_type',type=str,choices=["holo","antiholo","exp","log"],default="antiholo",\
help="holo: holomorphic mapping for f(z) = 1/z; antiholo: mapping for f(z) = 1/conj(z);\
exp: holomorphic mapping for f(z) = exp(z); log: f(z) = log(z)")
args = parser.parse_args() args = parser.parse_args()
# Load your texture image # Load your texture image
@ -88,12 +102,11 @@ if __name__ == "__main__":
# Set the width and height of the texture image # Set the width and height of the texture image
wt, ht = texture_image.width, texture_image.height wt, ht = texture_image.width, texture_image.height
mapparam = args.mapping_param
N_pics = args.num_frames N_pics = args.num_frames
if args.x_shift_per_frame==None: if args.x_shift_per_frame==None:
stepsize = wt / N_pics # one whole period stepsize = wt / N_pics # one whole period
else: else:
stepsize = args.x_shift_per_frame stepsize = args.x_shift_per_frame
output_images = create_frames(stepsize,N_pics,w,h,wt,ht,mapparam) output_images = create_frames(stepsize,N_pics,w,h,wt,ht,args.scale_in,args.scale_out,args.map_type)
imageio.mimsave(args.output_file, output_images) imageio.mimsave(args.output_file, output_images)

View File

@ -2,19 +2,10 @@
## What is the mapping? ## What is the mapping?
``` 1. Output image coordinates (x_img,y_img) normalized to [-0.5, 0.5] * [-0.5, 0.5] are multiplied by scale_in to obtain (x,y)
u = mapparam * (scale_s*scale_t) * x / (x*x+y*y) 2. (x,y) is mapped to (u,v) using a conformal or similar mapping.
v = mapparam * (scale_s*scale_t) * y / (x*x+y*y) 3. (u,v) is multiplied by scale_out, and then mapped to (u_tex, v_tex) such that the part inside [-0.5, 0.5] * [-0.5, 0.5] corresponds to the entire rectangular texture.
``` 4. If (u_tex, v_tex) is a valid coordinate in the input texture, we output the (filtered) texel color, otherwise, we output the default color black.
is equivalent to the normalized version:
```
u/scale_t = mapparam * (x/scale_s) / ((x/scale_s)**2 + (y/scale_s)**2)
v/scale_t = mapparam * (y/scale_s) / ((x/scale_s)**2 + (y/scale_s)**2)
```
This explains why there is a "hole" in the output image: small normalized x,y maps to big normalized u,v. If the normalized u,v has an absolute value greater than 0.5, there is no corresponding pixel in the texture input file, we output the default color black.
## How to get the example results ## How to get the example results
@ -22,4 +13,38 @@ Environment: Python 3.11. Do `pip install -r requirements.txt` to get necessary
`python map2video.py color_rotate.png color_rotate.gif --num_frames 40` `python map2video.py color_rotate.png color_rotate.gif --num_frames 40`
`python map2video.py grid.jpg grid.mp4 --num_frames 40` `python map2video.py grid.jpg grid_holo.mp4 --num_frames 40 --map_type holo`
`python map2video.py grid.jpg grid_anti.mp4 --num_frames 40 --map_type antiholo`
`python map2video.py grid.jpg grid_exp.mp4 --num_frames 40 --map_type exp --scale_out 0.25 --scale_in 6.28`
`python map2video.py grid.jpg grid_log.mp4 --num_frames 40 --map_type log --scale_out 0.159 --scale_in 4`
The scale in and scale out parameters are chosen so that combining the two mapping should give the identity mapping.
## 关于标题“保角变换”
保角变换就是保持变换前后角度不变的变换。
前面提到、代码中实现的变换是反演变换([Inversion Transformation](https://en.wikipedia.org/wiki/Inversion_transformation)),和保角变换([Conformal Mapping](https://en.wikipedia.org/wiki/Conformal_map))很类似。
具体而言,代码里实现的反演变换
```
(x, y) -> ( x/(x*x+y*y), y/(x*x+y*y) )
```
把平面上的点映射到关于单位圆的对称点上:半径变为原来的倒数,角度不变。这个变换保持角度不变(但会镜像)
### 关于二维保角变换
二维平面中,映射保角的充要条件是对应的复变函数在解析(全纯)且导数不为零。如果映射对应的复变函数的共轭是全纯的,那么映射保角度但会让角的“方向”反向。
比如,对复变函数 $f(z) = e^z$,由于$f'(z)=e^z$f 在全平面解析,所以带入 $z=x+iy$ 得到的映射$(x,y)\rightarrow (e^x\cos(y), e^x\sin(y))$ 就是一个保角映射。(选项 exp)
又比如复变函数 $f(z) = \log(z)$,解析,对应的映射是 $(x,y)\rightarrow (\log(x^2+y^2),\arctan(\frac{y}{x}))$,也保角(选项 log
$(x,y)\rightarrow (\frac{x}{x^2+y^2}, \frac{y}{x^2+y^2})$ 对应的复变函数是 $f(z)=z/|z|^2=\frac{z}{z\bar{z}}=\frac{1}{|z|}$,可以证明它不解析。然而,它的共轭$\bar{f(z)}=\frac{1}{z}$是解析的,因为导数是 $\frac{d}{dz}(\frac{1}{z}) = -\frac{1}{z^2}$。因此,这是一个反方向的保角映射。(选项 antiholo
$(x,y) \rightarrow (\frac{x}{x^2+y^2},\frac{-y}{x^2+y^2})$对应的复变函数是 $f(z)=\frac{1}{z}$,解析,因此这是一个真的保角映射。(选项 holo