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

Reading File
Finding Sources
Finding Sources
Finding Sources
Reading File
Finding Sources
Reading File
Finding Sources
Finding Sources
Reading File
Reading File
Reading File
Reading File
Reading File
Loading Image
Loading Image
Loading Image
I now have all the anatomy, bronchoscopic views, and segment-by-segment detail needed for a thorough guided tour. Let me compose the full response.

Bronchoscopy - A Camera's-Eye Tour Through the Lungs

Sources: Fischer's Mastery of Surgery 8e, Gray's Anatomy for Students, Murray & Nadel's Respiratory Medicine

Step 1 - Entry and the Trachea

The bronchoscope enters through the nose (preferred - via the inferior nasal meatus, the widest passage) or orally through the mouth. In intubated ICU patients it passes through the endotracheal tube. The tip is angled 60-90 degrees caudally as it transitions from the nasal cavity into the nasopharynx.
As you glide through the larynx and past the vocal cords, you enter the trachea - about 11 cm long, 2-2.5 cm wide. Key landmarks to orient yourself:
  • Anterior: C-shaped cartilaginous rings (hard, prominent)
  • Posterior: Smooth membranous wall with longitudinal corrugations that become more prominent approaching the carina
  • The trachea is narrowest at its very start (cricoid cartilage - the only complete ring)
Orientation tip: Always identify the tracheal rings first - they confirm you are anterior, which lets you distinguish left from right.
Bronchoscopy overview showing trachea, larynx, carina, and bronchoscope entry; with bronchoscopic views of the trachea and carina

Step 2 - The Carina

Advancing distally, you reach the carina at the level of T4/5 (sternal angle). This is the single most important bronchoscopic landmark.
  • It appears as a sharp, narrow, vertical ridge - like an upside-down keel of a boat
  • It is widest anteriorly and narrows centrally
  • The right main bronchus opens to your right; the left opens to your left
  • A normal carina is sharp and pale; widening or blunting suggests subcarinal lymphadenopathy or malignancy
Real bronchoscopic view of the carina showing the right and left main bronchi and their relationship to anterior and posterior

THE RIGHT LUNG - 3 Lobes, 10 Segments

Step 3 - Right Main Bronchus (RMB)

The right main bronchus is shorter and more vertical than the left - this is why misplaced endotracheal tubes preferentially enter here. As you enter, the mucosa looks similar to the trachea but the lumen narrows.

Step 4 - Right Upper Lobe (RUL) - 3 Segments

The RUL orifice opens laterally, typically 2-3 cm from the carina (occasionally directly opposite it). You angle your tip laterally to look inside. There you'll find three segmental orifices on a clock face:
SegmentBronchopulmonaryClock Position
ApicalB14 o'clock
AnteriorB212 o'clock
PosteriorB38 o'clock
The apical segment goes toward the apex (top) of the lung; anterior goes forward toward the chest wall; posterior goes backward.

Step 5 - Intermediate Bronchus

After the RUL takeoff, you re-enter the bronchus intermedius - a short segment (~2 cm) unique to the right side. It carries you toward the middle and lower lobes.

Step 6 - Right Middle Lobe (RML) - 2 Segments

Looking anteriorly in the intermediate bronchus, you'll see the RML orifice. Advance in and find two segments:
SegmentBronchopulmonaryClock Position
LateralB43-6 o'clock
MedialB59-12 o'clock
The RML bronchus quickly bifurcates; these two orifices are relatively close together and can be subtle.

Step 7 - Right Lower Lobe (RLL) - 5 Segments

Returning from the RML and looking directly posterior (opposite the RML orifice) is the first RLL segment:
SegmentBronchopulmonaryClock PositionNotes
Superior (apical)B65 o'clockJust past the middle lobe bronchus; directly POSTERIOR - opposite RML
Medial basalB79 o'clockUsually more proximal than other basal segments
Anterior basalB81 o'clock
Lateral basalB93 o'clock
Posterior basalB106 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.

