Compare commits
10 Commits
f525cf3039
...
c8fa5db7d3
| Author | SHA1 | Date | |
|---|---|---|---|
| c8fa5db7d3 | |||
| de602365e9 | |||
|
|
2c482150d4 | ||
|
|
d62373e810 | ||
|
|
1c8dbe1ccd | ||
|
|
5043f8c674 | ||
|
|
7ddb7969cc | ||
|
|
a1a6c36502 | ||
|
|
902b2a9386 | ||
|
|
e0bc413150 |
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Byte-compiled / build artifacts
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# Virtual envs / tooling
|
||||
.venv/
|
||||
.uv/
|
||||
|
||||
# Generated outputs (only at repo root; bundled assets must stay)
|
||||
/trajectories.png
|
||||
/mouse.onnx
|
||||
/mouse.onnx.data
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# pytest
|
||||
.pytest_cache/
|
||||
170
README.md
170
README.md
@@ -1,26 +1,148 @@
|
||||
# mouse_control
|
||||
一种基于神经网络来模拟人手移动鼠标的方法
|
||||
|
||||
一种基于神经网络来模拟人手移动鼠标的方法。
|
||||
|
||||
## 简介
|
||||
本项目源于该死的轨迹检测。经过初步验证,本项目是有效的。
|
||||
<img src="./imgs/Figure_1.png">
|
||||
上图为真人轨迹移动得到的散点图。这里我们用10个点来拟合轨迹。
|
||||
可以看到,人手的移动其实和pid,以及其他的曲线是有一定区别的。
|
||||
因此,我们选用神经网络来拟合真人鼠标移动轨迹。
|
||||
该神经网络其实很简单,只有三个全连接层。
|
||||
输入为目标距离当前位置的dx,dy。
|
||||
输出为10个点用来模拟真人的轨迹。
|
||||
## 收集鼠标轨迹
|
||||
首先运行collect_data.py。运行后,我们会看到这样一个界面。
|
||||
<img src="./imgs/collect.png">
|
||||
我们需要点击红球,就会开始记录鼠标轨迹,点击蓝球,结束记录。这样我们就成功收集到一条鼠标轨迹的数据。
|
||||
重复这样,每收集100次程序会退出。我们一共要收集约300条数据。
|
||||
这样,我们就收集好了数据。
|
||||
## 划分数据集(可选)
|
||||
我们将mouse_data.csv用vscode打开,选择一些数据剪切到mouse_data_test.csv中。
|
||||
## 训练模型
|
||||
运行train.py,程序就会开始训练模型。最终我们能看到控制台打印出的一条test数据。是一个dx,dy拟合出的十个点。
|
||||
## 验证
|
||||
<img src="./imgs/Figure_2.png">
|
||||
我们将刚刚得到的十个点放到show.py中,观察散点图,发现其轨迹类似于本人鼠标移动轨迹。
|
||||
todo
|
||||
使用onnxruntime在c++上进行推理
|
||||
|
||||
本项目源于该死的轨迹检测。我们用一个简单的三层全连接神经网络去拟合真人鼠标移动轨迹。
|
||||
|
||||
<img src="./imgs/Figure_1.png">
|
||||
|
||||
上图为真人轨迹移动得到的散点图——用 10 个关键点拟合一条轨迹。可以看到,人手移动和 PID、贝塞尔等曲线有明显区别,因此用神经网络来拟合更合适。
|
||||
|
||||
**网络结构**:
|
||||
|
||||
- 输入:`(dx, dy)` —— 目标点相对当前位置的偏移
|
||||
- 三层全连接:`Linear(2 → 64) → ReLU → Linear(64 → 32) → ReLU → Linear(32 → 20)`
|
||||
- 输出:`(10, 2)` —— 10 个轨迹点
|
||||
|
||||
仓库内含 `data/` 下的 400 条采集数据和 `src/mouse_control/assets/mouse.onnx` 预训练模型,clone 之后可以直接 `mouse-visualize` 看效果。
|
||||
|
||||
## 安装
|
||||
|
||||
使用 [uv](https://docs.astral.sh/uv/) 管理:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
或从构建产物安装:
|
||||
|
||||
```bash
|
||||
uv pip install dist/mouse_control-0.1.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
要求 Python ≥ 3.12。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
mouse_control/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── data/ # 训练 / 测试数据 (380 + 20 条)
|
||||
│ ├── mouse_data.csv
|
||||
│ └── mouse_data_test.csv
|
||||
├── imgs/ # README 图片
|
||||
└── src/mouse_control/ # 包源码 (src layout)
|
||||
├── __init__.py # 公共 API
|
||||
├── model.py # SimpleNet 网络定义
|
||||
├── collect.py # 数据采集 GUI
|
||||
├── train.py # 训练 + ONNX 导出
|
||||
├── visualize.py # 轨迹可视化
|
||||
├── inference.py # 模型加载 / 推理辅助
|
||||
└── assets/
|
||||
└── mouse.onnx # 随包发布的预训练模型
|
||||
```
|
||||
|
||||
## 命令行用法
|
||||
|
||||
安装后可以直接调用三个命令(也可以用 `uv run <cmd>`):
|
||||
|
||||
### 1. 采集真人鼠标轨迹
|
||||
|
||||
```bash
|
||||
uv run mouse-collect -o data/mouse_data.csv -n 100
|
||||
```
|
||||
|
||||
<img src="./imgs/collect.png">
|
||||
|
||||
全屏界面:点**红球**开始记录、移动到**蓝球**点击结束记录,每条轨迹保存 10 个关键点到 CSV。
|
||||
|
||||
- 默认采集 100 条后自动退出,按 `Esc` 提前退出
|
||||
- 推荐采集 ≥ 300 条以获得较好的训练效果
|
||||
|
||||
### 2. 划分训练 / 测试集
|
||||
|
||||
把 `mouse_data.csv` 里**剪切**若干行(10-20 条)到 `mouse_data_test.csv` 即可。
|
||||
|
||||
### 3. 训练模型
|
||||
|
||||
```bash
|
||||
uv run mouse-train \
|
||||
--train-csv data/mouse_data.csv \
|
||||
--test-csv data/mouse_data_test.csv \
|
||||
--output mouse.onnx \
|
||||
--epochs 1000
|
||||
```
|
||||
|
||||
训练结束后在当前目录生成 `mouse.onnx`。其它参数 `--batch-size` `--lr` 可调。
|
||||
|
||||
### 4. 可视化效果
|
||||
|
||||
```bash
|
||||
uv run mouse-visualize --n-trajectories 10 --seed 42
|
||||
```
|
||||
|
||||
<img src="./imgs/Figure_2.png">
|
||||
|
||||
随机生成若干目标点,每条轨迹包含 10 个模型输出点 + 80 个三次样条插值点。`--no-show` 跳过 GUI 窗口、只写 PNG;`--model` 指定其它 ONNX 文件。
|
||||
|
||||
## Python API
|
||||
|
||||
```python
|
||||
import mouse_control as mc
|
||||
|
||||
# 加载模型:优先 ./mouse.onnx,否则回退到包内打包的预训练模型
|
||||
session = mc.load_model()
|
||||
|
||||
# 推理:输入 (dx, dy),输出 (10, 2) 轨迹点
|
||||
pts = mc.predict(session, 120, -80)
|
||||
# -> ndarray, shape (10, 2), dtype float32
|
||||
|
||||
# 直接使用模型类(用于自训练 / 微调)
|
||||
from mouse_control import SimpleNet
|
||||
```
|
||||
|
||||
## ONNX 推理(其它语言)
|
||||
|
||||
### Python (onnxruntime)
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
|
||||
session = ort.InferenceSession("mouse.onnx")
|
||||
inp = np.array([[[100.0, 200.0]]], dtype=np.float32) # dx=100, dy=200
|
||||
output = session.run(None, {"input": inp})[0] # shape (1, 10, 2)
|
||||
```
|
||||
|
||||
### C++ (OpenCV DNN)
|
||||
|
||||
```cpp
|
||||
cv::dnn::Net net = cv::dnn::readNetFromONNX("mouse.onnx");
|
||||
cv::Mat blob(1, 2, CV_32F, cv::Scalar(100, 100)); // 输入 dx=100, dy=100
|
||||
net.setInput(blob, "input");
|
||||
cv::Mat output = net.forward("output"); // 输出 1*10*2
|
||||
std::cout << output.at<float>(0, 8, 0) << std::endl;
|
||||
```
|
||||
|
||||
## 打包发布
|
||||
|
||||
```bash
|
||||
uv build
|
||||
# -> dist/mouse_control-0.1.0-py3-none-any.whl
|
||||
# -> dist/mouse_control-0.1.0.tar.gz
|
||||
```
|
||||
|
||||
wheel 内含 `assets/mouse.onnx` 预训练模型,安装后直接 `mc.load_model()` 即可使用。
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import math
|
||||
import random
|
||||
import tkinter as tk
|
||||
import matplotlib.pyplot as plt
|
||||
import csv
|
||||
from tkinter import Label
|
||||
|
||||
# 创建窗口
|
||||
root = tk.Tk()
|
||||
root.attributes('-fullscreen', True) # 全屏显示
|
||||
|
||||
label_n = Label(root, text="n: 0", font=("Helvetica", 16))
|
||||
label_n.pack()
|
||||
|
||||
csv_file_path = "mouse_data.csv"
|
||||
|
||||
screen_width = root.winfo_screenwidth()
|
||||
screen_height = root.winfo_screenheight()
|
||||
|
||||
# 设置小球的初始位置
|
||||
ball1_pos = (screen_width/2, screen_height/2)
|
||||
ball2_pos = (ball1_pos[0] + random.randint(-200, 200),ball1_pos[1] + random.randint(-200, 200))
|
||||
|
||||
# 设置小球的半径
|
||||
ball_radius = 20
|
||||
|
||||
# 设置鼠标记录状态
|
||||
recording = False
|
||||
mouse_path = []
|
||||
|
||||
n=0
|
||||
|
||||
# 鼠标移动事件处理函数
|
||||
def motion(event):
|
||||
global recording, mouse_path, n
|
||||
if recording:
|
||||
mouse_path.append((event.x, event.y))
|
||||
|
||||
# 鼠标点击事件处理函数
|
||||
def mouse_click(event):
|
||||
global recording, mouse_path, ball2_pos, n
|
||||
|
||||
if event.x >= ball1_pos[0] - ball_radius and event.x <= ball1_pos[0] + ball_radius and event.y >= ball1_pos[1] - ball_radius and event.y <= ball1_pos[1] + ball_radius:
|
||||
recording = True
|
||||
mouse_path = [(event.x, event.y)]
|
||||
elif event.x >= ball2_pos[0] - ball_radius and event.x <= ball2_pos[0] + ball_radius and event.y >= ball2_pos[1] - ball_radius and event.y <= ball2_pos[1] + ball_radius:
|
||||
recording = False
|
||||
canvas.delete("ball2")
|
||||
#visualize_path(mouse_path) # 可视化鼠标轨迹
|
||||
save_to_csv(mouse_path)
|
||||
n = n+1
|
||||
if n == 100:
|
||||
root.destroy()
|
||||
label_n.config(text=f"n: {n}")
|
||||
mouse_path = []
|
||||
|
||||
# 重新生成第二个小球的位置
|
||||
ball2_pos = (ball1_pos[0] + random.randint(-200, 200), ball1_pos[1] + random.randint(-200, 200))
|
||||
|
||||
# 绘制新的第二个小球
|
||||
canvas.create_oval(ball2_pos[0]-ball_radius, ball2_pos[1]-ball_radius, ball2_pos[0]+ball_radius, ball2_pos[1]+ball_radius, fill="blue", tags="ball2")
|
||||
|
||||
|
||||
# 键盘事件处理函数
|
||||
def key(event):
|
||||
if event.keysym == "Escape":
|
||||
root.destroy()
|
||||
|
||||
def save_to_csv(path):
|
||||
# 将路径坐标转换为相对于起点的坐标
|
||||
x_rel = [px - path[0][0] for px, py in path]
|
||||
y_rel = [-(py - path[0][1]) for px, py in path]
|
||||
|
||||
# 计算每个点相对于起点的距离,用于z轴表示
|
||||
distances = [math.sqrt((x_rel[0] - px)**2 + (y_rel[0] - py)**2) for px, py in zip(x_rel, y_rel)]
|
||||
|
||||
# 选择10个关键点
|
||||
key_points_indices = [int(i) for i in range(0, len(path), max(1, len(path)//10))]
|
||||
key_points_x = [x_rel[i] for i in key_points_indices]
|
||||
key_points_y = [y_rel[i] for i in key_points_indices]
|
||||
key_points_distances = [distances[i] for i in key_points_indices]
|
||||
|
||||
# 打开 CSV 文件进行写操作
|
||||
with open(csv_file_path, mode='a', newline='') as csv_file:
|
||||
csv_writer = csv.writer(csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
|
||||
# 写入一行数据
|
||||
csv_writer.writerow([f"{key_points_x[-1]},{key_points_y[-1]}"] + [f"{key_points_x[i]},{key_points_y[i]}" for i in range(0,10)])
|
||||
|
||||
|
||||
def visualize_path(path):
|
||||
# 将路径坐标转换为相对于起点的坐标
|
||||
x_rel = [px - path[0][0] for px, py in path]
|
||||
y_rel = [-(py - path[0][1]) for px, py in path]
|
||||
|
||||
# 计算每个点相对于起点的距离,用于z轴表示
|
||||
distances = [math.sqrt((x_rel[0] - px)**2 + (y_rel[0] - py)**2) for px, py in zip(x_rel, y_rel)]
|
||||
|
||||
# 选择10个关键点
|
||||
key_points_indices = [int(i) for i in range(0, len(path), max(1, len(path)//10))]
|
||||
key_points_x = [x_rel[i] for i in key_points_indices]
|
||||
key_points_y = [y_rel[i] for i in key_points_indices]
|
||||
key_points_distances = [distances[i] for i in key_points_indices]
|
||||
|
||||
# 使用z轴信息,通过颜色表示距离的远近
|
||||
plt.scatter(key_points_x, key_points_y, c=key_points_distances, cmap='viridis', marker='o', s=50)
|
||||
|
||||
# 在关键点位置添加文本标签,显示终点到起点的距离
|
||||
|
||||
plt.text(key_points_x[-1], key_points_y[-1], f'Distance to Origin: {key_points_distances[-1]:.2f}', ha='right', va='bottom', bbox=dict(facecolor='white', alpha=0.5))
|
||||
|
||||
# 添加颜色条,表示z轴信息
|
||||
plt.colorbar(label='Distance to Endpoint')
|
||||
|
||||
plt.show()
|
||||
|
||||
# 绘制小球
|
||||
canvas = tk.Canvas(root, width=root.winfo_screenwidth(), height=root.winfo_screenheight())
|
||||
canvas.pack()
|
||||
canvas.create_oval(ball1_pos[0]-ball_radius, ball1_pos[1]-ball_radius, ball1_pos[0]+ball_radius, ball1_pos[1]+ball_radius, fill="red")
|
||||
canvas.create_oval(ball2_pos[0]-ball_radius, ball2_pos[1]-ball_radius, ball2_pos[0]+ball_radius, ball2_pos[1]+ball_radius, fill="blue", tags="ball2")
|
||||
|
||||
# 绑定鼠标事件
|
||||
canvas.bind('<Motion>', motion)
|
||||
canvas.bind('<Button-1>', mouse_click)
|
||||
|
||||
# 绑定键盘事件
|
||||
root.bind('<Key>', key)
|
||||
|
||||
# 运行窗口
|
||||
root.mainloop()
|
||||
380
data/mouse_data.csv
Normal file
380
data/mouse_data.csv
Normal file
@@ -0,0 +1,380 @@
|
||||
"189,-42","0,0","7,-1","58,-25","136,-49","188,-57","219,-60","216,-54","211,-51","205,-47","196,-43"
|
||||
"117,102","0,0","12,7","36,24","72,45","96,60","108,71","110,77","112,83","114,89","116,96"
|
||||
"-83,87","0,0","-4,2","-11,10","-26,28","-40,44","-56,60","-63,69","-70,75","-76,79","-80,83"
|
||||
"-55,-84","0,0","-6,-4","-12,-15","-22,-32","-33,-46","-41,-57","-45,-64","-46,-69","-49,-73","-51,-78"
|
||||
"-18,-150","0,0","-1,-5","-5,-19","-12,-49","-16,-97","-23,-117","-24,-129","-23,-138","-21,-146","-18,-154"
|
||||
"-172,-121","0,0","-9,-10","-40,-34","-80,-61","-112,-82","-132,-93","-138,-98","-146,-108","-159,-119","-165,-120"
|
||||
"69,-37","0,0","2,-2","6,-6","12,-11","19,-16","31,-22","46,-29","60,-32","64,-34","66,-36"
|
||||
"-166,-62","0,0","-8,-4","-29,-16","-75,-42","-118,-61","-128,-63","-134,-62","-144,-61","-154,-61","-161,-62"
|
||||
"99,-15","0,0","6,0","20,-3","57,-6","83,-7","113,-12","134,-14","130,-14","117,-14","105,-15"
|
||||
"65,106","0,0","-4,-1","2,5","11,18","28,34","47,54","55,70","59,83","63,96","65,106"
|
||||
"48,-83","0,0","4,-10","9,-21","12,-31","17,-44","24,-59","29,-70","37,-79","42,-85","46,-86"
|
||||
"-146,38","0,0","-11,0","-39,3","-80,11","-125,18","-149,19","-159,21","-164,23","-168,26","-157,32"
|
||||
"-149,-9","0,0","-13,-5","-40,-12","-76,-19","-104,-24","-128,-24","-143,-23","-144,-18","-145,-13","-149,-9"
|
||||
"-9,5","0,0","6,-5","15,-8","20,-9","25,-9","28,-7","25,-5","18,-2","11,0","6,1"
|
||||
"200,-109","0,0","2,-4","8,-11","30,-36","59,-60","85,-73","120,-87","152,-98","171,-104","184,-106"
|
||||
"-15,184","0,0","2,19","2,50","-3,83","-17,112","-20,132","-16,151","-16,164","-17,170","-18,176"
|
||||
"109,-132","0,0","-7,0","-2,-10","23,-38","63,-81","90,-109","107,-124","121,-136","126,-138","118,-136"
|
||||
"-127,-91","0,0","-8,-19","-34,-44","-66,-58","-83,-64","-90,-68","-103,-74","-114,-77","-120,-82","-124,-86"
|
||||
"-186,185","0,0","-10,13","-58,72","-106,118","-118,141","-133,158","-143,166","-152,171","-163,179","-181,190"
|
||||
"38,92","0,0","4,3","11,13","21,25","31,37","38,51","39,62","39,70","38,75","38,79"
|
||||
"52,154","0,0","1,7","15,37","35,80","57,108","64,121","64,129","65,137","64,145","59,152"
|
||||
"149,-106","0,0","4,-6","18,-21","44,-43","68,-61","89,-73","110,-87","120,-94","125,-96","132,-100"
|
||||
"-45,-81","0,0","-1,-16","-4,-45","-11,-60","-16,-73","-20,-82","-26,-87","-33,-89","-39,-91","-43,-88"
|
||||
"-18,44","0,0","0,2","-1,7","-4,20","-6,24","-7,29","-9,33","-11,36","-12,38","-13,39"
|
||||
"68,-144","0,0","11,-26","22,-70","41,-116","51,-137","83,-165","93,-172","86,-166","77,-159","73,-154"
|
||||
"-124,-125","0,0","-8,-5","-26,-20","-54,-41","-79,-68","-100,-95","-115,-114","-124,-122","-128,-125","-126,-128"
|
||||
"-109,62","0,0","-10,8","-22,21","-34,33","-56,48","-79,60","-86,63","-92,64","-98,64","-104,63"
|
||||
"-171,-36","0,0","-13,-1","-51,-14","-100,-33","-127,-45","-139,-47","-145,-44","-154,-42","-163,-42","-167,-40"
|
||||
"-129,-163","0,0","-5,-11","-22,-44","-47,-86","-79,-129","-87,-135","-93,-140","-100,-143","-106,-146","-117,-155"
|
||||
"92,21","0,0","10,-1","37,-3","77,4","114,14","128,20","120,21","112,22","106,22","99,22"
|
||||
"-182,-85","0,0","-13,-6","-38,-23","-72,-44","-106,-62","-139,-72","-153,-77","-158,-79","-165,-80","-171,-82"
|
||||
"-17,-72","0,0","0,-8","0,-14","0,-22","-3,-32","-7,-42","-11,-51","-14,-58","-17,-64","-18,-67"
|
||||
"-114,72","0,0","3,-3","4,-7","6,-9","1,-6","-11,5","-32,21","-56,39","-75,53","-96,64"
|
||||
"64,-108","0,0","1,-4","2,-12","6,-24","11,-44","24,-68","40,-89","44,-91","47,-94","51,-96"
|
||||
"-75,179","0,0","-3,12","-16,52","-39,98","-57,127","-65,143","-69,151","-71,158","-72,165","-74,172"
|
||||
"-133,-26","0,0","-9,-7","-28,-15","-51,-23","-78,-26","-98,-27","-106,-26","-113,-26","-119,-26","-127,-26"
|
||||
"143,126","0,0","3,3","25,20","59,46","102,75","140,95","142,102","142,108","142,115","143,120"
|
||||
"-71,-90","0,0","-3,-9","-16,-28","-34,-54","-45,-73","-49,-77","-52,-82","-54,-86","-56,-90","-62,-90"
|
||||
"2,-135","0,0","0,-5","-1,-15","-3,-30","-5,-50","-5,-71","-4,-93","-2,-112","-2,-125","-1,-131"
|
||||
"-102,152","0,0","-5,17","-38,85","-54,104","-64,113","-73,124","-79,130","-84,137","-90,144","-96,149"
|
||||
"174,-92","0,0","10,-8","25,-19","53,-37","88,-55","112,-65","130,-72","135,-74","140,-76","155,-84"
|
||||
"-76,155","0,0","-2,17","-9,48","-23,78","-38,99","-48,115","-53,125","-55,131","-59,135","-65,144"
|
||||
"116,81","0,0","10,7","24,17","61,41","74,59","79,63","83,69","94,74","101,77","109,79"
|
||||
"19,169","0,0","7,12","15,43","23,78","26,105","22,126","20,141","19,149","18,155","18,161"
|
||||
"74,-92","0,0","3,-8","14,-27","34,-51","42,-58","45,-62","53,-71","59,-79","67,-88","74,-92"
|
||||
"-114,198","0,0","-8,32","-28,80","-34,100","-47,121","-57,136","-71,157","-82,171","-92,176","-102,186"
|
||||
"116,110","0,0","16,12","44,35","78,65","90,77","95,91","105,108","119,121","127,127","122,119"
|
||||
"-155,-189","0,0","-1,-4","-12,-29","-34,-69","-69,-107","-103,-149","-123,-173","-132,-180","-137,-186","-144,-191"
|
||||
"60,76","0,0","-5,-2","1,2","17,16","39,44","64,65","71,72","75,78","79,86","75,84"
|
||||
"155,65","0,0","10,1","40,6","88,13","135,21","151,27","153,34","154,40","156,45","156,51"
|
||||
"74,-107","0,0","2,-8","20,-32","40,-60","49,-76","60,-93","68,-108","69,-114","72,-119","74,-113"
|
||||
"191,65","0,0","8,1","51,4","124,22","161,37","166,44","170,52","175,58","181,62","191,65"
|
||||
"41,110","0,0","-6,-1","-2,2","11,13","20,28","32,55","38,77","39,84","41,95","42,103"
|
||||
"114,56","0,0","7,1","30,3","63,11","86,20","95,25","98,30","106,35","112,41","116,48"
|
||||
"-138,105","0,0","-18,12","-48,36","-65,56","-85,70","-102,79","-110,83","-118,87","-125,92","-130,99"
|
||||
"-149,-188","0,0","-7,-18","-32,-50","-76,-103","-104,-148","-121,-167","-124,-182","-129,-186","-141,-187","-149,-188"
|
||||
"6,104","0,0","-1,4","1,10","4,23","8,42","11,60","11,76","7,86","7,92","6,98"
|
||||
"-27,-33","0,0","0,-1","-2,-7","-6,-13","-13,-20","-18,-23","-22,-27","-24,-30","-25,-32","-27,-33"
|
||||
"-130,127","0,0","-4,9","-23,36","-56,72","-84,89","-93,97","-101,107","-113,115","-121,119","-126,121"
|
||||
"97,-195","0,0","6,-13","29,-52","55,-108","71,-150","73,-166","75,-180","84,-193","88,-199","94,-199"
|
||||
"86,-169","0,0","17,-25","36,-58","50,-88","58,-108","61,-116","70,-133","72,-141","74,-149","78,-154"
|
||||
"-28,72","0,0","-1,4","-3,15","-7,31","-11,44","-12,49","-13,54","-16,61","-20,65","-25,69"
|
||||
"65,112","0,0","-4,7","-8,15","0,22","12,31","25,41","37,56","48,73","57,89","61,100"
|
||||
"-129,51","0,0","-6,4","-25,8","-78,14","-112,17","-117,22","-116,29","-120,35","-125,38","-128,45"
|
||||
"54,-8","0,0","3,-3","7,-6","19,-10","29,-11","34,-10","38,-9","42,-9","45,-8","49,-8"
|
||||
"-109,40","0,0","-12,1","-32,4","-57,10","-81,16","-90,19","-91,23","-95,26","-99,29","-104,33"
|
||||
"61,152","0,0","5,5","16,21","36,47","53,78","60,112","60,127","61,133","61,139","62,146"
|
||||
"-28,-124","0,0","-4,-15","-12,-35","-25,-54","-33,-73","-32,-88","-27,-97","-27,-102","-27,-107","-27,-112"
|
||||
"46,-133","0,0","3,-8","13,-25","23,-43","29,-59","34,-78","40,-102","45,-121","46,-125","46,-129"
|
||||
"-163,-133","0,0","-5,-1","-28,-17","-70,-51","-104,-79","-123,-92","-134,-102","-147,-112","-157,-125","-163,-133"
|
||||
"21,-18","0,0","2,-3","4,-7","7,-10","10,-12","11,-13","13,-14","15,-15","17,-16","19,-18"
|
||||
"-130,49","0,0","-5,2","-22,9","-48,14","-79,16","-98,18","-103,21","-109,28","-117,36","-123,42"
|
||||
"1,-135","0,0","3,-14","6,-40","5,-62","1,-81","-1,-95","-1,-108","-1,-119","3,-128","4,-133"
|
||||
"195,122","0,0","7,7","34,27","79,55","123,76","149,88","154,93","156,98","173,106","194,113"
|
||||
"-14,-86","0,0","-6,-2","-11,-3","-15,-5","-15,-13","-17,-28","-18,-47","-20,-58","-20,-64","-17,-75"
|
||||
"40,132","0,0","-2,7","-3,12","2,23","13,47","22,75","28,102","33,116","36,120","38,126"
|
||||
"-98,-108","0,0","-4,-9","-16,-26","-42,-50","-63,-71","-79,-85","-85,-91","-87,-94","-89,-98","-91,-101"
|
||||
"-111,29","0,0","-8,-2","-30,-2","-55,0","-72,6","-81,11","-86,17","-93,22","-100,24","-106,26"
|
||||
"-90,-68","0,0","-8,-4","-25,-12","-44,-24","-60,-36","-72,-49","-77,-56","-80,-59","-82,-61","-85,-64"
|
||||
"109,65","0,0","3,3","14,10","32,21","53,32","70,42","85,50","95,56","99,59","103,62"
|
||||
"-119,-158","0,0","-9,-10","-30,-37","-62,-80","-89,-111","-96,-123","-99,-131","-103,-138","-109,-147","-115,-154"
|
||||
"-178,6","0,0","-8,-1","-29,-5","-64,-10","-104,-10","-137,-7","-151,-4","-156,-3","-160,-1","-166,3"
|
||||
"94,156","0,0","0,10","20,34","39,74","54,100","65,111","91,129","101,141","102,153","94,156"
|
||||
"-24,91","0,0","0,6","-4,18","-8,34","-13,46","-16,53","-17,61","-17,72","-17,80","-18,85"
|
||||
"-2,-170","0,0","3,-9","7,-32","3,-58","0,-92","1,-124","-4,-134","-8,-139","-9,-144","-9,-152"
|
||||
"-36,-116","0,0","0,-11","-3,-33","-11,-52","-19,-72","-24,-92","-25,-102","-27,-105","-29,-108","-32,-112"
|
||||
"-16,-129","0,0","-1,-6","-3,-17","-9,-36","-17,-57","-25,-76","-28,-90","-29,-99","-30,-107","-26,-117"
|
||||
"-35,8","0,0","-3,0","-7,0","-12,0","-17,0","-21,1","-24,1","-26,2","-29,3","-31,5"
|
||||
"52,-83","0,0","3,-4","10,-18","17,-34","21,-48","25,-61","30,-71","37,-78","42,-83","44,-86"
|
||||
"94,8","0,0","3,1","8,1","15,1","25,2","36,3","46,4","57,4","68,4","78,4"
|
||||
"-97,61","0,0","-5,4","-12,12","-22,23","-32,34","-44,42","-57,49","-70,52","-81,53","-89,54"
|
||||
"18,59","0,0","2,2","5,11","7,17","12,25","15,31","17,37","18,43","18,49","18,53"
|
||||
"-79,-38","0,0","-4,-2","-11,-6","-20,-11","-30,-15","-38,-19","-45,-23","-52,-26","-59,-29","-65,-32"
|
||||
"-158,24","0,0","-11,-5","-30,-10","-46,-11","-63,-7","-85,2","-110,11","-119,15","-130,20","-150,23"
|
||||
"176,-89","0,0","13,-8","52,-27","95,-49","126,-61","137,-65","145,-72","155,-81","166,-89","176,-89"
|
||||
"51,-110","0,0","1,-11","6,-30","17,-51","25,-64","29,-75","32,-85","39,-97","43,-102","46,-105"
|
||||
"-9,-148","0,0","0,-6","-2,-19","-8,-39","-13,-64","-14,-91","-14,-114","-15,-127","-17,-133","-18,-138"
|
||||
"-51,-41","0,0","-3,-2","-7,-5","-13,-10","-23,-17","-32,-23","-41,-27","-47,-30","-50,-33","-52,-36"
|
||||
"160,-6","0,0","8,0","31,-5","66,-5","90,-4","105,-8","121,-12","140,-12","162,-12","164,-9"
|
||||
"7,-84","0,0","-1,-1","1,-8","2,-16","1,-27","-1,-41","-1,-57","1,-70","3,-76","4,-80"
|
||||
"-155,-175","0,0","-5,-19","-30,-64","-78,-113","-111,-140","-122,-151","-128,-156","-136,-164","-143,-171","-150,-173"
|
||||
"-45,-187","0,0","-1,-10","-7,-41","-18,-68","-29,-100","-38,-133","-44,-151","-48,-161","-49,-171","-47,-180"
|
||||
"159,-69","0,0","7,-3","24,-12","59,-26","107,-46","167,-66","197,-69","194,-69","185,-69","173,-69"
|
||||
"-76,74","0,0","-10,16","-46,52","-82,82","-92,113","-105,136","-90,144","-99,156","-93,136","-83,107"
|
||||
"-103,144","0,0","-7,16","-20,54","-37,93","-60,121","-66,133","-72,134","-79,134","-88,137","-103,144"
|
||||
"5,108","0,0","4,-1","7,4","9,14","12,31","16,46","18,57","17,68","13,82","9,93"
|
||||
"69,-138","0,0","3,-11","8,-39","15,-75","22,-95","29,-106","45,-126","58,-142","63,-146","65,-141"
|
||||
"-160,30","0,0","-2,6","-15,10","-37,15","-65,23","-110,33","-138,36","-147,38","-152,39","-157,36"
|
||||
"160,13","0,0","3,0","8,0","20,-2","44,-3","70,-3","93,0","114,4","134,7","150,10"
|
||||
"-64,77","0,0","-2,0","-4,6","-13,16","-28,32","-42,46","-51,56","-56,61","-60,66","-62,69"
|
||||
"-143,184","0,0","-3,3","-6,8","-16,23","-32,45","-51,73","-74,103","-103,135","-124,156","-131,167"
|
||||
"26,-152","0,0","3,-14","11,-46","19,-81","25,-106","27,-116","28,-121","29,-126","30,-134","32,-143"
|
||||
"0,72","0,0","1,4","2,10","3,14","3,20","3,30","3,42","3,52","3,60","1,68"
|
||||
"-11,151","0,0","0,5","0,12","-1,55","-1,82","-2,105","-7,125","-9,138","-10,145","-11,151"
|
||||
"-6,-95","0,0","0,-10","0,-22","-1,-38","-3,-55","-6,-70","-7,-77","-7,-81","-6,-86","-6,-89"
|
||||
"-9,40","0,0","0,3","0,6","-1,9","-1,12","-2,15","-4,21","-5,27","-6,32","-7,36"
|
||||
"-24,-62","0,0","0,-5","-1,-12","-5,-22","-9,-31","-14,-39","-18,-44","-19,-48","-20,-51","-21,-55"
|
||||
"25,10","0,0","2,1","4,1","6,2","8,3","11,4","13,6","16,6","18,8","20,9"
|
||||
"89,147","0,0","4,3","13,9","25,23","40,48","64,90","74,113","80,131","84,141","85,145"
|
||||
"195,-73","0,0","17,-8","49,-25","86,-42","115,-52","139,-59","160,-66","178,-72","187,-74","192,-75"
|
||||
"-144,26","0,0","-1,2","-23,6","-78,15","-125,25","-164,30","-184,32","-183,32","-168,27","-155,26"
|
||||
"154,-148","0,0","2,-10","17,-35","43,-60","76,-80","115,-105","142,-121","146,-125","150,-131","151,-138"
|
||||
"-145,94","0,0","-8,4","-23,11","-46,24","-72,37","-99,51","-122,64","-133,72","-136,77","-138,82"
|
||||
"-119,-52","0,0","-10,-2","-44,-15","-75,-26","-91,-29","-97,-32","-105,-37","-112,-40","-115,-45","-119,-52"
|
||||
"-139,208","0,0","-8,13","-43,52","-82,104","-100,132","-105,145","-113,163","-121,175","-131,193","-134,200"
|
||||
"95,-61","0,0","3,-4","8,-9","16,-18","26,-27","38,-36","52,-44","68,-52","81,-56","89,-59"
|
||||
"0,37","0,0","0,1","0,2","0,3","0,4","0,5","0,6","0,7","0,9","0,11"
|
||||
"-55,73","0,0","-5,7","-16,19","-29,34","-43,48","-49,53","-50,57","-52,60","-54,63","-55,68"
|
||||
"122,-114","0,0","-3,-4","-2,-10","8,-22","21,-36","38,-50","63,-69","88,-88","110,-104","116,-109"
|
||||
"52,-183","0,0","5,-14","13,-45","23,-88","31,-127","52,-169","71,-197","67,-197","62,-193","57,-186"
|
||||
"179,-29","0,0","12,-6","41,-12","81,-15","113,-17","134,-19","141,-20","148,-21","162,-24","173,-27"
|
||||
"-6,127","0,0","0,6","-2,17","-3,32","-3,51","-4,72","-5,92","-5,103","-4,114","-4,122"
|
||||
"-157,13","0,0","-19,-3","-68,-4","-124,-8","-175,-10","-182,-7","-185,-1","-184,3","-172,8","-163,12"
|
||||
"-65,-119","0,0","-10,-19","-23,-40","-39,-60","-51,-74","-53,-79","-55,-86","-58,-96","-59,-102","-60,-108"
|
||||
"138,154","0,0","10,4","31,19","59,41","87,69","105,96","112,112","120,125","127,139","134,148"
|
||||
"166,-100","0,0","4,-6","16,-17","31,-28","55,-45","91,-68","129,-90","143,-96","149,-97","155,-100"
|
||||
"-81,-53","0,0","-10,-5","-24,-14","-38,-24","-53,-31","-58,-33","-62,-36","-67,-40","-72,-44","-76,-49"
|
||||
"163,-4","0,0","10,-3","40,-5","78,-5","113,-5","131,-7","139,-7","152,-6","157,-5","163,-4"
|
||||
"85,-17","0,0","4,-2","35,-10","48,-11","61,-12","71,-14","77,-15","80,-15","83,-16","85,-17"
|
||||
"-11,-34","0,0","0,-3","0,-8","-2,-13","-5,-19","-8,-24","-10,-28","-11,-29","-11,-32","-11,-34"
|
||||
"34,32","0,0","2,0","4,0","12,4","17,8","22,13","26,17","30,22","32,27","34,30"
|
||||
"66,72","0,0","6,3","14,10","27,22","35,31","43,40","58,57","75,74","81,82","73,76"
|
||||
"127,-32","0,0","6,-1","20,-6","44,-13","76,-18","101,-20","112,-22","116,-24","118,-26","121,-28"
|
||||
"5,177","0,0","0,10","0,39","-1,83","0,111","0,126","1,144","2,152","2,160","4,167"
|
||||
"-16,126","0,0","0,9","-1,26","-5,52","-10,76","-14,94","-15,102","-16,106","-16,111","-16,116"
|
||||
"113,-60","0,0","26,-24","47,-39","68,-50","84,-59","96,-65","101,-68","104,-69","108,-69","111,-67"
|
||||
"-85,-53","0,0","-5,-3","-19,-13","-47,-24","-70,-32","-84,-38","-89,-42","-91,-44","-90,-48","-85,-53"
|
||||
"-92,-139","0,0","-5,-4","-15,-18","-25,-40","-37,-65","-48,-82","-57,-94","-62,-99","-65,-105","-72,-119"
|
||||
"165,6","0,0","3,2","12,2","29,2","55,3","81,5","101,7","116,7","136,6","153,5"
|
||||
"-34,-167","0,0","4,-25","4,-57","-6,-86","-14,-117","-16,-138","-25,-150","-32,-153","-36,-160","-38,-170"
|
||||
"159,125","0,0","3,4","18,10","48,23","95,45","118,64","125,75","130,86","140,102","151,115"
|
||||
"30,66","0,0","-1,-3","0,-2","4,1","7,7","12,19","18,35","23,48","28,57","29,62"
|
||||
"158,-43","0,0","4,-1","9,-5","23,-13","52,-25","90,-36","114,-40","124,-42","128,-43","137,-43"
|
||||
"-144,-54","0,0","-13,1","-29,-5","-51,-15","-76,-25","-100,-35","-125,-46","-141,-50","-146,-52","-144,-54"
|
||||
"-125,8","0,0","-13,-2","-34,-4","-46,-4","-58,-2","-72,2","-88,6","-97,8","-104,8","-111,8"
|
||||
"95,-154","0,0","3,-18","19,-58","42,-104","64,-137","65,-144","70,-149","75,-153","81,-155","89,-155"
|
||||
"124,33","0,0","8,0","21,-1","43,-1","73,1","103,7","125,12","129,15","129,20","130,24"
|
||||
"65,-65","0,0","3,-3","8,-11","17,-23","30,-36","43,-48","52,-54","55,-56","57,-58","60,-62"
|
||||
"140,161","0,0","9,1","27,11","54,38","94,70","103,82","108,101","117,123","129,139","136,152"
|
||||
"-54,-99","0,0","-2,-10","-12,-34","-32,-62","-49,-84","-57,-95","-59,-101","-59,-106","-59,-108","-57,-104"
|
||||
"4,26","0,0","0,2","0,4","1,6","1,9","2,12","3,15","3,18","4,20","4,22"
|
||||
"-131,-98","0,0","-12,-9","-30,-24","-58,-49","-80,-65","-92,-72","-103,-79","-108,-82","-114,-85","-120,-90"
|
||||
"-4,-21","0,0","-1,-3","-2,-6","-3,-9","-5,-12","-5,-14","-5,-16","-5,-18","-5,-20","-4,-21"
|
||||
"64,-18","0,0","5,-2","12,-6","24,-12","42,-17","64,-22","78,-25","78,-25","75,-23","71,-21"
|
||||
"-33,141","0,0","2,9","-2,27","-13,57","-20,80","-23,100","-24,121","-28,138","-32,147","-33,141"
|
||||
"-25,56","0,0","-1,4","-2,7","-2,10","-3,14","-4,20","-7,26","-12,36","-17,44","-20,50"
|
||||
"-5,157","0,0","-1,17","0,53","4,86","6,111","7,119","4,127","3,136","1,142","-2,151"
|
||||
"51,-82","0,0","6,-12","19,-31","30,-50","38,-66","41,-77","43,-84","47,-95","51,-98","51,-88"
|
||||
"-133,-57","0,0","-7,-4","-22,-13","-45,-23","-77,-31","-103,-37","-109,-40","-114,-42","-119,-44","-123,-47"
|
||||
"-111,-14","0,0","-6,0","-17,-4","-32,-10","-52,-15","-68,-16","-92,-17","-96,-16","-99,-15","-103,-14"
|
||||
"138,32","0,0","10,0","41,1","86,5","128,13","146,16","150,19","153,23","151,27","147,29"
|
||||
"95,-156","0,0","4,-8","14,-24","31,-49","48,-74","59,-94","65,-112","69,-123","78,-138","88,-149"
|
||||
"88,48","0,0","13,1","35,6","59,15","71,23","72,27","75,34","80,40","83,45","85,48"
|
||||
"-87,0","0,0","-5,-2","-14,-4","-27,-6","-41,-6","-59,-5","-75,-3","-87,-3","-94,-1","-95,0"
|
||||
"89,187","0,0","9,15","23,45","42,89","53,121","55,139","61,153","72,165","78,171","83,177"
|
||||
"123,-170","0,0","5,-8","13,-24","29,-52","55,-87","85,-121","106,-143","117,-153","118,-157","118,-162"
|
||||
"78,-63","0,0","0,2","4,-3","9,-7","13,-11","18,-15","25,-20","36,-28","51,-40","65,-52"
|
||||
"36,87","0,0","3,5","6,18","10,36","18,56","25,69","27,75","27,80","29,84","33,85"
|
||||
"148,83","0,0","22,10","60,33","72,44","82,53","103,65","116,72","127,79","132,83","140,83"
|
||||
"-63,-12","0,0","-3,-2","-6,-5","-12,-8","-23,-12","-37,-15","-49,-17","-57,-18","-59,-16","-61,-14"
|
||||
"118,27","0,0","7,5","30,14","47,18","69,19","88,20","97,21","101,22","108,24","115,25"
|
||||
"-113,-49","0,0","-4,-4","-16,-12","-32,-21","-50,-27","-63,-30","-70,-31","-74,-32","-80,-34","-90,-38"
|
||||
"-38,189","0,0","0,28","-1,59","-4,89","-8,115","-11,139","-14,156","-22,178","-28,182","-33,185"
|
||||
"-93,-99","0,0","-4,-6","-15,-15","-38,-32","-56,-50","-63,-61","-67,-66","-72,-72","-83,-83","-90,-95"
|
||||
"114,33","0,0","2,2","8,2","27,2","58,6","93,12","117,19","130,25","127,27","123,29"
|
||||
"140,110","0,0","2,4","24,13","57,27","86,42","102,56","111,67","115,73","124,81","136,94"
|
||||
"-3,-140","0,0","0,-18","-12,-52","-20,-93","-12,-136","-8,-154","-8,-160","-5,-163","-5,-157","-4,-149"
|
||||
"-119,-97","0,0","-14,-14","-61,-51","-80,-60","-89,-65","-94,-68","-105,-76","-116,-85","-120,-89","-121,-94"
|
||||
"36,101","0,0","2,2","4,5","9,13","16,28","26,48","33,67","37,83","38,90","38,94"
|
||||
"151,96","0,0","8,5","20,10","53,26","87,44","97,51","105,60","123,72","141,78","144,85"
|
||||
"148,190","0,0","21,17","46,39","82,76","120,114","128,136","131,154","137,168","141,177","143,185"
|
||||
"25,120","0,0","7,10","19,22","26,34","31,54","31,71","31,82","30,92","30,99","28,104"
|
||||
"161,161","0,0","5,2","33,19","75,48","110,75","133,96","145,116","153,135","157,150","161,158"
|
||||
"-95,46","0,0","-7,0","-22,2","-47,8","-65,15","-74,21","-86,28","-88,31","-90,37","-93,42"
|
||||
"-190,38","0,0","-4,0","-17,0","-45,5","-84,14","-121,22","-152,29","-174,36","-186,38","-190,38"
|
||||
"-120,61","0,0","-10,6","-24,17","-35,28","-49,37","-65,43","-77,44","-90,46","-101,50","-107,54"
|
||||
"102,33","0,0","11,-1","38,-3","63,-3","84,3","98,9","102,12","102,16","102,21","102,28"
|
||||
"-26,-38","0,0","-2,-5","-6,-11","-10,-16","-12,-19","-14,-21","-16,-25","-20,-28","-23,-31","-24,-35"
|
||||
"-59,102","0,0","-8,17","-16,39","-19,56","-26,72","-31,79","-38,87","-44,92","-50,96","-59,102"
|
||||
"84,84","0,0","-2,2","6,6","24,17","45,28","69,45","79,58","83,67","84,71","84,75"
|
||||
"23,126","0,0","2,2","5,5","10,11","16,20","22,30","27,52","27,74","26,96","25,108"
|
||||
"-17,-63","0,0","0,-10","-1,-20","-4,-30","-9,-38","-13,-45","-15,-51","-15,-55","-15,-58","-16,-60"
|
||||
"55,90","0,0","0,5","3,9","7,15","13,22","21,30","29,41","37,53","44,64","50,74"
|
||||
"-8,-89","0,0","2,-5","3,-13","4,-25","2,-39","0,-53","-3,-65","-4,-73","-5,-77","-6,-80"
|
||||
"-73,-74","0,0","-6,-8","-18,-22","-36,-33","-50,-42","-58,-48","-62,-52","-63,-58","-66,-63","-69,-67"
|
||||
"93,-93","0,0","7,-8","19,-22","34,-41","47,-57","58,-69","67,-79","78,-86","89,-92","93,-93"
|
||||
"53,191","0,0","-2,7","3,25","14,64","32,105","46,134","51,152","51,167","52,176","53,184"
|
||||
"-42,10","0,0","-4,1","-7,3","-11,5","-16,6","-20,7","-25,8","-30,8","-34,9","-38,9"
|
||||
"117,-79","0,0","7,-3","26,-22","51,-46","80,-67","102,-75","112,-77","123,-81","125,-82","120,-81"
|
||||
"40,43","0,0","3,6","10,16","15,22","20,28","23,31","26,33","28,36","30,40","33,41"
|
||||
"93,69","0,0","9,0","25,2","41,12","53,30","61,39","65,42","71,48","79,57","85,64"
|
||||
"80,117","0,0","-3,1","2,4","13,19","25,42","36,62","51,76","61,84","67,93","72,103"
|
||||
"-201,45","0,0","-8,2","-33,1","-72,6","-108,9","-115,12","-127,20","-141,27","-150,31","-169,38"
|
||||
"142,135","0,0","7,2","31,18","57,45","88,68","103,84","119,96","129,103","136,112","139,118"
|
||||
"-115,-8","0,0","-3,2","-8,0","-20,-2","-38,-4","-58,-7","-75,-10","-89,-12","-103,-12","-108,-11"
|
||||
"154,-100","0,0","4,0","15,-9","34,-25","61,-44","84,-62","112,-84","140,-100","146,-100","150,-100"
|
||||
"108,130","0,0","5,2","18,13","37,35","54,62","68,86","81,103","89,115","96,122","102,127"
|
||||
"-156,56","0,0","-7,2","-29,6","-70,14","-98,21","-121,29","-137,38","-142,42","-146,45","-149,48"
|
||||
"144,114","0,0","8,5","31,16","83,50","117,66","135,76","139,81","141,85","142,101","144,114"
|
||||
"120,22","0,0","2,2","5,2","8,2","20,3","32,5","45,8","59,12","76,15","94,18"
|
||||
"165,171","0,0","10,9","40,24","76,55","118,94","155,126","172,140","174,146","177,156","178,164"
|
||||
"-174,173","0,0","-9,16","-35,49","-58,79","-86,106","-120,132","-141,148","-154,158","-160,163","-165,168"
|
||||
"-99,-45","0,0","-10,-9","-28,-21","-48,-29","-68,-33","-78,-35","-83,-37","-86,-39","-90,-42","-95,-43"
|
||||
"69,-22","0,0","1,1","2,0","6,-2","11,-4","15,-7","21,-9","27,-12","34,-14","42,-16"
|
||||
"-60,-33","0,0","-2,-1","-5,-2","-8,-4","-12,-7","-17,-11","-23,-14","-29,-19","-36,-23","-42,-26"
|
||||
"-46,21","0,0","-3,1","-8,1","-12,2","-20,4","-26,7","-30,9","-35,13","-40,17","-46,21"
|
||||
"48,-29","0,0","1,-1","3,-3","7,-6","14,-12","22,-18","30,-23","37,-26","42,-28","46,-29"
|
||||
"-88,-111","0,0","-14,-22","-42,-51","-57,-71","-59,-78","-60,-87","-63,-91","-65,-100","-75,-104","-82,-108"
|
||||
"-145,73","0,0","-12,5","-28,13","-54,27","-78,51","-95,67","-108,74","-116,76","-128,76","-140,76"
|
||||
"-57,-95","0,0","0,-9","-8,-26","-20,-49","-27,-68","-33,-75","-38,-79","-38,-85","-43,-88","-48,-92"
|
||||
"-134,-6","0,0","-5,-2","-20,-7","-49,-10","-75,-10","-88,-9","-102,-4","-110,-1","-117,1","-125,2"
|
||||
"-22,53","0,0","0,5","-2,11","-3,16","-4,20","-5,25","-7,29","-9,33","-11,37","-13,43"
|
||||
"-100,-133","0,0","-4,-20","-19,-45","-42,-74","-54,-96","-54,-115","-54,-123","-55,-130","-62,-132","-76,-133"
|
||||
"173,82","0,0","15,4","47,13","83,29","124,45","141,58","156,66","166,71","170,76","173,82"
|
||||
"6,77","0,0","-1,4","-1,7","0,9","1,16","1,28","1,43","2,56","4,65","5,71"
|
||||
"-97,205","0,0","-4,23","-32,60","-64,98","-78,121","-83,136","-84,150","-84,168","-90,189","-94,198"
|
||||
"-88,163","0,0","-8,3","-18,15","-24,21","-36,36","-55,72","-70,99","-78,128","-82,147","-86,157"
|
||||
"-143,171","0,0","-13,12","-53,41","-103,76","-124,96","-134,115","-138,127","-143,137","-144,146","-144,159"
|
||||
"-12,177","0,0","0,6","0,16","0,43","-7,86","-9,124","-10,146","-13,158","-13,165","-13,172"
|
||||
"40,-83","0,0","7,-10","11,-18","17,-32","22,-43","26,-53","30,-62","34,-69","37,-74","38,-76"
|
||||
"-188,-37","0,0","-10,-4","-25,-12","-43,-22","-66,-27","-95,-27","-122,-27","-145,-28","-168,-30","-183,-34"
|
||||
"-118,32","0,0","-4,0","-12,0","-24,2","-42,4","-62,9","-82,15","-96,21","-107,26","-112,29"
|
||||
"201,28","0,0","25,-6","84,-9","126,-3","141,-1","152,-1","163,2","181,6","197,14","201,21"
|
||||
"84,21","0,0","6,0","16,0","30,1","44,2","58,4","70,7","79,11","81,14","82,16"
|
||||
"-5,68","0,0","-1,3","-1,8","-3,16","-5,24","-5,30","-5,36","-5,42","-5,49","-5,57"
|
||||
"-89,43","0,0","-7,1","-21,6","-34,11","-50,16","-63,19","-69,22","-75,28","-81,34","-84,37"
|
||||
"94,50","0,0","5,0","13,0","23,2","35,8","49,18","67,30","81,39","90,45","93,47"
|
||||
"-171,-163","0,0","-6,-29","-33,-68","-78,-116","-107,-140","-114,-146","-121,-148","-136,-151","-156,-157","-164,-160"
|
||||
"163,-116","0,0","7,-6","20,-20","38,-38","62,-58","93,-81","118,-99","128,-102","142,-106","157,-112"
|
||||
"-56,-44","0,0","-2,-3","-6,-8","-13,-14","-27,-23","-37,-30","-45,-36","-50,-40","-53,-41","-55,-42"
|
||||
"-156,183","0,0","-16,28","-56,81","-104,127","-124,149","-132,160","-139,169","-144,175","-151,180","-156,183"
|
||||
"179,93","0,0","5,2","19,7","42,16","72,29","108,45","144,61","165,74","171,79","174,84"
|
||||
"42,-11","0,0","-1,-1","1,-2","3,-4","6,-7","9,-10","12,-12","15,-14","32,-15","39,-13"
|
||||
"-101,24","0,0","-4,0","-12,1","-31,4","-63,12","-89,18","-103,21","-110,23","-112,25","-108,25"
|
||||
"-64,-53","0,0","-2,-7","-11,-19","-21,-27","-29,-32","-38,-39","-46,-45","-51,-48","-57,-50","-60,-52"
|
||||
"-124,137","0,0","-3,9","-7,20","-12,32","-34,63","-73,97","-94,109","-102,114","-112,123","-119,128"
|
||||
"78,-183","0,0","4,-7","14,-28","31,-60","40,-87","44,-106","50,-125","56,-141","67,-159","76,-172"
|
||||
"-132,23","0,0","-9,2","-32,6","-59,11","-81,13","-99,16","-115,18","-121,20","-124,21","-128,23"
|
||||
"180,27","0,0","19,-2","62,-3","110,3","144,5","154,6","162,9","170,13","175,17","178,22"
|
||||
"81,-6","0,0","10,-7","26,-11","43,-12","55,-11","60,-9","64,-8","68,-8","74,-8","78,-8"
|
||||
"-130,-125","0,0","-4,-7","-13,-22","-34,-42","-53,-60","-63,-69","-86,-87","-114,-107","-124,-114","-127,-118"
|
||||
"-190,179","0,0","-43,27","-113,73","-148,103","-152,115","-159,128","-172,151","-179,161","-184,165","-187,173"
|
||||
"27,96","0,0","2,6","6,14","12,26","20,44","25,61","27,70","27,77","27,85","27,91"
|
||||
"58,-10","0,0","3,-1","6,-3","10,-5","14,-6","21,-8","29,-9","38,-10","47,-10","54,-10"
|
||||
"155,-25","0,0","1,3","7,3","23,-3","50,-12","92,-21","120,-24","140,-26","148,-26","152,-26"
|
||||
"-67,-82","0,0","-6,-6","-15,-18","-24,-27","-30,-35","-37,-44","-45,-51","-54,-56","-61,-66","-65,-76"
|
||||
"101,162","0,0","-3,7","7,22","35,40","68,68","92,103","102,122","105,133","107,141","105,152"
|
||||
"95,99","0,0","2,4","6,8","13,14","22,24","34,38","48,54","61,69","75,82","86,90"
|
||||
"-9,-155","0,0","0,-10","-3,-31","-9,-60","-13,-85","-13,-104","-10,-123","-10,-139","-9,-150","-9,-155"
|
||||
"10,158","0,0","-1,6","-1,18","-1,37","0,63","3,91","7,114","10,130","11,139","10,150"
|
||||
"-10,-207","0,0","-1,-11","-2,-41","-1,-82","-1,-122","-3,-152","-7,-171","-10,-181","-9,-191","-8,-200"
|
||||
"59,218","0,0","9,31","29,79","63,144","85,175","88,185","86,195","84,206","80,217","72,219"
|
||||
"35,-93","0,0","5,-7","13,-22","20,-37","24,-47","28,-59","31,-74","31,-84","32,-87","34,-89"
|
||||
"-28,43","0,0","-2,-1","-4,0","-6,5","-9,12","-14,21","-18,29","-22,35","-25,39","-28,43"
|
||||
"-173,43","0,0","-10,0","-54,7","-111,16","-143,23","-144,29","-147,35","-153,37","-159,39","-166,40"
|
||||
"17,-149","0,0","-1,0","1,-10","5,-25","10,-44","14,-60","17,-76","20,-95","22,-116","24,-136"
|
||||
"41,92","0,0","2,6","7,14","14,26","21,36","26,50","31,64","34,75","36,82","38,85"
|
||||
"-81,72","0,0","-10,6","-25,18","-36,29","-48,41","-62,52","-70,59","-74,64","-78,68","-81,72"
|
||||
"-49,-121","0,0","-2,-16","-9,-42","-14,-80","-19,-112","-21,-123","-25,-126","-31,-127","-37,-125","-43,-122"
|
||||
"153,-39","0,0","8,-5","19,-9","39,-14","67,-20","95,-25","117,-27","136,-28","147,-30","150,-32"
|
||||
"-169,-118","0,0","-15,-8","-64,-36","-129,-68","-152,-77","-159,-82","-175,-95","-183,-106","-184,-115","-177,-118"
|
||||
"118,139","0,0","7,9","30,31","50,60","68,85","80,102","90,119","105,135","116,145","118,148"
|
||||
"50,-113","0,0","7,-15","15,-33","20,-53","25,-76","33,-99","39,-116","40,-119","43,-121","47,-118"
|
||||
"-114,157","0,0","-2,9","-17,36","-55,84","-81,109","-93,119","-101,131","-103,143","-105,150","-109,155"
|
||||
"-160,-25","0,0","-6,2","-13,4","-21,3","-41,-5","-75,-17","-103,-24","-117,-25","-127,-24","-144,-24"
|
||||
"-19,-121","0,0","1,-21","3,-53","-1,-72","-3,-84","-5,-90","-8,-99","-11,-108","-15,-112","-17,-117"
|
||||
"-121,-73","0,0","-6,-3","-18,-10","-36,-21","-59,-32","-80,-45","-98,-57","-110,-64","-118,-68","-120,-70"
|
||||
"-61,105","0,0","-6,9","-16,27","-28,47","-42,69","-48,83","-50,88","-53,92","-55,98","-58,102"
|
||||
"42,48","0,0","4,4","7,10","12,16","18,23","24,28","31,33","37,38","40,42","42,45"
|
||||
"-182,69","0,0","-6,8","-44,16","-99,21","-144,30","-154,36","-157,43","-160,48","-168,55","-178,63"
|
||||
"-15,103","0,0","-2,11","-5,32","-6,49","-7,63","-8,70","-8,78","-9,87","-11,95","-13,99"
|
||||
"41,-102","0,0","2,-7","9,-21","18,-38","27,-55","33,-71","37,-82","39,-88","40,-93","40,-96"
|
||||
"82,-168","0,0","-1,-5","4,-19","12,-39","19,-63","30,-93","51,-130","70,-154","77,-160","79,-164"
|
||||
"135,-169","0,0","5,1","18,-9","41,-36","66,-69","85,-94","101,-116","119,-140","130,-154","131,-160"
|
||||
"-157,64","0,0","-3,3","-11,8","-23,15","-44,23","-68,32","-92,40","-112,47","-128,51","-141,56"
|
||||
"171,58","0,0","10,0","57,5","104,20","133,34","141,41","148,46","160,52","165,56","171,58"
|
||||
"-148,208","0,0","-8,8","-17,16","-37,53","-69,120","-98,176","-119,209","-131,221","-140,225","-145,218"
|
||||
"-160,9","0,0","-10,-1","-27,-2","-50,-4","-72,-5","-92,-3","-114,1","-129,5","-139,6","-149,8"
|
||||
"24,31","0,0","3,1","6,3","9,7","12,11","14,15","16,18","17,20","18,23","20,26"
|
||||
"197,-170","0,0","23,-17","92,-65","167,-123","192,-145","226,-179","224,-183","215,-178","203,-173","197,-170"
|
||||
"-44,50","0,0","-1,-2","8,-6","12,-10","0,-10","-17,-3","-23,13","-32,30","-40,37","-43,43"
|
||||
"31,173","0,0","26,14","36,50","36,85","36,103","36,120","41,133","41,143","37,156","35,165"
|
||||
"-29,-41","0,0","-2,-4","-3,-6","-5,-10","-9,-17","-13,-22","-17,-28","-21,-32","-25,-36","-27,-39"
|
||||
"-89,-115","0,0","-3,-12","-12,-30","-27,-54","-41,-70","-50,-78","-61,-95","-65,-102","-69,-105","-74,-109"
|
||||
"126,-157","0,0","5,-12","18,-30","37,-54","55,-78","70,-97","81,-109","91,-120","109,-138","121,-148"
|
||||
"-39,-192","0,0","0,-15","-7,-62","-16,-92","-17,-111","-17,-124","-19,-134","-24,-145","-29,-161","-33,-176"
|
||||
"-200,58","0,0","-14,3","-61,20","-120,35","-135,41","-145,43","-159,47","-172,50","-181,53","-190,55"
|
||||
"29,20","0,0","1,0","4,1","7,3","10,5","13,8","16,10","19,13","22,15","25,18"
|
||||
"18,130","0,0","6,13","11,34","21,63","26,89","26,103","26,111","24,118","21,124","18,130"
|
||||
"100,-78","0,0","4,-4","14,-15","27,-27","44,-40","57,-49","65,-54","70,-59","80,-67","93,-75"
|
||||
"-97,-142","0,0","-19,-38","-50,-65","-59,-75","-64,-82","-71,-91","-74,-101","-78,-110","-81,-118","-90,-131"
|
||||
"155,171","0,0","10,17","38,38","68,58","87,83","106,106","119,121","136,138","146,154","152,163"
|
||||
"-136,78","0,0","-8,9","-28,26","-74,50","-119,67","-147,70","-167,74","-172,77","-152,77","-136,78"
|
||||
"10,-165","0,0","-3,-8","-8,-29","-9,-60","-2,-90","5,-115","7,-127","8,-138","7,-147","8,-157"
|
||||
"67,-140","0,0","4,-11","9,-27","21,-51","34,-72","41,-86","45,-96","51,-109","58,-124","65,-135"
|
||||
"-156,-120","0,0","-7,-7","-21,-31","-86,-89","-109,-113","-114,-116","-122,-114","-133,-113","-140,-114","-148,-117"
|
||||
"61,76","0,0","7,4","16,10","26,26","39,47","51,62","55,69","58,78","64,88","66,88"
|
||||
"116,148","0,0","7,7","21,25","40,60","61,94","78,116","89,128","96,136","104,140","109,143"
|
||||
"86,-195","0,0","3,-11","14,-43","32,-82","39,-112","54,-160","60,-179","66,-181","74,-181","79,-185"
|
||||
"-57,-164","0,0","5,-35","1,-92","-13,-123","-23,-132","-28,-151","-32,-176","-37,-181","-44,-182","-50,-180"
|
||||
"-76,190","0,0","-1,14","-7,46","-29,106","-51,144","-55,156","-55,168","-56,178","-59,184","-67,188"
|
||||
"-50,162","0,0","-2,16","-7,36","-17,65","-24,95","-30,112","-34,119","-36,127","-42,142","-49,154"
|
||||
"151,65","0,0","3,4","7,9","18,9","44,9","77,19","113,33","134,43","136,48","145,59"
|
||||
"181,-139","0,0","10,-17","48,-64","92,-111","127,-134","151,-148","172,-158","181,-160","183,-154","182,-146"
|
||||
"130,-120","0,0","8,-9","24,-25","52,-52","77,-78","92,-93","101,-100","117,-112","124,-117","130,-120"
|
||||
"-16,114","0,0","-3,4","-7,23","-10,44","-10,62","-11,76","-14,90","-16,98","-16,104","-16,109"
|
||||
"-195,91","0,0","-9,3","-33,13","-97,29","-169,43","-188,50","-187,57","-189,65","-192,72","-195,82"
|
||||
"97,31","0,0","4,1","9,1","15,2","23,4","36,9","53,15","70,21","86,27","95,29"
|
||||
"-126,31","0,0","-3,0","-7,0","-13,1","-24,3","-35,6","-48,11","-62,15","-76,18","-90,22"
|
||||
"-144,-117","0,0","-10,-6","-29,-18","-56,-36","-80,-59","-104,-83","-122,-99","-128,-107","-130,-110","-134,-113"
|
||||
"2,-191","0,0","1,-23","1,-71","-1,-117","-1,-138","-4,-149","-8,-160","-8,-167","-7,-175","-6,-181"
|
||||
"129,-74","0,0","6,-3","18,-13","38,-27","58,-41","80,-55","112,-71","134,-81","138,-84","134,-77"
|
||||
"-63,90","0,0","-2,4","-7,12","-18,28","-30,44","-44,59","-55,71","-56,76","-57,81","-60,86"
|
||||
"149,20","0,0","13,-4","48,-6","83,-5","111,-5","117,-2","123,2","126,7","130,10","139,16"
|
||||
"-72,141","0,0","-3,11","-18,41","-40,74","-56,91","-61,97","-65,109","-65,116","-67,124","-70,135"
|
||||
"19,-133","0,0","5,-13","18,-40","27,-68","28,-85","28,-94","25,-102","23,-109","21,-118","19,-127"
|
||||
"184,-66","0,0","14,-7","38,-21","76,-34","104,-41","126,-47","139,-51","145,-53","150,-55","156,-57"
|
||||
"128,-202","0,0","17,-11","49,-34","75,-61","87,-94","90,-124","106,-159","112,-168","118,-180","126,-195"
|
||||
"-79,-143","0,0","-1,-2","-1,-30","-4,-51","-11,-68","-20,-89","-30,-106","-40,-115","-51,-121","-60,-128"
|
||||
"-30,-132","0,0","2,0","2,-16","-3,-41","-12,-67","-20,-87","-24,-102","-27,-110","-29,-115","-30,-124"
|
||||
"-60,13","0,0","-1,0","-3,-2","-6,-4","-10,-5","-14,-5","-17,-4","-22,-1","-31,3","-42,7"
|
||||
"160,153","0,0","9,7","52,35","119,80","144,114","140,121","143,128","148,137","151,144","155,148"
|
||||
"-109,-47","0,0","-8,-6","-28,-21","-57,-37","-76,-47","-85,-52","-91,-52","-96,-51","-101,-51","-106,-51"
|
||||
"59,113","0,0","0,3","3,11","15,37","23,51","33,64","42,79","51,92","55,101","57,106"
|
||||
"-128,23","0,0","-13,0","-47,-2","-70,-3","-80,-2","-84,1","-90,5","-102,11","-110,16","-116,18"
|
||||
"50,108","0,0","5,11","14,33","25,53","34,68","39,79","40,86","42,91","45,94","48,99"
|
||||
"123,-61","0,0","16,-4","43,-22","73,-39","94,-48","108,-51","113,-52","116,-54","119,-57","123,-61"
|
||||
"23,63","0,0","2,5","6,13","11,19","16,25","20,32","22,39","23,47","23,55","23,60"
|
||||
"-59,-167","0,0","-6,-14","-27,-56","-45,-98","-50,-121","-46,-135","-43,-156","-47,-164","-52,-171","-56,-172"
|
||||
"-28,-53","0,0","-3,-1","-6,-6","-11,-14","-16,-22","-19,-31","-22,-39","-24,-44","-26,-47","-27,-50"
|
||||
"197,-85","0,0","8,-9","26,-23","58,-41","91,-55","120,-62","147,-67","156,-67","165,-73","180,-79"
|
||||
"-148,-23","0,0","-6,-1","-29,-6","-66,-13","-96,-20","-115,-22","-123,-22","-129,-22","-136,-22","-144,-22"
|
||||
"-102,-162","0,0","-10,-10","-30,-44","-47,-81","-54,-106","-67,-125","-79,-141","-86,-146","-91,-151","-97,-156"
|
||||
"198,157","0,0","13,10","63,39","133,80","162,104","163,119","177,135","186,142","191,148","198,152"
|
||||
"-91,177","0,0","2,6","3,19","-9,50","-23,89","-41,116","-60,134","-73,146","-82,159","-88,170"
|
||||
"40,-163","0,0","0,-11","3,-35","8,-63","13,-96","20,-122","28,-142","34,-153","35,-158","37,-162"
|
||||
"25,-149","0,0","-2,-3","0,-16","8,-49","12,-80","11,-102","10,-116","14,-127","16,-136","21,-144"
|
||||
"62,22","0,0","3,0","7,0","11,1","18,3","30,8","42,13","51,18","56,23","58,26"
|
||||
"-158,-111","0,0","-4,-3","-10,-12","-29,-31","-60,-57","-99,-84","-130,-104","-145,-112","-150,-113","-154,-111"
|
||||
"-22,53","0,0","0,4","-1,9","-2,13","-5,18","-7,26","-10,35","-12,40","-15,45","-19,50"
|
||||
"-119,188","0,0","-3,3","-12,13","-35,34","-68,66","-87,97","-100,130","-109,155","-112,169","-115,177"
|
||||
"105,102","0,0","-1,6","2,9","15,15","41,29","68,49","82,70","91,88","96,96","99,98"
|
||||
"174,-106","0,0","22,-19","62,-46","102,-66","140,-96","179,-129","194,-137","193,-128","187,-117","181,-112"
|
||||
"-64,124","0,0","-1,3","-3,7","-7,15","-15,33","-28,59","-39,81","-48,97","-55,106","-60,111"
|
||||
"-27,63","0,0","0,3","-3,8","-6,14","-11,22","-15,32","-19,45","-22,51","-23,54","-24,56"
|
||||
"-67,-158","0,0","-4,-14","-14,-46","-19,-82","-22,-101","-29,-120","-31,-146","-36,-149","-42,-150","-49,-151"
|
||||
"126,-113","0,0","24,-32","57,-67","78,-87","90,-100","104,-116","120,-127","127,-129","130,-124","127,-119"
|
||||
"32,-34","0,0","0,0","3,-2","7,-7","12,-12","17,-16","22,-21","25,-24","28,-26","28,-28"
|
||||
"111,9","0,0","4,0","11,-2","19,-2","28,-3","43,-3","59,-2","74,-1","85,2","96,4"
|
||||
"166,183","0,0","15,12","49,39","73,67","89,108","114,143","123,160","130,171","137,173","145,175"
|
||||
"149,7","0,0","2,-1","10,-1","23,-1","43,2","66,6","82,8","100,8","125,7","142,7"
|
||||
"-49,-28","0,0","-3,-1","-10,-6","-21,-15","-27,-20","-30,-22","-33,-23","-37,-23","-41,-24","-44,-25"
|
||||
"173,-21","0,0","3,-1","10,-4","47,-10","70,-10","91,-10","111,-11","124,-11","130,-12","137,-16"
|
||||
"-75,130","0,0","1,5","1,11","-1,23","-6,39","-17,68","-39,99","-62,120","-70,125","-73,127"
|
||||
"-153,46","0,0","-4,0","-11,0","-24,3","-44,9","-69,17","-92,26","-115,34","-135,42","-148,46"
|
||||
"-119,6","0,0","-4,0","-10,1","-19,1","-30,2","-47,3","-65,4","-83,4","-101,5","-119,6"
|
||||
"131,-9","0,0","8,-1","21,-5","46,-10","75,-13","106,-14","129,-13","139,-9","142,-7","136,-8"
|
||||
"-82,-134","0,0","-3,-32","-21,-74","-40,-101","-49,-109","-54,-120","-57,-134","-60,-141","-65,-144","-75,-141"
|
||||
"-141,51","0,0","-6,6","-24,15","-57,29","-84,37","-99,43","-106,48","-114,52","-125,53","-131,53"
|
||||
"189,-189","0,0","9,-15","27,-41","55,-74","96,-115","132,-144","161,-167","176,-179","179,-181","183,-184"
|
||||
|
20
data/mouse_data_test.csv
Normal file
20
data/mouse_data_test.csv
Normal file
@@ -0,0 +1,20 @@
|
||||
"-122,-17","0,0","-3,-3","-9,-7","-19,-11","-32,-16","-51,-19","-68,-20","-84,-20","-102,-19","-114,-17"
|
||||
"-115,-153","0,0","-10,-13","-34,-62","-68,-105","-74,-128","-78,-139","-82,-147","-89,-152","-98,-155","-107,-156"
|
||||
"-77,193","0,0","-2,9","-12,31","-26,68","-41,106","-54,136","-63,156","-67,170","-71,178","-75,183"
|
||||
"-85,-86","0,0","-5,-5","-20,-19","-47,-36","-69,-50","-80,-57","-85,-61","-85,-66","-85,-71","-85,-77"
|
||||
"29,-175","0,0","9,-10","25,-51","32,-90","30,-119","30,-138","30,-146","29,-152","33,-165","36,-175"
|
||||
"-201,197","0,0","-12,20","-64,77","-113,129","-130,160","-139,175","-152,178","-171,181","-184,183","-191,187"
|
||||
"-124,-16","0,0","-6,-1","-20,-4","-42,-8","-67,-13","-90,-16","-109,-19","-116,-21","-119,-20","-122,-18"
|
||||
"-123,53","0,0","-16,7","-47,17","-73,22","-92,24","-106,29","-109,32","-112,38","-115,45","-118,49"
|
||||
"-146,-125","0,0","0,-12","-12,-40","-33,-58","-64,-84","-95,-115","-116,-123","-126,-125","-134,-122","-141,-122"
|
||||
"132,49","0,0","9,5","32,11","56,18","81,30","95,36","102,39","108,41","114,44","123,48"
|
||||
"157,164","0,0","9,10","36,35","72,68","107,97","132,121","146,137","149,145","151,152","154,158"
|
||||
"-52,48","0,0","-5,5","-14,11","-20,18","-26,24","-29,30","-30,36","-31,41","-32,47","-40,48"
|
||||
"-135,-106","0,0","-13,-15","-34,-37","-56,-61","-75,-78","-86,-90","-93,-98","-99,-104","-104,-105","-116,-107"
|
||||
"197,99","0,0","3,6","23,21","62,33","111,48","157,63","177,74","181,82","184,86","192,91"
|
||||
"129,-194","0,0","7,-7","19,-26","45,-55","77,-85","104,-117","117,-139","118,-149","119,-162","123,-180"
|
||||
"167,-56","0,0","14,-6","42,-15","79,-23","115,-31","138,-39","149,-45","152,-48","155,-50","159,-51"
|
||||
"132,143","0,0","28,4","84,24","113,57","116,77","118,88","120,98","120,107","122,124","126,135"
|
||||
"-113,89","0,0","-11,8","-38,23","-71,41","-81,48","-86,56","-89,62","-92,67","-97,73","-103,79"
|
||||
"-46,-8","0,0","-2,1","-4,1","-7,1","-11,0","-16,-2","-24,-4","-30,-5","-35,-7","-39,-8"
|
||||
"-40,181","0,0","0,37","-11,92","-12,120","-12,133","-12,148","-14,168","-21,178","-25,184","-32,184"
|
||||
|
@@ -1 +0,0 @@
|
||||
|
||||
|
|
@@ -1 +0,0 @@
|
||||
|
||||
|
|
51
pyproject.toml
Normal file
51
pyproject.toml
Normal file
@@ -0,0 +1,51 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mouse-control"
|
||||
version = "0.1.0"
|
||||
description = "Neural-network mouse trajectory generation that mimics human motion."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
keywords = ["mouse", "trajectory", "neural-network", "onnx", "anti-detection"]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
]
|
||||
dependencies = [
|
||||
"matplotlib>=3.10",
|
||||
"numpy>=2.0",
|
||||
"onnxruntime>=1.20",
|
||||
"onnxscript>=0.7",
|
||||
"pandas>=2.0",
|
||||
"scipy>=1.14",
|
||||
"torch>=2.5",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
mouse-collect = "mouse_control.collect:main"
|
||||
mouse-train = "mouse_control.train:main"
|
||||
mouse-visualize = "mouse_control.visualize:main"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["pytest>=8.0"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = "-ra"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/mouse_control"]
|
||||
artifacts = ["src/mouse_control/assets/*.onnx"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = [
|
||||
"src/",
|
||||
"data/",
|
||||
"imgs/",
|
||||
"README.md",
|
||||
"pyproject.toml",
|
||||
]
|
||||
25
show.py
25
show.py
@@ -1,25 +0,0 @@
|
||||
import matplotlib.pyplot as plt
|
||||
import torch
|
||||
|
||||
# 您的tensor数据
|
||||
points = torch.tensor([
|
||||
[ 0.4762, 0.9082],
|
||||
[ -31.5831, -39.9315],
|
||||
[ -52.1844, -71.0071],
|
||||
[ -60.4211, -83.4873],
|
||||
[ -69.3365, -100.6035],
|
||||
[ -80.3101, -110.0435],
|
||||
[ -86.7765, -120.0472],
|
||||
[ -93.2574, -127.8565],
|
||||
[ -98.5992, -135.3300]
|
||||
])
|
||||
|
||||
# 提取x和y坐标
|
||||
x = points[:, 0].tolist()
|
||||
y = points[:, 1].tolist()
|
||||
|
||||
# 绘制散点图
|
||||
plt.scatter(x, y)
|
||||
|
||||
# 显示图形
|
||||
plt.show()
|
||||
7
src/mouse_control/__init__.py
Normal file
7
src/mouse_control/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Neural-network mouse trajectory generation that mimics human motion."""
|
||||
|
||||
from mouse_control.inference import load_model, predict
|
||||
from mouse_control.model import SimpleNet
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__all__ = ["SimpleNet", "load_model", "predict", "__version__"]
|
||||
0
src/mouse_control/assets/__init__.py
Normal file
0
src/mouse_control/assets/__init__.py
Normal file
BIN
src/mouse_control/assets/mouse.onnx
Normal file
BIN
src/mouse_control/assets/mouse.onnx
Normal file
Binary file not shown.
133
src/mouse_control/collect.py
Normal file
133
src/mouse_control/collect.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import argparse
|
||||
import csv
|
||||
import random
|
||||
import tkinter as tk
|
||||
from pathlib import Path
|
||||
|
||||
BG_COLOR = "#2b2b2b"
|
||||
BALL_RADIUS = 20
|
||||
TARGET_OFFSET_RANGE = 200
|
||||
N_KEYPOINTS = 10
|
||||
|
||||
|
||||
class TrajectoryCollector:
|
||||
def __init__(self, csv_path: Path, n_trajectories: int = 100) -> None:
|
||||
self.csv_path = Path(csv_path)
|
||||
self.n_target = n_trajectories
|
||||
|
||||
self.root = tk.Tk()
|
||||
self.root.attributes("-fullscreen", True)
|
||||
self.root.configure(bg=BG_COLOR)
|
||||
|
||||
screen_w = self.root.winfo_screenwidth()
|
||||
screen_h = self.root.winfo_screenheight()
|
||||
self.ball1_pos = (screen_w / 2, screen_h / 2)
|
||||
self.ball2_pos = self._new_target()
|
||||
|
||||
self.label = tk.Label(
|
||||
self.root, text="n: 0", font=("Helvetica", 16),
|
||||
bg=BG_COLOR, fg="white",
|
||||
)
|
||||
self.label.pack()
|
||||
self.canvas = tk.Canvas(
|
||||
self.root, width=screen_w, height=screen_h,
|
||||
bg=BG_COLOR, highlightthickness=0,
|
||||
)
|
||||
self.canvas.pack()
|
||||
self._draw_ball(self.ball1_pos, "red")
|
||||
self._draw_ball(self.ball2_pos, "blue", tags="ball2")
|
||||
|
||||
self.recording = False
|
||||
self.mouse_path: list[tuple[int, int]] = []
|
||||
self.n = 0
|
||||
|
||||
self.canvas.bind("<Motion>", self._on_motion)
|
||||
self.canvas.bind("<Button-1>", self._on_click)
|
||||
self.root.bind("<Key>", self._on_key)
|
||||
|
||||
def _new_target(self) -> tuple[float, float]:
|
||||
cx, cy = self.ball1_pos
|
||||
return (
|
||||
cx + random.randint(-TARGET_OFFSET_RANGE, TARGET_OFFSET_RANGE),
|
||||
cy + random.randint(-TARGET_OFFSET_RANGE, TARGET_OFFSET_RANGE),
|
||||
)
|
||||
|
||||
def _draw_ball(self, pos: tuple[float, float], color: str, tags: str = "") -> None:
|
||||
x, y = pos
|
||||
self.canvas.create_oval(
|
||||
x - BALL_RADIUS, y - BALL_RADIUS,
|
||||
x + BALL_RADIUS, y + BALL_RADIUS,
|
||||
fill=color, tags=tags,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _hit(event: tk.Event, pos: tuple[float, float]) -> bool:
|
||||
return (
|
||||
abs(event.x - pos[0]) <= BALL_RADIUS
|
||||
and abs(event.y - pos[1]) <= BALL_RADIUS
|
||||
)
|
||||
|
||||
def _on_motion(self, event: tk.Event) -> None:
|
||||
if self.recording:
|
||||
self.mouse_path.append((event.x, event.y))
|
||||
|
||||
def _on_click(self, event: tk.Event) -> None:
|
||||
if self._hit(event, self.ball1_pos):
|
||||
self.recording = True
|
||||
self.mouse_path = [(event.x, event.y)]
|
||||
elif self._hit(event, self.ball2_pos):
|
||||
if not self.recording or len(self.mouse_path) < N_KEYPOINTS:
|
||||
return
|
||||
self.recording = False
|
||||
self.canvas.delete("ball2")
|
||||
self._save_path(self.mouse_path)
|
||||
self.n += 1
|
||||
if self.n >= self.n_target:
|
||||
self.root.destroy()
|
||||
return
|
||||
self.label.config(text=f"n: {self.n}")
|
||||
self.mouse_path = []
|
||||
self.ball2_pos = self._new_target()
|
||||
self._draw_ball(self.ball2_pos, "blue", tags="ball2")
|
||||
|
||||
def _on_key(self, event: tk.Event) -> None:
|
||||
if event.keysym == "Escape":
|
||||
self.root.destroy()
|
||||
|
||||
def _save_path(self, path: list[tuple[int, int]]) -> None:
|
||||
origin_x, origin_y = path[0]
|
||||
x_rel = [px - origin_x for px, _ in path]
|
||||
y_rel = [-(py - origin_y) for _, py in path]
|
||||
step = max(1, len(path) // N_KEYPOINTS)
|
||||
idx = list(range(0, len(path), step))
|
||||
kx = [x_rel[i] for i in idx]
|
||||
ky = [y_rel[i] for i in idx]
|
||||
with open(self.csv_path, "a", newline="") as f:
|
||||
writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
|
||||
row = [f"{kx[-1]},{ky[-1]}"] + [
|
||||
f"{kx[i]},{ky[i]}" for i in range(N_KEYPOINTS)
|
||||
]
|
||||
writer.writerow(row)
|
||||
|
||||
def run(self) -> None:
|
||||
self.root.mainloop()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Collect human mouse trajectories via a fullscreen Tk GUI.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", "-o", type=Path, default=Path("mouse_data.csv"),
|
||||
help="CSV file to append trajectories to (default: mouse_data.csv).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--count", "-n", type=int, default=100,
|
||||
help="Number of trajectories to collect before exiting (default: 100).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
TrajectoryCollector(args.output, args.count).run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
28
src/mouse_control/inference.py
Normal file
28
src/mouse_control/inference.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from importlib.resources import files
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
|
||||
|
||||
def load_model(model_path: str | Path | None = None) -> ort.InferenceSession:
|
||||
"""Load the ONNX mouse-trajectory model.
|
||||
|
||||
Resolution order when ``model_path`` is None:
|
||||
1. ``./mouse.onnx`` in the current working directory (freshly trained).
|
||||
2. The model bundled inside the installed package.
|
||||
"""
|
||||
if model_path is not None:
|
||||
return ort.InferenceSession(str(model_path))
|
||||
cwd_model = Path("mouse.onnx")
|
||||
if cwd_model.is_file():
|
||||
return ort.InferenceSession(str(cwd_model))
|
||||
data = files("mouse_control.assets").joinpath("mouse.onnx").read_bytes()
|
||||
return ort.InferenceSession(data)
|
||||
|
||||
|
||||
def predict(session: ort.InferenceSession, dx: float, dy: float) -> np.ndarray:
|
||||
"""Return a (10, 2) array of trajectory points for the given displacement."""
|
||||
inp = np.array([[[float(dx), float(dy)]]], dtype=np.float32)
|
||||
input_name = session.get_inputs()[0].name
|
||||
return session.run(None, {input_name: inp})[0][0]
|
||||
17
src/mouse_control/model.py
Normal file
17
src/mouse_control/model.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
class SimpleNet(nn.Module):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.fc1 = nn.Linear(2, 64)
|
||||
self.fc2 = nn.Linear(64, 32)
|
||||
self.fc3 = nn.Linear(32, 20)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x = torch.flatten(x, start_dim=1)
|
||||
x = nn.functional.relu(self.fc1(x))
|
||||
x = nn.functional.relu(self.fc2(x))
|
||||
x = self.fc3(x)
|
||||
return x.view(-1, 10, 2)
|
||||
113
src/mouse_control/train.py
Normal file
113
src/mouse_control/train.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import DataLoader, Dataset
|
||||
|
||||
from mouse_control.model import SimpleNet
|
||||
|
||||
|
||||
class TrajectoryDataset(Dataset):
|
||||
def __init__(self, dx_dy: torch.Tensor, labels: torch.Tensor) -> None:
|
||||
self.dx_dy = dx_dy
|
||||
self.labels = labels
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.dx_dy)
|
||||
|
||||
def __getitem__(self, idx: int) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
return self.dx_dy[idx], self.labels[idx]
|
||||
|
||||
|
||||
def _load_csv(csv_path: Path) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
df = pd.read_csv(csv_path, header=None)
|
||||
inputs = df.iloc[:, 0].apply(lambda x: list(map(int, x.split(","))))
|
||||
labels = df.apply(
|
||||
lambda row: [list(map(int, row[i].split(","))) for i in range(1, 11)],
|
||||
axis=1,
|
||||
)
|
||||
inputs_t = torch.Tensor(inputs.tolist()).unsqueeze(1)
|
||||
labels_t = torch.Tensor(labels.tolist())
|
||||
return inputs_t, labels_t
|
||||
|
||||
|
||||
def train(
|
||||
train_csv: Path,
|
||||
test_csv: Path,
|
||||
output: Path,
|
||||
epochs: int = 1000,
|
||||
batch_size: int = 64,
|
||||
lr: float = 1e-3,
|
||||
) -> None:
|
||||
train_x, train_y = _load_csv(train_csv)
|
||||
test_x, test_y = _load_csv(test_csv)
|
||||
|
||||
train_loader = DataLoader(
|
||||
TrajectoryDataset(train_x, train_y), batch_size=batch_size, shuffle=True,
|
||||
)
|
||||
test_loader = DataLoader(
|
||||
TrajectoryDataset(test_x, test_y), batch_size=batch_size, shuffle=False,
|
||||
)
|
||||
|
||||
model = SimpleNet()
|
||||
criterion = nn.MSELoss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=lr)
|
||||
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.9)
|
||||
|
||||
model.train()
|
||||
for epoch in range(epochs):
|
||||
epoch_loss = 0.0
|
||||
for batch_x, batch_y in train_loader:
|
||||
optimizer.zero_grad()
|
||||
output_t = model(batch_x)
|
||||
loss = criterion(output_t, batch_y)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
scheduler.step()
|
||||
epoch_loss += loss.item()
|
||||
if (epoch + 1) % 50 == 0 or epoch == 0:
|
||||
print(f"epoch {epoch + 1:>4}/{epochs} train_loss={epoch_loss / len(train_loader):.4f}")
|
||||
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
total = 0.0
|
||||
last_batch = None
|
||||
for batch_x, batch_y in test_loader:
|
||||
pred = model(batch_x)
|
||||
total += criterion(pred, batch_y).item()
|
||||
last_batch = (batch_x, pred)
|
||||
print(f"test_loss={total / len(test_loader):.4f}")
|
||||
if last_batch is not None:
|
||||
sample_x, sample_pred = last_batch
|
||||
print("sample input :", sample_x[0].tolist())
|
||||
print("sample output:", sample_pred[0].tolist())
|
||||
|
||||
dummy = torch.randn(1, 1, 2)
|
||||
torch.onnx.export(
|
||||
model, dummy, str(output),
|
||||
input_names=["input"], output_names=["output"],
|
||||
external_data=False,
|
||||
)
|
||||
print(f"exported -> {output}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Train the mouse trajectory model.")
|
||||
parser.add_argument("--train-csv", type=Path, default=Path("data/mouse_data.csv"))
|
||||
parser.add_argument("--test-csv", type=Path, default=Path("data/mouse_data_test.csv"))
|
||||
parser.add_argument("--output", "-o", type=Path, default=Path("mouse.onnx"))
|
||||
parser.add_argument("--epochs", type=int, default=1000)
|
||||
parser.add_argument("--batch-size", type=int, default=64)
|
||||
parser.add_argument("--lr", type=float, default=1e-3)
|
||||
args = parser.parse_args()
|
||||
train(
|
||||
args.train_csv, args.test_csv, args.output,
|
||||
epochs=args.epochs, batch_size=args.batch_size, lr=args.lr,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
104
src/mouse_control/visualize.py
Normal file
104
src/mouse_control/visualize.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from scipy.interpolate import CubicSpline
|
||||
|
||||
from mouse_control.inference import load_model, predict
|
||||
|
||||
|
||||
def _interpolate(xs: np.ndarray, ys: np.ndarray, n_points: int) -> tuple[np.ndarray, np.ndarray]:
|
||||
chord = np.cumsum(np.sqrt(np.diff(xs) ** 2 + np.diff(ys) ** 2))
|
||||
t = np.concatenate([[0.0], chord])
|
||||
if t[-1] == 0:
|
||||
return xs, ys
|
||||
cs_x = CubicSpline(t, xs)
|
||||
cs_y = CubicSpline(t, ys)
|
||||
t_dense = np.linspace(0, t[-1], n_points)
|
||||
return cs_x(t_dense), cs_y(t_dense)
|
||||
|
||||
|
||||
def visualize(
|
||||
model_path: Path | None = None,
|
||||
n_trajectories: int = 10,
|
||||
n_interp: int = 80,
|
||||
seed: int | None = None,
|
||||
save_path: Path | None = Path("trajectories.png"),
|
||||
show: bool = True,
|
||||
) -> None:
|
||||
rng = np.random.default_rng(seed)
|
||||
targets = rng.integers(low=-200, high=201, size=(n_trajectories, 2))
|
||||
|
||||
session = load_model(model_path)
|
||||
cmap = plt.get_cmap("tab10")
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
|
||||
for i, (dx, dy) in enumerate(targets):
|
||||
pred = predict(session, dx, dy) # (10, 2)
|
||||
xs = np.concatenate([[0.0], pred[:, 0]])
|
||||
ys = np.concatenate([[0.0], pred[:, 1]])
|
||||
xs_dense, ys_dense = _interpolate(xs, ys, n_interp)
|
||||
|
||||
color = cmap(i % cmap.N)
|
||||
ax.plot(xs_dense, ys_dense, "-", color=color, alpha=0.85, linewidth=1.6)
|
||||
ax.scatter(xs_dense, ys_dense, color=color, s=6, alpha=0.5, zorder=2)
|
||||
ax.scatter(
|
||||
pred[:, 0], pred[:, 1], color=color, s=35, zorder=3,
|
||||
edgecolors="white", linewidths=0.6,
|
||||
)
|
||||
ax.scatter(
|
||||
[dx], [dy], color=color, marker="x", s=140, linewidths=2.5,
|
||||
zorder=4, label=f"target ({dx}, {dy})",
|
||||
)
|
||||
|
||||
ax.scatter([0], [0], color="black", s=90, zorder=5, label="start (0, 0)")
|
||||
ax.axhline(0, color="gray", linewidth=0.5, alpha=0.5)
|
||||
ax.axvline(0, color="gray", linewidth=0.5, alpha=0.5)
|
||||
ax.set_aspect("equal")
|
||||
ax.set_title(
|
||||
f"Mouse trajectories — 10 model points + cubic-spline interpolation ({n_interp} pts)",
|
||||
)
|
||||
ax.set_xlabel("dx")
|
||||
ax.set_ylabel("dy")
|
||||
ax.legend(loc="best", fontsize=8, ncol=2)
|
||||
ax.grid(True, alpha=0.3)
|
||||
plt.tight_layout()
|
||||
|
||||
if save_path is not None:
|
||||
plt.savefig(save_path, dpi=120)
|
||||
print(f"saved -> {save_path}")
|
||||
if show:
|
||||
plt.show()
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render predicted mouse trajectories for random targets.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model", type=Path, default=None,
|
||||
help="Path to ONNX model (defaults to ./mouse.onnx or bundled model).",
|
||||
)
|
||||
parser.add_argument("--n-trajectories", "-n", type=int, default=10)
|
||||
parser.add_argument("--n-interp", type=int, default=80)
|
||||
parser.add_argument("--seed", type=int, default=None)
|
||||
parser.add_argument("--save", type=Path, default=Path("trajectories.png"))
|
||||
parser.add_argument(
|
||||
"--no-show", action="store_true",
|
||||
help="Skip opening the matplotlib window (still writes --save).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
visualize(
|
||||
model_path=args.model,
|
||||
n_trajectories=args.n_trajectories,
|
||||
n_interp=args.n_interp,
|
||||
seed=args.seed,
|
||||
save_path=args.save,
|
||||
show=not args.no_show,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
32
tests/conftest.py
Normal file
32
tests/conftest.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = REPO_ROOT / "data"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def train_csv() -> Path:
|
||||
path = DATA_DIR / "mouse_data.csv"
|
||||
if not path.is_file():
|
||||
pytest.skip(f"missing training csv: {path}")
|
||||
return path
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_csv() -> Path:
|
||||
path = DATA_DIR / "mouse_data_test.csv"
|
||||
if not path.is_file():
|
||||
pytest.skip(f"missing test csv: {path}")
|
||||
return path
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def bundled_session():
|
||||
"""Shared inference session for the bundled model (loading is slow)."""
|
||||
from mouse_control import load_model
|
||||
|
||||
return load_model()
|
||||
49
tests/test_cli.py
Normal file
49
tests/test_cli.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
CLI_COMMANDS = ["mouse-collect", "mouse-train", "mouse-visualize"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command", CLI_COMMANDS)
|
||||
def test_cli_help(command: str) -> None:
|
||||
"""Every CLI must respond to --help with exit 0 and an argparse usage line."""
|
||||
result = subprocess.run(
|
||||
[command, "--help"], capture_output=True, text=True, check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "usage:" in result.stdout
|
||||
|
||||
|
||||
def test_visualize_no_show_produces_png(tmp_path: Path) -> None:
|
||||
save_path = tmp_path / "out.png"
|
||||
result = subprocess.run(
|
||||
[
|
||||
"mouse-visualize", "--no-show", "--seed", "1",
|
||||
"--n-trajectories", "3", "--n-interp", "20",
|
||||
"--save", str(save_path),
|
||||
],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert save_path.exists()
|
||||
assert save_path.stat().st_size > 1000
|
||||
|
||||
|
||||
def test_visualize_module_invocation(tmp_path: Path) -> None:
|
||||
"""The module is also runnable via `python -m mouse_control.visualize`."""
|
||||
save_path = tmp_path / "out.png"
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable, "-m", "mouse_control.visualize",
|
||||
"--no-show", "--seed", "2", "--n-trajectories", "2",
|
||||
"--save", str(save_path),
|
||||
],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert save_path.exists()
|
||||
95
tests/test_inference.py
Normal file
95
tests/test_inference.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from importlib.resources import as_file, files
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
import pytest
|
||||
|
||||
from mouse_control import load_model, predict
|
||||
|
||||
|
||||
def _bundled_path() -> Path:
|
||||
"""Resolve the bundled mouse.onnx to a stable filesystem path for tests."""
|
||||
with as_file(files("mouse_control.assets").joinpath("mouse.onnx")) as p:
|
||||
return Path(p)
|
||||
|
||||
|
||||
def test_load_model_returns_inference_session(bundled_session) -> None:
|
||||
assert isinstance(bundled_session, ort.InferenceSession)
|
||||
|
||||
|
||||
def test_bundled_input_output_names(bundled_session) -> None:
|
||||
assert bundled_session.get_inputs()[0].name == "input"
|
||||
assert bundled_session.get_outputs()[0].name == "output"
|
||||
|
||||
|
||||
def test_load_model_explicit_path() -> None:
|
||||
session = load_model(_bundled_path())
|
||||
assert isinstance(session, ort.InferenceSession)
|
||||
|
||||
|
||||
def test_load_model_missing_path_raises() -> None:
|
||||
with pytest.raises(Exception):
|
||||
load_model("/no/such/path.onnx")
|
||||
|
||||
|
||||
def test_load_model_cwd_override(tmp_path, monkeypatch) -> None:
|
||||
"""When ./mouse.onnx exists, it should take precedence over the bundle."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
shutil.copy(_bundled_path(), tmp_path / "mouse.onnx")
|
||||
session = load_model() # implicit
|
||||
assert isinstance(session, ort.InferenceSession)
|
||||
# Must produce the same output as the bundle since the file is identical.
|
||||
pkg_session = load_model(_bundled_path())
|
||||
a = predict(session, 75, -40)
|
||||
b = predict(pkg_session, 75, -40)
|
||||
np.testing.assert_array_equal(a, b)
|
||||
|
||||
|
||||
def test_load_model_falls_back_to_bundled(tmp_path, monkeypatch) -> None:
|
||||
"""With no ./mouse.onnx, load_model() must succeed via the bundle."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
assert not (tmp_path / "mouse.onnx").exists()
|
||||
session = load_model()
|
||||
assert isinstance(session, ort.InferenceSession)
|
||||
|
||||
|
||||
def test_predict_shape_and_dtype(bundled_session) -> None:
|
||||
out = predict(bundled_session, 100.0, 200.0)
|
||||
assert out.shape == (10, 2)
|
||||
assert out.dtype == np.float32
|
||||
|
||||
|
||||
def test_predict_is_deterministic(bundled_session) -> None:
|
||||
a = predict(bundled_session, 100, 100)
|
||||
b = predict(bundled_session, 100, 100)
|
||||
np.testing.assert_array_equal(a, b)
|
||||
|
||||
|
||||
def test_predict_int_and_float_args_equivalent(bundled_session) -> None:
|
||||
a = predict(bundled_session, 50, 50)
|
||||
b = predict(bundled_session, 50.0, 50.0)
|
||||
np.testing.assert_array_equal(a, b)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"dx,dy",
|
||||
[
|
||||
(150, 0), (-150, 0), (0, 150), (0, -150),
|
||||
(100, 100), (-100, 100), (100, -100), (-100, -100),
|
||||
],
|
||||
)
|
||||
def test_predict_endpoint_near_target(bundled_session, dx: int, dy: int) -> None:
|
||||
"""Trained model's endpoint should fall within ~50px of the target."""
|
||||
end = predict(bundled_session, dx, dy)[-1]
|
||||
err = float(np.linalg.norm(end - np.array([dx, dy])))
|
||||
assert err < 50, f"target ({dx},{dy}) endpoint err={err:.1f}"
|
||||
|
||||
|
||||
def test_predict_zero_displacement_stays_near_origin(bundled_session) -> None:
|
||||
end = predict(bundled_session, 0, 0)[-1]
|
||||
assert abs(end[0]) < 20
|
||||
assert abs(end[1]) < 20
|
||||
74
tests/test_model.py
Normal file
74
tests/test_model.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from mouse_control import SimpleNet
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model() -> SimpleNet:
|
||||
return SimpleNet()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [1, 4, 16, 64])
|
||||
def test_forward_with_unsqueezed_input(model: SimpleNet, batch_size: int) -> None:
|
||||
out = model(torch.randn(batch_size, 1, 2))
|
||||
assert out.shape == (batch_size, 10, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [1, 8, 32])
|
||||
def test_forward_with_flat_input(model: SimpleNet, batch_size: int) -> None:
|
||||
"""SimpleNet's first layer flattens, so (N, 2) is also valid."""
|
||||
out = model(torch.randn(batch_size, 2))
|
||||
assert out.shape == (batch_size, 10, 2)
|
||||
|
||||
|
||||
def test_layers_have_expected_dimensions(model: SimpleNet) -> None:
|
||||
assert model.fc1.in_features == 2
|
||||
assert model.fc1.out_features == 64
|
||||
assert model.fc2.in_features == 64
|
||||
assert model.fc2.out_features == 32
|
||||
assert model.fc3.in_features == 32
|
||||
assert model.fc3.out_features == 20
|
||||
|
||||
|
||||
def test_model_has_trainable_parameters(model: SimpleNet) -> None:
|
||||
params = list(model.parameters())
|
||||
assert params, "SimpleNet should expose parameters"
|
||||
assert all(p.requires_grad for p in params)
|
||||
|
||||
|
||||
def test_forward_is_differentiable(model: SimpleNet) -> None:
|
||||
inp = torch.randn(2, 1, 2, requires_grad=True)
|
||||
out = model(inp)
|
||||
out.sum().backward()
|
||||
assert inp.grad is not None
|
||||
assert inp.grad.shape == inp.shape
|
||||
|
||||
|
||||
def test_onnx_export_round_trips(tmp_path, model: SimpleNet) -> None:
|
||||
"""Trained or not, the model architecture must round-trip via ONNX."""
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
|
||||
onnx_path = tmp_path / "round_trip.onnx"
|
||||
dummy = torch.randn(1, 1, 2)
|
||||
model.eval()
|
||||
torch.onnx.export(
|
||||
model, dummy, str(onnx_path),
|
||||
input_names=["input"], output_names=["output"],
|
||||
external_data=False,
|
||||
)
|
||||
assert onnx_path.exists()
|
||||
assert not onnx_path.with_suffix(onnx_path.suffix + ".data").exists(), \
|
||||
"external_data=False must produce a single-file ONNX"
|
||||
|
||||
session = ort.InferenceSession(str(onnx_path))
|
||||
inp = np.array([[[42.0, -17.0]]], dtype=np.float32)
|
||||
out = session.run(None, {"input": inp})[0]
|
||||
assert out.shape == (1, 10, 2)
|
||||
|
||||
with torch.no_grad():
|
||||
torch_out = model(torch.from_numpy(inp)).numpy()
|
||||
np.testing.assert_allclose(out, torch_out, atol=1e-4)
|
||||
103
tests/test_train.py
Normal file
103
tests/test_train.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from mouse_control.train import TrajectoryDataset, _load_csv, train
|
||||
|
||||
|
||||
def _write_csv(path: Path, rows: list[list[tuple[int, int]]]) -> None:
|
||||
"""Write rows in the project's quirky single-quoted-pair format."""
|
||||
with open(path, "w", newline="") as f:
|
||||
writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
|
||||
for row in rows:
|
||||
# First column is the target (last keypoint), then 10 keypoints.
|
||||
target_x, target_y = row[-1]
|
||||
cells = [f"{target_x},{target_y}"] + [f"{x},{y}" for x, y in row]
|
||||
writer.writerow(cells)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def synthetic_csv(tmp_path: Path) -> Path:
|
||||
"""Tiny linear trajectories from origin -> target."""
|
||||
path = tmp_path / "synth.csv"
|
||||
rows = []
|
||||
rng = np.random.default_rng(0)
|
||||
for _ in range(20):
|
||||
tx, ty = int(rng.integers(-150, 151)), int(rng.integers(-150, 151))
|
||||
keypoints = [
|
||||
(int(round(tx * i / 9)), int(round(ty * i / 9))) for i in range(10)
|
||||
]
|
||||
rows.append(keypoints)
|
||||
_write_csv(path, rows)
|
||||
return path
|
||||
|
||||
|
||||
def test_load_csv_shapes(synthetic_csv: Path) -> None:
|
||||
inputs, labels = _load_csv(synthetic_csv)
|
||||
assert inputs.shape == (20, 1, 2)
|
||||
assert labels.shape == (20, 10, 2)
|
||||
assert inputs.dtype == torch.float32
|
||||
assert labels.dtype == torch.float32
|
||||
|
||||
|
||||
def test_load_csv_round_trip(synthetic_csv: Path) -> None:
|
||||
"""The target column (input) must equal the last keypoint of the labels."""
|
||||
inputs, labels = _load_csv(synthetic_csv)
|
||||
last_keypoint = labels[:, -1, :]
|
||||
np.testing.assert_array_equal(inputs.squeeze(1).numpy(), last_keypoint.numpy())
|
||||
|
||||
|
||||
def test_dataset_indexing(synthetic_csv: Path) -> None:
|
||||
inputs, labels = _load_csv(synthetic_csv)
|
||||
ds = TrajectoryDataset(inputs, labels)
|
||||
assert len(ds) == 20
|
||||
x, y = ds[0]
|
||||
assert x.shape == (1, 2)
|
||||
assert y.shape == (10, 2)
|
||||
|
||||
|
||||
def test_load_csv_missing_file_raises(tmp_path: Path) -> None:
|
||||
with pytest.raises(FileNotFoundError):
|
||||
_load_csv(tmp_path / "missing.csv")
|
||||
|
||||
|
||||
def test_train_end_to_end_produces_single_file_onnx(
|
||||
synthetic_csv: Path, tmp_path: Path,
|
||||
) -> None:
|
||||
"""A quick training run must export a valid, self-contained ONNX model."""
|
||||
output = tmp_path / "trained.onnx"
|
||||
train(
|
||||
train_csv=synthetic_csv,
|
||||
test_csv=synthetic_csv,
|
||||
output=output,
|
||||
epochs=3,
|
||||
batch_size=8,
|
||||
)
|
||||
assert output.exists()
|
||||
assert output.stat().st_size > 1000
|
||||
# external_data=False -> no .data sidecar
|
||||
assert not output.with_suffix(output.suffix + ".data").exists()
|
||||
|
||||
session = ort.InferenceSession(str(output))
|
||||
inp = np.array([[[100.0, 50.0]]], dtype=np.float32)
|
||||
out = session.run(None, {"input": inp})[0]
|
||||
assert out.shape == (1, 10, 2)
|
||||
|
||||
|
||||
def test_train_with_real_data(train_csv: Path, test_csv: Path, tmp_path: Path) -> None:
|
||||
"""Smoke test against the project's actual data."""
|
||||
output = tmp_path / "real.onnx"
|
||||
train(
|
||||
train_csv=train_csv,
|
||||
test_csv=test_csv,
|
||||
output=output,
|
||||
epochs=2,
|
||||
batch_size=32,
|
||||
)
|
||||
assert output.exists()
|
||||
97
train.py
97
train.py
@@ -1,97 +0,0 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
import onnxruntime
|
||||
import onnx
|
||||
from onnxsim import simplify
|
||||
|
||||
# 读取CSV文件
|
||||
train_csv_path = 'mouse_data.csv' # 替换为你的CSV文件路径
|
||||
train_csv = pd.read_csv(train_csv_path, header=None)
|
||||
test_csv_path = 'mouse_data_test.csv' # 替换为你的CSV文件路径
|
||||
test_csv = pd.read_csv(test_csv_path, header=None)
|
||||
|
||||
# 提取dx, dy和标签
|
||||
dx_dy_train = train_csv.iloc[:, 0].apply(lambda x: list(map(int, x.split(','))))
|
||||
dx_dy_labels_train = train_csv.apply(lambda row: [list(map(int, row[i].split(','))) for i in range(1,10)], axis=1)
|
||||
dx_dy_test = test_csv.iloc[:, 0].apply(lambda x: list(map(int, x.split(','))))
|
||||
dx_dy_labels_test = test_csv.apply(lambda row: [list(map(int, row[i].split(','))) for i in range(1,10)], axis=1)
|
||||
|
||||
# 转换为PyTorch Tensor
|
||||
dx_dy_train_tensor = torch.Tensor(dx_dy_train.tolist())
|
||||
dx_dy_train_tensor = dx_dy_train_tensor.unsqueeze(1)
|
||||
labels_train_tensor = torch.Tensor(dx_dy_labels_train.tolist())
|
||||
dx_dy_test_tensor = torch.Tensor(dx_dy_test.tolist())
|
||||
dx_dy_test_tensor = dx_dy_test_tensor.unsqueeze(1)
|
||||
labels_test_tensor = torch.Tensor(dx_dy_labels_test.tolist())
|
||||
|
||||
# 创建自定义Dataset
|
||||
class CustomDataset(Dataset):
|
||||
def __init__(self, dx_dy, labels):
|
||||
self.dx_dy = dx_dy
|
||||
self.labels = labels
|
||||
|
||||
def __len__(self):
|
||||
return len(self.dx_dy)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.dx_dy[idx], self.labels[idx]
|
||||
|
||||
# 创建Dataset和DataLoader
|
||||
train_dataset = CustomDataset(dx_dy_train_tensor, labels_train_tensor)
|
||||
test_dataset = CustomDataset(dx_dy_test_tensor, labels_test_tensor)
|
||||
batch_size = 64
|
||||
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
|
||||
test_dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
|
||||
|
||||
# 定义神经网络模型
|
||||
class SimpleNet(nn.Module):
|
||||
def __init__(self):
|
||||
super(SimpleNet, self).__init__()
|
||||
self.fc1 = nn.Linear(2, 64)
|
||||
self.fc2 = nn.Linear(64, 32)
|
||||
self.fc3 = nn.Linear(32, 18)
|
||||
|
||||
def forward(self, input_data):
|
||||
x = torch.flatten(input_data, start_dim=1)
|
||||
x = self.fc1(x)
|
||||
x = nn.ReLU()(x)
|
||||
x = self.fc2(x)
|
||||
x = nn.ReLU()(x)
|
||||
x = self.fc3(x)
|
||||
return x.view(-1, 9, 2)
|
||||
|
||||
# 初始化模型、损失函数和优化器
|
||||
model = SimpleNet()
|
||||
criterion = nn.MSELoss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=0.001)
|
||||
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=50, gamma=0.9)
|
||||
|
||||
# 训练模型
|
||||
epochs = 1000
|
||||
for epoch in range(epochs):
|
||||
for batch_dx_dy, batch_labels in train_dataloader:
|
||||
optimizer.zero_grad()
|
||||
output = model(batch_dx_dy)
|
||||
loss = criterion(output, batch_labels)
|
||||
print(loss)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
scheduler.step()
|
||||
print('test')
|
||||
for idx, data in enumerate(test_dataloader):
|
||||
batch_dx_dy, batch_labels = data
|
||||
output = model(batch_dx_dy)
|
||||
loss = criterion(output, batch_labels)
|
||||
print(loss)
|
||||
if idx == len(test_dataloader)-1:
|
||||
print(batch_dx_dy[0])
|
||||
print(output[0])
|
||||
|
||||
model.eval()
|
||||
onnx_name = 'mouse.onnx'
|
||||
dummy = torch.randn(1, 1, 2)
|
||||
torch.onnx.export(model, dummy, onnx_name,verbose=True, input_names=['input'], output_names=['output'])
|
||||
Reference in New Issue
Block a user