Cách tốt nhất để chuyển đổi vectơ 2D thành hướng la bàn 8 hướng gần nhất là gì?


17

Nếu bạn có một vectơ 2D được biểu thị là x và y, cách tốt nhất để chuyển đổi nó thành hướng la bàn gần nhất là gì?

ví dụ

x:+1,  y:+1 => NE
x:0,   y:+3 => N
x:+10, y:-2 => E   // closest compass direction

Bạn muốn nó như một chuỗi hoặc một enum? (vâng, nó quan trọng)
Philipp

Hoặc, vì nó sẽ được sử dụng theo cả hai cách :) Mặc dù nếu tôi phải chọn, tôi sẽ lấy một chuỗi.
izb

1
Bạn có quan tâm về hiệu suất là tốt, hoặc chỉ về sự đồng nhất?
Marcin Seredynski

2
góc var = Math.atan2 (y, x); trả về <Direction> Math.floor ((Math.round (angle / (2 * Math.PI / 8)) + 8 + 2)% 8); Tôi sử dụng cái này
Kikaimaru

Súc tích: được đánh dấu bằng sự ngắn gọn của biểu thức hoặc tuyên bố: miễn phí từ tất cả các chi tiết phức tạp và thừa. Chỉ cần ném nó ra khỏi đó ...
Dialock

Câu trả lời:


25

Cách đơn giản nhất có lẽ là lấy góc của vectơ bằng cách sử dụng atan2(), như Tetrad gợi ý trong các bình luận, sau đó chia tỷ lệ và làm tròn nó, ví dụ (mã giả):

// enumerated counterclockwise, starting from east = 0:
enum compassDir {
    E = 0, NE = 1,
    N = 2, NW = 3,
    W = 4, SW = 5,
    S = 6, SE = 7
};

// for string conversion, if you can't just do e.g. dir.toString():
const string[8] headings = { "E", "NE", "N", "NW", "W", "SW", "S", "SE" };

// actual conversion code:
float angle = atan2( vector.y, vector.x );
int octant = round( 8 * angle / (2*PI) + 8 ) % 8;

compassDir dir = (compassDir) octant;  // typecast to enum: 0 -> E etc.
string dirStr = headings[octant];

Các octant = round( 8 * angle / (2*PI) + 8 ) % 8dòng có thể cần một số lời giải thích. Trong khá nhiều tất cả các ngôn ngữ mà tôi biết rằng có nó, các atan2()chức năng trả về góc theo radian. Chia nó cho 2 π chuyển đổi nó từ radian thành các phân số của một vòng tròn đầy đủ và nhân với 8 sau đó chuyển đổi nó thành tám phần tư của một vòng tròn, sau đó chúng ta làm tròn thành số nguyên gần nhất. Cuối cùng, chúng tôi giảm modulo 8 để chăm sóc phần bao quanh, sao cho cả 0 và 8 được ánh xạ chính xác về phía đông.

Lý do + 8, mà tôi đã bỏ qua ở trên, là trong một số ngôn ngữ atan2()có thể trả về kết quả âm (nghĩa là từ - π đến + π thay vì từ 0 đến 2 π ) và toán tử modulo ( %) có thể được xác định để trả về giá trị âm cho đối số phủ định (hoặc hành vi của nó đối với đối số phủ định có thể không được xác định). Việc thêm 8(tức là một lượt đầy đủ) vào đầu vào trước khi giảm đảm bảo rằng các đối số luôn dương, không ảnh hưởng đến kết quả theo bất kỳ cách nào khác.

Nếu ngôn ngữ của bạn không xảy ra để cung cấp một hàm tròn gần nhất thuận tiện, bạn có thể sử dụng một chuyển đổi số nguyên cắt ngắn thay vào đó và chỉ cần thêm 0,5 vào đối số, như sau:

int octant = int( 8 * angle / (2*PI) + 8.5 ) % 8;  // int() rounds down

Lưu ý rằng, trong một số ngôn ngữ, chuyển đổi float sang số nguyên mặc định làm tròn các đầu vào âm lên về 0 thay vì xuống, đó là một lý do khác để đảm bảo rằng đầu vào luôn dương.

