Guide me as a bronchoscopy camera through all the lobes and divisions of lung while doing the procedure....


| Segment | Bronchopulmonary | Clock Position |
|---|---|---|
| Apical | B1 | 4 o'clock |
| Anterior | B2 | 12 o'clock |
| Posterior | B3 | 8 o'clock |
The apical segment goes toward the apex (top) of the lung; anterior goes forward toward the chest wall; posterior goes backward.
| Segment | Bronchopulmonary | Clock Position |
|---|---|---|
| Lateral | B4 | 3-6 o'clock |
| Medial | B5 | 9-12 o'clock |
| Segment | Bronchopulmonary | Clock Position | Notes |
|---|---|---|---|
| Superior (apical) | B6 | 5 o'clock | Just past the middle lobe bronchus; directly POSTERIOR - opposite RML |
| Medial basal | B7 | 9 o'clock | Usually more proximal than other basal segments |
| Anterior basal | B8 | 1 o'clock | |
| Lateral basal | B9 | 3 o'clock | |
| Posterior basal | B10 | 6 o'clock |
Note: In more than half of patients, a subapical (subsuperior) segment is found - its bronchus arises posteriorly anywhere along the inferior lobe bronchus between B6 and the final basilar division.
| Segment | Bronchopulmonary | Notes |
|---|---|---|
| Apicoposterior | B1+3 | Fused on the left - no separate apical/posterior segments |
| Anterior | B2 |
| Segment | Bronchopulmonary | Clock Position |
|---|---|---|
| Superior lingular | B4 | 10 o'clock |
| Inferior lingular | B5 | 2 o'clock |
The lingula is the left lung's analog to the right middle lobe. It's thin and tongue-like, projecting anteriorly. Lingular pneumonia is a common clinical entity.
| Segment | Bronchopulmonary | Clock Position | Notes |
|---|---|---|---|
| Superior (apical) | B6 | 6 o'clock | Just past origin of lower lobe; posterior |
| Anteromedial basal | B8+7 | 12 o'clock | Fused on left (no separate medial/anterior basal) |
| Lateral basal | B9 | 9 o'clock | |
| Posterior basal | B10 | 5 o'clock |

