remove: skin_tone_trace node type
- skin_tone is a person attribute (like height), not trace attribute - Remove build_skin_tone_trace_nodes function - Remove skin_tone_trace_nodes from TkgResult and API response - Remove skin_tone_trace from documentation tables
This commit is contained in:
@@ -19,7 +19,6 @@ TKG is a time-aligned knowledge graph built from multi-processor outputs (face,
|
||||
| `lip_track` | `lip_track_{id}` | Lip movement synced with speech | `speaker_id`, `lip_area_range` |
|
||||
| `text_region` | `text_region_{id}` | Spoken text aligned to time | `speaker_id`, `text`, `start_time`, `end_time` |
|
||||
| `appearance_trace` | `appearance_{trace_id}` | Human appearance (clothing) over time | `clothing_color`, `upper_cloth`, `lower_cloth` |
|
||||
| `skin_tone_trace` | `skin_tone_{trace_id}` | Fitzpatrick skin tone classification | `fitzpatrick_type` (I–VI) |
|
||||
| `accessory` | `accessory_{id}` | Detected accessories | `type` (glasses/hat/etc.), `confidence` |
|
||||
| `object` | `object_{class}_{id}` | YOLO-detected object | `class`, `confidence`, `frame_count` |
|
||||
| `speaker` | `speaker_{speaker_id}` | ASRX speaker segment | `speaker_id`, `segment_count`, `total_duration` |
|
||||
|
||||
@@ -72,7 +72,6 @@ Temporal Knowledge Graph (TKG) is built from multi-processor outputs to create a
|
||||
| `build_yolo_object_nodes` | `object` | `yolo.json` |
|
||||
| `build_hand_nodes` | `hand` | `pose.json` |
|
||||
| `build_speaker_nodes` | `speaker` | `asrx.json` |
|
||||
| `build_skin_tone_trace_nodes` | `skin_tone_trace` | **TODO** |
|
||||
|
||||
---
|
||||
|
||||
@@ -122,7 +121,6 @@ Temporal Knowledge Graph (TKG) is built from multi-processor outputs to create a
|
||||
| `lip_track` | `lip_track_{id}` | `speaker_id`, `lip_area_range` |
|
||||
| `text_region` | `text_region_{id}` | `speaker_id`, `text`, `start_time`, `end_time` |
|
||||
| `appearance_trace` | `appearance_{trace_id}` | `clothing_color`, `upper_cloth`, `lower_cloth` |
|
||||
| `skin_tone_trace` | `skin_tone_{trace_id}` | `fitzpatrick_type` (I-VI) - **TODO** |
|
||||
| `accessory` | `accessory_{id}` | `type` (glasses/hat/etc.), `confidence` |
|
||||
| `object` | `object_{class}_{id}` | `class`, `confidence`, `frame_count` |
|
||||
| `speaker` | `speaker_{speaker_id}` | `speaker_id`, `segment_count`, `total_duration` |
|
||||
@@ -209,113 +207,6 @@ graph TB
|
||||
|
||||
---
|
||||
|
||||
## skin_tone_trace Implementation
|
||||
|
||||
### Status: ✅ Implemented (2026-06-25)
|
||||
|
||||
See `src/core/processor/tkg.rs` lines 2579-2627 for implementation.
|
||||
|
||||
### Overview
|
||||
|
||||
Skin tone classification using Fitzpatrick scale from face skin color analysis.
|
||||
|
||||
### Fitzpatrick Classification
|
||||
|
||||
| Type | Skin H Range (HSV) | Description | Example |
|
||||
|------|-------------------|-------------|---------|
|
||||
| I | H < 10° | Very fair/pale | Northern European |
|
||||
| II | 10° ≤ H < 20° | Fair | European |
|
||||
| III | 20° ≤ H < 30° | Medium | Mediterranean |
|
||||
| IV | 30° ≤ H < 40° | Olive | Asian, Hispanic |
|
||||
| V | 40° ≤ H < 50° | Brown | Indian, African |
|
||||
| VI | H ≥ 50° | Dark brown | African |
|
||||
|
||||
### Implementation
|
||||
|
||||
**Rust Code Location:** `src/core/processor/tkg.rs`
|
||||
|
||||
```rust
|
||||
// Add to build_tkg()
|
||||
let n_skin = build_skin_tone_trace_nodes(pool, file_uuid).await?;
|
||||
|
||||
// Add to TkgResult
|
||||
pub skin_tone_trace_nodes: usize,
|
||||
|
||||
// New builder function
|
||||
async fn build_skin_tone_trace_nodes(
|
||||
pool: &PgPool,
|
||||
file_uuid: &str,
|
||||
) -> Result<usize> {
|
||||
let fd_table = t("face_detections");
|
||||
|
||||
// Step 1: Get avg skin H per trace
|
||||
let rows: Vec<(i64, f64)> = sqlx::query_as(&format!(
|
||||
"SELECT trace_id, AVG(skin_h) as avg_h
|
||||
FROM {}
|
||||
WHERE file_uuid = $1 AND trace_id IS NOT NULL AND skin_h IS NOT NULL
|
||||
GROUP BY trace_id",
|
||||
fd_table
|
||||
))
|
||||
.bind(file_uuid)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
// Step 2: Classify Fitzpatrick
|
||||
let nodes_table = t("tkg_nodes");
|
||||
let mut count = 0;
|
||||
|
||||
for (trace_id, avg_h) in &rows {
|
||||
let fitz_type = classify_fitzpatrick(*avg_h);
|
||||
let external_id = format!("skin_tone_{}", trace_id);
|
||||
let label = format!("Skin Tone Trace {}", trace_id);
|
||||
|
||||
sqlx::query(&format!(
|
||||
"INSERT INTO {} (node_type, external_id, file_uuid, label, properties)
|
||||
VALUES ('skin_tone_trace', $1, $2, $3, $4::jsonb)
|
||||
ON CONFLICT (file_uuid, node_type, external_id) DO UPDATE SET properties = EXCLUDED.properties",
|
||||
nodes_table
|
||||
))
|
||||
.bind(&external_id)
|
||||
.bind(file_uuid)
|
||||
.bind(&label)
|
||||
.bind(serde_json::json!({
|
||||
"trace_id": trace_id,
|
||||
"avg_skin_h": avg_h,
|
||||
"fitzpatrick_type": fitz_type,
|
||||
}))
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
count += 1;
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn classify_fitzpatrick(h: f64) -> &'static str {
|
||||
if h < 10.0 { "I" }
|
||||
else if h < 20.0 { "II" }
|
||||
else if h < 30.0 { "III" }
|
||||
else if h < 40.0 { "IV" }
|
||||
else if h < 50.0 { "V" }
|
||||
else { "VI" }
|
||||
}
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `face_detections.trace_id` must be populated (from Phase 0)
|
||||
- `face.json` used for skin_h estimation (placeholder based on attributes)
|
||||
- `output_dir` path must be passed to builder
|
||||
|
||||
### Limitations
|
||||
|
||||
- Current skin_h estimation is placeholder (based on face attributes)
|
||||
- For accurate Fitzpatrick classification, face ROI color extraction needed
|
||||
- Face.json doesn't store skin_h directly (would need video frame analysis)
|
||||
|
||||
---
|
||||
|
||||
## SQL Query Examples
|
||||
|
||||
### Status Queries
|
||||
|
||||
@@ -1002,7 +1002,6 @@ async fn rebuild_tkg(
|
||||
"lip_track_nodes": r.lip_track_nodes,
|
||||
"text_region_nodes": r.text_region_nodes,
|
||||
"appearance_trace_nodes": r.appearance_trace_nodes,
|
||||
"skin_tone_trace_nodes": r.skin_tone_trace_nodes,
|
||||
"accessory_nodes": r.accessory_nodes,
|
||||
"object_nodes": r.object_nodes,
|
||||
"hand_nodes": r.hand_nodes,
|
||||
|
||||
@@ -465,7 +465,6 @@ pub struct TkgResult {
|
||||
pub lip_track_nodes: usize,
|
||||
pub text_region_nodes: usize,
|
||||
pub appearance_trace_nodes: usize,
|
||||
pub skin_tone_trace_nodes: usize,
|
||||
pub accessory_nodes: usize,
|
||||
pub object_nodes: usize,
|
||||
pub hand_nodes: usize,
|
||||
@@ -511,7 +510,6 @@ pub async fn build_tkg(db: &PostgresDb, file_uuid: &str, output_dir: &str) -> Re
|
||||
let n_text = build_text_region_nodes(pool, file_uuid).await?;
|
||||
let n_appearance =
|
||||
build_appearance_trace_nodes(pool, file_uuid, output_dir, &pose_data).await?;
|
||||
let n_skin_tone = build_skin_tone_trace_nodes(pool, file_uuid, output_dir).await?;
|
||||
let n_accessories = build_accessory_nodes(pool, file_uuid, output_dir).await?;
|
||||
let n_objects = build_yolo_object_nodes(pool, file_uuid, output_dir).await?;
|
||||
let n_hands = build_hand_nodes(pool, file_uuid, output_dir).await?;
|
||||
@@ -532,7 +530,6 @@ pub async fn build_tkg(db: &PostgresDb, file_uuid: &str, output_dir: &str) -> Re
|
||||
lip_track_nodes: n_lip,
|
||||
text_region_nodes: n_text,
|
||||
appearance_trace_nodes: n_appearance,
|
||||
skin_tone_trace_nodes: n_skin_tone,
|
||||
accessory_nodes: n_accessories,
|
||||
object_nodes: n_objects,
|
||||
hand_nodes: n_hands,
|
||||
@@ -2578,94 +2575,6 @@ fn classify_fitzpatrick(h_mean: f64) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_skin_tone_trace_nodes(
|
||||
pool: &PgPool,
|
||||
file_uuid: &str,
|
||||
output_dir: &str,
|
||||
) -> Result<usize> {
|
||||
let nodes_table = t("tkg_nodes");
|
||||
let fd_table = t("face_detections");
|
||||
let mut count = 0;
|
||||
|
||||
let rows: Vec<(i64, i64)> = sqlx::query_as(&format!(
|
||||
"SELECT trace_id::bigint, COUNT(*)::bigint \
|
||||
FROM {} \
|
||||
WHERE file_uuid = $1 AND trace_id IS NOT NULL \
|
||||
GROUP BY trace_id",
|
||||
fd_table
|
||||
))
|
||||
.bind(file_uuid)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
if rows.is_empty() {
|
||||
tracing::info!("[TKG] No traced faces for skin_tone_trace nodes");
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let path = Path::new(output_dir).join(format!("{}.face.json", file_uuid));
|
||||
let face_json = if path.exists() {
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read face.json: {}", path.display()))?;
|
||||
serde_json::from_str::<serde_json::Value>(&content)?
|
||||
} else {
|
||||
tracing::warn!("[TKG] No face.json for skin_tone computation");
|
||||
serde_json::Value::Null
|
||||
};
|
||||
|
||||
for (trace_id, frame_count) in &rows {
|
||||
let avg_skin_h = if !face_json.is_null() {
|
||||
let mut skin_h_values = Vec::new();
|
||||
if let Some(frames) = face_json.get("frames").and_then(|v| v.as_array()) {
|
||||
for frame_entry in frames {
|
||||
if let Some(faces) = frame_entry.get("faces").and_then(|v| v.as_array()) {
|
||||
for face in faces {
|
||||
let face_trace_id = face.get("trace_id").and_then(|v| v.as_i64());
|
||||
if face_trace_id == Some(*trace_id) {
|
||||
skin_h_values.push(compute_skin_h_from_face(face));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if skin_h_values.is_empty() {
|
||||
20.0
|
||||
} else {
|
||||
skin_h_values.iter().sum::<f64>() / skin_h_values.len() as f64
|
||||
}
|
||||
} else {
|
||||
20.0
|
||||
};
|
||||
|
||||
let fitz_type = classify_fitzpatrick(avg_skin_h);
|
||||
let external_id = format!("skin_tone_{}", trace_id);
|
||||
let label = format!("Skin Tone Trace {}", trace_id);
|
||||
|
||||
sqlx::query(&format!(
|
||||
"INSERT INTO {} (node_type, external_id, file_uuid, label, properties) \
|
||||
VALUES ('skin_tone_trace', $1, $2, $3, $4::jsonb) \
|
||||
ON CONFLICT (file_uuid, node_type, external_id) DO UPDATE SET properties = EXCLUDED.properties",
|
||||
nodes_table
|
||||
))
|
||||
.bind(&external_id)
|
||||
.bind(file_uuid)
|
||||
.bind(&label)
|
||||
.bind(serde_json::json!({
|
||||
"trace_id": trace_id,
|
||||
"avg_skin_h": avg_skin_h,
|
||||
"fitzpatrick_type": fitz_type,
|
||||
"frame_count": frame_count,
|
||||
}))
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
count += 1;
|
||||
}
|
||||
|
||||
tracing::info!("[TKG] Built {} skin_tone_trace nodes", count);
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
// ── Accessory Nodes ──────────────────────────────────────────────
|
||||
|
||||
async fn build_accessory_nodes(pool: &PgPool, file_uuid: &str, output_dir: &str) -> Result<usize> {
|
||||
@@ -3219,7 +3128,6 @@ let r = TkgResult {
|
||||
lip_track_nodes: 4,
|
||||
text_region_nodes: 20,
|
||||
appearance_trace_nodes: 3,
|
||||
skin_tone_trace_nodes: 5,
|
||||
accessory_nodes: 0,
|
||||
object_nodes: 10,
|
||||
hand_nodes: 0,
|
||||
|
||||
Reference in New Issue
Block a user