THE LEFT LUNG - 2 Lobes, 8 Segments (American nomenclature)

Step 8 - Left Main Bronchus (LMB)

Return to the carina and now advance into the left main bronchus. It is:
  • Longer than the right (~5 cm)
  • Takes a more acute, horizontal angle (about 45-55 degrees from the trachea vs 20-30 on the right)
  • Passes under the aortic arch - you may see pulsations transmitted through the wall

Step 9 - Left Upper Lobe (LUL) - 4 Segments (2 divisions)

The LUL orifice lies superior and slightly lateral to the LLL orifice. The LUL bronchus quickly bifurcates into two distinct divisions:
Upper Division (orifice at 8 o'clock):
SegmentBronchopulmonaryNotes
ApicoposteriorB1+3Fused on the left - no separate apical/posterior segments
AnteriorB2
Lingular Division (orifice at 2 o'clock - the "tongue"):
SegmentBronchopulmonaryClock Position
Superior lingularB410 o'clock
Inferior lingularB52 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.

Step 10 - Left Lower Lobe (LLL) - 4 Segments

The LLL orifice is inferior and slightly medial to the LUL. Advancing in:
SegmentBronchopulmonaryClock PositionNotes
Superior (apical)B66 o'clockJust past origin of lower lobe; posterior
Anteromedial basalB8+712 o'clockFused on left (no separate medial/anterior basal)
Lateral basalB99 o'clock
Posterior basalB105 o'clock

The Full Bronchopulmonary Map

Tracheobronchial tree showing main, lobar, and segmental bronchi with bronchoscopic views at each major division; and color-coded lobes/segments of both lungs

Quick Reference: The Complete Route

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)

Key Clinical Pearls from the Camera's Perspective

FindingSignificance
Widened, blunted carinaSubcarinal lymph node enlargement (malignancy, sarcoidosis, TB)
RUL orifice directly at carinaNormal variant - don't mistake for pathology
Pulsating LMB wallNormal - transmitted aortic pulsation
RMB shorter, more verticalETT misplacement goes here preferentially
B6 directly opposite RMLClassic "posterior sentinel" - never miss it
Peripheral lesions beyond segmentalsNot visible by camera - need radial EBUS, ENB, or robotic bronchoscopy
Mucosal irregularity/bleedingEndobronchial malignancy - biopsy here
The right lung has 10 segments; the left has 8 in American nomenclature (B1+B3 fused into B1+3, and B7+B8 fused into anteromedial basal) - though British literature recognizes 10 on the left as well.
  • Fischer's Mastery of Surgery 8e, pp. 2055-2059
  • Gray's Anatomy for Students, p. 218
  • Murray & Nadel's Textbook of Respiratory Medicine, p. 558

Create a printable quick-reference pocket card of segmental orifices (clock positions)

Reading File
Running Command

ls /home/daytona/workspace/ 2>/dev/null && echo "---" && python3 -c "import repo…

Running Command

mkdir -p /home/daytona/workspace/bronchoscopy-card

Writing File

~/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}")
Running Command

python3 /home/daytona/workspace/bronchoscopy-card/make_card.py 2>&1

Running Command

ls -lh /home/daytona/workspace/bronchoscopy-card/bronchoscopy_pocket_card.pdf

Your printable pocket card is ready:

bronchoscopy pocket card

PDF Document · PDF


Here's what's on the card:
Format: Landscape A5 (210 × 148 mm) - standard white-coat pocket size. Print on cardstock and laminate for durability.
Layout:
Left Panel - RIGHT LUNGRight Panel - LEFT LUNG
3 annotated clock dials (RUL, RML, RLL)2 annotated clock dials (LUL, LLL)
Colour-coded legend table per lobeColour-coded legend table per lobe
Each clock dial shows:
  • Hour tick marks with 12/3/6/9 labels
  • A colour-coded spoke + dot for every segment at its bronchoscopic clock position
  • Segment label (e.g. B6 Superior) outside the rim
