diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..6e9fdc400be1b9a4d35fe1f6e2d465303c04ce4f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,35 +1,6 @@ -*.7z filter=lfs diff=lfs merge=lfs -text -*.arrow filter=lfs diff=lfs merge=lfs -text *.bin filter=lfs diff=lfs merge=lfs -text -*.bz2 filter=lfs diff=lfs merge=lfs -text -*.ckpt filter=lfs diff=lfs merge=lfs -text -*.ftz filter=lfs diff=lfs merge=lfs -text -*.gz filter=lfs diff=lfs merge=lfs -text -*.h5 filter=lfs diff=lfs merge=lfs -text -*.joblib filter=lfs diff=lfs merge=lfs -text -*.lfs.* filter=lfs diff=lfs merge=lfs -text -*.mlmodel filter=lfs diff=lfs merge=lfs -text -*.model filter=lfs diff=lfs merge=lfs -text -*.msgpack filter=lfs diff=lfs merge=lfs -text *.npy filter=lfs diff=lfs merge=lfs -text *.npz filter=lfs diff=lfs merge=lfs -text -*.onnx filter=lfs diff=lfs merge=lfs -text -*.ot filter=lfs diff=lfs merge=lfs -text -*.parquet filter=lfs diff=lfs merge=lfs -text -*.pb filter=lfs diff=lfs merge=lfs -text -*.pickle filter=lfs diff=lfs merge=lfs -text -*.pkl filter=lfs diff=lfs merge=lfs -text -*.pt filter=lfs diff=lfs merge=lfs -text -*.pth filter=lfs diff=lfs merge=lfs -text -*.rar filter=lfs diff=lfs merge=lfs -text -*.safetensors filter=lfs diff=lfs merge=lfs -text -saved_model/**/* filter=lfs diff=lfs merge=lfs -text -*.tar.* filter=lfs diff=lfs merge=lfs -text -*.tar filter=lfs diff=lfs merge=lfs -text -*.tflite filter=lfs diff=lfs merge=lfs -text -*.tgz filter=lfs diff=lfs merge=lfs -text -*.wasm filter=lfs diff=lfs merge=lfs -text -*.xz filter=lfs diff=lfs merge=lfs -text -*.zip filter=lfs diff=lfs merge=lfs -text -*.zst filter=lfs diff=lfs merge=lfs -text -*tfevents* filter=lfs diff=lfs merge=lfs -text +*.obj filter=lfs diff=lfs merge=lfs -text +*.nii filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e725a4e36462a10c4763a33ae5c139301080e571 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.git +__pycache__ +*.pyc +.env diff --git a/README.md b/README.md index fe15829d94f30901aba15670a9812b425aa47bc8..4389093bfce850d3b91572e89e4d49b465d5112a 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,10 @@ ---- +--- title: MindVisualizer API sdk: docker app_port: 7860 pinned: false --- -# MindVisualizer Python API - -Small FastAPI adapter for the hosted web page. - -The GitHub Pages frontend renders particles locally. This backend is only for algorithm-sensitive operations: - -- recomputing probe trajectories with the original Python `TriLinearSampler` -- mapping a trajectory to ROI vectors with the original `ManifoldToROIKNN` -- building the original `ROIFlowAnalyzer` context -- optionally calling the original `ROIFlowLLM` if `OPENAI_API_KEY` is configured - -## Cheapest hosting shape - -Use a free Python container host and keep the service public: - -1. Bundle the minimal `mindVisualizer` source/data folder into the Space as `/app/mindVisualizer`. -2. Deploy this directory as a Docker/FastAPI service. -3. Set `MINDVIS_REPO=/app/mindVisualizer`. -4. Set `CORS_ORIGINS=https://pixedar.github.io,http://localhost:3000`. -5. In the GitHub Pages build, set `NEXT_PUBLIC_MINDVIS_API_URL=https://your-backend-url`. -6. Optional: set `OPENAI_API_KEY` as a Space secret and `OPENAI_MODEL` as a Space variable. - -Current Space: - -- https://huggingface.co/spaces/Pixedar/mindvisualizer-api -- https://pixedar-mindvisualizer-api.hf.space - -Do not install the full `mindVisualizer` `pyproject.toml` on a tiny free host; it pulls desktop/VTK dependencies. This adapter uses `PYTHONPATH` and installs only the packages needed for the API path. - -## Local run - -```bash -cd backend/mindvis-api -python -m venv .venv -.venv\Scripts\activate -pip install -r requirements.txt -$env:MINDVIS_REPO="C:\Users\assas\PycharmProjects\mindVisualizer" -uvicorn app:app --reload --port 7860 -``` - -Then build/run the web page with: +# MindVisualizer API -```bash -$env:NEXT_PUBLIC_MINDVIS_API_URL="http://localhost:7860" -pnpm dev -``` +Python-backed probe trajectory and explanation service. diff --git a/app.py b/app.py index 3e0305b4d0eb5e4201c52e6c8649c440b7435874..968a5ea62cbd96843b86cbe1c5533ae25ea36d86 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,3 @@ -import json import os import sys from functools import lru_cache @@ -11,20 +10,20 @@ from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field -FieldMode = Literal["mean", "gated", "mu1", "mu2"] +FieldMode = Literal["mean", "gated", "mu1", "mu2", "mu3", "mu4", "mu5", "mu6"] class TrajectoryRequest(BaseModel): seed: list[float] = Field(default_factory=lambda: [0.5, 0.5, 0.5], min_length=3, max_length=3) - fieldMode: FieldMode = "gated" - steps: int = Field(default=220, ge=8, le=1200) - dt: float = Field(default=0.018, gt=0, le=0.1) - speedScale: float = Field(default=0.48, gt=0, le=4) + fieldMode: FieldMode = "mean" + steps: int = Field(default=700, ge=8, le=1600) + dt: float = Field(default=1.0, gt=0, le=8.0) + speedScale: float = Field(default=1.0, gt=0, le=8.0) class ExplainRequest(BaseModel): trajectory: list[list[float]] = Field(min_length=2) - fieldMode: FieldMode = "gated" + fieldMode: FieldMode = "mean" callLlm: bool = True @@ -40,49 +39,80 @@ def mindvis_repo() -> Path: @lru_cache(maxsize=1) def services(): repo = mindvis_repo() + data = repo / "data" from src.field_loader import TriLinearSampler, load_field - from src.roi_flow import ManifoldToROIKNN, ROIFlowAnalyzer + from src.mesh_overlay import FlowMeshOverlay - data = repo / "data" - flow = load_field(data / "roi_flow" / "mdn_universal_raw_grid64_meta.json") + flow = load_field(data / "mdn" / "mdn_particles_rdcim_teacher_edge_hq_grid125_meta.json") mean = flow["mean"] - mu1 = flow["mus"][0] - mu2 = flow["mus"][1] + mus = flow["mus"] pi = flow["pi"] - gated = np.where(np.argmax(pi, axis=-1)[..., None] == 0, mu1, mu2).astype(np.float32) - - samplers = { - "mean": TriLinearSampler(mean, flow["amin"], flow["amax"]), - "gated": TriLinearSampler(gated, flow["amin"], flow["amax"]), - "mu1": TriLinearSampler(mu1, flow["amin"], flow["amax"]), - "mu2": TriLinearSampler(mu2, flow["amin"], flow["amax"]), - } - - embed = np.load(data / "roi_flow" / "probe_embed.npy").astype(np.float32) - roi = np.load(data / "roi_flow" / "probe_roi.npy").astype(np.float32) - centers = np.load(data / "roi_flow" / "probe_roi_centers.npy").astype(np.float32) - structures = json.loads((data / "structures.json").read_text(encoding="utf-8")) - - names = [] - for idx in range(roi.shape[1]): - item = structures[idx] if idx < len(structures) else {} - names.append(item.get("name") or item.get("acronym") or f"ROI {idx + 1}") + ent = flow["ENT"] + amin = flow["amin"] + amax = flow["amax"] + span = amax - amin + diag = float(np.linalg.norm(span)) + target_step = 0.01 * max(diag, 1e-6) + + winner = np.argmax(pi, axis=-1) + gated = np.zeros_like(mean, dtype=np.float32) + for idx, mu in enumerate(mus): + gated += mu * (winner[..., None] == idx).astype(np.float32) + + fields = {"mean": mean, "gated": gated} + fields.update({f"mu{idx + 1}": mu for idx, mu in enumerate(mus)}) + samplers = {name: TriLinearSampler(field, amin, amax) for name, field in fields.items()} + + def vmax(field): + mag = np.linalg.norm(field.reshape(-1, 3), axis=1) + nz = mag[mag > 0] + return float(np.percentile(nz, 100.0)) if nz.size else 1.0 + + vmax_by_name = {name: vmax(field) for name, field in fields.items()} + + # The original Python app uses FlowMeshOverlay + its cached label grid for + # probe interpretation. This API intentionally reuses that path. + import vtk + + ren = vtk.vtkRenderer() + win = vtk.vtkRenderWindow() + win.OffScreenRenderingOn() + win.AddRenderer(ren) + mesh = FlowMeshOverlay(ren=ren, win=win, mesh_dir=data / "meshes", alignment_file=data / "brain_alignment.json") + cache = data / "label_grid_cache.npz" + if not mesh.load_label_grid(cache): + raise RuntimeError("Python label_grid_cache is missing; run setup_brain_data.py in mindVisualizer first.") + + # Optional finer parcellation, exactly as the desktop flow mode auto-detects. + try: + from src.extra_parcellation import ExtraParcellation + + atlas = data / "extra_parcellation" / "combined_atlas.nii.gz" + labels = data / "extra_parcellation" / "combined_atlas_labels.json" + if atlas.exists(): + extra = ExtraParcellation(atlas, labels if labels.exists() else None) + if extra.load(): + mesh.set_extra_parcellation(extra) + except Exception as exc: + print(f"[extra-parcellation] disabled: {exc}") - mapper = ManifoldToROIKNN(embed, roi, k=256) - analyzer = ROIFlowAnalyzer(names, centers[: len(names)]) + ent_sampler = TriLinearSampler(mean, amin, amax) if ent is not None else None return { - "amin": flow["amin"], - "amax": flow["amax"], - "span": flow["amax"] - flow["amin"], + "amin": amin, + "amax": amax, + "span": span, "samplers": samplers, - "mapper": mapper, - "analyzer": analyzer, + "vmax": vmax_by_name, + "target_step": target_step, + "mesh": mesh, + "entropy": ent, + "entropy_sampler": ent_sampler, } -app = FastAPI(title="MindVisualizer API", version="0.1.0") +app = FastAPI(title="MindVisualizer API", version="1.0.0") origins = [origin.strip() for origin in os.environ.get("CORS_ORIGINS", "*").split(",") if origin.strip()] app.add_middleware( @@ -105,10 +135,16 @@ def health(): @app.post("/api/mindvis/trajectory") def trajectory(req: TrajectoryRequest): svc = services() - path = integrate_path(req.seed, req.fieldMode, req.steps, req.dt, req.speedScale) + world, mags, stopped = integrate_probe(req, svc) + transitions = analyze_regions(world, mags, req.callLlm if hasattr(req, "callLlm") else False) return { "fieldMode": req.fieldMode, - "trajectory": normalized(path, svc["amin"], svc["span"]).tolist(), + "trajectory": normalized(world, svc["amin"], svc["span"]).tolist(), + "worldTrajectory": world.tolist(), + "fieldMagnitudes": mags.tolist(), + "stopped": stopped, + "transitions": transitions, + "regionText": format_transition_text(transitions), } @@ -120,42 +156,82 @@ def explain(req: ExplainRequest): raise HTTPException(status_code=400, detail="trajectory must be an array of [x,y,z] points") world = denormalized(np.clip(traj, 0.0, 1.0), svc["amin"], svc["span"]) - mapper = svc["mapper"] - analyzer = svc["analyzer"] - start_roi = mapper.query(world[0]) - end_roi = mapper.query(world[-1]) - delta = analyzer.compute_delta(start_roi, end_roi) - context = analyzer.build_llm_context(delta) - - text = "" - if req.callLlm and os.environ.get("OPENAI_API_KEY"): - text = call_openai_interpreter(context) - - if not text: - analysis = analyzer.analyze_flow_pattern(delta) - text = ( - f"Pattern: {analysis['pattern_type']}. " - f"Bulk direction: {analysis['bulk_direction']}. " - f"Positive ROIs: {analysis['n_significant_positive']}; " - f"negative ROIs: {analysis['n_significant_negative']}." + sampler = svc["samplers"].get(req.fieldMode, svc["samplers"]["mean"]) + mags = np.linalg.norm(sampler.sample_vec(world), axis=1).astype(np.float32) + transitions = analyze_regions(world, mags, req.callLlm) + text = format_transition_text(transitions) + + if req.callLlm: + from src.region_analyzer import analyze_with_gpt + + text = analyze_with_gpt( + transitions, + use_rag=os.environ.get("MINDVIS_USE_RAG", "0") == "1", + model=os.environ.get("OPENAI_MODEL", "gpt-5.4-mini"), + debug=os.environ.get("MINDVIS_DEBUG", "0") == "1", ) - return {"text": text, "context": context} + return {"text": text, "transitions": transitions, "regionText": format_transition_text(transitions)} + + +def integrate_probe(req: TrajectoryRequest, svc: dict) -> tuple[np.ndarray, np.ndarray, bool]: + p = denormalized(np.asarray(req.seed[:3], dtype=np.float32)[None, :], svc["amin"], svc["span"]) + sampler = svc["samplers"].get(req.fieldMode, svc["samplers"]["mean"]) + step = req.dt * req.speedScale * (svc["target_step"] / max(svc["vmax"].get(req.fieldMode, 1.0), 1e-9)) + path = [] + mags = [] + stopped = False + + for _ in range(req.steps): + path.append(p[0].copy()) + velocity = sampler.sample_vec(p)[0] + mags.append(float(np.linalg.norm(velocity))) + new_pos = np.clip(p[0] + velocity * step, svc["amin"], svc["amax"]) + + # Original probe boundary behavior: if a step leaves labelled brain + # space, try half-step; otherwise stop instead of wrapping. + if not is_valid_probe_position(new_pos, svc["mesh"]): + half = np.clip(p[0] + velocity * step * 0.5, svc["amin"], svc["amax"]) + if is_valid_probe_position(half, svc["mesh"]): + new_pos = half + else: + stopped = True + break + if np.linalg.norm(new_pos - p[0]) < 1e-5: + stopped = True + break + p[0] = new_pos.astype(np.float32) -def integrate_path(seed: list[float], field_mode: FieldMode, steps: int, dt: float, speed_scale: float) -> np.ndarray: + return np.asarray(path, dtype=np.float32), np.asarray(mags, dtype=np.float32), stopped + + +def is_valid_probe_position(point: np.ndarray, mesh) -> bool: + key = mesh.get_region_at_point(point) + if key is None: + key = mesh.find_nearest_region(point, search_radius=2, max_distance_mm=5.0) + return key is not None + + +def analyze_regions(world: np.ndarray, mags: np.ndarray, call_llm: bool) -> list[dict]: svc = services() - p = denormalized(np.asarray(seed[:3], dtype=np.float32)[None, :], svc["amin"], svc["span"]) - sampler = svc["samplers"][field_mode] - path = np.empty((steps, 3), dtype=np.float32) + from src.region_analyzer import analyze_probe_path + + transitions = analyze_probe_path( + world, + svc["mesh"], + sample_every=5, + field_mags=mags, + entropy_sampler=svc["entropy_sampler"], + entropy_field=svc["entropy"], + ) + return make_json_safe(transitions) + - for idx in range(steps): - path[idx] = p[0] - v = sampler.sample_vec(p) - p = p + v * (dt * speed_scale) - p = ((p - svc["amin"]) % svc["span"]) + svc["amin"] +def format_transition_text(transitions: list[dict]) -> str: + from src.region_analyzer import format_transitions_text - return path + return format_transitions_text(transitions) def denormalized(points: np.ndarray, amin: np.ndarray, span: np.ndarray) -> np.ndarray: @@ -166,44 +242,15 @@ def normalized(points: np.ndarray, amin: np.ndarray, span: np.ndarray) -> np.nda return (points.astype(np.float32) - amin[None, :]) / span[None, :] -def call_openai_interpreter(context: str) -> str: - from openai import OpenAI - - model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini") - instructions = ( - "You are a neuroscientist interpreting brain state dynamics from a " - "manifold flow simulation. The user traced a path through a learned " - "neural manifold. You are given how each ROI's contribution changed " - "along this path.\n\n" - "The delta values do not mean simple activation. They measure how each " - "ROI's contribution to the overall brain state shifted. Positive means " - "the region became a stronger contributor; negative means it became a " - "weaker contributor.\n\n" - "Do not list every ROI. Synthesize the spatial pattern, directionality, " - "and network shift into one coherent interpretation. Keep the response " - "to 2 concise paragraphs." - ) - - try: - client = OpenAI(timeout=60.0) - if hasattr(client, "responses"): - response = client.responses.create( - model=model, - instructions=instructions, - input=context, - max_output_tokens=900, - ) - return (response.output_text or "").strip() - - response = client.chat.completions.create( - model=model, - messages=[ - {"role": "system", "content": instructions}, - {"role": "user", "content": context}, - ], - max_tokens=900, - temperature=0.3, - ) - return (response.choices[0].message.content or "").strip() - except Exception as exc: - return f"[GPT ERROR] {exc}" +def make_json_safe(value): + if isinstance(value, np.ndarray): + return value.tolist() + if isinstance(value, np.generic): + return value.item() + if isinstance(value, dict): + return {str(k): make_json_safe(v) for k, v in value.items() if not str(k).startswith("_")} + if isinstance(value, list): + return [make_json_safe(v) for v in value] + if isinstance(value, tuple): + return [make_json_safe(v) for v in value] + return value diff --git a/mindVisualizer/data/brain_alignment.json b/mindVisualizer/data/brain_alignment.json new file mode 100644 index 0000000000000000000000000000000000000000..2c7002ec79a5343c9a1b4922b81eae695b759a90 --- /dev/null +++ b/mindVisualizer/data/brain_alignment.json @@ -0,0 +1,28 @@ +{ + "matrix": [ + [ + -0.03483789662196803, + -0.1654855793300339, + 0.9305766368497694, + -68.4377205759064 + ], + [ + -0.9422190580860553, + -0.06752555868994387, + -0.047281905216520856, + 106.73199000287599 + ], + [ + 0.0747101104018069, + -0.9287770749837014, + -0.16236864674663903, + 115.80117022013123 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] +} \ No newline at end of file diff --git a/mindVisualizer/data/extra_parcellation/README.md b/mindVisualizer/data/extra_parcellation/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ae29dc39e915aaa6cfbb8cb9a6b3f437cd46cf2d --- /dev/null +++ b/mindVisualizer/data/extra_parcellation/README.md @@ -0,0 +1,53 @@ +# Extra Parcellation Data + +Provides fine-grained subregion labels alongside the primary Allen atlas meshes. + +## Setup (recommended) + +Run the setup script to build the combined atlas automatically: + +```bash +python scripts/setup_extra_parcellation.py +``` + +This downloads and merges three complementary MNI-space atlases via nilearn: + +| Layer | Source | Regions | Role | +|-------|--------|---------|------| +| 1 | Harvard-Oxford Cortical | 48 | Broad cortical coverage | +| 2 | Harvard-Oxford Subcortical | 17 | Thalamus, putamen, caudate, hippocampus, amygdala, etc. | +| 3 | **Julich-Brain** (highest priority) | 62 | Cytoarchitectonic: motor BA4a/4p, somatosensory BA1-3, visual V1-V5, auditory, Broca's, hippocampal subfields | + +**Total: 127 labeled regions**, Julich-Brain labels override coarser Harvard-Oxford +where available. + +Output files (auto-detected by flow mode): +- `combined_atlas.nii.gz` — NIfTI volume (~700KB) +- `combined_atlas_labels.json` — label ID to name map + +## How it works + +The combined atlas is in MNI152 1mm space. The Allen atlas scene space is +approximately MNI-aligned, so probe coordinates can be used directly for lookups. +When the exact voxel is unlabeled, a nearby-search fallback finds the closest +labeled region within a few mm. + +When both the primary Allen mesh and the extra parcellation cover a probe position, +both are highlighted: the Allen mesh in red, the extra parcellation subregion in +orange. The subregion name is included in the LLM context as additional detail. + +## Custom atlas + +You can also provide any NIfTI atlas in MNI space: + +```bash +python examples/flow_probe_example.py \ + --extra-parcellation my_atlas.nii.gz \ + --extra-parcellation-labels my_atlas_labels.json +``` + +## Requirements + +```bash +pip install nilearn nibabel scikit-image +``` diff --git a/mindVisualizer/data/extra_parcellation/combined_atlas.nii.gz b/mindVisualizer/data/extra_parcellation/combined_atlas.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..4b42a04a9e5d9054a2b84716983f1e257022826f --- /dev/null +++ b/mindVisualizer/data/extra_parcellation/combined_atlas.nii.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68b7dbb0b9949e0e3021d60db9d6f1499615560cba955e0a2f6ab2e41962d25a +size 693799 diff --git a/mindVisualizer/data/extra_parcellation/combined_atlas_labels.json b/mindVisualizer/data/extra_parcellation/combined_atlas_labels.json new file mode 100644 index 0000000000000000000000000000000000000000..9cf00f0ce02032a08450b40f44dbd993bd95e0de --- /dev/null +++ b/mindVisualizer/data/extra_parcellation/combined_atlas_labels.json @@ -0,0 +1,129 @@ +{ + "1": "Frontal Pole", + "2": "Insular Cortex", + "3": "Superior Frontal Gyrus", + "4": "Middle Frontal Gyrus", + "5": "Inferior Frontal Gyrus, pars triangularis", + "6": "Inferior Frontal Gyrus, pars opercularis", + "7": "Precentral Gyrus", + "8": "Temporal Pole", + "9": "Superior Temporal Gyrus, anterior division", + "10": "Superior Temporal Gyrus, posterior division", + "11": "Middle Temporal Gyrus, anterior division", + "12": "Middle Temporal Gyrus, posterior division", + "13": "Middle Temporal Gyrus, temporooccipital part", + "14": "Inferior Temporal Gyrus, anterior division", + "15": "Inferior Temporal Gyrus, posterior division", + "16": "Inferior Temporal Gyrus, temporooccipital part", + "17": "Postcentral Gyrus", + "18": "Superior Parietal Lobule", + "19": "Supramarginal Gyrus, anterior division", + "20": "Supramarginal Gyrus, posterior division", + "21": "Angular Gyrus", + "22": "Lateral Occipital Cortex, superior division", + "23": "Lateral Occipital Cortex, inferior division", + "24": "Intracalcarine Cortex", + "25": "Frontal Medial Cortex", + "26": "Juxtapositional Lobule Cortex (formerly Supplementary Motor Cortex)", + "27": "Subcallosal Cortex", + "28": "Paracingulate Gyrus", + "29": "Cingulate Gyrus, anterior division", + "30": "Cingulate Gyrus, posterior division", + "31": "Precuneous Cortex", + "32": "Cuneal Cortex", + "33": "Frontal Orbital Cortex", + "34": "Parahippocampal Gyrus, anterior division", + "35": "Parahippocampal Gyrus, posterior division", + "36": "Lingual Gyrus", + "37": "Temporal Fusiform Cortex, anterior division", + "38": "Temporal Fusiform Cortex, posterior division", + "39": "Temporal Occipital Fusiform Cortex", + "40": "Occipital Fusiform Gyrus", + "41": "Frontal Opercular Cortex", + "42": "Central Opercular Cortex", + "43": "Parietal Opercular Cortex", + "44": "Planum Polare", + "45": "Heschl's Gyrus (includes H1 and H2)", + "46": "Planum Temporale", + "47": "Supracalcarine Cortex", + "48": "Occipital Pole", + "103": "Left Lateral Ventricle", + "104": "Left Thalamus", + "105": "Left Caudate", + "106": "Left Putamen", + "107": "Left Pallidum", + "108": "Brain-Stem", + "109": "Left Hippocampus", + "110": "Left Amygdala", + "111": "Left Accumbens", + "114": "Right Lateral Ventricle", + "115": "Right Thalamus", + "116": "Right Caudate", + "117": "Right Putamen", + "118": "Right Pallidum", + "119": "Right Hippocampus", + "120": "Right Amygdala", + "121": "Right Accumbens", + "201": "Amygdala_centromedial group", + "202": "Amygdala_laterobasal group", + "203": "Amygdala_superficial group", + "204": "Anterior intra-parietal sulcus hIP1", + "205": "Anterior intra-parietal sulcus hIP2", + "206": "Anterior intra-parietal sulcus hIP3", + "207": "Broca's area BA44", + "208": "Broca's area BA45", + "209": "Hippocampus cornu ammonis", + "210": "Hippocampus dentate gyrus", + "211": "Hippocampus entorhinal cortex", + "212": "Hippocampus hippocampal-amygdaloid transition area", + "213": "Hippocampus subiculum", + "214": "Inferior parietal lobule PF", + "215": "Inferior parietal lobule PFcm", + "216": "Inferior parietal lobule PFm", + "217": "Inferior parietal lobule PFop", + "218": "Inferior parietal lobule PFt", + "219": "Inferior parietal lobule PGp", + "220": "Inferior parietal lobule Pga", + "221": "Insula Id1", + "222": "Insula Ig1", + "223": "Insula Ig2", + "224": "Lateral geniculate body", + "225": "Mamillary body", + "226": "Medial geniculate body", + "227": "Premotor cortex BA6", + "228": "Primary auditory cortex TE1.0", + "229": "Primary auditory cortex TE1.1", + "230": "Primary auditory cortex TE1.2", + "231": "Primary motor cortex BA4a", + "232": "Primary motor cortex BA4p", + "233": "Primary somatosensory cortex BA1", + "234": "Primary somatosensory cortex BA2", + "235": "Primary somatosensory cortex BA3a", + "236": "Primary somatosensory cortex BA3b", + "237": "Secondary somatosensory cortex / Parietal operculum OP1", + "238": "Secondary somatosensory cortex / Parietal operculum OP2", + "239": "Secondary somatosensory cortex / Parietal operculum OP3", + "240": "Secondary somatosensory cortex / Parietal operculum OP4", + "241": "Superior parietal lobule 5Ci", + "242": "Superior parietal lobule 5L", + "243": "Superior parietal lobule 5M", + "244": "Superior parietal lobule 7A", + "245": "Superior parietal lobule 7M", + "246": "Superior parietal lobule 7P", + "247": "Superior parietal lobule 7PC", + "248": "Visual cortex V1 BA17", + "249": "Visual cortex V2 BA18", + "250": "Visual cortex V3V", + "251": "Visual cortex V4", + "252": "Visual cortex V5", + "253": "WM: Acoustic radiation", + "254": "WM: Callosal body", + "255": "WM: Cingulum", + "256": "WM: Corticospinal tract", + "257": "WM: Fornix", + "258": "WM: Inferior occipito-frontal fascicle", + "259": "WM: Optic radiation", + "260": "WM: Superior longitudinal fascicle", + "261": "WM: Superior occipito-frontal fascicle", + "262": "WM: Uncinate fascicle" +} \ No newline at end of file diff --git a/mindVisualizer/data/label_grid_cache.keys.json b/mindVisualizer/data/label_grid_cache.keys.json new file mode 100644 index 0000000000000000000000000000000000000000..95bdfe83711a5d3809cb9f88379c3740d1848180 --- /dev/null +++ b/mindVisualizer/data/label_grid_cache.keys.json @@ -0,0 +1 @@ +["", "10159", "10292", "10306", "10307", "10308", "10311", "10313", "10327", "10329", "10330", "10331", "10332", "10333", "10334", "10335", "10336", "10337", "10338", "10339", "10342", "10343", "10344", "10346", "10349", "10361", "10383", "10384", "10389", "10390", "10465", "10466", "10467", "10595", "10596", "10597", "10598", "10599", "10600", "10602", "10648", "10649", "10650", "10651", "10653", "10654", "10655", "10656", "10657", "10658", "10659", "10660", "10661", "10662", "10669", "12114", "12115", "12116", "12117", "12118", "12119", "12121", "12122", "12123", "12124", "12125", "12126", "12127", "12132", "12133", "12136", "12137", "12138", "12140", "12142", "12143", "12144", "12145", "12146", "12147", "12148", "12149", "12150", "12151", "12152", "12153", "12154", "12155", "12156", "12157", "12158", "12159", "12160", "12162", "12163", "12164", "12165", "12166", "12170", "12171", "12173", "12174", "12176", "12177", "12178", "12179", "12181", "12195", "12291", "12292", "12305", "12330", "12354", "12369", "12384", "12390", "12405", "12416", "12535", "12538", "12805", "12826", "13229", "13230", "146034872", "146034876", "146034884", "146034888", "266441657"] \ No newline at end of file diff --git a/mindVisualizer/data/label_grid_cache.npz b/mindVisualizer/data/label_grid_cache.npz new file mode 100644 index 0000000000000000000000000000000000000000..81d7797137531b49f3103f7c59b52e637ce43a4c --- /dev/null +++ b/mindVisualizer/data/label_grid_cache.npz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab71af20799b4cefb9986791337fcb609105b4653141cde9285d486cbfaa4791 +size 101223 diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_entropy.bin b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_entropy.bin new file mode 100644 index 0000000000000000000000000000000000000000..4bb120fcc4dec11a91f64d64041c4c0e28248c2a --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_entropy.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2dc61d9b05f915dad3e8ade5e5d9e5f1df27983cbeaf34c0232010ab7e0008d7 +size 7812500 diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mean_xyz3.bin b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mean_xyz3.bin new file mode 100644 index 0000000000000000000000000000000000000000..f32b4975fa4f7a6e5de97db599322c1006542788 --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mean_xyz3.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34525f6bf4a68ac06f87801aa180b57cfea10c8c8559ea67d23d66ea54865109 +size 23437500 diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_meta.json b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_meta.json new file mode 100644 index 0000000000000000000000000000000000000000..97961ae0e0f3bed6e09569f538d18c6c9dbb9d61 --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_meta.json @@ -0,0 +1 @@ +{"grid":125,"axis_min":[-69.5,-104.5625,-54.25],"axis_max":[70.0625,76.09375,81.8125],"files":{"mean_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mean_xyz3.bin","pi":"mdn_particles_rdcim_teacher_edge_hq_grid125_pi_xyz6.bin","entropy":"mdn_particles_rdcim_teacher_edge_hq_grid125_entropy.bin","mu1_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu1_xyz3.bin","mu2_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu2_xyz3.bin","mu3_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu3_xyz3.bin","mu4_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu4_xyz3.bin","mu5_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu5_xyz3.bin","mu6_xyz3":"mdn_particles_rdcim_teacher_edge_hq_grid125_mu6_xyz3.bin"},"dtype":"float32","layout":"C-order (x,y,z,3) for *_xyz3; (x,y,z,K) for pi_xyzK; (x,y,z) for entropy","endianness":"little","mixture_mean_field":true,"model":{"quality":"hq","components":6},"mdn_source":"particles/rdcim_teacher_edge","bounds_mode":"full_brain","mode":"field","notes":"Trained on teacher segments (XYZ→ΔS) with optional rDCM conditioning and ROI embeddings","training_points_npy":"mdn_particles_rdcim_teacher_edge_hq_training_points.npy"} \ No newline at end of file diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu1_xyz3.bin b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu1_xyz3.bin new file mode 100644 index 0000000000000000000000000000000000000000..684974e8f081ee759909379a2d01d9517a2dca80 --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu1_xyz3.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44c45d1ccd4c2afb936d7a4622493b1d363df9664f6ce53cb26cd5bd892b79e8 +size 23437500 diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu2_xyz3.bin b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu2_xyz3.bin new file mode 100644 index 0000000000000000000000000000000000000000..1dea321b1e46911dd57c3fd69aef2d1430197677 --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu2_xyz3.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f1b9505a5ae611528425d57b09387e35dd921c20377a3b232a4267133b19baf +size 23437500 diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu3_xyz3.bin b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu3_xyz3.bin new file mode 100644 index 0000000000000000000000000000000000000000..a1a3afc5a3ad8d8100f1c3a24b2d998308b0b6f9 --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu3_xyz3.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53b0999649dfccf31f8726695c4d5a738009f408389a16954144be89d2a58c87 +size 23437500 diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu4_xyz3.bin b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu4_xyz3.bin new file mode 100644 index 0000000000000000000000000000000000000000..b79f3a693e60f108df082e07cd5bb0bd1494b72d --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu4_xyz3.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afd81e6a49823e2ddb42f8e4256f2bf58f268f5fbf83d95fbf0dc0f0eedef559 +size 23437500 diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu5_xyz3.bin b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu5_xyz3.bin new file mode 100644 index 0000000000000000000000000000000000000000..be04e37476cd77933b0ab3d3c9074c1601daae9f --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu5_xyz3.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68c1f5eceb934435f58ea77c3b623abb114256cfc23affaa2dba1a202139a4aa +size 23437500 diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu6_xyz3.bin b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu6_xyz3.bin new file mode 100644 index 0000000000000000000000000000000000000000..30fbcc140d88337d398d195d4b32e3f98edd7861 --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_mu6_xyz3.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9827f9f7a923994c84e1a44773fc2fe88dd91fd7905e718083dc752d24a31f4d +size 23437500 diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_pi_xyz6.bin b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_pi_xyz6.bin new file mode 100644 index 0000000000000000000000000000000000000000..7eece50b7104fcce5a585d64bcf2e21d804061aa --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_grid125_pi_xyz6.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffb8a88b783e3a9aefeb8b7c14cd925769fcd3b4ef7d9ca029e4a8f71cd25265 +size 46875000 diff --git a/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_training_points.npy b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_training_points.npy new file mode 100644 index 0000000000000000000000000000000000000000..a837e666681a36038bfc81eb04e577c830bdfa1a --- /dev/null +++ b/mindVisualizer/data/mdn/mdn_particles_rdcim_teacher_edge_hq_training_points.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f4ac819327e772ab01ed85b81150d2a28111df1ba4ebd9b2b3af200a011849b +size 14400128 diff --git a/mindVisualizer/data/meshes_obj/10153.obj b/mindVisualizer/data/meshes_obj/10153.obj new file mode 100644 index 0000000000000000000000000000000000000000..26bfe872647b218e9878389e87cfca6e22d8213c --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10153.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03245194c13e6081c41f81bfafb018f9aa819af57642f1729d6830db236912cb +size 9316827 diff --git a/mindVisualizer/data/meshes_obj/10154.obj b/mindVisualizer/data/meshes_obj/10154.obj new file mode 100644 index 0000000000000000000000000000000000000000..e3cc669e811373f068dcb9fec9b9c6312ba3347a --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10154.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0026eafd072ba11e02f3f4eab7f768ebcf89f32f5a0144fa38d8774576a1baff +size 13523828 diff --git a/mindVisualizer/data/meshes_obj/10155.obj b/mindVisualizer/data/meshes_obj/10155.obj new file mode 100644 index 0000000000000000000000000000000000000000..9df5a2952d1d1601daf7da52e2f17759a8e96a4c --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10155.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89e07d4dc266bd9c69404da5b1ce8a1ea23dd2a41560e30f350b3c49a23e4c65 +size 13523828 diff --git a/mindVisualizer/data/meshes_obj/10156.obj b/mindVisualizer/data/meshes_obj/10156.obj new file mode 100644 index 0000000000000000000000000000000000000000..8b32bfd79ac58bf06c6f30cbaaccec500b31ab49 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10156.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8dd907bcc3ed5f654395b9b2bec702f2f00355d6e7a9d208e18e01766f2782eb +size 12090622 diff --git a/mindVisualizer/data/meshes_obj/10157.obj b/mindVisualizer/data/meshes_obj/10157.obj new file mode 100644 index 0000000000000000000000000000000000000000..35bc3216a4caa2df3ee13c7eb6682cfd862beae2 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10157.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1432ccc092ed2e7b1495e506a11179420fcfb0919519824bb35329ee8171002 +size 3291242 diff --git a/mindVisualizer/data/meshes_obj/10158.obj b/mindVisualizer/data/meshes_obj/10158.obj new file mode 100644 index 0000000000000000000000000000000000000000..0ac2b4d5104ce194d374aee23514fd52c9dca9af --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10158.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be09e3ec39b5cacb24af0e9f7d8e69086fd35853899daea7729bf7f048bb5c6d +size 1992516 diff --git a/mindVisualizer/data/meshes_obj/10159.obj b/mindVisualizer/data/meshes_obj/10159.obj new file mode 100644 index 0000000000000000000000000000000000000000..ef161b112302f52b7e1a78acf4231e0833cee691 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10159.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa7c48344b0af6f41f069450f319e9c360ef8b45fe8f36dded24cb45e7ba0aef +size 149484 diff --git a/mindVisualizer/data/meshes_obj/10292.obj b/mindVisualizer/data/meshes_obj/10292.obj new file mode 100644 index 0000000000000000000000000000000000000000..e858ab1dd18c5a9ef70cec3e0e4d7464f6620ba6 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10292.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c4b4771c184c4512cfadef75b152e0bcbe9d816a62102d7b93349c967d0d955 +size 112083 diff --git a/mindVisualizer/data/meshes_obj/10306.obj b/mindVisualizer/data/meshes_obj/10306.obj new file mode 100644 index 0000000000000000000000000000000000000000..f93a4707d85ea3fffbeddfcc5be9c0e478eae356 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10306.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aef8de5471788784415bdd77b44e5c532cabc79f5fc3f3c4be37e9dd6c5222e8 +size 112083 diff --git a/mindVisualizer/data/meshes_obj/10307.obj b/mindVisualizer/data/meshes_obj/10307.obj new file mode 100644 index 0000000000000000000000000000000000000000..d0e933316a48021b20e86c93af9d756e21e5a999 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10307.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25a071fd3a955681fcd64f6d28fd917c921e9558b792f622649f93ebace11aa6 +size 19205 diff --git a/mindVisualizer/data/meshes_obj/10308.obj b/mindVisualizer/data/meshes_obj/10308.obj new file mode 100644 index 0000000000000000000000000000000000000000..d77c99104bbcdab0291e3ed1658aec3eb662f817 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10308.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83c6c01f3d02a6980403f340692930dbe47339c03784b4c6a41d6cc8bcf45dd2 +size 28542 diff --git a/mindVisualizer/data/meshes_obj/10311.obj b/mindVisualizer/data/meshes_obj/10311.obj new file mode 100644 index 0000000000000000000000000000000000000000..c315f2ad657a194523489c2ba002c95e506171bf --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10311.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e94088b6a00b31e0077ecb75f28d21621f8b8f4ae1a033b76756713b66737fe +size 57711 diff --git a/mindVisualizer/data/meshes_obj/10313.obj b/mindVisualizer/data/meshes_obj/10313.obj new file mode 100644 index 0000000000000000000000000000000000000000..de618be9c90db7ce3ec7ae9ce7f42cdf6986846a --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10313.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22a69c74d7c982a8372ac63c49df436bf8bc0fc1edb401710c4939c85fd9866f +size 48263 diff --git a/mindVisualizer/data/meshes_obj/10327.obj b/mindVisualizer/data/meshes_obj/10327.obj new file mode 100644 index 0000000000000000000000000000000000000000..0928bd1d39509e583577a737434e03af51b5432a --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10327.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61602d7e56749544c6773467d7b8af22aa7788de7640de8c0b18e6fc9630239a +size 48263 diff --git a/mindVisualizer/data/meshes_obj/10329.obj b/mindVisualizer/data/meshes_obj/10329.obj new file mode 100644 index 0000000000000000000000000000000000000000..4f7643766bc7f33df97d002ec3a1dbc464fe6b22 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10329.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f145efd0531e791a5f2059f89a4c649a83b12ffee4bd06648569cc36396bbd3c +size 17345 diff --git a/mindVisualizer/data/meshes_obj/10330.obj b/mindVisualizer/data/meshes_obj/10330.obj new file mode 100644 index 0000000000000000000000000000000000000000..ba7e19a477d8087b255fd8554d41ab4177d1e135 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10330.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d96c21416298bac87134b1f41f762ac122854cd66a93f53bd97a8d833f7b99c +size 36168 diff --git a/mindVisualizer/data/meshes_obj/10331.obj b/mindVisualizer/data/meshes_obj/10331.obj new file mode 100644 index 0000000000000000000000000000000000000000..aaeda3097344cfa25f90ee032f8ef810ae74f4e8 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10331.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e092fe926797259b4568d792ea0439daf36297ab5c9946a689892abe4c3b9aa +size 1853173 diff --git a/mindVisualizer/data/meshes_obj/10332.obj b/mindVisualizer/data/meshes_obj/10332.obj new file mode 100644 index 0000000000000000000000000000000000000000..0e18e9a92710136c7a6e2568fb7e2b649d80598a --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10332.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5030b54f80cad2316a5cc57db2649ac44e38d31d61677bda1710ebb29e1418f +size 1027691 diff --git a/mindVisualizer/data/meshes_obj/10333.obj b/mindVisualizer/data/meshes_obj/10333.obj new file mode 100644 index 0000000000000000000000000000000000000000..36ad701b5de1f2593370bc97d037f112d81c3c61 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10333.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df7e6a6ae97116b644f5090984b56ddcf5da574b5387c22bb8c554f9fff089ce +size 949235 diff --git a/mindVisualizer/data/meshes_obj/10334.obj b/mindVisualizer/data/meshes_obj/10334.obj new file mode 100644 index 0000000000000000000000000000000000000000..4d6681422de10e0fab7364d8efab75ee5f3e6f40 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10334.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a16d8d4d1fcb2cc0ac4be610e5f200c6efd910b3de982492432f563c39f6c2b9 +size 498360 diff --git a/mindVisualizer/data/meshes_obj/10335.obj b/mindVisualizer/data/meshes_obj/10335.obj new file mode 100644 index 0000000000000000000000000000000000000000..02339fd31653adcf58ebcc2f1e4cf609f4e59d96 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10335.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f4c64f991bb22248463cf2415ab33c6cf89235bb85fa26425672958c6362708 +size 225016 diff --git a/mindVisualizer/data/meshes_obj/10336.obj b/mindVisualizer/data/meshes_obj/10336.obj new file mode 100644 index 0000000000000000000000000000000000000000..9459de8daa095face827d07774e99bffb92b3516 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10336.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88b46eb320fe5c91aa1da562277d436caaabace45b0547a1b0d71e13cfa86d81 +size 157144 diff --git a/mindVisualizer/data/meshes_obj/10337.obj b/mindVisualizer/data/meshes_obj/10337.obj new file mode 100644 index 0000000000000000000000000000000000000000..b0479b10eb3b0b8851ab48023772acc459380b23 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10337.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f60b0f68b013cfca7c50fc2afd63db26e64098d8e6476990d7cbc339613d68eb +size 162134 diff --git a/mindVisualizer/data/meshes_obj/10338.obj b/mindVisualizer/data/meshes_obj/10338.obj new file mode 100644 index 0000000000000000000000000000000000000000..a7f302bcc9d63282971bc673aa5421e00911db16 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10338.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a913a6bd57ddc20209c2a7d0c873705c234eb0f116ae0dfbb7fa88a7074d017 +size 374785 diff --git a/mindVisualizer/data/meshes_obj/10339.obj b/mindVisualizer/data/meshes_obj/10339.obj new file mode 100644 index 0000000000000000000000000000000000000000..bad4c44e12f9400762a222849edd45e45260bf57 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10339.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0521f9f55fd1c10d2d9560fff80cb2385a6a9fab003bea1c524e4db1a154bcbb +size 74905 diff --git a/mindVisualizer/data/meshes_obj/10342.obj b/mindVisualizer/data/meshes_obj/10342.obj new file mode 100644 index 0000000000000000000000000000000000000000..1c4ae9ca469b1db4f7eab0971f57bffa89b7cb23 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10342.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0864087632dbee936a66dd7d0ab48f208db7fc57118bdef6a1b37e0dbe38b162 +size 142593 diff --git a/mindVisualizer/data/meshes_obj/10343.obj b/mindVisualizer/data/meshes_obj/10343.obj new file mode 100644 index 0000000000000000000000000000000000000000..6f4a1ae6231e7a0c67ce271cc8d855b2a69463d7 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10343.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c02b9faf662b97b9275ec64a0da4818c28d5c60f0c7299b0f02212b1f0cf2ea +size 121130 diff --git a/mindVisualizer/data/meshes_obj/10344.obj b/mindVisualizer/data/meshes_obj/10344.obj new file mode 100644 index 0000000000000000000000000000000000000000..349eb44f72b0f961f5181922ab476600feff3025 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10344.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf6753d2b9f64cd0f2d6aa8202d6bcdddea7faf10fc4daf6e7c30892c89a2c45 +size 53546 diff --git a/mindVisualizer/data/meshes_obj/10346.obj b/mindVisualizer/data/meshes_obj/10346.obj new file mode 100644 index 0000000000000000000000000000000000000000..fc3bb914fc6c722f8b64aec8d3c3cd959a85a0f1 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10346.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2af0bc2a2c14637a896b13b5ccee911dd9f0d214353ed87aaebbe4b686cdc3d0 +size 396936 diff --git a/mindVisualizer/data/meshes_obj/10349.obj b/mindVisualizer/data/meshes_obj/10349.obj new file mode 100644 index 0000000000000000000000000000000000000000..fecad40334193cff612d106a540ad1d18ffe777c --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10349.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce207044a37d2cbafbcbc36b3469ff44094f16caae936efe64cbbed076f41c05 +size 106623 diff --git a/mindVisualizer/data/meshes_obj/10361.obj b/mindVisualizer/data/meshes_obj/10361.obj new file mode 100644 index 0000000000000000000000000000000000000000..50a89b93487c93022b50f45851b9036757c06a1b --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10361.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb310fac98101e639c504fc751aac087a8d8b8cbded5d5f4e5c0e3764ab03396 +size 374607 diff --git a/mindVisualizer/data/meshes_obj/10383.obj b/mindVisualizer/data/meshes_obj/10383.obj new file mode 100644 index 0000000000000000000000000000000000000000..f4a8e181a92266b2f731a9750597c421d3089c6b --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10383.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:def4c08531f630c8d1f3bfa6ed17cab28b19369edba83d628200a36056c91ce2 +size 116231 diff --git a/mindVisualizer/data/meshes_obj/10384.obj b/mindVisualizer/data/meshes_obj/10384.obj new file mode 100644 index 0000000000000000000000000000000000000000..082e6edadeb22de28567316f5ae4f0c50433b356 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10384.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3acf7f9fdad0d138646f5c944ed1606517cad1e4cb39e1abb06ad074b7df2393 +size 116231 diff --git a/mindVisualizer/data/meshes_obj/10389.obj b/mindVisualizer/data/meshes_obj/10389.obj new file mode 100644 index 0000000000000000000000000000000000000000..4ccecd6399fd67400eaf7fba6544e84208661ea9 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10389.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2959f8557ff9349be5db4c7ed5f7675a8278da8814460ec8e47cc8353700230 +size 1269437 diff --git a/mindVisualizer/data/meshes_obj/10390.obj b/mindVisualizer/data/meshes_obj/10390.obj new file mode 100644 index 0000000000000000000000000000000000000000..c74cf52b9a9f5b114ff8e766aace44c600b95850 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10390.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c387592d26831c252acce15809e6729ba0a0acfd82d99d7cfa214db42fb89909 +size 1211800 diff --git a/mindVisualizer/data/meshes_obj/10465.obj b/mindVisualizer/data/meshes_obj/10465.obj new file mode 100644 index 0000000000000000000000000000000000000000..a58d00461f91af9bfe7507b0f28c945e6f7cb967 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10465.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38506f9126d0e884f429b62d9c4328fe2247ed7d54478c2c1b7c397accf0ac3f +size 29976 diff --git a/mindVisualizer/data/meshes_obj/10466.obj b/mindVisualizer/data/meshes_obj/10466.obj new file mode 100644 index 0000000000000000000000000000000000000000..fb6f83f25acd3785ac28e799a7995e066f4de49d --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10466.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddc40d16f32d80f832e4923b2b8367e19f5ed0fba996622d02c377514cfe0d00 +size 29976 diff --git a/mindVisualizer/data/meshes_obj/10467.obj b/mindVisualizer/data/meshes_obj/10467.obj new file mode 100644 index 0000000000000000000000000000000000000000..04883c17d4107d21aee7986d064ead54768fc3fd --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10467.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f75904e067dcb495977417495089f4e8097255b0c97c52fdf02ea90aee548ed2 +size 26647 diff --git a/mindVisualizer/data/meshes_obj/10557.obj b/mindVisualizer/data/meshes_obj/10557.obj new file mode 100644 index 0000000000000000000000000000000000000000..97cfd19fa95e118cbeec4a0bba53a45480d2fd7c --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10557.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:585bf50b033a56b9706d29654f18a50e125c3a4209b0ad24cc70734e7416f565 +size 21306378 diff --git a/mindVisualizer/data/meshes_obj/10595.obj b/mindVisualizer/data/meshes_obj/10595.obj new file mode 100644 index 0000000000000000000000000000000000000000..a8e59114e16d967dfb9f05ad9a75c9574d2f9ce3 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10595.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c6d1d7ddc793b3e280be78df04fee6bbb34ef62490fc25d96b96810fcbf0a78 +size 1207882 diff --git a/mindVisualizer/data/meshes_obj/10596.obj b/mindVisualizer/data/meshes_obj/10596.obj new file mode 100644 index 0000000000000000000000000000000000000000..35e75c463098c754bc9aecaf472d7e55314ad440 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10596.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:975b8bfd36587af81110e4a935263b3280878d6d4be90af2436fd62e5c51b14e +size 1135648 diff --git a/mindVisualizer/data/meshes_obj/10597.obj b/mindVisualizer/data/meshes_obj/10597.obj new file mode 100644 index 0000000000000000000000000000000000000000..fd7daab9a093ea371b98a877c97179e39e89b422 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10597.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8a197cd5d518213c1de446dced561e29dcaecb630682e22f0db9a71ce43fba8 +size 363233 diff --git a/mindVisualizer/data/meshes_obj/10598.obj b/mindVisualizer/data/meshes_obj/10598.obj new file mode 100644 index 0000000000000000000000000000000000000000..e3c9d92d0f1eb9dbafc3301a62b544345d5a1237 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10598.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3969bed0ece09fa26650bd74357405de379ea48f5716da11af07234e76978976 +size 285736 diff --git a/mindVisualizer/data/meshes_obj/10599.obj b/mindVisualizer/data/meshes_obj/10599.obj new file mode 100644 index 0000000000000000000000000000000000000000..3960468805dea68188e3da91f1ee80066e8ec181 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10599.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34d0002dc0f217f8ca002e2784c8ce28295d2ea0f19b98aea4b1a48738e5c825 +size 64287 diff --git a/mindVisualizer/data/meshes_obj/10600.obj b/mindVisualizer/data/meshes_obj/10600.obj new file mode 100644 index 0000000000000000000000000000000000000000..bae4af76242defc221301e25c47de23af35edd3f --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10600.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3a7515fe8e1590c0232cd2550b4eb7a969cb1f349c961ed433b1166d5156002 +size 261696 diff --git a/mindVisualizer/data/meshes_obj/10602.obj b/mindVisualizer/data/meshes_obj/10602.obj new file mode 100644 index 0000000000000000000000000000000000000000..35026ad8c54e2fe43653b163e225dffe7c0fc1fb --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10602.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:083525f8e6b168d55007de4743b3d967865ca21490423f65d85241b0633e015d +size 60183 diff --git a/mindVisualizer/data/meshes_obj/10609.obj b/mindVisualizer/data/meshes_obj/10609.obj new file mode 100644 index 0000000000000000000000000000000000000000..3b5174f39755ea47574910455641a37d5cb0e9f1 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10609.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70d5805170883e21dd580488d02cddbb6e806c0578884374af0802e7c05c6eaf +size 24970539 diff --git a/mindVisualizer/data/meshes_obj/10648.obj b/mindVisualizer/data/meshes_obj/10648.obj new file mode 100644 index 0000000000000000000000000000000000000000..370608d94f52c3636ac32c451ae1dbc892519a54 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10648.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:167345c94b0e77d00592694f950ed34d4831b485c53cb42f596139a8b41d7646 +size 384366 diff --git a/mindVisualizer/data/meshes_obj/10649.obj b/mindVisualizer/data/meshes_obj/10649.obj new file mode 100644 index 0000000000000000000000000000000000000000..ae8a186b01ce7189c34362dba3c2345578316bde --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10649.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46ed49218abb1d6abc57cc58ef40570fdc4543cb0865ee427f7b1b5a7f878e14 +size 267287 diff --git a/mindVisualizer/data/meshes_obj/10650.obj b/mindVisualizer/data/meshes_obj/10650.obj new file mode 100644 index 0000000000000000000000000000000000000000..896cecc8744ecaeb6885fb36f44cfd8db701ef9c --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10650.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b88f2efcaf1a09847f97cd513cb090d3bd1960a3544a3c90f892dc94f8cb773 +size 208885 diff --git a/mindVisualizer/data/meshes_obj/10651.obj b/mindVisualizer/data/meshes_obj/10651.obj new file mode 100644 index 0000000000000000000000000000000000000000..71f6839a0b84a467b9ecca4d47d04bfe8fd1a5e2 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10651.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5605b9e27eeabc9c587d1491f74597215ed6913148987b60a0f7a53c07043cc1 +size 19216 diff --git a/mindVisualizer/data/meshes_obj/10653.obj b/mindVisualizer/data/meshes_obj/10653.obj new file mode 100644 index 0000000000000000000000000000000000000000..9d055cb035244510aac6fb633e5d03b4bdf1de76 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10653.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44f0ac48d5bcb61fcab330100ed2fa82b38c1fe6e78d76d20ef0d0549e0bafe7 +size 2358613 diff --git a/mindVisualizer/data/meshes_obj/10654.obj b/mindVisualizer/data/meshes_obj/10654.obj new file mode 100644 index 0000000000000000000000000000000000000000..a8016f5a5a5743904182df39fe659679ad90be83 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10654.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c5d3abc4e700d115d11a234779ae29e356adeb749a7fd6141a7166a60cc875d +size 3544662 diff --git a/mindVisualizer/data/meshes_obj/10655.obj b/mindVisualizer/data/meshes_obj/10655.obj new file mode 100644 index 0000000000000000000000000000000000000000..082094b4c3e352408fbeedcb10e16790c95ae30e --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10655.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee4ddd140cdedd1bbaa82e3b4318f2ca06ebecac8f5b4b719f93c6fd43a2893b +size 3459944 diff --git a/mindVisualizer/data/meshes_obj/10656.obj b/mindVisualizer/data/meshes_obj/10656.obj new file mode 100644 index 0000000000000000000000000000000000000000..4c6474e1c226563728bf5fe14d9b1149286412e0 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10656.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64564caa0e72057bcdac59245b0cfe47f68f8eea7137ce31a89aaddcfc536b57 +size 3102041 diff --git a/mindVisualizer/data/meshes_obj/10657.obj b/mindVisualizer/data/meshes_obj/10657.obj new file mode 100644 index 0000000000000000000000000000000000000000..5d6aaa523d4bbfbe1cdf30d53adf94a957b4081c --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10657.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bb2ffde5017fb080131381fa2a5649b80e866218e356447045d925d1fc4916b +size 3017443 diff --git a/mindVisualizer/data/meshes_obj/10658.obj b/mindVisualizer/data/meshes_obj/10658.obj new file mode 100644 index 0000000000000000000000000000000000000000..7c5da507a9b27058da1a579b816bc3ebe0be854a --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10658.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7723ef027c48d25f5a7bc0a11894f6172a33b498b051daa5e629242d337328e0 +size 336299 diff --git a/mindVisualizer/data/meshes_obj/10659.obj b/mindVisualizer/data/meshes_obj/10659.obj new file mode 100644 index 0000000000000000000000000000000000000000..20cb94eb622b61158a10de276d23b08dd6dd3124 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10659.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78b2f07997d3ca0162b1a9274cd0f193d681eca030136d3dd5a04afca8f26bd5 +size 2860098 diff --git a/mindVisualizer/data/meshes_obj/10660.obj b/mindVisualizer/data/meshes_obj/10660.obj new file mode 100644 index 0000000000000000000000000000000000000000..7e4eea40a6cd074f2ad05de04370776d6f61fb00 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10660.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5c88fed97d961850a08737dc5924f663db934948eb8a0df7fd32ac9f3bb0a8a +size 84737 diff --git a/mindVisualizer/data/meshes_obj/10661.obj b/mindVisualizer/data/meshes_obj/10661.obj new file mode 100644 index 0000000000000000000000000000000000000000..e90e3f6c413128423e2debebcfca9eccbdcea67d --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10661.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6825d7c0e678dc6af726dbf4d5702a4693051d58a861820b3e7a046364c4821d +size 320856 diff --git a/mindVisualizer/data/meshes_obj/10662.obj b/mindVisualizer/data/meshes_obj/10662.obj new file mode 100644 index 0000000000000000000000000000000000000000..a992deb3a2152df56cb441e13ac92ef729f44ed6 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10662.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:622d1dc203cdfbb8a0dba07a975686723e3b5d5c27f5923f5aa364f940586c8f +size 156946 diff --git a/mindVisualizer/data/meshes_obj/10668.obj b/mindVisualizer/data/meshes_obj/10668.obj new file mode 100644 index 0000000000000000000000000000000000000000..3224f5438fb836c7db34ebf04c35de8cd24670e2 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10668.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f59c3aa1c7a7342ee1905a8084f8d7e7a9136a1b079588dc1a087f591bda2a4 +size 2710068 diff --git a/mindVisualizer/data/meshes_obj/10669.obj b/mindVisualizer/data/meshes_obj/10669.obj new file mode 100644 index 0000000000000000000000000000000000000000..7ed50ea191373f35541f5063da2c6d12084bbf6a --- /dev/null +++ b/mindVisualizer/data/meshes_obj/10669.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8936c956fe026d7d7c3fd8fde2e4ad860230b6598bf16f0ab9ade5f0696debd0 +size 146189 diff --git a/mindVisualizer/data/meshes_obj/12112.obj b/mindVisualizer/data/meshes_obj/12112.obj new file mode 100644 index 0000000000000000000000000000000000000000..d0e0f669224eb159b801496626485bb840b1b434 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12112.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:578e35c437c55a3c2cb6040bcb31b7476e268ea1bbe29ff33b78d50a99dd25f2 +size 24978151 diff --git a/mindVisualizer/data/meshes_obj/12113.obj b/mindVisualizer/data/meshes_obj/12113.obj new file mode 100644 index 0000000000000000000000000000000000000000..610ba444af6334392ad06a2fc709578d73322043 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12113.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:096e7a0d1b188abd87e7a128c1af0e7980b213168b1ea5c1ecdf0f7a2a1fa415 +size 9720765 diff --git a/mindVisualizer/data/meshes_obj/12114.obj b/mindVisualizer/data/meshes_obj/12114.obj new file mode 100644 index 0000000000000000000000000000000000000000..c75cffc9b149ee9f7f579ba4e9b3b0e87eef7960 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12114.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0753dff2732096dfc33fdc6d590197664b5962c643b45160b38978276046db4 +size 1874111 diff --git a/mindVisualizer/data/meshes_obj/12115.obj b/mindVisualizer/data/meshes_obj/12115.obj new file mode 100644 index 0000000000000000000000000000000000000000..404acaa899145b14ff8bca325fa92758fc80afbc --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12115.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de843826a32cb3ebe6e8ec16a72820001e8bcf86b2fc6b3f533d64cb1caa5120 +size 2747516 diff --git a/mindVisualizer/data/meshes_obj/12116.obj b/mindVisualizer/data/meshes_obj/12116.obj new file mode 100644 index 0000000000000000000000000000000000000000..569cd3c7f4992158f5ca8c671f43233134a4ac19 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12116.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a4b659f333592ff74aa4605d33949ac06400b7d53840158d14954589280cd2a +size 2238130 diff --git a/mindVisualizer/data/meshes_obj/12117.obj b/mindVisualizer/data/meshes_obj/12117.obj new file mode 100644 index 0000000000000000000000000000000000000000..42a1a0ef61732ff52eca3d6a55f1ce9aa3fc536f --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12117.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7fa9077163c098c180061ade06d363ad21bb5d7667b3137aa1dbb89b11460228 +size 1086983 diff --git a/mindVisualizer/data/meshes_obj/12118.obj b/mindVisualizer/data/meshes_obj/12118.obj new file mode 100644 index 0000000000000000000000000000000000000000..60c102d09ff7d34ff750bb8a486fb2de04ae1268 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12118.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d799bf2b601e9de14fd0d51777429c1a836532e4e873b51cbaa50fbd8e6f3f28 +size 694317 diff --git a/mindVisualizer/data/meshes_obj/12119.obj b/mindVisualizer/data/meshes_obj/12119.obj new file mode 100644 index 0000000000000000000000000000000000000000..f833a0808a7f81f6f2b462eb04802188033b7951 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12119.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43cf3569255b3de29765cd5dcdf7065fd6d85b616b89d3b12a8d602f2ce2a0a0 +size 471273 diff --git a/mindVisualizer/data/meshes_obj/12121.obj b/mindVisualizer/data/meshes_obj/12121.obj new file mode 100644 index 0000000000000000000000000000000000000000..76084dab7ec0b0dcd112dfbf338019b54b5e1ea4 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12121.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88c6fca9a0f4a3eb2020a2d2da86fb26cc0a6475899ca183f2b435513809f655 +size 336867 diff --git a/mindVisualizer/data/meshes_obj/12122.obj b/mindVisualizer/data/meshes_obj/12122.obj new file mode 100644 index 0000000000000000000000000000000000000000..e417d291a144c809a8a9a68af612508b9fd7e105 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12122.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ca603b9eeca7547b66754c005d87aee480db4cfcfb2b3e3204557610fe5e15b +size 438226 diff --git a/mindVisualizer/data/meshes_obj/12123.obj b/mindVisualizer/data/meshes_obj/12123.obj new file mode 100644 index 0000000000000000000000000000000000000000..7ef818ba475aafd191b0b36271fbc4995a6915f7 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12123.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f69201b2d75e6456287290d8799fa4d51755d186d87ef21ee055b202a8004236 +size 249258 diff --git a/mindVisualizer/data/meshes_obj/12124.obj b/mindVisualizer/data/meshes_obj/12124.obj new file mode 100644 index 0000000000000000000000000000000000000000..0879cc605b1d6b05514a2c75fbe5f0ad026ba73e --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12124.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f009998041d5b560b4e0cfdaa22f0f1e6d41d1891fcfbd64a742198387072635 +size 355966 diff --git a/mindVisualizer/data/meshes_obj/12125.obj b/mindVisualizer/data/meshes_obj/12125.obj new file mode 100644 index 0000000000000000000000000000000000000000..5d0466ce82f46f06f91b98331c75d9a8a97ff249 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12125.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee8afe5307d7c6b3c073120c675f87dea48cd004b8ca34327983fe8b4d2c33de +size 551177 diff --git a/mindVisualizer/data/meshes_obj/12126.obj b/mindVisualizer/data/meshes_obj/12126.obj new file mode 100644 index 0000000000000000000000000000000000000000..7a94fa32a403acede52db389d188ae3cda1aa0cf --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12126.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a343378c55c7ab0c6e3868e5b9b3f34b8d3e8b8972b000adb238f0fec66f5c6 +size 349423 diff --git a/mindVisualizer/data/meshes_obj/12127.obj b/mindVisualizer/data/meshes_obj/12127.obj new file mode 100644 index 0000000000000000000000000000000000000000..9327442fa3748166a691c2f84a383e056c99d21a --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12127.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c80da64dddfe9acdc1705ba36098af1196e156f5221d0b496791550a108ac9e +size 235612 diff --git a/mindVisualizer/data/meshes_obj/12131.obj b/mindVisualizer/data/meshes_obj/12131.obj new file mode 100644 index 0000000000000000000000000000000000000000..5e0af541d1937b4ffec8d8a9875db67a292aa082 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12131.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38ae817c2dd5691194bb1a9ed5a800bf22bd5d7e1ae125f3192ef70f4cc925eb +size 6385458 diff --git a/mindVisualizer/data/meshes_obj/12132.obj b/mindVisualizer/data/meshes_obj/12132.obj new file mode 100644 index 0000000000000000000000000000000000000000..79184a11d94a4d6329120d47ed30518fd69735af --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12132.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df4cbf2de2d4a60b662348cb342e7661355a11899f1ca1364e4930cada9d61a8 +size 1771312 diff --git a/mindVisualizer/data/meshes_obj/12133.obj b/mindVisualizer/data/meshes_obj/12133.obj new file mode 100644 index 0000000000000000000000000000000000000000..69c6e61c516562a4a94d9cf38d8fae2c948d571b --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12133.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ee3aff19c07fd079435bc1e29ddf5be256bab7b36247cd81f04d10b0601b8a8 +size 1340659 diff --git a/mindVisualizer/data/meshes_obj/12134.obj b/mindVisualizer/data/meshes_obj/12134.obj new file mode 100644 index 0000000000000000000000000000000000000000..e0f61b328c37794122321cfdd15425fe794e048d --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12134.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0606da3e272bac8f06a9b2a3807993b3f172eb0ee18c19231ca2338480acd1ce +size 2277035 diff --git a/mindVisualizer/data/meshes_obj/12135.obj b/mindVisualizer/data/meshes_obj/12135.obj new file mode 100644 index 0000000000000000000000000000000000000000..a8ff271460b82c00044cf6a32c8087308eb984f3 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12135.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e10d73f4c87da6d5b7b6b74b5b43020de3bef75d8c7cee2a6679edcedc751190 +size 1129994 diff --git a/mindVisualizer/data/meshes_obj/12136.obj b/mindVisualizer/data/meshes_obj/12136.obj new file mode 100644 index 0000000000000000000000000000000000000000..ae6b39c728ad1975db823bfefc704427b8719602 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12136.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:903ed73d986de326c928528581232b5cf22ae4982a3ba30a61cf65e5669e206d +size 1259755 diff --git a/mindVisualizer/data/meshes_obj/12137.obj b/mindVisualizer/data/meshes_obj/12137.obj new file mode 100644 index 0000000000000000000000000000000000000000..51dc02792787dc291ef5e420b8718078b034ec00 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12137.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7789f0ff6b670010a64444c7934ff9286074578251a12e7c86d2780d4a5c674b +size 1257302 diff --git a/mindVisualizer/data/meshes_obj/12138.obj b/mindVisualizer/data/meshes_obj/12138.obj new file mode 100644 index 0000000000000000000000000000000000000000..52e9d9c77a71186656736d3391c0d7d9a910203d --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12138.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c44970e7dc96edfc0e98f462d12f6e085cdbe8211f9b4c3294bba9ec77768f02 +size 261407 diff --git a/mindVisualizer/data/meshes_obj/12139.obj b/mindVisualizer/data/meshes_obj/12139.obj new file mode 100644 index 0000000000000000000000000000000000000000..e3fa1e2727b92805d5c3b468fe9d11d0e0a5c468 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12139.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:587a481076c6c2c2325835784b63903612ec1c883b81c7b67c81f6641f2996d0 +size 4869934 diff --git a/mindVisualizer/data/meshes_obj/12140.obj b/mindVisualizer/data/meshes_obj/12140.obj new file mode 100644 index 0000000000000000000000000000000000000000..d4fc518d328963fe4d90b30f8286cacf193437c1 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12140.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:937350cd319d2de508b46bd62101a7f33af4f0ed1ab7cd3ade124bba7c9cb451 +size 1121638 diff --git a/mindVisualizer/data/meshes_obj/12141.obj b/mindVisualizer/data/meshes_obj/12141.obj new file mode 100644 index 0000000000000000000000000000000000000000..0da70b84c3c437ba1ca219ec841b229888e287db --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12141.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5662ba68f1b464d0ced0427db5204c83ca3cd2a4d2727795cd5a9260f66291c +size 1856777 diff --git a/mindVisualizer/data/meshes_obj/12142.obj b/mindVisualizer/data/meshes_obj/12142.obj new file mode 100644 index 0000000000000000000000000000000000000000..f6cb1328b2c020e3377e09896aa18eae98631115 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12142.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30cef05768b32bc147d518726a185a0109de2ee918079d8f79051635c5e16fcc +size 1322430 diff --git a/mindVisualizer/data/meshes_obj/12143.obj b/mindVisualizer/data/meshes_obj/12143.obj new file mode 100644 index 0000000000000000000000000000000000000000..d252fd99bd2d1982a4b18c9ae66281fd22a7ad7c --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12143.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe22d0190841509ea1f0740b6207fbfe836642c850d4fd6437dd4dd59a0a0539 +size 725404 diff --git a/mindVisualizer/data/meshes_obj/12144.obj b/mindVisualizer/data/meshes_obj/12144.obj new file mode 100644 index 0000000000000000000000000000000000000000..df6a1e95f8cdcd63a8717bf946cd60aaf875f919 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12144.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87c7db1602e4fb98b61f1bab332d94e8b2453d3d615332f461dd6004bfc0bd36 +size 302543 diff --git a/mindVisualizer/data/meshes_obj/12145.obj b/mindVisualizer/data/meshes_obj/12145.obj new file mode 100644 index 0000000000000000000000000000000000000000..4b41f3c2cbdaf93ee65995298134ee02c6ec8da6 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12145.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36029ca3944191328b3d316cfb6dc8b03aa1a646c9b27d0d7471433f797bc0c7 +size 293779 diff --git a/mindVisualizer/data/meshes_obj/12146.obj b/mindVisualizer/data/meshes_obj/12146.obj new file mode 100644 index 0000000000000000000000000000000000000000..1dca3a3da676c0b0f762e778119167c4e35d3cc0 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12146.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14a013c83da52d6cbdf27d6aa32719ff3c944c73a0235b06fc498d554b9a151f +size 374239 diff --git a/mindVisualizer/data/meshes_obj/12147.obj b/mindVisualizer/data/meshes_obj/12147.obj new file mode 100644 index 0000000000000000000000000000000000000000..929c3eb9355da607a52f8c6aa2036a7a215ac9f0 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12147.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c0f1e1e93d3753c88d1c2b42ee96295c73adc2fe55e0b41c107f2562f549146 +size 425893 diff --git a/mindVisualizer/data/meshes_obj/12148.obj b/mindVisualizer/data/meshes_obj/12148.obj new file mode 100644 index 0000000000000000000000000000000000000000..730cc9afcf32fef0d3fa851ac4a8ad0e0a32bef8 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12148.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bf7b8d298b2ae8071c176a79cb08e5309160843ce558d227c0103e6be9a2b1c +size 3692837 diff --git a/mindVisualizer/data/meshes_obj/12149.obj b/mindVisualizer/data/meshes_obj/12149.obj new file mode 100644 index 0000000000000000000000000000000000000000..b1a99ede208dc06d69e0068a6bfd37d5a7503913 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12149.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29898d6958985c06c331b65a08d77610f3c30cce4bff49f5310a020f645fe119 +size 282160 diff --git a/mindVisualizer/data/meshes_obj/12150.obj b/mindVisualizer/data/meshes_obj/12150.obj new file mode 100644 index 0000000000000000000000000000000000000000..8e02414d91468089fe85700ea9d4a3879ee54342 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12150.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abb044bc40081b10119fda6b4b47970781a4e0800b7a3da0249eea66b7016fed +size 760265 diff --git a/mindVisualizer/data/meshes_obj/12151.obj b/mindVisualizer/data/meshes_obj/12151.obj new file mode 100644 index 0000000000000000000000000000000000000000..f00acc279682105a5bdc38685820a8019a41d546 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12151.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01fda3a48c97c7e483738bf89f8ef25a6d985441402e283dec770e9665e5dc54 +size 1167556 diff --git a/mindVisualizer/data/meshes_obj/12152.obj b/mindVisualizer/data/meshes_obj/12152.obj new file mode 100644 index 0000000000000000000000000000000000000000..3e27089f8be616066860c58529e3a82e0bf3b2cf --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12152.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:459b95c9928e5d8696e1952c3e7f7399a8064cb2e5956362a14602e6ac34baeb +size 279313 diff --git a/mindVisualizer/data/meshes_obj/12153.obj b/mindVisualizer/data/meshes_obj/12153.obj new file mode 100644 index 0000000000000000000000000000000000000000..7a01a27fb05852f6b1c7472140fbbfde3c109f19 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12153.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0bec42f5f15d6f133e6cecce78d4a8506c6cb0c4276eba9d57271e91505329e +size 1121718 diff --git a/mindVisualizer/data/meshes_obj/12154.obj b/mindVisualizer/data/meshes_obj/12154.obj new file mode 100644 index 0000000000000000000000000000000000000000..cd342b1e990b2c5b60b2975efe2befba369b66b9 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12154.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af877beadae12a37d518b185fd8ef2e67623ff2a4877606b5eb92f1e2b3316d4 +size 1099560 diff --git a/mindVisualizer/data/meshes_obj/12155.obj b/mindVisualizer/data/meshes_obj/12155.obj new file mode 100644 index 0000000000000000000000000000000000000000..8d8a9d082f54e3d5bc0b6bd42ea7de5de5cd5f2d --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12155.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65d3119a5ba7373668b33edb03d4eeb8232db04463a71aef2434d20d977f78e4 +size 2452029 diff --git a/mindVisualizer/data/meshes_obj/12156.obj b/mindVisualizer/data/meshes_obj/12156.obj new file mode 100644 index 0000000000000000000000000000000000000000..ad35f29491c655af5a317b0d58e236a1a3d74b81 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12156.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bb34676082c11f5b20f19d3bd9e2646b6a8b99304ff8bd53ba0b554ad0f1956 +size 1379161 diff --git a/mindVisualizer/data/meshes_obj/12157.obj b/mindVisualizer/data/meshes_obj/12157.obj new file mode 100644 index 0000000000000000000000000000000000000000..d60602c21b986395df54d24aafe652e5e4d6faba --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12157.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96e649438633f3a63b9ce76e76991dc399d7cc68b5cbd2b893203a9178355169 +size 590594 diff --git a/mindVisualizer/data/meshes_obj/12158.obj b/mindVisualizer/data/meshes_obj/12158.obj new file mode 100644 index 0000000000000000000000000000000000000000..9a29e01478877299e5d6a5f71625919c3facbdf1 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12158.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735223c9243559357f694339c979aa6c6efddd19f1eacba773493812b9fb8c63 +size 740724 diff --git a/mindVisualizer/data/meshes_obj/12159.obj b/mindVisualizer/data/meshes_obj/12159.obj new file mode 100644 index 0000000000000000000000000000000000000000..40d517cfa36f0cc74adeb1495b794ef905f814c8 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12159.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1075bdc23d549acf09252207ef637138a08a58fd37094f9b0210793255aa59d4 +size 93502 diff --git a/mindVisualizer/data/meshes_obj/12160.obj b/mindVisualizer/data/meshes_obj/12160.obj new file mode 100644 index 0000000000000000000000000000000000000000..9da38b9f69d24b2772004d3a74a8230b91aa524d --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12160.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:00143239053a02a434ebfa271509316105e6dfbf90d72b3031784c1ae7a09945 +size 82678 diff --git a/mindVisualizer/data/meshes_obj/12162.obj b/mindVisualizer/data/meshes_obj/12162.obj new file mode 100644 index 0000000000000000000000000000000000000000..6f93e816158a5fb969e6b539d6ab8141ec08458d --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12162.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8dc105eee019b1991719bf030356d0184e29e1a5ed5b9d100457dcfe53d9948e +size 561352 diff --git a/mindVisualizer/data/meshes_obj/12163.obj b/mindVisualizer/data/meshes_obj/12163.obj new file mode 100644 index 0000000000000000000000000000000000000000..fdd35267438e81020439cc7ddac2c39cb3bc7976 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12163.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f3b461c65d09e4448622ec5af405baa72305cd86e4da0ce32660d8be37b8fb4 +size 303814 diff --git a/mindVisualizer/data/meshes_obj/12164.obj b/mindVisualizer/data/meshes_obj/12164.obj new file mode 100644 index 0000000000000000000000000000000000000000..f8b68c0c577a66ccd5c0b77cc3ff77bcf5e3e289 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12164.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:527107cbf93ff91c9c7f389857068cf742b0297f4bdfd5a023d0dae3c6f754ca +size 235117 diff --git a/mindVisualizer/data/meshes_obj/12165.obj b/mindVisualizer/data/meshes_obj/12165.obj new file mode 100644 index 0000000000000000000000000000000000000000..81e5e779eac5d797fd59d6c8ec93d3951738fd5d --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12165.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b114f9e0ab4a3399e1939d7d62b1d284c461c35ee282801c0ceb721abbce29c5 +size 48967 diff --git a/mindVisualizer/data/meshes_obj/12166.obj b/mindVisualizer/data/meshes_obj/12166.obj new file mode 100644 index 0000000000000000000000000000000000000000..efd8dc213b6d2e4c2c6bc7cecaeb6a7ce1775b36 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12166.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83f2f1d07a0cbbe0bd0cdf191c95595d78fb72025765afa682548883610321d1 +size 48967 diff --git a/mindVisualizer/data/meshes_obj/12170.obj b/mindVisualizer/data/meshes_obj/12170.obj new file mode 100644 index 0000000000000000000000000000000000000000..879e9f4f22853a465972427cdd2d45af0c0a132b --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12170.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47412aaac5118354b3aa4a5bc444abe4d32d5131c3ab54ba2c4b57162e248089 +size 421200 diff --git a/mindVisualizer/data/meshes_obj/12171.obj b/mindVisualizer/data/meshes_obj/12171.obj new file mode 100644 index 0000000000000000000000000000000000000000..e39da89105cac0dd181bacf79aa19432f4097377 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12171.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:215566c1a2901c3b1d803963643f64b6aae1071f78e6e1fe5aec604bfc0568db +size 157743 diff --git a/mindVisualizer/data/meshes_obj/12173.obj b/mindVisualizer/data/meshes_obj/12173.obj new file mode 100644 index 0000000000000000000000000000000000000000..a5c54c71cf50d625a0742b2154f0b1e14ac01a4e --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12173.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fe912544a865c8a6732007d77baabfb53a485126409cef082b33780c446136b +size 142662 diff --git a/mindVisualizer/data/meshes_obj/12174.obj b/mindVisualizer/data/meshes_obj/12174.obj new file mode 100644 index 0000000000000000000000000000000000000000..f9cc23c6b260bb914bfe2085be6ea3fb2c4e0e2b --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12174.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28bc005381b17f1fd709711a213609f9a82d71ee6ff57ed188f23913e5702394 +size 136459 diff --git a/mindVisualizer/data/meshes_obj/12176.obj b/mindVisualizer/data/meshes_obj/12176.obj new file mode 100644 index 0000000000000000000000000000000000000000..099ec3f6a07882d0ca2c26d04563d0b5e14c9bfe --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12176.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bcddcfc397dfd7230a4caa028d679c998c6d91ae6cb40cbdb88972d740b759c +size 794613 diff --git a/mindVisualizer/data/meshes_obj/12177.obj b/mindVisualizer/data/meshes_obj/12177.obj new file mode 100644 index 0000000000000000000000000000000000000000..22db4c725d1a4a3da7c7fb68cd1032a5d9942342 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12177.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6d02b80e9f740eb6032702f353e0c225ff88e40a45be420e79bb26acc37a5f8 +size 332253 diff --git a/mindVisualizer/data/meshes_obj/12178.obj b/mindVisualizer/data/meshes_obj/12178.obj new file mode 100644 index 0000000000000000000000000000000000000000..791052f00ec355b6ce52797e97535825adc556a9 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12178.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:213584f7507394c3def538d4aa7f0f8cb9de0ff537a9c4c9c8a7e8aafedc5a44 +size 529708 diff --git a/mindVisualizer/data/meshes_obj/12179.obj b/mindVisualizer/data/meshes_obj/12179.obj new file mode 100644 index 0000000000000000000000000000000000000000..3731014d7464621095c60f510621de16e6773216 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12179.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22fa862fab6f50ba260b8cc52e429ed1befc0ddb6fd345c4f198c33233abcf79 +size 26774 diff --git a/mindVisualizer/data/meshes_obj/12181.obj b/mindVisualizer/data/meshes_obj/12181.obj new file mode 100644 index 0000000000000000000000000000000000000000..6cff697ec1dd3ef6b823d4c499b29c2f58bc87bf --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12181.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a7871ecf00b0e7ee2027798c3b5e12e65645515951bf77aa994532bdff89589 +size 40725 diff --git a/mindVisualizer/data/meshes_obj/12195.obj b/mindVisualizer/data/meshes_obj/12195.obj new file mode 100644 index 0000000000000000000000000000000000000000..6144ecf3c9ad7783089a0a898479cf137c5fd70b --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12195.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2d5109fac0c6d709838c7dfcf8c89e802a97277b452fc4787f0c5cd0d295f2c +size 242277 diff --git a/mindVisualizer/data/meshes_obj/12291.obj b/mindVisualizer/data/meshes_obj/12291.obj new file mode 100644 index 0000000000000000000000000000000000000000..abb94839d2155fb7460a6c920f953ec6c1c78a66 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12291.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50f9f817896c8751de1f620750a47e93e1d39e6fa53f8b76e2a5674447b7abb6 +size 68325 diff --git a/mindVisualizer/data/meshes_obj/12292.obj b/mindVisualizer/data/meshes_obj/12292.obj new file mode 100644 index 0000000000000000000000000000000000000000..d2f3d9653b3228efa2671b8417f84bbaf35f50e0 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12292.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:838df099e4db593f2a25b4c7339a8274c6fb95689e97c8222c21777f28b94453 +size 38146 diff --git a/mindVisualizer/data/meshes_obj/12305.obj b/mindVisualizer/data/meshes_obj/12305.obj new file mode 100644 index 0000000000000000000000000000000000000000..466d2c1d89f7a83e68c04b1aced592f5a3b4c237 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12305.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97210562142f089bb2ae983d4ec3b2859934e27eeb459b280820ac29abba565b +size 46626 diff --git a/mindVisualizer/data/meshes_obj/12330.obj b/mindVisualizer/data/meshes_obj/12330.obj new file mode 100644 index 0000000000000000000000000000000000000000..4a23fe6d45497ad4066c95f3c564be488029c833 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12330.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5125c52ad3b77a154fb8a0518e0c26cdd30a9a4be2d220e53cf7045e382f437 +size 140523 diff --git a/mindVisualizer/data/meshes_obj/12354.obj b/mindVisualizer/data/meshes_obj/12354.obj new file mode 100644 index 0000000000000000000000000000000000000000..b4a38472f983aa0ffe56a039cd2e9e674d4b5178 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12354.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c35a6e650d51bce958cb26b68ad317c20d3555458fdc376c496cc839e23a2af8 +size 60975 diff --git a/mindVisualizer/data/meshes_obj/12369.obj b/mindVisualizer/data/meshes_obj/12369.obj new file mode 100644 index 0000000000000000000000000000000000000000..1e077b9856c3940ff9a04a70ce7d4ec1d9c36f1c --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12369.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d3470b4ed53b794b5edd8d23bad71a55edae42b06c4cb4e14da24936a377364 +size 19216 diff --git a/mindVisualizer/data/meshes_obj/12384.obj b/mindVisualizer/data/meshes_obj/12384.obj new file mode 100644 index 0000000000000000000000000000000000000000..5d0937823d255faaa045852bb7cd8f07470d8339 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12384.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3be186a5a5a178e43769234622bacb5ababf1997c91a90071adb9f44f30a9c78 +size 969896 diff --git a/mindVisualizer/data/meshes_obj/12390.obj b/mindVisualizer/data/meshes_obj/12390.obj new file mode 100644 index 0000000000000000000000000000000000000000..a2cc484a5edab6d04563e3695f12068524142074 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12390.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5fc44b9bcf7f3b04f7ef791d5b0a52e29a18a9fbdac8028be894fe476f24705c +size 2151603 diff --git a/mindVisualizer/data/meshes_obj/12405.obj b/mindVisualizer/data/meshes_obj/12405.obj new file mode 100644 index 0000000000000000000000000000000000000000..f04659e700885fef647047e72d56f4d88444ef87 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12405.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0439df898b306e13edfb8d3181bff127c48b66ab7f66d334efcf05d8f71d11e5 +size 223190 diff --git a/mindVisualizer/data/meshes_obj/12416.obj b/mindVisualizer/data/meshes_obj/12416.obj new file mode 100644 index 0000000000000000000000000000000000000000..3326d56042f02d37189bfb632cc151fa7d232f7c --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12416.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9a2df4a7fdcec17ffc724eb290ab441619b51ccb4815cd0b6b875de56269184 +size 209147 diff --git a/mindVisualizer/data/meshes_obj/12535.obj b/mindVisualizer/data/meshes_obj/12535.obj new file mode 100644 index 0000000000000000000000000000000000000000..e2e37231bb3914d954a23515e8091de00d631678 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12535.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fccc7bfa5be2555bb9959348b4c87f9c30b953484316ba6096a1fc9dc46bd1ed +size 66467 diff --git a/mindVisualizer/data/meshes_obj/12538.obj b/mindVisualizer/data/meshes_obj/12538.obj new file mode 100644 index 0000000000000000000000000000000000000000..3152610aa9e983a1a236aa35326ba2b9f12b5626 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12538.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4583e744e59bc7fbb8fd0f9403bcef7a5185c37c521a0c428bfc60fc668868da +size 154387 diff --git a/mindVisualizer/data/meshes_obj/12805.obj b/mindVisualizer/data/meshes_obj/12805.obj new file mode 100644 index 0000000000000000000000000000000000000000..4fa13217776a3842ae63b4700c44ebba39dc5600 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12805.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b18206a8da73a374e889c33fd2924311a690386a6c303667f0f5b0946cb0c455 +size 143212 diff --git a/mindVisualizer/data/meshes_obj/12826.obj b/mindVisualizer/data/meshes_obj/12826.obj new file mode 100644 index 0000000000000000000000000000000000000000..9a2afb5cba8192a4de2aea58fc51bd30620a4c89 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/12826.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45cf366930a0d5bcd011786a8792020be22a0a608d6c1cf6608b4fd6bc616ed2 +size 4412 diff --git a/mindVisualizer/data/meshes_obj/13229.obj b/mindVisualizer/data/meshes_obj/13229.obj new file mode 100644 index 0000000000000000000000000000000000000000..06c50cf81e410d0e77ea417679c5c87b542f7e82 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/13229.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49561248fc2231853a3a9f529198f10c6de9d70b2b5347b68632433ddf4addfd +size 42714 diff --git a/mindVisualizer/data/meshes_obj/13230.obj b/mindVisualizer/data/meshes_obj/13230.obj new file mode 100644 index 0000000000000000000000000000000000000000..509dc67b326701324acbf205da4023da56ed977a --- /dev/null +++ b/mindVisualizer/data/meshes_obj/13230.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bf25eefac2d8839c33def1696db0ee7ad0565accb520a4c540b77359d801011 +size 444179 diff --git a/mindVisualizer/data/meshes_obj/146034872.obj b/mindVisualizer/data/meshes_obj/146034872.obj new file mode 100644 index 0000000000000000000000000000000000000000..177a12fa324cb8bd0015398aa9dea4b5c31dcdf4 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/146034872.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2884c4d4bd0001746593954ea6b1fdb7e6b6264ca4b8a84627d43dcdf53cb18 +size 952036 diff --git a/mindVisualizer/data/meshes_obj/146034876.obj b/mindVisualizer/data/meshes_obj/146034876.obj new file mode 100644 index 0000000000000000000000000000000000000000..c79040b24f1e2300575c6ba78110df772e028ce4 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/146034876.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53df810f146949b85903e6c41b7f2a3cca396b38dfb113aa9a567f2584b80735 +size 190086 diff --git a/mindVisualizer/data/meshes_obj/146034884.obj b/mindVisualizer/data/meshes_obj/146034884.obj new file mode 100644 index 0000000000000000000000000000000000000000..7298d475cc0fa6587591979316860ad728ebb6d1 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/146034884.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71e25e58eba03756ddbb026fbe33a2edeca2c230d3b05d1ca38c8361b0a0e7f0 +size 322688 diff --git a/mindVisualizer/data/meshes_obj/146034888.obj b/mindVisualizer/data/meshes_obj/146034888.obj new file mode 100644 index 0000000000000000000000000000000000000000..fd42d1489823d7387f6a392d4e6f63c779e56ed1 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/146034888.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36bc27c67b693d87c8d61c0df0c26e0daea0c7095f18637851f0fdacdaed9701 +size 467586 diff --git a/mindVisualizer/data/meshes_obj/266441657.obj b/mindVisualizer/data/meshes_obj/266441657.obj new file mode 100644 index 0000000000000000000000000000000000000000..6725e019f4b3b87ddf343498409024e16da64093 --- /dev/null +++ b/mindVisualizer/data/meshes_obj/266441657.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:432f068d3dbb369f0f460ed10f9bcc3b94b0a78e38e51da0cca6d4ab91acecde +size 184342 diff --git a/mindVisualizer/data/rag_knowledge/README.md b/mindVisualizer/data/rag_knowledge/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9ebad4d3b0289246dee0a2558a1bbea11f4910b3 --- /dev/null +++ b/mindVisualizer/data/rag_knowledge/README.md @@ -0,0 +1,46 @@ +# RAG Knowledge Base + +Drop your own neuroscience knowledge files here to improve LLM interpretations. + +## Supported formats + +### JSON files (`*.json`) + +Each JSON file should be a flat object mapping region names to descriptions: + +```json +{ + "Amygdala (AMY)": "The amygdala processes threat detection and fear conditioning. It receives rapid subcortical visual input via the superior colliculus and pulvinar for fast threat assessment...", + "Hippocampal Formation (HPF)": "The hippocampus is essential for episodic memory encoding and spatial navigation. Place cells in CA1 fire at specific locations..." +} +``` + +Region names do not need to match the Allen atlas names exactly -- the system uses semantic similarity (embedding search) to find the most relevant knowledge for any query. + +### Text files (`*.txt`) + +Use `## Region Name` headers to separate entries: + +``` +## Amygdala (AMY) +The amygdala processes threat detection and fear conditioning... + +## Hippocampal Formation (HPF) +The hippocampus is essential for episodic memory encoding... +``` + +## Partial coverage is fine + +You do not need to cover every brain region. The system merges your files with a built-in fallback knowledge base (21 regions). For any region not covered by your files or the fallback, the LLM uses its own training knowledge. + +Priority order: +1. Your files in this directory (highest priority) +2. Built-in fallback knowledge base (`REGION_KNOWLEDGE` in `src/region_analyzer.py`) +3. LLM's own neuroscience training knowledge + +## Tips + +- More detail is better -- include connectivity, cell types, clinical relevance +- Multiple JSON/TXT files are merged automatically +- If two files define the same region, the last one loaded (alphabetical order) wins +- Changes take effect on the next app startup (no need to rebuild anything) diff --git a/mindVisualizer/data/rag_knowledge/builtin_regions.json b/mindVisualizer/data/rag_knowledge/builtin_regions.json new file mode 100644 index 0000000000000000000000000000000000000000..d517d72191375e67023ca5be9336bb6df8b9a205 --- /dev/null +++ b/mindVisualizer/data/rag_knowledge/builtin_regions.json @@ -0,0 +1,26 @@ +{ + "Retrosplenial Area (RSP)": "The retrosplenial cortex is involved in spatial memory, navigation, and contextual memory. It integrates information from the hippocampus and visual cortex to form spatial representations.", + "Anterior Cingulate Area (ACA)": "The anterior cingulate cortex plays roles in attention, error monitoring, conflict detection, emotional regulation, and decision-making. It connects limbic and prefrontal regions.", + "Prelimbic Area (PL)": "The prelimbic cortex is part of the medial prefrontal cortex, involved in working memory, goal-directed behavior, fear expression, and emotional regulation.", + "Infralimbic Area (ILA)": "The infralimbic cortex is involved in fear extinction, autonomic regulation, and emotional processing. It has reciprocal connections with the amygdala.", + "Orbital Area (ORB)": "The orbitofrontal cortex is critical for reward-based decision making, value encoding, and behavioral flexibility. It integrates sensory and emotional information.", + "Primary Somatosensory Area (SSp)": "The primary somatosensory cortex processes tactile information from the body including touch, pressure, temperature, and pain.", + "Supplemental Somatosensory Area (SSs)": "The supplemental somatosensory area processes higher-order tactile information and is involved in texture discrimination and object recognition by touch.", + "Primary Motor Area (MOp)": "The primary motor cortex directly controls voluntary movements by sending signals to muscles via the corticospinal tract.", + "Secondary Motor Area (MOs)": "The secondary motor area (premotor cortex) is involved in movement planning, preparation, and the selection of appropriate motor programs.", + "Primary Visual Area (VISp)": "The primary visual cortex (V1) processes basic visual features including orientation, spatial frequency, and color.", + "Anterolateral Visual Area (VISal)": "The anterolateral visual area processes complex visual features and is part of the ventral visual stream involved in object recognition.", + "Anteromedial Visual Area (VISam)": "The anteromedial visual area is part of the dorsal visual stream, involved in spatial processing and visuospatial attention.", + "Posterior Parietal Association Area (PTLp)": "The posterior parietal cortex integrates sensory information for spatial awareness, attention, and sensorimotor transformations for reaching and grasping.", + "Primary Auditory Area (AUDp)": "The primary auditory cortex processes basic acoustic features including frequency, intensity, and temporal patterns of sound.", + "Dorsal Auditory Area (AUDd)": "The dorsal auditory area is part of the auditory 'where' pathway, processing spatial aspects of sound localization.", + "Ventral Auditory Area (AUDv)": "The ventral auditory area is part of the auditory 'what' pathway, processing sound identity and complex spectral features.", + "Temporal Association Area (TEa)": "The temporal association area integrates auditory and visual information for multisensory processing and is involved in social cognition.", + "Perirhinal Area (PERI)": "The perirhinal cortex is critical for object recognition memory and serves as a gateway between neocortical areas and the hippocampus.", + "Ectorhinal Area (ECT)": "The ectorhinal cortex is involved in the processing of contextual information and forms part of the parahippocampal region.", + "Hippocampal Formation (HPF)": "The hippocampal formation is essential for episodic memory formation, spatial navigation, and memory consolidation. It includes the hippocampus proper, dentate gyrus, and subiculum.", + "Entorhinal Area (ENT)": "The entorhinal cortex is the main interface between the hippocampus and neocortex. It contains grid cells for spatial navigation and is critical for memory encoding.", + "Thalamus (TH)": "The thalamus is a major relay station for sensory and motor information. It routes signals between subcortical areas and the cortex and plays roles in consciousness, sleep, and alertness.", + "Hypothalamus (HY)": "The hypothalamus regulates homeostatic functions including body temperature, hunger, thirst, circadian rhythms, and the autonomic nervous system. It controls the pituitary gland.", + "Cerebellum (CB)": "The cerebellum coordinates voluntary movements, balance, and motor learning. It also contributes to cognitive functions including attention and language." +} diff --git a/mindVisualizer/data/rag_knowledge/example_regions.json b/mindVisualizer/data/rag_knowledge/example_regions.json new file mode 100644 index 0000000000000000000000000000000000000000..303dffd13d7f0c997964fcb6ae657632d3ca9984 --- /dev/null +++ b/mindVisualizer/data/rag_knowledge/example_regions.json @@ -0,0 +1,5 @@ +{ + "Amygdala (AMY)": "The amygdala is a bilateral almond-shaped structure in the medial temporal lobe critical for emotional processing, particularly fear conditioning and threat detection. It receives rapid subcortical visual input via the superior colliculus-pulvinar pathway for fast threat assessment, bypassing cortical processing. The basolateral complex (BLA) integrates sensory input with emotional valence, while the central nucleus (CeA) coordinates autonomic and behavioral fear responses. In resting state, the amygdala shows intrinsic connectivity with the prefrontal cortex (emotion regulation), insula (interoception), and hippocampus (emotional memory). Disrupted amygdala connectivity is implicated in anxiety disorders, PTSD, and depression.", + "Prefrontal Cortex (PFC)": "The prefrontal cortex encompasses the anterior portion of the frontal lobe and is the neural substrate for executive functions including working memory, planning, decision-making, cognitive flexibility, and impulse control. The dorsolateral PFC (dlPFC) supports working memory and cognitive control. The ventromedial PFC (vmPFC) integrates emotional valence with decision-making and is critical for value-based choices. The orbitofrontal cortex (OFC) encodes reward prediction and behavioral flexibility. In resting state, the medial PFC is a core hub of the default mode network (DMN), active during self-referential thought, future planning, and social cognition.", + "Insular Cortex (IC)": "The insula is a cortical region folded deep within the lateral sulcus, serving as a hub for interoceptive awareness -- the sense of the body's internal state. The posterior insula processes primary interoceptive signals (heartbeat, respiration, gut feelings), while the anterior insula integrates these with emotional and cognitive context to generate subjective feelings. The anterior insula is a key node in the salience network, detecting behaviorally relevant stimuli and switching between the default mode and central executive networks. It plays roles in empathy, risk assessment, addiction, and pain perception." +} diff --git a/mindVisualizer/data/roi_flow/mdn_training_all_points.npy b/mindVisualizer/data/roi_flow/mdn_training_all_points.npy deleted file mode 100644 index c87e14dab7ddfad03e7d96434a80f0eccab3b1a0..0000000000000000000000000000000000000000 --- a/mindVisualizer/data/roi_flow/mdn_training_all_points.npy +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bf047435bda457ab7eb4d59940ae8463555bb92cffadaed9d13aad234123887f -size 223208 diff --git a/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_entropy.bin b/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_entropy.bin deleted file mode 100644 index 55bb17ee16b9c292175fa4eaa3a49e857e8cebbd..0000000000000000000000000000000000000000 --- a/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_entropy.bin +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5ef8fb7dace16f77b06b262e09dbbbd0070c21d5ac71e49b280a4af5e34fee2e -size 1048576 diff --git a/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_mean_xyz3.bin b/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_mean_xyz3.bin deleted file mode 100644 index 4e79b4eb378191b3f6b811fabb3c517c182ee6ae..0000000000000000000000000000000000000000 --- a/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_mean_xyz3.bin +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4157011b84e35e0a861730baa16de6a97b40ca42b5a0001356760d02a4cddaef -size 3145728 diff --git a/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_mu1_xyz3.bin b/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_mu1_xyz3.bin deleted file mode 100644 index 5c1b259fef6a9332f5fc7cae3287bab5aaa452d4..0000000000000000000000000000000000000000 --- a/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_mu1_xyz3.bin +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0e64c8f46eaa1c0d3fa269440687f9ff24b561567feaa7fdbb7f4d1aa259976f -size 3145728 diff --git a/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_mu2_xyz3.bin b/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_mu2_xyz3.bin deleted file mode 100644 index 11f2a1274f4bd8dbd8378520984dcb901cece6a5..0000000000000000000000000000000000000000 --- a/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_mu2_xyz3.bin +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d418aa24f897fdd78cee36f35e499d680db00fe987a72cd935ad4e09b2ea6b95 -size 3145728 diff --git a/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_pi.bin b/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_pi.bin deleted file mode 100644 index 5168f8b50b89b1c0d4ff165b56728db3c33416eb..0000000000000000000000000000000000000000 --- a/mindVisualizer/data/roi_flow/mdn_universal_all_grid64_pi.bin +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1c8cf547e39333a47b43030ac903fd0eec449a12e3dd7be131dafb7caff9d8ea -size 1048576 diff --git a/mindVisualizer/data/roi_flow/mdn_universal_raw_grid64_meta.json b/mindVisualizer/data/roi_flow/mdn_universal_raw_grid64_meta.json deleted file mode 100644 index de7787a5460dfeb13a0176273722ec1362acc95e..0000000000000000000000000000000000000000 --- a/mindVisualizer/data/roi_flow/mdn_universal_raw_grid64_meta.json +++ /dev/null @@ -1 +0,0 @@ -{"grid":64,"axis_min":[-0.5743489265441895,-0.18553070724010468,-0.38263601064682007],"axis_max":[0.3224758803844452,0.597403883934021,0.37152180075645447],"files":{"mean_xyz3":"mdn_universal_all_grid64_mean_xyz3.bin","mu1_xyz3":"mdn_universal_all_grid64_mu1_xyz3.bin","mu2_xyz3":"mdn_universal_all_grid64_mu2_xyz3.bin","pi":"mdn_universal_all_grid64_pi.bin","entropy":"mdn_universal_all_grid64_entropy.bin"},"training_points_npy":"mdn_training_all_points.npy","dtype":"float32","layout":"C-order (x, y, z, 3) for *_xyz3; (x, y, z) for scalars","endianness":"little","mixture_mean_field":true,"model":{"hidden":200,"iters":35045,"keep_q":1.0,"min_frac":1.0},"mdn_source":"universal_all","bounds_mode":"full","bounds_pad":0.0} \ No newline at end of file diff --git a/mindVisualizer/data/roi_flow/probe_embed.npy b/mindVisualizer/data/roi_flow/probe_embed.npy deleted file mode 100644 index e98d0879b985fdd8373ac1c7b0a5cef0deba096d..0000000000000000000000000000000000000000 --- a/mindVisualizer/data/roi_flow/probe_embed.npy +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:baecae7d26b1d76bace68d96f005b0dfc0e737dd83383345e819b669259b5af1 -size 113384 diff --git a/mindVisualizer/data/roi_flow/probe_roi.npy b/mindVisualizer/data/roi_flow/probe_roi.npy deleted file mode 100644 index bde77ac686603c9ffe773ee12e4005958eb1a4ef..0000000000000000000000000000000000000000 --- a/mindVisualizer/data/roi_flow/probe_roi.npy +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:beef9cdff1f1453f9430fc9e2943b6a946203b57fb9a24044a2081bb76a377f1 -size 37752128 diff --git a/mindVisualizer/data/roi_flow/probe_roi_centers.npy b/mindVisualizer/data/roi_flow/probe_roi_centers.npy deleted file mode 100644 index 7f85de2c1101f332adc491830910e61c35ffc993..0000000000000000000000000000000000000000 --- a/mindVisualizer/data/roi_flow/probe_roi_centers.npy +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a64ab89787f423cb3d4ac4e953a5fe056c4be9a7b3fe4738be529d14fac7b6b -size 12128 diff --git a/mindVisualizer/data/sch400_rDCM_A.npy b/mindVisualizer/data/sch400_rDCM_A.npy new file mode 100644 index 0000000000000000000000000000000000000000..5673fbaec784f8cdcc8554c6499c1bda499f08c6 --- /dev/null +++ b/mindVisualizer/data/sch400_rDCM_A.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:899e60603066760437d7222aef25364dcdb7a1b6bc6dbe9ad0933d4ca0f4da31 +size 640128 diff --git a/mindVisualizer/data/schaefer400_centroids_MNI.npy b/mindVisualizer/data/schaefer400_centroids_MNI.npy new file mode 100644 index 0000000000000000000000000000000000000000..29ddb5fee3e274e48d164633f63e145e66d1e9a2 --- /dev/null +++ b/mindVisualizer/data/schaefer400_centroids_MNI.npy @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f00c8e62c5530bba3f876589be03600624a20a6b91c6e2be152141504feba7e +size 9728 diff --git a/mindVisualizer/src/__main__.py b/mindVisualizer/src/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..5d6a8109e6ce6ef4587c5fb02bb9d621aa3fa09b --- /dev/null +++ b/mindVisualizer/src/__main__.py @@ -0,0 +1,3 @@ +from .main import main + +main() diff --git a/mindVisualizer/src/brain_state.py b/mindVisualizer/src/brain_state.py new file mode 100644 index 0000000000000000000000000000000000000000..5dda8899c9e87e3c69bc1a7f94996abc5bb8a63d --- /dev/null +++ b/mindVisualizer/src/brain_state.py @@ -0,0 +1,896 @@ +"""Brain region state database. + +Stores the current cognitive/functional state of each brain region. States can be: + - Auto-initialized from a global brain state description via GPT + - Manually edited per-region + - Cleared and re-initialized at any time + - Persisted to disk as JSON + +Used by the probe system to simulate information propagation: when a probe +enters a region, the state can be modified and propagated to downstream regions. +""" + +import json +import os +import sys +from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout +from pathlib import Path +from typing import Optional + + +def _ensure_ssl(): + """Fix SSL cert path for environments where Git overrides it. + + Must run BEFORE importing openai/langchain so the httpx client + picks up the correct certificate. + """ + try: + import certifi + cert_file = certifi.where() + cur = os.environ.get("SSL_CERT_FILE", "") + if not cur or not os.path.isfile(cur): + os.environ["SSL_CERT_FILE"] = cert_file + cur2 = os.environ.get("REQUESTS_CA_BUNDLE", "") + if not cur2 or not os.path.isfile(cur2): + os.environ["REQUESTS_CA_BUNDLE"] = cert_file + except ImportError: + pass + + +# Fix SSL before any network library is imported +_ensure_ssl() + +import openai +from dotenv import load_dotenv +from langchain_openai import ChatOpenAI +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.output_parsers import StrOutputParser + + +DEFAULT_STATE_FILE = Path(__file__).resolve().parent.parent / "data" / "brain_states.json" + + +DEFAULT_MODEL = "gpt-5.4-mini" +HQ_MODEL = "gpt-5.4" + + +class BrainStateDB: + """Database of brain region states, persisted to JSON.""" + + def __init__(self, path: Path | str = DEFAULT_STATE_FILE, model: str = DEFAULT_MODEL, + debug: bool = False): + self.path = Path(path) + self.model = model + self.debug = debug + self.states: dict[str, str] = {} # region_name -> state description + self.global_state: str = "" # the overall brain state description + if self.path.exists(): + self.load() + + def _debug_prompt(self, method_name: str, prompt_text: str): + """Print the full prompt if debug mode is enabled.""" + if self.debug: + print(f"\n{'='*60}") + print(f"[DEBUG PROMPT] {method_name}") + print(f"{'='*60}") + print(prompt_text) + print(f"{'='*60}\n") + sys.stdout.flush() + + # ---------- persistence ---------- + + def load(self): + """Load states from JSON file.""" + try: + data = json.loads(self.path.read_text(encoding="utf-8")) + self.states = data.get("states", {}) + self.global_state = data.get("global_state", "") + print(f"[brain-state] loaded {len(self.states)} region states") + except Exception as e: + print(f"[brain-state] could not load {self.path}: {e}") + + def save(self): + """Save states to JSON file.""" + data = { + "global_state": self.global_state, + "states": self.states, + } + self.path.parent.mkdir(parents=True, exist_ok=True) + self.path.write_text(json.dumps(data, indent=2, ensure_ascii=False), + encoding="utf-8") + print(f"[brain-state] saved {len(self.states)} region states to {self.path}") + + def clear(self): + """Clear all states.""" + self.states.clear() + self.global_state = "" + if self.path.exists(): + self.path.unlink() + print("[brain-state] cleared all states") + + # ---------- access ---------- + + def get(self, region_name: str) -> str: + return self.states.get(region_name, "") + + def set(self, region_name: str, state: str): + self.states[region_name] = state + + def has_states(self) -> bool: + return bool(self.states) + + # ---------- GPT initialization ---------- + + def initialize_from_global(self, global_state: str, region_names: list[str], + callback=None) -> str: + """Use GPT to generate states for all regions from a global brain state. + + Args: + global_state: e.g. "someone thinking about loved ones" or "" for GPT to choose + region_names: list of region names to generate states for + callback: optional fn(status_msg) for progress updates + + Returns: + Summary of initialization. + """ + _ensure_ssl() + + load_dotenv() + + + if not global_state: + global_state = "a resting state with spontaneous mind-wandering" + + self.global_state = global_state + + # Build region list (batch into groups of ~30 for efficiency) + all_states = {} + batch_size = 30 + + for i in range(0, len(region_names), batch_size): + batch = region_names[i:i + batch_size] + if callback: + callback(f"Generating states for regions {i+1}-{i+len(batch)}/{len(region_names)}...") + + region_list = "\n".join(f"- {name}" for name in batch) + + prompt = ChatPromptTemplate.from_template( + """You are a neuroscience expert. The brain is currently in this overall state: +"{global_state}" + +For each brain region below, write a SHORT (1-2 sentences) specific description of +what this region is likely doing right now given the overall brain state. + +IMPORTANT RULES: +- Be SPECIFIC about the actual cognitive content, not generic function descriptions +- Don't force every region to match the global state — some regions may be doing + their own thing (e.g., sensory processing, homeostasis) independent of the global state +- First consider what the region generally does, then derive what it's specifically + doing in this context +- Focus on CURRENT ACTIVITY, not general capabilities + +Regions: +{regions} + +Reply as JSON object mapping region name to state description. Example: +{{"Amygdala (AMY)": "Low-level monitoring for threats; no active fear processing", ...}} +Only output valid JSON, nothing else.""" + ) + + llm = ChatOpenAI(model=self.model, temperature=0.4, max_tokens=4000) + chain = prompt | llm | StrOutputParser() + + self._debug_prompt("initialize_from_global", + prompt.format(global_state=global_state, regions=region_list)) + + try: + result = chain.invoke({ + "global_state": global_state, + "regions": region_list, + }) + # Parse JSON from response + result = result.strip() + if result.startswith("```"): + result = result.split("\n", 1)[1].rsplit("```", 1)[0] + parsed = json.loads(result) + all_states.update(parsed) + # Show each region's state in real time + for rname, rstate in parsed.items(): + if callback: + callback(f" {rname}: {rstate}") + except Exception as e: + print(f"[brain-state] GPT batch error: {e}") + for name in batch: + all_states[name] = f"(initialization failed: {e})" + + self.states = all_states + self.save() + return f"Initialized {len(self.states)} region states for: {global_state}" + + def validate_perturbation(self, region_name: str, perturbation: str) -> dict: + """Check if a perturbation is appropriate for a brain region. + + Returns: + dict with keys: + - valid (bool): True if perturbation is plausible + - region_function (str): what this region does + - suggestion (str): suggested alternative if not valid + - warning (str): warning message if perturbation is questionable + """ + _ensure_ssl() + + load_dotenv() + + + current = self.states.get(region_name, "unknown state") + + prompt = ChatPromptTemplate.from_template( + """You are a neuroscience expert. Evaluate whether this perturbation makes sense +for the specified brain region. + +Region: {region} +Current state: {current} +Requested perturbation: "{perturbation}" + +Consider: +1. What does this brain region actually do? (its primary functions) +2. Is the requested perturbation something this region CAN process? +3. If not, what WOULD be an appropriate perturbation for this region? + +Reply as JSON: +{{ + "valid": true/false, + "region_function": "brief description of what this region does", + "warning": "warning if questionable (empty string if fine)", + "suggestion": "suggested alternative perturbation if invalid (empty string if valid)" +}} +Only output valid JSON.""" + ) + + llm = ChatOpenAI(model=self.model, temperature=0.2, max_tokens=300) + chain = prompt | llm | StrOutputParser() + + self._debug_prompt("validate_perturbation", + prompt.format(region=region_name, current=current, + perturbation=perturbation)) + + try: + result = chain.invoke({ + "region": region_name, + "current": current, + "perturbation": perturbation, + }).strip() + if result.startswith("```"): + result = result.split("\n", 1)[1].rsplit("```", 1)[0] + return json.loads(result) + except Exception as e: + return {"valid": True, "region_function": "unknown", + "warning": f"(validation failed: {e})", "suggestion": ""} + + def propose_perturbations(self, region_name: str) -> list[str]: + """Ask GPT to propose 4 plausible perturbations for a brain region. + + Uses the openai SDK directly (not langchain) for reliability in + background threads. + + Args: + region_name: the region to perturb + + Returns: + List of 4 perturbation description strings. + """ + import sys + _defaults = [ + "Heightened activity in this region", + "Suppressed activity in this region", + "Shift to an alternative processing mode", + "Disrupted connectivity with downstream regions", + ] + + _ensure_ssl() + + load_dotenv() + + current = self.states.get(region_name, "unknown state") + + # Use a shorter name for prompt if the full name is very long + short_name = region_name + if len(short_name) > 80 and "(" in short_name: + short_name = short_name.split("(", 1)[1].rstrip(")") + + api_key = os.environ.get("OPENAI_API_KEY", "") + if not api_key: + print("[brain-state] OPENAI_API_KEY not set, using defaults") + sys.stdout.flush() + return _defaults + + print(f"[brain-state] propose_perturbations: calling OpenAI for '{short_name}'...") + sys.stdout.flush() + + try: + client = openai.OpenAI(api_key=api_key, timeout=30.0) + + msg_content = ( + f"You are a neuroscience expert. A user wants to perturb a brain region " + f"in a resting-state simulation.\n\n" + f"Region: {short_name}\n" + f"Current state: \"{current}\"\n\n" + f"Based on the region's known functions and its current state, propose " + f"exactly 4 different plausible ways this region's state could change. " + f"Each should be specific to this region's actual function, a realistic " + f"state change, described in 1 short sentence, and diverse from each other.\n\n" + f"Reply as a JSON array of exactly 4 strings:\n" + f'["perturbation 1", "perturbation 2", "perturbation 3", "perturbation 4"]\n' + f"Only output valid JSON." + ) + + self._debug_prompt("propose_perturbations", msg_content) + + print(f"[brain-state] propose_perturbations: sending API request...") + sys.stdout.flush() + + def _call(): + return client.chat.completions.create( + model=self.model, + temperature=0.5, + max_completion_tokens=400, + messages=[{"role": "user", "content": msg_content}], + ) + with ThreadPoolExecutor(max_workers=1) as pool: + future = pool.submit(_call) + try: + response = future.result(timeout=30) + except FutureTimeout: + print("[brain-state] propose_perturbations: TIMEOUT after 30s") + sys.stdout.flush() + return _defaults + + result = response.choices[0].message.content.strip() + print(f"[brain-state] propose_perturbations: got response, parsing...") + sys.stdout.flush() + + if result.startswith("```"): + result = result.split("\n", 1)[1].rsplit("```", 1)[0] + proposals = json.loads(result) + if isinstance(proposals, list) and len(proposals) >= 4: + return proposals[:4] + return proposals if isinstance(proposals, list) else _defaults + except Exception as e: + print(f"[brain-state] proposal error: {type(e).__name__}: {e}") + sys.stdout.flush() + import traceback + traceback.print_exc() + sys.stdout.flush() + return _defaults + + def alter_region_state(self, region_name: str, modification: str, + callback=None, skip_validation=False) -> str: + """Use GPT to alter a specific region's state. + + Args: + region_name: the region to modify + modification: description of how to change it + callback: optional fn(status_msg) for progress + skip_validation: skip perturbation validation + + Returns: + The new state description. + """ + _ensure_ssl() + + load_dotenv() + + + # Validate perturbation first + if not skip_validation: + if callback: + callback(f"Validating perturbation for {region_name}...") + validation = self.validate_perturbation(region_name, modification) + + if callback: + callback(f"Region function: {validation.get('region_function', '?')}") + + if not validation.get("valid", True): + warning = validation.get("warning", "") + suggestion = validation.get("suggestion", "") + msg = f"[WARNING] Perturbation may not be appropriate for {region_name}." + if warning: + msg += f"\n Reason: {warning}" + if suggestion: + msg += f"\n Suggestion: {suggestion}" + if callback: + callback(msg) + print(msg) + # Still proceed but note the warning + elif validation.get("warning"): + if callback: + callback(f"Note: {validation['warning']}") + + current = self.states.get(region_name, "unknown state") + + prompt = ChatPromptTemplate.from_template( + """You are a neuroscience expert. A brain region's state needs to be modified. + +Region: {region} +Current state: {current} +Modification requested: {modification} + +Write a new SHORT (1-2 sentences) specific state description that incorporates +the requested modification while staying neuroscientifically plausible. +Only output the new state description, nothing else.""" + ) + + llm = ChatOpenAI(model=self.model, temperature=0.3, max_tokens=200) + chain = prompt | llm | StrOutputParser() + + self._debug_prompt("alter_region_state", + prompt.format(region=region_name, current=current, + modification=modification)) + + try: + new_state = chain.invoke({ + "region": region_name, + "current": current, + "modification": modification, + }).strip() + self.states[region_name] = new_state + self.save() + return new_state + except Exception as e: + return f"(alter failed: {e})" + + def propagate_through_graph(self, source_region: str, + connections: list[dict], + A, # numpy ndarray (R x R) + roi_names: list[str], + callback=None) -> dict[str, str]: + """Propagate perturbation through the connectivity graph, depth by depth. + + For each affected region individually: + 1. Determine if the incoming signal is strong or weak (relative to that + region's other connections) + 2. Find additional un-perturbed regions strongly connected to this target + for context + 3. Ask GPT: given region X's function, its previous state, the incoming + signal from Y (strong/weak), and contextual connections — what does + X's state change to? + + Processes depth-1 targets first, then depth-2 using already-updated states, + so the signal cascades realistically. + + Args: + source_region: name of the perturbed region + connections: list of dicts from get_strongest_connections, each with + target_idx, weight, abs_weight, depth, source_idx + A: full connectivity matrix (R x R numpy array) + roi_names: list of ROI names matching A's indices + callback: optional fn(status_msg) + + Returns: + Dict of {region_name: new_state} for all updated regions. + """ + import numpy as np + _ensure_ssl() + + load_dotenv() + + + llm = ChatOpenAI(model=self.model, temperature=0.3, max_tokens=300) + + # Group connections by depth + by_depth: dict[int, list[dict]] = {} + for c in connections: + by_depth.setdefault(c["depth"], []).append(c) + + # Build name->index map + name_to_idx = {n: i for i, n in enumerate(roi_names)} + + # Track before/after states and which regions were updated + before_states: dict[str, str] = {} + all_updates: dict[str, str] = {} + perturbed_idxs = set() + source_idx = name_to_idx.get(source_region) + if source_idx is not None: + perturbed_idxs.add(source_idx) + + # Diagonal-zeroed matrix for connection lookups + Ac = np.array(A, dtype=np.float32, copy=True) + np.fill_diagonal(Ac, 0.0) + + # Process depth by depth + max_depth = max(by_depth.keys()) if by_depth else 0 + for d in range(1, max_depth + 1): + depth_conns = by_depth.get(d, []) + if not depth_conns: + continue + + if callback: + callback(f"Propagating depth {d}: {len(depth_conns)} regions...") + + for c in depth_conns: + ti = c["target_idx"] + target_name = roi_names[ti] + from_idx = c["source_idx"] + from_name = roi_names[from_idx] + + # Skip if target has no state + target_prev_state = self.states.get(target_name, "") + if not target_prev_state: + target_prev_state = "unknown / not initialized" + before_states[target_name] = target_prev_state + + # Get the incoming source's current state (may have been + # updated in a previous depth iteration) + from_state = all_updates.get(from_name, + self.states.get(from_name, "unknown")) + + # ---- Determine signal strength and type ---- + # Compare this connection weight to the target's other incoming + # connections (row ti of A) + incoming_weights = np.abs(Ac[ti, :]) + incoming_weights[ti] = 0 # no self + median_incoming = float(np.median(incoming_weights[incoming_weights > 0])) \ + if np.any(incoming_weights > 0) else 0.001 + conn_abs_w = c["abs_weight"] + if conn_abs_w > median_incoming * 2.0: + strength_label = "STRONG (well above average)" + elif conn_abs_w > median_incoming * 0.8: + strength_label = "moderate" + else: + strength_label = "weak (below average)" + + # Determine excitatory vs inhibitory based on connection sign + raw_weight = c.get("weight", conn_abs_w) + if raw_weight < 0: + sign_label = "INHIBITORY (negative connection weight)" + else: + sign_label = "EXCITATORY (positive connection weight)" + + # ---- Find context: other strong un-perturbed connections ---- + context_lines = [] + top_others = np.argsort(incoming_weights)[::-1][:10] + for oi in top_others: + if int(oi) == from_idx or int(oi) in perturbed_idxs: + continue + oi = int(oi) + other_name = roi_names[oi] + other_state = self.states.get(other_name, "") + if not other_state: + continue + other_w = float(incoming_weights[oi]) + if other_w < conn_abs_w * 0.3: + break # only include meaningfully strong ones + context_lines.append( + f"- {other_name} (connection weight {other_w:.4f}): " + f"state = \"{other_state}\"" + ) + if len(context_lines) >= 3: + break + + context_text = "" + if context_lines: + context_text = ( + "\n\nAdditional context — other strong connections to this region " + "(NOT perturbed, their states remain stable):\n" + + "\n".join(context_lines) + ) + + # ---- GPT call for this single region ---- + prompt = ChatPromptTemplate.from_template( + """You are a computational neuroscientist analyzing intrinsic information flow in a resting-state brain network (rs-fMRI effective connectivity). + +TARGET REGION: {target} +TARGET PREVIOUS STATE: {target_state} + +INCOMING SIGNAL FROM: {source} +INCOMING SIGNAL STRENGTH: {strength} (connection weight: {weight:.4f}) +CONNECTION TYPE: {sign} +SOURCE REGION'S CURRENT STATE: "{source_state}" +{context} +CRITICAL INSTRUCTIONS: +1. NO SEMANTIC ECHOING: Do not simply copy the semantic concept of the source region. You must TRANSLATE the incoming signal into the strict anatomical and functional domain of the TARGET REGION. If the source is about "visual beauty", the motor cortex should NOT start "appreciating beauty" — it should show changes in motor readiness or postural tone. +2. RESTING-STATE CONTEXT: The connectivity data reflects intrinsic resting-state dynamics. Information flow here represents spontaneous internal cognition or modulation of the target region's resting equilibrium. +3. INHIBITORY vs EXCITATORY: If the connection is INHIBITORY, the incoming signal SUPPRESSES or DAMPENS the target region's activity. If EXCITATORY, it AMPLIFIES or FACILITATES the target's function. This fundamentally changes the nature of the state change. +4. INTRINSIC DYNAMICS: Focus on how the target region's OWN function shifts, not on relaying the source's content. +5. Output ONLY the precise description of the target region's new state (1-2 sentences). No conversational filler. + +Note: connection weights are directed influence estimates from resting-state fMRI — interpret propagation as the most likely influence pattern, not literal neural signal timing.""" + ) + + chain = prompt | llm | StrOutputParser() + + self._debug_prompt("propagate_through_graph", + prompt.format(target=target_name, + target_state=target_prev_state, + strength=strength_label, + sign=sign_label, + source=from_name, + source_state=from_state, + weight=c["abs_weight"], + context=context_text)) + + try: + new_state = chain.invoke({ + "target": target_name, + "target_state": target_prev_state, + "strength": strength_label, + "sign": sign_label, + "source": from_name, + "source_state": from_state, + "weight": c["abs_weight"], + "context": context_text, + }).strip() + all_updates[target_name] = new_state + perturbed_idxs.add(ti) + + if callback: + callback(f" {target_name}: {new_state}") + + except Exception as e: + print(f"[brain-state] propagation error for {target_name}: {e}") + + # Record source before state too + before_states[source_region] = self.states.get(source_region, "unknown") + + # Apply all updates + for name, new_state in all_updates.items(): + self.states[name] = new_state + self.save() + + # Attach before_states so summarize_changes can use them + self._last_before_states = before_states + + return all_updates + + def propagate_through_regions(self, source_region: str, affected_regions: list[str], + flow_strengths: dict[str, float] | None = None, + callback=None) -> dict[str, str]: + """Simpler propagation for MDN flow mode (no connectivity matrix). + + Uses flow strengths as a proxy for connection weights. Processes regions + in order of the flow path (which is already serial). + """ + _ensure_ssl() + + load_dotenv() + + + llm = ChatOpenAI(model=self.model, temperature=0.3, max_tokens=300) + + source_state = self.states.get(source_region, "unknown") + all_updates = {} + before_states = {source_region: source_state} + + # Process regions in flow order (serial propagation) + prev_name = source_region + prev_state = source_state + + if flow_strengths and affected_regions: + all_strengths = [flow_strengths.get(n, 0.0) for n in affected_regions] + max_s = max(all_strengths) if all_strengths else 1.0 + else: + max_s = 1.0 + + for name in affected_regions: + target_state = self.states.get(name, "unknown") + before_states[name] = target_state + + strength = flow_strengths.get(name, 0.0) if flow_strengths else 0.0 + if max_s > 0: + rel_strength = strength / max_s + if rel_strength > 0.6: + strength_label = "STRONG" + elif rel_strength > 0.3: + strength_label = "moderate" + else: + strength_label = "weak" + else: + strength_label = "moderate" + + prompt = ChatPromptTemplate.from_template( + """You are a computational neuroscientist analyzing intrinsic information flow in a resting-state brain network. + +TARGET REGION: {target} +TARGET PREVIOUS STATE: {target_state} + +INCOMING SIGNAL FROM: {source} +INCOMING SIGNAL STRENGTH: {strength} +SOURCE REGION'S CURRENT STATE: "{source_state}" + +CRITICAL INSTRUCTIONS: +1. NO SEMANTIC ECHOING: Do not simply copy the semantic concept of the source region. You must TRANSLATE the incoming signal into the strict anatomical and functional domain of the TARGET REGION ({target}). For example, if the source is a visual area processing "edge detection" and the target is a motor area, do NOT say the motor area is doing "edge detection" — describe how the motor area's OWN function shifts in response. +2. RESTING-STATE CONTEXT: This is intrinsic resting-state dynamics, not task-driven activity. Describe subtle modulations, not dramatic activations. +3. INTRINSIC DYNAMICS: Focus on how {target}'s OWN function shifts given the incoming signal. The target region does what IT does, influenced by the source — not what the source does. + +Note: signal strength here is a proxy from the flow field magnitude, not a measured biological quantity. Interpret as the most likely direction of influence, not neural propagation timing. + +Output ONLY the precise description of {target}'s new state (1-2 sentences). No labels, no prefixes.""" + ) + + chain = prompt | llm | StrOutputParser() + + self._debug_prompt("propagate_through_regions", + prompt.format(target=name, target_state=target_state, + strength=strength_label, source=prev_name, + source_state=prev_state)) + + try: + new_state = chain.invoke({ + "target": name, + "target_state": target_state, + "strength": strength_label, + "source": prev_name, + "source_state": prev_state, + }).strip() + all_updates[name] = new_state + if callback: + callback(f" {name}: {new_state}") + # Next hop uses this updated state as source + prev_name = name + prev_state = new_state + except Exception as e: + print(f"[brain-state] propagation error for {name}: {e}") + + # Apply + for name, new_state in all_updates.items(): + self.states[name] = new_state + self.save() + + self._last_before_states = before_states + return all_updates + + def summarize_changes(self, updates: dict[str, str], + source_region: str) -> str: + """Summarize what changed: show before/after states (without revealing the + perturbation) and ask GPT to build a coherent picture. + + Args: + updates: dict of {region_name: new_state} + source_region: the region that was perturbed + + Returns: + Human-readable coherent summary. + """ + _ensure_ssl() + + load_dotenv() + + + # Get before states (saved by propagation methods) + before = getattr(self, '_last_before_states', {}) + + # Build separate before and after state maps + all_regions = [source_region] + [n for n in updates if n != source_region] + before_lines = [] + after_lines = [] + for name in all_regions: + prev = before.get(name, "unknown") + current = self.states.get(name, updates.get(name, "unknown")) + before_lines.append(f"- {name}: \"{prev}\"") + after_lines.append(f"- {name}: \"{current}\"") + + llm = ChatOpenAI(model=self.model, temperature=0.3, max_tokens=800) + + prompt = ChatPromptTemplate.from_template( + """You are a network neuroscientist analyzing a macroscopic shift in resting-state brain activity based on effective connectivity changes. + +INITIAL BRAIN STATE MAP: +{comparisons_before} + +POST-PROPAGATION BRAIN STATE MAP: +{comparisons_after} + +CRITICAL INSTRUCTIONS: +1. DO NOT list the regions or compare them one by one. +2. This is a resting-state brain network — interpret changes as shifts in intrinsic functional organization, not external stimulus-response narratives. +3. FOCUS ON INTRINSIC STATES: Synthesize this data into ONE coherent paragraph explaining the overall shift in the subject's internal cognitive, emotional, or physiological baseline. +4. NETWORK LEVEL INTEGRATION: Identify the broad functional domains driving the new equilibrium and describe the holistic network-level transition based purely on the provided state changes. + +Note: the underlying connectivity is directed effective connectivity from resting-state fMRI — read the overall shift as a change in inter-regional influence patterns, not a literal neural event cascade. + +Provide your coherent resting-state network insight below:""" + ) + + chain = prompt | llm | StrOutputParser() + + self._debug_prompt("summarize_changes", + prompt.format(comparisons_before="\n".join(before_lines), + comparisons_after="\n".join(after_lines))) + + try: + return chain.invoke({ + "comparisons_before": "\n".join(before_lines), + "comparisons_after": "\n".join(after_lines), + }).strip() + except Exception as e: + return f"(summary failed: {e})" + + def generate_flow_story(self, updates: dict[str, str], + source_region: str, + connections: list[dict] | None = None) -> str: + """Generate a narrative story of how information flowed through the brain. + + Unlike summarize_changes (which gives a holistic snapshot), this tells + the story of the signal's journey: where it started, how each region + processed and transformed it, and what the downstream effects were. + + Args: + updates: dict of {region_name: new_state} + source_region: the origin region + connections: optional list of connection dicts (with depth, weight info) + + Returns: + A narrative paragraph describing the information flow journey. + """ + _ensure_ssl() + + load_dotenv() + + + before = getattr(self, '_last_before_states', {}) + + # Build ordered flow description + flow_steps = [] + source_before = before.get(source_region, "unknown") + source_after = self.states.get(source_region, + updates.get(source_region, "unknown")) + flow_steps.append( + f"ORIGIN — {source_region}: \"{source_before}\" -> \"{source_after}\"" + ) + + # Order by depth if connections available + if connections: + by_depth: dict[int, list] = {} + for c in connections: + name = c.get("target_name", "") + if not name and "target_idx" in c: + continue + by_depth.setdefault(c.get("depth", 1), []).append(c) + for d in sorted(by_depth.keys()): + for c in by_depth[d]: + name = c.get("target_name", "") + if name in updates: + prev = before.get(name, "unknown") + sign = "excitatory" if c.get("weight", 0) >= 0 else "inhibitory" + flow_steps.append( + f"DEPTH {d} ({sign}) — {name}: \"{prev}\" -> \"{updates[name]}\"" + ) + else: + for name, new_state in updates.items(): + if name == source_region: + continue + prev = before.get(name, "unknown") + flow_steps.append(f"STEP — {name}: \"{prev}\" -> \"{new_state}\"") + + flow_text = "\n".join(flow_steps) + + llm = ChatOpenAI(model=self.model, temperature=0.4, max_tokens=800) + + prompt = ChatPromptTemplate.from_template( + """You are a science writer narrating how a signal traveled through a resting-state brain network. + +SIGNAL FLOW PATH (in order of propagation): +{flow_path} + +Write a SHORT narrative story (2-3 paragraphs) of how the information traveled through the brain: +- Start with where the signal originated and what it carried +- Describe how each region it reached processed and TRANSFORMED the signal according to its own function +- Highlight how the signal's meaning changed as it moved through different functional domains +- End with the overall effect on the brain's resting state + +RULES: +- Do NOT list regions mechanically — weave them into a flowing narrative +- Use concrete, vivid language about what each region actually does +- Show how the signal was transformed at each hop, not just passed along +- Keep it grounded in neuroscience but accessible to a general audience""" + ) + + chain = prompt | llm | StrOutputParser() + + self._debug_prompt("generate_flow_story", + prompt.format(flow_path=flow_text)) + + try: + return chain.invoke({"flow_path": flow_text}).strip() + except Exception as e: + return f"(story generation failed: {e})" diff --git a/mindVisualizer/src/colormaps.py b/mindVisualizer/src/colormaps.py new file mode 100644 index 0000000000000000000000000000000000000000..f7402f611f541e9bb863081af274257ded04e8f1 --- /dev/null +++ b/mindVisualizer/src/colormaps.py @@ -0,0 +1,50 @@ +"""Colormap utilities: Turbo, Rainbow, and bipolar white-center.""" + +import numpy as np + + +def turbo_rgb01(t): + t = np.clip(np.asarray(t, np.float32), 0, 1) + r = (34.61 + t * (1172.33 + t * (-10793.56 + t * (33300.12 + t * (-38345.17 + 14829.80 * t))))) / 255.0 + g = (23.31 + t * (557.33 + t * (1225.33 + t * (-3574.96 + t * 2199.29)))) / 255.0 + b = (27.20 + t * (3211.10 + t * (-15327.97 + t * (34592.87 + t * (-30538.66 + 9347.97 * t))))) / 255.0 + rgb = np.stack([r, g, b], axis=-1) + return (np.clip(rgb, 0, 1) * 255 + 0.5).astype(np.uint8) + + +def rainbow_rgb01(t): + t = np.clip(np.asarray(t, np.float32), 0.0, 1.0) + h = t * 4.0 + i = np.floor(h).astype(np.int32) + f = h - i + q = 1.0 - f + i = np.clip(i, 0, 3) + zeros = np.zeros_like(t) + ones = np.ones_like(t) + r = np.choose(i, [zeros, zeros, f, ones]) + g = np.choose(i, [f, ones, ones, q]) + b = np.choose(i, [ones, q, zeros, zeros]) + start = (t <= 0.0) + end = (t >= 1.0) + if np.any(start): + r[start], g[start], b[start] = 0.0, 0.0, 1.0 + if np.any(end): + r[end], g[end], b[end] = 1.0, 0.0, 0.0 + rgb = np.stack([r, g, b], axis=-1) + return (np.clip(rgb, 0, 1) * 255 + 0.5).astype(np.uint8) + + +def bicolor_white_center(dent_vals, pos=(255, 120, 0), neg=(0, 100, 255)): + d = np.asarray(dent_vals, np.float32) + a = np.abs(d) + scale = float(np.percentile(a, 97)) if a.size else 1.0 + if not np.isfinite(scale) or scale <= 1e-12: + scale = 1.0 + t = np.clip(a / scale, 0.0, 1.0)[:, None] + white = np.array([[255, 255, 255]], np.float32) + pos_c = np.array(pos, np.float32)[None, :] + neg_c = np.array(neg, np.float32)[None, :] + out = np.where(d[:, None] >= 0.0, + (1.0 - t) * white + t * pos_c, + (1.0 - t) * white + t * neg_c) + return out.astype(np.uint8) diff --git a/mindVisualizer/src/extra_parcellation.py b/mindVisualizer/src/extra_parcellation.py new file mode 100644 index 0000000000000000000000000000000000000000..9e61a34cca4bc525a9d3ccffb5142042301d1de2 --- /dev/null +++ b/mindVisualizer/src/extra_parcellation.py @@ -0,0 +1,315 @@ +"""Optional extra brain parcellation support. + +Loads volumetric NIfTI parcellation atlases (e.g., Brainnetome, AAL3, Schaefer) +and provides point-in-region queries + on-demand mesh generation via marching cubes. + +Dependencies: nibabel, scikit-image (optional — install with: + pip install nibabel scikit-image +) + +Usage: + from src.extra_parcellation import ExtraParcellation + + extra = ExtraParcellation( + nifti_path="data/extra_parcellation/BN_Atlas_246_1mm.nii.gz", + label_map_path="data/extra_parcellation/BN_Atlas_labels.json" # optional + ) + extra.load() + + # Query a point in MNI space + result = extra.get_region_at_point(np.array([-30.0, 15.0, 50.0])) + # -> {"label_id": 42, "name": "SFG_R_7_2", "volume_mm3": 1523.0} + + # Get VTK mesh for visualization + polydata = extra.get_region_mesh(42) +""" + +import json +from pathlib import Path +from typing import Optional + +import numpy as np + +try: + import nibabel as nib + HAS_NIBABEL = True +except ImportError: + HAS_NIBABEL = False + +try: + from skimage.measure import marching_cubes + HAS_SKIMAGE = True +except ImportError: + HAS_SKIMAGE = False + +try: + import vtk + from vtkmodules.util.numpy_support import numpy_to_vtk + HAS_VTK = True +except ImportError: + HAS_VTK = False + + +class ExtraParcellation: + """Volumetric NIfTI parcellation atlas for additional region granularity. + + Designed to work alongside the primary Allen atlas meshes. When both + parcellations cover a point, the caller (FlowMeshOverlay) can decide + which to use (typically the smaller/more specific one). + """ + + def __init__(self, nifti_path: Path | str, + label_map_path: Path | str | None = None): + """ + Args: + nifti_path: Path to NIfTI volume (.nii or .nii.gz) with integer labels. + label_map_path: Optional JSON mapping label IDs to human-readable names. + Format: {"1": "Region A", "2": "Region B", ...} + If omitted, labels will be "Region_". + """ + self.nifti_path = Path(nifti_path) + self.label_map_path = Path(label_map_path) if label_map_path else None + + # Set after load() + self._data: Optional[np.ndarray] = None # (X,Y,Z) int label volume + self._affine: Optional[np.ndarray] = None # 4x4 voxel-to-world + self._inv_affine: Optional[np.ndarray] = None # 4x4 world-to-voxel + self._voxel_vol: float = 1.0 # mm^3 per voxel + self._label_names: dict[int, str] = {} # label_id -> name + self._label_volumes: dict[int, float] = {} # label_id -> volume in mm^3 + self._mesh_cache: dict[int, object] = {} # label_id -> vtkPolyData + self._unique_labels: set[int] = set() + + def load(self) -> bool: + """Load the NIfTI volume and label map. + + Returns True on success, False on failure (with printed warning). + """ + if not HAS_NIBABEL: + print("[extra-parcellation] nibabel not installed. " + "Install with: pip install nibabel") + return False + + if not self.nifti_path.exists(): + print(f"[extra-parcellation] File not found: {self.nifti_path}") + return False + + try: + img = nib.load(str(self.nifti_path)) + self._data = np.asarray(img.dataobj, dtype=np.int32) + self._affine = img.affine.copy() + self._inv_affine = np.linalg.inv(self._affine) + + # Compute voxel volume from affine + vox_sizes = np.sqrt(np.sum(self._affine[:3, :3] ** 2, axis=0)) + self._voxel_vol = float(np.prod(vox_sizes)) + + # Find all unique labels (excluding 0 = background) + self._unique_labels = set(np.unique(self._data)) - {0} + + # Pre-compute volumes + for label_id in self._unique_labels: + voxel_count = int(np.sum(self._data == label_id)) + self._label_volumes[label_id] = voxel_count * self._voxel_vol + + # Load label names + self._label_names = {} + if self.label_map_path and self.label_map_path.exists(): + try: + raw = json.loads(self.label_map_path.read_text(encoding="utf-8")) + for k, v in raw.items(): + self._label_names[int(k)] = str(v) + except Exception as e: + print(f"[extra-parcellation] Warning: could not load label map: {e}") + + # Fill in missing names + for label_id in self._unique_labels: + if label_id not in self._label_names: + self._label_names[label_id] = f"Region_{label_id}" + + print(f"[extra-parcellation] Loaded {self.nifti_path.name}: " + f"{len(self._unique_labels)} regions, " + f"voxel size {vox_sizes[0]:.1f}x{vox_sizes[1]:.1f}x{vox_sizes[2]:.1f} mm, " + f"volume shape {self._data.shape}") + return True + + except Exception as e: + print(f"[extra-parcellation] Failed to load {self.nifti_path}: {e}") + return False + + def is_loaded(self) -> bool: + return self._data is not None + + def _world_to_voxel(self, point: np.ndarray) -> np.ndarray: + """Convert world (MNI) coordinates to voxel indices.""" + pt4 = np.array([point[0], point[1], point[2], 1.0]) + vox = self._inv_affine @ pt4 + return vox[:3] + + def get_region_at_point(self, point: np.ndarray) -> Optional[dict]: + """Look up which region a world-space point falls in. + + Args: + point: (3,) array in world/MNI coordinates (mm) + + Returns: + dict with {"label_id": int, "name": str, "volume_mm3": float} + or None if point is outside any labeled region. + """ + if self._data is None: + return None + + vox = self._world_to_voxel(point) + ix, iy, iz = int(round(vox[0])), int(round(vox[1])), int(round(vox[2])) + + # Bounds check + if (ix < 0 or iy < 0 or iz < 0 or + ix >= self._data.shape[0] or + iy >= self._data.shape[1] or + iz >= self._data.shape[2]): + return None + + label = int(self._data[ix, iy, iz]) + if label == 0: + return None + + return { + "label_id": label, + "name": self._label_names.get(label, f"Region_{label}"), + "volume_mm3": self._label_volumes.get(label, 0.0), + } + + def get_nearby_region(self, point: np.ndarray, search_radius: int = 2) -> Optional[dict]: + """Search nearby voxels if exact point has no label. + + Args: + point: (3,) world coordinates + search_radius: search cube half-size in voxels + + Returns: + dict or None + """ + if self._data is None: + return None + + vox = self._world_to_voxel(point) + cx, cy, cz = int(round(vox[0])), int(round(vox[1])), int(round(vox[2])) + + best_label = None + best_dist = float('inf') + + for dx in range(-search_radius, search_radius + 1): + for dy in range(-search_radius, search_radius + 1): + for dz in range(-search_radius, search_radius + 1): + if dx == 0 and dy == 0 and dz == 0: + continue + ix, iy, iz = cx + dx, cy + dy, cz + dz + if (ix < 0 or iy < 0 or iz < 0 or + ix >= self._data.shape[0] or + iy >= self._data.shape[1] or + iz >= self._data.shape[2]): + continue + label = int(self._data[ix, iy, iz]) + if label == 0: + continue + dist = dx*dx + dy*dy + dz*dz + if dist < best_dist: + best_dist = dist + best_label = label + + if best_label is None: + return None + + return { + "label_id": best_label, + "name": self._label_names.get(best_label, f"Region_{best_label}"), + "volume_mm3": self._label_volumes.get(best_label, 0.0), + } + + def get_region_name(self, label_id: int) -> str: + return self._label_names.get(label_id, f"Region_{label_id}") + + def get_region_volume(self, label_id: int) -> float: + return self._label_volumes.get(label_id, 0.0) + + def get_all_labels(self) -> list[int]: + return sorted(self._unique_labels) + + def get_region_mesh(self, label_id: int): + """Generate a VTK polydata mesh for a region via marching cubes. + + Meshes are cached after first generation. Returns None if + scikit-image is not installed or the region has too few voxels. + """ + if label_id in self._mesh_cache: + return self._mesh_cache[label_id] + + if not HAS_SKIMAGE: + print("[extra-parcellation] scikit-image not installed for mesh generation. " + "Install with: pip install scikit-image") + return None + + if not HAS_VTK: + return None + + if self._data is None: + return None + + # Create binary mask + mask = (self._data == label_id).astype(np.float32) + if mask.sum() < 10: # too small for meaningful mesh + return None + + # Pad to avoid edge artifacts + padded = np.pad(mask, 1, mode='constant', constant_values=0) + + try: + verts, faces, normals, _ = marching_cubes(padded, level=0.5) + # Remove padding offset + verts = verts - 1.0 + + # Transform vertices from voxel to world space + ones = np.ones((len(verts), 1), dtype=np.float64) + verts_h = np.hstack([verts, ones]) + world_verts = (self._affine @ verts_h.T).T[:, :3] + + # Build VTK polydata + points = vtk.vtkPoints() + points.SetData(numpy_to_vtk(world_verts.astype(np.float64), deep=True)) + + cells = vtk.vtkCellArray() + for face in faces: + cells.InsertNextCell(3) + for idx in face: + cells.InsertCellPoint(int(idx)) + + polydata = vtk.vtkPolyData() + polydata.SetPoints(points) + polydata.SetPolys(cells) + + # Compute normals for smooth rendering + normal_gen = vtk.vtkPolyDataNormals() + normal_gen.SetInputData(polydata) + normal_gen.ComputePointNormalsOn() + normal_gen.Update() + polydata = normal_gen.GetOutput() + + self._mesh_cache[label_id] = polydata + return polydata + + except Exception as e: + print(f"[extra-parcellation] Mesh generation failed for label {label_id}: {e}") + return None + + def get_region_center(self, label_id: int) -> Optional[np.ndarray]: + """Get the center of mass of a region in world coordinates.""" + if self._data is None: + return None + + coords = np.argwhere(self._data == label_id) + if len(coords) == 0: + return None + + center_vox = coords.mean(axis=0) + center_world = self._affine @ np.array([*center_vox, 1.0]) + return center_world[:3] diff --git a/mindVisualizer/src/main.py b/mindVisualizer/src/main.py new file mode 100644 index 0000000000000000000000000000000000000000..15ba06da988500b25f0f837c2c0e0493de3c0975 --- /dev/null +++ b/mindVisualizer/src/main.py @@ -0,0 +1,1098 @@ +#!/usr/bin/env python3 +"""mindVisualizer — Brain flow particle visualizer with probe analysis. + +Filtered and modular version of simulate_mdn_flow.py, focused on: + - MDN particle flow with OOS seeding and far-from-OOS death acceleration + - Flow mesh overlay (all available brain region meshes, key: q/w) + - Probe system: single, multi-probe, and branching modes + - GPT-powered region transition analysis (RAG or direct) + - Brain region state database with perturbation propagation + +Run with no arguments for defaults: + python -m src.main + +Key bindings: + space pause/resume + f cycle field (mean / gated / comp1..6) + v cycle color mode (SPEED / ENTROPY / DELTA_ENTROPY / DIR_DELTA_ENTROPY) + m cycle colormap (TURBO / RAINBOW) + q / w cycle flow mesh (prev / next) + \\ (backslash) hide flow mesh + 9 / 0 mesh opacity -/+ + [ / ] dt -/+ + + / - speed scale +/- + 1 / 2 particle lifetime -/+ + 7 / 8 particle opacity -/+ + y toggle speed filter + u filter direction (top/bottom) + z / x filter fraction -/+ + o toggle OOS overlay + g place probe (click in scene first) + G (shift+g) analyze probe path with GPT (async) + b toggle branching mode + n cycle multi-probe count (1/4/8) + s initialize brain states (prompts in console) + S (shift+s) propagate state through probe path + c clear probe + Escape quit +""" + +import argparse +import os +import sys +import threading +from pathlib import Path + +import numpy as np +import vtk +from vtkmodules.util.numpy_support import numpy_to_vtk +from vtkmodules.vtkCommonCore import vtkUnsignedCharArray +from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera +from vtkmodules.vtkFiltersModeling import vtkOutlineFilter +from scipy.spatial import cKDTree + +from .field_loader import load_field, TriLinearSampler, load_points_any +from .colormaps import turbo_rgb01, rainbow_rgb01, bicolor_white_center +from .mesh_overlay import FlowMeshOverlay +from .probe import ProbeSystem +from .region_analyzer import (analyze_probe_path, analyze_with_gpt, + format_transitions_text) +from .brain_state import BrainStateDB + +# ---------- paths (relative to project root) ---------- +PROJECT_ROOT = Path(__file__).resolve().parent.parent +DATA_DIR = PROJECT_ROOT / "data" +MDN_DIR = DATA_DIR / "mdn" +MESH_DIR = DATA_DIR / "meshes" +ALIGNMENT_FILE = DATA_DIR / "brain_alignment.json" +DEFAULT_META = MDN_DIR / "mdn_particles_rdcim_teacher_edge_hq_grid125_meta.json" +DEFAULT_OOS = MDN_DIR / "mdn_particles_rdcim_teacher_edge_hq_training_points.npy" +DEFAULT_EXTRA_PARCELLATION = DATA_DIR / "extra_parcellation" / "combined_atlas.nii.gz" +DEFAULT_EXTRA_LABELS = DATA_DIR / "extra_parcellation" / "combined_atlas_labels.json" + + +# ---------- utility functions ---------- + +def lattice_positions(n_per_axis, amin, amax, margin=0.02, jitter=0.012, seed=42): + rng = np.random.default_rng(seed) + fx = np.linspace(margin, 1.0 - margin, n_per_axis, dtype=np.float32) + X, Y, Z = np.meshgrid(fx, fx, fx, indexing="ij") + P = np.stack([X, Y, Z], axis=-1).reshape(-1, 3) + if jitter > 0: + P += (rng.random(P.shape, dtype=np.float32) - 0.5) * 2.0 * (jitter / max(n_per_axis - 1, 1)) + P = np.clip(P, 0, 1) + span = (amax - amin).astype(np.float32) + return (amin + P * span).astype(np.float32) + + +def wrap_inside(P, amin, amax, margin_frac=0.0): + span = amax - amin + offset = amin + margin_frac * span + inner = span * (1.0 - 2.0 * margin_frac) + D = P - offset + D = (D % inner + inner) % inner + return offset + D + + +def build_cloud(P_init, point_size=2.0, rgba=None, opacity=1.0): + N = len(P_init) + vtk_points = vtk.vtkPoints() + vtk_points.SetData(numpy_to_vtk(P_init, deep=True)) + pd = vtk.vtkPolyData() + pd.SetPoints(vtk_points) + verts = vtk.vtkCellArray() + verts.Allocate(N) + verts.InsertNextCell(N) + for i in range(N): + verts.InsertCellPoint(i) + pd.SetVerts(verts) + + colors = vtkUnsignedCharArray() + if rgba is not None: + C = rgba.astype(np.uint8, copy=False) + else: + C = np.tile(np.array([[255, 255, 255, 48]], np.uint8), (N, 1)) + colors.SetName("RGBA") + colors.SetNumberOfComponents(4) + colors.SetNumberOfTuples(N) + colors.DeepCopy(numpy_to_vtk(C, deep=True)) + pd.GetPointData().SetScalars(colors) + + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(pd) + mapper.SetColorModeToDirectScalars() + mapper.ScalarVisibilityOn() + mapper.SetScalarModeToUsePointData() + + actor = vtk.vtkActor() + actor.SetMapper(mapper) + actor.GetProperty().SetPointSize(point_size) + actor.GetProperty().SetOpacity(float(opacity)) + actor.GetProperty().LightingOff() + actor.GetProperty().SetInterpolationToFlat() + return vtk_points, colors, pd, actor + + +def compute_fraction_mask(vals, frac, top): + N = len(vals) + if N == 0: + return np.zeros(0, dtype=bool) + frac = float(np.clip(frac, 0.0, 1.0)) + k = int(np.ceil(N * frac)) + if k <= 0: + return np.zeros(N, dtype=bool) + if k >= N: + return np.ones(N, dtype=bool) + mask = np.zeros(N, dtype=bool) + if top: + idx = np.argpartition(vals, N - k)[N - k:] + else: + idx = np.argpartition(vals, k)[:k] + mask[idx] = True + return mask + + +def densify_oos_surface(base_pts, full_oos_pts, amin, amax, extra_count, + k=16, jitter_frac=0.4, power=2.0, seed=2025): + if extra_count <= 0 or len(base_pts) == 0 or len(full_oos_pts) == 0: + return np.zeros((0, 3), np.float32) + cloud = np.asarray(full_oos_pts, dtype=np.float32) + m_add = min(extra_count, 500_000) + eps = 1e-9 + k_eff = int(max(3, min(k, len(cloud)))) + tree = cKDTree(cloud) + dists, _ = tree.query(base_pts, k=k_eff) + if dists.ndim == 1: + dists = dists[:, None] + r_k = dists[:, -1].astype(np.float32) + eps + med = float(np.median(r_k)) + eps + w = np.power(r_k / med, float(power)).astype(np.float64) + sw = float(np.sum(w)) + if not np.isfinite(sw) or sw <= 0.0: + return np.zeros((0, 3), np.float32) + w /= sw + rng = np.random.default_rng(seed) + sel = rng.choice(len(base_pts), size=m_add, replace=True, p=w) + extras = np.zeros((m_add, 3), np.float32) + for j, i in enumerate(sel): + s_loc = float(r_k[i]) * float(jitter_frac) + extras[j] = base_pts[i] + rng.standard_normal(3).astype(np.float32) * s_loc + return np.clip(extras, amin, amax).astype(np.float32) + + +def add_window_legend(ren, lines, font_px=13): + txt = "\n".join(lines) + ta = vtk.vtkTextActor() + ta.SetInput(txt) + tp = ta.GetTextProperty() + tp.SetColor(1, 1, 1) + tp.SetFontSize(int(font_px)) + tp.SetOpacity(0.92) + tp.SetFontFamilyToCourier() + ta.SetPosition(10, 10) + ren.AddActor(ta) + return ta + + +class VtkTextOverlay: + """Scrolling text log + GPT explanation panel in the VTK window.""" + + def __init__(self, ren: vtk.vtkRenderer, max_lines: int = 8): + self._ren = ren + self._max_lines = max_lines + self._log_lines: list[str] = [] + + self._log_actor = vtk.vtkTextActor() + self._log_actor.SetInput("") + tp = self._log_actor.GetTextProperty() + tp.SetColor(1.0, 0.9, 0.3) + tp.SetFontSize(13) + tp.SetOpacity(0.95) + tp.SetFontFamilyToCourier() + tp.SetJustificationToRight() + tp.SetVerticalJustificationToTop() + self._log_actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay() + self._log_actor.GetPositionCoordinate().SetValue(0.99, 0.97) + ren.AddActor(self._log_actor) + + self._gpt_actor = vtk.vtkTextActor() + self._gpt_actor.SetInput("") + tp2 = self._gpt_actor.GetTextProperty() + tp2.SetColor(0.8, 1.0, 0.8) + tp2.SetFontSize(14) + tp2.SetOpacity(0.95) + tp2.SetFontFamilyToCourier() + tp2.SetJustificationToRight() + tp2.SetVerticalJustificationToTop() + self._gpt_actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay() + self._gpt_actor.GetPositionCoordinate().SetValue(0.99, 0.70) + self._gpt_actor.VisibilityOff() + ren.AddActor(self._gpt_actor) + + def add_log(self, text: str): + self._log_lines.append(text) + if len(self._log_lines) > self._max_lines: + self._log_lines = self._log_lines[-self._max_lines:] + self._log_actor.SetInput("\n".join(self._log_lines)) + + def clear_log(self): + self._log_lines.clear() + self._log_actor.SetInput("") + + def show_gpt(self, text: str): + wrapped = [] + for line in text.split("\n"): + while len(line) > 60: + brk = line.rfind(" ", 0, 60) + if brk <= 0: + brk = 60 + wrapped.append(line[:brk]) + line = line[brk:].lstrip() + wrapped.append(line) + if len(wrapped) > 20: + wrapped = wrapped[:20] + ["..."] + self._gpt_actor.SetInput("\n".join(wrapped)) + self._gpt_actor.VisibilityOn() + + def hide_gpt(self): + self._gpt_actor.SetInput("") + self._gpt_actor.VisibilityOff() + + +# ---------- main ---------- + +def main(): + # Fix SSL cert path (Git on Windows sets bad path) + try: + import certifi + cert_file = certifi.where() + cur = os.environ.get("SSL_CERT_FILE", "") + if not cur or not os.path.isfile(cur): + os.environ["SSL_CERT_FILE"] = cert_file + cur2 = os.environ.get("REQUESTS_CA_BUNDLE", "") + if not cur2 or not os.path.isfile(cur2): + os.environ["REQUESTS_CA_BUNDLE"] = cert_file + except ImportError: + pass + try: + from dotenv import load_dotenv + load_dotenv(PROJECT_ROOT / ".env") + except ImportError: + pass + + ap = argparse.ArgumentParser(description="mindVisualizer - Brain flow particle visualizer") + ap.add_argument("--meta", type=Path, default=DEFAULT_META) + ap.add_argument("--oos", type=Path, default=DEFAULT_OOS) + ap.add_argument("--fps", type=int, default=60) + ap.add_argument("--stride", type=int, default=3) + ap.add_argument("--dt", type=float, default=1.0) + ap.add_argument("--max-step-frac", type=float, default=0.01) + ap.add_argument("--speed-scale", type=float, default=1.0) + ap.add_argument("--margin", type=float, default=0.0) + ap.add_argument("--respawn-jitter", type=float, default=0.015) + ap.add_argument("--oos-seed-limit", type=int, default=50000) + ap.add_argument("--oos-fill-count", type=int, default=10000) + ap.add_argument("--overlap-frac", type=float, default=0.01) + ap.add_argument("--overlap-jitter", type=float, default=0.002) + ap.add_argument("--overlap-limit", type=int, default=200000) + ap.add_argument("--no-far-from-oos", action="store_true") + ap.add_argument("--oos-dist-grid", type=int, default=96) + ap.add_argument("--oos-dist-thresh-frac", type=float, default=0.009) + ap.add_argument("--oos-dist-gamma", type=float, default=3.0) + ap.add_argument("--oos-death-boost", type=float, default=8.0) + ap.add_argument("--no-flow-mesh", action="store_true") + ap.add_argument("--no-rag", action="store_true", + help="Skip RAG, use only GPT's own knowledge") + ap.add_argument("--multi-probe", type=int, default=1, + help="Number of probes to spawn (1=single, 4=neighborhood)") + ap.add_argument("--branching", action="store_true", + help="Enable MDN component branching") + ap.add_argument("--window-size", type=int, nargs=2, default=[1200, 800]) + ap.add_argument("--hq", action="store_true", + help="Use high-quality GPT model (gpt-5.4) instead of gpt-5.4-mini") + ap.add_argument("--debug", action="store_true", + help="Print full LLM prompts to console before each call") + ap.add_argument("--extra-parcellation", type=Path, default=None, + help="Path to NIfTI parcellation file for finer subregion labels " + "(e.g., Brainnetome BN_Atlas_246_1mm.nii.gz)") + ap.add_argument("--extra-parcellation-labels", type=Path, default=None, + help="JSON label map for extra parcellation (label_id -> name)") + args = ap.parse_args() + + model = "gpt-5.4" if args.hq else "gpt-5.4-mini" + print(f"[config] LLM model: {model}") + + far_from_oos = not args.no_far_from_oos + flow_mesh_enabled = not args.no_flow_mesh + use_rag = not args.no_rag + debug_mode = args.debug + + # ---------- load field ---------- + print(f"[field] loading {args.meta} ...") + fld = load_field(args.meta) + G = fld["G"] + amin, amax = fld["amin"], fld["amax"] + V_mean = fld["mean"] + MUS = fld["mus"] + PI = fld["pi"] + ENT = fld["ENT"] + K = len(MUS) + + sampler_mean = TriLinearSampler(V_mean, amin, amax) + diag = float(np.linalg.norm(amax - amin)) + target_step = args.max_step_frac * max(diag, 1e-6) + + def interior_percentile(V, q=100.0): + mag = np.linalg.norm(V.reshape(-1, 3), axis=1) + nz = mag[mag > 0] + return float(np.percentile(nz, q)) if nz.size else 1.0 + + vmax_mean = interior_percentile(V_mean, q=100.0) + + sampler_gated = None + V_gated = None + if PI is not None and PI.ndim == 4 and PI.shape[-1] == K and K > 0: + idx = np.argmax(PI, axis=-1) + V_gated = np.zeros_like(MUS[0], dtype=np.float32) + for k_i in range(K): + mask = (idx == k_i)[..., None].astype(np.float32) + V_gated += MUS[k_i] * mask + sampler_gated = TriLinearSampler(V_gated, amin, amax) + + samplers_comp = [TriLinearSampler(mu, amin, amax) for mu in MUS] + + field_modes = ["mean"] + if sampler_gated is not None: + field_modes.append("gated") + field_modes += [f"comp{i + 1}" for i in range(K)] + field_mode = [field_modes[0]] + + sampler_by_name = {"mean": sampler_mean} + if sampler_gated is not None: + sampler_by_name["gated"] = sampler_gated + for i, s in enumerate(samplers_comp): + sampler_by_name[f"comp{i + 1}"] = s + + vmax_by_name = {"mean": vmax_mean} + if V_gated is not None: + vmax_by_name["gated"] = interior_percentile(V_gated, q=100.0) + for i, mu in enumerate(MUS): + vmax_by_name[f"comp{i + 1}"] = interior_percentile(mu, q=100.0) + + def current_sampler(): + return sampler_by_name.get(field_mode[0], sampler_mean) + def vmax_for_mode(): + return vmax_by_name.get(field_mode[0], vmax_mean) + + ent_min = ent_max = 0.0 + if ENT is not None: + ent_min, ent_max = float(np.min(ENT)), float(np.max(ENT)) + + # ---------- load OOS ---------- + oos_pts = None + if args.oos is not None: + try: + raw = load_points_any(Path(args.oos)) + if raw.size > 0: + oos_pts = raw.astype(np.float32) + print(f"[oos] loaded {len(oos_pts)} points") + except Exception as e: + print("[oos] failed to load:", e) + + # ---------- seeding ---------- + rng = np.random.default_rng(2025) + overlap_radius = float(args.overlap_frac) * max(diag, 1e-9) + overlap_sigma = float(args.overlap_jitter) * max(diag, 1e-9) + seed_mode = "grid" + seed_points = None + n_axis = None + + if fld["TRAIN"] is not None and oos_pts is not None and len(fld["TRAIN"]) > 0 and len(oos_pts) > 0: + try: + tree = cKDTree(fld["TRAIN"].astype(np.float32)) + dists, _ = tree.query(oos_pts.astype(np.float32), k=1) + cand = oos_pts[dists <= overlap_radius] + if cand.size > 0: + seed_mode = "overlap" + if args.overlap_limit and len(cand) > args.overlap_limit: + idx = rng.choice(len(cand), size=int(args.overlap_limit), replace=False) + cand = cand[idx] + seed_points = cand.astype(np.float32) + print(f"[seed] overlap: {len(seed_points)} points") + except Exception as e: + print("[seed] overlap failed:", e) + + if seed_mode == "overlap" and seed_points is not None: + if args.oos_seed_limit > 0 and len(seed_points) > args.oos_seed_limit: + prev = len(seed_points) + idx_seed = rng.choice(prev, size=args.oos_seed_limit, replace=False) + seed_points = seed_points[idx_seed] + print(f"[seed] capped to {len(seed_points)} (from {prev})") + + if args.oos_fill_count > 0 and oos_pts is not None and seed_mode == "overlap" and seed_points is not None: + extras = densify_oos_surface(base_pts=seed_points, full_oos_pts=oos_pts, + amin=amin, amax=amax, extra_count=args.oos_fill_count) + if extras is not None and len(extras) > 0: + seed_points = np.concatenate([seed_points, extras], axis=0).astype(np.float32) + print(f"[seed] oos-fill: added {len(extras)} -> total {len(seed_points)}") + + if seed_mode == "overlap" and seed_points is not None: + if overlap_sigma > 0: + J = rng.standard_normal(seed_points.shape).astype(np.float32) * overlap_sigma + P0 = np.clip(seed_points + J, amin, amax) + else: + P0 = np.clip(seed_points, amin, amax) + else: + n_axis = max(2, G // max(1, args.stride)) + P0 = lattice_positions(n_axis, amin, amax, margin=0.02, jitter=args.respawn_jitter, seed=2025) + + P = P0.copy() + Np = len(P) + _base_ttl_lo, _base_ttl_hi = max(8, 60 // 2), 60 + ttl_state = {"scale": 1.0} + + def _sample_ttl(n): + base = rng.integers(_base_ttl_lo, _base_ttl_hi + 1, size=int(n)) + return np.maximum(1, np.round(base * ttl_state["scale"])).astype(np.int32) + + ttl = _sample_ttl(Np) + ages = rng.integers(0, np.maximum(1, ttl), size=Np, dtype=np.int32) + + ent_prev = sampler_mean.sample_scalar(ENT, P).astype(np.float32) if ENT is not None else None + ent_d_ema = np.zeros(Np, np.float32) if ENT is not None else None + ent_alpha = 0.2 + + # ---------- VTK setup ---------- + init_rgba = np.tile(np.array([[255, 255, 255, 48]], np.uint8), (Np, 1)) + points, color_arr, poly, p_actor = build_cloud(P, point_size=2.0, rgba=init_rgba) + + ren = vtk.vtkRenderer() + ren.SetBackground(0, 0, 0) + ren.AddActor(p_actor) + ren.ResetCamera() + + o_actor = None + if oos_pts is not None and len(oos_pts) > 0: + rgb_oos = np.tile(np.array([[255, 255, 255]], np.uint8), (len(oos_pts), 1)) + oos_rgba = np.concatenate([rgb_oos, np.full((len(oos_pts), 1), 72, np.uint8)], axis=1) + _, _, _, o_actor = build_cloud(oos_pts, point_size=1.5, rgba=oos_rgba, opacity=72 / 255.0) + oos_visible = [False] + + # OOS distance grid + OOS_DIST = None + sampler_oosdist = None + far_oos_params = None + if far_from_oos and oos_pts is not None and len(oos_pts) > 0: + Ng = int(max(8, args.oos_dist_grid)) + print(f"[oos-dist] precomputing {Ng}^3 ...") + tree_oos = cKDTree(oos_pts.astype(np.float32)) + gridP = lattice_positions(Ng, amin, amax, margin=0.0, jitter=0.0, seed=0) + d, _ = tree_oos.query(gridP, k=1) + OOS_DIST = d.reshape(Ng, Ng, Ng).astype(np.float32) + sampler_oosdist = TriLinearSampler(np.zeros((Ng, Ng, Ng, 3), np.float32), amin, amax) + far_oos_params = { + "d0": float(args.oos_dist_thresh_frac) * diag, + "gamma": float(args.oos_dist_gamma), + "boost": float(args.oos_death_boost), + } + print(f"[oos-dist] ready: d0={far_oos_params['d0']:.4g}") + + win = vtk.vtkRenderWindow() + win.AddRenderer(ren) + win.SetSize(*args.window_size) + win.SetWindowName("mindVisualizer") + + # Flow mesh overlay + flow_mesh = None + if flow_mesh_enabled: + try: + flow_mesh = FlowMeshOverlay(ren=ren, win=win, + mesh_dir=MESH_DIR, alignment_file=ALIGNMENT_FILE) + n_regions = len(flow_mesh.get_all_region_keys()) + print(f"[flow mesh] ready ({n_regions} regions)") + # Load or build voxel label grid for fast point-in-region queries + grid_cache = DATA_DIR / "label_grid_cache.npz" + if not flow_mesh.load_label_grid(grid_cache): + print("[flow mesh] No cached label grid found — building now " + "(this takes ~30-60s, one-time only)...") + flow_mesh.build_label_grid() + flow_mesh.save_label_grid(grid_cache) + print("[flow mesh] Label grid cached for future runs.") + except Exception as e: + print("[flow mesh] disabled:", e) + flow_mesh = None + + # Optional extra parcellation (auto-detect combined atlas if no explicit path) + if flow_mesh: + _ep_nifti = args.extra_parcellation + _ep_labels = args.extra_parcellation_labels + if _ep_nifti is None and DEFAULT_EXTRA_PARCELLATION.exists(): + _ep_nifti = DEFAULT_EXTRA_PARCELLATION + if _ep_labels is None and DEFAULT_EXTRA_LABELS.exists(): + _ep_labels = DEFAULT_EXTRA_LABELS + if _ep_nifti is not None: + try: + from .extra_parcellation import ExtraParcellation + extra = ExtraParcellation(_ep_nifti, _ep_labels) + if extra.load(): + flow_mesh.set_extra_parcellation(extra) + except Exception as e: + print(f"[extra-parcellation] disabled: {e}") + + # Text overlay + text_overlay = VtkTextOverlay(ren) + + # Brain state database + brain_state_db = BrainStateDB(model=model, debug=args.debug) + + # Probe system + probe_sys = ProbeSystem(ren, win, amin, amax) + if flow_mesh is not None: + probe_sys.set_mesh_overlay(flow_mesh) + probe_sys.set_multi_count(args.multi_probe) + if args.branching and PI is not None: + probe_sys.set_branching(True, pi_field=PI, mus_samplers=samplers_comp) + + # Boundary constraint + if sampler_oosdist is not None and OOS_DIST is not None and far_oos_params is not None: + _boundary_d0 = far_oos_params["d0"] * 2.0 + + def _probe_boundary_check(pos): + d = float(sampler_oosdist.sample_scalar(OOS_DIST, pos[None, :])[0]) + return d < _boundary_d0 + + probe_sys.set_boundary_check(_probe_boundary_check) + + # Region change callback + def _on_region_change(entered, left, is_ghost=False, label=""): + tag = " [branch]" if is_ghost else "" + for name in left: + text_overlay.add_log(f"LEFT{tag}: {name}") + for name in entered: + text_overlay.add_log(f"ENTERED{tag}: {name}") + + probe_sys.set_on_region_change(_on_region_change) + + picker = vtk.vtkCellPicker() + picker.SetTolerance(0.01) + + # ---------- state ---------- + available_colour_modes = ( + ["SPEED"] + + (["ENTROPY"] if ENT is not None else []) + + (["DELTA_ENTROPY"] if ENT is not None else []) + + (["DIR_DELTA_ENTROPY"] if ENT is not None else []) + ) + colour_idx = [0] + cmap_mode = ["TURBO"] + state = {"paused": False, "dt": float(args.dt), "scale": float(args.speed_scale)} + filter_state = {"mode": "OFF", "dir_top": True, "frac": 1.0} + clip_state = {"on": False, "frac": 1.0} + mdn_alpha = [1.0] + last_rgba = [init_rgba.copy()] + probe_mode = [False] + + # Async GPT result holder + gpt_pending = {"result": None, "running": False} + + def map_rgb_from_t(t01): + return rainbow_rgb01(t01) if cmap_mode[0] == "RAINBOW" else turbo_rgb01(t01) + + def apply_colors(speed_vals, P_world, dent_vals=None, vis_mask=None): + mode = available_colour_modes[colour_idx[0]] + eps = 1e-12 + if mode == "ENTROPY" and ENT is not None: + scal = sampler_mean.sample_scalar(ENT, P_world) + t = (scal - ent_min) / max(ent_max - ent_min, eps) + base_rgb = map_rgb_from_t(np.clip(t, 0, 1)) + elif mode == "DELTA_ENTROPY" and dent_vals is not None: + a = np.abs(dent_vals) + scale = float(np.percentile(a, 97)) if a.size else 1.0 + if not np.isfinite(scale) or scale <= eps: + scale = 1.0 + base_rgb = map_rgb_from_t(np.clip(a / scale, 0, 1)) + elif mode == "DIR_DELTA_ENTROPY" and dent_vals is not None: + base_rgb = bicolor_white_center(dent_vals) + else: + smin = float(np.min(speed_vals)) if speed_vals.size else 0.0 + smax = float(np.max(speed_vals)) if speed_vals.size else 1.0 + t = (speed_vals - smin) / max(smax - smin, eps) + base_rgb = map_rgb_from_t(np.clip(t, 0, 1)) + + a_val = int(np.clip(255.0 * mdn_alpha[0], 5, 255)) + a = np.full(len(base_rgb), a_val, np.uint8) + if vis_mask is not None: + a = a.copy() + a[~vis_mask] = 0 + + rgba = np.concatenate([base_rgb, a[:, None]], axis=1).astype(np.uint8) + color_arr.DeepCopy(numpy_to_vtk(rgba, deep=True)) + color_arr.Modified() + poly.Modified() + last_rgba[0] = rgba + + # Warm-up + for _ in range(12): + V = sampler_mean.sample_vec(P) + P = wrap_inside(P + V * (args.dt * args.speed_scale) * (target_step / max(vmax_mean, 1e-9)), + amin, amax, margin_frac=args.margin) + if ENT is not None: + ent_prev = sampler_mean.sample_scalar(ENT, P).astype(np.float32) + ent_d_ema[:] = 0.0 + + # ---------- timer callback ---------- + def on_timer(_o, _e): + nonlocal P, ages, ttl, ent_prev, ent_d_ema + + # Check for async GPT result + if gpt_pending["result"] is not None: + explanation = gpt_pending["result"] + gpt_pending["result"] = None + gpt_pending["running"] = False + print("\n--- GPT INTERPRETATION ---") + print(explanation) + print("--- END ---\n") + text_overlay.show_gpt(explanation) + text_overlay.add_log("GPT analysis complete") + + if state["paused"]: + return + + samp = current_sampler() + Vraw = samp.sample_vec(P) + + vmx = float(vmax_for_mode()) + if clip_state["on"]: + thr = float(max(1e-9, clip_state["frac"] * vmx)) + s = np.linalg.norm(Vraw, axis=1) + k = np.minimum(1.0, thr / (s + 1e-9)).astype(np.float32) + Vstep = Vraw * k[:, None] + eff_vmax = min(vmx, thr) + else: + Vstep = Vraw + eff_vmax = vmx + + step = state["dt"] * state["scale"] * (target_step / max(eff_vmax, 1e-9)) + P = wrap_inside(P + Vstep * step, amin, amax, margin_frac=args.margin) + + dent = None + if ENT is not None: + ent_now = sampler_mean.sample_scalar(ENT, P).astype(np.float32) + d = ent_now - ent_prev + ent_d_ema = (1.0 - ent_alpha) * ent_d_ema + ent_alpha * d + ent_prev = ent_now + dent = ent_d_ema + + age_inc = np.ones(len(P), np.int32) + if sampler_oosdist is not None and OOS_DIST is not None and far_oos_params is not None: + d_oos = sampler_oosdist.sample_scalar(OOS_DIST, P).astype(np.float32) + d0 = max(1e-9, far_oos_params["d0"]) + t = np.clip((d_oos - d0) / d0, 0.0, 1.0) + gate = np.power(t, float(max(0.1, far_oos_params["gamma"]))).astype(np.float32) + extra = np.floor(gate * float(max(0.0, far_oos_params["boost"])) + 1e-9).astype(np.int32) + age_inc += extra + ages += age_inc + + dead = ages >= ttl + if np.any(dead): + if seed_mode == "overlap" and seed_points is not None and len(seed_points) > 0: + sel = rng.integers(0, len(seed_points), size=dead.sum()) + base = seed_points[sel] + if overlap_sigma > 0: + J = rng.standard_normal((dead.sum(), 3)).astype(np.float32) * overlap_sigma + P[dead] = np.clip(base + J, amin, amax) + else: + P[dead] = np.clip(base, amin, amax) + else: + jitter_world = (amax - amin) * (args.respawn_jitter / max((n_axis or 2) - 1, 1)) + J = (rng.random((dead.sum(), 3)).astype(np.float32) - 0.5) * 2.0 * jitter_world + P[dead] = np.clip(P0[dead] + J, amin, amax) + ages[dead] = 0 + ttl[dead] = _sample_ttl(dead.sum()) + if ENT is not None: + ent_prev[dead] = sampler_mean.sample_scalar(ENT, P[dead]).astype(np.float32) + ent_d_ema[dead] = 0.0 + + # Probe step + if probe_sys.active: + probe_sys.step(samp, step) + + # Render + speed = np.linalg.norm(Vstep, axis=1) + if filter_state["mode"] == "OFF": + vis_mask = np.ones(len(P), dtype=bool) + else: + vis_mask = compute_fraction_mask(speed, filter_state["frac"], + top=filter_state["dir_top"]) + + points.SetData(numpy_to_vtk(P, deep=True)) + points.Modified() + apply_colors(speed, P, dent_vals=dent, vis_mask=vis_mask) + win.Render() + + # ---------- click handler ---------- + def on_left_click(obj, ev): + if not probe_mode[0]: + return + x, y = obj.GetEventPosition() + if picker.Pick(x, y, 0, ren) <= 0: + return + px, py, pz = picker.GetPickPosition() + if not np.isfinite([px, py, pz]).all(): + return + pos = np.array([px, py, pz], np.float32) + probe_sys.place(pos) + probe_mode[0] = False + n = len(probe_sys.probes) + print(f"[probe] {n} probe(s) placed. Shift+G to analyze, C to clear.") + + # ---------- async GPT ---------- + def _run_gpt_full_async(probe_snapshots, use_rag_flag): + """Run FULL analysis + GPT in background thread (no VTK calls).""" + try: + all_transitions = [] + branch_transitions = [] + for snap in probe_snapshots: + path_arr = snap["path"] + field_mags = snap["field_mags"] + transitions = analyze_probe_path( + path_arr, flow_mesh, sample_every=5, + field_mags=field_mags if len(field_mags) == len(path_arr) else None, + entropy_sampler=sampler_mean if ENT is not None else None, + entropy_field=ENT, + ) + if snap["ghost"]: + branch_transitions.extend(transitions) + else: + all_transitions.extend(transitions) + + if not all_transitions: + gpt_pending["result"] = "No brain regions detected along probe path." + return + + # Log transitions (console only, no VTK) + print("\n--- PROBE TRAJECTORY ANALYSIS ---") + for i, t in enumerate(all_transitions, 1): + print(f" {i}. {t['region_name']} ({t['relative_position']}), " + f"steps {t['entry_idx']}-{t['exit_idx']}") + if branch_transitions: + print(f" + {len(branch_transitions)} branch transitions") + + rag_label = "RAG" if use_rag_flag else "direct" + print(f"\n[GPT] Sending to GPT ({rag_label})...") + explanation = analyze_with_gpt(all_transitions, use_rag=use_rag_flag, + model=model, debug=debug_mode) + gpt_pending["result"] = explanation + except Exception as e: + gpt_pending["result"] = f"[GPT ERROR] {e}" + + # ---------- key handler ---------- + def on_keypress(obj, ev): + nonlocal ttl, ages + key = obj.GetKeySym() + key_lower = key.lower() if key else "" + shift = bool(obj.GetShiftKey()) + + if key_lower == "escape": + obj.TerminateApp() + + elif key_lower == "space": + state["paused"] = not state["paused"] + print(f"[{'PAUSED' if state['paused'] else 'RUNNING'}]") + + elif key_lower == "f": + i = field_modes.index(field_mode[0]) + field_mode[0] = field_modes[(i + 1) % len(field_modes)] + print(f"[field] {field_mode[0]}") + + elif key_lower == "v": + if available_colour_modes: + colour_idx[0] = (colour_idx[0] + 1) % len(available_colour_modes) + print(f"[colour] {available_colour_modes[colour_idx[0]]}") + + elif key_lower == "m": + cmap_mode[0] = "RAINBOW" if cmap_mode[0] == "TURBO" else "TURBO" + print(f"[cmap] {cmap_mode[0]}") + + # Flow mesh + elif key_lower == "q" and flow_mesh: + flow_mesh.cycle(-1) + elif key_lower == "w" and flow_mesh: + flow_mesh.cycle(+1) + elif key_lower == "backslash" and flow_mesh: + flow_mesh.hide() + elif key_lower == "9" and flow_mesh: + flow_mesh.set_opacity(1.0 / 1.25) + elif key_lower == "0" and flow_mesh: + flow_mesh.set_opacity(1.25) + + # Sim controls + elif key_lower == "bracketleft": + state["dt"] /= 1.25 + print(f"[dt] {state['dt']:.4f}") + elif key_lower == "bracketright": + state["dt"] *= 1.25 + print(f"[dt] {state['dt']:.4f}") + elif key_lower in ("plus", "equal"): + state["scale"] *= 1.25 + print(f"[speed-scale] {state['scale']:.3f}") + elif key_lower in ("minus", "underscore"): + state["scale"] /= 1.25 + print(f"[speed-scale] {state['scale']:.3f}") + + elif key_lower == "1": + ttl_state["scale"] = float(np.clip(ttl_state["scale"] / 1.25, 0.1, 100.0)) + ttl[:] = np.maximum(1, np.round(ttl * (1.0 / 1.25))).astype(np.int32) + print(f"[lifetime] scale = {ttl_state['scale']:.3f}") + elif key_lower == "2": + ttl_state["scale"] = float(np.clip(ttl_state["scale"] * 1.25, 0.1, 100.0)) + ttl[:] = np.maximum(1, np.round(ttl * 1.25)).astype(np.int32) + print(f"[lifetime] scale = {ttl_state['scale']:.3f}") + + elif key_lower == "7": + mdn_alpha[0] = float(np.clip(mdn_alpha[0] / 1.25, 0.02, 1.0)) + print(f"[alpha] {mdn_alpha[0]:.2f}") + elif key_lower == "8": + mdn_alpha[0] = float(np.clip(mdn_alpha[0] * 1.25, 0.02, 1.0)) + print(f"[alpha] {mdn_alpha[0]:.2f}") + + elif key_lower == "y": + filter_state["mode"] = "OFF" if filter_state["mode"] == "SPEED" else "SPEED" + print(f"[filter] mode={filter_state['mode']}") + elif key_lower == "u": + filter_state["dir_top"] = not filter_state["dir_top"] + print(f"[filter] dir={'TOP' if filter_state['dir_top'] else 'BOTTOM'}") + elif key_lower == "z": + filter_state["frac"] = float(np.clip(filter_state["frac"] / 1.25, 0.0, 1.0)) + print(f"[filter] frac={filter_state['frac'] * 100:.1f}%") + elif key_lower == "x": + filter_state["frac"] = float(np.clip(filter_state["frac"] * 1.25, 0.0, 1.0)) + print(f"[filter] frac={filter_state['frac'] * 100:.1f}%") + + elif key_lower == "j": + clip_state["on"] = not clip_state["on"] + print(f"[speed-clip] {'ON' if clip_state['on'] else 'OFF'}") + elif key_lower == "a": + clip_state["frac"] = float(np.clip(clip_state["frac"] / 1.25, 0.02, 10.0)) + print(f"[speed-clip] frac={clip_state['frac']:.3f}") + elif key_lower == "d": + clip_state["frac"] = float(np.clip(clip_state["frac"] * 1.25, 0.02, 10.0)) + print(f"[speed-clip] frac={clip_state['frac']:.3f}") + + elif key_lower == "o": + if o_actor is not None: + if oos_visible[0]: + ren.RemoveActor(o_actor) + oos_visible[0] = False + else: + ren.AddActor(o_actor) + oos_visible[0] = True + print(f"[oos overlay] visible = {oos_visible[0]}") + win.Render() + + # ---------- probe controls ---------- + elif key_lower == "g" and not shift: + probe_mode[0] = True + print("[probe] Click in the scene to place probe...") + + elif key_lower == "g" and shift: + if not probe_sys.active: + print("[probe] No probe active. Press g then click.") + text_overlay.add_log("No probe to analyze") + elif flow_mesh is None: + print("[probe] Flow mesh required.") + elif gpt_pending["running"]: + print("[probe] GPT analysis already running...") + else: + # Stop probe movement — we're analyzing the path so far + probe_sys.freeze() + print("[probe] Probe stopped. Analyzing trajectory...") + text_overlay.add_log("Please wait — analyzing probe path...") + win.Render() + + # Collect probe data snapshots (lightweight, no VTK calls) + probe_snapshots = [] + for p in probe_sys.get_all_probes(): + if not p.path or len(p.path) < 3: + continue + probe_snapshots.append({ + "path": p.get_path_array().copy(), + "field_mags": p.get_field_mags_array().copy(), + "ghost": p.ghost, + "label": p.label, + }) + + if not probe_snapshots: + print("[probe] No probe data.") + text_overlay.add_log("No probe data") + else: + gpt_pending["running"] = True + t = threading.Thread( + target=_run_gpt_full_async, + args=(probe_snapshots, use_rag), + daemon=True + ) + t.start() + + elif key_lower == "c": + probe_sys.clear() + probe_mode[0] = False + text_overlay.clear_log() + text_overlay.hide_gpt() + + # ---------- branching / multi-probe ---------- + elif key_lower == "b": + new_state = not probe_sys._branching_enabled + if PI is not None: + probe_sys.set_branching(new_state, pi_field=PI, mus_samplers=samplers_comp) + text_overlay.add_log(f"Branching {'ON' if new_state else 'OFF'}") + else: + print("[probe] No PI field - branching not available") + + elif key_lower == "n": + counts = [1, 4, 8] + cur = probe_sys._multi_count + idx = counts.index(cur) if cur in counts else 0 + new_count = counts[(idx + 1) % len(counts)] + probe_sys.set_multi_count(new_count) + text_overlay.add_log(f"Multi-probe: {new_count}") + + # ---------- brain state ---------- + elif key_lower == "s" and not shift: + # Initialize brain states + if flow_mesh is None: + print("[brain-state] Flow mesh required.") + return + region_names = [] + for k in flow_mesh.get_all_region_keys(): + name = flow_mesh.get_region_name(k) + if hasattr(flow_mesh, '_hemispheres') and k in flow_mesh._hemispheres: + region_names.append(f"{name} (left)") + region_names.append(f"{name} (right)") + else: + region_names.append(name) + print("[brain-state] Enter global brain state (or press Enter for default):") + print(" Example: 'someone thinking about loved ones'") + + def _init_states_async(): + try: + # Read from stdin (blocking) + import sys + global_state = input(" > ").strip() + result = brain_state_db.initialize_from_global( + global_state, region_names, + callback=lambda msg: print(f" {msg}") + ) + print(f"[brain-state] {result}") + text_overlay.add_log("Brain states initialized") + except Exception as e: + print(f"[brain-state] Error: {e}") + + t = threading.Thread(target=_init_states_async, daemon=True) + t.start() + + elif key_lower == "s" and shift: + # Propagate state through probe path + if not probe_sys.active or flow_mesh is None: + print("[brain-state] Need active probe + flow mesh.") + return + if not brain_state_db.has_states(): + print("[brain-state] Initialize states first (press 's').") + return + + # Snapshot probe data (lightweight) + primary = probe_sys.get_primary_probe() + if primary is None or len(primary.path) < 3: + print("[brain-state] No probe path.") + return + prop_snapshot = { + "path": primary.get_path_array().copy(), + "field_mags": primary.get_field_mags_array().copy(), + } + + def _propagate_async(): + try: + path_arr = prop_snapshot["path"] + field_mags = prop_snapshot["field_mags"] + transitions = analyze_probe_path( + path_arr, flow_mesh, sample_every=5, + field_mags=field_mags if len(field_mags) == len(path_arr) else None, + ) + if not transitions: + print("[brain-state] No regions in path.") + return + + def _hemi_name(t): + name = t["region_name"] + h = t.get("hemisphere") + return f"{name} ({h})" if h else name + + source = _hemi_name(transitions[0]) + affected = [_hemi_name(t) for t in transitions[1:]] + strengths = {_hemi_name(t): t.get("avg_flow_strength", 0.0) or 0.0 + for t in transitions} + + print(f"[brain-state] Propagating from {source}...") + updates = brain_state_db.propagate_through_regions( + source, affected, flow_strengths=strengths, + callback=lambda m: print(f" {m}") + ) + if updates: + summary = brain_state_db.summarize_changes(updates, source) + story = brain_state_db.generate_flow_story(updates, source) + print(f"\n--- PROPAGATION SUMMARY ---") + print(summary) + print(f"\n--- INFORMATION FLOW STORY ---") + print(story) + print("--- END ---\n") + gpt_pending["result"] = f"{summary}\n\n{story}" + else: + print("[brain-state] No state changes.") + except Exception as e: + print(f"[brain-state] Error: {e}") + + t = threading.Thread(target=_propagate_async, daemon=True) + t.start() + + # ---------- legend ---------- + add_window_legend(ren, [ + "mindVisualizer", + "space pause | f field | v colour | m cmap | q/w mesh | \\ hide", + "[ ] dt | +/- speed | 1/2 lifetime | 7/8 alpha | y/u/z/x filter", + "o OOS | j clip | a/d frac | g probe | G(shift) analyze(async)", + "b branching | n multi(1/4/8) | s states | S propagate", + "c clear | Esc quit", + ], font_px=12) + + # ---------- interactor ---------- + iren = vtk.vtkRenderWindowInteractor() + iren.SetRenderWindow(win) + iren.SetInteractorStyle(vtkInteractorStyleTrackballCamera()) + iren.Initialize() + iren.AddObserver("TimerEvent", on_timer) + iren.AddObserver("KeyPressEvent", on_keypress) + iren.AddObserver("LeftButtonPressEvent", on_left_click) + iren.CreateRepeatingTimer(max(1, int(1000 / max(1, args.fps)))) + win.Render() + + rag_status = "RAG" if use_rag else "direct (no RAG)" + print("\n=== mindVisualizer ready ===") + print(f" Particles: {Np} | Seed: {seed_mode} | Field: {field_mode[0]}") + print(f" Colours: {', '.join(available_colour_modes)}") + print(f" Meshes: {len(flow_mesh.get_all_region_keys()) if flow_mesh else 0} regions") + print(f" GPT: {rag_status} | Multi-probe: {probe_sys._multi_count} | " + f"Branching: {'ON' if probe_sys._branching_enabled else 'OFF'}") + print(f" Brain states: {'loaded' if brain_state_db.has_states() else 'not initialized (press s)'}") + print(f" Press 'g' then click to place probe. Shift+G to analyze.") + print() + iren.Start() + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print("[ERROR]", e) + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/mindVisualizer/src/mesh_overlay.py b/mindVisualizer/src/mesh_overlay.py new file mode 100644 index 0000000000000000000000000000000000000000..6328f181eed7532a2f54e67c2260648004569cc3 --- /dev/null +++ b/mindVisualizer/src/mesh_overlay.py @@ -0,0 +1,821 @@ +"""Brain-region mesh overlay for VTK renderers. + +Loads OBJ meshes from data/meshes_obj/, applies um->mm scaling + alignment matrix, +and provides cycling/toggling/opacity controls. Loads anatomical names from Allen +Human Brain Atlas structures.json. + +Hemisphere detection: symmetric meshes (two connected components split along X) +are stored as "{key}_left" and "{key}_right" for per-hemisphere queries. + +Region lookup: a 3D voxel label grid is pre-computed at startup so that +point-in-region queries are O(1) array lookups — fast and reliable regardless +of mesh quality (holes, non-watertight surfaces, etc.). +""" + +import json +from pathlib import Path + +import numpy as np +import vtk + +DEFAULT_MESH_SCALE = 0.001 # um -> mm +LABEL_GRID_RESOLUTION = 100 # voxels per axis for region lookup grid + + +# --------------------------------------------------------------------------- +# helpers +# --------------------------------------------------------------------------- + +def _load_alignment(path: Path): + """Load 4x4 alignment matrix from JSON.""" + try: + M = json.loads(Path(path).read_text())["matrix"] + mx = vtk.vtkMatrix4x4() + for i in range(4): + for j in range(4): + mx.SetElement(i, j, float(M[i][j])) + return mx + except Exception: + print("[mesh] no/invalid alignment json; showing raw OBJ coords") + return None + + +def _load_structures(data_dir: Path) -> dict[str, str]: + """Load region ID -> name mapping from Allen atlas structures.json.""" + mapping: dict[str, str] = {} + structs_path = data_dir / "structures.json" + if structs_path.exists(): + try: + structs = json.loads(structs_path.read_text(encoding="utf-8")) + for s in structs: + sid = str(s["id"]) + name = s.get("name", sid) + acronym = s.get("acronym", "") + if acronym and acronym != "root": + mapping[sid] = f"{name} ({acronym})" + else: + mapping[sid] = name + except Exception as e: + print(f"[mesh] warning: could not parse structures.json: {e}") + mapping.setdefault("brain_outline", "Whole Brain Outline") + return mapping + + +def _polydata_center_x(poly: vtk.vtkPolyData) -> float: + """Return the X coordinate of the center of mass of *poly*.""" + com = vtk.vtkCenterOfMass() + com.SetInputData(poly) + com.SetUseScalarsAsWeights(False) + com.Update() + return com.GetCenter()[0] + + +def _polydata_bounds(poly: vtk.vtkPolyData) -> tuple[float, ...]: + """Return (xmin, xmax, ymin, ymax, zmin, zmax).""" + return poly.GetBounds() + + +# --------------------------------------------------------------------------- +# main class +# --------------------------------------------------------------------------- + +class FlowMeshOverlay: + """Brain-region mesh overlay for the main flow renderer. + + Loads OBJ meshes from ``mesh_dir/../meshes_obj``, applies alignment + matrix (um->mm scale + JSON alignment), and optionally splits symmetric + meshes into left/right hemispheres. + """ + + def __init__(self, ren: vtk.vtkRenderer, win: vtk.vtkRenderWindow, + mesh_dir: Path, alignment_file: Path, + data_dir: Path | None = None): + self.ren = ren + self.win = win + self.dir = Path(mesh_dir) + self._actors: dict[str, vtk.vtkActor] = {} + self._polydata: dict[str, vtk.vtkPolyData] = {} + self._visible: str | None = None + self._opacity = 0.35 + self._align = _load_alignment(alignment_file) + + # Pre-computed bounding boxes: key -> (xmin, xmax, ymin, ymax, zmin, zmax) + self._bboxes: dict[str, tuple[float, ...]] = {} + + # Hemisphere splits: key -> {"left": vtkPolyData, "right": vtkPolyData} + self._hemispheres: dict[str, dict[str, vtk.vtkPolyData]] = {} + + # Load region names from atlas + if data_dir is None: + data_dir = self.dir.parent + self._names = _load_structures(data_dir) + + # ----- discover OBJ meshes only ----- + obj_dir = self.dir.parent / "meshes_obj" + if not obj_dir.exists(): + obj_dir = self.dir # fall back to meshes/ if meshes_obj/ absent + self.keys: dict[str, Path] = {} + if obj_dir.exists(): + for obj in sorted(obj_dir.glob("*.obj")): + self.keys[obj.stem] = obj + else: + print(f"[mesh] warning: OBJ directory not found: {obj_dir}") + + # Skip overly broad structural IDs + _skip = {"10153", "10154", "10155", "10156", "10157", "10158", + "10557", "10668"} + self._order = [k for k in self.keys + if k != "brain_outline" and k not in _skip] + + # Compute bounding-box volumes and filter oversized meshes + self._bb_volumes: dict[str, float] = {} + self._compute_volumes_and_filter() + + # Optional extra parcellation (set via set_extra_parcellation) + self._extra = None + + print(f"[mesh] discovered {len(self._order)} region meshes " + f"({len(self.keys)} OBJ)") + + # ------------------------------------------------------------------ + # extra parcellation + # ------------------------------------------------------------------ + + def set_extra_parcellation(self, extra): + """Attach an optional ExtraParcellation for finer subregion queries. + + Args: + extra: ExtraParcellation instance (already loaded) + """ + self._extra = extra + + def get_hierarchical_regions_at_point(self, point: np.ndarray) -> dict: + """Return both primary (Allen) and subregion (extra parcellation) at a point. + + Returns: + { + "primary": str | None, # Allen atlas region key + "primary_name": str | None, # Allen atlas region name + "subregion": dict | None, # {"label_id", "name", "volume_mm3"} from extra + "subregion_mesh": vtkPolyData | None # mesh if available + } + """ + result = { + "primary": None, + "primary_name": None, + "subregion": None, + "subregion_mesh": None, + } + + # Primary lookup (Allen atlas) + primary_key = self.get_region_at_point(point) + if primary_key is None: + primary_key = self.find_nearest_region(point, search_radius=2, + max_distance_mm=5.0) + if primary_key: + result["primary"] = primary_key + result["primary_name"] = self.get_region_name(primary_key) + + # Extra parcellation lookup + if self._extra is not None and self._extra.is_loaded(): + sub = self._extra.get_region_at_point(point) + if sub is None: + sub = self._extra.get_nearby_region(point, search_radius=2) + if sub is not None: + result["subregion"] = sub + # Try to get mesh for highlighting + mesh = self._extra.get_region_mesh(sub["label_id"]) + if mesh is not None: + result["subregion_mesh"] = mesh + + return result + + # ------------------------------------------------------------------ + # volume filtering + # ------------------------------------------------------------------ + + def _compute_volumes_and_filter(self): + """Compute bounding-box volumes and exclude overly large meshes.""" + vols: dict[str, float] = {} + for key in list(self._order): + poly = self.get_polydata(key) + if poly is not None and poly.GetNumberOfPoints() > 0: + b = poly.GetBounds() + vol = (b[1] - b[0]) * (b[3] - b[2]) * (b[5] - b[4]) + vols[key] = vol + self._bb_volumes = vols + + if not vols: + return + + max_vol = max(vols.values()) + threshold = max_vol * 0.25 + oversized = [k for k, v in vols.items() if v > threshold] + if oversized: + names = [self.get_region_name(k) for k in oversized] + print(f"[mesh] excluding {len(oversized)} oversized regions: " + f"{', '.join(names[:5])}{'...' if len(names) > 5 else ''}") + self._order = [k for k in self._order if k not in oversized] + + # ------------------------------------------------------------------ + # voxel label grid (pre-computed for fast point-in-region queries) + # ------------------------------------------------------------------ + + def build_label_grid(self): + """Pre-compute a 3D voxel grid mapping each voxel to a region key. + + After this call, point_in_region() / get_region_at_point() use fast + O(1) array lookups instead of expensive vtkSelectEnclosedPoints. + """ + # Determine brain bounds from brain outline or all meshes + brain_poly = self.get_polydata("10155") # brain outline + if brain_poly is not None: + bb = brain_poly.GetBounds() + else: + # Fallback: union of all mesh bounding boxes + all_bounds = [self.get_polydata(k).GetBounds() + for k in self._order if self.get_polydata(k) is not None] + if not all_bounds: + print("[mesh] no meshes loaded, cannot build label grid") + return + bb = (min(b[0] for b in all_bounds), max(b[1] for b in all_bounds), + min(b[2] for b in all_bounds), max(b[3] for b in all_bounds), + min(b[4] for b in all_bounds), max(b[5] for b in all_bounds)) + + margin = 2.0 + self._grid_origin = np.array([bb[0] - margin, bb[2] - margin, + bb[4] - margin], dtype=np.float32) + self._grid_max = np.array([bb[1] + margin, bb[3] + margin, + bb[5] + margin], dtype=np.float32) + N = LABEL_GRID_RESOLUTION + self._grid_spacing = (self._grid_max - self._grid_origin) / N + self._grid_N = N + + # key_index: 0 = no region, 1..len = region keys + self._grid_keys = [""] # index 0 = empty + key_to_idx = {} + for ki, key in enumerate(self._order): + self._grid_keys.append(key) + key_to_idx[key] = ki + 1 + + # Initialize grid to 0 (no region) + self._label_grid = np.zeros((N, N, N), dtype=np.int16) + + # Build all voxel centers + print(f"[mesh] building {N}^3 label grid for {len(self._order)} regions...") + total = N * N * N + all_pts = vtk.vtkPoints() + all_pts.SetNumberOfPoints(total) + idx = 0 + for iz in range(N): + z = self._grid_origin[2] + (iz + 0.5) * self._grid_spacing[2] + for iy in range(N): + y = self._grid_origin[1] + (iy + 0.5) * self._grid_spacing[1] + for ix in range(N): + x = self._grid_origin[0] + (ix + 0.5) * self._grid_spacing[0] + all_pts.SetPoint(idx, x, y, z) + idx += 1 + + all_pd = vtk.vtkPolyData() + all_pd.SetPoints(all_pts) + + # For each mesh, batch test all voxels + # Process smallest meshes first (they are most specific / finest granularity) + ordered_keys = sorted(self._order, + key=lambda k: self._bb_volumes.get(k, 0)) + + for mi, key in enumerate(ordered_keys): + poly = self.get_polydata(key) + if poly is None or poly.GetNumberOfCells() < 10: + continue + + mesh_bb = poly.GetBounds() + + # Index range that overlaps this mesh bbox + ix_lo = max(0, int((mesh_bb[0] - self._grid_origin[0]) / + self._grid_spacing[0] - 1)) + ix_hi = min(N, int((mesh_bb[1] - self._grid_origin[0]) / + self._grid_spacing[0] + 2)) + iy_lo = max(0, int((mesh_bb[2] - self._grid_origin[1]) / + self._grid_spacing[1] - 1)) + iy_hi = min(N, int((mesh_bb[3] - self._grid_origin[1]) / + self._grid_spacing[1] + 2)) + iz_lo = max(0, int((mesh_bb[4] - self._grid_origin[2]) / + self._grid_spacing[2] - 1)) + iz_hi = min(N, int((mesh_bb[5] - self._grid_origin[2]) / + self._grid_spacing[2] + 2)) + + # Collect candidate flat indices + candidates = [] + for iz in range(iz_lo, iz_hi): + for iy in range(iy_lo, iy_hi): + for ix in range(ix_lo, ix_hi): + candidates.append((ix, iy, iz, + iz * N * N + iy * N + ix)) + + if not candidates: + continue + + # Build test polydata with only candidate points + test_pts = vtk.vtkPoints() + test_pts.SetNumberOfPoints(len(candidates)) + for ci, (_, _, _, flat) in enumerate(candidates): + test_pts.SetPoint(ci, all_pts.GetPoint(flat)) + test_pd = vtk.vtkPolyData() + test_pd.SetPoints(test_pts) + + enc = vtk.vtkSelectEnclosedPoints() + enc.SetSurfaceData(poly) + enc.SetTolerance(0.001) + enc.SetInputData(test_pd) + enc.Update() + + region_idx = key_to_idx[key] + count = 0 + for ci, (ix, iy, iz, _) in enumerate(candidates): + if enc.IsInside(ci): + self._label_grid[ix, iy, iz] = region_idx + count += 1 + + enc.Complete() + + if (mi + 1) % 20 == 0 or count > 0: + name = self.get_region_name(key) + filled = int(np.count_nonzero(self._label_grid)) + print(f" [{mi+1}/{len(ordered_keys)}] {name}: " + f"{count} voxels, total filled: {filled}") + + total_filled = int(np.count_nonzero(self._label_grid)) + print(f"[mesh] label grid complete: {total_filled}/{total} voxels " + f"filled ({total_filled/total*100:.1f}%)") + self._label_grid_ready = True + + def save_label_grid(self, path: Path): + """Save the pre-computed label grid to disk for fast startup.""" + if not getattr(self, '_label_grid_ready', False): + print("[mesh] no label grid to save") + return + path = Path(path) + data = { + "grid": self._label_grid, + "origin": self._grid_origin, + "max": self._grid_max, + "spacing": self._grid_spacing, + "N": self._grid_N, + "keys": self._grid_keys, + } + np.savez_compressed(str(path), **{ + "grid": self._label_grid, + "origin": self._grid_origin, + "grid_max": self._grid_max, + "spacing": self._grid_spacing, + }) + # Save keys separately as JSON (string list) + keys_path = path.with_suffix('.keys.json') + keys_path.write_text(json.dumps(self._grid_keys), encoding='utf-8') + print(f"[mesh] saved label grid to {path}") + + def load_label_grid(self, path: Path) -> bool: + """Load a pre-computed label grid from disk. + + Returns True if loaded successfully, False otherwise. + """ + path = Path(path) + keys_path = path.with_suffix('.keys.json') + if not path.exists() or not keys_path.exists(): + return False + try: + d = np.load(str(path)) + self._label_grid = d["grid"] + self._grid_origin = d["origin"].astype(np.float32) + self._grid_max = d["grid_max"].astype(np.float32) + self._grid_spacing = d["spacing"].astype(np.float32) + self._grid_N = int(self._label_grid.shape[0]) + self._grid_keys = json.loads(keys_path.read_text(encoding='utf-8')) + + # Validate keys match loaded meshes + loaded_mesh_keys = set(self._order) + grid_keys_set = set(self._grid_keys[1:]) # skip index 0 (empty) + if not grid_keys_set.issubset(loaded_mesh_keys): + missing = grid_keys_set - loaded_mesh_keys + print(f"[mesh] cached grid has {len(missing)} keys not in current meshes, rebuilding...") + return False + + total_filled = int(np.count_nonzero(self._label_grid)) + total = self._grid_N ** 3 + print(f"[mesh] loaded cached label grid: {total_filled}/{total} voxels " + f"filled ({total_filled/total*100:.1f}%)") + self._label_grid_ready = True + return True + except Exception as e: + print(f"[mesh] failed to load cached grid: {e}") + return False + + def get_region_at_point(self, point: np.ndarray) -> str | None: + """Return the region key at a 3D point using the label grid. + + Returns None if no region, or the key string (e.g. "12114"). + """ + if not getattr(self, '_label_grid_ready', False): + return None + p = np.asarray(point, dtype=np.float32).ravel()[:3] + idx = ((p - self._grid_origin) / self._grid_spacing).astype(int) + N = self._grid_N + if np.any(idx < 0) or np.any(idx >= N): + return None + val = self._label_grid[idx[0], idx[1], idx[2]] + if val == 0: + return None + return self._grid_keys[val] + + def get_all_regions_at_point(self, point: np.ndarray) -> list[str]: + """Return all region keys near a 3D point. + + Checks the point's voxel and its 26 neighbors to catch points near + region boundaries. + """ + if not getattr(self, '_label_grid_ready', False): + return [] + p = np.asarray(point, dtype=np.float32).ravel()[:3] + idx = ((p - self._grid_origin) / self._grid_spacing).astype(int) + N = self._grid_N + if np.any(idx < 0) or np.any(idx >= N): + return [] + + found = set() + # Check center voxel first + val = self._label_grid[idx[0], idx[1], idx[2]] + if val > 0: + found.add(self._grid_keys[val]) + + return list(found) + + def find_nearest_region(self, point: np.ndarray, + search_radius: int = 3, + max_distance_mm: float = 0.0) -> str | None: + """Find nearest region within search_radius voxels of point. + + Useful when a point falls between regions (in a tiny gap). + + Args: + point: 3D position in mm. + search_radius: max voxels to search outward. + max_distance_mm: if > 0, reject matches farther than this (in mm). + Distance is measured to the nearest labelled voxel + center, which approximates distance to surface for + the grid resolution we use. + """ + if not getattr(self, '_label_grid_ready', False): + return None + p = np.asarray(point, dtype=np.float32).ravel()[:3] + idx = ((p - self._grid_origin) / self._grid_spacing).astype(int) + N = self._grid_N + + # Spiral outward from center + for r in range(0, search_radius + 1): + for dz in range(-r, r + 1): + for dy in range(-r, r + 1): + for dx in range(-r, r + 1): + if max(abs(dx), abs(dy), abs(dz)) != r: + continue # only check shell at distance r + ix = idx[0] + dx + iy = idx[1] + dy + iz = idx[2] + dz + if 0 <= ix < N and 0 <= iy < N and 0 <= iz < N: + val = self._label_grid[ix, iy, iz] + if val > 0: + if max_distance_mm > 0: + voxel_center = self._grid_origin + \ + (np.array([ix, iy, iz]) + 0.5) * self._grid_spacing + dist = float(np.linalg.norm(p - voxel_center)) + if dist > max_distance_mm: + continue + return self._grid_keys[val] + return None + + # ------------------------------------------------------------------ + # mesh loading + # ------------------------------------------------------------------ + + def _load_mesh(self, filepath: Path) -> vtk.vtkPolyData | None: + """Load an OBJ mesh file.""" + if filepath.suffix.lower() != ".obj": + return None + reader = vtk.vtkOBJReader() + reader.SetFileName(str(filepath)) + reader.Update() + return reader.GetOutput() + + def _transform_polydata(self, poly: vtk.vtkPolyData) -> vtk.vtkPolyData: + """Apply um->mm scaling and alignment matrix to *poly*.""" + tf_scale = vtk.vtkTransform() + tf_scale.Scale(DEFAULT_MESH_SCALE, DEFAULT_MESH_SCALE, DEFAULT_MESH_SCALE) + f1 = vtk.vtkTransformPolyDataFilter() + f1.SetInputData(poly) + f1.SetTransform(tf_scale) + f1.Update() + out = f1.GetOutput() + + if self._align is not None: + tf = vtk.vtkTransform() + tf.SetMatrix(self._align) + f2 = vtk.vtkTransformPolyDataFilter() + f2.SetInputData(out) + f2.SetTransform(tf) + f2.Update() + out = f2.GetOutput() + + return out + + # ------------------------------------------------------------------ + # hemisphere detection + # ------------------------------------------------------------------ + + def _detect_hemispheres(self, key: str, poly: vtk.vtkPolyData): + """If *poly* has exactly two connected regions split along X, store + them as left / right hemispheres for *key*.""" + conn = vtk.vtkConnectivityFilter() + conn.SetInputData(poly) + conn.SetExtractionModeToAllRegions() + conn.ColorRegionsOn() + conn.Update() + + n_regions = conn.GetNumberOfExtractedRegions() + if n_regions != 2: + return + + # Extract each region separately + regions: list[vtk.vtkPolyData] = [] + for rid in range(2): + extract = vtk.vtkConnectivityFilter() + extract.SetInputData(poly) + extract.SetExtractionModeToSpecifiedRegions() + extract.AddSpecifiedRegion(rid) + extract.Update() + regions.append(extract.GetOutput()) + + cx0 = _polydata_center_x(regions[0]) + cx1 = _polydata_center_x(regions[1]) + + # Overall midpoint along X from the full mesh bounds + bounds = poly.GetBounds() + mid_x = (bounds[0] + bounds[1]) / 2.0 + + # Check that the two regions sit on opposite sides of the midpoint + if (cx0 < mid_x and cx1 > mid_x): + self._hemispheres[key] = {"left": regions[0], "right": regions[1]} + elif (cx1 < mid_x and cx0 > mid_x): + self._hemispheres[key] = {"left": regions[1], "right": regions[0]} + # else: not a clean left/right split -- skip + + # ------------------------------------------------------------------ + # actor creation + # ------------------------------------------------------------------ + + def _actor_for(self, key: str) -> vtk.vtkActor | None: + if key in self._actors: + return self._actors[key] + if key not in self.keys: + return None + filepath = self.keys[key] + if not filepath.exists(): + print(f"[flow mesh] missing file: {filepath}") + return None + + poly = self._load_mesh(filepath) + if poly is None or poly.GetNumberOfPoints() == 0: + return None + + out = self._transform_polydata(poly) + + # Cache bounding box + self._bboxes[key] = _polydata_bounds(out) + + # Hemisphere detection + self._detect_hemispheres(key, out) + + # Build actor + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(out) + act = vtk.vtkActor() + act.SetMapper(mapper) + if key == "brain_outline": + act.GetProperty().SetColor(0.85, 0.85, 0.85) + act.GetProperty().SetOpacity(max(0.15, self._opacity * 0.7)) + else: + act.GetProperty().SetColor(0.92, 0.92, 0.92) + act.GetProperty().SetOpacity(self._opacity) + act.GetProperty().LightingOff() + + self._actors[key] = act + self._polydata[key] = out + return act + + # ------------------------------------------------------------------ + # public: polydata / region info + # ------------------------------------------------------------------ + + def get_polydata(self, key: str) -> vtk.vtkPolyData | None: + if key not in self._polydata: + self._actor_for(key) + return self._polydata.get(key) + + def get_region_name(self, key: str) -> str: + return self._names.get(key, f"Region {key}") + + def get_all_region_keys(self) -> list[str]: + return list(self._order) + + def get_visible_key(self) -> str | None: + return self._visible + + # ------------------------------------------------------------------ + # public: hemisphere queries + # ------------------------------------------------------------------ + + def get_hemisphere_polydata(self, key: str, + point: np.ndarray) -> vtk.vtkPolyData | None: + """Return the hemisphere polydata (left or right) that contains *point*. + + If the mesh was cleanly split into 2 connected components, returns the + matching half. Otherwise, if the mesh spans both hemispheres (wide X + extent), clips it to the probe's side using a plane at the midpoint. + """ + # Ensure mesh is loaded (triggers hemisphere detection) + full = self.get_polydata(key) + hemi = self._hemispheres.get(key) + if hemi is not None: + label = self._classify_hemisphere(key, point) + return hemi.get(label, full) + + # Fallback: clip by X-plane if mesh spans both hemispheres + if full is None: + return full + bounds = self._bboxes.get(key) + if bounds is None: + return full + x_extent = bounds[1] - bounds[0] + y_extent = bounds[3] - bounds[2] + # Only clip if X extent is large relative to Y (indicates bilateral mesh) + if x_extent > y_extent * 0.6: + mid_x = (bounds[0] + bounds[1]) / 2.0 + plane = vtk.vtkPlane() + plane.SetOrigin(mid_x, 0, 0) + # Keep the side where the probe is: + # vtkClipPolyData keeps points where implicit function >= 0 (default) + # Plane function: dot(point - origin, normal) + # If probe is at x < mid_x, we want to keep x < mid_x + # normal = (-1,0,0): function = -(x - mid_x) = mid_x - x, positive for x < mid_x => kept + if float(point[0]) < mid_x: + plane.SetNormal(-1, 0, 0) # keep x < mid (probe's side) + else: + plane.SetNormal(1, 0, 0) # keep x > mid (probe's side) + clipper = vtk.vtkClipPolyData() + clipper.SetInputData(full) + clipper.SetClipFunction(plane) + clipper.Update() + clipped = clipper.GetOutput() + if clipped.GetNumberOfCells() > 0: + return clipped + + return full + + def get_hemisphere_label(self, key: str, point: np.ndarray) -> str | None: + """Return ``"left"`` or ``"right"`` for *point* relative to *key*. + + Returns ``None`` if the mesh has no hemisphere split. + """ + self.get_polydata(key) # ensure loaded + if key not in self._hemispheres: + return None + return self._classify_hemisphere(key, point) + + def _classify_hemisphere(self, key: str, point: np.ndarray) -> str: + """Decide whether *point* falls in the left or right hemisphere of + *key* by comparing its X coordinate to the mesh midpoint.""" + bounds = self._bboxes.get(key) + if bounds is None: + bounds = _polydata_bounds(self._polydata[key]) + mid_x = (bounds[0] + bounds[1]) / 2.0 + # After alignment transform, determine which side is anatomical left/right + # by checking probe X relative to mesh midpoint + return "left" if float(point[0]) < mid_x else "right" + + # ------------------------------------------------------------------ + # public: point-in-mesh with fast rejection + # ------------------------------------------------------------------ + + def fast_point_in_mesh(self, point: np.ndarray, key: str) -> bool: + """Check if *point* is inside the mesh identified by *key*. + + Performs a cheap bounding-box test first; only runs + ``vtkSelectEnclosedPoints`` if the point falls within the box. + """ + poly = self.get_polydata(key) + if poly is None or poly.GetNumberOfCells() == 0: + return False + + # --- fast bounding-box rejection --- + bb = self._bboxes.get(key) + if bb is None: + bb = _polydata_bounds(poly) + self._bboxes[key] = bb + px, py, pz = float(point[0]), float(point[1]), float(point[2]) + if (px < bb[0] or px > bb[1] or + py < bb[2] or py > bb[3] or + pz < bb[4] or pz > bb[5]): + return False + + # --- expensive enclosed-point test --- + return self._enclosed_test(poly, point) + + def point_in_mesh(self, point: np.ndarray, key: str) -> bool: + """Check if a 3D point is inside a mesh (no fast rejection).""" + poly = self.get_polydata(key) + if poly is None or poly.GetNumberOfCells() == 0: + return False + return self._enclosed_test(poly, point) + + @staticmethod + def _enclosed_test(poly: vtk.vtkPolyData, point: np.ndarray) -> bool: + pts = vtk.vtkPoints() + pts.InsertNextPoint(float(point[0]), float(point[1]), float(point[2])) + test_pd = vtk.vtkPolyData() + test_pd.SetPoints(pts) + + enc = vtk.vtkSelectEnclosedPoints() + enc.SetInputData(test_pd) + enc.SetSurfaceData(poly) + enc.SetTolerance(0.0001) + enc.Update() + return bool(enc.IsInside(0)) + + # ------------------------------------------------------------------ + # public: display + # ------------------------------------------------------------------ + + def show(self, key: str): + if key == self._visible: + return + self.hide() + act = self._actor_for(key) + if act is not None: + self.ren.AddActor(act) + self._visible = key + name = self.get_region_name(key) + print(f"[flow mesh] showing: {name} ({key})") + if self.win is not None: + self.win.Render() + + def hide(self): + if self._visible and self._visible in self._actors: + try: + self.ren.RemoveActor(self._actors[self._visible]) + except Exception: + pass + self._visible = None + if self.win is not None: + self.win.Render() + + def cycle(self, step: int = +1): + if not self._order: + return + if self._visible not in self._order: + idx = 0 if step >= 0 else len(self._order) - 1 + else: + idx = (self._order.index(self._visible) + step) % len(self._order) + self.show(self._order[idx]) + + def set_opacity(self, mul: float): + self._opacity = float(np.clip(self._opacity * float(mul), 0.05, 1.0)) + for k, a in self._actors.items(): + if k == "brain_outline": + a.GetProperty().SetOpacity(max(0.1, self._opacity * 0.7)) + else: + a.GetProperty().SetOpacity(self._opacity) + if self.win is not None: + self.win.Render() + + # ------------------------------------------------------------------ + # public: geometry queries + # ------------------------------------------------------------------ + + def get_mesh_center(self, key: str) -> np.ndarray | None: + poly = self.get_polydata(key) + if poly is None: + return None + com = vtk.vtkCenterOfMass() + com.SetInputData(poly) + com.SetUseScalarsAsWeights(False) + com.Update() + return np.array(com.GetCenter(), dtype=np.float32) + + def get_mesh_bounds(self, key: str) -> np.ndarray | None: + poly = self.get_polydata(key) + if poly is None: + return None + return np.array(poly.GetBounds(), dtype=np.float32) + + def get_bb_volume(self, key: str) -> float: + """Return cached bounding-box volume (or inf if unknown).""" + return self._bb_volumes.get(key, float('inf')) diff --git a/mindVisualizer/src/probe.py b/mindVisualizer/src/probe.py new file mode 100644 index 0000000000000000000000000000000000000000..470f35efa115a38e084ce4ff430105335e8313e7 --- /dev/null +++ b/mindVisualizer/src/probe.py @@ -0,0 +1,800 @@ +"""Flow probe system: single probe, multi-probe, and branching support. + +The probe is advected by the same velocity field as the particles. Its trajectory +is recorded and can be analyzed for brain region transitions. + +Features: + - Single probe: click to place, follows mean flow + - Multi-probe: initialize N probes in a local neighborhood + - Branching: when MDN components are highly uncertain (50/50 split), spawn + a ghost probe that follows the dominant alternative component + - Live region highlighting in RED (ghost highlights more transparent) +""" + +import numpy as np +import vtk + + +class FlowProbe: + """A single probe that follows the flow field.""" + + def __init__(self, ren: vtk.vtkRenderer, amin: np.ndarray, amax: np.ndarray, + color=(0.0, 1.0, 0.3), opacity=0.9, ghost=False, label=""): + self.ren = ren + self.amin = amin.astype(np.float32) + self.amax = amax.astype(np.float32) + self.active = False + self.position = None + self.path: list[np.ndarray] = [] + self.speeds: list[float] = [] + self.raw_field_mags: list[float] = [] # field magnitude (independent of speed scale) + self._max_path_len = 50000 + self._mesh_overlay = None + self._highlighted_regions: set[str] = set() + self._highlight_actors: dict[str, vtk.vtkActor] = {} + self._check_interval = 30 + self._step_counter = 0 + self.current_regions: set[str] = set() + self._boundary_check = None + self._on_region_change = None + self.ghost = ghost + self.label = label + self._color = color + self.steps_alive = 0 + self._stuck_counter = 0 + self._stuck_threshold = 150 # steps with near-zero movement before warning + self._stuck_warned = False + self._stuck_eps = 1e-6 # minimum displacement per step + # Debounce: require N consecutive detections before entering, N misses before leaving + self._debounce_enter = 3 # consecutive checks to confirm entry + self._debounce_leave = 5 # consecutive misses to confirm exit + self._region_hit_count: dict[str, int] = {} # key -> consecutive hit count + self._region_miss_count: dict[str, int] = {} # key -> consecutive miss count + self._nearest_max_dist_mm = 5.0 # hard limit for nearest-region snapping (mm) + + diag = float(np.linalg.norm(amax - amin)) + marker_alpha = 0.4 if ghost else 1.0 + trail_alpha = 0.35 if ghost else 0.9 + + # Probe marker (sphere) + self._sphere = vtk.vtkSphereSource() + self._sphere.SetRadius(0.012 * diag if not ghost else 0.008 * diag) + self._sphere.SetThetaResolution(16) + self._sphere.SetPhiResolution(16) + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputConnection(self._sphere.GetOutputPort()) + self.marker_actor = vtk.vtkActor() + self.marker_actor.SetMapper(mapper) + self.marker_actor.GetProperty().SetColor(*color) + self.marker_actor.GetProperty().SetOpacity(marker_alpha) + self.marker_actor.GetProperty().LightingOff() + self.marker_actor.VisibilityOff() + ren.AddActor(self.marker_actor) + + # Trail line + self._trail_points = vtk.vtkPoints() + self._trail_cells = vtk.vtkCellArray() + self._trail_pd = vtk.vtkPolyData() + self._trail_pd.SetPoints(self._trail_points) + self._trail_pd.SetLines(self._trail_cells) + trail_mapper = vtk.vtkPolyDataMapper() + trail_mapper.SetInputData(self._trail_pd) + self.trail_actor = vtk.vtkActor() + self.trail_actor.SetMapper(trail_mapper) + self.trail_actor.GetProperty().SetColor(*color) + self.trail_actor.GetProperty().SetLineWidth(3.0 if not ghost else 2.0) + self.trail_actor.GetProperty().SetOpacity(trail_alpha) + self.trail_actor.GetProperty().LightingOff() + self.trail_actor.VisibilityOff() + ren.AddActor(self.trail_actor) + + def set_mesh_overlay(self, mesh_overlay): + self._mesh_overlay = mesh_overlay + + def set_boundary_check(self, fn): + self._boundary_check = fn + + def set_on_region_change(self, fn): + self._on_region_change = fn + + def place(self, position: np.ndarray): + """Place probe at position and start recording.""" + self.position = np.array(position, dtype=np.float32).ravel()[:3] + + # Boundary validation + if self._boundary_check is not None and not self._boundary_check(self.position): + found = False + diag = float(np.linalg.norm(self.amax - self.amin)) + for scale in [0.01, 0.02, 0.05, 0.1, 0.2]: + for _ in range(50): + offset = np.random.randn(3).astype(np.float32) * scale * diag + candidate = np.clip(self.position + offset, self.amin, self.amax) + if self._boundary_check(candidate): + self.position = candidate + found = True + break + if found: + break + if not found: + self.position = (self.amin + self.amax) / 2.0 + print(f"[probe{self.label}] Could not find valid position, using domain center") + + self.path = [self.position.copy()] + self.speeds = [0.0] + self.raw_field_mags = [0.0] + self.active = True + self._step_counter = 0 + self.steps_alive = 0 + self._stuck_counter = 0 + self._stuck_warned = False + + # Reset trail + self._trail_points = vtk.vtkPoints() + self._trail_cells = vtk.vtkCellArray() + self._trail_points.InsertNextPoint(*self.position.tolist()) + self._trail_pd.SetPoints(self._trail_points) + self._trail_pd.SetLines(self._trail_cells) + self._trail_pd.Modified() + + self.marker_actor.VisibilityOn() + self.marker_actor.SetPosition(*self.position.tolist()) + self.trail_actor.VisibilityOn() + + self._update_region_highlights() + tag = " (ghost)" if self.ghost else "" + print(f"[probe{self.label}{tag}] placed at " + f"({self.position[0]:.2f}, {self.position[1]:.2f}, {self.position[2]:.2f})") + + def step(self, sampler, dt_step: float): + """Advect one step using sampler.""" + if not self.active or self.position is None: + return + V = sampler.sample_vec(self.position[None, :]) + velocity = V[0] + raw_mag = float(np.linalg.norm(velocity)) + + new_pos = self.position + velocity * dt_step + new_pos = np.clip(new_pos, self.amin, self.amax) + + # Boundary constraint + if self._boundary_check is not None and not self._boundary_check(new_pos): + half_pos = self.position + velocity * dt_step * 0.5 + half_pos = np.clip(half_pos, self.amin, self.amax) + if self._boundary_check(half_pos): + new_pos = half_pos + else: + return # hit boundary + + self.position = new_pos.astype(np.float32) + speed = float(np.linalg.norm(self.position - self.path[-1])) if self.path else 0.0 + + # Stuck / weak flow detection + if speed < self._stuck_eps and raw_mag < self._stuck_eps: + self._stuck_counter += 1 + if self._stuck_counter >= self._stuck_threshold and not self._stuck_warned: + self._stuck_warned = True + tag = f" (ghost)" if self.ghost else "" + print(f"\n[probe{self.label}{tag}] Flow is very weak or has ended here. " + f"The probe is stuck at ({self.position[0]:.1f}, {self.position[1]:.1f}, {self.position[2]:.1f}).") + print(f"[probe{self.label}{tag}] Try placing the probe deeper in the brain " + f"where flow is stronger (press 'c' to clear, then 'g' + click).\n") + else: + self._stuck_counter = 0 + + if len(self.path) < self._max_path_len: + self.path.append(self.position.copy()) + self.speeds.append(speed) + self.raw_field_mags.append(raw_mag) + + self.marker_actor.SetPosition(*self.position.tolist()) + + # Append to trail + idx = self._trail_points.InsertNextPoint(*self.position.tolist()) + if idx > 0: + self._trail_cells.InsertNextCell(2) + self._trail_cells.InsertCellPoint(idx - 1) + self._trail_cells.InsertCellPoint(idx) + self._trail_points.Modified() + self._trail_cells.Modified() + self._trail_pd.Modified() + + # Periodic region check + self._step_counter += 1 + self.steps_alive += 1 + if self._step_counter % self._check_interval == 0: + self._update_region_highlights() + + def _point_in_bbox(self, point, bounds): + """Fast bounding-box containment check. bounds is (xmin,xmax,ymin,ymax,zmin,zmax).""" + return (bounds[0] <= point[0] <= bounds[1] and + bounds[2] <= point[1] <= bounds[3] and + bounds[4] <= point[2] <= bounds[5]) + + def _update_region_highlights(self): + if self._mesh_overlay is None or self.position is None: + return + + # --- Raw detection (what region key is at the probe right now?) --- + raw_key = None + if hasattr(self._mesh_overlay, 'get_region_at_point'): + raw_key = self._mesh_overlay.get_region_at_point(self.position) + if raw_key is None and hasattr(self._mesh_overlay, 'find_nearest_region'): + raw_key = self._mesh_overlay.find_nearest_region( + self.position, search_radius=2, + max_distance_mm=self._nearest_max_dist_mm) + else: + for key in self._mesh_overlay.get_all_region_keys(): + if hasattr(self._mesh_overlay, 'fast_point_in_mesh'): + if self._mesh_overlay.fast_point_in_mesh(self.position, key): + raw_key = key + break + elif self._mesh_overlay.point_in_mesh(self.position, key): + raw_key = key + break + + raw_detected = {raw_key} if raw_key else set() + + # --- Debounce: require consecutive detections to enter, consecutive misses to leave --- + # Update hit/miss counters for detected key + for key in raw_detected: + self._region_hit_count[key] = self._region_hit_count.get(key, 0) + 1 + self._region_miss_count.pop(key, None) + + # Update miss counters for keys that were NOT detected this tick + for key in list(self._region_hit_count.keys()): + if key not in raw_detected: + self._region_miss_count[key] = self._region_miss_count.get(key, 0) + 1 + self._region_hit_count[key] = 0 + + # Determine stable set: regions that passed the entry threshold + # and have not yet exceeded the leave threshold + new_regions = set() + for key in set(list(self._region_hit_count.keys()) + + list(self._highlighted_regions)): + hits = self._region_hit_count.get(key, 0) + misses = self._region_miss_count.get(key, 0) + if key in self._highlighted_regions: + # Already highlighted — keep it unless misses exceed threshold + if misses < self._debounce_leave: + new_regions.add(key) + else: + # Not yet highlighted — add if hits exceed entry threshold + if hits >= self._debounce_enter: + new_regions.add(key) + + # Clean up stale counters + for key in list(self._region_miss_count.keys()): + if self._region_miss_count[key] > self._debounce_leave + 2: + self._region_miss_count.pop(key, None) + self._region_hit_count.pop(key, None) + + # --- Apply changes (enter/leave) --- + left = self._highlighted_regions - new_regions + left_names = [] + for key in left: + if key in self._highlight_actors: + try: + self.ren.RemoveActor(self._highlight_actors[key]) + except Exception: + pass + del self._highlight_actors[key] + name = self._mesh_overlay.get_region_name(key) + if hasattr(self._mesh_overlay, 'get_hemisphere_label'): + hemi = self._mesh_overlay.get_hemisphere_label(key, self.position) + if hemi: + name = f"{name} ({hemi})" + left_names.append(name) + tag = " [branch]" if self.ghost else "" + print(f"[probe{self.label}] LEFT: {name}{tag}") + + entered = new_regions - self._highlighted_regions + entered_names = [] + highlight_opacity = 0.12 if self.ghost else 0.25 + for key in entered: + poly = None + if hasattr(self._mesh_overlay, 'get_hemisphere_polydata'): + poly = self._mesh_overlay.get_hemisphere_polydata(key, self.position) + if poly is None: + poly = self._mesh_overlay.get_polydata(key) + if poly is not None: + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(poly) + actor = vtk.vtkActor() + actor.SetMapper(mapper) + actor.GetProperty().SetColor(1.0, 0.6, 0.0) # orange (all regions) + actor.GetProperty().SetOpacity(highlight_opacity) + actor.GetProperty().LightingOff() + self.ren.AddActor(actor) + self._highlight_actors[key] = actor + name = self._mesh_overlay.get_region_name(key) + if hasattr(self._mesh_overlay, 'get_hemisphere_label'): + hemi = self._mesh_overlay.get_hemisphere_label(key, self.position) + if hemi: + name = f"{name} ({hemi})" + entered_names.append(name) + tag = " [branch]" if self.ghost else "" + # Detailed entry log with position context + pos_detail = "" + if self.position is not None and self._mesh_overlay is not None: + try: + center = self._mesh_overlay.get_mesh_center(key) + bounds = self._mesh_overlay.get_mesh_bounds(key) + if center is not None and bounds is not None: + extent = [bounds[1]-bounds[0], bounds[3]-bounds[2], + bounds[5]-bounds[4]] + char_size = sum(extent) / 3.0 + dist = float(np.linalg.norm(self.position - center)) + depth = max(0.0, 1.0 - min(dist / (char_size * 0.5), 1.0)) + pos_parts = [] + diff = self.position - center + if abs(diff[2]) > extent[2] * 0.15: + pos_parts.append("dorsal" if diff[2] > 0 else "ventral") + if abs(diff[0]) > extent[0] * 0.15: + pos_parts.append("lateral-R" if diff[0] > 0 else "lateral-L") + if abs(diff[1]) > extent[1] * 0.15: + pos_parts.append("anterior" if diff[1] > 0 else "posterior") + pos_str = "-".join(pos_parts) if pos_parts else "central" + pos_detail = f" [{pos_str}, depth={depth:.0%}]" + except Exception: + pass + print(f"[probe{self.label}{tag}] ENTERED: {name}{pos_detail}") + + self._highlighted_regions = new_regions + self.current_regions = new_regions + + # --- Extra parcellation subregion highlighting --- + # Extra parcellation subregions also get orange, same as main regions. + if (self._mesh_overlay is not None and + hasattr(self._mesh_overlay, '_extra') and + self._mesh_overlay._extra is not None and + self.position is not None and new_regions): + try: + hier = self._mesh_overlay.get_hierarchical_regions_at_point(self.position) + sub = hier.get("subregion") + sub_mesh = hier.get("subregion_mesh") + sub_key = f"_extra_{sub['label_id']}" if sub else None + + # Remove old subregion highlight if changed + old_sub_key = getattr(self, '_current_subregion_key', None) + if old_sub_key and old_sub_key != sub_key: + if old_sub_key in self._highlight_actors: + try: + self.ren.RemoveActor(self._highlight_actors[old_sub_key]) + except Exception: + pass + del self._highlight_actors[old_sub_key] + + # Add new subregion highlight (orange, same as all regions) + if sub_key and sub_mesh and sub_key not in self._highlight_actors: + display_mesh = sub_mesh + try: + bounds = [0.0] * 6 + sub_mesh.GetBounds(bounds) + x_extent = bounds[1] - bounds[0] + if x_extent > 10.0: + x_mid = (bounds[0] + bounds[1]) / 2.0 + plane = vtk.vtkPlane() + plane.SetOrigin(x_mid, 0, 0) + if self.position[0] >= x_mid: + plane.SetNormal(1, 0, 0) + else: + plane.SetNormal(-1, 0, 0) + clipper = vtk.vtkClipPolyData() + clipper.SetInputData(sub_mesh) + clipper.SetClipFunction(plane) + clipper.SetInsideOut(False) + clipper.Update() + clipped = clipper.GetOutput() + if clipped and clipped.GetNumberOfPoints() > 0: + display_mesh = clipped + except Exception: + pass + + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputData(display_mesh) + actor = vtk.vtkActor() + actor.SetMapper(mapper) + actor.GetProperty().SetColor(1.0, 0.6, 0.0) # orange + actor.GetProperty().SetOpacity(0.3) + actor.GetProperty().LightingOff() + self.ren.AddActor(actor) + self._highlight_actors[sub_key] = actor + tag = " [branch]" if self.ghost else "" + hemi_str = "" + if self.position is not None and self.position[0] >= 0: + hemi_str = " (right hemisphere)" + elif self.position is not None: + hemi_str = " (left hemisphere)" + print(f"[probe{self.label}{tag}] SUBREGION: {sub['name']}{hemi_str}") + + # Hide coarser main-atlas regions when we have a finer subregion + if sub_key and sub_key in self._highlight_actors: + hidden = set() + for rkey in new_regions: + if rkey in self._highlight_actors and not rkey.startswith("_extra_"): + self._highlight_actors[rkey].VisibilityOff() + hidden.add(rkey) + self._hidden_orange_keys = hidden + elif not sub_key: + for rkey in getattr(self, '_hidden_orange_keys', set()): + if rkey in self._highlight_actors: + self._highlight_actors[rkey].VisibilityOn() + self._hidden_orange_keys = set() + + self._current_subregion_key = sub_key + except Exception: + pass + + # --- Red hotspot: clip mesh near probe position --- + # Instead of coloring an entire region red, clip the mesh surface + # within a sphere around the probe and show that patch in red. + self._update_hotspot() + + if self._on_region_change and (entered_names or left_names): + self._on_region_change(entered_names, left_names, + is_ghost=self.ghost, label=self.label) + + def _update_hotspot(self): + """Color highlighted region meshes with a distance-based heatmap. + + Vertices near the probe are red, fading smoothly to orange further away. + Uses per-vertex RGBA scalars — no clipping, no extra actors. + """ + from vtkmodules.util.numpy_support import vtk_to_numpy, numpy_to_vtk + + if self.position is None or not self._highlight_actors: + return + + probe_pos = self.position.astype(np.float64) + fade_radius = 15.0 # mm — distance over which red fades to orange + + for key, actor in self._highlight_actors.items(): + mapper = actor.GetMapper() + if mapper is None: + continue + poly = mapper.GetInput() + if poly is None or poly.GetNumberOfPoints() < 3: + continue + + pts_vtk = poly.GetPoints() + if pts_vtk is None: + continue + verts = vtk_to_numpy(pts_vtk.GetData()).astype(np.float64) + + # Distance from each vertex to probe + dists = np.linalg.norm(verts - probe_pos, axis=1) + t = np.clip(dists / fade_radius, 0.0, 1.0) # 0=at probe, 1=far + + # Color gradient: red (1,0,0) at probe → orange (1,0.6,0) far + r = np.full(len(t), 255, np.uint8) + g = (t * 0.6 * 255).astype(np.uint8) + b = np.zeros(len(t), np.uint8) + + # Alpha: brighter near probe, dimmer far away + base_opacity = 0.45 if not self.ghost else 0.2 + far_opacity = 0.2 if not self.ghost else 0.08 + alpha_f = far_opacity + (base_opacity - far_opacity) * (1.0 - t) + a = (np.clip(alpha_f, 0.0, 1.0) * 255).astype(np.uint8) + + rgba = np.column_stack([r, g, b, a]) + scalars = numpy_to_vtk(rgba, deep=True) + scalars.SetNumberOfComponents(4) + scalars.SetName("HeatmapColors") + poly.GetPointData().SetScalars(scalars) + + mapper.SetColorModeToDirectScalars() + mapper.SetScalarModeToUsePointData() + mapper.ScalarVisibilityOn() + actor.GetProperty().LightingOff() + # Override the flat color — let scalars drive everything + actor.GetProperty().SetOpacity(1.0) + poly.Modified() + + def clear(self): + self.active = False + self.position = None + self.path = [] + self.speeds = [] + self.raw_field_mags = [] + self._step_counter = 0 + self.steps_alive = 0 + self.marker_actor.VisibilityOff() + self.trail_actor.VisibilityOff() + self._trail_points = vtk.vtkPoints() + self._trail_cells = vtk.vtkCellArray() + self._trail_pd.SetPoints(self._trail_points) + self._trail_pd.SetLines(self._trail_cells) + self._trail_pd.Modified() + for key, actor in self._highlight_actors.items(): + try: + self.ren.RemoveActor(actor) + except Exception: + pass + self._highlight_actors.clear() + self._highlighted_regions.clear() + self.current_regions.clear() + + def destroy(self): + """Remove all actors from renderer.""" + self.clear() + try: + self.ren.RemoveActor(self.marker_actor) + except Exception: + pass + try: + self.ren.RemoveActor(self.trail_actor) + except Exception: + pass + + def get_path_array(self) -> np.ndarray: + if not self.path: + return np.zeros((0, 3), np.float32) + return np.array(self.path, dtype=np.float32) + + def get_speeds_array(self) -> np.ndarray: + if not self.speeds: + return np.zeros(0, np.float32) + return np.array(self.speeds, dtype=np.float32) + + def get_field_mags_array(self) -> np.ndarray: + if not self.raw_field_mags: + return np.zeros(0, np.float32) + return np.array(self.raw_field_mags, dtype=np.float32) + + +class ProbeSystem: + """Manages single/multi probes and branching behavior. + + Modes: + - single: one probe following mean flow + - multi: N probes in local neighborhood, all following mean flow + - branching: when PI uncertainty is high, spawn ghost probes + + For state-propagation mode, defaults to multi(4) + branching. + """ + + def __init__(self, ren: vtk.vtkRenderer, win: vtk.vtkRenderWindow, + amin: np.ndarray, amax: np.ndarray): + self.ren = ren + self.win = win + self.amin = amin.astype(np.float32) + self.amax = amax.astype(np.float32) + self.probes: list[FlowProbe] = [] + self.ghost_probes: list[FlowProbe] = [] + self._mesh_overlay = None + self._boundary_check = None + self._on_region_change = None + self._branching_enabled = False + self._multi_count = 1 + self._branch_threshold = 0.35 # max ratio between top 2 PI components + self._branch_min_pi = 0.25 # min weight of 2nd component + self._branch_check_interval = 50 + self._branch_step_counter = 0 + self._ghost_color = (0.4, 0.7, 1.0) # pale blue for ghosts + self._pi_field = None + self._mus_samplers = None + + def set_mesh_overlay(self, overlay): + self._mesh_overlay = overlay + + def set_boundary_check(self, fn): + self._boundary_check = fn + + def set_on_region_change(self, fn): + self._on_region_change = fn + + def set_branching(self, enabled: bool, pi_field=None, mus_samplers=None): + """Enable/disable branching. + + Args: + enabled: toggle branching + pi_field: the PI weight grid (G,G,G,K) numpy array + mus_samplers: list of TriLinearSampler for each component + """ + self._branching_enabled = enabled + self._pi_field = pi_field + self._mus_samplers = mus_samplers + print(f"[probe-system] branching {'ON' if enabled else 'OFF'}") + + def set_multi_count(self, n: int): + self._multi_count = max(1, n) + print(f"[probe-system] multi-probe count: {self._multi_count}") + + @property + def active(self) -> bool: + return any(p.active for p in self.probes) + + @property + def path(self): + """Return path of first probe (for backward compat).""" + return self.probes[0].path if self.probes else [] + + def place(self, position: np.ndarray): + """Place probe(s) at position.""" + self.clear() + diag = float(np.linalg.norm(self.amax - self.amin)) + jitter_scale = 0.04 * diag + + for i in range(self._multi_count): + if i == 0: + pos = position.copy() + label = "" if self._multi_count == 1 else f"#{i+1}" + else: + offset = np.random.randn(3).astype(np.float32) * jitter_scale + pos = np.clip(position + offset, self.amin, self.amax) + label = f"#{i+1}" + + probe = FlowProbe(self.ren, self.amin, self.amax, + color=(0.0, 1.0, 0.3), ghost=False, label=label) + if self._mesh_overlay: + probe.set_mesh_overlay(self._mesh_overlay) + if self._boundary_check: + probe.set_boundary_check(self._boundary_check) + if self._on_region_change: + probe.set_on_region_change(self._on_region_change) + probe.place(pos) + self.probes.append(probe) + + def step(self, sampler, dt_step: float, pi_sampler=None): + """Step all probes.""" + for p in self.probes: + if p.active: + p.step(sampler, dt_step) + + for g in self.ghost_probes: + if g.active: + # Ghost probes follow their specific component sampler + comp_sampler = getattr(g, '_comp_sampler', sampler) + g.step(comp_sampler, dt_step) + + # Prune ghost probes that have been alive > 200 steps but did not diverge + diag = float(np.linalg.norm(self.amax - self.amin)) + prune_dist = 0.03 * diag + to_remove = [] + for g in self.ghost_probes: + if not g.active or g.position is None: + continue + if g.steps_alive > 200: + for p in self.probes: + if not p.active or p.position is None: + continue + dist = float(np.linalg.norm(g.position - p.position)) + if dist < prune_dist: + print("[branch] pruned ghost - did not diverge") + to_remove.append(g) + break + for g in to_remove: + g.destroy() + self.ghost_probes.remove(g) + + # Check for branching + if self._branching_enabled: + self._branch_step_counter += 1 + if self._branch_step_counter % self._branch_check_interval == 0: + self._check_branching(sampler, dt_step) + + def _check_branching(self, mean_sampler, dt_step): + """Check if any active probe is at a high-uncertainty location and branch.""" + if self._pi_field is None or self._mus_samplers is None: + return + if not self.probes: + return + + from .field_loader import TriLinearSampler + + for p in self.probes: + if not p.active or p.position is None: + continue + # Sample PI weights at probe position + pos = p.position[None, :] + pi_vals = mean_sampler.sample_vec(pos) # dummy, we need PI + # Actually sample PI field directly + try: + K = self._pi_field.shape[-1] + weights = np.zeros(K, np.float32) + for k in range(K): + pi_k = self._pi_field[..., k:k+1] + # Use sampler to interpolate + w = mean_sampler.sample_scalar( + self._pi_field[..., k], + p.position[None, :] + ) + weights[k] = float(w[0]) + except Exception: + continue + + # Normalize + ws = weights.sum() + if ws <= 0: + continue + weights /= ws + + # Sort to find top 2 + sorted_idx = np.argsort(weights)[::-1] + w1 = weights[sorted_idx[0]] + w2 = weights[sorted_idx[1]] if len(sorted_idx) > 1 else 0.0 + + # Check if uncertain enough + if w2 < self._branch_min_pi: + continue + ratio = w2 / max(w1, 1e-9) + if ratio < self._branch_threshold: + continue + + # Check we haven't already branched from near this position + already_branched = False + for g in self.ghost_probes: + if g.active and g.path: + d = np.linalg.norm(g.path[0] - p.position) + if d < 0.02 * float(np.linalg.norm(self.amax - self.amin)): + already_branched = True + break + if already_branched: + continue + + # Branch! Create ghost probe following 2nd component + comp_idx = int(sorted_idx[1]) + if comp_idx >= len(self._mus_samplers): + continue + + ghost = FlowProbe(self.ren, self.amin, self.amax, + color=self._ghost_color, ghost=True, + label=f"~branch(comp{comp_idx+1})") + if self._mesh_overlay: + ghost.set_mesh_overlay(self._mesh_overlay) + if self._boundary_check: + ghost.set_boundary_check(self._boundary_check) + if self._on_region_change: + ghost.set_on_region_change(self._on_region_change) + ghost._comp_sampler = self._mus_samplers[comp_idx] + ghost.place(p.position.copy()) + self.ghost_probes.append(ghost) + + print(f"[branch] SPLIT at ({p.position[0]:.2f}, {p.position[1]:.2f}, " + f"{p.position[2]:.2f}): comp{sorted_idx[0]+1}={w1:.2f} vs " + f"comp{comp_idx+1}={w2:.2f}") + if self._on_region_change: + self._on_region_change( + [f"BRANCH: comp{comp_idx+1} (weight={w2:.2f})"], [], + is_ghost=True, label="branch" + ) + + def clear(self): + """Clear all probes.""" + for p in self.probes: + p.destroy() + for g in self.ghost_probes: + g.destroy() + self.probes.clear() + self.ghost_probes.clear() + self._branch_step_counter = 0 + + def freeze(self): + """Stop all probes from moving (keep path and highlights intact).""" + for p in self.probes: + p.active = False + for g in self.ghost_probes: + g.active = False + print("[probe-system] probes frozen") + + def get_all_probes(self) -> list[FlowProbe]: + """Return all probes (main + ghost) for analysis.""" + return self.probes + self.ghost_probes + + def get_primary_probe(self) -> FlowProbe | None: + """Return the first active probe.""" + for p in self.probes: + if p.active: + return p + return None + + def get_all_paths(self) -> list[tuple[np.ndarray, bool, str]]: + """Return [(path_array, is_ghost, label), ...] for all probes.""" + result = [] + for p in self.probes: + if p.path: + result.append((p.get_path_array(), False, p.label)) + for g in self.ghost_probes: + if g.path: + result.append((g.get_path_array(), True, g.label)) + return result diff --git a/requirements.txt b/requirements.txt index ac76950680100eac6750b0ccb02fd2c5cddd5e3f..fad12d963fe769380cf09bc4b26ce3cb1f4adab4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,8 @@ numpy==2.2.1 scipy==1.15.1 python-dotenv==1.0.1 openai==1.59.6 +vtk==9.3.1 +certifi==2024.12.14 +langchain-openai==0.2.14 +langchain-community==0.3.13 +faiss-cpu==1.9.0.post1