Tất nhiên, bạn có thể thay thế tất cả các lần xuất hiện 8trên dòng đó bằng một số số khác (ví dụ 4 hoặc 16 hoặc thậm chí 6 hoặc 12 nếu bạn đang ở trên bản đồ hex) để chia vòng tròn thành nhiều hướng. Chỉ cần điều chỉnh enum / mảng cho phù hợp.


Lưu ý rằng nó thường atan2(y,x), không atan2(x,y).
sam hocevar

@Sam: Rất tiếc, đã sửa. Tất nhiên, atan2(x,y)cũng sẽ hoạt động, nếu người ta chỉ liệt kê các tiêu đề la bàn theo thứ tự theo chiều kim đồng hồ bắt đầu từ phía bắc.
Ilmari Karonen

2
Nhân tiện, tôi thực sự nghĩ rằng đây là câu trả lời thẳng thắn và nghiêm khắc nhất.
sam hocevar

1
@TheLima:octant = round(8 * angle / 360 + 8) % 8
Ilmari Karonen

1
Xin lưu ý rằng điều này có thể dễ dàng được chuyển đổi thành la bàn 4 chiều: quadtant = round(4 * angle / (2*PI) + 4) % 4và sử dụng enum : { E, N, W, S }.
Spoike

10

Bạn có 8 tùy chọn (hoặc 16 trở lên nếu bạn muốn độ chính xác cao hơn nữa).

nhập mô tả hình ảnh ở đây

Sử dụng atan2(y,x)để có được góc cho vector của bạn.

atan2() hoạt động theo cách sau:

nhập mô tả hình ảnh ở đây

Vì vậy, x = 1, y = 0 sẽ dẫn đến 0 và không liên tục tại x = -1, y = 0, chứa cả π và -π.

Bây giờ chúng ta chỉ cần ánh xạ đầu ra của atan2()khớp với la bàn mà chúng ta có ở trên.

Có khả năng đơn giản nhất để thực hiện là kiểm tra các góc tăng dần. Đây là một số mã giả dễ dàng được sửa đổi để tăng độ chính xác:

//start direction from the lowest value, in this case it's west with -π
enum direction {
west,
south,
east,
north
}

increment = (2PI)/direction.count
angle = atan2(y,x);
testangle = -PI + increment/2
index = 0

while angle > testangle
    index++
    if(index > direction.count - 1)
        return direction[0] //roll over
    testangle += increment


return direction[index]

Bây giờ để thêm độ chính xác, chỉ cần thêm các giá trị cho enum hướng.

Thuật toán hoạt động bằng cách kiểm tra các giá trị tăng xung quanh la bàn để xem góc của chúng ta có nằm ở đâu đó giữa nơi chúng ta kiểm tra lần cuối và vị trí mới hay không. Đó là lý do tại sao chúng tôi bắt đầu với mức tăng -PI + / 2. Chúng tôi muốn bù đắp séc của chúng tôi để bao gồm không gian bằng nhau xung quanh mỗi hướng. Một cái gì đó như thế này:

nhập mô tả hình ảnh ở đây

Tây bị phá vỡ làm hai vì các giá trị trả về của atan2()West không liên tục.


4
Một cách dễ dàng để "chuyển đổi chúng thành một góc" là sử dụng atan2, mặc dù hãy nhớ rằng 0 độ có thể sẽ ở phía đông chứ không phải phía bắc.
Tetrad

1
Bạn không cần angle >=kiểm tra trong mã ở trên; ví dụ: nếu góc nhỏ hơn 45 thì phía bắc sẽ được trả về, vì vậy bạn không cần kiểm tra xem góc> = 45 đối với kiểm tra phía đông. Tương tự như vậy, bạn không cần bất kỳ kiểm tra nào trước khi quay về hướng tây - đó là khả năng duy nhất còn lại.
MrKWatkins

4
Tôi sẽ không gọi đây là một cách ngắn gọn để có được hướng đi. Nó có vẻ khá lộn xộn và sẽ đòi hỏi nhiều thay đổi để thích ứng với "độ phân giải" khác nhau. Không nói về một tấn ifbáo cáo nếu bạn muốn đi từ 16 hướng trở lên.
bummzack

