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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/components/DatasetMetadataModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { MetricCategory, PerformanceEntry } from '../lib/performance';
import { oklchToRgb } from '../lib/plotlyChrome';
import { EmbeddingPlot2D } from './EmbeddingPlot2D';
import { EmbeddingPlot3D } from './EmbeddingPlot3D';
import { PointCloudPlot3D } from './PointCloudPlot3D';
import { ScoringMethodologyModal } from './ScoringMethodologyModal';
import styles from './DatasetMetadataModal.module.css';

Expand Down Expand Up @@ -1401,8 +1402,12 @@ export function DatasetMetadataModal({
)}

<section className={styles.section}>
<h3 className={styles.sectionTitle}>Sample image</h3>
{hasExampleImage(dataset.examples_image_url) ? (
<h3 className={styles.sectionTitle}>
{dataset.sensor_modality === 'point_cloud' ? 'Sample point cloud' : 'Sample image'}
</h3>
{dataset.sensor_modality === 'point_cloud' && dataset.point_cloud_sample_url ? (
<PointCloudPlot3D sampleUrl={dataset.point_cloud_sample_url} />
) : hasExampleImage(dataset.examples_image_url) ? (
<figure className={styles.figure}>
<img className={styles.exampleImage} src={dataset.examples_image_url} alt={`Example for ${dataset.name}`} />
</figure>
Expand Down Expand Up @@ -1630,3 +1635,4 @@ export function DatasetMetadataModal({
</div>
);
}

199 changes: 199 additions & 0 deletions src/components/PointCloudPlot3D.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import BrowserOnly from '@docusaurus/BrowserOnly';
import { usePlotlyChrome } from '../lib/plotlyChrome';
import { toTitleCase } from '../lib/datasets';
import styles from './DatasetMetadataModal.module.css';

export type PointCloudPoint = { x: number; y: number; z: number; label: number };
export type PointCloudCropSample = { crop: string; points: PointCloudPoint[] };

const DEFAULT_CAMERA_EYE = { x: 1.1, y: 1.1, z: 0.9 };
const LABEL_COLORS = ['#4CAF50', '#FF9800', '#2196F3', '#E91E63', '#9C27B0', '#00BCD4'];

function buildTraces(
points: PointCloudPoint[],
hoverlabel: { bgcolor: string; bordercolor: string; font: { color: string } },
markerRing: string,
) {
const labels = Array.from(new Set(points.map((p) => p.label))).sort((a, b) => a - b);
return labels.map((label, i) => {
const subset = points.filter((p) => p.label === label);
return {
type: 'scatter3d',
mode: 'markers',
name: `mask value ${label}`,
x: subset.map((p) => p.x),
y: subset.map((p) => p.y),
z: subset.map((p) => p.z),
marker: { size: 2, color: LABEL_COLORS[i % LABEL_COLORS.length], opacity: 0.75, line: { width: 0.3, color: markerRing } },
hoverinfo: 'skip',
hoverlabel,
};
});
}

// Fetches the precomputed multi-crop sample file. Each dataset's sample now holds
// one entry per crop (see scripts/generate_point_cloud_sample.py), instead of a
// single flat points array, so a dataset covering multiple species (e.g. Pheno4D's
// maize + tomato) can show either one via the crop selector below.
function usePointCloudSamples(sampleUrl: string) {
const [samples, setSamples] = useState<PointCloudCropSample[] | null>(null);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
let cancelled = false;
fetch(sampleUrl)
.then((res) => {
if (!res.ok) throw new Error(`Failed to fetch point cloud sample: ${res.status}`);
return res.json();
})
.then((data) => {
if (!cancelled) setSamples(data.samples ?? []);
})
.catch((err) => {
if (!cancelled) setError(err.message);
});
return () => {
cancelled = true;
};
}, [sampleUrl]);

return { samples, error };
}

export function PointCloudPlot3D({ sampleUrl }: { sampleUrl: string }) {
const chrome = usePlotlyChrome();
const { samples, error } = usePointCloudSamples(sampleUrl);
const [activeCrop, setActiveCrop] = useState<string | null>(null);

// Default to the first crop once samples load, mirrors EmbeddingScatter's
// view state pattern (2D/3D toggle) but for crop selection instead.
useEffect(() => {
if (samples && samples.length > 0 && activeCrop == null) {
setActiveCrop(samples[0].crop);
}
}, [samples, activeCrop]);

const activeSample = useMemo(
() => samples?.find((s) => s.crop === activeCrop) ?? samples?.[0] ?? null,
[samples, activeCrop],
);

const hoverlabel = useMemo(
() => ({
bgcolor: chrome.tooltipBg,
bordercolor: chrome.tooltipBorder,
font: { color: chrome.tooltipText },
}),
[chrome],
);
const traces = useMemo(
() => (activeSample ? buildTraces(activeSample.points, hoverlabel, chrome.markerRing) : []),
[activeSample, hoverlabel, chrome.markerRing],
);

const plotlyRef = useRef<any>(null);
const graphDivRef = useRef<HTMLElement | null>(null);
const handleResetView = () => {
if (!plotlyRef.current || !graphDivRef.current) return;
plotlyRef.current.relayout(graphDivRef.current, {
'scene.camera.eye': DEFAULT_CAMERA_EYE,
'scene.xaxis.autorange': true,
'scene.yaxis.autorange': true,
'scene.zaxis.autorange': true,
});
};

const makeAxis = () => ({
showgrid: true,
gridcolor: chrome.grid,
zeroline: false,
showticklabels: true,
linecolor: chrome.grid,
tickfont: { color: chrome.text, family: chrome.fontFamily, size: 10 },
});

if (error) {
return <div className={styles.embedPlotFallback}>Point cloud preview unavailable.</div>;
}

return (
<div>
{/* Crop selector, only shown when there's more than one crop to choose from,
mirrors the embedToggle 2D/3D button pattern used elsewhere on this page. */}
{samples && samples.length > 1 && (
<div className={styles.embedToggle}>
{samples.map((s) => (
<button
key={s.crop}
type="button"
className={`${styles.embedToggleButton} ${activeCrop === s.crop ? styles.embedToggleActive : ''}`}
onClick={() => setActiveCrop(s.crop)}
>
{toTitleCase(s.crop)}
</button>
))}
</div>
)}

<div className={styles.embedViewport}>
<BrowserOnly fallback={<div className={styles.embedPlotFallback}>Loading 3D view…</div>}>
{() => {
if (!activeSample) return <div className={styles.embedPlotFallback}>Loading point cloud…</div>;
const Plotly = require('plotly.js-dist-min');
plotlyRef.current = Plotly;
const createPlotlyComponent = require('react-plotly.js/factory').default;
const Plot = createPlotlyComponent(Plotly);
return (
<Plot
data={traces}
onInitialized={(_figure: unknown, graphDiv: HTMLElement) => {
graphDivRef.current = graphDiv;
}}
onUpdate={(_figure: unknown, graphDiv: HTMLElement) => {
graphDivRef.current = graphDiv;
}}
layout={{
autosize: true,
margin: { l: 20, r: 20, t: 15, b: 35 },
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
scene: {
aspectmode: 'data',
xaxis: makeAxis(),
yaxis: makeAxis(),
zaxis: makeAxis(),
camera: { eye: DEFAULT_CAMERA_EYE },
},
showlegend: true,
legend: { font: { color: chrome.text, family: chrome.fontFamily, size: 10 } },
hoverlabel: {
bgcolor: chrome.tooltipBg,
bordercolor: chrome.tooltipBorder,
font: { color: chrome.tooltipText },
},
// keyed by active crop so switching crops resets the camera to the
// default framing instead of carrying over the previous crop's view
uirevision: `point-cloud-3d-${activeSample.crop}`,
}}
config={{ displayModeBar: false, responsive: true }}
style={{ width: '100%', height: '100%' }}
useResizeHandler
/>
);
}}
</BrowserOnly>
<button
type="button"
className={styles.embedResetButton}
onClick={handleResetView}
title="Drag to orbit, scroll to zoom · click to reset the view"
>
⟲ Reset view
</button>
</div>
</div>
);
}

export default PointCloudPlot3D;
7 changes: 7 additions & 0 deletions src/lib/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface Dataset {
stats_mean: number[] | null;
stats_std: number[] | null;
examples_image_url: string | null;
point_cloud_sample_url: string | null;
license: string | null;
citation: string | null;
parent_dataset?: string | null;
Expand Down Expand Up @@ -218,6 +219,10 @@ function normalizeDataset(raw: unknown): Dataset | null {
raw.examples_url,
raw.image_url,
),
point_cloud_sample_url: firstString(
raw.point_cloud_sample_url,
raw.point_cloud_url,
),
license: firstString(raw.license),
citation: firstString(raw.citation),
parent_dataset: firstString(raw.parent_dataset, raw.parentDataset),
Expand Down Expand Up @@ -283,6 +288,8 @@ function mergeDataset(current: Dataset, incoming: Dataset): Dataset {
stats_std: current.stats_std ?? incoming.stats_std,
examples_image_url:
current.examples_image_url ?? incoming.examples_image_url,
point_cloud_sample_url:
current.point_cloud_sample_url ?? incoming.point_cloud_sample_url,
license: current.license ?? incoming.license,
citation: current.citation ?? incoming.citation,
parent_dataset: current.parent_dataset ?? incoming.parent_dataset,
Expand Down
32 changes: 31 additions & 1 deletion static/data/hf_datasets.json
Original file line number Diff line number Diff line change
Expand Up @@ -11783,5 +11783,35 @@
"source": "huggingface",
"hf_link": "https://huggingface.co/datasets/Project-AgML/maize_individual_detection",
"examples_image_url": "/img/agml/sample_images/maize_individual_detection_sample.webp"
},
{
"name": "pheno4d_point_cloud_segmentation",
"source": "huggingface",
"hf_link": "https://huggingface.co/datasets/Project-AgML/pheno4d_point_cloud_segmentation",
"machine_learning_task": "semantic_segmentation",
"agricultural_task": "plant_organ_segmentation",
"location": [
"Germany"
],
"environment": "greenhouse",
"crop_types": [
"maize",
"tomato"
],
"sensor_modality": "point_cloud",
"real_or_synthetic": "real",
"platform": "fixed",
"input_data_format": "image_folder",
"annotation_format": "segmentationMask",
"num_images": 126,
"documentation": "https://doi.org/10.1371/journal.pone.0256340",
"examples_image_url": "/img/agml/sample_images/pheno4d_point_cloud_segmentation_sample.png",
"point_cloud_sample_url": "/data/point_cloud_samples/pheno4d_point_cloud_segmentation_sample.json",
"license": "cc-by-4.0",
"citation": "Schunck, D., Magistri, F., Rosu, R.A., et al. (2021). Pheno4D: A spatio-temporal dataset of maize and tomato plant point clouds for phenotyping and advanced plant analysis. PLOS ONE, 16(8).",
"parent_dataset": null,
"zip_size_bytes": 5293657341,
"stats_mean": null,
"stats_std": null
}
]
]

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading