Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ logs/
*.sqlite3
*.db-shm
*.db-wal
!data/catalog/stars.db
data/catalog/raw/*
data/catalog/index/*
data/catalog/meta/*
!data/catalog/meta/manifest.json
!data/catalog/README.md

# ====================
# 配置文件(敏感信息)
Expand Down
19 changes: 19 additions & 0 deletions data/catalog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# 星表数据库说明 / Star Catalog Database Notes

- 主库文件:`data/catalog/stars.db`
- 数据源(默认):HYG Database v3(CSV)
- 用途:提供极轴解算与调试控制台的星点查询、匹配与维护

## 字段说明 / Fields

- `source_id`: 唯一标识 / Unique identifier
- `ra`, `dec`: 赤经赤纬(度)/ Right ascension and declination in degrees
- `pmra`, `pmdec`: 自行参数 / Proper motion parameters
- `phot_g_mean_mag`: 亮度星等 / Magnitude
- `name_en`, `name_zh`: 英文/中文名称
- `description_en`, `description_zh`: 英文/中文描述

## 维护方式 / Maintenance

- 可通过 `/api/catalog/*` 与 `/debug/analysis` 执行下载、索引、CRUD。
- 数据库文件会随 Git 提交与版本发行。
12 changes: 12 additions & 0 deletions data/catalog/meta/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"generated_at": "2026-03-27T14:47:15.117645+00:00",
"source_file": "data/catalog/raw/catalog_raw.csv",
"db_path": "data/catalog/stars.db",
"magnitude_limit": 12.0,
"ra_bin_size_deg": 5.0,
"record_count": 117931,
"bucket_count": 72,
"source_sha256": "d9f69fd86bbf90a4e4d52b4c5c53eacfa6dfc0bfdef85bfd94f095e0bebe4ebd",
"epoch": "JNow(approx)",
"status": "ready"
}
Binary file added data/catalog/stars.db
Binary file not shown.
7 changes: 7 additions & 0 deletions ogscope/algorithms/plate_solve/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
星图解算模块导出 / Plate solving module exports
"""

from ogscope.algorithms.plate_solve.solver import PlateSolver, SolveResult

__all__ = ["PlateSolver", "SolveResult"]
90 changes: 90 additions & 0 deletions ogscope/algorithms/plate_solve/solver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""
简化星图解算器 / Simplified plate solver
"""

from __future__ import annotations

from dataclasses import dataclass
from math import cos, radians
from typing import Any

import numpy as np

from ogscope.algorithms.star_extract import StarPoint
from ogscope.data.catalog.service import catalog_service


@dataclass(slots=True)
class SolveResult:
"""解算结果 / Solving result"""

ra_deg: float
dec_deg: float
confidence: float
solve_source: str
matched_catalog_stars: int
detected_stars: int

def to_dict(self) -> dict[str, Any]:
return {
"ra_deg": self.ra_deg,
"dec_deg": self.dec_deg,
"confidence": self.confidence,
"solve_source": self.solve_source,
"matched_catalog_stars": self.matched_catalog_stars,
"detected_stars": self.detected_stars,
}


class PlateSolver:
"""基于提示位姿与星表密度的轻量解算 / Lightweight solver with hint and catalog density"""

def __init__(self, fov_deg: float = 16.0) -> None:
self.fov_deg = fov_deg

def solve(
self,
stars: list[StarPoint],
frame_shape: tuple[int, ...],
hint_ra_deg: float,
hint_dec_deg: float,
solve_source: str = "full",
) -> SolveResult:
"""解算画面中心坐标 / Solve frame center coordinate"""
if not stars:
return SolveResult(
ra_deg=hint_ra_deg % 360.0,
dec_deg=float(np.clip(hint_dec_deg, -90.0, 90.0)),
confidence=0.0,
solve_source=solve_source,
matched_catalog_stars=0,
detected_stars=0,
)

height, width = frame_shape[:2]
points = np.array([[s.x, s.y, s.flux] for s in stars], dtype=np.float64)
flux = np.clip(points[:, 2], 1e-6, None)
centroid_x = float(np.average(points[:, 0], weights=flux))
centroid_y = float(np.average(points[:, 1], weights=flux))
dx = centroid_x - (width / 2.0)
dy = centroid_y - (height / 2.0)

deg_per_pixel = self.fov_deg / max(width, 1)
dec = float(np.clip(hint_dec_deg - (dy * deg_per_pixel), -90.0, 90.0))
cos_dec = max(0.01, cos(radians(dec)))
ra = (hint_ra_deg + (dx * deg_per_pixel / cos_dec)) % 360.0

nearby_catalog = catalog_service.load_records_for_region(ra_deg=ra, search_bins=1)
expected_stars = max(1, len(nearby_catalog))
detected_stars = len(stars)
matched = min(detected_stars, expected_stars)
confidence = min(1.0, detected_stars / expected_stars)

return SolveResult(
ra_deg=ra,
dec_deg=dec,
confidence=float(confidence),
solve_source=solve_source,
matched_catalog_stars=matched,
detected_stars=detected_stars,
)
7 changes: 7 additions & 0 deletions ogscope/algorithms/star_extract/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
星点提取模块导出 / Star extraction module exports
"""

from ogscope.algorithms.star_extract.extractor import StarExtractor, StarPoint

__all__ = ["StarExtractor", "StarPoint"]
65 changes: 65 additions & 0 deletions ogscope/algorithms/star_extract/extractor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""
星点提取器 / Star extractor
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import cv2
import numpy as np


@dataclass(slots=True)
class StarPoint:
"""星点数据 / Star point data"""

x: float
y: float
flux: float
area: float

def to_dict(self) -> dict[str, Any]:
return {"x": self.x, "y": self.y, "flux": self.flux, "area": self.area}


class StarExtractor:
"""简单星点提取 / Lightweight star extraction"""

def __init__(self, max_stars: int = 80) -> None:
self.max_stars = max_stars

def extract(self, frame: np.ndarray) -> list[StarPoint]:
"""提取星点 / Extract star points"""
if frame.ndim == 3:
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
else:
gray = frame.copy()

# 使用高斯模糊降低高频噪声 / Apply gaussian blur for high-frequency noise
blur = cv2.GaussianBlur(gray, (3, 3), 0)
_, binary = cv2.threshold(
blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
contours, _ = cv2.findContours(
binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)

points: list[StarPoint] = []
for contour in contours:
area = float(cv2.contourArea(contour))
if area <= 0.5:
continue
m = cv2.moments(contour)
if m["m00"] <= 0:
continue
cx = float(m["m10"] / m["m00"])
cy = float(m["m01"] / m["m00"])
mask = np.zeros_like(gray, dtype=np.uint8)
cv2.drawContours(mask, [contour], -1, color=255, thickness=-1)
flux = float(cv2.mean(gray, mask=mask)[0] * area)
points.append(StarPoint(x=cx, y=cy, flux=flux, area=area))

points.sort(key=lambda p: p.flux, reverse=True)
return points[: self.max_stars]
7 changes: 7 additions & 0 deletions ogscope/algorithms/star_match/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
星点匹配模块导出 / Star matching module exports
"""

from ogscope.algorithms.star_match.tracker import FastTracker, TrackResult

__all__ = ["FastTracker", "TrackResult"]
57 changes: 57 additions & 0 deletions ogscope/algorithms/star_match/tracker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
快速跟踪器 / Fast tracker
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

import numpy as np

from ogscope.algorithms.star_extract import StarPoint


@dataclass(slots=True)
class TrackResult:
"""跟踪结果 / Tracking result"""

delta_x: float
delta_y: float
matched_points: int
confidence: float

def to_dict(self) -> dict[str, Any]:
return {
"delta_x": self.delta_x,
"delta_y": self.delta_y,
"matched_points": self.matched_points,
"confidence": self.confidence,
}


class FastTracker:
"""基于质心偏移的轻量跟踪 / Lightweight tracking based on centroid shift"""

def track(
self, previous: list[StarPoint], current: list[StarPoint]
) -> TrackResult:
"""估计帧间位移 / Estimate inter-frame shift"""
if not previous or not current:
return TrackResult(delta_x=0.0, delta_y=0.0, matched_points=0, confidence=0.0)

prev = np.array([[p.x, p.y, max(p.flux, 1e-6)] for p in previous], dtype=np.float64)
cur = np.array([[p.x, p.y, max(p.flux, 1e-6)] for p in current], dtype=np.float64)
prev_cx = float(np.average(prev[:, 0], weights=prev[:, 2]))
prev_cy = float(np.average(prev[:, 1], weights=prev[:, 2]))
cur_cx = float(np.average(cur[:, 0], weights=cur[:, 2]))
cur_cy = float(np.average(cur[:, 1], weights=cur[:, 2]))

matched_points = min(len(previous), len(current))
confidence = min(1.0, matched_points / 20.0)
return TrackResult(
delta_x=cur_cx - prev_cx,
delta_y=cur_cy - prev_cy,
matched_points=matched_points,
confidence=confidence,
)
15 changes: 15 additions & 0 deletions ogscope/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,21 @@ class Settings(BaseSettings):
# 文件路径配置 / File path configuration
data_dir: Path = Field(default=Path("./data"), description="数据目录")
upload_dir: Path = Field(default=Path("./uploads"), description="上传目录")
analysis_dir: Path = Field(
default=Path("./data/analysis"), description="分析任务目录"
)
catalog_dir: Path = Field(default=Path("./data/catalog"), description="星表目录")
static_dir: Path = Field(default=Path("./web/static"), description="静态文件目录")
template_dir: Path = Field(default=Path("./web/templates"), description="模板目录")

# 星图解算配置 / Plate solving configuration
solver_hint_ra_deg: float = Field(default=0.0, description="默认解算RA提示(度)")
solver_hint_dec_deg: float = Field(default=90.0, description="默认解算Dec提示(度)")
solver_fov_deg: float = Field(default=16.0, description="视场角(度)")
solver_max_stars: int = Field(default=80, description="用于解算的最大星点数量")
solver_fullsolve_interval_frames: int = Field(
default=10, description="实时模式全量解算间隔帧数"
)

model_config = SettingsConfigDict(
env_file=".env",
Expand All @@ -71,6 +84,8 @@ def __init__(self, **kwargs):
# 创建必要的目录 / Create necessary directories
self.data_dir.mkdir(parents=True, exist_ok=True)
self.upload_dir.mkdir(parents=True, exist_ok=True)
self.analysis_dir.mkdir(parents=True, exist_ok=True)
self.catalog_dir.mkdir(parents=True, exist_ok=True)


@lru_cache()
Expand Down
7 changes: 7 additions & 0 deletions ogscope/core/realtime/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
实时解算模块导出 / Realtime solving exports
"""

from ogscope.core.realtime.service import RealtimeSolveService, realtime_solve_service

__all__ = ["RealtimeSolveService", "realtime_solve_service"]
Loading
Loading