2
Không cần bình thường hóa vectơ: góc vẫn giữ nguyên so với thay đổi cường độ.
Kylotan

Cảm ơn @bummzack, tôi đã chỉnh sửa bài đăng để làm cho nó ngắn gọn hơn và dễ dàng tăng độ chính xác chỉ bằng cách thêm nhiều giá trị enum.
MichaelHouse

8

Bất cứ khi nào bạn đang xử lý các vectơ, hãy xem xét các hoạt động vectơ cơ bản thay vì chuyển đổi thành các góc trong một số khung cụ thể.

Cho một vectơ truy vấn vvà một tập các vectơ đơn vị s, vectơ được căn chỉnh nhiều nhất là vectơ s_itối đa hóa dot(v,s_i). Điều này là do sản phẩm chấm được cung cấp độ dài cố định cho các tham số có mức tối đa cho các vectơ có cùng hướng và tối thiểu cho vectơ có hướng ngược nhau, thay đổi giữa các điểm một cách trơn tru.

Điều này khái quát hóa tầm thường thành nhiều chiều hơn hai chiều, có thể mở rộng với các hướng tùy ý và không gặp phải các vấn đề cụ thể về khung như độ dốc vô hạn.

Thực hiện theo cách khôn ngoan, điều này sẽ giúp bạn liên kết từ một vectơ theo từng hướng chính với một mã định danh (enum, chuỗi, bất cứ thứ gì bạn cần) đại diện cho hướng đó. Sau đó, bạn sẽ lặp qua bộ chỉ đường của mình, tìm hướng có sản phẩm chấm cao nhất.

map<float2,Direction> candidates;
candidates[float2(1,0)] = E; candidates[float2(0,1)] = N; // etc.

for each (float2 dir in candidates)
{
    float goodness = dot(dir, v);
    if (goodness > bestResult)
    {
        bestResult = goodness;
        bestDir = candidates[dir];
    }    
}

2
Việc thực hiện này cũng có thể được viết không phân nhánh và véc tơ mà không gặp quá nhiều khó khăn.
Hứa hẹn

1
A mapvới float2làm chìa khóa? Điều này không có vẻ rất nghiêm trọng.
sam hocevar

Đó là "mã giả" theo cách mô phạm. Nếu bạn muốn triển khai tối ưu hóa hoảng loạn, GDSE có thể không phải là nơi dành cho bản sao của bạn. Đối với việc sử dụng float2 làm khóa, một số float có thể biểu thị chính xác toàn bộ số chúng ta sử dụng ở đây và bạn có thể tạo một bộ so sánh hoàn toàn tốt cho chúng. Các phím dấu phẩy động chỉ không phù hợp nếu chúng chứa các giá trị đặc biệt hoặc bạn cố gắng tra cứu kết quả tính toán. Lặp lại một chuỗi kết hợp là tốt. Tôi có thể đã sử dụng một tìm kiếm tuyến tính trong một mảng, chắc chắn, nhưng nó chỉ là sự lộn xộn vô nghĩa.
Lars Viklund

3

Một cách chưa được đề cập ở đây là coi các vectơ là số phức. Chúng không yêu cầu lượng giác và có thể khá trực quan để thêm, nhân hoặc làm tròn các phép quay, đặc biệt vì bạn đã có các tiêu đề của bạn được biểu thị dưới dạng các cặp số.

Trong trường hợp bạn không quen thuộc với chúng, các hướng được thể hiện dưới dạng a + b (i) với thành phần thực và b (i) là tưởng tượng. Nếu bạn tưởng tượng mặt phẳng cartesian với X là thật và Y là tưởng tượng, 1 sẽ ở phía đông (phải), tôi sẽ ở phía bắc.

Đây là phần quan trọng: 8 hướng chính được biểu diễn riêng với các số 1, -1 hoặc 0 cho các thành phần thực và ảo của chúng. Vì vậy, tất cả những gì bạn phải làm là giảm tọa độ X, Y của mình theo tỷ lệ và làm tròn cả hai thành số nguyên gần nhất để có hướng.

NW (-1 + i)       N (i)        NE (1 + i)
W  (-1)          Origin        E  (1)
SW (-1 - i)      S (-i)        SE (1 - i)