NOSE/MOUTH → LARYNX → VOCAL CORDS → TRACHEA → CARINA
│
┌─────────────────────────────────────┼────────────────────────────────────────┐
│ │ │
RIGHT MAIN BRONCHUS LEFT MAIN BRONCHUS
│ │
┌─────────┴──────────┐ ┌───────────────┴──────────────┐
│ │ │ │
RUL Bronchus Intermedius LUL LLL
B1 (Apical) │ Upper Division B6 (Superior)
B2 (Anterior) ┌─────┴──────┐ B1+3 (Apicopost.) B7+8 (Anteromedial basal)
B3 (Posterior) │ │ B2 (Anterior) B9 (Lateral basal)
RML RLL Lingular Division B10 (Posterior basal)
B4 (Lateral) B6 (Superior) B4 (Sup. lingular)
B5 (Medial) B7 (Med. basal) B5 (Inf. lingular)
B8 (Ant. basal)
B9 (Lat. basal)
B10 (Post. basal)
| Finding | Significance |
|---|---|
| Widened, blunted carina | Subcarinal lymph node enlargement (malignancy, sarcoidosis, TB) |
| RUL orifice directly at carina | Normal variant - don't mistake for pathology |
| Pulsating LMB wall | Normal - transmitted aortic pulsation |
| RMB shorter, more vertical | ETT misplacement goes here preferentially |
| B6 directly opposite RML | Classic "posterior sentinel" - never miss it |
| Peripheral lesions beyond segmentals | Not visible by camera - need radial EBUS, ENB, or robotic bronchoscopy |
| Mucosal irregularity/bleeding | Endobronchial malignancy - biopsy here |
Create a printable quick-reference pocket card of segmental orifices (clock positions)
ls /home/daytona/workspace/ 2>/dev/null && echo "---" && python3 -c "import repo…
mkdir -p /home/daytona/workspace/bronchoscopy-card
~/bronchoscopy-card/make_card.py
#!/usr/bin/env python3
"""
Bronchoscopy Segmental Orifice Quick-Reference Pocket Card
Landscape A5 (148 x 210 mm) - fits in a white-coat pocket
"""
import math
from reportlab.lib.pagesizes import A5, landscape
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.lib.colors import HexColor
# ── page setup ──────────────────────────────────────────────────────────────
PAGE = landscape(A5) # 210 x 148 mm
W, H = PAGE
OUT = "/home/daytona/workspace/bronchoscopy-card/bronchoscopy_pocket_card.pdf"
# ── palette ─────────────────────────────────────────────────────────────────
NAVY = HexColor("#0A2342")
SKY = HexColor("#1A6BAC")
LTBLUE = HexColor("#D6E9F8")
PINK = HexColor("#F5E6EC")
CREAM = HexColor("#FFFDF5")
WHITE = colors.white
GOLD = HexColor("#C8860A")
DGRAY = HexColor("#333333")
MGRAY = HexColor("#666666")
LGRAY = HexColor("#CCCCCC")
RRED = HexColor("#D63B2F") # right lung accent
LBLUE2 = HexColor("#2B7BBD") # left lung accent
GREENS = HexColor("#2E7D32") # clock tick
ORANGE = HexColor("#E65100")
c = canvas.Canvas(OUT, pagesize=PAGE)
c.setTitle("Bronchoscopy Pocket Card – Segmental Orifice Clock Positions")
c.setAuthor("Orris Medical Reference")
# ── background ───────────────────────────────────────────────────────────────
c.setFillColor(CREAM)
c.rect(0, 0, W, H, fill=1, stroke=0)
# ── header band ──────────────────────────────────────────────────────────────
c.setFillColor(NAVY)
c.rect(0, H - 16*mm, W, 16*mm, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 11)
c.drawCentredString(W/2, H - 9.5*mm, "BRONCHOSCOPY · SEGMENTAL ORIFICE CLOCK POSITIONS")
c.setFont("Helvetica", 7)
c.setFillColor(LTBLUE)
c.drawCentredString(W/2, H - 13.5*mm, "Fischer's Mastery of Surgery 8e · Gray's Anatomy for Students")
# ── footer ───────────────────────────────────────────────────────────────────
c.setFillColor(NAVY)
c.rect(0, 0, W, 6*mm, fill=1, stroke=0)
c.setFillColor(GOLD)
c.setFont("Helvetica-BoldOblique", 6)
c.drawString(4*mm, 2*mm, "Orris Medical | Quick Reference Pocket Card")
c.setFillColor(WHITE)
c.setFont("Helvetica", 6)
c.drawRightString(W - 4*mm, 2*mm, "Right: 10 segments · Left: 8 segments (American nomenclature)")
# ══════════════════════════════════════════════════════════════════════════════
# CLOCK FACE HELPER
# ══════════════════════════════════════════════════════════════════════════════
def clock_face(cx, cy, r, segments, title, title_color, ring_color):
"""Draw an annotated clock dial with segment orifice positions."""
# outer ring
c.setStrokeColor(ring_color)
c.setFillColor(WHITE)
c.setLineWidth(1.5)
c.circle(cx, cy, r, fill=1, stroke=1)
# inner hub
c.setFillColor(ring_color)
c.circle(cx, cy, r*0.08, fill=1, stroke=0)
# clock hour ticks (thin)
c.setStrokeColor(LGRAY)
c.setLineWidth(0.4)
for h in range(12):
angle = math.radians(90 - h * 30)
x1 = cx + (r * 0.82) * math.cos(angle)
y1 = cy + (r * 0.82) * math.sin(angle)
x2 = cx + r * math.cos(angle)
y2 = cy + r * math.sin(angle)
c.line(x1, y1, x2, y2)
# hour numbers (small)
c.setFont("Helvetica", 4.5)
c.setFillColor(MGRAY)
for h in [12, 3, 6, 9]:
angle = math.radians(90 - (h % 12) * 30)
xn = cx + (r * 0.68) * math.cos(angle)
yn = cy + (r * 0.68) * math.sin(angle) - 1.5
c.drawCentredString(xn, yn, str(h))
# segments: each is (label, hour_float, color)
for (label, hour_float, seg_color) in segments:
# hour_float e.g. 4.0 → angle
# support ranges like "3-6" → midpoint
if isinstance(hour_float, str) and "-" in hour_float:
parts = hour_float.split("-")
lo, hi = float(parts[0]), float(parts[1])
hour_float = (lo + hi) / 2
angle = math.radians(90 - hour_float * 30)
# spoke line
c.setStrokeColor(seg_color)
c.setLineWidth(1.2)
x_spoke = cx + (r * 0.75) * math.cos(angle)
y_spoke = cy + (r * 0.75) * math.sin(angle)
c.line(cx, cy, x_spoke, y_spoke)
# dot on rim
xd = cx + (r * 0.88) * math.cos(angle)
yd = cy + (r * 0.88) * math.sin(angle)
c.setFillColor(seg_color)
c.circle(xd, yd, 2.2, fill=1, stroke=0)
# label outside rim
xl = cx + (r * 1.22) * math.cos(angle)
yl = cy + (r * 1.22) * math.sin(angle) - 1.8
c.setFillColor(seg_color)
c.setFont("Helvetica-Bold", 4.8)
c.drawCentredString(xl, yl, label)
# lobe title above clock
c.setFillColor(title_color)
c.setFont("Helvetica-Bold", 7)
c.drawCentredString(cx, cy + r + 10, title)
# ══════════════════════════════════════════════════════════════════════════════
# LAYOUT: 3 clocks LEFT side (right lung), 2 clocks RIGHT side (left lung)
# ══════════════════════════════════════════════════════════════════════════════
TOP_Y = H - 16*mm - 2*mm # just below header
BOT_Y = 6*mm # just above footer
USABLE = TOP_Y - BOT_Y # usable height
# divide width: left panel (right lung) | divider | right panel (left lung)
DIV_X = W * 0.50
# ── Panel headers ──────────────────────────────────────────────────────────
def panel_header(x, y, w, text, bg, fg):
c.setFillColor(bg)
c.rect(x, y, w, 8*mm, fill=1, stroke=0)
c.setFillColor(fg)
c.setFont("Helvetica-Bold", 8.5)
c.drawCentredString(x + w/2, y + 2.5*mm, text)
panel_header(0, TOP_Y - 8*mm, DIV_X, "RIGHT LUNG (10 segments)", RRED, WHITE)
panel_header(DIV_X + 1, TOP_Y - 8*mm, W - DIV_X - 1, "LEFT LUNG (8 segments)", LBLUE2, WHITE)
# vertical divider
c.setStrokeColor(LGRAY)
c.setLineWidth(0.8)
c.line(DIV_X, BOT_Y, DIV_X, TOP_Y)
# ── RIGHT LUNG – 3 clocks ──────────────────────────────────────────────────
# clock radius
R = 14 * mm
panel_top = TOP_Y - 8*mm - 2*mm # below panel header
# 3 clocks stacked evenly in left half
clock_area_h = panel_top - BOT_Y
spacing = clock_area_h / 3
rul_cy = panel_top - spacing * 0.5
rml_cy = panel_top - spacing * 1.5
rll_cy = panel_top - spacing * 2.5
cx_r = DIV_X * 0.38 # centered in left half with room for labels
# color scheme per lobe
C_RUL = HexColor("#1565C0") # deep blue
C_RML = HexColor("#AD1457") # rose
C_RLL = HexColor("#2E7D32") # forest green
rul_segs = [
("B1 Apical", 4.0, HexColor("#1565C0")),
("B2 Anterior", 12.0, HexColor("#0288D1")),
("B3 Posterior",8.0, HexColor("#4FC3F7")),
]
rml_segs = [
("B4 Lateral", "3-6", HexColor("#AD1457")),
("B5 Medial", "9-12", HexColor("#E91E63")),
]
rll_segs = [
("B6 Superior", 5.0, HexColor("#2E7D32")),
("B7 Med.Basal", 9.0, HexColor("#558B2F")),
("B8 Ant.Basal", 1.0, HexColor("#9CCC65")),
("B9 Lat.Basal", 3.0, HexColor("#F57F17")),
("B10 Post.Basal", 6.0, HexColor("#E65100")),
]
clock_face(cx_r, rul_cy, R, rul_segs, "RIGHT UPPER LOBE", C_RUL, C_RUL)
clock_face(cx_r, rml_cy, R, rml_segs, "RIGHT MIDDLE LOBE", C_RML, C_RML)
clock_face(cx_r, rll_cy, R, rll_segs, "RIGHT LOWER LOBE", C_RLL, C_RLL)
# ── RIGHT LUNG – legend table alongside each clock ──────────────────────────
def seg_table(x, y_top, rows, header_color):
"""Small legend: segment | clock | note."""
row_h = 7.5
col_w = [22*mm, 13*mm, 12*mm]
# header
c.setFillColor(header_color)
c.rect(x, y_top, sum(col_w), row_h, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 5.5)
headers = ["Segment", "Clock", "Note"]
xc = x
for i, h in enumerate(headers):
c.drawString(xc + 1.5, y_top + 2, h)
xc += col_w[i]
# rows
c.setFont("Helvetica", 5.5)
for ri, (seg, clk, note, seg_color) in enumerate(rows):
ry = y_top - (ri + 1) * row_h
bg = LTBLUE if ri % 2 == 0 else WHITE
c.setFillColor(bg)
c.rect(x, ry, sum(col_w), row_h, fill=1, stroke=0)
# colored left stripe
c.setFillColor(seg_color)
c.rect(x, ry, 2.5, row_h, fill=1, stroke=0)
c.setFillColor(DGRAY)
c.drawString(x + 3.5, ry + 2, seg)
c.drawString(x + col_w[0] + 1.5, ry + 2, clk)
c.setFillColor(MGRAY)
c.drawString(x + col_w[0] + col_w[1] + 1.5, ry + 2, note)
# border
c.setStrokeColor(LGRAY)
c.setLineWidth(0.4)
c.rect(x, y_top - len(rows)*row_h, sum(col_w), (len(rows)+1)*row_h, fill=0, stroke=1)
table_x_r = cx_r + R + 10*mm
rul_rows = [
("B1 – Apical", "4 o'cl", "Lateral wall", HexColor("#1565C0")),
("B2 – Anterior", "12 o'cl", "12 o'clock", HexColor("#0288D1")),
("B3 – Posterior", "8 o'cl", "Post. wall", HexColor("#4FC3F7")),
]
rml_rows = [
("B4 – Lateral", "3–6", "Anterior", HexColor("#AD1457")),
("B5 – Medial", "9–12", "Anterior", HexColor("#E91E63")),
]
rll_rows = [
("B6 – Superior", "5 o'cl", "Opp. RML ✦", HexColor("#2E7D32")),
("B7 – Med.Basal", "9 o'cl", "Most proximal", HexColor("#558B2F")),
("B8 – Ant.Basal", "1 o'cl", "", HexColor("#9CCC65")),
("B9 – Lat.Basal", "3 o'cl", "", HexColor("#F57F17")),
("B10 – Post.Basal", "6 o'cl", "", HexColor("#E65100")),
]
seg_table(table_x_r, rul_cy + 5.5*mm + 7.5, rul_rows, C_RUL)
seg_table(table_x_r, rml_cy + 5.5*mm + 7.5, rml_rows, C_RML)
seg_table(table_x_r, rll_cy + 5.5*mm + 7.5, rll_rows, C_RLL)
# ── LEFT LUNG – 2 clocks ──────────────────────────────────────────────────
lul_cy = panel_top - spacing * 0.85
lll_cy = panel_top - spacing * 2.15
cx_l = DIV_X + (W - DIV_X) * 0.32
C_LUL = HexColor("#6A1B9A") # purple
C_LLL = HexColor("#00695C") # teal
lul_segs = [
("B1+3 ApPost", 8.0, HexColor("#6A1B9A")),
("B2 Anterior", 12.0, HexColor("#AB47BC")),
("B4 Sup.Ling", 10.0, HexColor("#E040FB")), # lingular
("B5 Inf.Ling", 2.0, HexColor("#CE93D8")),
]
lll_segs = [
("B6 Superior", 6.0, HexColor("#00695C")),
("B7+8 AM.Basal", 12.0, HexColor("#00897B")),
("B9 Lat.Basal", 9.0, HexColor("#4DB6AC")),
("B10 Post.Basal", 5.0, HexColor("#F57F17")),
]
clock_face(cx_l, lul_cy, R, lul_segs, "LEFT UPPER LOBE (+ Lingula)", C_LUL, C_LUL)
clock_face(cx_l, lll_cy, R, lll_segs, "LEFT LOWER LOBE", C_LLL, C_LLL)
# tables for left lung
table_x_l = cx_l + R + 8*mm
lul_rows = [
("B1+3 Apicopost.", "8 o'cl", "Upper div.", HexColor("#6A1B9A")),
("B2 – Anterior", "12 o'cl", "Upper div.", HexColor("#AB47BC")),
("B4 – Sup.Ling.", "10 o'cl", "Lingular div.", HexColor("#E040FB")),
("B5 – Inf.Ling.", "2 o'cl", "Lingular div.", HexColor("#CE93D8")),
]
lll_rows = [
("B6 – Superior", "6 o'cl", "Post., opp. LUL", HexColor("#00695C")),
("B7+8 AM.Basal", "12 o'cl", "Fused on left", HexColor("#00897B")),
("B9 – Lat.Basal", "9 o'cl", "", HexColor("#4DB6AC")),
("B10 – Post.Basal", "5 o'cl", "", HexColor("#F57F17")),
]
seg_table(table_x_l, lul_cy + 5.5*mm + 7.5, lul_rows, C_LUL)
seg_table(table_x_l, lll_cy + 5.5*mm + 7.5, lll_rows, C_LLL)
# ── NOTES BOX ────────────────────────────────────────────────────────────────
notes_x = DIV_X + 1
notes_y = BOT_Y
notes_w = W - DIV_X - 2
notes_h = 14*mm
c.setFillColor(HexColor("#FFF8E1"))
c.setStrokeColor(GOLD)
c.setLineWidth(0.7)
c.roundRect(notes_x, notes_y, notes_w, notes_h, 2*mm, fill=1, stroke=1)
c.setFont("Helvetica-Bold", 6)
c.setFillColor(GOLD)
c.drawString(notes_x + 2*mm, notes_y + notes_h - 5.5, "★ CLINICAL PEARLS")
pearls = [
"• RMB shorter & more vertical → ETT misplacement goes right",
"• Carina widened/blunted → subcarinal LN enlargement",
"• B6 RLL is DIRECTLY opposite RML orifice (posterior sentinel)",
"• LMB longer, ~45° angle; aortic pulsation often visible",
"• ✦ Subapical RLL segment present in >50% of patients",
]
c.setFont("Helvetica", 5.2)
c.setFillColor(DGRAY)
for i, pearl in enumerate(pearls):
c.drawString(notes_x + 2.5*mm, notes_y + notes_h - 10 - i * 6.5, pearl)
# ── save ─────────────────────────────────────────────────────────────────────
c.save()
print(f"Saved: {OUT}")
python3 /home/daytona/workspace/bronchoscopy-card/make_card.py 2>&1
ls -lh /home/daytona/workspace/bronchoscopy-card/bronchoscopy_pocket_card.pdf
bronchoscopy pocket card
PDF Document · PDF
| Left Panel - RIGHT LUNG | Right Panel - LEFT LUNG |
|---|---|
| 3 annotated clock dials (RUL, RML, RLL) | 2 annotated clock dials (LUL, LLL) |
| Colour-coded legend table per lobe | Colour-coded legend table per lobe |
Create a laminated, waterproof version for the OR/procedure room
~/bronchoscopy-card/make_or_card.py
#!/usr/bin/env python3
"""
Bronchoscopy OR/Procedure-Room Laminated Card
Two pages on A4 (print → cut → laminate both sides):
Page 1 (FRONT) – RIGHT LUNG full segmental clock chart
Page 2 (BACK) – LEFT LUNG full segmental clock chart
Design goals for OR/procedure room:
• Maximum contrast (dark bg, bright text) – readable under OR lights
• Large, bold fonts – readable through gloves
• 3 mm bleed on all sides + crop-mark corners
• 5 mm lamination border shown as dashed guide
• Glossy-ready: no gradients, flat solid fills
• Explicit "FRONT / BACK" labels for cutting guidance
• Rounded-corner safe-zone (standard lamination pouch)
"""
import math
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor, white, black
from reportlab.pdfgen import canvas
OUT = "/home/daytona/workspace/bronchoscopy-card/bronchoscopy_OR_laminated.pdf"
# ── Card dimensions (fits standard 54×86 mm lamination pouch, scaled up 2× for readability)
# We print two cards per A4 sheet, landscape-oriented cards on portrait A4
CARD_W = 148 * mm # A5 landscape width
CARD_H = 105 * mm # A5 landscape height / 2
BLEED = 3 * mm
LAM = 5 * mm # lamination border
PW, PH = A4
# ── Palette – high contrast OR style ────────────────────────────────────────
BG = HexColor("#0D1B2A") # deep navy – OR dark
BG2 = HexColor("#0A2E1E") # deep teal for left page
HEADER_R = HexColor("#B71C1C") # deep red header (right lung)
HEADER_L = HexColor("#1A237E") # deep indigo header (left lung)
GOLD = HexColor("#FFD600") # gold accent
WHITE = white
OFF_WHITE = HexColor("#F5F5F5")
LGRAY = HexColor("#424242")
MGRAY = HexColor("#757575")
LAM_COLOR = HexColor("#00E5FF") # cyan dashed lamination border
# Segment palettes – vivid, distinguishable under OR lights
SEG_COLORS = {
"B1": HexColor("#40C4FF"),
"B2": HexColor("#69F0AE"),
"B3": HexColor("#FFD740"),
"B4": HexColor("#FF6D00"),
"B5": HexColor("#E040FB"),
"B6": HexColor("#FF5252"),
"B7": HexColor("#FFAB40"),
"B8": HexColor("#B2FF59"),
"B9": HexColor("#EA80FC"),
"B10": HexColor("#80D8FF"),
"B13": HexColor("#CCFF90"), # B1+3
"B78": HexColor("#FFD180"), # B7+8
}
# ═════════════════════════════════════════════════════════════════════════════
# UTILITY FUNCTIONS
# ═════════════════════════════════════════════════════════════════════════════
def crop_marks(c, x0, y0, cw, ch, bleed, size=4*mm, gap=1.5*mm):
"""Draw crop marks at all four corners."""
c.setStrokeColor(black)
c.setLineWidth(0.4)
corners = [
(x0, y0), (x0+cw, y0),
(x0, y0+ch), (x0+cw, y0+ch),
]
for (cx2, cy2) in corners:
# horizontal mark
dx = 1 if cx2 == x0 else -1
dy = 1 if cy2 == y0 else -1
# H
c.line(cx2 + dx*(gap+bleed), cy2,
cx2 + dx*(gap+bleed+size), cy2)
# V
c.line(cx2, cy2 + dy*(gap+bleed),
cx2, cy2 + dy*(gap+bleed+size))
def lam_border(c, x0, y0, cw, ch, lam, color):
"""Dashed lamination guide border."""
c.setStrokeColor(color)
c.setLineWidth(0.6)
c.setDash([2, 3])
c.roundRect(x0+lam, y0+lam, cw-2*lam, ch-2*lam, 3*mm, fill=0, stroke=1)
c.setDash([])
def draw_clock(c, cx, cy, r, segments, bg):
"""
Draw a single clock face.
segments = list of (code, label, hour_float_or_range_str, color)
bg = background color of page (for contrast)
"""
# Shadow
c.setFillColor(HexColor("#000000"))
c.setFillAlpha(0.3)
c.circle(cx+1.5, cy-1.5, r, fill=1, stroke=0)
c.setFillAlpha(1.0)
# Face
c.setFillColor(HexColor("#1C2E40"))
c.setStrokeColor(GOLD)
c.setLineWidth(1.8)
c.circle(cx, cy, r, fill=1, stroke=1)
# Hour ticks
for h in range(12):
angle = math.radians(90 - h * 30)
is_major = (h % 3 == 0)
r_in = r * (0.75 if is_major else 0.83)
r_out = r * 0.93
c.setStrokeColor(GOLD if is_major else LGRAY)
c.setLineWidth(1.2 if is_major else 0.5)
c.line(cx + r_in*math.cos(angle), cy + r_in*math.sin(angle),
cx + r_out*math.cos(angle), cy + r_out*math.sin(angle))
# Hour numbers at 12,3,6,9
c.setFont("Helvetica-Bold", 6)
c.setFillColor(GOLD)
for h in [12, 3, 6, 9]:
angle = math.radians(90 - (h % 12) * 30)
xn = cx + r*0.65*math.cos(angle)
yn = cy + r*0.65*math.sin(angle) - 2
c.drawCentredString(xn, yn, str(h))
# Center hub
c.setFillColor(GOLD)
c.circle(cx, cy, r*0.07, fill=1, stroke=0)
# Segments
for (code, label, hour_val, col) in segments:
if isinstance(hour_val, str) and "-" in hour_val:
lo, hi = [float(x) for x in hour_val.split("-")]
hf = (lo + hi) / 2.0
else:
hf = float(hour_val)
angle = math.radians(90 - hf * 30)
# Spoke
c.setStrokeColor(col)
c.setLineWidth(1.8)
x_end = cx + r*0.72*math.cos(angle)
y_end = cy + r*0.72*math.sin(angle)
c.line(cx, cy, x_end, y_end)
# Rim dot
xd = cx + r*0.89*math.cos(angle)
yd = cy + r*0.89*math.sin(angle)
c.setFillColor(col)
c.setStrokeColor(WHITE)
c.setLineWidth(0.7)
c.circle(xd, yd, 3.2, fill=1, stroke=1)
# Code inside dot
c.setFillColor(BG)
c.setFont("Helvetica-Bold", 4)
c.drawCentredString(xd, yd - 1.4, code)
def draw_legend_block(c, x, y_top, title, title_color, rows, row_h=9):
"""
rows = list of (code, full_name, clock_str, note, color)
Returns bottom y.
"""
block_w = 70 * mm
header_h = 8
# Title bar
c.setFillColor(title_color)
c.roundRect(x, y_top - header_h, block_w, header_h, 1.5*mm, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 7)
c.drawCentredString(x + block_w/2, y_top - header_h + 2.2, title)
# Column headers
cols = [10*mm, 28*mm, 16*mm, 16*mm]
hdr_y = y_top - header_h - row_h
c.setFillColor(HexColor("#1A2A3A"))
c.rect(x, hdr_y, block_w, row_h, fill=1, stroke=0)
c.setFillColor(GOLD)
c.setFont("Helvetica-Bold", 5.5)
hdr_labels = ["Code", "Segment Name", "Clock", "Note"]
xc = x
for i, hl in enumerate(hdr_labels):
c.drawString(xc + 1.5, hdr_y + 2.5, hl)
xc += cols[i]
# Data rows
for ri, (code, name, clk, note, col) in enumerate(rows):
ry = y_top - header_h - row_h*(ri+2)
alt = HexColor("#0F1F2F") if ri % 2 == 0 else HexColor("#162535")
c.setFillColor(alt)
c.rect(x, ry, block_w, row_h, fill=1, stroke=0)
# color stripe
c.setFillColor(col)
c.rect(x, ry, 3, row_h, fill=1, stroke=0)
# code
c.setFillColor(col)
c.setFont("Helvetica-Bold", 6)
c.drawString(x + 4, ry + 2.3, code)
# name
c.setFillColor(OFF_WHITE)
c.setFont("Helvetica", 5.8)
c.drawString(x + cols[0] + 1, ry + 2.3, name)
# clock
c.setFillColor(GOLD)
c.setFont("Helvetica-Bold", 5.8)
c.drawString(x + cols[0]+cols[1] + 1, ry + 2.3, clk)
# note
c.setFillColor(MGRAY)
c.setFont("Helvetica", 5)
c.drawString(x + cols[0]+cols[1]+cols[2] + 1, ry + 2.3, note)
# border
total_h = header_h + row_h*(len(rows)+1)
c.setStrokeColor(title_color)
c.setLineWidth(0.6)
c.roundRect(x, y_top - total_h, block_w, total_h, 1.5*mm, fill=0, stroke=1)
return y_top - total_h
def page_background(c, bg_color, x0, y0, cw, ch):
c.setFillColor(bg_color)
c.roundRect(x0, y0, cw, ch, 4*mm, fill=1, stroke=0)
def page_header(c, x0, top_y, cw, title, subtitle, hdr_color):
hh = 11*mm
c.setFillColor(hdr_color)
c.roundRect(x0, top_y - hh, cw, hh, 3*mm, fill=1, stroke=0)
# cover bottom radius so it looks flush against body
c.rect(x0, top_y - hh, cw, hh/2, fill=1, stroke=0)
c.setFillColor(WHITE)
c.setFont("Helvetica-Bold", 10)
c.drawCentredString(x0 + cw/2, top_y - 7, title)
c.setFont("Helvetica", 6)
c.setFillColor(HexColor("#FFCCBC"))
c.drawCentredString(x0 + cw/2, top_y - 12.5, subtitle)
def page_footer(c, x0, y0, cw, text_left, text_right):
fh = 7*mm
c.setFillColor(LGRAY)
c.rect(x0, y0, cw, fh, fill=1, stroke=0)
# cover top radius
c.setFillColor(LGRAY)
c.rect(x0, y0 + fh/2, cw, fh/2, fill=1, stroke=0)
c.setFillColor(GOLD)
c.setFont("Helvetica-BoldOblique", 6)
c.drawString(x0 + 3*mm, y0 + 2.3, text_left)
c.setFillColor(OFF_WHITE)
c.setFont("Helvetica", 5.5)
c.drawRightString(x0 + cw - 3*mm, y0 + 2.3, text_right)
def pearls_box(c, x, y_top, w, pearls):
h = len(pearls)*7 + 10
c.setFillColor(HexColor("#1A1A00"))
c.setStrokeColor(GOLD)
c.setLineWidth(0.8)
c.roundRect(x, y_top - h, w, h, 2*mm, fill=1, stroke=1)
c.setFillColor(GOLD)
c.setFont("Helvetica-Bold", 6.5)
c.drawString(x + 3, y_top - 8, "★ CLINICAL PEARLS")
c.setFont("Helvetica", 5.5)
c.setFillColor(OFF_WHITE)
for i, p in enumerate(pearls):
c.drawString(x + 3, y_top - 15 - i*7, p)
# ═════════════════════════════════════════════════════════════════════════════
# DRAW PAGES
# ═════════════════════════════════════════════════════════════════════════════
c = canvas.Canvas(OUT, pagesize=A4)
c.setTitle("Bronchoscopy OR Laminated Card")
c.setAuthor("Orris Medical")
for page_num in [1, 2]:
# Card occupies centre of A4
X0 = (PW - CARD_W) / 2
Y0 = (PH - CARD_H) / 2
CW = CARD_W
CH = CARD_H
# ── White background for crop marks ──────────────────────────────────────
c.setFillColor(white)
c.rect(0, 0, PW, PH, fill=1, stroke=0)
# ── "FRONT / BACK" print instruction ─────────────────────────────────────
side = "FRONT – RIGHT LUNG" if page_num == 1 else "BACK – LEFT LUNG"
c.setFillColor(black)
c.setFont("Helvetica-Bold", 7)
c.drawCentredString(PW/2, Y0 - BLEED - 8*mm, f"[ {side} ] Print on card stock · Cut on crop marks · Laminate")
# ── Card body ────────────────────────────────────────────────────────────
bg = BG if page_num == 1 else BG2
page_background(c, bg, X0, Y0, CW, CH)
# Lamination border
lam_border(c, X0, Y0, CW, CH, LAM, LAM_COLOR)
# Crop marks (outside bleed)
crop_marks(c, X0 - BLEED, Y0 - BLEED, CW + 2*BLEED, CH + 2*BLEED, BLEED)
# ── Header ───────────────────────────────────────────────────────────────
if page_num == 1:
page_header(c, X0, Y0+CH, CW,
"RIGHT LUNG · 10 Bronchopulmonary Segments",
"Bronchoscopic Clock Positions · Fischer's Mastery of Surgery 8e",
HEADER_R)
else:
page_header(c, X0, Y0+CH, CW,
"LEFT LUNG · 8 Bronchopulmonary Segments",
"Bronchoscopic Clock Positions · Fischer's Mastery of Surgery 8e",
HEADER_L)
# ── Footer ───────────────────────────────────────────────────────────────
page_footer(c, X0, Y0, CW,
"Orris Medical | OR Quick Reference",
"Right: 10 segs · Left: 8 segs (US nomenclature)")
CONTENT_TOP = Y0 + CH - 11*mm - 2*mm
CONTENT_BOT = Y0 + 7*mm + 2*mm
USABLE_H = CONTENT_TOP - CONTENT_BOT
# ═══════════════════════════════════════════════════════════════════════
# PAGE 1 – RIGHT LUNG
# ═══════════════════════════════════════════════════════════════════════
if page_num == 1:
# 3 clock faces + legend blocks arranged left→right
R_CLOCK = 14.5 * mm
# Y centres for 3 lobes
lobe_y = [
CONTENT_TOP - USABLE_H*0.18,
CONTENT_TOP - USABLE_H*0.52,
CONTENT_TOP - USABLE_H*0.83,
]
# X for clocks (left side), X for legends (right side)
CX = X0 + LAM + R_CLOCK + 4*mm
LX = X0 + LAM + 2*R_CLOCK + 12*mm
# ── RUL ─────────────────────────────────────────────────────────────
rul_segs = [
("B1", "Apical", 4.0, SEG_COLORS["B1"]),
("B2", "Anterior", 12.0, SEG_COLORS["B2"]),
("B3", "Posterior", 8.0, SEG_COLORS["B3"]),
]
draw_clock(c, CX, lobe_y[0], R_CLOCK,
[(s[0], s[0], s[2], s[3]) for s in rul_segs], bg)
# lobe label
c.setFillColor(HEADER_R)
c.setFont("Helvetica-Bold", 7.5)
c.drawCentredString(CX, lobe_y[0] + R_CLOCK + 5, "RIGHT UPPER LOBE")
draw_legend_block(c, LX, lobe_y[0] + R_CLOCK + 13,
"RIGHT UPPER LOBE (B1–B3)",
HEADER_R,
[(s[0], s[1], f"{s[2]} o'cl", "", s[3]) for s in rul_segs])
# ── RML ─────────────────────────────────────────────────────────────
rml_segs = [
("B4", "Lateral", "3-6", SEG_COLORS["B4"]),
("B5", "Medial", "9-12", SEG_COLORS["B5"]),
]
draw_clock(c, CX, lobe_y[1], R_CLOCK,
[(s[0], s[0], s[2], s[3]) for s in rml_segs], bg)
c.setFillColor(HexColor("#F06292"))
c.setFont("Helvetica-Bold", 7.5)
c.drawCentredString(CX, lobe_y[1] + R_CLOCK + 5, "RIGHT MIDDLE LOBE")
draw_legend_block(c, LX, lobe_y[1] + R_CLOCK + 13,
"RIGHT MIDDLE LOBE (B4–B5)",
HexColor("#C62828"),
[(s[0], s[1], s[2], "Anterior orifice", s[3]) for s in rml_segs])
# ── RLL ─────────────────────────────────────────────────────────────
rll_segs = [
("B6", "Superior", 5.0, SEG_COLORS["B6"]),
("B7", "Med. Basal", 9.0, SEG_COLORS["B7"]),
("B8", "Ant. Basal", 1.0, SEG_COLORS["B8"]),
("B9", "Lat. Basal", 3.0, SEG_COLORS["B9"]),
("B10", "Post. Basal", 6.0, SEG_COLORS["B10"]),
]
draw_clock(c, CX, lobe_y[2], R_CLOCK,
[(s[0], s[0], s[2], s[3]) for s in rll_segs], bg)
c.setFillColor(HexColor("#66BB6A"))
c.setFont("Helvetica-Bold", 7.5)
c.drawCentredString(CX, lobe_y[2] + R_CLOCK + 5, "RIGHT LOWER LOBE")
rll_notes = ["Opp. RML ★", "Most proximal", "", "", ""]
draw_legend_block(c, LX, lobe_y[2] + R_CLOCK + 13,
"RIGHT LOWER LOBE (B6–B10)",
HexColor("#1B5E20"),
[(s[0], s[1], f"{s[2]} o'cl", rll_notes[i], s[3])
for i, s in enumerate(rll_segs)])
# ── Pearls right side ─────────────────────────────────────────────
prl_x = LX + 70*mm + 4*mm
prl_w = X0 + CW - LAM - prl_x - 2*mm
if prl_w > 10*mm:
pearls = [
"• RMB shorter/more vertical",
" → ETT misplacement goes right",
"• B6 (RLL) is directly OPPOSITE",
" the RML orifice (posterior)",
"• B7 Medial basal = most proximal",
" of the basal segments",
"• Subapical RLL seg. in >50% pts",
" (arises between B6 & basals)",
"• Carina widened/blunted =",
" subcarinal LN enlargement",
]
pearls_box(c, prl_x, CONTENT_TOP, prl_w, pearls)
# ═══════════════════════════════════════════════════════════════════════
# PAGE 2 – LEFT LUNG
# ═══════════════════════════════════════════════════════════════════════
else:
R_CLOCK = 17.5 * mm
lobe_y = [
CONTENT_TOP - USABLE_H*0.28,
CONTENT_TOP - USABLE_H*0.74,
]
CX = X0 + LAM + R_CLOCK + 4*mm
LX = X0 + LAM + 2*R_CLOCK + 14*mm
# ── LUL (incl. Lingula) ─────────────────────────────────────────────
lul_segs = [
("AP", "Apicoposterior", 8.0, SEG_COLORS["B13"]),
("B2", "Anterior", 12.0, SEG_COLORS["B2"]),
("B4", "Sup. Lingular", 10.0, SEG_COLORS["B4"]),
("B5", "Inf. Lingular", 2.0, SEG_COLORS["B5"]),
]
draw_clock(c, CX, lobe_y[0], R_CLOCK,
[(s[0], s[0], s[2], s[3]) for s in lul_segs], bg)
c.setFillColor(HexColor("#CE93D8"))
c.setFont("Helvetica-Bold", 7.5)
c.drawCentredString(CX, lobe_y[0] + R_CLOCK + 6, "LEFT UPPER LOBE + LINGULA")
lul_notes = ["Upper div. (fused B1+3)", "Upper div.", "Lingular div.", "Lingular div."]
draw_legend_block(c, LX, lobe_y[0] + R_CLOCK + 14,
"LEFT UPPER LOBE (B1+3, B2, B4, B5)",
HexColor("#4A148C"),
[(s[0], s[1], f"{s[2]} o'cl", lul_notes[i], s[3])
for i, s in enumerate(lul_segs)],
row_h=9)
# ── LLL ─────────────────────────────────────────────────────────────
lll_segs = [
("B6", "Superior", 6.0, SEG_COLORS["B6"]),
("AM", "Anteromedial B.", 12.0, SEG_COLORS["B78"]),
("B9", "Lat. Basal", 9.0, SEG_COLORS["B9"]),
("B10", "Post. Basal", 5.0, SEG_COLORS["B10"]),
]
draw_clock(c, CX, lobe_y[1], R_CLOCK,
[(s[0], s[0], s[2], s[3]) for s in lll_segs], bg)
c.setFillColor(HexColor("#80CBC4"))
c.setFont("Helvetica-Bold", 7.5)
c.drawCentredString(CX, lobe_y[1] + R_CLOCK + 6, "LEFT LOWER LOBE")
lll_notes = ["Post. opp. LUL ★", "Fused B7+B8 on left", "", ""]
draw_legend_block(c, LX, lobe_y[1] + R_CLOCK + 14,
"LEFT LOWER LOBE (B6, B7+8, B9, B10)",
HexColor("#00695C"),
[(s[0], s[1], f"{s[2]} o'cl", lll_notes[i], s[3])
for i, s in enumerate(lll_segs)],
row_h=9)
# ── Pearls right side ─────────────────────────────────────────────
prl_x = LX + 70*mm + 4*mm
prl_w = X0 + CW - LAM - prl_x - 2*mm
if prl_w > 10*mm:
pearls = [
"• LMB longer, ~45° from trachea",
" (vs 20–30° on right)",
"• Aortic pulsation often visible",
" through LMB wall – normal!",
"• LUL quick bifurcation into:",
" Upper div. + Lingular div.",
"• Lingular = left-side analog",
" of right middle lobe",
"• B7+B8 fused as anteromedial",
" basal on left (US nomenclature)",
"• Left = 8 segs (US) or 10 (UK)",
]
pearls_box(c, prl_x, CONTENT_TOP, prl_w, pearls)
# ── Comparison table at bottom ──────────────────────────────────
comp_y = CONTENT_BOT + 2*mm
comp_x = X0 + LAM + 2*mm
comp_w = CW - 2*LAM - 4*mm
comp_h = 14*mm
c.setFillColor(HexColor("#0D1B2A"))
c.setStrokeColor(GOLD)
c.setLineWidth(0.6)
c.roundRect(comp_x, comp_y, comp_w, comp_h, 2*mm, fill=1, stroke=1)
c.setFillColor(GOLD)
c.setFont("Helvetica-Bold", 6.5)
c.drawCentredString(comp_x + comp_w/2, comp_y + comp_h - 6, "SIDE-BY-SIDE COMPARISON")
rows = [
("Feature", "RIGHT LUNG", "LEFT LUNG"),
("Main bronchus", "Short, ~20–30° from trachea", "Long (~5 cm), ~45° from trachea"),
("Lobes", "3 (Upper, Middle, Lower)", "2 (Upper + Lingula, Lower)"),
("Segments", "10 (US + UK nomenclature)", "8 (US) / 10 (UK)"),
("Unique structure", "Bronchus intermedius", "No bronchus intermedius"),
]
c.setFont("Helvetica", 5.3)
for ri, (a, b, cc) in enumerate(rows):
ry2 = comp_y + comp_h - 6 - (ri+1)*8
if ry2 < comp_y + 1: break
fw = comp_w / 3
c.setFillColor(GOLD if ri==0 else OFF_WHITE)
c.setFont("Helvetica-Bold" if ri==0 else "Helvetica", 5.3)
c.drawString(comp_x + 2, ry2, a)
c.setFillColor(HexColor("#FF8A80") if ri>0 else GOLD)
c.drawString(comp_x + fw + 2, ry2, b)
c.setFillColor(HexColor("#80D8FF") if ri>0 else GOLD)
c.drawString(comp_x + 2*fw + 2, ry2, cc)
c.showPage()
c.save()
print(f"Saved: {OUT}")
python3 /home/daytona/workspace/bronchoscopy-card/make_or_card.py 2>&1
ls -lh /home/daytona/workspace/bronchoscopy-card/
bronchoscopy OR laminated
PDF Document · PDF
| Feature | Detail |
|---|---|
| Background | Dark navy/teal - high contrast under OR lights, minimal glare |
| Text | White/gold on dark - readable through sterile gloves |
| Fonts | Bold, oversized segment codes on clock dots |
| Crop marks | 3 mm bleed, corner marks for clean cutting |
| Lamination guide | Cyan dashed border 5 mm inside the card edge - align your lamination pouch to this line |
| Rounded corners | 4 mm radius on card body - standard lamination pouch safe zone |
| Colour coding | Vivid, saturated colours (not pastels) - distinguishable under yellow OR light |
| Print instructions | Labelled at bottom of each page so no confusion front/back |
Diagram presentation
Video
Video of entire procedure including representation of lobes and divisions
python3 -c "import matplotlib; import numpy; print('matplotlib', matplotlib.__ve…
~/bronchoscopy-card/make_bronchoscopy_video.py
#!/usr/bin/env python3
"""
Bronchoscopy Animated Video
============================
Simulates a camera journey through the tracheobronchial tree.
Each scene shows:
LEFT panel – bronchial tree diagram with animated camera dot travelling
RIGHT panel – bronchoscope "tunnel view" (circular, dark, pulsing)
BOTTOM BAR – current location text + clock-face orifice mini-diagram
Output: bronchoscopy_procedure.mp4 (1280×720, 24 fps)
"""
import os, math, numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
from matplotlib.patches import FancyArrowPatch, Circle, FancyBboxPatch, Wedge
from matplotlib.collections import LineCollection
from matplotlib.colors import to_rgba
import subprocess, glob as globmod, shutil
OUT_DIR = "/home/daytona/workspace/bronchoscopy-card/frames"
OUT_VID = "/home/daytona/workspace/bronchoscopy-card/bronchoscopy_procedure.mp4"
FPS = 24
W_PX, H_PX = 1280, 720
DPI = 96
FIG_W = W_PX / DPI
FIG_H = H_PX / DPI
os.makedirs(OUT_DIR, exist_ok=True)
# ─────────────────────────────────────────────────────────────────────────────
# COLOUR PALETTE
# ─────────────────────────────────────────────────────────────────────────────
BG = "#0D1B2A"
PANEL_BG = "#111E2D"
GOLD = "#FFD600"
WHITE = "#F5F5F5"
MGRAY = "#888888"
TRACHEA_C = "#B0BEC5"
CARINA_C = "#FFD600"
LOBE_COLORS = {
"trachea": "#B0BEC5",
"carina": "#FFD600",
"RMB": "#EF9A9A",
"RUL": "#42A5F5",
"RML": "#EC407A",
"RI": "#CE93D8", # bronchus intermedius
"RLL": "#66BB6A",
"LMB": "#80DEEA",
"LUL": "#AB47BC",
"LING": "#FF8A65",
"LLL": "#26C6DA",
}
SEG_COLORS = {
"B1": "#40C4FF", "B2": "#69F0AE", "B3": "#FFD740",
"B4": "#FF6D00", "B5": "#E040FB",
"B6": "#FF5252", "B7": "#FFAB40", "B8": "#B2FF59",
"B9": "#EA80FC", "B10":"#80D8FF",
"B13":"#CCFF90", "B78":"#FFD180",
"B4L":"#FF8A65","B5L":"#FFCC80",
}
# ─────────────────────────────────────────────────────────────────────────────
# BRONCHIAL TREE NODE POSITIONS (x, y) in [0,1] diagram space
# ─────────────────────────────────────────────────────────────────────────────
NODES = {
# trachea column x=0.50
"NOSE": (0.50, 0.98),
"VC": (0.50, 0.92), # vocal cords
"TRA": (0.50, 0.82), # trachea mid
"CAR": (0.50, 0.70), # carina
# right
"RMB": (0.65, 0.62),
"RUL": (0.75, 0.55),
"B1": (0.83, 0.58),
"B2": (0.80, 0.50),
"B3": (0.76, 0.45),
"RI": (0.66, 0.52), # bronchus intermedius
"RML": (0.73, 0.43),
"B4": (0.78, 0.40),
"B5": (0.74, 0.35),
"RLL": (0.66, 0.40),
"B6": (0.72, 0.33),
"B7": (0.70, 0.26),
"B8": (0.67, 0.20),
"B9": (0.63, 0.15),
"B10": (0.60, 0.10),
# left
"LMB": (0.35, 0.60),
"LUL": (0.25, 0.52),
"LUU": (0.19, 0.48), # upper division
"B13": (0.14, 0.44),
"B2L": (0.18, 0.38),
"LING": (0.23, 0.41),
"B4L": (0.20, 0.33),
"B5L": (0.26, 0.28),
"LLL": (0.34, 0.43),
"B6L": (0.29, 0.36),
"B78": (0.32, 0.28),
"B9L": (0.37, 0.22),
"B10L": (0.40, 0.15),
}
# Edges of the tree (parent → child)
EDGES = [
("NOSE","VC"),("VC","TRA"),("TRA","CAR"),
("CAR","RMB"),("RMB","RUL"),("RUL","B1"),("RUL","B2"),("RUL","B3"),
("RMB","RI"),("RI","RML"),("RML","B4"),("RML","B5"),
("RI","RLL"),("RLL","B6"),("RLL","B7"),("RLL","B8"),("RLL","B9"),("RLL","B10"),
("CAR","LMB"),("LMB","LUL"),("LUL","LUU"),("LUU","B13"),("LUU","B2L"),
("LUL","LING"),("LING","B4L"),("LING","B5L"),
("LMB","LLL"),("LLL","B6L"),("LLL","B78"),("LLL","B9L"),("LLL","B10L"),
]
# ─────────────────────────────────────────────────────────────────────────────
# SCENES – each describes one "stop" on the journey
# ─────────────────────────────────────────────────────────────────────────────
SCENES = [
# (node, lobe_key, title, subtitle, clock_hour, clock_label, seg_color, n_frames)
("NOSE", "trachea",
"ENTRY – Nasal Passage",
"Scope passed via inferior nasal meatus → nasopharynx",
None, None, WHITE, 36),
("VC", "trachea",
"LARYNX – Vocal Cords",
"Tip angled 60–90° caudally through laryngeal aditus",
None, None, WHITE, 36),
("TRA", "trachea",
"TRACHEA",
"11 cm long · 2–2.5 cm wide · C-rings anterior · Smooth posterior wall",
None, None, TRACHEA_C, 42),
("CAR", "carina",
"CARINA (T4/5)",
"Sharp vertical keel · Right opens right · Left opens left · Widening = subcarinal LN",
None, None, GOLD, 48),
# ── RIGHT SIDE ──────────────────────────────────────────────────────────
("RMB", "RMB",
"RIGHT MAIN BRONCHUS",
"Shorter · More vertical (20–30°) · ETT misplacement risk",
None, None, LOBE_COLORS["RMB"], 36),
("RUL", "RUL",
"RIGHT UPPER LOBE · 3 segments",
"Orifice lateral, 2–3 cm from carina",
None, None, LOBE_COLORS["RUL"], 30),
("B1", "RUL",
"B1 – Apical Segment",
"Orifice at 4 o'clock",
4.0, "B1\nApical", SEG_COLORS["B1"], 36),
("B2", "RUL",
"B2 – Anterior Segment",
"Orifice at 12 o'clock",
12.0, "B2\nAnterior", SEG_COLORS["B2"], 36),
("B3", "RUL",
"B3 – Posterior Segment",
"Orifice at 8 o'clock",
8.0, "B3\nPosterior", SEG_COLORS["B3"], 36),
("RI", "RI",
"BRONCHUS INTERMEDIUS",
"Unique to right side · ~2 cm · Carries to RML + RLL",
None, None, LOBE_COLORS["RI"], 30),
("RML", "RML",
"RIGHT MIDDLE LOBE · 2 segments",
"Orifice ANTERIOR in intermediate bronchus",
None, None, LOBE_COLORS["RML"], 30),
("B4", "RML",
"B4 – Lateral Segment",
"Orifice at 3–6 o'clock",
4.5, "B4\nLateral", SEG_COLORS["B4"], 36),
("B5", "RML",
"B5 – Medial Segment",
"Orifice at 9–12 o'clock",
10.5, "B5\nMedial", SEG_COLORS["B5"], 36),
("RLL", "RLL",
"RIGHT LOWER LOBE · 5 segments",
"B6 directly OPPOSITE RML (posterior sentinel)",
None, None, LOBE_COLORS["RLL"], 30),
("B6", "RLL",
"B6 – Superior (Apical) Segment",
"Orifice at 5 o'clock · DIRECTLY opposite RML",
5.0, "B6\nSuperior", SEG_COLORS["B6"], 36),
("B7", "RLL",
"B7 – Medial Basal Segment",
"Orifice at 9 o'clock · Most proximal basal",
9.0, "B7\nMed.Basal", SEG_COLORS["B7"], 36),
("B8", "RLL",
"B8 – Anterior Basal Segment",
"Orifice at 1 o'clock",
1.0, "B8\nAnt.Basal", SEG_COLORS["B8"], 36),
("B9", "RLL",
"B9 – Lateral Basal Segment",
"Orifice at 3 o'clock",
3.0, "B9\nLat.Basal", SEG_COLORS["B9"], 36),
("B10", "RLL",
"B10 – Posterior Basal Segment",
"Orifice at 6 o'clock",
6.0, "B10\nPost.Basal", SEG_COLORS["B10"], 36),
# ── RETURN TO CARINA ────────────────────────────────────────────────────
("CAR", "carina",
"RETURN TO CARINA",
"Withdrawing · Now entering LEFT main bronchus",
None, None, GOLD, 30),
# ── LEFT SIDE ────────────────────────────────────────────────────────────
("LMB", "LMB",
"LEFT MAIN BRONCHUS",
"Longer (~5 cm) · ~45° angle · Aortic pulsation visible through wall",
None, None, LOBE_COLORS["LMB"], 36),
("LUL", "LUL",
"LEFT UPPER LOBE · 4 segments",
"Orifice superior + lateral to LLL · Quickly bifurcates",
None, None, LOBE_COLORS["LUL"], 30),
("LUU", "LUL",
"UPPER DIVISION",
"B1+3 Apicoposterior + B2 Anterior",
None, None, LOBE_COLORS["LUL"], 24),
("B13", "LUL",
"B1+3 – Apicoposterior Segment",
"Orifice at 8 o'clock · Fused on left (no separate apical/posterior)",
8.0, "B1+3\nApicoPosterior", SEG_COLORS["B13"], 36),
("B2L", "LUL",
"B2 – Anterior Segment (Left)",
"Orifice at 12 o'clock",
12.0, "B2\nAnterior", SEG_COLORS["B2"], 36),
("LING", "LING",
"LINGULAR DIVISION",
"Left-side analog of right middle lobe · Tongue-like projection anteriorly",
None, None, LOBE_COLORS["LING"], 30),
("B4L", "LING",
"B4 – Superior Lingular Segment",
"Orifice at 10 o'clock",
10.0, "B4\nSup.Ling", SEG_COLORS["B4L"], 36),
("B5L", "LING",
"B5 – Inferior Lingular Segment",
"Orifice at 2 o'clock",
2.0, "B5\nInf.Ling", SEG_COLORS["B5L"], 36),
("LLL", "LLL",
"LEFT LOWER LOBE · 4 segments",
"B6 Superior posterior · B7+8 fused as Anteromedial Basal on left",
None, None, LOBE_COLORS["LLL"], 30),
("B6L", "LLL",
"B6 – Superior (Apical) Segment",
"Orifice at 6 o'clock · Posterior to LUL",
6.0, "B6\nSuperior", SEG_COLORS["B6"], 36),
("B78", "LLL",
"B7+8 – Anteromedial Basal Segment",
"Orifice at 12 o'clock · B7 & B8 fused on left",
12.0, "B7+8\nAnteromedial", SEG_COLORS["B78"], 36),
("B9L", "LLL",
"B9 – Lateral Basal Segment",
"Orifice at 9 o'clock",
9.0, "B9\nLat.Basal", SEG_COLORS["B9"], 36),
("B10L", "LLL",
"B10 – Posterior Basal Segment",
"Orifice at 5 o'clock",
5.0, "B10\nPost.Basal", SEG_COLORS["B10"], 36),
# ── END ──────────────────────────────────────────────────────────────────
("CAR", "carina",
"PROCEDURE COMPLETE",
"All lobes & segments surveyed · Withdrawing bronchoscope",
None, None, GOLD, 48),
]
# ─────────────────────────────────────────────────────────────────────────────
# HELPER: draw bronchial tree (left panel)
# ─────────────────────────────────────────────────────────────────────────────
def draw_tree(ax, active_node, active_lobe, visited_nodes, progress_frac):
ax.set_facecolor(PANEL_BG)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis("off")
# Draw edges
for (p, ch) in EDGES:
px, py = NODES[p]
cx2, cy2 = NODES[ch]
# colour based on visited status
n_visited = ch in visited_nodes
p_visited = p in visited_nodes
if n_visited and p_visited:
col = LOBE_COLORS.get(active_lobe, WHITE)
alpha = 0.9
lw = 2.2
elif p_visited:
col = GOLD
alpha = 0.7
lw = 2.0
else:
col = "#2A3A4A"
alpha = 0.5
lw = 1.2
ax.plot([px, cx2], [py, cy2], color=col, lw=lw, alpha=alpha,
solid_capstyle="round", zorder=2)
# Draw nodes
for nid, (nx, ny) in NODES.items():
if nid == active_node:
# pulsing glow
for r, a in [(0.025, 0.15), (0.018, 0.3), (0.012, 1.0)]:
ax.add_patch(Circle((nx, ny), r,
color=GOLD, alpha=a, zorder=5,
transform=ax.transData))
elif nid in visited_nodes:
r = 0.007
col = LOBE_COLORS.get(active_lobe, WHITE)
ax.add_patch(Circle((nx, ny), r,
color=col, alpha=0.9, zorder=4,
transform=ax.transData))
else:
r = 0.005
ax.add_patch(Circle((nx, ny), r,
color="#2A3A4A", alpha=0.7, zorder=3,
transform=ax.transData))
# Label active node
nx, ny = NODES[active_node]
ax.text(nx, ny - 0.05, active_node.replace("L","L ") ,
ha="center", va="top", fontsize=7, color=GOLD,
fontweight="bold", zorder=6,
path_effects=[pe.withStroke(linewidth=2, foreground=PANEL_BG)])
# Progress bar at bottom of tree panel
bar_y = 0.02
ax.add_patch(FancyBboxPatch((0.05, bar_y), 0.90, 0.018,
boxstyle="round,pad=0.002",
linewidth=0.5, edgecolor="#333",
facecolor="#1A2A3A", zorder=7,
transform=ax.transData))
ax.add_patch(FancyBboxPatch((0.05, bar_y), 0.90 * progress_frac, 0.018,
boxstyle="round,pad=0.002",
linewidth=0, edgecolor="none",
facecolor=GOLD, alpha=0.85, zorder=8,
transform=ax.transData))
ax.text(0.50, bar_y + 0.008, f"Progress {int(progress_frac*100)}%",
ha="center", va="center", fontsize=6, color=BG,
fontweight="bold", zorder=9,
transform=ax.transData)
ax.text(0.50, 0.995, "TRACHEOBRONCHIAL TREE",
ha="center", va="top", fontsize=8, color=GOLD,
fontweight="bold",
path_effects=[pe.withStroke(linewidth=2, foreground=PANEL_BG)],
transform=ax.transData)
# ─────────────────────────────────────────────────────────────────────────────
# HELPER: bronchoscope tunnel view (right panel)
# ─────────────────────────────────────────────────────────────────────────────
def draw_tunnel(ax, lobe_key, title, subtitle, clock_hour, clock_label,
seg_color, frame_in_scene, total_frames):
ax.set_facecolor(BG)
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_aspect("equal")
ax.axis("off")
lobe_col = LOBE_COLORS.get(lobe_key, WHITE)
# Pulsing tunnel rings (simulate depth / breathing motion)
pulse = 0.5 + 0.5*math.sin(frame_in_scene / total_frames * 2 * math.pi)
for i in range(5, 0, -1):
r = 0.18*i + 0.03*pulse
alpha = max(0, 0.55 - i*0.08)
c_ring = lobe_col if i <= 2 else "#1A2A3A"
ax.add_patch(Circle((0, 0), r, color=c_ring, alpha=alpha, zorder=i))
# Inner airway lumen
ax.add_patch(Circle((0, 0), 0.18, color="#050D14", zorder=6))
# Mucosal texture – concentric faint arcs
for a in range(0, 360, 30):
rad = math.radians(a)
xs = [0.18*math.cos(rad)*t for t in np.linspace(0.3, 1, 5)]
ys = [0.18*math.sin(rad)*t for t in np.linspace(0.3, 1, 5)]
ax.plot(xs, ys, color=lobe_col, alpha=0.12, lw=0.7, zorder=7)
# Carina-like ridge if at CAR
if lobe_key == "carina":
ax.plot([0, 0], [-0.18, 0.03], color=GOLD, lw=2.5, alpha=0.9, zorder=8)
ax.text(0.22, 0.05, "RIGHT", fontsize=7, color=LOBE_COLORS["RMB"],
fontweight="bold", va="center")
ax.text(-0.55, 0.05, "LEFT", fontsize=7, color=LOBE_COLORS["LMB"],
fontweight="bold", va="center")
# ── Clock face (if we have an orifice) ────────────────────────────────
if clock_hour is not None:
r_clk = 0.72
# clock circle
ax.add_patch(Circle((0, 0), r_clk,
fill=False, edgecolor=GOLD, lw=1.0, alpha=0.4, zorder=9))
# ticks
for h in range(12):
ang = math.radians(90 - h * 30)
r0 = r_clk * (0.88 if h % 3 == 0 else 0.92)
ax.plot([r0*math.cos(ang), r_clk*math.cos(ang)],
[r0*math.sin(ang), r_clk*math.sin(ang)],
color=GOLD, alpha=0.45, lw=0.8, zorder=9)
# hour labels
for h in [12, 3, 6, 9]:
ang = math.radians(90 - (h % 12) * 30)
ax.text(r_clk*0.78*math.cos(ang), r_clk*0.78*math.sin(ang),
str(h), ha="center", va="center",
fontsize=6.5, color=GOLD, alpha=0.65, fontweight="bold")
# Orifice marker
if isinstance(clock_hour, (int, float)):
ang = math.radians(90 - clock_hour * 30)
else:
ang = math.radians(90 - float(clock_hour)*30)
# animated: dot pulses
dot_r = 0.045 + 0.015*pulse
xd = r_clk*0.93*math.cos(ang)
yd = r_clk*0.93*math.sin(ang)
ax.add_patch(Circle((xd, yd), dot_r+0.015,
color=seg_color, alpha=0.25, zorder=10))
ax.add_patch(Circle((xd, yd), dot_r,
color=seg_color, alpha=0.9, zorder=11))
# arrow from centre to orifice
ax.annotate("", xy=(xd*0.88, yd*0.88), xytext=(0, 0),
arrowprops=dict(arrowstyle="-|>",
color=seg_color, lw=1.6,
mutation_scale=12),
zorder=10)
# label
xl = r_clk*1.18*math.cos(ang)
yl = r_clk*1.18*math.sin(ang)
ax.text(xl, yl,
clock_label if clock_label else "",
ha="center", va="center",
fontsize=7, color=seg_color, fontweight="bold",
multialignment="center",
path_effects=[pe.withStroke(linewidth=2, foreground=BG)])
# Lobe badge top-left corner
ax.add_patch(FancyBboxPatch((-0.95, 0.78), 1.90, 0.18,
boxstyle="round,pad=0.03",
facecolor=lobe_col, alpha=0.18,
edgecolor=lobe_col, linewidth=0.8, zorder=12))
ax.text(0, 0.87, title,
ha="center", va="center",
fontsize=9.5, color=WHITE, fontweight="bold",
path_effects=[pe.withStroke(linewidth=2.5, foreground=BG)],
zorder=13)
ax.text(0, -0.89, subtitle,
ha="center", va="center",
fontsize=7, color=MGRAY,
path_effects=[pe.withStroke(linewidth=1.5, foreground=BG)],
zorder=13)
# ─────────────────────────────────────────────────────────────────────────────
# RENDER FRAMES
# ─────────────────────────────────────────────────────────────────────────────
total_scenes = len(SCENES)
total_frames_count = sum(s[7] for s in SCENES)
visited = set()
frame_idx = 0
frames_done = 0
for scene_i, (node, lobe, title, subtitle, clk_h, clk_lbl, seg_col, n_frames) in enumerate(SCENES):
visited.add(node)
progress = (frames_done + n_frames/2) / total_frames_count
for fi in range(n_frames):
fig = plt.figure(figsize=(FIG_W, FIG_H), facecolor=BG)
# ── layout: left=tree (40%), right=tunnel (60%) ───────────────────
ax_tree = fig.add_axes([0.00, 0.10, 0.38, 0.90]) # left panel
ax_tunnel = fig.add_axes([0.39, 0.10, 0.60, 0.90]) # right panel
# ── bottom bar ───────────────────────────────────────────────────
ax_bar = fig.add_axes([0.00, 0.00, 1.00, 0.10])
ax_bar.set_facecolor("#050D14")
ax_bar.axis("off")
# Side indicator
if node in ("NOSE","VC","TRA","CAR") or "return" in title.lower() or "complete" in title.lower():
side_text = "CENTRAL AIRWAY"
side_col = GOLD
elif node.startswith("L"):
side_text = "◀ LEFT LUNG"
side_col = LOBE_COLORS["LMB"]
else:
side_text = "RIGHT LUNG ▶"
side_col = LOBE_COLORS["RMB"]
ax_bar.text(0.01, 0.55, side_text, fontsize=9, color=side_col,
fontweight="bold", va="center", transform=ax_bar.transAxes)
ax_bar.text(0.50, 0.55, f"Scene {scene_i+1} / {total_scenes} · Location: {node}",
fontsize=8, color=MGRAY,
ha="center", va="center", transform=ax_bar.transAxes)
ax_bar.text(0.99, 0.55, "Orris Medical · Bronchoscopy Reference",
fontsize=7, color="#444", ha="right", va="center",
transform=ax_bar.transAxes)
# Scene sub-title row
ax_bar.text(0.50, 0.12, subtitle,
fontsize=7.5, color=WHITE, alpha=0.8,
ha="center", va="center", transform=ax_bar.transAxes)
draw_tree(ax_tree, node, lobe, visited, progress)
draw_tunnel(ax_tunnel, lobe, title, subtitle, clk_h, clk_lbl,
seg_col, fi, n_frames)
# Divider line
fig.add_artist(plt.Line2D([0.385, 0.385], [0.0, 1.0],
transform=fig.transFigure,
color=GOLD, linewidth=0.8, alpha=0.4))
fname = os.path.join(OUT_DIR, f"frame_{frame_idx:05d}.png")
fig.savefig(fname, dpi=DPI, facecolor=BG, bbox_inches="tight",
pad_inches=0)
plt.close(fig)
frame_idx += 1
frames_done += n_frames
print(f" Scene {scene_i+1:2d}/{total_scenes} [{node:6s}] {frames_done}/{total_frames_count} frames")
print(f"\nAll {frame_idx} frames rendered.")
# ─────────────────────────────────────────────────────────────────────────────
# ENCODE VIDEO
# ─────────────────────────────────────────────────────────────────────────────
print("Encoding video...")
cmd = [
"ffmpeg", "-y",
"-framerate", str(FPS),
"-i", os.path.join(OUT_DIR, "frame_%05d.png"),
"-vf", f"scale={W_PX}:{H_PX}:force_original_aspect_ratio=decrease,pad={W_PX}:{H_PX}:(ow-iw)/2:(oh-ih)/2:color={BG[1:]}",
"-c:v", "libx264",
"-preset", "fast",
"-crf", "20",
"-pix_fmt", "yuv420p",
"-movflags", "+faststart",
OUT_VID,
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print("ffmpeg stderr:", result.stderr[-2000:])
else:
size_mb = os.path.getsize(OUT_VID) / 1e6
print(f"Video saved: {OUT_VID} ({size_mb:.1f} MB)")
# Cleanup frames
shutil.rmtree(OUT_DIR)
print("Done.")
cd /home/daytona/workspace/bronchoscopy-card && python3 make_bronchoscopy_video.…
ls /home/daytona/workspace/bronchoscopy-card/frames/ 2>/dev/null | wc -l
~/bronchoscopy-card/make_video_fast.py
#!/usr/bin/env python3
"""
Bronchoscopy Animated Video - FAST VERSION
Renders fewer frames per scene, lower DPI → encodes quickly
"""
import os, math, numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
from matplotlib.patches import Circle, FancyBboxPatch
import subprocess, shutil
OUT_DIR = "/home/daytona/workspace/bronchoscopy-card/frames2"
OUT_VID = "/home/daytona/workspace/bronchoscopy-card/bronchoscopy_procedure.mp4"
FPS = 12
W_PX, H_PX = 1280, 720
DPI = 72 # lower DPI = much faster rendering
os.makedirs(OUT_DIR, exist_ok=True)
FIG_W = W_PX / DPI
FIG_H = H_PX / DPI
# ── Palette ──────────────────────────────────────────────────────────────────
BG = "#0D1B2A"
PANEL_BG = "#111E2D"
GOLD = "#FFD600"
WHITE = "#F5F5F5"
MGRAY = "#666666"
LOBE_COLORS = {
"trachea": "#B0BEC5", "carina": "#FFD600",
"RMB": "#EF9A9A", "RUL": "#42A5F5", "RML": "#EC407A",
"RI": "#CE93D8", "RLL": "#66BB6A",
"LMB": "#80DEEA", "LUL": "#AB47BC", "LING": "#FF8A65", "LLL": "#26C6DA",
}
SEG = {
"B1":"#40C4FF","B2":"#69F0AE","B3":"#FFD740",
"B4":"#FF6D00","B5":"#E040FB",
"B6":"#FF5252","B7":"#FFAB40","B8":"#B2FF59",
"B9":"#EA80FC","B10":"#80D8FF",
"B13":"#CCFF90","B78":"#FFD180",
"B4L":"#FF8A65","B5L":"#FFCC80",
}
# ── Tree nodes ────────────────────────────────────────────────────────────────
N = {
"NOSE":(0.50,0.98),"VC":(0.50,0.92),"TRA":(0.50,0.82),"CAR":(0.50,0.70),
"RMB":(0.65,0.62),"RUL":(0.75,0.55),
"B1":(0.83,0.58),"B2":(0.80,0.50),"B3":(0.76,0.45),
"RI":(0.66,0.52),"RML":(0.73,0.43),"B4":(0.78,0.40),"B5":(0.74,0.35),
"RLL":(0.66,0.40),"B6":(0.72,0.33),"B7":(0.70,0.26),
"B8":(0.67,0.20),"B9":(0.63,0.15),"B10":(0.60,0.10),
"LMB":(0.35,0.60),"LUL":(0.25,0.52),"LUU":(0.19,0.48),
"B13":(0.14,0.44),"B2L":(0.18,0.38),"LING":(0.23,0.41),
"B4L":(0.20,0.33),"B5L":(0.26,0.28),"LLL":(0.34,0.43),
"B6L":(0.29,0.36),"B78":(0.32,0.28),"B9L":(0.37,0.22),"B10L":(0.40,0.15),
}
EDGES = [
("NOSE","VC"),("VC","TRA"),("TRA","CAR"),
("CAR","RMB"),("RMB","RUL"),("RUL","B1"),("RUL","B2"),("RUL","B3"),
("RMB","RI"),("RI","RML"),("RML","B4"),("RML","B5"),
("RI","RLL"),("RLL","B6"),("RLL","B7"),("RLL","B8"),("RLL","B9"),("RLL","B10"),
("CAR","LMB"),("LMB","LUL"),("LUL","LUU"),("LUU","B13"),("LUU","B2L"),
("LUL","LING"),("LING","B4L"),("LING","B5L"),
("LMB","LLL"),("LLL","B6L"),("LLL","B78"),("LLL","B9L"),("LLL","B10L"),
]
# ── Scenes: (node, lobe, title, subtitle, clock_h, clock_lbl, seg_col, n_frames) ──
# n_frames reduced to 10-16 per scene (at 12fps = ~1-1.3 sec each)
SCENES = [
("NOSE","trachea","ENTRY – Nasal Passage","Scope via inferior nasal meatus → nasopharynx · tip angled 60–90° caudally",None,None,WHITE,12),
("VC","trachea","LARYNX – Vocal Cords","Through laryngeal aditus between vocal cords → proximal trachea",None,None,WHITE,12),
("TRA","trachea","TRACHEA","11 cm long · 2–2.5 cm wide · C-rings anterior · smooth posterior wall · corrugations widen at carina",None,None,"#B0BEC5",14),
("CAR","carina","CARINA (T4/5)","Sharp vertical keel · Widest anteriorly · Widening/blunting = subcarinal LN enlargement",None,None,GOLD,16),
("RMB","RMB","RIGHT MAIN BRONCHUS","Shorter · More vertical (~20–30°) · ETT preferentially enters here if passed too deep",None,None,"#EF9A9A",12),
("RUL","RUL","RIGHT UPPER LOBE · 3 segments","Orifice LATERAL · 2–3 cm from carina · occasionally directly opposite carina",None,None,"#42A5F5",10),
("B1","RUL","B1 – Apical Segment","Orifice at 4 o'clock · Toward apex of lung",4.0,"B1\nApical",SEG["B1"],12),
("B2","RUL","B2 – Anterior Segment","Orifice at 12 o'clock · Toward anterior chest wall",12.0,"B2\nAnterior",SEG["B2"],12),
("B3","RUL","B3 – Posterior Segment","Orifice at 8 o'clock · Toward posterior chest wall",8.0,"B3\nPosterior",SEG["B3"],12),
("RI","RI","BRONCHUS INTERMEDIUS","Unique to right side · ~2 cm · Bridge to RML (anterior) and RLL",None,None,"#CE93D8",10),
("RML","RML","RIGHT MIDDLE LOBE · 2 segments","Orifice ANTERIOR in intermediate bronchus",None,None,"#EC407A",10),
("B4","RML","B4 – Lateral Segment","Orifice at 3–6 o'clock · Anterior orifice",4.5,"B4\nLateral",SEG["B4"],12),
("B5","RML","B5 – Medial Segment","Orifice at 9–12 o'clock · Anterior orifice",10.5,"B5\nMedial",SEG["B5"],12),
("RLL","RLL","RIGHT LOWER LOBE · 5 segments","B6 directly OPPOSITE RML = posterior sentinel · Subapical segment in >50% pts",None,None,"#66BB6A",10),
("B6","RLL","B6 – Superior Segment","Orifice at 5 o'clock · Directly POSTERIOR, opposite RML orifice",5.0,"B6\nSuperior",SEG["B6"],12),
("B7","RLL","B7 – Medial Basal Segment","Orifice at 9 o'clock · Most proximal basal segment",9.0,"B7\nMed.Basal",SEG["B7"],12),
("B8","RLL","B8 – Anterior Basal Segment","Orifice at 1 o'clock",1.0,"B8\nAnt.Basal",SEG["B8"],12),
("B9","RLL","B9 – Lateral Basal Segment","Orifice at 3 o'clock",3.0,"B9\nLat.Basal",SEG["B9"],12),
("B10","RLL","B10 – Posterior Basal Segment","Orifice at 6 o'clock",6.0,"B10\nPost.Basal",SEG["B10"],12),
("CAR","carina","RETURN TO CARINA","Withdrawing from right lung · Now crossing to LEFT main bronchus",None,None,GOLD,12),
("LMB","LMB","LEFT MAIN BRONCHUS","Longer (~5 cm) · ~45° from trachea · Aortic pulsation often visible",None,None,"#80DEEA",12),
("LUL","LUL","LEFT UPPER LOBE · 4 segments","Orifice superior + lateral to LLL · Quickly bifurcates into Upper div. + Lingula",None,None,"#AB47BC",10),
("B13","LUL","B1+3 – Apicoposterior Segment","Orifice at 8 o'clock · Fused on left (no separate apical/posterior)",8.0,"B1+3\nApicoPosterior",SEG["B13"],12),
("B2L","LUL","B2 – Anterior Segment (Left)","Orifice at 12 o'clock · Upper division",12.0,"B2\nAnterior",SEG["B2"],12),
("LING","LING","LINGULAR DIVISION","Left analog of right middle lobe · Tongue-like projection anteriorly",None,None,"#FF8A65",10),
("B4L","LING","B4 – Superior Lingular Segment","Orifice at 10 o'clock · Lingular division",10.0,"B4\nSup.Ling",SEG["B4L"],12),
("B5L","LING","B5 – Inferior Lingular Segment","Orifice at 2 o'clock · Lingular division",2.0,"B5\nInf.Ling",SEG["B5L"],12),
("LLL","LLL","LEFT LOWER LOBE · 4 segments","B7+B8 fused as Anteromedial Basal on left (US nomenclature)",None,None,"#26C6DA",10),
("B6L","LLL","B6 – Superior Segment (Left)","Orifice at 6 o'clock · Posterior, opposite LUL",6.0,"B6\nSuperior",SEG["B6"],12),
("B78","LLL","B7+8 – Anteromedial Basal Segment","Orifice at 12 o'clock · Fused on left",12.0,"B7+8\nAnteromedial",SEG["B78"],12),
("B9L","LLL","B9 – Lateral Basal Segment (Left)","Orifice at 9 o'clock",9.0,"B9\nLat.Basal",SEG["B9"],12),
("B10L","LLL","B10 – Posterior Basal Segment (Left)","Orifice at 5 o'clock",5.0,"B10\nPost.Basal",SEG["B10"],12),
("CAR","carina","PROCEDURE COMPLETE","All lobes & segments surveyed · Withdrawing bronchoscope",None,None,GOLD,16),
]
total_frames = sum(s[7] for s in SCENES)
total_scenes = len(SCENES)
def draw_tree(ax, active_node, lobe, visited, progress):
ax.set_facecolor(PANEL_BG)
ax.set_xlim(0,1); ax.set_ylim(0,1); ax.axis("off")
lobe_col = LOBE_COLORS.get(lobe, WHITE)
for p,ch in EDGES:
px,py = N[p]; cx2,cy2 = N[ch]
if ch in visited and p in visited:
col,alpha,lw = lobe_col,0.85,2.2
elif p in visited:
col,alpha,lw = GOLD,0.6,1.8
else:
col,alpha,lw = "#1E2E3E",0.5,1.0
ax.plot([px,cx2],[py,cy2],color=col,lw=lw,alpha=alpha,solid_capstyle="round",zorder=2)
for nid,(nx,ny) in N.items():
if nid == active_node:
ax.add_patch(Circle((nx,ny),0.022,color=GOLD,alpha=0.18,zorder=4,transform=ax.transData))
ax.add_patch(Circle((nx,ny),0.013,color=GOLD,alpha=0.7,zorder=5,transform=ax.transData))
elif nid in visited:
ax.add_patch(Circle((nx,ny),0.007,color=lobe_col,alpha=0.8,zorder=3,transform=ax.transData))
else:
ax.add_patch(Circle((nx,ny),0.005,color="#1E2E3E",alpha=0.6,zorder=2,transform=ax.transData))
# progress bar
ax.add_patch(FancyBboxPatch((0.05,0.018),0.90,0.016,
boxstyle="round,pad=0.001",facecolor="#1A2A3A",
edgecolor="#333",linewidth=0.4,zorder=6,transform=ax.transData))
ax.add_patch(FancyBboxPatch((0.05,0.018),0.90*progress,0.016,
boxstyle="round,pad=0.001",facecolor=GOLD,alpha=0.85,
edgecolor="none",linewidth=0,zorder=7,transform=ax.transData))
ax.text(0.50,0.026,f"{int(progress*100)}%",ha="center",va="center",
fontsize=6,color=BG,fontweight="bold",zorder=8,transform=ax.transData)
ax.text(0.50,0.995,"BRONCHIAL TREE",ha="center",va="top",fontsize=8,color=GOLD,
fontweight="bold",transform=ax.transData,
path_effects=[pe.withStroke(linewidth=2,foreground=PANEL_BG)])
ax.text(N[active_node][0],N[active_node][1]-0.05,active_node,
ha="center",va="top",fontsize=6.5,color=GOLD,fontweight="bold",zorder=8,
path_effects=[pe.withStroke(linewidth=2,foreground=PANEL_BG)])
def draw_tunnel(ax, lobe, title, subtitle, clk_h, clk_lbl, seg_col, fi, nf):
ax.set_facecolor(BG)
ax.set_xlim(-1,1); ax.set_ylim(-1,1)
ax.set_aspect("equal"); ax.axis("off")
lc = LOBE_COLORS.get(lobe, WHITE)
pulse = 0.5 + 0.5*math.sin(fi/max(nf,1)*2*math.pi)
# tunnel rings
for i in range(6,0,-1):
r = 0.16*i + 0.025*pulse
alpha = max(0, 0.5-i*0.07)
cc = lc if i<=2 else "#111E2D"
ax.add_patch(Circle((0,0),r,color=cc,alpha=alpha,zorder=i))
ax.add_patch(Circle((0,0),0.16,color="#030A10",zorder=7))
# mucosal lines
for a in range(0,360,45):
rad = math.radians(a)
xs = [0.16*math.cos(rad)*t for t in np.linspace(0.3,1,4)]
ys = [0.16*math.sin(rad)*t for t in np.linspace(0.3,1,4)]
ax.plot(xs,ys,color=lc,alpha=0.10,lw=0.6,zorder=8)
# carina ridge
if lobe == "carina":
ax.plot([0,0],[-0.16,0.03],color=GOLD,lw=2.5,alpha=0.9,zorder=9)
ax.text(0.28,0.04,"RIGHT",fontsize=7,color=LOBE_COLORS["RMB"],fontweight="bold",va="center")
ax.text(-0.60,0.04,"LEFT",fontsize=7,color=LOBE_COLORS["LMB"],fontweight="bold",va="center")
# clock
if clk_h is not None:
r_clk = 0.70
ax.add_patch(Circle((0,0),r_clk,fill=False,edgecolor=GOLD,lw=0.8,alpha=0.4,zorder=9))
for h in range(12):
ang = math.radians(90-h*30)
r0 = r_clk*(0.87 if h%3==0 else 0.92)
ax.plot([r0*math.cos(ang),r_clk*math.cos(ang)],
[r0*math.sin(ang),r_clk*math.sin(ang)],
color=GOLD,alpha=0.4,lw=0.8,zorder=9)
for h in [12,3,6,9]:
ang = math.radians(90-(h%12)*30)
ax.text(r_clk*0.76*math.cos(ang),r_clk*0.76*math.sin(ang),
str(h),ha="center",va="center",fontsize=6,color=GOLD,alpha=0.6,fontweight="bold")
ang = math.radians(90-float(clk_h)*30)
dot_r = 0.042+0.015*pulse
xd = r_clk*0.92*math.cos(ang); yd = r_clk*0.92*math.sin(ang)
ax.add_patch(Circle((xd,yd),dot_r+0.02,color=seg_col,alpha=0.2,zorder=10))
ax.add_patch(Circle((xd,yd),dot_r,color=seg_col,alpha=0.9,zorder=11))
ax.annotate("",xy=(xd*0.87,yd*0.87),xytext=(0,0),
arrowprops=dict(arrowstyle="-|>",color=seg_col,lw=1.5,mutation_scale=12),zorder=10)
ax.text(r_clk*1.20*math.cos(ang),r_clk*1.20*math.sin(ang),
clk_lbl or "",ha="center",va="center",fontsize=7.5,
color=seg_col,fontweight="bold",multialignment="center",
path_effects=[pe.withStroke(linewidth=2,foreground=BG)])
# title badge
ax.add_patch(FancyBboxPatch((-0.93,0.78),1.86,0.20,
boxstyle="round,pad=0.02",facecolor=lc,alpha=0.16,
edgecolor=lc,linewidth=0.6,zorder=12))
ax.text(0,0.88,title,ha="center",va="center",fontsize=9,color=WHITE,
fontweight="bold",zorder=13,
path_effects=[pe.withStroke(linewidth=2.5,foreground=BG)])
ax.text(0,-0.87,subtitle,ha="center",va="center",fontsize=6.5,color=MGRAY,
zorder=13,wrap=True,
path_effects=[pe.withStroke(linewidth=1.5,foreground=BG)])
# ── RENDER ───────────────────────────────────────────────────────────────────
visited = set()
frame_idx = 0
frames_done = 0
for si,(node,lobe,title,subtitle,clk_h,clk_lbl,seg_col,nf) in enumerate(SCENES):
visited.add(node)
progress = (frames_done + nf/2) / total_frames
for fi in range(nf):
fig = plt.figure(figsize=(FIG_W, FIG_H), facecolor=BG)
ax_tree = fig.add_axes([0.00, 0.09, 0.37, 0.91])
ax_tunnel = fig.add_axes([0.38, 0.09, 0.61, 0.91])
ax_bar = fig.add_axes([0.00, 0.00, 1.00, 0.09])
ax_bar.set_facecolor("#050D14"); ax_bar.axis("off")
# side indicator
if node in ("NOSE","VC","TRA","CAR") or "complete" in title.lower():
stxt,scol = "CENTRAL AIRWAY",GOLD
elif "L" in node and node not in ("NOSE",):
stxt,scol = "◀ LEFT LUNG",LOBE_COLORS["LMB"]
else:
stxt,scol = "RIGHT LUNG ▶",LOBE_COLORS["RMB"]
ax_bar.text(0.01,0.60,stxt,fontsize=8,color=scol,fontweight="bold",va="center",transform=ax_bar.transAxes)
ax_bar.text(0.50,0.60,f"Scene {si+1}/{total_scenes} · {node}",
fontsize=7,color=MGRAY,ha="center",va="center",transform=ax_bar.transAxes)
ax_bar.text(0.99,0.60,"Orris Medical · Bronchoscopy Reference",
fontsize=6,color="#333",ha="right",va="center",transform=ax_bar.transAxes)
ax_bar.text(0.50,0.18,subtitle,fontsize=6.5,color=WHITE,alpha=0.75,
ha="center",va="center",transform=ax_bar.transAxes)
draw_tree(ax_tree, node, lobe, visited, progress)
draw_tunnel(ax_tunnel, lobe, title, subtitle, clk_h, clk_lbl, seg_col, fi, nf)
fig.add_artist(plt.Line2D([0.375,0.375],[0.0,1.0],
transform=fig.transFigure,
color=GOLD,linewidth=0.6,alpha=0.4))
fname = os.path.join(OUT_DIR, f"frame_{frame_idx:05d}.png")
fig.savefig(fname, dpi=DPI, facecolor=BG, bbox_inches="tight", pad_inches=0)
plt.close(fig)
frame_idx += 1
frames_done += nf
print(f" Scene {si+1:2d}/{total_scenes} [{node:6s}] done ({frames_done} total)")
print(f"\n{frame_idx} frames rendered. Encoding...")
cmd = [
"ffmpeg","-y",
"-framerate", str(FPS),
"-i", os.path.join(OUT_DIR, "frame_%05d.png"),
"-vf", f"scale={W_PX}:{H_PX}:force_original_aspect_ratio=decrease,pad={W_PX}:{H_PX}:(ow-iw)/2:(oh-ih)/2",
"-c:v","libx264","-preset","fast","-crf","20",
"-pix_fmt","yuv420p","-movflags","+faststart",
OUT_VID,
]
r = subprocess.run(cmd, capture_output=True, text=True)
if r.returncode != 0:
print("ffmpeg error:", r.stderr[-1500:])
else:
mb = os.path.getsize(OUT_VID)/1e6
print(f"Saved: {OUT_VID} ({mb:.1f} MB)")
shutil.rmtree(OUT_DIR)
print("Done.")
# Clean up old frames first, then run rm -rf /home/daytona/workspace/bronchoscop…
Have you done
ls -lh /home/daytona/workspace/bronchoscopy-card/ 2>/dev/null && echo "---" && l…
python3 /home/daytona/workspace/bronchoscopy-card/make_video_fast.py 2>&1
How long it takes
python3 -c " import time, math, numpy as np import matplotlib matplotlib.use('Ag…
python3 -c " import time, math, numpy as np import matplotlib matplotlib.use('Ag…
python3 /home/daytona/workspace/bronchoscopy-card/make_video_fast.py 2>&1
ls /home/daytona/workspace/bronchoscopy-card/frames2/ 2>/dev/null | wc -l && pyt…
# Remove partial frames and run fresh with timeout=0 (no limit) rm -rf /home/da…