Colour coding by lobe:
  • RUL - Deep blue | RML - Rose | RLL - Forest green
  • LUL - Purple (with lingular orifices shown) | LLL - Teal
Clinical pearls box at the bottom-right reminds you of the most important orientation tips (ETT misplacement, carina widening, B6 as the posterior sentinel, left lung aortic pulsation, and the subapical RLL segment variant).

Create a laminated, waterproof version for the OR/procedure room

Writing File

~/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}")
Running Command

python3 /home/daytona/workspace/bronchoscopy-card/make_or_card.py 2>&1

Running Command

ls -lh /home/daytona/workspace/bronchoscopy-card/

Your OR/procedure-room laminated card is ready:

bronchoscopy OR laminated

PDF Document · PDF


What's in the file

2 pages on A4 - print both, cut on crop marks, laminate back-to-back in a standard card pouch.

Page 1 - FRONT - Right Lung (deep navy background)

  • 3 annotated clock dials: RUL · RML · RLL
  • Each dial has colour-coded spokes + labelled rim dots for every segment orifice
  • Matching legend table (code · full name · clock position · note) beside each dial
  • Clinical pearls column on the right edge

Page 2 - BACK - Left Lung (deep teal background)

  • 2 large annotated clock dials: LUL+Lingula · LLL
  • Larger clocks (more room) since only 2 lobes
  • Full legend tables with division notes (Upper/Lingular; fused segments flagged)
  • Clinical pearls column on the right
  • Side-by-side comparison table at the bottom (RMB vs LMB, lobe count, segment count, unique structures)

OR-specific design features

FeatureDetail
BackgroundDark navy/teal - high contrast under OR lights, minimal glare
TextWhite/gold on dark - readable through sterile gloves
FontsBold, oversized segment codes on clock dots
Crop marks3 mm bleed, corner marks for clean cutting
Lamination guideCyan dashed border 5 mm inside the card edge - align your lamination pouch to this line
Rounded corners4 mm radius on card body - standard lamination pouch safe zone
Colour codingVivid, saturated colours (not pastels) - distinguishable under yellow OR light
Print instructionsLabelled at bottom of each page so no confusion front/back
Recommended production: Print on 200-250 gsm card stock at 100% scale → cut on crop marks → laminate with a 75-80 micron gloss pouch. The result is a rigid, wipe-clean card that holds up to gloved handling and alcohol swab cleaning.

Diagram presentation

Video

Asking for Preferences

Video of entire procedure including representation of lobes and divisions

Running Command

python3 -c "import matplotlib; import numpy; print('matplotlib', matplotlib.__ve…

Writing File

~/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.")
Running Command

cd /home/daytona/workspace/bronchoscopy-card && python3 make_bronchoscopy_video.…

Running Command

ls /home/daytona/workspace/bronchoscopy-card/frames/ 2>/dev/null | wc -l

Writing File

~/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.")
Running Command

# Clean up old frames first, then run rm -rf /home/daytona/workspace/bronchoscop…

Have you done

Running Command

ls -lh /home/daytona/workspace/bronchoscopy-card/ 2>/dev/null && echo "---" && l…

Running Command

python3 /home/daytona/workspace/bronchoscopy-card/make_video_fast.py 2>&1

How long it takes

Running Command

python3 -c " import time, math, numpy as np import matplotlib matplotlib.use('Ag…

Running Command

python3 -c " import time, math, numpy as np import matplotlib matplotlib.use('Ag…

Running Command

python3 /home/daytona/workspace/bronchoscopy-card/make_video_fast.py 2>&1

Running Command

ls /home/daytona/workspace/bronchoscopy-card/frames2/ 2>/dev/null | wc -l && pyt…

Running Command

# Remove partial frames and run fresh with timeout=0 (no limit) rm -rf /home/da…

This is a shared conversation. Sign in to Orris to start your own chat.