novo modelo 3d

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-11 15:50:15 -03:00
1 parent bc7e77b007
commit 08cb8c426f
11 files changed
+301 -256

No files matched your search

+1 -10
View File
@@ -3,17 +3,8 @@
# Alpine essa dependência é bem mais dolorosa de resolver.
FROM node:20-bookworm
RUN apt-get update \
&& apt-get install -y --no-install-recommends assimp-utils ca-certificates python3 python3-venv \
&& apt-get install -y --no-install-recommends assimp-utils ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Ambiente Python isolado só pra buscar as organelas reais da célula
# (fetch-cell-organelles.py) — cloud-volume/trimesh não têm nada a ver com
# o resto do pipeline (100% Node/glTF-transform), então ficam num venv
# próprio em vez de poluir o Python do sistema.
RUN python3 -m venv /opt/venv-cell \
&& /opt/venv-cell/bin/pip install --no-cache-dir --upgrade pip \
&& /opt/venv-cell/bin/pip install --no-cache-dir cloud-volume trimesh
WORKDIR /app
COPY package.json ./
RUN npm install
+2 -4
View File
@@ -9,12 +9,10 @@
"extract:manifest": "node scripts/extract-manifest.mjs",
"build:tegumentar": "node seed/build-manifest.mjs",
"build:microscopy": "node seed/build-manifest-microscopy.mjs",
"cell:fetch": "/opt/venv-cell/bin/python scripts/fetch-cell-organelles.py",
"cell:model": "node scripts/build-cell-model.mjs",
"cell:manifest": "node seed/build-manifest-cell.mjs",
"build:cell": "node seed/build-manifest-cell.mjs",
"merge:manifest": "node scripts/merge-manifests.mjs",
"seed:db": "node scripts/seed-db.mjs",
"run:all": "npm run fetch:models && npm run convert:models && npm run extract:manifest && npm run build:tegumentar && npm run build:microscopy && npm run cell:fetch && npm run cell:model && npm run cell:manifest && npm run merge:manifest && npm run seed:db"
"run:all": "npm run fetch:models && npm run convert:models && npm run extract:manifest && npm run build:tegumentar && npm run build:microscopy && npm run build:cell && npm run merge:manifest && npm run seed:db"
},
"dependencies": {
"@gltf-transform/core": "^4.0.4",
@@ -1,56 +0,0 @@
// Lê citologia-raw.glb (gerado por fetch-cell-organelles.py a partir de
// dados REAIS de FIB-SEM do Janelia OpenOrganelle/CellMap — ver aquele
// script para a curadoria completa e a licença CC BY 4.0), simplifica e
// comprime com o MESMO recipe já usado pras malhas do Z-Anatomy em
// extract-manifest.mjs, e salva o resultado final em seed/models/citologia.glb.
//
// Diferente do Z-Anatomy, aqui não precisamos marcar nós excluídos nem
// resolver nomes duplicados — o .glb de entrada já tem exatamente 5 nós,
// um por organela, com o nome final (ex: "celula-nucleo") definido no
// próprio script Python. weld/simplify/dedup/meshopt só afetam a
// geometria, não o nome dos nós, então o "meshName" no manifesto
// (build-manifest-cell.mjs) pode confiar que bate exatamente com isso.
import { NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { prune, dedup, weld, simplify, meshopt } from "@gltf-transform/functions";
import { MeshoptSimplifier, MeshoptEncoder } from "meshoptimizer";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { mkdirSync } from "node:fs";
await MeshoptSimplifier.ready;
await MeshoptEncoder.ready;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const IN_PATH = path.resolve(__dirname, "../converted/citologia/citologia-raw.glb");
const OUT_DIR = path.resolve(__dirname, "../seed/models");
mkdirSync(OUT_DIR, { recursive: true });
const io = new NodeIO()
.registerExtensions(ALL_EXTENSIONS)
.registerDependencies({ "meshopt.encoder": MeshoptEncoder });
console.log(`Lendo ${IN_PATH}...`);
const doc = await io.read(IN_PATH);
const meshNames = doc
.getRoot()
.listNodes()
.map((n) => n.getName())
.filter(Boolean);
console.log(`Organelas encontradas: ${meshNames.join(", ")}`);
await doc.transform(
weld(),
// As organelas maiores (membrana, RE) chegam com bem mais geometria do
// que o necessário pra estudo interativo — mesmo ratio conservador já
// usado no resto do atlas.
simplify({ simplifier: MeshoptSimplifier, ratio: 0.5, error: 0.01 }),
prune(),
dedup(),
meshopt({ encoder: MeshoptEncoder, level: "medium" }),
);
const outPath = path.join(OUT_DIR, "citologia.glb");
await io.write(outPath, doc);
console.log(`✓ ${outPath} salvo.`);
@@ -1,126 +0,0 @@
#!/usr/bin/env python3
# Busca organelas REAIS de uma célula HeLa inteira, reconstruída em 3D a
# partir de FIB-SEM (microscopia eletrônica volumétrica) e segmentada por
# machine learning pelo projeto CellMap/COSEM do Janelia Research Campus
# (HHMI) — publicado como OpenOrganelle, CC BY 4.0.
# https://openorganelle.janelia.org/ | dataset "jrc_hela-2"
# DOI da coleção: https://doi.org/10.25378/janelia.c.7456966
#
# POR QUE ISSO E NÃO GEOMETRIA PROCEDURAL: é o mesmo princípio já usado pro
# resto do atlas (malhas reais do Z-Anatomy) — aqui não existe um "Z-Anatomy
# da célula" pronto pra converter de FBX, mas o Janelia publica a segmentação
# 3D real como malhas no formato Neuroglancer "precomputed" (multi-resolução,
# comprimido com Draco). Este script usa a biblioteca `cloud-volume` (Seung
# Lab) pra baixar e decodificar essas malhas direto do bucket público, sem
# fabricar nenhuma forma.
#
# Escolhas de curadoria (documentadas pra poder ajustar depois):
# - Cada organela é uma "camada" de segmentação própria no dataset
# (nucleus_seg, mito_seg, er_seg, golgi_seg, pm_seg), com um ID por
# instância. Pegamos a maior instância de núcleo/membrana (a célula
# mais completa no campo de visão) e TODAS as instâncias de
# mitocôndria/RE/Golgi (são realmente múltiplas organelas de verdade,
# não fragmentos de ruído — juntar todas reproduz a distribuição real).
# - LOD (nível de detalhe) escolhido por organela: mais fino pra
# estruturas pequenas/poucas (núcleo, Golgi), mais grosso pras que têm
# MUITA geometria agregada (mitocôndrias, RE) — o pipeline Node.js
# (build-cell-model.mjs) simplifica/comprime tudo de qualquer forma.
# - Todas as peças são recentralizadas usando o MESMO deslocamento (o
# centro da membrana plasmática) — cada organela NÃO pode ser
# recentralizada individualmente, isso destruiria a posição relativa
# real dentro da célula.
# - Escala: nanômetros (unidade nativa do dataset) → micrômetros (÷1000),
# só por conveniência numérica — a câmera do app se ajusta ao tamanho
# real do conteúdo de qualquer forma (ver Scene.tsx/CameraController).
#
# Vesículas (~40 mil instâncias) e endossomos (~5.600) ficaram de fora
# desta primeira versão por volume de dados — dá pra adicionar depois como
# mais uma organela seguindo exatamente o mesmo padrão abaixo.
import os
import time
import numpy as np
import cloudvolume as cv
import trimesh
DATASET = "jrc_hela-2"
BASE = f"precomputed://https://janelia-cosem-datasets.s3.amazonaws.com/{DATASET}/neuroglancer/mesh"
OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "converted", "citologia")
# (nome do nó no glb, camada de segmentação, IDs de instância, LOD)
GROUPS = [
("celula-membrana-plasmatica", "pm_seg", [2], 4),
("celula-nucleo", "nucleus_seg", [1], 3),
("celula-reticulo-endoplasmatico", "er_seg", list(range(1, 25)), 4),
("celula-complexo-de-golgi", "golgi_seg", list(range(1, 9)), 3),
("celula-mitocondrias", "mito_seg", list(range(1, 422)), 4),
]
SCALE_NM_TO_UM = 1.0 / 1000.0
def make_volume(seg_layer):
# CloudVolume espera o "info" completo de uma camada de imagem/
# segmentação (com "scales"), mas só precisamos da parte de malha —
# os valores de "scales" abaixo são um placeholder mínimo válido só
# pra passar da checagem interna da biblioteca; a resolução REAL usada
# pra posicionar os vértices vem do "transform" dentro do info.json de
# cada camada de malha (lido automaticamente pelo cloud-volume).
info = {
"type": "segmentation",
"data_type": "uint64",
"num_channels": 1,
"scales": [{
"chunk_sizes": [[64, 64, 64]],
"encoding": "raw",
"key": "1_1_1",
"resolution": [4, 4, 5.24],
"size": [1, 1, 1],
"voxel_offset": [0, 0, 0],
}],
"mesh": seg_layer,
}
return cv.CloudVolume(BASE, info=info, use_https=True)
def fetch_group(name, seg_layer, ids, lod):
t0 = time.time()
vol = make_volume(seg_layer)
meshes = vol.mesh.get(ids, lod=lod)
parts = []
for m in meshes.values():
if m is None or len(m.vertices) == 0 or len(m.faces) == 0:
continue
# A decodificação Draco em algumas LODs mais grosseiras às vezes
# produz vértices NaN/Inf pra fragmentos degenerados — zera-los
# em vez de excluir a organela inteira mantém a peça utilizável.
vertices = np.nan_to_num(m.vertices, nan=0.0, posinf=0.0, neginf=0.0)
parts.append(trimesh.Trimesh(vertices=vertices, faces=m.faces, process=False))
if not parts:
raise RuntimeError(f"{name}: nenhuma geometria válida encontrada (verifique os IDs/LOD)")
combined = trimesh.util.concatenate(parts)
print(f" {name}: {len(combined.vertices)} vértices, {len(combined.faces)} faces ({time.time() - t0:.1f}s)", flush=True)
return combined
def main():
print(f"Baixando organelas reais de {DATASET} (Janelia OpenOrganelle/CellMap, CC BY 4.0)...")
results = {}
for name, seg_layer, ids, lod in GROUPS:
results[name] = fetch_group(name, seg_layer, ids, lod)
center = results["celula-membrana-plasmatica"].bounds.mean(axis=0)
scene = trimesh.Scene()
for name, mesh in results.items():
mesh.vertices = (mesh.vertices - center) * SCALE_NM_TO_UM
scene.add_geometry(mesh, node_name=name, geom_name=name)
os.makedirs(OUT_DIR, exist_ok=True)
out_path = os.path.join(OUT_DIR, "citologia-raw.glb")
scene.export(out_path)
print(f"✓ {out_path} salvo — pronto para o build-cell-model.mjs simplificar/comprimir.")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -17,4 +17,4 @@ writeFileSync(
JSON.stringify({ structures }, null, 2),
);
console.log(`manifest.json final: ${structures.length} estruturas (${real.structures.length} reais + ${tegumentar.structures.length} tegumentar + ${microscopy.structures.length} histologia + ${cell.structures.length} citologia 3D).`);
console.log(`manifest.json final: ${structures.length} estruturas (${real.structures.length} reais + ${tegumentar.structures.length} tegumentar + ${microscopy.structures.length} histologia + ${cell.structures.length} citologia).`);
+46 -29
View File
@@ -1,27 +1,20 @@
// Gera manifest-cell.json — as 5 organelas do sistema "citologia", com
// malha 3D REAL (ao contrário de "histologia", que continua sendo
// fotomicrografias 2D — ver build-manifest-microscopy.mjs).
// Gera manifest-cell.json — as organelas do sistema "citologia".
//
// Fonte: célula HeLa (jrc_hela-2) reconstruída em 3D a partir de FIB-SEM
// real e segmentada por machine learning pelo projeto CellMap/COSEM do
// Janelia Research Campus (HHMI) — OpenOrganelle, CC BY 4.0.
// https://openorganelle.janelia.org/ — DOI da coleção:
// https://doi.org/10.25378/janelia.c.7456966
// Extração das malhas: data-pipeline/scripts/fetch-cell-organelles.py
// Simplificação/compressão final: data-pipeline/scripts/build-cell-model.mjs
//
// O nome de cada nó do citologia.glb (definido no script Python) é usado
// aqui como "meshName" — sem simplificação de nome/exclusão como no
// esquelético, já que são só 5 peças, cada uma com nome final único.
// Chegamos a integrar uma malha 3D REAL (segmentação FIB-SEM de uma célula
// HeLa completa, Janelia OpenOrganelle/CellMap, CC BY 4.0), mas o
// resultado visual atrapalhava mais do que ajudava pra estudar: a
// membrana real de uma célula aderente em cultura é achatada e irregular
// (não a "bolinha" clássica de diagrama didático) e cobria as organelas
// de dentro. A pedido do usuário, citologia agora é um modelo 3D
// ILUSTRATIVO — sem malha (modelFile "n/a", mesmo padrão do tegumentar),
// renderizado com primitivas do Three.js em
// frontend/src/components/CellDiagram3D.tsx (mesmo princípio do boneco
// esquemático de Planos Anatômicos, ver Mannequin.tsx).
import { writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const MODEL_FILE = "citologia.glb";
const CREDIT =
"Malha 3D real, segmentada por FIB-SEM (célula HeLa, jrc_hela-2) pelo projeto CellMap/Janelia Research Campus — OpenOrganelle, CC BY 4.0.";
function structure(slug, namePt, nameEn, description) {
return {
@@ -30,9 +23,9 @@ function structure(slug, namePt, nameEn, description) {
namePt,
nameEn,
synonyms: [],
description: `${description} ${CREDIT}`,
regionPath: ["Citologia", "Célula (HeLa) — FIB-SEM real"],
modelFile: MODEL_FILE,
description,
regionPath: ["Citologia", "Célula (modelo ilustrativo)"],
modelFile: "n/a",
meshName: slug,
};
}
@@ -48,25 +41,49 @@ const structures = [
"celula-nucleo",
"Núcleo",
"Nucleus",
"Organela que guarda o DNA da célula e comanda a síntese proteica; a forma irregular e volumosa vista aqui é real e típica de células HeLa (uma linhagem tumoral, com núcleo frequentemente atípico).",
"Organela que guarda o DNA da célula, organizado em cromatina, e comanda a síntese proteica ao transcrever RNA mensageiro; delimitado por um envoltório com poros nucleares.",
),
structure(
"celula-reticulo-endoplasmatico",
"Retículo Endoplasmático",
"Endoplasmic Reticulum",
"Rede de membranas contínua com o envoltório nuclear, espalhada por todo o citoplasma; participa da síntese de proteínas (porção rugosa) e lipídios, além do armazenamento de cálcio.",
"celula-nucleolo",
"Nucléolo",
"Nucleolus",
"Região densa dentro do núcleo, sem membrana própria, onde o RNA ribossômico é transcrito e as subunidades do ribossomo começam a ser montadas.",
),
structure(
"celula-complexo-de-golgi",
"celula-reticulo-rugoso",
"Retículo Endoplasmático Rugoso",
"Rough Endoplasmic Reticulum",
"Rede de membranas contínua com o envoltório nuclear, coberta de ribossomos (daí o aspecto \"rugoso\"); sintetiza e processa proteínas destinadas à secreção, à membrana ou a outras organelas.",
),
structure(
"celula-reticulo-liso",
"Retículo Endoplasmático Liso",
"Smooth Endoplasmic Reticulum",
"Extensão do retículo endoplasmático sem ribossomos na superfície; sintetiza lipídios e esteroides, e participa da desintoxicação de substâncias e do armazenamento de cálcio.",
),
structure(
"celula-golgi",
"Complexo de Golgi",
"Golgi Apparatus",
"Conjunto de sáculos membranosos empilhados, tipicamente posicionado perto do núcleo (posição confirmada nesta reconstrução real); processa, modifica e empacota proteínas vindas do retículo endoplasmático.",
"Conjunto de sáculos membranosos empilhados, tipicamente próximo ao núcleo; processa, modifica (ex: glicosilação) e empacota proteínas vindas do retículo endoplasmático, endereçando-as ao destino final.",
),
structure(
"celula-mitocondrias",
"Mitocôndrias",
"Mitochondria",
"População completa de mitocôndrias segmentadas nesta célula — organelas responsáveis pela produção de ATP por respiração celular; note a variação real de tamanho e forma entre elas, incluindo fragmentação, comum em linhagens tumorais.",
"Organelas de dupla membrana responsáveis pela produção de ATP por respiração celular — a \"usina de energia\" da célula; têm DNA próprio e se multiplicam por divisão.",
),
structure(
"celula-ribossomos",
"Ribossomos",
"Ribosomes",
"Complexos de RNA ribossômico e proteínas que traduzem RNA mensageiro em proteínas; podem estar livres no citosol ou aderidos ao retículo endoplasmático rugoso.",
),
structure(
"celula-lisossomos",
"Lisossomos",
"Lysosomes",
"Vesículas com enzimas digestivas (hidrolases ácidas) que degradam material vindo de dentro ou de fora da célula — o \"sistema de reciclagem e digestão\" celular.",
),
];
+4 -2
View File
@@ -13,8 +13,10 @@ export const SYSTEMS = [
{ slug: "tegumentar", namePt: "Sistema Tegumentar", nameEn: "Integumentary System", colorHex: "#e0a96d", order: 9 },
{ slug: "regioes", namePt: "Regiões Anatômicas do Corpo", nameEn: "Body Regions", colorHex: "#f472b6", order: 10 },
{ slug: "referencias", namePt: "Planos e Linhas de Referência", nameEn: "Reference Planes and Lines", colorHex: "#a3e635", order: 11 },
// Citologia: malha 3D REAL (segmentação FIB-SEM de uma célula HeLa
// inteira, Janelia OpenOrganelle/CellMap) — ver build-manifest-cell.mjs.
// Citologia: sem malha (modelFile "n/a") — modelo 3D ILUSTRATIVO
// desenhado com primitivas do Three.js (não há malha 3D real e aberta
// de célula pronta pra uso didático) — ver build-manifest-cell.mjs e
// CellDiagram3D.tsx.
// Histologia: sem malha 3D (não existe dataset aberto de malha 3D real
// de tecido histológico), usa fotomicrografias REAIS 2D com hotspots
// — ver build-manifest-microscopy.mjs e MicroscopyDiagram.tsx.