Để chuyển đổi đường chéo từ đầu đến gần nhất, hãy giảm cả X và Y theo tỷ lệ để giá trị lớn hơn chính xác là 1 hoặc -1. Bộ

// Some pseudocode

enum xDir { West = -1, Center = 0, East = 1 }
enum yDir { South = -1, Center = 0, North = 1 }

xDir GetXdirection(Vector2 heading)
{
    return round(heading.x / Max(heading.x, heading.y));
}

yDir GetYdirection(Vector2 heading)
{
    return round(heading.y / Max(heading.x, heading.y));
}

Làm tròn cả hai thành phần của những gì ban đầu (10, -2) mang lại cho bạn 1 + 0 (i) hoặc 1. Vì vậy, hướng gần nhất là hướng đông.

Ở trên không thực sự yêu cầu sử dụng cấu trúc số phức, nhưng nghĩ về chúng như vậy sẽ giúp tìm ra 8 hướng chính nhanh hơn. Bạn có thể làm toán vectơ theo cách thông thường nếu bạn muốn lấy tiêu đề thuần của hai hoặc nhiều vectơ. (Là số phức, bạn không thêm, nhưng nhân cho kết quả)


1
Điều này thật tuyệt vời, nhưng mắc một lỗi tương tự như lỗi tôi đã làm trong nỗ lực của riêng tôi. Các câu trả lời gần nhưng không đúng. Góc biên giữa E và NE là 22,5 độ, nhưng điều này bị cắt ở 26,6 độ.
izb

Max(x, y)nên Max(Abs(x, y))làm việc cho các góc phần tư âm. Tôi đã thử nó và nhận được kết quả tương tự như izb - điều này chuyển hướng la bàn ở các góc sai. Tôi đoán nó sẽ chuyển đổi khi title.y / title.x vượt qua 0,5 (vì vậy giá trị làm tròn chuyển từ 0 sang 1), đó là arctan (0,5) = 26,565 °.
amitp

Một cách khác để sử dụng số phức ở đây là quan sát rằng phép nhân số phức liên quan đến một phép quay. Nếu bạn xây dựng một số phức đại diện cho 1/8 vòng xoay quanh một vòng tròn, thì mỗi khi bạn nhân với nó, bạn sẽ di chuyển một quãng tám. Vì vậy, bạn có thể hỏi: chúng ta có thể đếm được bao nhiêu phép nhân đã đi từ Đông sang nhóm hiện tại không? Câu trả lời cho "chúng ta phải nhân lên bao nhiêu lần" là một logarit . Nếu bạn tra cứu logarit cho các số phức thì nó sử dụng atan2. Vì vậy, điều này kết thúc tương đương với câu trả lời của Ilmari.
amitp

-2

điều này dường như hoạt động:

public class So49290 {
    int piece(int x,int y) {
        double angle=Math.atan2(y,x);
        if(angle<0) angle+=2*Math.PI;
        int piece=(int)Math.round(n*angle/(2*Math.PI));
        if(piece==n)
            piece=0;
        return piece;
    }
    void run(int x,int y) {
        System.out.println("("+x+","+y+") is "+s[piece(x,y)]);
    }
    public static void main(String[] args) {
        So49290 so=new So49290();
        so.run(1,0);
        so.run(1,1);
        so.run(0,1);
        so.run(-1,1);
        so.run(-1,0);
        so.run(-1,-1);
        so.run(0,-1);
        so.run(1,-1);
    }
    int n=8;
    static final String[] s=new String[] {"e","ne","n","nw","w","sw","s","se"};
}

Tại sao điều này được bỏ phiếu xuống?
Ray Tayek

Rất có thể bởi vì không có lời giải thích đằng sau mã của bạn. Tại sao đây là giải pháp và làm thế nào nó hoạt động?
Vaillancourt

bạn đã chạy nó chưa?
Ray Tayek

Không, và đưa ra tên của lớp, tôi giả sử bạn đã làm và nó đã làm việc. Và điều đó thật tuyệt. Nhưng bạn hỏi tại sao mọi người bỏ phiếu xuống và tôi trả lời; Tôi không bao giờ ngụ ý rằng nó không hoạt động :)
Vaillancourt

-2

E = 0, NE = 1, N = 2, Tây Bắc = 3, W = 4, SW = 5, S = 6, SE = 7

f (x, y) = mod ((4-2 * (1 + dấu (x)) * (1 ký (y ^ 2)) - (2 + dấu (x)) * dấu (y)

    -(1+sign(abs(sign(x*y)*atan((abs(x)-abs(y))/(abs(x)+abs(y))))

    -pi()/(8+10^-15)))/2*sign((x^2-y^2)*(x*y))),8)

Hiện tại, đây chỉ là một loạt các nhân vật không có nhiều ý nghĩa; Tại sao đây là một giải pháp sẽ làm việc cho câu hỏi, làm thế nào nó hoạt động?
Vaillancourt

Tôi viết công thức như tôi đã viết jn excel và đang hoạt động hoàn hảo.
theodore panagos

= MOD ((4-2 * (1 + ĐĂNG KÝ (X1)) * (1-KÝ (Y1 ^ 2)) - (2 + ĐĂNG (X1)) * ĐĂNG KÝ (Y1) - (1 + ĐĂNG KÝ (ABS (ĐĂNG KÝ) (X1 * Y1) * ATAN ((ABS (X1) -ABS (Y1)) / (ABS (X1) + ABS (Y1)))) - PI () / (8 + 10 ^ -15)) / 2 * ĐĂNG KÝ ((X1 ^ 2-Y1 ^ 2) * (X1 * Y1))), 8)
panagos theodore

-4

Khi bạn muốn một chuỗi:

h_axis = ""
v_axis = ""

if (x > 0) h_axis = "E"    
if (x < 0) h_axis = "W"    
if (y > 0) v_axis = "S"    
if (y < 0) v_axis = "N"

return v_axis.append_string(h_axis)

Điều này cung cấp cho bạn hằng số bằng cách sử dụng bitfield:

// main direction constants
DIR_E = 0x1
DIR_W = 0x2
DIR_S = 0x4
DIR_N = 0x8
// mixed direction constants
DIR_NW = DIR_N | DIR_W    
DIR_SW = DIR_S | DIR_W
DIR_NE = DIR_N | DIR_E
DIR_SE = DIR_S | DIR_E

// calculating the direction
dir = 0x0

if (x > 0) dir |= DIR_E 
if (x < 0) dir |= DIR_W    
if (y > 0) dir |= DIR_S    
if (y < 0) dir |= DIR_N

return dir

Một cải tiến hiệu suất nhỏ sẽ là đưa các <-check vào nhánh khác của tương ứng> -check , nhưng tôi đã kiềm chế không làm điều đó vì nó gây hại cho khả năng đọc.


2
Xin lỗi, nhưng điều đó sẽ không đưa ra chính xác câu trả lời tôi đang tìm kiếm. Với mã đó, nó sẽ chỉ mang lại "N" nếu vectơ chính xác ở phía bắc và NE hoặc NW nếu x là bất kỳ giá trị nào khác. Cái tôi cần là hướng la bàn gần nhất, ví dụ nếu vectơ gần N hơn NA thì nó sẽ mang lại N.
izb

Điều này thực sự sẽ đưa ra hướng gần nhất? Có vẻ như một vectơ (0,00001,100) sẽ cung cấp cho bạn về phía đông bắc. chỉnh sửa: bạn đánh tôi với nó izb.
CiscoIPPhone

bạn đã không nói rằng bạn muốn hướng gần nhất.
Philipp

1
Xin lỗi, tôi đã giấu nó trong tiêu đề. Đáng lẽ phải rõ ràng hơn trong phần câu hỏi
izb

1
Còn việc sử dụng định mức vô hạn thì sao? Chia cho max (abs (vector.components)) cung cấp cho bạn một vectơ chuẩn hóa đối với định mức đó. Bây giờ bạn có thể viết một bảng kiểm tra nhỏ dựa trên if (x > 0.9) dir |= DIR_Evà tất cả phần còn lại. Nó phải tốt hơn mã gốc của Phillipp và rẻ hơn một chút so với sử dụng định mức L2 và atan2. Có lẽ .. hoặc có thể không.
teodron
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.