Ecoer Logo
scipio

@scipio

25

Does it matter who's right, or who's left?

hive.blog/@scipio
VOTING POWER78.88%
DOWNVOTE POWER100.00%
RESOURCE CREDITS100.00%
REPUTATION PROGRESS0.00%
Net Worth
1,689.755USD
HIVE
214.681HIVE
HBD
0.000HBD
Effective Power
6,196.024HP
├── Own HP
4,009.707HP
├── Incoming Deleg
+2,529.778HP
└── Outgoing Deleg
-343.461HP

Detailed Balance

HIVE
balance
214.681HIVE
market_balance
0.000HIVE
savings_balance
0.000HIVE
reward_hive_balance
0.000HIVE
HIVE POWER
Own HP
4,009.707HP
Delegated Out
343.461HP
Delegation In
2,529.778HP
Effective Power
6,196.024HP
Reward HP (pending)
0.080HP
HBD
hbd_balance
0.000HBD
hbd_conversions
0.000HBD
hbd_market_balance
0.000HBD
savings_hbd_balance
0.000HBD
reward_hbd_balance
0.000HBD
{
  "balance": "214.681 HIVE",
  "savings_balance": "0.000 HIVE",
  "reward_hive_balance": "0.000 HIVE",
  "vesting_shares": "6509603.023860 VESTS",
  "delegated_vesting_shares": "557595.784818 VESTS",
  "received_vesting_shares": "4106995.174504 VESTS",
  "hbd_balance": "0.000 HBD",
  "savings_hbd_balance": "0.000 HBD",
  "reward_hbd_balance": "0.000 HBD"
}

Account Info

namescipio
id422033
rank0
reputation0
created2017-10-24T09:41:36
recovery_accountblocktrades
proxyNone
invited_bynull
post_count2,611
comment_count0
lifetime_vote_count0
witnesses_voted_for21
last_post2026-06-06T07:05:06
last_root_post2026-06-06T07:05:06
last_vote_time2026-06-06T08:21:24
proxied_vsf_votes0, 0, 0, 0
can_vote1
voting_power7,584
delayed_votesNone
governance_vote_expiration_ts2027-04-22T00:58:12
balance214.681 HIVE
savings_balance0.000 HIVE
hbd_balance0.000 HBD
savings_hbd_balance0.000 HBD
vesting_shares6509603.023860 VESTS
delegated_vesting_shares557595.784818 VESTS
received_vesting_shares4106995.174504 VESTS
reward_vesting_balance129.922049 VESTS
vesting_balance0.000 HIVE
vesting_withdraw_rate0.000000 VESTS
next_vesting_withdrawal1969-12-31T23:59:59
withdrawn0
to_withdraw0
withdraw_routes0
savings_withdraw_requests0
last_account_recovery1970-01-01T00:00:00
reset_accountnull
last_owner_update1970-01-01T00:00:00
last_account_update2019-06-15T07:21:54
minedNo
hbd_seconds0
hbd_last_interest_payment2020-05-12T20:42:45
savings_hbd_last_interest_payment1970-01-01T00:00:00
{
  "active": {
    "account_auths": [],
    "key_auths": [
      [
        "STM8RLCJUVQyKPiGzkZQpXLmoy2w5bMTv8shWEAfBPPaP4FpUX7To",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "balance": "214.681 HIVE",
  "can_vote": true,
  "comment_count": 0,
  "created": "2017-10-24T09:41:36",
  "curation_rewards": 623302,
  "delayed_votes": [],
  "delegated_vesting_shares": "557595.784818 VESTS",
  "downvote_manabar": {
    "current_mana": 2514750603386,
    "last_update_time": 1780734084
  },
  "governance_vote_expiration_ts": "2027-04-22T00:58:12",
  "guest_bloggers": [],
  "hbd_balance": "0.000 HBD",
  "hbd_last_interest_payment": "2020-05-12T20:42:45",
  "hbd_seconds": "0",
  "hbd_seconds_last_update": "2020-05-12T20:42:45",
  "id": 422033,
  "json_metadata": "{\"profile\":{\"name\":\"scipio\",\"about\":\"Does it matter who's right, or who's left?\",\"cover_image\":\"https://static.pexels.com/photos/290386/pexels-photo-290386.jpeg\",\"profile_image\":\"https://cdn.steemitimages.com/DQmSHCeBbjgGEsnigAh9qJTdcFuKvm8vaxRYACdkvPi8WBg/scipio.jpg\"}}",
  "last_account_recovery": "1970-01-01T00:00:00",
  "last_account_update": "2019-06-15T07:21:54",
  "last_owner_update": "1970-01-01T00:00:00",
  "last_post": "2026-06-06T07:05:06",
  "last_root_post": "2026-06-06T07:05:06",
  "last_vote_time": "2026-06-06T08:21:24",
  "lifetime_vote_count": 0,
  "market_history": [],
  "memo_key": "STM8ZMNP7R48LekiaQu9Kz4UNVYcnVwFsY1fRhgY7MGDx2hxxukvS",
  "mined": false,
  "name": "scipio",
  "next_vesting_withdrawal": "1969-12-31T23:59:59",
  "open_recurrent_transfers": 0,
  "other_history": [],
  "owner": {
    "account_auths": [],
    "key_auths": [
      [
        "STM54sX7GE6LRFVn9b3rRyZkkgrAJeXAHsaupuxTCfrjqAuHgf6Ej",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "pending_claimed_accounts": 0,
  "pending_transfers": 0,
  "post_bandwidth": 0,
  "post_count": 2611,
  "post_history": [],
  "post_voting_power": "10059002.413546 VESTS",
  "posting": {
    "account_auths": [
      [
        "utopian.app",
        1
      ],
      [
        "utopianpay",
        1
      ]
    ],
    "key_auths": [
      [
        "STM8eHBTT5K4piEfvYAk99g3CxxuAGFQdNqA98BPtRp1SucFxgBqD",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "posting_json_metadata": "{\"profile\":{\"name\":\"scipio\",\"about\":\"Does it matter who's right, or who's left?\",\"cover_image\":\"https://static.pexels.com/photos/290386/pexels-photo-290386.jpeg\",\"profile_image\":\"https://cdn.steemitimages.com/DQmSHCeBbjgGEsnigAh9qJTdcFuKvm8vaxRYACdkvPi8WBg/scipio.jpg\"}}",
  "posting_rewards": 4889451,
  "previous_owner_update": "1970-01-01T00:00:00",
  "proxied_vsf_votes": [
    0,
    0,
    0,
    0
  ],
  "proxy": "",
  "received_vesting_shares": "4106995.174504 VESTS",
  "recovery_account": "blocktrades",
  "reputation": 0,
  "reset_account": "null",
  "reward_hbd_balance": "0.000 HBD",
  "reward_hive_balance": "0.000 HIVE",
  "reward_vesting_balance": "129.922049 VESTS",
  "reward_vesting_hive": "0.080 HIVE",
  "savings_balance": "0.000 HIVE",
  "savings_hbd_balance": "0.000 HBD",
  "savings_hbd_last_interest_payment": "1970-01-01T00:00:00",
  "savings_hbd_seconds": "0",
  "savings_hbd_seconds_last_update": "1970-01-01T00:00:00",
  "savings_withdraw_requests": 0,
  "tags_usage": [],
  "to_withdraw": 0,
  "transfer_history": [],
  "vesting_balance": "0.000 HIVE",
  "vesting_shares": "6509603.023860 VESTS",
  "vesting_withdraw_rate": "0.000000 VESTS",
  "vote_history": [],
  "voting_manabar": {
    "current_mana": 7629133205792,
    "last_update_time": 1780734084
  },
  "voting_power": 7584,
  "withdraw_routes": 0,
  "withdrawn": 0,
  "witness_votes": [
    "arcange",
    "ausbitbank",
    "blocktrades",
    "blue-witness",
    "dalz",
    "deathwing",
    "drakos",
    "good-karma",
    "gtg",
    "guiltyparties",
    "mahdiyari",
    "neoxian",
    "nuthman",
    "pharesim",
    "rishi556",
    "roelandp",
    "someguy123",
    "steempeak",
    "stoodkev",
    "threespeak",
    "ura-soul"
  ],
  "witnesses_voted_for": 21,
  "rank": 0
}

Withdraw Routes

IncomingOutgoing
Empty
Empty
{
  "incoming": [],
  "outgoing": []
}
From Date
To Date
2026/06/06 11:43:15
authorscipio
pending payout1.867 HBD
permlinklearn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house
rshares4562672246
total vote weight24559119576897
votergamersclassified
weight4562672246
Transaction InfoBlock #107034274/Trx 362be342f3073c4ccd1993cb121bacba05cf3ef5
View Raw JSON Data
{
  "block": 107034274,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "1.867 HBD",
      "permlink": "learn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house",
      "rshares": 4562672246,
      "total_vote_weight": 24559119576897,
      "voter": "gamersclassified",
      "weight": 4562672246
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T11:43:15",
  "trx_id": "362be342f3073c4ccd1993cb121bacba05cf3ef5",
  "trx_in_block": 12,
  "virtual_op": true
}
2026/06/06 11:43:15
authorscipio
permlinklearn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house
votergamersclassified
weight210 (2.10%)
Transaction InfoBlock #107034274/Trx 362be342f3073c4ccd1993cb121bacba05cf3ef5
View Raw JSON Data
{
  "block": 107034274,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house",
      "voter": "gamersclassified",
      "weight": 210
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T11:43:15",
  "trx_id": "362be342f3073c4ccd1993cb121bacba05cf3ef5",
  "trx_in_block": 12,
  "virtual_op": false
}
scipioreceived 0.006 HP curation reward for @steevc / 18712824092-3118819131
2026/06/06 11:35:57
authorsteevc
curatorscipio
payout must be claimedtrue
permlink18712824092-3118819131
reward9.744111 VESTS
Transaction InfoBlock #107034129/Virtual Operation 4294967295:115
View Raw JSON Data
{
  "block": 107034129,
  "op": [
    "curation_reward",
    {
      "author": "steevc",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "18712824092-3118819131",
      "reward": "9.744111 VESTS"
    }
  ],
  "op_in_trx": 115,
  "timestamp": "2026-06-06T11:35:57",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
scipioreceived 0.013 HP curation reward for @papilloncharity / crow-guards-on-duty
2026/06/06 11:16:48
authorpapilloncharity
curatorscipio
payout must be claimedtrue
permlinkcrow-guards-on-duty
reward21.112264 VESTS
Transaction InfoBlock #107033747/Virtual Operation 4294967295:115
View Raw JSON Data
{
  "block": 107033747,
  "op": [
    "curation_reward",
    {
      "author": "papilloncharity",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "crow-guards-on-duty",
      "reward": "21.112264 VESTS"
    }
  ],
  "op_in_trx": 115,
  "timestamp": "2026-06-06T11:16:48",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
2026/06/06 11:07:03
authorscipio
pending payout1.386 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares457662559098
total vote weight18256169497750
votermaarnio
weight457662559098
Transaction InfoBlock #107033552/Trx 02d2a1799687dfa91679e6bf38f9bbca72eaca32
View Raw JSON Data
{
  "block": 107033552,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "1.386 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 457662559098,
      "total_vote_weight": 18256169497750,
      "voter": "maarnio",
      "weight": 457662559098
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T11:07:03",
  "trx_id": "02d2a1799687dfa91679e6bf38f9bbca72eaca32",
  "trx_in_block": 1,
  "virtual_op": true
}
2026/06/06 11:07:03
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
votermaarnio
weight10000 (100.00%)
Transaction InfoBlock #107033552/Trx 02d2a1799687dfa91679e6bf38f9bbca72eaca32
View Raw JSON Data
{
  "block": 107033552,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "maarnio",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T11:07:03",
  "trx_id": "02d2a1799687dfa91679e6bf38f9bbca72eaca32",
  "trx_in_block": 1,
  "virtual_op": false
}
scipioreceived 0.008 HP curation reward for @maxwellmarcusart / drawing-a-portrait-2109
2026/06/06 10:47:18
authormaxwellmarcusart
curatorscipio
payout must be claimedtrue
permlinkdrawing-a-portrait-2109
reward12.992183 VESTS
Transaction InfoBlock #107033158/Virtual Operation 4294967295:71
View Raw JSON Data
{
  "block": 107033158,
  "op": [
    "curation_reward",
    {
      "author": "maxwellmarcusart",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "drawing-a-portrait-2109",
      "reward": "12.992183 VESTS"
    }
  ],
  "op_in_trx": 71,
  "timestamp": "2026-06-06T10:47:18",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
2026/06/06 10:47:09
authorindiaunited
curatorscipio
payout must be claimedtrue
permlinkindiaunited-new-contest-and-last-ae0ff15b5f76c
reward19.488274 VESTS
Transaction InfoBlock #107033155/Virtual Operation 4294967295:49
View Raw JSON Data
{
  "block": 107033155,
  "op": [
    "curation_reward",
    {
      "author": "indiaunited",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "indiaunited-new-contest-and-last-ae0ff15b5f76c",
      "reward": "19.488274 VESTS"
    }
  ],
  "op_in_trx": 49,
  "timestamp": "2026-06-06T10:47:09",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
scipioreceived 0.005 HP curation reward for @michupa / earthquake-fly-away
2026/06/06 10:46:24
authormichupa
curatorscipio
payout must be claimedtrue
permlinkearthquake-fly-away
reward8.120114 VESTS
Transaction InfoBlock #107033140/Virtual Operation 4294967295:198
View Raw JSON Data
{
  "block": 107033140,
  "op": [
    "curation_reward",
    {
      "author": "michupa",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "earthquake-fly-away",
      "reward": "8.120114 VESTS"
    }
  ],
  "op_in_trx": 198,
  "timestamp": "2026-06-06T10:46:24",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
scipioreceived 0.007 HP curation reward for @forykw / powerup-june-2026-9-years-on-hive
2026/06/06 10:28:42
authorforykw
curatorscipio
payout must be claimedtrue
permlinkpowerup-june-2026-9-years-on-hive
reward11.368171 VESTS
Transaction InfoBlock #107032787/Virtual Operation 4294967295:456
View Raw JSON Data
{
  "block": 107032787,
  "op": [
    "curation_reward",
    {
      "author": "forykw",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "powerup-june-2026-9-years-on-hive",
      "reward": "11.368171 VESTS"
    }
  ],
  "op_in_trx": 456,
  "timestamp": "2026-06-06T10:28:42",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
scipioreceived 0.004 HP curation reward for @riverflows / winter-walk-urqhuarts-to-sunnymead
2026/06/06 10:16:36
authorriverflows
curatorscipio
payout must be claimedtrue
permlinkwinter-walk-urqhuarts-to-sunnymead
reward6.496102 VESTS
Transaction InfoBlock #107032546/Virtual Operation 4294967295:204
View Raw JSON Data
{
  "block": 107032546,
  "op": [
    "curation_reward",
    {
      "author": "riverflows",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "winter-walk-urqhuarts-to-sunnymead",
      "reward": "6.496102 VESTS"
    }
  ],
  "op_in_trx": 204,
  "timestamp": "2026-06-06T10:16:36",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
2026/06/06 09:25:33
authorgrindle
curatorscipio
payout must be claimedtrue
permlinkart-for-the-gamer-gameplay-playing-for-impact-moma-vilnius
reward9.744179 VESTS
Transaction InfoBlock #107031526/Virtual Operation 4294967295:229
View Raw JSON Data
{
  "block": 107031526,
  "op": [
    "curation_reward",
    {
      "author": "grindle",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "art-for-the-gamer-gameplay-playing-for-impact-moma-vilnius",
      "reward": "9.744179 VESTS"
    }
  ],
  "op_in_trx": 229,
  "timestamp": "2026-06-06T09:25:33",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
2026/06/06 09:15:27
authorscipio
pending payout1.345 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares57752650434
total vote weight17798506938652
votervegoutt-travel
weight57752650434
Transaction InfoBlock #107031324/Trx 239443a0b1ba2e3b01c5baf3a5c434315920a343
View Raw JSON Data
{
  "block": 107031324,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "1.345 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 57752650434,
      "total_vote_weight": 17798506938652,
      "voter": "vegoutt-travel",
      "weight": 57752650434
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T09:15:27",
  "trx_id": "239443a0b1ba2e3b01c5baf3a5c434315920a343",
  "trx_in_block": 3,
  "virtual_op": true
}
2026/06/06 09:15:27
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
votervegoutt-travel
weight4000 (40.00%)
Transaction InfoBlock #107031324/Trx 239443a0b1ba2e3b01c5baf3a5c434315920a343
View Raw JSON Data
{
  "block": 107031324,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "vegoutt-travel",
      "weight": 4000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T09:15:27",
  "trx_id": "239443a0b1ba2e3b01c5baf3a5c434315920a343",
  "trx_in_block": 3,
  "virtual_op": false
}
2026/06/06 09:15:27
authorscipio
pending payout1.340 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares1283080327354
total vote weight17740754288218
votercaptainhive
weight1283080327354
Transaction InfoBlock #107031324/Trx 8f80a2d4ff5d768fa6fd635fc905b6b84ff1ec04
View Raw JSON Data
{
  "block": 107031324,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "1.340 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 1283080327354,
      "total_vote_weight": 17740754288218,
      "voter": "captainhive",
      "weight": 1283080327354
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T09:15:27",
  "trx_id": "8f80a2d4ff5d768fa6fd635fc905b6b84ff1ec04",
  "trx_in_block": 2,
  "virtual_op": true
}
2026/06/06 09:15:27
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
votercaptainhive
weight4000 (40.00%)
Transaction InfoBlock #107031324/Trx 8f80a2d4ff5d768fa6fd635fc905b6b84ff1ec04
View Raw JSON Data
{
  "block": 107031324,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "captainhive",
      "weight": 4000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T09:15:27",
  "trx_id": "8f80a2d4ff5d768fa6fd635fc905b6b84ff1ec04",
  "trx_in_block": 2,
  "virtual_op": false
}
2026/06/06 09:14:54
authorscipio
pending payout1.243 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares3499611450480
total vote weight16457673960864
voterspectrumecons
weight3499611450480
Transaction InfoBlock #107031313/Trx 1b52815f4e61a23be05f3a1c2e075f8b98b98a34
View Raw JSON Data
{
  "block": 107031313,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "1.243 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 3499611450480,
      "total_vote_weight": 16457673960864,
      "voter": "spectrumecons",
      "weight": 3499611450480
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T09:14:54",
  "trx_id": "1b52815f4e61a23be05f3a1c2e075f8b98b98a34",
  "trx_in_block": 5,
  "virtual_op": true
}
2026/06/06 09:14:54
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterspectrumecons
weight4000 (40.00%)
Transaction InfoBlock #107031313/Trx 1b52815f4e61a23be05f3a1c2e075f8b98b98a34
View Raw JSON Data
{
  "block": 107031313,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "spectrumecons",
      "weight": 4000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T09:14:54",
  "trx_id": "1b52815f4e61a23be05f3a1c2e075f8b98b98a34",
  "trx_in_block": 5,
  "virtual_op": false
}
scipioreceived 0.007 HP curation reward for @oflyhigh / 4x6d6a-o
2026/06/06 09:05:06
authoroflyhigh
curatorscipio
payout must be claimedtrue
permlink4x6d6a-o
reward11.368222 VESTS
Transaction InfoBlock #107031117/Virtual Operation 4294967295:143
View Raw JSON Data
{
  "block": 107031117,
  "op": [
    "curation_reward",
    {
      "author": "oflyhigh",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "4x6d6a-o",
      "reward": "11.368222 VESTS"
    }
  ],
  "op_in_trx": 143,
  "timestamp": "2026-06-06T09:05:06",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
2026/06/06 08:23:21
authorscipio
pending payout1.850 HBD
permlinklearn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house
rshares14708332734
total vote weight24554556904651
voteropticus
weight14708332734
Transaction InfoBlock #107030285/Trx 0d179b7f4072058695285263e278affedce456e7
View Raw JSON Data
{
  "block": 107030285,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "1.850 HBD",
      "permlink": "learn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house",
      "rshares": 14708332734,
      "total_vote_weight": 24554556904651,
      "voter": "opticus",
      "weight": 14708332734
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T08:23:21",
  "trx_id": "0d179b7f4072058695285263e278affedce456e7",
  "trx_in_block": 7,
  "virtual_op": true
}
2026/06/06 08:23:21
authorscipio
permlinklearn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house
voteropticus
weight420 (4.20%)
Transaction InfoBlock #107030285/Trx 0d179b7f4072058695285263e278affedce456e7
View Raw JSON Data
{
  "block": 107030285,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house",
      "voter": "opticus",
      "weight": 420
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T08:23:21",
  "trx_id": "0d179b7f4072058695285263e278affedce456e7",
  "trx_in_block": 7,
  "virtual_op": false
}
scipioeffective vote applied for @tarazkp / barriers-against-puusti
2026/06/06 08:21:27
authortarazkp
pending payout1.051 HBD
permlinkbarriers-against-puusti
rshares201130048271
total vote weight13955287494153
voterscipio
weight201130048271
Transaction InfoBlock #107030248/Trx 78c9700491394b05089d9065eff2d498d064ef3f
View Raw JSON Data
{
  "block": 107030248,
  "op": [
    "effective_comment_vote",
    {
      "author": "tarazkp",
      "pending_payout": "1.051 HBD",
      "permlink": "barriers-against-puusti",
      "rshares": 201130048271,
      "total_vote_weight": 13955287494153,
      "voter": "scipio",
      "weight": 201130048271
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T08:21:27",
  "trx_id": "78c9700491394b05089d9065eff2d498d064ef3f",
  "trx_in_block": 0,
  "virtual_op": true
}
2026/06/06 08:21:27
authortarazkp
permlinkbarriers-against-puusti
voterscipio
weight10000 (100.00%)
Transaction InfoBlock #107030248/Trx 78c9700491394b05089d9065eff2d498d064ef3f
View Raw JSON Data
{
  "block": 107030248,
  "op": [
    "vote",
    {
      "author": "tarazkp",
      "permlink": "barriers-against-puusti",
      "voter": "scipio",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T08:21:27",
  "trx_id": "78c9700491394b05089d9065eff2d498d064ef3f",
  "trx_in_block": 0,
  "virtual_op": false
}
scipioreceived 0.012 HP curation reward for @stresskiller / how-i-spended-my-tax
2026/06/06 08:18:12
authorstresskiller
curatorscipio
payout must be claimedtrue
permlinkhow-i-spended-my-tax
reward19.488429 VESTS
Transaction InfoBlock #107030183/Virtual Operation 4294967295:103
View Raw JSON Data
{
  "block": 107030183,
  "op": [
    "curation_reward",
    {
      "author": "stresskiller",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "how-i-spended-my-tax",
      "reward": "19.488429 VESTS"
    }
  ],
  "op_in_trx": 103,
  "timestamp": "2026-06-06T08:18:12",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
2026/06/06 08:03:21
authorscipio
pending payout1.846 HBD
permlinklearn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house
rshares477600532
total vote weight24539848571917
votervalerianis
weight477600532
Transaction InfoBlock #107029887/Trx c5ddbec6257fb3caae3c7f4fa30c9a8efb2cd18c
View Raw JSON Data
{
  "block": 107029887,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "1.846 HBD",
      "permlink": "learn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house",
      "rshares": 477600532,
      "total_vote_weight": 24539848571917,
      "voter": "valerianis",
      "weight": 477600532
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T08:03:21",
  "trx_id": "c5ddbec6257fb3caae3c7f4fa30c9a8efb2cd18c",
  "trx_in_block": 3,
  "virtual_op": true
}
2026/06/06 08:03:21
authorscipio
permlinklearn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house
votervalerianis
weight126 (1.26%)
Transaction InfoBlock #107029887/Trx c5ddbec6257fb3caae3c7f4fa30c9a8efb2cd18c
View Raw JSON Data
{
  "block": 107029887,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ethical-hacking-48-insider-threats-when-the-call-is-coming-from-inside-the-house",
      "voter": "valerianis",
      "weight": 126
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T08:03:21",
  "trx_id": "c5ddbec6257fb3caae3c7f4fa30c9a8efb2cd18c",
  "trx_in_block": 3,
  "virtual_op": false
}
2026/06/06 07:37:33
authorscipio
pending payout0.973 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares7562166456
total vote weight12958062510384
voterretrodroid
weight7562166456
Transaction InfoBlock #107029373/Trx a2fc1aa9c9ab216a6da10e407cb4b6da3ecedf74
View Raw JSON Data
{
  "block": 107029373,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.973 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 7562166456,
      "total_vote_weight": 12958062510384,
      "voter": "retrodroid",
      "weight": 7562166456
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:37:33",
  "trx_id": "a2fc1aa9c9ab216a6da10e407cb4b6da3ecedf74",
  "trx_in_block": 6,
  "virtual_op": true
}
2026/06/06 07:37:33
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterretrodroid
weight1100 (11.00%)
Transaction InfoBlock #107029373/Trx a2fc1aa9c9ab216a6da10e407cb4b6da3ecedf74
View Raw JSON Data
{
  "block": 107029373,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "retrodroid",
      "weight": 1100
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:37:33",
  "trx_id": "a2fc1aa9c9ab216a6da10e407cb4b6da3ecedf74",
  "trx_in_block": 6,
  "virtual_op": false
}
2026/06/06 07:37:33
authorscipio
pending payout0.972 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares17314614294
total vote weight12950500343928
voterdarth-cryptic
weight17314614294
Transaction InfoBlock #107029373/Trx 6a72b841ef82cbe975e12a51e86d3dbd85e50cfa
View Raw JSON Data
{
  "block": 107029373,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.972 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 17314614294,
      "total_vote_weight": 12950500343928,
      "voter": "darth-cryptic",
      "weight": 17314614294
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:37:33",
  "trx_id": "6a72b841ef82cbe975e12a51e86d3dbd85e50cfa",
  "trx_in_block": 2,
  "virtual_op": true
}
2026/06/06 07:37:33
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterdarth-cryptic
weight1100 (11.00%)
Transaction InfoBlock #107029373/Trx 6a72b841ef82cbe975e12a51e86d3dbd85e50cfa
View Raw JSON Data
{
  "block": 107029373,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "darth-cryptic",
      "weight": 1100
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:37:33",
  "trx_id": "6a72b841ef82cbe975e12a51e86d3dbd85e50cfa",
  "trx_in_block": 2,
  "virtual_op": false
}
2026/06/06 07:37:33
authorscipio
pending payout0.971 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares6863139872
total vote weight12933185729634
votergrider123
weight6863139872
Transaction InfoBlock #107029373/Trx 1e4ddb10e40a83f5a0b328d449a690e538f0508d
View Raw JSON Data
{
  "block": 107029373,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.971 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 6863139872,
      "total_vote_weight": 12933185729634,
      "voter": "grider123",
      "weight": 6863139872
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:37:33",
  "trx_id": "1e4ddb10e40a83f5a0b328d449a690e538f0508d",
  "trx_in_block": 0,
  "virtual_op": true
}
2026/06/06 07:37:33
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
votergrider123
weight1100 (11.00%)
Transaction InfoBlock #107029373/Trx 1e4ddb10e40a83f5a0b328d449a690e538f0508d
View Raw JSON Data
{
  "block": 107029373,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "grider123",
      "weight": 1100
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:37:33",
  "trx_id": "1e4ddb10e40a83f5a0b328d449a690e538f0508d",
  "trx_in_block": 0,
  "virtual_op": false
}
2026/06/06 07:36:57
authorscipio
pending payout0.970 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares85179127707
total vote weight12926322589762
voterdarth-azrael
weight85179127707
Transaction InfoBlock #107029361/Trx 975d47e092dbef70999f6ff1924dffc1a1f88aa8
View Raw JSON Data
{
  "block": 107029361,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.970 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 85179127707,
      "total_vote_weight": 12926322589762,
      "voter": "darth-azrael",
      "weight": 85179127707
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:36:57",
  "trx_id": "975d47e092dbef70999f6ff1924dffc1a1f88aa8",
  "trx_in_block": 1,
  "virtual_op": true
}
2026/06/06 07:36:57
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterdarth-azrael
weight1100 (11.00%)
Transaction InfoBlock #107029361/Trx 975d47e092dbef70999f6ff1924dffc1a1f88aa8
View Raw JSON Data
{
  "block": 107029361,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "darth-azrael",
      "weight": 1100
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:36:57",
  "trx_id": "975d47e092dbef70999f6ff1924dffc1a1f88aa8",
  "trx_in_block": 1,
  "virtual_op": false
}
2026/06/06 07:20:51
authorscipio
pending payout0.963 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares11162786581
total vote weight12841143462055
voterjeronimorubio
weight11162786581
Transaction InfoBlock #107029040/Trx 3f16cf087c7e7b4b8e0fdd53d8ad39745f75a10c
View Raw JSON Data
{
  "block": 107029040,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.963 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 11162786581,
      "total_vote_weight": 12841143462055,
      "voter": "jeronimorubio",
      "weight": 11162786581
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:20:51",
  "trx_id": "3f16cf087c7e7b4b8e0fdd53d8ad39745f75a10c",
  "trx_in_block": 8,
  "virtual_op": true
}
2026/06/06 07:20:51
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterjeronimorubio
weight10000 (100.00%)
Transaction InfoBlock #107029040/Trx 3f16cf087c7e7b4b8e0fdd53d8ad39745f75a10c
View Raw JSON Data
{
  "block": 107029040,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "jeronimorubio",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:20:51",
  "trx_id": "3f16cf087c7e7b4b8e0fdd53d8ad39745f75a10c",
  "trx_in_block": 8,
  "virtual_op": false
}
scipioclaimed reward balance: 5.070 HIVE, 5.472 HP
2026/06/06 07:12:24
accountscipio
reward hbd0.000 HBD
reward hive5.070 HIVE
reward vests8883.533179 VESTS
Transaction InfoBlock #107028871/Trx 8f879266ecc097496541e0ab5c367b5d8c8f9ddb
View Raw JSON Data
{
  "block": 107028871,
  "op": [
    "claim_reward_balance",
    {
      "account": "scipio",
      "reward_hbd": "0.000 HBD",
      "reward_hive": "5.070 HIVE",
      "reward_vests": "8883.533179 VESTS"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:12:24",
  "trx_id": "8f879266ecc097496541e0ab5c367b5d8c8f9ddb",
  "trx_in_block": 0,
  "virtual_op": false
}
2026/06/06 07:11:33
authorcarminasalazarte
curatorscipio
payout must be claimedtrue
permlinkexposicion-pictorica-expresiones-de-miguel-pedriquez-en-puerto-ordaz-conociendo-al-maestroengesp
reward11.368291 VESTS
Transaction InfoBlock #107028854/Virtual Operation 4294967295:101
View Raw JSON Data
{
  "block": 107028854,
  "op": [
    "curation_reward",
    {
      "author": "carminasalazarte",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "exposicion-pictorica-expresiones-de-miguel-pedriquez-en-puerto-ordaz-conociendo-al-maestroengesp",
      "reward": "11.368291 VESTS"
    }
  ],
  "op_in_trx": 101,
  "timestamp": "2026-06-06T07:11:33",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
2026/06/06 07:11:15
authorscipio
pending payout0.962 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares2754500389
total vote weight12829980675474
voterhive-117638
weight2754500389
Transaction InfoBlock #107028848/Trx 583c1ca4466508b3227930736d199acbc32e95f9
View Raw JSON Data
{
  "block": 107028848,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.962 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 2754500389,
      "total_vote_weight": 12829980675474,
      "voter": "hive-117638",
      "weight": 2754500389
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:11:15",
  "trx_id": "583c1ca4466508b3227930736d199acbc32e95f9",
  "trx_in_block": 12,
  "virtual_op": true
}
2026/06/06 07:11:15
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterhive-117638
weight5000 (50.00%)
Transaction InfoBlock #107028848/Trx 583c1ca4466508b3227930736d199acbc32e95f9
View Raw JSON Data
{
  "block": 107028848,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "hive-117638",
      "weight": 5000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:11:15",
  "trx_id": "583c1ca4466508b3227930736d199acbc32e95f9",
  "trx_in_block": 12,
  "virtual_op": false
}
2026/06/06 07:10:51
authorscipio
pending payout0.961 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares1674622319
total vote weight12827226175085
votercommentators
weight1674622319
Transaction InfoBlock #107028840/Trx 4de6d04f55a268045a9c0d2eefc2962e7ec3eee9
View Raw JSON Data
{
  "block": 107028840,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.961 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 1674622319,
      "total_vote_weight": 12827226175085,
      "voter": "commentators",
      "weight": 1674622319
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:10:51",
  "trx_id": "4de6d04f55a268045a9c0d2eefc2962e7ec3eee9",
  "trx_in_block": 5,
  "virtual_op": true
}
2026/06/06 07:10:51
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
votercommentators
weight500 (5.00%)
Transaction InfoBlock #107028840/Trx 4de6d04f55a268045a9c0d2eefc2962e7ec3eee9
View Raw JSON Data
{
  "block": 107028840,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "commentators",
      "weight": 500
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:10:51",
  "trx_id": "4de6d04f55a268045a9c0d2eefc2962e7ec3eee9",
  "trx_in_block": 5,
  "virtual_op": false
}
2026/06/06 07:10:42
authorscipio
pending payout0.961 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares4993395831814
total vote weight12825551552766
voternuthman
weight4993395831814
Transaction InfoBlock #107028837/Trx dbd4dca1d35abb8344113c70f062dccc9e41c155
View Raw JSON Data
{
  "block": 107028837,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.961 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 4993395831814,
      "total_vote_weight": 12825551552766,
      "voter": "nuthman",
      "weight": 4993395831814
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:10:42",
  "trx_id": "dbd4dca1d35abb8344113c70f062dccc9e41c155",
  "trx_in_block": 2,
  "virtual_op": true
}
2026/06/06 07:10:42
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voternuthman
weight10000 (100.00%)
Transaction InfoBlock #107028837/Trx dbd4dca1d35abb8344113c70f062dccc9e41c155
View Raw JSON Data
{
  "block": 107028837,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "nuthman",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:10:42",
  "trx_id": "dbd4dca1d35abb8344113c70f062dccc9e41c155",
  "trx_in_block": 2,
  "virtual_op": false
}
2026/06/06 07:10:27
authorscipio
pending payout0.587 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares4222182602
total vote weight7832155720952
voterhive-103505
weight4222182602
Transaction InfoBlock #107028832/Trx 8bb6d8ad18471a31567b6d341c44cf0c26946f98
View Raw JSON Data
{
  "block": 107028832,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.587 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 4222182602,
      "total_vote_weight": 7832155720952,
      "voter": "hive-103505",
      "weight": 4222182602
    }
  ],
  "op_in_trx": 3,
  "timestamp": "2026-06-06T07:10:27",
  "trx_id": "8bb6d8ad18471a31567b6d341c44cf0c26946f98",
  "trx_in_block": 0,
  "virtual_op": true
}
2026/06/06 07:10:27
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterhive-103505
weight500 (5.00%)
Transaction InfoBlock #107028832/Trx 8bb6d8ad18471a31567b6d341c44cf0c26946f98
View Raw JSON Data
{
  "block": 107028832,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "hive-103505",
      "weight": 500
    }
  ],
  "op_in_trx": 2,
  "timestamp": "2026-06-06T07:10:27",
  "trx_id": "8bb6d8ad18471a31567b6d341c44cf0c26946f98",
  "trx_in_block": 0,
  "virtual_op": false
}
2026/06/06 07:10:27
authorscipio
pending payout0.587 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares31890756
total vote weight7827933538350
voterpsychophilo
weight31890756
Transaction InfoBlock #107028832/Trx 8bb6d8ad18471a31567b6d341c44cf0c26946f98
View Raw JSON Data
{
  "block": 107028832,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.587 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 31890756,
      "total_vote_weight": 7827933538350,
      "voter": "psychophilo",
      "weight": 31890756
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:10:27",
  "trx_id": "8bb6d8ad18471a31567b6d341c44cf0c26946f98",
  "trx_in_block": 0,
  "virtual_op": true
}
2026/06/06 07:10:27
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterpsychophilo
weight500 (5.00%)
Transaction InfoBlock #107028832/Trx 8bb6d8ad18471a31567b6d341c44cf0c26946f98
View Raw JSON Data
{
  "block": 107028832,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "psychophilo",
      "weight": 500
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:10:27",
  "trx_id": "8bb6d8ad18471a31567b6d341c44cf0c26946f98",
  "trx_in_block": 0,
  "virtual_op": false
}
2026/06/06 07:10:18
authorscipio
pending payout0.587 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares32628042712
total vote weight7827901647594
voterwe-are-ai
weight32628042712
Transaction InfoBlock #107028829/Trx 55e0e401d967ec1d63bafeee7ca620c5a4443a94
View Raw JSON Data
{
  "block": 107028829,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.587 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 32628042712,
      "total_vote_weight": 7827901647594,
      "voter": "we-are-ai",
      "weight": 32628042712
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:10:18",
  "trx_id": "55e0e401d967ec1d63bafeee7ca620c5a4443a94",
  "trx_in_block": 6,
  "virtual_op": true
}
2026/06/06 07:10:18
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterwe-are-ai
weight500 (5.00%)
Transaction InfoBlock #107028829/Trx 55e0e401d967ec1d63bafeee7ca620c5a4443a94
View Raw JSON Data
{
  "block": 107028829,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "we-are-ai",
      "weight": 500
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:10:18",
  "trx_id": "55e0e401d967ec1d63bafeee7ca620c5a4443a94",
  "trx_in_block": 6,
  "virtual_op": false
}
2026/06/06 07:09:48
authorscipio
pending payout0.584 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares6288924139541
total vote weight7795273604882
voteralexis555
weight6288924139541
Transaction InfoBlock #107028819/Trx 62f1029f49b092b6f7d45eb29a26726a5d6d357a
View Raw JSON Data
{
  "block": 107028819,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.584 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 6288924139541,
      "total_vote_weight": 7795273604882,
      "voter": "alexis555",
      "weight": 6288924139541
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:09:48",
  "trx_id": "62f1029f49b092b6f7d45eb29a26726a5d6d357a",
  "trx_in_block": 8,
  "virtual_op": true
}
2026/06/06 07:09:48
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voteralexis555
weight4100 (41.00%)
Transaction InfoBlock #107028819/Trx 62f1029f49b092b6f7d45eb29a26726a5d6d357a
View Raw JSON Data
{
  "block": 107028819,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "alexis555",
      "weight": 4100
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:09:48",
  "trx_id": "62f1029f49b092b6f7d45eb29a26726a5d6d357a",
  "trx_in_block": 8,
  "virtual_op": false
}
2026/06/06 07:05:51
authorscipio
pending payout0.112 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares2061290219
total vote weight1506349465341
voternewsrx
weight2061290219
Transaction InfoBlock #107028740/Trx b3e4f896d176ca125c0b1cdd7b232663ba5de64f
View Raw JSON Data
{
  "block": 107028740,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.112 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 2061290219,
      "total_vote_weight": 1506349465341,
      "voter": "newsrx",
      "weight": 2061290219
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:05:51",
  "trx_id": "b3e4f896d176ca125c0b1cdd7b232663ba5de64f",
  "trx_in_block": 5,
  "virtual_op": true
}
2026/06/06 07:05:51
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voternewsrx
weight10000 (100.00%)
Transaction InfoBlock #107028740/Trx b3e4f896d176ca125c0b1cdd7b232663ba5de64f
View Raw JSON Data
{
  "block": 107028740,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "newsrx",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:05:51",
  "trx_id": "b3e4f896d176ca125c0b1cdd7b232663ba5de64f",
  "trx_in_block": 5,
  "virtual_op": false
}
2026/06/06 07:05:45
authorscipio
pending payout0.112 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares4185108046
total vote weight1504288175122
voterblue-witness
weight4185108046
Transaction InfoBlock #107028738/Trx 08c63b58be1bdc55385090b0843b7b090b1258d2
View Raw JSON Data
{
  "block": 107028738,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.112 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 4185108046,
      "total_vote_weight": 1504288175122,
      "voter": "blue-witness",
      "weight": 4185108046
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:05:45",
  "trx_id": "08c63b58be1bdc55385090b0843b7b090b1258d2",
  "trx_in_block": 9,
  "virtual_op": true
}
2026/06/06 07:05:45
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterblue-witness
weight10000 (100.00%)
Transaction InfoBlock #107028738/Trx 08c63b58be1bdc55385090b0843b7b090b1258d2
View Raw JSON Data
{
  "block": 107028738,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "blue-witness",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:05:45",
  "trx_id": "08c63b58be1bdc55385090b0843b7b090b1258d2",
  "trx_in_block": 9,
  "virtual_op": false
}
2026/06/06 07:05:21
authorscipio
pending payout0.112 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares513489055127
total vote weight1500103067076
votersteem-ua
weight513489055127
Transaction InfoBlock #107028730/Trx fe3cf49ef4cf5e897f681ef4bc4b2ed0c6c2334b
View Raw JSON Data
{
  "block": 107028730,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.112 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 513489055127,
      "total_vote_weight": 1500103067076,
      "voter": "steem-ua",
      "weight": 513489055127
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:05:21",
  "trx_id": "fe3cf49ef4cf5e897f681ef4bc4b2ed0c6c2334b",
  "trx_in_block": 2,
  "virtual_op": true
}
2026/06/06 07:05:21
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
votersteem-ua
weight10000 (100.00%)
Transaction InfoBlock #107028730/Trx fe3cf49ef4cf5e897f681ef4bc4b2ed0c6c2334b
View Raw JSON Data
{
  "block": 107028730,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "steem-ua",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:05:21",
  "trx_id": "fe3cf49ef4cf5e897f681ef4bc4b2ed0c6c2334b",
  "trx_in_block": 2,
  "virtual_op": false
}
2026/06/06 07:05:18
authorscipio
pending payout0.073 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares15882476354
total vote weight986614011949
voterisnochys
weight15882476354
Transaction InfoBlock #107028729/Trx 7c0a00c8e7ee72b7f3aa7acf885764d211626774
View Raw JSON Data
{
  "block": 107028729,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.073 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 15882476354,
      "total_vote_weight": 986614011949,
      "voter": "isnochys",
      "weight": 15882476354
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:05:18",
  "trx_id": "7c0a00c8e7ee72b7f3aa7acf885764d211626774",
  "trx_in_block": 5,
  "virtual_op": true
}
2026/06/06 07:05:18
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterisnochys
weight500 (5.00%)
Transaction InfoBlock #107028729/Trx 7c0a00c8e7ee72b7f3aa7acf885764d211626774
View Raw JSON Data
{
  "block": 107028729,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "isnochys",
      "weight": 500
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:05:18",
  "trx_id": "7c0a00c8e7ee72b7f3aa7acf885764d211626774",
  "trx_in_block": 5,
  "virtual_op": false
}
2026/06/06 07:05:15
authorscipio
pending payout0.072 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares769779157987
total vote weight970731535595
voterbluerobo
weight769779157987
Transaction InfoBlock #107028728/Trx fd2afeec1b74feee6ccbbc2d7141bd157c609de8
View Raw JSON Data
{
  "block": 107028728,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.072 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 769779157987,
      "total_vote_weight": 970731535595,
      "voter": "bluerobo",
      "weight": 769779157987
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:05:15",
  "trx_id": "fd2afeec1b74feee6ccbbc2d7141bd157c609de8",
  "trx_in_block": 14,
  "virtual_op": true
}
2026/06/06 07:05:15
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterbluerobo
weight10000 (100.00%)
Transaction InfoBlock #107028728/Trx fd2afeec1b74feee6ccbbc2d7141bd157c609de8
View Raw JSON Data
{
  "block": 107028728,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "bluerobo",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:05:15",
  "trx_id": "fd2afeec1b74feee6ccbbc2d7141bd157c609de8",
  "trx_in_block": 14,
  "virtual_op": false
}
2026/06/06 07:05:15
authorscipio
pending payout0.015 HBD
permlinklearn-ai-series-89-medical-and-scientific-imaging
rshares200952377608
total vote weight200952377608
voterscipio
weight200952377608
Transaction InfoBlock #107028728/Trx d91ad66d63031b0fcb004603ef7b51da10a069db
View Raw JSON Data
{
  "block": 107028728,
  "op": [
    "effective_comment_vote",
    {
      "author": "scipio",
      "pending_payout": "0.015 HBD",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "rshares": 200952377608,
      "total_vote_weight": 200952377608,
      "voter": "scipio",
      "weight": 200952377608
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T07:05:15",
  "trx_id": "d91ad66d63031b0fcb004603ef7b51da10a069db",
  "trx_in_block": 11,
  "virtual_op": true
}
2026/06/06 07:05:15
authorscipio
permlinklearn-ai-series-89-medical-and-scientific-imaging
voterscipio
weight10000 (100.00%)
Transaction InfoBlock #107028728/Trx d91ad66d63031b0fcb004603ef7b51da10a069db
View Raw JSON Data
{
  "block": 107028728,
  "op": [
    "vote",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "voter": "scipio",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:05:15",
  "trx_id": "d91ad66d63031b0fcb004603ef7b51da10a069db",
  "trx_in_block": 11,
  "virtual_op": false
}
2026/06/06 07:05:09
authorscipio
body# Learn AI Series (#89) - Medical and Scientific Imaging ![variant-c-12-green.png](https://images.hive.blog/DQmP6whkhWUYdkmpCQ1kNbtQDbkZS4gB7HojM4NRt86wph1/variant-c-12-green.png) ### What will I learn - You will learn the unique challenges of medical imaging: small datasets, class imbalance, and regulatory requirements; - transfer learning from natural images to medical domains and why it works despite massive visual differences; - data augmentation strategies specific to medical images (and which standard augmentations will break your model); - handling class imbalance when rare conditions are what matter most; - explainability requirements in healthcare AI and how Grad-CAM reveals what your model actually learned; - regulatory considerations for deploying AI in clinical settings; - scientific imaging beyond healthcare: satellite data, microscopy, and astronomical image analysis. ### Requirements - A working modern computer running macOS, Windows or Ubuntu; - An installed Python 3(.11+) distribution; - The ambition to learn AI and machine learning. ### Difficulty - Beginner ### Curriculum (of the `Learn AI Series`): - [Learn AI Series (#1) - What Machine Learning Actually Is](https://hive.blog/hive-196387/@scipio/learn-ai-series-1-what-machine-learning-actually-is) - [Learn AI Series (#2) - Setting Up Your AI Workbench - Python and NumPy](https://hive.blog/hive-196387/@scipio/learn-ai-series-2-setting-up-your-ai-workbench-python-and-numpy) - [Learn AI Series (#3) - Your Data Is Just Numbers - How Machines See the World](https://hive.blog/hive-196387/@scipio/learn-ai-series-3-your-data-is-just-numbers-how-machines-see-the-world) - [Learn AI Series (#4) - Your First Prediction - No Math, Just Intuition](https://hive.blog/hive-196387/@scipio/learn-ai-series-4-your-first-prediction-no-math-just-intuition) - [Learn AI Series (#5) - Patterns in Data - What "Learning" Actually Looks Like](https://hive.blog/hive-196387/@scipio/learn-ai-series-5-patterns-in-data-what-learning-actually-looks-like) - [Learn AI Series (#6) - From Intuition to Math - Why We Need Formulas](https://hive.blog/hive-196387/@scipio/learn-ai-series-6-from-intuition-to-math-why-we-need-formulas) - [Learn AI Series (#7) - The Training Loop - See It Work Step by Step](https://hive.blog/hive-196387/@scipio/learn-ai-series-7-the-training-loop-see-it-work-step-by-step) - [Learn AI Series (#8) - The Math You Actually Need (Part 1) - Linear Algebra](https://hive.blog/hive-196387/@scipio/learn-ai-series-8-the-math-you-actually-need-part-1-linear-algebra) - [Learn AI Series (#9) - The Math You Actually Need (Part 2) - Calculus and Probability](https://hive.blog/hive-196387/@scipio/learn-ai-series-9-the-math-you-actually-need-part-2-calculus-and-probability) - [Learn AI Series (#10) - Your First ML Model - Linear Regression From Scratch](https://hive.blog/hive-196387/@scipio/learn-ai-series-10-your-first-ml-model-linear-regression-from-scratch) - [Learn AI Series (#11) - Making Linear Regression Real](https://hive.blog/hive-196387/@scipio/learn-ai-series-11-making-linear-regression-real) - [Learn AI Series (#12) - Classification - Logistic Regression From Scratch](https://hive.blog/hive-196387/@scipio/learn-ai-series-12-classification-logistic-regression-from-scratch) - [Learn AI Series (#13) - Evaluation - How to Know If Your Model Actually Works](https://hive.blog/hive-196387/@scipio/learn-ai-series-13-evaluation-how-to-know-if-your-model-actually-works) - [Learn AI Series (#14) - Data Preparation - The 80% Nobody Talks About](https://hive.blog/hive-196387/@scipio/learn-ai-series-14-data-preparation-the-80-nobody-talks-about) - [Learn AI Series (#15) - Feature Engineering and Selection](https://hive.blog/hive-196387/@scipio/learn-ai-series-15-feature-engineering-and-selection) - [Learn AI Series (#16) - Scikit-Learn - The Standard Library of ML](https://hive.blog/hive-196387/@scipio/learn-ai-series-16-scikit-learn-the-standard-library-of-ml) - [Learn AI Series (#17) - Decision Trees - How Machines Make Decisions](https://hive.blog/hive-196387/@scipio/learn-ai-series-17-decision-trees-how-machines-make-decisions) - [Learn AI Series (#18) - Random Forests - Wisdom of Crowds](https://hive.blog/hive-196387/@scipio/learn-ai-series-18-random-forests-wisdom-of-crowds) - [Learn AI Series (#19) - Gradient Boosting - The Kaggle Champion](https://hive.blog/hive-196387/@scipio/learn-ai-series-19-gradient-boosting-the-kaggle-champion) - [Learn AI Series (#20) - Support Vector Machines - Drawing the Perfect Boundary](https://hive.blog/hive-196387/@scipio/learn-ai-series-20-support-vector-machines-drawing-the-perfect-boundary) - [Learn AI Series (#21) - Mini Project - Predicting Crypto Market Regimes](https://hive.blog/hive-196387/@scipio/learn-ai-series-21-mini-project-predicting-crypto-market-regimes) - [Learn AI Series (#22) - K-Means Clustering - Finding Groups](https://hive.blog/hive-196387/@scipio/learn-ai-series-22-k-means-clustering-finding-groups) - [Learn AI Series (#23) - Advanced Clustering - Beyond K-Means](https://hive.blog/hive-196387/@scipio/learn-ai-series-23-advanced-clustering-beyond-k-means) - [Learn AI Series (#24) - Dimensionality Reduction - PCA](https://hive.blog/hive-196387/@scipio/learn-ai-series-24-dimensionality-reduction-pca) - [Learn AI Series (#25) - Advanced Dimensionality Reduction - t-SNE and UMAP](https://hive.blog/hive-196387/@scipio/learn-ai-series-25-advanced-dimensionality-reduction-t-sne-and-umap) - [Learn AI Series (#26) - Anomaly Detection - Finding What Doesn't Belong](https://hive.blog/hive-196387/@scipio/learn-ai-series-26-anomaly-detection-finding-what-doesnt-belong) - [Learn AI Series (#27) - Recommendation Systems - "Users Like You Also Liked..."](https://hive.blog/hive-196387/@scipio/learn-ai-series-27-recommendation-systems-users-like-you-also-liked) - [Learn AI Series (#28) - Time Series Fundamentals - When Order Matters](https://hive.blog/hive-196387/@scipio/learn-ai-series-28-time-series-fundamentals-when-order-matters) - [Learn AI Series (#29) - Time Series Forecasting - Predicting What Comes Next](https://hive.blog/hive-196387/@scipio/learn-ai-series-29-time-series-forecasting-predicting-what-comes-next) - [Learn AI Series (#30) - Natural Language Processing - Text as Data](https://hive.blog/hive-196387/@scipio/learn-ai-series-30-natural-language-processing-text-as-data) - [Learn AI Series (#31) - Word Embeddings - Meaning in Numbers](https://hive.blog/hive-196387/@scipio/learn-ai-series-31-word-embeddings-meaning-in-numbers) - [Learn AI Series (#32) - Bayesian Methods - Thinking in Probabilities](https://hive.blog/hive-196387/@scipio/learn-ai-series-32-bayesian-methods-thinking-in-probabilities) - [Learn AI Series (#33) - Ensemble Methods Deep Dive - Stacking and Blending](https://hive.blog/hive-196387/@scipio/learn-ai-series-33-ensemble-methods-deep-dive-stacking-and-blending) - [Learn AI Series (#34) - ML Engineering - From Notebook to Production](https://hive.blog/hive-196387/@scipio/learn-ai-series-34-ml-engineering-from-notebook-to-production) - [Learn AI Series (#35) - Data Ethics and Bias in ML](https://hive.blog/hive-196387/@scipio/learn-ai-series-35-data-ethics-and-bias-in-ml) - [Learn AI Series (#36) - Mini Project - Complete ML Pipeline](https://hive.blog/hive-196387/@scipio/learn-ai-series-36-mini-project-complete-ml-pipeline) - [Learn AI Series (#37) - The Perceptron - Where It All Started](https://hive.blog/hive-196387/@scipio/learn-ai-series-37-the-perceptron-where-it-all-started) - [Learn AI Series (#38) - Neural Networks From Scratch - Forward Pass](https://hive.blog/hive-196387/@scipio/learn-ai-series-38-neural-networks-from-scratch-forward-pass) - [Learn AI Series (#39) - Neural Networks From Scratch - Backpropagation](https://hive.blog/hive-196387/@scipio/learn-ai-series-39-neural-networks-from-scratch-backpropagation) - [Learn AI Series (#40) - Training Neural Networks - Practical Challenges](https://hive.blog/hive-196387/@scipio/learn-ai-series-40-training-neural-networks-practical-challenges) - [Learn AI Series (#41) - Optimization Algorithms - SGD, Momentum, Adam](https://hive.blog/hive-196387/@scipio/learn-ai-series-41-optimization-algorithms-sgd-momentum-adam) - [Learn AI Series (#42) - PyTorch Fundamentals - Tensors and Autograd](https://hive.blog/hive-196387/@scipio/learn-ai-series-42-pytorch-fundamentals-tensors-and-autograd) - [Learn AI Series (#43) - PyTorch Data and Training](https://hive.blog/hive-196387/@scipio/learn-ai-series-43-pytorch-data-and-training) - [Learn AI Series (#44) - PyTorch nn.Module - Building Real Networks](https://hive.blog/hive-196387/@scipio/learn-ai-series-44-pytorch-nnmodule-building-real-networks) - [Learn AI Series (#45) - Convolutional Neural Networks - Theory](https://hive.blog/hive-196387/@scipio/learn-ai-series-45-convolutional-neural-networks-theory) - [Learn AI Series (#46) - CNNs in Practice - Classic to Modern Architectures](https://hive.blog/hive-196387/@scipio/learn-ai-series-46-cnns-in-practice-classic-to-modern-architectures) - [Learn AI Series (#47) - CNN Applications - Detection, Segmentation, Style Transfer](https://hive.blog/hive-196387/@scipio/learn-ai-series-47-cnn-applications-detection-segmentation-style-transfer) - [Learn AI Series (#48) - Recurrent Neural Networks - Sequences](https://hive.blog/hive-196387/@scipio/learn-ai-series-48-recurrent-neural-networks-sequences) - [Learn AI Series (#49) - LSTM and GRU - Solving the Memory Problem](https://hive.blog/hive-196387/@scipio/learn-ai-series-49-lstm-and-gru-solving-the-memory-problem) - [Learn AI Series (#50) - Sequence-to-Sequence Models](https://hive.blog/hive-196387/@scipio/learn-ai-series-50-sequence-to-sequence-models) - [Learn AI Series (#51) - Attention Mechanisms](https://hive.blog/hive-196387/@scipio/learn-ai-series-51-attention-mechanisms) - [Learn AI Series (#52) - The Transformer Architecture (Part 1)](https://hive.blog/hive-196387/@scipio/learn-ai-series-52-the-transformer-architecture-part-1) - [Learn AI Series (#53) - The Transformer Architecture (Part 2)](https://hive.blog/hive-196387/@scipio/learn-ai-series-53-the-transformer-architecture-part-2) - [Learn AI Series (#54) - Vision Transformers](https://hive.blog/hive-196387/@scipio/learn-ai-series-54-vision-transformers) - [Learn AI Series (#55) - Generative Adversarial Networks](https://hive.blog/hive-196387/@scipio/learn-ai-series-55-generative-adversarial-networks) - [Learn AI Series (#56) - Mini Project - Building a Transformer From Scratch](https://hive.blog/hive-196387/@scipio/learn-ai-series-56-mini-project-building-a-transformer-from-scratch) - [Learn AI Series (#57) - Language Modeling - Predicting the Next Word](https://hive.blog/hive-196387/@scipio/learn-ai-series-57-language-modeling-predicting-the-next-word) - [Learn AI Series (#58) - GPT Architecture - Decoder-Only Transformers](https://hive.blog/hive-196387/@scipio/learn-ai-series-58-gpt-architecture-decoder-only-transformers) - [Learn AI Series (#59) - BERT and Encoder Models](https://hive.blog/hive-196387/@scipio/learn-ai-series-59-bert-and-encoder-models) - [Learn AI Series (#60) - Training Large Language Models](https://hive.blog/hive-196387/@scipio/learn-ai-series-60-training-large-language-models) - [Learn AI Series (#61) - Instruction Tuning and Alignment](https://hive.blog/hive-196387/@scipio/learn-ai-series-61-instruction-tuning-and-alignment) - [Learn AI Series (#62) - Prompt Engineering - Getting the Most from LLMs](https://hive.blog/hive-196387/@scipio/learn-ai-series-62-prompt-engineering-getting-the-most-from-llms) - [Learn AI Series (#63) - Embeddings and Vector Search](https://hive.blog/hive-196387/@scipio/learn-ai-series-63-embeddings-and-vector-search) - [Learn AI Series (#64) - Retrieval-Augmented Generation (RAG) - Basics](https://hive.blog/hive-196387/@scipio/learn-ai-series-64-retrieval-augmented-generation-rag-basics) - [Learn AI Series (#65) - RAG - Advanced Techniques](https://hive.blog/hive-196387/@scipio/learn-ai-series-65-rag-advanced-techniques) - [Learn AI Series (#66) - Working with LLM APIs](https://hive.blog/hive-196387/@scipio/learn-ai-series-66-working-with-llm-apis) - [Learn AI Series (#67) - Building AI Agents (Part 1) - Foundations](https://hive.blog/hive-196387/@scipio/learn-ai-series-67-building-ai-agents-part-1-foundations) - [Learn AI Series (#68) - Building AI Agents (Part 2) - Advanced Patterns](https://hive.blog/hive-196387/@scipio/learn-ai-series-68-building-ai-agents-part-2-advanced-patterns) - [Learn AI Series (#69) - Fine-Tuning Language Models](https://hive.blog/hive-196387/@scipio/learn-ai-series-69-fine-tuning-language-models) - [Learn AI Series (#70) - Running Local Models](https://hive.blog/hive-196387/@scipio/learn-ai-series-70-running-local-models) - [Learn AI Series (#71) - Text Generation Techniques](https://hive.blog/hive-196387/@scipio/learn-ai-series-71-text-generation-techniques) - [Learn AI Series (#72) - Tokenization Deep Dive](https://hive.blog/hive-196387/@scipio/learn-ai-series-72-tokenization-deep-dive) - [Learn AI Series (#73) - LLM Evaluation](https://hive.blog/hive-196387/@scipio/learn-ai-series-73-llm-evaluation) - [Learn AI Series (#74) - The Hugging Face Ecosystem](https://hive.blog/hive-196387/@scipio/learn-ai-series-74-the-hugging-face-ecosystem) - [Learn AI Series (#75) - Multimodal Models - Text Meets Vision](https://hive.blog/hive-196387/@scipio/learn-ai-series-75-multimodal-models-text-meets-vision) - [Learn AI Series (#76) - Mini Project - Your Own AI Assistant](https://hive.blog/hive-196387/@scipio/learn-ai-series-76-mini-project-your-own-ai-assistant) - [Learn AI Series (#77) - Image Processing Fundamentals](https://hive.blog/hive-196387/@scipio/learn-ai-series-77-image-processing-fundamentals) - [Learn AI Series (#78) - Object Detection (Part 1) - Foundations](https://hive.blog/hive-196387/@scipio/learn-ai-series-78-object-detection-part-1-foundations) - [Learn AI Series (#79) - Object Detection (Part 2) - Modern Approaches](https://hive.blog/hive-196387/@scipio/learn-ai-series-79-object-detection-part-2-modern-approaches) - [Learn AI Series (#80) - Image Segmentation](https://hive.blog/hive-196387/@scipio/learn-ai-series-80-image-segmentation) - [Learn AI Series (#81) - Pose Estimation and Tracking](https://hive.blog/hive-196387/@scipio/learn-ai-series-81-pose-estimation-and-tracking) - [Learn AI Series (#82) - Optical Character Recognition](https://hive.blog/hive-196387/@scipio/learn-ai-series-82-optical-character-recognition) - [Learn AI Series (#83) - Video Understanding](https://hive.blog/hive-196387/@scipio/learn-ai-series-83-video-understanding) - [Learn AI Series (#84) - Generative Images - Diffusion Models (Part 1)](https://hive.blog/hive-196387/@scipio/learn-ai-series-84-generative-images-diffusion-models-part-1) - [Learn AI Series (#85) - Generative Images - Diffusion Models (Part 2)](https://hive.blog/hive-196387/@scipio/learn-ai-series-85-generative-images-diffusion-models-part-2) - [Learn AI Series (#86) - Image-to-Image and Editing](https://hive.blog/hive-196387/@scipio/learn-ai-series-86-image-to-image-and-editing) - [Learn AI Series (#87) - 3D Vision](https://hive.blog/hive-196387/@scipio/learn-ai-series-87-3d-vision) - [Learn AI Series (#88) - Face Analysis](https://hive.blog/hive-196387/@scipio/learn-ai-series-88-face-analysis) - [Learn AI Series (#89) - Medical and Scientific Imaging](https://hive.blog/hive-196387/@scipio/learn-ai-series-89-medical-and-scientific-imaging) (this post) # Learn AI Series (#89) - Medical and Scientific Imaging ### Solutions to Episode #88 Exercises **Exercise 1:** Face embedding similarity analyzer. ```python import numpy as np class EmbeddingSimilarityAnalyzer: """Analyze face embedding similarity distributions for identity verification.""" def __init__(self, num_ids=5, photos_per=10, embed_dim=512, noise_sigma=0.15, seed=42): rng = np.random.RandomState(seed) self.num_ids = num_ids self.photos_per = photos_per self.labels = [] self.embeddings = [] for i in range(num_ids): centroid = rng.randn(embed_dim) centroid /= np.linalg.norm(centroid) for _ in range(photos_per): variant = centroid + rng.randn( embed_dim) * noise_sigma variant /= np.linalg.norm(variant) self.embeddings.append(variant) self.labels.append(i) self.embeddings = np.array(self.embeddings) self.labels = np.array(self.labels) def cosine_matrix(self): norms = np.linalg.norm( self.embeddings, axis=1, keepdims=True) normed = self.embeddings / norms return normed @ normed.T def analyze(self): sim = self.cosine_matrix() n = len(self.labels) same_sims = [] diff_sims = [] for i in range(n): for j in range(i + 1, n): if self.labels[i] == self.labels[j]: same_sims.append(sim[i, j]) else: diff_sims.append(sim[i, j]) same_sims = np.array(same_sims) diff_sims = np.array(diff_sims) print(f"Same-identity mean: " f"{same_sims.mean():.4f}") print(f"Diff-identity mean: " f"{diff_sims.mean():.4f}") print(f"Hardest positive: " f"{same_sims.min():.4f}") print(f"Hardest negative: " f"{diff_sims.max():.4f}") gap = same_sims.min() - diff_sims.max() print(f"Gap: {gap:.4f}") print(f"\n{'Thresh':>7} {'TPR':>6} " f"{'FPR':>6} {'Acc':>6}") print("-" * 28) best_acc, best_t = 0, 0 for t in [0.3, 0.4, 0.5, 0.6, 0.7, 0.8]: tp = (same_sims >= t).mean() fp = (diff_sims >= t).mean() acc = ((same_sims >= t).sum() + (diff_sims < t).sum()) / ( len(same_sims) + len(diff_sims)) if acc > best_acc: best_acc = acc best_t = t print(f"{t:>7.1f} {tp:>6.3f} " f"{fp:>6.3f} {acc:>6.3f}") print(f"\nBest: thresh={best_t}, " f"acc={best_acc:.3f}") return same_sims, diff_sims analyzer = EmbeddingSimilarityAnalyzer() analyzer.analyze() ``` With noise_sigma=0.15, the same-identity pairs cluster around cosine similarity 0.85-0.95 (high, because the noise is modest relative to the 512-dimensional centroid), while different-identity pairs settle near 0.0 (random unit vectors in 512D are nearly orthogonal). The gap between the hardest positive and hardest negative is comfortably positive, meaning a single threshold cleanly separates all identities. If you increase noise_sigma to 0.5 or higher, the same-identity distribution spreads out and starts overlapping with the different-identity distribution -- the gap shrinks toward zero and no threshold achieves perfect accuracy. This directly demonstrates why face recognition systems demand well-lit, frontal, aligned crops: reducing noise in the embedding space widens the gap. **Exercise 2:** Landmark-based face alignment tool. ```python import numpy as np class FaceAligner: """Align faces using 5-point landmarks via affine transformation.""" def __init__(self, target_eye_dist=70, target_center=(112, 112)): self.target_dist = target_eye_dist self.target_cx = target_center[0] self.target_cy = target_center[1] def compute_alignment(self, left_eye, right_eye): dx = right_eye[0] - left_eye[0] dy = right_eye[1] - left_eye[1] angle = np.degrees(np.arctan2(dy, dx)) dist = np.sqrt(dx ** 2 + dy ** 2) scale = self.target_dist / max( dist, 1e-8) cos_a = np.cos(np.radians(-angle)) sin_a = np.sin(np.radians(-angle)) R = np.array([[cos_a, -sin_a], [sin_a, cos_a]]) * scale mid_x = (left_eye[0] + right_eye[0]) / 2 mid_y = (left_eye[1] + right_eye[1]) / 2 tx = self.target_cx - ( R[0, 0] * mid_x + R[0, 1] * mid_y) ty = self.target_cy - ( R[1, 0] * mid_x + R[1, 1] * mid_y) M = np.zeros((2, 3)) M[:2, :2] = R M[0, 2] = tx M[1, 2] = ty return M, angle, dist def apply_affine(self, points, M): pts = np.array(points) ones = np.ones((len(pts), 1)) aug = np.hstack([pts, ones]) return (M @ aug.T).T def run(self): rng = np.random.RandomState(42) print(f"{'Case':>5} {'InAngle':>8} " f"{'OutAngle':>9} {'InDist':>7} " f"{'OutDist':>8} {'EyeCenter':>14}") print("-" * 56) for i in range(5): angle = rng.uniform(-30, 30) dist = rng.uniform(40, 120) cx = rng.uniform(80, 160) cy = rng.uniform(80, 160) rad = np.radians(angle) le = (cx - dist / 2 * np.cos(rad), cy - dist / 2 * np.sin(rad)) re = (cx + dist / 2 * np.cos(rad), cy + dist / 2 * np.sin(rad)) M, in_angle, in_dist = ( self.compute_alignment(le, re)) landmarks = [le, re, (cx, cy + 20), (cx - 15, cy + 40), (cx + 15, cy + 40)] aligned = self.apply_affine( landmarks, M) out_dx = aligned[1, 0] - aligned[0, 0] out_dy = aligned[1, 1] - aligned[0, 1] out_angle = np.degrees( np.arctan2(out_dy, out_dx)) out_dist = np.sqrt( out_dx ** 2 + out_dy ** 2) eye_cx = (aligned[0, 0] + aligned[1, 0]) / 2 eye_cy = (aligned[0, 1] + aligned[1, 1]) / 2 print(f"{i + 1:>5} {in_angle:>8.1f} " f"{out_angle:>9.4f} " f"{in_dist:>7.1f} " f"{out_dist:>8.1f} " f"({eye_cx:.0f}, {eye_cy:.0f})") aligner = FaceAligner() aligner.run() ``` Every test case produces an aligned eye angle of essentially 0 degrees (within floating point precision), an inter-eye distance of exactly 70 pixels, and eye centers at (112, 112) -- regardless of the input rotation, scale, or position. This consistency is exactly why alignment matters for face recognition: the downstream embedding network sees faces in a canonical pose every time, so it can focus all of its capacity on identity-discriminative features rather than wasting parameters on learning to handle rotations and scale variations. **Exercise 3:** Expression confusion matrix analyzer. ```python import numpy as np class ExpressionAnalyzer: """Analyze expression classification confusion patterns.""" EXPRESSIONS = [ "angry", "disgusted", "fearful", "happy", "neutral", "sad", "surprised" ] def __init__(self, samples_per=100, seed=42): self.n = samples_per self.seed = seed def generate_confusion(self, correct_probs= None, boost=None): rng = np.random.RandomState(self.seed) if correct_probs is None: correct_probs = { "angry": 0.65, "disgusted": 0.60, "fearful": 0.60, "happy": 0.80, "neutral": 0.70, "sad": 0.68, "surprised": 0.72} if boost: for cls, amt in boost.items(): correct_probs[cls] = min( correct_probs[cls] + amt, 0.95) confusion_pairs = { ("angry", "disgusted"): 0.12, ("disgusted", "angry"): 0.12, ("fearful", "surprised"): 0.15, ("surprised", "fearful"): 0.10, ("sad", "neutral"): 0.08, ("neutral", "sad"): 0.06} nc = len(self.EXPRESSIONS) cm = np.zeros((nc, nc), dtype=int) for i, expr in enumerate(self.EXPRESSIONS): p_correct = correct_probs[expr] remaining = 1.0 - p_correct probs = np.zeros(nc) probs[i] = p_correct used = 0.0 for (a, b), p in confusion_pairs.items(): if a == expr: j = self.EXPRESSIONS.index(b) probs[j] = min(p, remaining) used += probs[j] leftover = remaining - used for j in range(nc): if j != i and probs[j] == 0: probs[j] = leftover / max( nc - 1 - sum( 1 for k in range(nc) if k != i and probs[k] > 0), 1) probs /= probs.sum() cm[i] = rng.multinomial(self.n, probs) return cm def metrics(self, cm): nc = cm.shape[0] results = {} for i, expr in enumerate(self.EXPRESSIONS): tp = cm[i, i] fp = cm[:, i].sum() - tp fn = cm[i, :].sum() - tp prec = tp / max(tp + fp, 1) rec = tp / max(tp + fn, 1) f1 = (2 * prec * rec / max(prec + rec, 1e-8)) results[expr] = { "precision": prec, "recall": rec, "f1": f1} return results def run(self): cm = self.generate_confusion() # Print confusion matrix header = " " + " ".join( f"{e[:4]:>5}" for e in self.EXPRESSIONS) print(header) for i, expr in enumerate( self.EXPRESSIONS): row = f"{expr[:4]:>5} " + " ".join( f"{cm[i, j]:>5}" for j in range(len( self.EXPRESSIONS))) print(row) m = self.metrics(cm) print(f"\n{'Class':<12} {'Prec':>6} " f"{'Rec':>6} {'F1':>6}") print("-" * 32) for expr in self.EXPRESSIONS: d = m[expr] print(f"{expr:<12} " f"{d['precision']:>6.3f} " f"{d['recall']:>6.3f} " f"{d['f1']:>6.3f}") # Most confused pairs nc = len(self.EXPRESSIONS) pairs = [] for i in range(nc): for j in range(nc): if i != j: pairs.append( (cm[i, j], self.EXPRESSIONS[i], self.EXPRESSIONS[j])) pairs.sort(reverse=True) print(f"\nTop confused pairs:") for cnt, a, b in pairs[:4]: print(f" {a} -> {b}: {cnt}") ranked = sorted(m.items(), key=lambda x: x[1]["f1"]) print(f"\nDifficulty ranking (worst F1 " f"first):") for expr, d in ranked: print(f" {expr}: F1={d['f1']:.3f}") # Boost worst two worst = [ranked[0][0], ranked[1][0]] print(f"\nBoosting {worst} by +0.05:") cm2 = self.generate_confusion( boost={w: 0.05 for w in worst}) m2 = self.metrics(cm2) for w in worst: print(f" {w}: F1 {m[w]['f1']:.3f}" f" -> {m2[w]['f1']:.3f}") analyzer = ExpressionAnalyzer() analyzer.run() ``` The angry/disgusted and fearful/surprised pairs dominate the off-diagonal entries -- exactly as configured, and consistent with real expression recognition research. These pairs are genuinely hard because the underlying facial muscle movements overlap: anger and disgust both involve brow lowering and nose wrinkling, while fear and surprise both involve wide eyes and raised eyebrows. Happy achieves the highest F1 because smiling is the most visually distinctive expression (unique cheek raise + lip corner pull), while disgusted and fearful rank lowest due to their high mutual confusion rates. Adding 0.05 to the correct-class probability for the two worst performers improves their F1 scores modestly, demonstrating the diminishing returns of more data when the fundamental visual similarity between confusable classes remains. ### On to today's episode Here we go! We've spent the past thirteen episodes building a thorough understanding of computer vision: from basic image processing (#77) through object detection (#78-79), segmentation (#80), pose estimation (#81), OCR (#82), video (#83), diffusion models (#84-85), image editing (#86), 3D reconstruction (#87), and face analysis (#88). That entire arc dealt with **natural images** -- photographs of everyday scenes, objects, people, and places. But some of the highest-impact applications of computer vision don't involve natural photos at all. They involve **medical images**: chest X-rays, retinal scans, pathology slides, CT volumes. And beyond medicine, there's a whole universe of **scientific imaging**: satellite photos, microscopy, spectrograms, astronomical observations. These domains share a common property -- the images look nothing like ImageNet, the datasets are small and expensive to label, and getting the answer wrong can have serious consequences. This episode covers how to adapt everything we've learned to domains where the stakes are literally life-and-death, and where the standard "download a big dataset, train a deep model, deploy" pipeline falls apart completely ;-) ## Why medical imaging is fundamentally different If you've been following along since episode #13 (evaluation) and #14 (data preparation), you already know that data quality matters more than model architecture. Medical imaging takes every data challenge from those episodes and turns the dial to 11. **Small datasets**: a hospital might have 500 labeled chest X-rays showing a rare condition. Compare that to ImageNet's 14 million images. You can't just train a deeper model and hope it generalizes -- you'll overfit before the first epoch finishes. **Annotation cost**: labeling a single medical image often requires a board-certified specialist spending 10-30 minutes. Pixel-level segmentation masks (delineating tumor boundaries on a pathology slide) can take _hours_ per image. Some images need consensus from multiple experts because even specialists disagree on borderline cases. You can't crowdsource this on Mechanical Turk like you can with "is there a cat in this picture?" **Class imbalance**: in screening applications, the positive rate might be 1-5%. A model that always predicts "healthy" achieves 95%+ accuracy while being completely useless. Remember the precision-recall tradeoff from episode #13? This is where it becomes a matter of life and death. **High stakes**: a false negative (missed cancer) can kill. A false positive (unnecessary biopsy) causes harm, stress, and expense. The error _profile_ matters far more than the aggregate accuracy number. Telling a doctor "my model is 97% accurate" is meaningless without specifying sensitivity and specificity separately. **Domain shift**: a model trained on X-rays from Hospital A's GE scanner may fail on Hospital B's Siemens scanner. Different imaging protocols, sensor characteristics, patient demographics, and even the brand of contrast dye can shift the data distribution enough to break a model that worked perfectly in development. This is the same distribution shift problem from episode #14, but with much higher consequences. ```python import numpy as np class MedicalDatasetProfiler: """Profile a medical imaging dataset for common challenges: imbalance, size, and annotation cost estimation.""" def __init__(self, n_total, n_positive, annotation_minutes=15): self.n_total = n_total self.n_pos = n_positive self.n_neg = n_total - n_positive self.ann_min = annotation_minutes def profile(self): ratio = self.n_pos / self.n_total imbalance = self.n_neg / max( self.n_pos, 1) # Estimate annotation budget total_hours = ( self.n_total * self.ann_min / 60) cost_per_hour = 150 # specialist rate total_cost = total_hours * cost_per_hour # Effective training size accounting # for imbalance effective = min( self.n_pos, self.n_neg) * 2 print(f"Total samples: {self.n_total:,}") print(f"Positive: {self.n_pos:,} " f"({ratio * 100:.1f}%)") print(f"Negative: {self.n_neg:,} " f"({(1 - ratio) * 100:.1f}%)") print(f"Imbalance ratio: " f"{imbalance:.1f}:1") print(f"Effective samples: " f"{effective:,}") print(f"Annotation time: " f"{total_hours:,.0f} hours") print(f"Annotation cost: " f"${total_cost:,.0f}") if ratio < 0.05: print("WARNING: Severe imbalance -- " "focal loss recommended") if self.n_total < 1000: print("WARNING: Small dataset -- " "transfer learning essential") if effective < 200: print("WARNING: Very few effective " "samples -- consider few-shot") # Typical medical datasets print("=== Rare disease screening ===") MedicalDatasetProfiler(500, 15).profile() print() print("=== Chest X-ray (pneumonia) ===") MedicalDatasetProfiler(5000, 250).profile() print() print("=== Retinal scan (diabetic) ===") MedicalDatasetProfiler(10000, 1500).profile() ``` The numbers are sobering. A rare disease screening dataset with 500 images and 15 positives gives you an effective training set of just 30 samples after accounting for imbalance. And annotating those 500 images cost someone over $18,000 in specialist time. This is the reality of medical ML -- you don't get to complain about "only" having 50,000 training images like you might in a Kaggle competition. ## Transfer learning: standing on ImageNet's shoulders Despite the visual differences between ImageNet photos (dogs, cars, landscapes) and chest X-rays (greyscale, abstract anatomical structures), transfer learning works surprisingly well for medical images. The early layers of an ImageNet-pretrained CNN detect edges, textures, gradients, and simple shapes -- these are universal visual features that appear in every type of image. Only the later layers specialize for the target domain: ```python import torch import torch.nn as nn import torchvision.models as models class MedicalClassifier(nn.Module): """Medical image classifier built on top of an ImageNet-pretrained backbone.""" def __init__(self, num_classes=2, pretrained=True): super().__init__() self.backbone = models.resnet50( weights='DEFAULT' if pretrained else None) num_features = self.backbone.fc.in_features self.backbone.fc = nn.Sequential( nn.Dropout(0.5), nn.Linear(num_features, num_classes)) def forward(self, x): return self.backbone(x) def setup_gradual_unfreezing(model, lr_backbone=1e-5, lr_head=1e-3): """Different learning rates for pretrained backbone vs new classification head. Slow updates preserve learned features, fast updates adapt the new head.""" backbone_params = [] head_params = [] for name, param in model.named_parameters(): if 'fc' in name: head_params.append(param) else: backbone_params.append(param) return torch.optim.Adam([ {'params': backbone_params, 'lr': lr_backbone}, {'params': head_params, 'lr': lr_head}]) model = MedicalClassifier(num_classes=3) optimizer = setup_gradual_unfreezing(model) print(f"Backbone params: " f"{sum(p.numel() for p in model.backbone.parameters()):,}") print(f"Head params: " f"{sum(p.numel() for p in model.backbone.fc.parameters()):,}") ``` The strategy is: freeze the backbone initially, train only the classification head for a few epochs until it converges, then gradually unfreeze backbone layers from top to bottom with a much smaller learning rate. This prevents the pretrained features from being destroyed by agressive updates on a small medical dataset. The differential learning rate (100x difference between head and backbone) is critical -- the head needs to learn fast because it starts from random weights, while the backbone needs to adapt slowly because it already has useful features. **Medical foundation models** like MedCLIP and BiomedCLIP are now available -- pretrained on large collections of medical images and clinical reports. They serve as better starting points than ImageNet for medical tasks, similar to how domain-specific language models outperform general ones (as we discussed in episode #69). Having said that, ImageNet pretraining remains a surprisingly strong baseline even for medical domains, and many published results showing "medical foundation model beats ImageNet transfer" disappear when you control carefully for training procedure and hyperparameters. ## Data augmentation: what works and what breaks things Standard augmentation needs careful adaptation for medical images. You can't just copy-paste augmentation pipelines from a dog-vs-cat classifier and expect sensible results: ```python import torchvision.transforms as T import numpy as np from PIL import Image from scipy.ndimage import gaussian_filter from scipy.ndimage import map_coordinates class ElasticDeformation: """Elastic deformation: simulates tissue variation in medical images. The single most effective augmentation for soft-tissue segmentation tasks.""" def __init__(self, alpha=50, sigma=5): self.alpha = alpha self.sigma = sigma def __call__(self, image): img = np.array(image) shape = img.shape[:2] dx = gaussian_filter( np.random.randn(*shape), self.sigma) * self.alpha dy = gaussian_filter( np.random.randn(*shape), self.sigma) * self.alpha y, x = np.meshgrid( np.arange(shape[0]), np.arange(shape[1]), indexing='ij') indices = [ np.clip(y + dy, 0, shape[0] - 1), np.clip(x + dx, 0, shape[1] - 1)] if img.ndim == 3: result = np.stack([ map_coordinates(img[:, :, c], indices, order=1) for c in range(img.shape[2])], axis=-1) else: result = map_coordinates( img, indices, order=1) return Image.fromarray( result.astype(np.uint8)) # Safe augmentations for medical images medical_train_transforms = T.Compose([ T.RandomRotation(degrees=15), T.RandomAffine( degrees=0, translate=(0.05, 0.05), scale=(0.95, 1.05)), T.RandomResizedCrop( 224, scale=(0.85, 1.0)), T.RandomAdjustSharpness( sharpness_factor=2, p=0.3), T.GaussianBlur( kernel_size=3, sigma=(0.1, 1.0)), T.ColorJitter( brightness=0.2, contrast=0.2), T.ToTensor(), T.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) # Things you MUST NOT do to medical images: print("SAFE augmentations:") print(" Small rotations (< 15 degrees)") print(" Small translations") print(" Elastic deformation") print(" Brightness/contrast jitter") print(" Gaussian blur/noise") print() print("DANGEROUS augmentations:") print(" Horizontal flip (heart is on the LEFT)") print(" Vertical flip (anatomy has a top)") print(" Heavy color jitter (diagnostic info)") print(" Random erasing (may mask pathology)") ``` The horizontal flip warning is not theoretical. The human heart is on the left side of the chest. A horizontally flipped chest X-ray represents **situs inversus** -- a rare anatomical condition where all organs are mirrored. If you augment with horizontal flips, your model sees "normal" anatomy in a mirrored configuration and learns that the heart can be on either side. This is a subtle but real failure mode that has tripped up published research papers. Elastic deformation, on the other hand, is a goldmine for medical images. It simulates the natural biological variation in soft tissues -- muscles stretch and compress differently across patients, organs shift slightly with breathing, and pathological changes deform surrounding tissue. It's the single most effective domain-specific augmentation for segmentation tasks like tumor boundary delineation. ## Handling class imbalance When only 2% of your training images show the condition you're trying to detect, standard training produces a model that never predicts positive. Three complementary strategies: ```python import torch import torch.nn as nn from torch.utils.data import WeightedRandomSampler from torch.utils.data import DataLoader # Strategy 1: Weighted cross-entropy loss class_counts = [9800, 200] # healthy, diseased weights = torch.tensor( [1.0 / c for c in class_counts]) weights = weights / weights.sum() criterion = nn.CrossEntropyLoss(weight=weights) print(f"Class weights: {weights.tolist()}") # Strategy 2: Focal loss (Lin et al., 2017) class FocalLoss(nn.Module): """Down-weight easy examples, focus on hard ones. Elegant solution to class imbalance without manual weight tuning.""" def __init__(self, alpha=0.25, gamma=2.0): super().__init__() self.alpha = alpha self.gamma = gamma def forward(self, logits, targets): ce = nn.functional.cross_entropy( logits, targets, reduction='none') p = torch.exp(-ce) focal_weight = ( self.alpha * (1 - p) ** self.gamma) return (focal_weight * ce).mean() # Strategy 3: Oversampling with # WeightedRandomSampler def make_balanced_loader(dataset, labels, batch_size=32): """Each batch gets roughly equal representation of both classes.""" class_counts_per = np.bincount(labels) sample_weights = [ 1.0 / class_counts_per[l] for l in labels] sampler = WeightedRandomSampler( sample_weights, num_samples=len(dataset)) return DataLoader( dataset, batch_size=batch_size, sampler=sampler) # Compare focal loss behavior focal = FocalLoss(alpha=0.25, gamma=2.0) logits = torch.tensor([[2.0, -2.0], [0.5, 0.5], [-1.0, 1.0]]) targets = torch.tensor([0, 0, 1]) loss = focal(logits, targets) print(f"Focal loss: {loss.item():.4f}") ``` Focal loss is particularly elegant. When the model is already confident about an easy normal case (high `p`), the `(1-p)^gamma` term makes the gradient nearly zero -- the model doesn't waste time getting more confident about cases it already handles. When it's uncertain about a difficult positive case (low `p`), the gradient is large, so the model focuses its learning capacity where it matters. No manual tuning of class weights required -- the `gamma` parameter controls how aggressively easy examples are down-weighted, and gamma=2.0 works well across a wide range of imbalance ratios. ## Evaluation: the metrics that actually matter Standard accuracy is misleading for imbalanced medical data. The metrics clinicians care about: ```python from sklearn.metrics import roc_auc_score from sklearn.metrics import confusion_matrix import numpy as np def medical_evaluation(y_true, y_prob, threshold=0.5): """Comprehensive evaluation for medical binary classification. Prints the metrics that actually matter in clinical settings.""" y_pred = (y_prob >= threshold).astype(int) cm = confusion_matrix(y_true, y_pred) tn, fp, fn, tp = cm.ravel() sensitivity = tp / (tp + fn + 1e-10) specificity = tn / (tn + fp + 1e-10) ppv = tp / (tp + fp + 1e-10) npv = tn / (tn + fn + 1e-10) auc = roc_auc_score(y_true, y_prob) print(f"AUC: {auc:.4f}") print(f"Sensitivity: {sensitivity:.4f} " f"(missed {fn} of {tp + fn} cases)") print(f"Specificity: {specificity:.4f} " f"(false alarms: {fp})") print(f"PPV: {ppv:.4f}, NPV: {npv:.4f}") print(f"Confusion matrix:") print(f" TN={tn}, FP={fp}") print(f" FN={fn}, TP={tp}") # Find threshold for 95% sensitivity thresholds = np.linspace(0, 1, 1000) for t in thresholds: preds = (y_prob >= t).astype(int) sens = preds[y_true == 1].sum() / max( (y_true == 1).sum(), 1) if sens >= 0.95: spec = ( 1 - preds[y_true == 0]).sum() / max( (y_true == 0).sum(), 1) print(f"\nAt 95% sensitivity: " f"thresh={t:.3f}, " f"specificity={spec:.4f}") break # Simulated screening results rng = np.random.RandomState(42) y_true = np.array([0] * 950 + [1] * 50) y_prob = np.where( y_true == 1, rng.beta(8, 3, len(y_true)), rng.beta(2, 8, len(y_true))) medical_evaluation(y_true, y_prob) ``` The threshold choice is a **clinical decision**, not a technical one. For screening (catching every possible case -- think mammography), you want high sensitivity even at the cost of more false positives. For confirmatory diagnosis (being sure about a positive result before scheduling surgery), you want high specificity. The model doesn't change -- only the operating point on the ROC curve changes. This is why AUC is the standard metric for comparing models: it measures performance across _all_ possible thresholds, independent of the clinical use case. ## Explainability: showing the model's reasoning In healthcare, a black-box prediction is often unacceptable. Clinicians need to understand _why_ the model flagged an image. **Grad-CAM** (Gradient-weighted Class Activation Mapping) produces heatmaps showing which regions of the image contributed most to the prediction: ```python import torch import torch.nn.functional as F class GradCAM: """Grad-CAM: visual explanations for CNN predictions. Highlights which image regions drove the classification decision.""" def __init__(self, model, target_layer): self.model = model self.gradients = None self.activations = None target_layer.register_forward_hook( self._save_activation) target_layer.register_full_backward_hook( self._save_gradient) def _save_activation(self, module, input, output): self.activations = output.detach() def _save_gradient(self, module, grad_in, grad_out): self.gradients = grad_out[0].detach() def generate(self, input_image, target_class): self.model.eval() output = self.model(input_image) self.model.zero_grad() output[0, target_class].backward() # Weight each channel by its # average gradient weights = self.gradients.mean( dim=[2, 3], keepdim=True) cam = (weights * self.activations).sum( dim=1, keepdim=True) cam = F.relu(cam) cam = F.interpolate( cam, size=input_image.shape[2:], mode='bilinear', align_corners=False) cam = cam / (cam.max() + 1e-8) return cam.squeeze() # Demo with a ResNet50 backbone = models.resnet50(weights='DEFAULT') backbone.eval() target = backbone.layer4[-1] # last conv layer gradcam = GradCAM(backbone, target) fake_input = torch.randn(1, 3, 224, 224) heatmap = gradcam.generate(fake_input, target_class=0) print(f"Heatmap shape: {heatmap.shape}") print(f"Range: [{heatmap.min():.3f}, " f"{heatmap.max():.3f}]") ``` The resulting heatmap highlights the region the model focused on when making its prediction. If the model correctly predicts pneumonia and the heatmap highlights the lower right lung where the consolidation is visible, that builds clinical trust. If the heatmap highlights the patient's name label in the corner of the X-ray, that reveals a **data leakage** problem -- the model learned to associate certain patients (who appear in both training and test sets) with certain diagnoses, rather than learning actual radiological features. This data leakage scenario is not hypothetical. A 2018 study found that a pneumonia detection model achieved suspiciously high accuracy because it learned to recognize which hospital the X-ray came from (via scanner-specific markings and formatting), and different hospitals had different patient populations with different disease prevalence. The model was predicting hospital identity, not pathology. Grad-CAM caught it ;-) ## Beyond medicine: scientific imaging at large Medical imaging gets the most attention (and funding), but the same techniques apply across scientific disciplines. The common thread: specialized imaging modalities, expensive expert annotations, small datasets, and high-stakes decisions. **Satellite and remote sensing**: images have more channels than RGB (infrared, radar, multispectral bands with 10-200+ channels), much larger spatial extents (a single Sentinel-2 tile is 10,980 x 10,980 pixels at 10m resolution), and the objects of interest are often tiny relative to the image (a single building, a few hectares of deforestation, individual vehicles). The preprocessing pipeline alone -- atmospheric correction, cloud masking, radiometric calibration, georeferencing -- is a field of its own. **Microscopy**: fluorescence microscopy produces multichannel images where each channel corresponds to a different stain or fluorescent marker. The images are often very noisy (photon shot noise at low light levels), may be 3D (confocal z-stacks), and the relevant structures (cell nuclei, mitochondria, protein aggregates) range from a few pixels to thousands of pixels in size. Cell segmentation -- finding individual cell boundaries in densely packed tissue -- is one of the enduring computer vision challenges that gets harder as cells overlap and deform. **Astronomy**: images from telescopes deal with extreme dynamic range (a star might be millions of times brighter than the background), noise that follows Poisson statistics rather than Gaussian, and objects so faint they're barely distinguishable from noise artifacts. Galaxy morphology classification (spiral, elliptical, irregular) is one of the classic ML astronomy tasks, and the Galaxy Zoo citizen science project produced one of the first large-scale crowd-annotated astronomical datasets. ```python import numpy as np class MultiChannelNormalizer: """Normalize scientific images with arbitrary channel counts (satellite, microscopy, spectral). Each channel gets independent normalization.""" def __init__(self): self.means = None self.stds = None def fit(self, images): """Compute per-channel statistics from a batch of images. images: (N, C, H, W)""" self.means = images.mean( axis=(0, 2, 3)) self.stds = images.std( axis=(0, 2, 3)) self.stds[self.stds < 1e-8] = 1.0 return self def transform(self, image): """Normalize a single image. image: (C, H, W)""" normalized = np.zeros_like( image, dtype=np.float32) for c in range(image.shape[0]): normalized[c] = ( (image[c] - self.means[c]) / self.stds[c]) return normalized def percentile_clip(self, image, low=1, high=99): """Clip each channel to percentile range -- handles extreme values in satellite and astronomical data.""" clipped = np.zeros_like( image, dtype=np.float32) for c in range(image.shape[0]): lo = np.percentile(image[c], low) hi = np.percentile(image[c], high) clipped[c] = np.clip( image[c], lo, hi) rng = hi - lo if rng > 1e-8: clipped[c] = ( (clipped[c] - lo) / rng) return clipped # Simulate a 13-band satellite image rng = np.random.RandomState(42) # Different channels have very different # value ranges (realistic for Sentinel-2) sat_image = np.zeros((13, 256, 256)) for c in range(13): base = rng.uniform(100, 5000) scale = rng.uniform(50, 2000) sat_image[c] = rng.normal( base, scale, (256, 256)) norm = MultiChannelNormalizer() batch = sat_image[np.newaxis] norm.fit(batch) result = norm.transform(sat_image) clipped = norm.percentile_clip(sat_image) print(f"Channels: {sat_image.shape[0]}") print(f"\n{'Ch':>3} {'RawMean':>10} " f"{'RawStd':>10} {'NormMean':>10} " f"{'ClipRange':>12}") print("-" * 48) for c in range(sat_image.shape[0]): print(f"{c:>3} " f"{sat_image[c].mean():>10.1f} " f"{sat_image[c].std():>10.1f} " f"{result[c].mean():>10.4f} " f"[{clipped[c].min():.3f}, " f"{clipped[c].max():.3f}]") ``` The key insight for all scientific imaging: you can't use ImageNet normalization statistics (mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) when your images have 13 bands with value ranges spanning three orders of magnitude. Each domain needs its own preprocessing pipeline, its own normalization strategy, and often its own evaluation metrics. The model architectures we've been building throughout this series (CNNs, transformers, U-Nets) transfer well across domains -- it's the data pipeline that needs the most domain-specific engineering. ## The regulatory landscape Medical AI devices require regulatory approval before clinical deployment. This isn't optional -- it's the law, and it fundamentally shapes how medical AI is developed: **FDA (US)**: Software as a Medical Device (SaMD) classification. AI diagnostics typically fall under Class II (510(k) pathway, showing "substantial equivalence" to an existing cleared device) or Class III (premarket approval, for higher-risk applications). The FDA has cleared hundreds of AI medical devices as of 2025, mostly in radiology and cardiology. **CE marking (EU)**: Under the Medical Device Regulation (MDR), AI systems must demonstrate safety and performance through clinical evaluation. The EU's AI Act adds additional requirements for "high-risk" AI systems, which includes most medical diagnostic applications. Both require: documented training data provenance (where did every image come from, with what consent?), validated performance on representative populations (does it work for all demographics?), post-market surveillance plans (how will you catch failures after deployment?), and clear intended use statements (this model is for X, not for Y). You can't just train a model and put it in a hospital. The regulatory process also means that medical AI development follows a fundamentally different timesline than typical ML projects. From initial model development to regulatory clearance takes 1-3 years and costs hundreds of thousands to millions of dollars. This is why most medical AI research stays in academic papers rather than reaching patients -- the gap between "works on our test set" and "approved for clinical use" is enormous. ## Samengevat - Medical imaging has unique constraints that fundamentally alter the ML pipeline: small datasets (hundreds, not millions), expensive expert annotations ($150+/hour specialists), severe class imbalance (1-5% positive rates), and high-stakes errors where false negatives can be fatal; - **transfer learning from ImageNet works** for medical images despite massive visual differences; the early CNN layers learn universal edge/texture features that transfer across domains; medical foundation models (MedCLIP, BiomedCLIP) are emerging as better starting points but ImageNet remains a strong baseline; - augmentation must be **domain-aware**: elastic deformation is highly effective for simulating biological tissue variation, but horizontal flipping breaks anatomical assumptions (the heart is on the left); every augmentation choice must be validated against domain knowledge; - **focal loss** automatically focuses training on hard examples without manual class weight tuning; the `(1-p)^gamma` modulation makes the gradient near-zero for easy cases and large for difficult ones; - evaluation requires **sensitivity, specificity, and AUC** rather than accuracy; threshold selection is a clinical decision that trades off missed cases against false alarms, and different clinical use cases (screening vs confirmation) demand different operating points; - **Grad-CAM** provides visual explanations essential for clinical trust and debugging data leakage -- verifying that models focus on pathology rather than scanner artifacts or patient labels is non-negotiable; - **scientific imaging** beyond medicine (satellite, microscopy, astronomy) shares the same challenges of specialized modalities, small datasets, and domain-specific preprocessing but with additional complications like multi-channel inputs, extreme dynamic ranges, and non-Gaussian noise models; - regulatory approval (FDA, CE marking) is mandatory before clinical deployment and shapes the entire development process -- from data provenance documentation to post-market surveillance plans. We've now covered how computer vision applies to specialized domains where getting it right really matters. The vision arc of this series has been extensive -- from raw pixels all the way to generating images, reconstructing 3D scenes, analyzing faces, and now domain-specific scientific applications. There's one more foundational concept in vision that we haven't explored yet: how machines can learn powerful visual representations _without_ any human labels at all, using only the structure of the data itself. ### Exercises **Exercise 1:** Build a **medical dataset augmentation validator**. Create a class `AugmentationValidator` that: (a) generates a synthetic 64x64 "chest X-ray" as a numpy array with a circle in the left half (simulating the heart) and a smaller circle in the right half (simulating a nodule), both with different intensities against a noisy background, (b) implements `check_anatomical_consistency(original, augmented)` that verifies the heart-like circle is still in the left half after augmentation -- compute the column-wise intensity sum for each half and check that the brighter half hasn't switched sides, (c) applies 5 different augmentations to the synthetic image: (1) small rotation 10 degrees, (2) horizontal flip, (3) elastic deformation with alpha=30 sigma=4, (4) brightness shift +20%, (5) Gaussian noise sigma=0.05, (d) for each augmentation, prints whether anatomical consistency is preserved, (e) computes a "signal preservation score" for each augmentation: the correlation coefficient between the original and augmented images (values near 1.0 mean the content is well preserved, values near 0.0 mean significant distortion). Verify that horizontal flip fails the anatomical consistency check while all other augmentations pass, and that elastic deformation preserves anatomy while having lower correlation than simple brightness changes. **Exercise 2:** Build a **class imbalance strategy comparator**. Create a class `ImbalanceComparator` that: (a) generates a synthetic binary classification dataset with 1000 samples where only 30 are positive, with features drawn from overlapping Gaussians (positive: mean=1.0, std=1.5; negative: mean=0.0, std=1.0) in 10 dimensions, (b) trains 3 logistic regression classifiers (using sklearn): one with default settings, one with `class_weight='balanced'`, and one using SMOTE oversampling on the training set (use imblearn.over_sampling.SMOTE or implement a simple random oversampler if imblearn is not available), (c) evaluates each on a held-out test set (20% split) using sensitivity, specificity, AUC, and F1, (d) prints a comparison table showing all 4 metrics for all 3 strategies, (e) for each strategy, finds the threshold that achieves >= 90% sensitivity and reports the corresponding specificity. Verify that the default (unweighted) classifier has high specificity but poor sensitivity, while balanced weighting and oversampling improve sensitivity at the cost of some specificity. **Exercise 3:** Build a **multi-channel scientific image normalizer and analyzer**. Create a class `ScientificImageAnalyzer` that: (a) generates a synthetic 8-channel "satellite image" of size 128x128 where: channels 0-2 are visible RGB (values 0-255), channels 3-4 are near-infrared (values 500-3000), channel 5 is thermal (values 250-320, representing Kelvin), channels 6-7 are radar backscatter (values in dB, range -25 to 5), (b) implements `per_channel_normalize(image)` that z-score normalizes each channel independently, (c) implements `percentile_clip(image, low_pct=2, high_pct=98)` that clips each channel to its 2nd and 98th percentiles then rescales to [0, 1], (d) implements `compute_ndvi(image)` that computes the Normalized Difference Vegetation Index from the NIR and Red channels: NDVI = (NIR - Red) / (NIR + Red + 1e-8), (e) prints per-channel statistics (mean, std, min, max) before and after normalization, (f) computes and prints NDVI statistics (mean, std, fraction of pixels with NDVI > 0.3 indicating vegetation), (g) compares the correlation between channel pairs before and after normalization to verify that normalization preserves inter-channel relationships. Verify that per-channel normalization brings all channels to mean~0, std~1 regardless of their original value ranges, and that NDVI values are invariant to the normalization method used. ### Thanks for reading! @scipio
json metadata{"tags": ["stem", "stemsocial", "steemstem", "python", "programming"], "app": "hiveblog/0.1", "format": "markdown", "image": ["https://images.hive.blog/DQmP6whkhWUYdkmpCQ1kNbtQDbkZS4gB7HojM4NRt86wph1/variant-c-12-green.png"]}
parent author
parent permlinkhive-196387
permlinklearn-ai-series-89-medical-and-scientific-imaging
titleLearn AI Series (#89) - Medical and Scientific Imaging
Transaction InfoBlock #107028726/Trx bd048f75341b420b2bee01a07362384eac2af931
View Raw JSON Data
{
  "block": 107028726,
  "op": [
    "comment",
    {
      "author": "scipio",
      "body": "# Learn AI Series (#89) - Medical and Scientific Imaging\n\n![variant-c-12-green.png](https://images.hive.blog/DQmP6whkhWUYdkmpCQ1kNbtQDbkZS4gB7HojM4NRt86wph1/variant-c-12-green.png)\n\n### What will I learn\n- You will learn the unique challenges of medical imaging: small datasets, class imbalance, and regulatory requirements;\n- transfer learning from natural images to medical domains and why it works despite massive visual differences;\n- data augmentation strategies specific to medical images (and which standard augmentations will break your model);\n- handling class imbalance when rare conditions are what matter most;\n- explainability requirements in healthcare AI and how Grad-CAM reveals what your model actually learned;\n- regulatory considerations for deploying AI in clinical settings;\n- scientific imaging beyond healthcare: satellite data, microscopy, and astronomical image analysis.\n\n### Requirements\n- A working modern computer running macOS, Windows or Ubuntu;\n- An installed Python 3(.11+) distribution;\n- The ambition to learn AI and machine learning.\n\n### Difficulty\n- Beginner\n\n### Curriculum (of the `Learn AI Series`):\n- [Learn AI Series (#1) - What Machine Learning Actually Is](https://hive.blog/hive-196387/@scipio/learn-ai-series-1-what-machine-learning-actually-is)\n- [Learn AI Series (#2) - Setting Up Your AI Workbench - Python and NumPy](https://hive.blog/hive-196387/@scipio/learn-ai-series-2-setting-up-your-ai-workbench-python-and-numpy)\n- [Learn AI Series (#3) - Your Data Is Just Numbers - How Machines See the World](https://hive.blog/hive-196387/@scipio/learn-ai-series-3-your-data-is-just-numbers-how-machines-see-the-world)\n- [Learn AI Series (#4) - Your First Prediction - No Math, Just Intuition](https://hive.blog/hive-196387/@scipio/learn-ai-series-4-your-first-prediction-no-math-just-intuition)\n- [Learn AI Series (#5) - Patterns in Data - What \"Learning\" Actually Looks Like](https://hive.blog/hive-196387/@scipio/learn-ai-series-5-patterns-in-data-what-learning-actually-looks-like)\n- [Learn AI Series (#6) - From Intuition to Math - Why We Need Formulas](https://hive.blog/hive-196387/@scipio/learn-ai-series-6-from-intuition-to-math-why-we-need-formulas)\n- [Learn AI Series (#7) - The Training Loop - See It Work Step by Step](https://hive.blog/hive-196387/@scipio/learn-ai-series-7-the-training-loop-see-it-work-step-by-step)\n- [Learn AI Series (#8) - The Math You Actually Need (Part 1) - Linear Algebra](https://hive.blog/hive-196387/@scipio/learn-ai-series-8-the-math-you-actually-need-part-1-linear-algebra)\n- [Learn AI Series (#9) - The Math You Actually Need (Part 2) - Calculus and Probability](https://hive.blog/hive-196387/@scipio/learn-ai-series-9-the-math-you-actually-need-part-2-calculus-and-probability)\n- [Learn AI Series (#10) - Your First ML Model - Linear Regression From Scratch](https://hive.blog/hive-196387/@scipio/learn-ai-series-10-your-first-ml-model-linear-regression-from-scratch)\n- [Learn AI Series (#11) - Making Linear Regression Real](https://hive.blog/hive-196387/@scipio/learn-ai-series-11-making-linear-regression-real)\n- [Learn AI Series (#12) - Classification - Logistic Regression From Scratch](https://hive.blog/hive-196387/@scipio/learn-ai-series-12-classification-logistic-regression-from-scratch)\n- [Learn AI Series (#13) - Evaluation - How to Know If Your Model Actually Works](https://hive.blog/hive-196387/@scipio/learn-ai-series-13-evaluation-how-to-know-if-your-model-actually-works)\n- [Learn AI Series (#14) - Data Preparation - The 80% Nobody Talks About](https://hive.blog/hive-196387/@scipio/learn-ai-series-14-data-preparation-the-80-nobody-talks-about)\n- [Learn AI Series (#15) - Feature Engineering and Selection](https://hive.blog/hive-196387/@scipio/learn-ai-series-15-feature-engineering-and-selection)\n- [Learn AI Series (#16) - Scikit-Learn - The Standard Library of ML](https://hive.blog/hive-196387/@scipio/learn-ai-series-16-scikit-learn-the-standard-library-of-ml)\n- [Learn AI Series (#17) - Decision Trees - How Machines Make Decisions](https://hive.blog/hive-196387/@scipio/learn-ai-series-17-decision-trees-how-machines-make-decisions)\n- [Learn AI Series (#18) - Random Forests - Wisdom of Crowds](https://hive.blog/hive-196387/@scipio/learn-ai-series-18-random-forests-wisdom-of-crowds)\n- [Learn AI Series (#19) - Gradient Boosting - The Kaggle Champion](https://hive.blog/hive-196387/@scipio/learn-ai-series-19-gradient-boosting-the-kaggle-champion)\n- [Learn AI Series (#20) - Support Vector Machines - Drawing the Perfect Boundary](https://hive.blog/hive-196387/@scipio/learn-ai-series-20-support-vector-machines-drawing-the-perfect-boundary)\n- [Learn AI Series (#21) - Mini Project - Predicting Crypto Market Regimes](https://hive.blog/hive-196387/@scipio/learn-ai-series-21-mini-project-predicting-crypto-market-regimes)\n- [Learn AI Series (#22) - K-Means Clustering - Finding Groups](https://hive.blog/hive-196387/@scipio/learn-ai-series-22-k-means-clustering-finding-groups)\n- [Learn AI Series (#23) - Advanced Clustering - Beyond K-Means](https://hive.blog/hive-196387/@scipio/learn-ai-series-23-advanced-clustering-beyond-k-means)\n- [Learn AI Series (#24) - Dimensionality Reduction - PCA](https://hive.blog/hive-196387/@scipio/learn-ai-series-24-dimensionality-reduction-pca)\n- [Learn AI Series (#25) - Advanced Dimensionality Reduction - t-SNE and UMAP](https://hive.blog/hive-196387/@scipio/learn-ai-series-25-advanced-dimensionality-reduction-t-sne-and-umap)\n- [Learn AI Series (#26) - Anomaly Detection - Finding What Doesn't Belong](https://hive.blog/hive-196387/@scipio/learn-ai-series-26-anomaly-detection-finding-what-doesnt-belong)\n- [Learn AI Series (#27) - Recommendation Systems - \"Users Like You Also Liked...\"](https://hive.blog/hive-196387/@scipio/learn-ai-series-27-recommendation-systems-users-like-you-also-liked)\n- [Learn AI Series (#28) - Time Series Fundamentals - When Order Matters](https://hive.blog/hive-196387/@scipio/learn-ai-series-28-time-series-fundamentals-when-order-matters)\n- [Learn AI Series (#29) - Time Series Forecasting - Predicting What Comes Next](https://hive.blog/hive-196387/@scipio/learn-ai-series-29-time-series-forecasting-predicting-what-comes-next)\n- [Learn AI Series (#30) - Natural Language Processing - Text as Data](https://hive.blog/hive-196387/@scipio/learn-ai-series-30-natural-language-processing-text-as-data)\n- [Learn AI Series (#31) - Word Embeddings - Meaning in Numbers](https://hive.blog/hive-196387/@scipio/learn-ai-series-31-word-embeddings-meaning-in-numbers)\n- [Learn AI Series (#32) - Bayesian Methods - Thinking in Probabilities](https://hive.blog/hive-196387/@scipio/learn-ai-series-32-bayesian-methods-thinking-in-probabilities)\n- [Learn AI Series (#33) - Ensemble Methods Deep Dive - Stacking and Blending](https://hive.blog/hive-196387/@scipio/learn-ai-series-33-ensemble-methods-deep-dive-stacking-and-blending)\n- [Learn AI Series (#34) - ML Engineering - From Notebook to Production](https://hive.blog/hive-196387/@scipio/learn-ai-series-34-ml-engineering-from-notebook-to-production)\n- [Learn AI Series (#35) - Data Ethics and Bias in ML](https://hive.blog/hive-196387/@scipio/learn-ai-series-35-data-ethics-and-bias-in-ml)\n- [Learn AI Series (#36) - Mini Project - Complete ML Pipeline](https://hive.blog/hive-196387/@scipio/learn-ai-series-36-mini-project-complete-ml-pipeline)\n- [Learn AI Series (#37) - The Perceptron - Where It All Started](https://hive.blog/hive-196387/@scipio/learn-ai-series-37-the-perceptron-where-it-all-started)\n- [Learn AI Series (#38) - Neural Networks From Scratch - Forward Pass](https://hive.blog/hive-196387/@scipio/learn-ai-series-38-neural-networks-from-scratch-forward-pass)\n- [Learn AI Series (#39) - Neural Networks From Scratch - Backpropagation](https://hive.blog/hive-196387/@scipio/learn-ai-series-39-neural-networks-from-scratch-backpropagation)\n- [Learn AI Series (#40) - Training Neural Networks - Practical Challenges](https://hive.blog/hive-196387/@scipio/learn-ai-series-40-training-neural-networks-practical-challenges)\n- [Learn AI Series (#41) - Optimization Algorithms - SGD, Momentum, Adam](https://hive.blog/hive-196387/@scipio/learn-ai-series-41-optimization-algorithms-sgd-momentum-adam)\n- [Learn AI Series (#42) - PyTorch Fundamentals - Tensors and Autograd](https://hive.blog/hive-196387/@scipio/learn-ai-series-42-pytorch-fundamentals-tensors-and-autograd)\n- [Learn AI Series (#43) - PyTorch Data and Training](https://hive.blog/hive-196387/@scipio/learn-ai-series-43-pytorch-data-and-training)\n- [Learn AI Series (#44) - PyTorch nn.Module - Building Real Networks](https://hive.blog/hive-196387/@scipio/learn-ai-series-44-pytorch-nnmodule-building-real-networks)\n- [Learn AI Series (#45) - Convolutional Neural Networks - Theory](https://hive.blog/hive-196387/@scipio/learn-ai-series-45-convolutional-neural-networks-theory)\n- [Learn AI Series (#46) - CNNs in Practice - Classic to Modern Architectures](https://hive.blog/hive-196387/@scipio/learn-ai-series-46-cnns-in-practice-classic-to-modern-architectures)\n- [Learn AI Series (#47) - CNN Applications - Detection, Segmentation, Style Transfer](https://hive.blog/hive-196387/@scipio/learn-ai-series-47-cnn-applications-detection-segmentation-style-transfer)\n- [Learn AI Series (#48) - Recurrent Neural Networks - Sequences](https://hive.blog/hive-196387/@scipio/learn-ai-series-48-recurrent-neural-networks-sequences)\n- [Learn AI Series (#49) - LSTM and GRU - Solving the Memory Problem](https://hive.blog/hive-196387/@scipio/learn-ai-series-49-lstm-and-gru-solving-the-memory-problem)\n- [Learn AI Series (#50) - Sequence-to-Sequence Models](https://hive.blog/hive-196387/@scipio/learn-ai-series-50-sequence-to-sequence-models)\n- [Learn AI Series (#51) - Attention Mechanisms](https://hive.blog/hive-196387/@scipio/learn-ai-series-51-attention-mechanisms)\n- [Learn AI Series (#52) - The Transformer Architecture (Part 1)](https://hive.blog/hive-196387/@scipio/learn-ai-series-52-the-transformer-architecture-part-1)\n- [Learn AI Series (#53) - The Transformer Architecture (Part 2)](https://hive.blog/hive-196387/@scipio/learn-ai-series-53-the-transformer-architecture-part-2)\n- [Learn AI Series (#54) - Vision Transformers](https://hive.blog/hive-196387/@scipio/learn-ai-series-54-vision-transformers)\n- [Learn AI Series (#55) - Generative Adversarial Networks](https://hive.blog/hive-196387/@scipio/learn-ai-series-55-generative-adversarial-networks)\n- [Learn AI Series (#56) - Mini Project - Building a Transformer From Scratch](https://hive.blog/hive-196387/@scipio/learn-ai-series-56-mini-project-building-a-transformer-from-scratch)\n- [Learn AI Series (#57) - Language Modeling - Predicting the Next Word](https://hive.blog/hive-196387/@scipio/learn-ai-series-57-language-modeling-predicting-the-next-word)\n- [Learn AI Series (#58) - GPT Architecture - Decoder-Only Transformers](https://hive.blog/hive-196387/@scipio/learn-ai-series-58-gpt-architecture-decoder-only-transformers)\n- [Learn AI Series (#59) - BERT and Encoder Models](https://hive.blog/hive-196387/@scipio/learn-ai-series-59-bert-and-encoder-models)\n- [Learn AI Series (#60) - Training Large Language Models](https://hive.blog/hive-196387/@scipio/learn-ai-series-60-training-large-language-models)\n- [Learn AI Series (#61) - Instruction Tuning and Alignment](https://hive.blog/hive-196387/@scipio/learn-ai-series-61-instruction-tuning-and-alignment)\n- [Learn AI Series (#62) - Prompt Engineering - Getting the Most from LLMs](https://hive.blog/hive-196387/@scipio/learn-ai-series-62-prompt-engineering-getting-the-most-from-llms)\n- [Learn AI Series (#63) - Embeddings and Vector Search](https://hive.blog/hive-196387/@scipio/learn-ai-series-63-embeddings-and-vector-search)\n- [Learn AI Series (#64) - Retrieval-Augmented Generation (RAG) - Basics](https://hive.blog/hive-196387/@scipio/learn-ai-series-64-retrieval-augmented-generation-rag-basics)\n- [Learn AI Series (#65) - RAG - Advanced Techniques](https://hive.blog/hive-196387/@scipio/learn-ai-series-65-rag-advanced-techniques)\n- [Learn AI Series (#66) - Working with LLM APIs](https://hive.blog/hive-196387/@scipio/learn-ai-series-66-working-with-llm-apis)\n- [Learn AI Series (#67) - Building AI Agents (Part 1) - Foundations](https://hive.blog/hive-196387/@scipio/learn-ai-series-67-building-ai-agents-part-1-foundations)\n- [Learn AI Series (#68) - Building AI Agents (Part 2) - Advanced Patterns](https://hive.blog/hive-196387/@scipio/learn-ai-series-68-building-ai-agents-part-2-advanced-patterns)\n- [Learn AI Series (#69) - Fine-Tuning Language Models](https://hive.blog/hive-196387/@scipio/learn-ai-series-69-fine-tuning-language-models)\n- [Learn AI Series (#70) - Running Local Models](https://hive.blog/hive-196387/@scipio/learn-ai-series-70-running-local-models)\n- [Learn AI Series (#71) - Text Generation Techniques](https://hive.blog/hive-196387/@scipio/learn-ai-series-71-text-generation-techniques)\n- [Learn AI Series (#72) - Tokenization Deep Dive](https://hive.blog/hive-196387/@scipio/learn-ai-series-72-tokenization-deep-dive)\n- [Learn AI Series (#73) - LLM Evaluation](https://hive.blog/hive-196387/@scipio/learn-ai-series-73-llm-evaluation)\n- [Learn AI Series (#74) - The Hugging Face Ecosystem](https://hive.blog/hive-196387/@scipio/learn-ai-series-74-the-hugging-face-ecosystem)\n- [Learn AI Series (#75) - Multimodal Models - Text Meets Vision](https://hive.blog/hive-196387/@scipio/learn-ai-series-75-multimodal-models-text-meets-vision)\n- [Learn AI Series (#76) - Mini Project - Your Own AI Assistant](https://hive.blog/hive-196387/@scipio/learn-ai-series-76-mini-project-your-own-ai-assistant)\n- [Learn AI Series (#77) - Image Processing Fundamentals](https://hive.blog/hive-196387/@scipio/learn-ai-series-77-image-processing-fundamentals)\n- [Learn AI Series (#78) - Object Detection (Part 1) - Foundations](https://hive.blog/hive-196387/@scipio/learn-ai-series-78-object-detection-part-1-foundations)\n- [Learn AI Series (#79) - Object Detection (Part 2) - Modern Approaches](https://hive.blog/hive-196387/@scipio/learn-ai-series-79-object-detection-part-2-modern-approaches)\n- [Learn AI Series (#80) - Image Segmentation](https://hive.blog/hive-196387/@scipio/learn-ai-series-80-image-segmentation)\n- [Learn AI Series (#81) - Pose Estimation and Tracking](https://hive.blog/hive-196387/@scipio/learn-ai-series-81-pose-estimation-and-tracking)\n- [Learn AI Series (#82) - Optical Character Recognition](https://hive.blog/hive-196387/@scipio/learn-ai-series-82-optical-character-recognition)\n- [Learn AI Series (#83) - Video Understanding](https://hive.blog/hive-196387/@scipio/learn-ai-series-83-video-understanding)\n- [Learn AI Series (#84) - Generative Images - Diffusion Models (Part 1)](https://hive.blog/hive-196387/@scipio/learn-ai-series-84-generative-images-diffusion-models-part-1)\n- [Learn AI Series (#85) - Generative Images - Diffusion Models (Part 2)](https://hive.blog/hive-196387/@scipio/learn-ai-series-85-generative-images-diffusion-models-part-2)\n- [Learn AI Series (#86) - Image-to-Image and Editing](https://hive.blog/hive-196387/@scipio/learn-ai-series-86-image-to-image-and-editing)\n- [Learn AI Series (#87) - 3D Vision](https://hive.blog/hive-196387/@scipio/learn-ai-series-87-3d-vision)\n- [Learn AI Series (#88) - Face Analysis](https://hive.blog/hive-196387/@scipio/learn-ai-series-88-face-analysis)\n- [Learn AI Series (#89) - Medical and Scientific Imaging](https://hive.blog/hive-196387/@scipio/learn-ai-series-89-medical-and-scientific-imaging) (this post)\n\n# Learn AI Series (#89) - Medical and Scientific Imaging\n\n### Solutions to Episode #88 Exercises\n\n**Exercise 1:** Face embedding similarity analyzer.\n\n```python\nimport numpy as np\n\n\nclass EmbeddingSimilarityAnalyzer:\n    \"\"\"Analyze face embedding similarity\n    distributions for identity verification.\"\"\"\n\n    def __init__(self, num_ids=5, photos_per=10,\n                 embed_dim=512, noise_sigma=0.15,\n                 seed=42):\n        rng = np.random.RandomState(seed)\n        self.num_ids = num_ids\n        self.photos_per = photos_per\n        self.labels = []\n        self.embeddings = []\n\n        for i in range(num_ids):\n            centroid = rng.randn(embed_dim)\n            centroid /= np.linalg.norm(centroid)\n            for _ in range(photos_per):\n                variant = centroid + rng.randn(\n                    embed_dim) * noise_sigma\n                variant /= np.linalg.norm(variant)\n                self.embeddings.append(variant)\n                self.labels.append(i)\n\n        self.embeddings = np.array(self.embeddings)\n        self.labels = np.array(self.labels)\n\n    def cosine_matrix(self):\n        norms = np.linalg.norm(\n            self.embeddings, axis=1, keepdims=True)\n        normed = self.embeddings / norms\n        return normed @ normed.T\n\n    def analyze(self):\n        sim = self.cosine_matrix()\n        n = len(self.labels)\n        same_sims = []\n        diff_sims = []\n        for i in range(n):\n            for j in range(i + 1, n):\n                if self.labels[i] == self.labels[j]:\n                    same_sims.append(sim[i, j])\n                else:\n                    diff_sims.append(sim[i, j])\n        same_sims = np.array(same_sims)\n        diff_sims = np.array(diff_sims)\n\n        print(f\"Same-identity mean: \"\n              f\"{same_sims.mean():.4f}\")\n        print(f\"Diff-identity mean: \"\n              f\"{diff_sims.mean():.4f}\")\n        print(f\"Hardest positive:   \"\n              f\"{same_sims.min():.4f}\")\n        print(f\"Hardest negative:   \"\n              f\"{diff_sims.max():.4f}\")\n        gap = same_sims.min() - diff_sims.max()\n        print(f\"Gap: {gap:.4f}\")\n\n        print(f\"\\n{'Thresh':>7} {'TPR':>6} \"\n              f\"{'FPR':>6} {'Acc':>6}\")\n        print(\"-\" * 28)\n        best_acc, best_t = 0, 0\n        for t in [0.3, 0.4, 0.5, 0.6, 0.7, 0.8]:\n            tp = (same_sims >= t).mean()\n            fp = (diff_sims >= t).mean()\n            acc = ((same_sims >= t).sum()\n                   + (diff_sims < t).sum()) / (\n                       len(same_sims) + len(diff_sims))\n            if acc > best_acc:\n                best_acc = acc\n                best_t = t\n            print(f\"{t:>7.1f} {tp:>6.3f} \"\n                  f\"{fp:>6.3f} {acc:>6.3f}\")\n        print(f\"\\nBest: thresh={best_t}, \"\n              f\"acc={best_acc:.3f}\")\n        return same_sims, diff_sims\n\n\nanalyzer = EmbeddingSimilarityAnalyzer()\nanalyzer.analyze()\n```\n\nWith noise_sigma=0.15, the same-identity pairs cluster around cosine similarity 0.85-0.95 (high, because the noise is modest relative to the 512-dimensional centroid), while different-identity pairs settle near 0.0 (random unit vectors in 512D are nearly orthogonal). The gap between the hardest positive and hardest negative is comfortably positive, meaning a single threshold cleanly separates all identities. If you increase noise_sigma to 0.5 or higher, the same-identity distribution spreads out and starts overlapping with the different-identity distribution -- the gap shrinks toward zero and no threshold achieves perfect accuracy. This directly demonstrates why face recognition systems demand well-lit, frontal, aligned crops: reducing noise in the embedding space widens the gap.\n\n**Exercise 2:** Landmark-based face alignment tool.\n\n```python\nimport numpy as np\n\n\nclass FaceAligner:\n    \"\"\"Align faces using 5-point landmarks\n    via affine transformation.\"\"\"\n\n    def __init__(self, target_eye_dist=70,\n                 target_center=(112, 112)):\n        self.target_dist = target_eye_dist\n        self.target_cx = target_center[0]\n        self.target_cy = target_center[1]\n\n    def compute_alignment(self, left_eye,\n                          right_eye):\n        dx = right_eye[0] - left_eye[0]\n        dy = right_eye[1] - left_eye[1]\n        angle = np.degrees(np.arctan2(dy, dx))\n        dist = np.sqrt(dx ** 2 + dy ** 2)\n        scale = self.target_dist / max(\n            dist, 1e-8)\n\n        cos_a = np.cos(np.radians(-angle))\n        sin_a = np.sin(np.radians(-angle))\n        R = np.array([[cos_a, -sin_a],\n                      [sin_a, cos_a]]) * scale\n\n        mid_x = (left_eye[0] + right_eye[0]) / 2\n        mid_y = (left_eye[1] + right_eye[1]) / 2\n        tx = self.target_cx - (\n            R[0, 0] * mid_x + R[0, 1] * mid_y)\n        ty = self.target_cy - (\n            R[1, 0] * mid_x + R[1, 1] * mid_y)\n\n        M = np.zeros((2, 3))\n        M[:2, :2] = R\n        M[0, 2] = tx\n        M[1, 2] = ty\n        return M, angle, dist\n\n    def apply_affine(self, points, M):\n        pts = np.array(points)\n        ones = np.ones((len(pts), 1))\n        aug = np.hstack([pts, ones])\n        return (M @ aug.T).T\n\n    def run(self):\n        rng = np.random.RandomState(42)\n        print(f\"{'Case':>5} {'InAngle':>8} \"\n              f\"{'OutAngle':>9} {'InDist':>7} \"\n              f\"{'OutDist':>8} {'EyeCenter':>14}\")\n        print(\"-\" * 56)\n\n        for i in range(5):\n            angle = rng.uniform(-30, 30)\n            dist = rng.uniform(40, 120)\n            cx = rng.uniform(80, 160)\n            cy = rng.uniform(80, 160)\n            rad = np.radians(angle)\n            le = (cx - dist / 2 * np.cos(rad),\n                  cy - dist / 2 * np.sin(rad))\n            re = (cx + dist / 2 * np.cos(rad),\n                  cy + dist / 2 * np.sin(rad))\n\n            M, in_angle, in_dist = (\n                self.compute_alignment(le, re))\n            landmarks = [le, re,\n                         (cx, cy + 20),\n                         (cx - 15, cy + 40),\n                         (cx + 15, cy + 40)]\n            aligned = self.apply_affine(\n                landmarks, M)\n\n            out_dx = aligned[1, 0] - aligned[0, 0]\n            out_dy = aligned[1, 1] - aligned[0, 1]\n            out_angle = np.degrees(\n                np.arctan2(out_dy, out_dx))\n            out_dist = np.sqrt(\n                out_dx ** 2 + out_dy ** 2)\n            eye_cx = (aligned[0, 0]\n                      + aligned[1, 0]) / 2\n            eye_cy = (aligned[0, 1]\n                      + aligned[1, 1]) / 2\n\n            print(f\"{i + 1:>5} {in_angle:>8.1f} \"\n                  f\"{out_angle:>9.4f} \"\n                  f\"{in_dist:>7.1f} \"\n                  f\"{out_dist:>8.1f} \"\n                  f\"({eye_cx:.0f}, {eye_cy:.0f})\")\n\n\naligner = FaceAligner()\naligner.run()\n```\n\nEvery test case produces an aligned eye angle of essentially 0 degrees (within floating point precision), an inter-eye distance of exactly 70 pixels, and eye centers at (112, 112) -- regardless of the input rotation, scale, or position. This consistency is exactly why alignment matters for face recognition: the downstream embedding network sees faces in a canonical pose every time, so it can focus all of its capacity on identity-discriminative features rather than wasting parameters on learning to handle rotations and scale variations.\n\n**Exercise 3:** Expression confusion matrix analyzer.\n\n```python\nimport numpy as np\n\n\nclass ExpressionAnalyzer:\n    \"\"\"Analyze expression classification\n    confusion patterns.\"\"\"\n\n    EXPRESSIONS = [\n        \"angry\", \"disgusted\", \"fearful\",\n        \"happy\", \"neutral\", \"sad\", \"surprised\"\n    ]\n\n    def __init__(self, samples_per=100, seed=42):\n        self.n = samples_per\n        self.seed = seed\n\n    def generate_confusion(self, correct_probs=\n            None, boost=None):\n        rng = np.random.RandomState(self.seed)\n        if correct_probs is None:\n            correct_probs = {\n                \"angry\": 0.65, \"disgusted\": 0.60,\n                \"fearful\": 0.60, \"happy\": 0.80,\n                \"neutral\": 0.70, \"sad\": 0.68,\n                \"surprised\": 0.72}\n        if boost:\n            for cls, amt in boost.items():\n                correct_probs[cls] = min(\n                    correct_probs[cls] + amt, 0.95)\n\n        confusion_pairs = {\n            (\"angry\", \"disgusted\"): 0.12,\n            (\"disgusted\", \"angry\"): 0.12,\n            (\"fearful\", \"surprised\"): 0.15,\n            (\"surprised\", \"fearful\"): 0.10,\n            (\"sad\", \"neutral\"): 0.08,\n            (\"neutral\", \"sad\"): 0.06}\n\n        nc = len(self.EXPRESSIONS)\n        cm = np.zeros((nc, nc), dtype=int)\n\n        for i, expr in enumerate(self.EXPRESSIONS):\n            p_correct = correct_probs[expr]\n            remaining = 1.0 - p_correct\n            probs = np.zeros(nc)\n            probs[i] = p_correct\n            used = 0.0\n            for (a, b), p in confusion_pairs.items():\n                if a == expr:\n                    j = self.EXPRESSIONS.index(b)\n                    probs[j] = min(p, remaining)\n                    used += probs[j]\n            leftover = remaining - used\n            for j in range(nc):\n                if j != i and probs[j] == 0:\n                    probs[j] = leftover / max(\n                        nc - 1 - sum(\n                            1 for k in range(nc)\n                            if k != i\n                            and probs[k] > 0), 1)\n            probs /= probs.sum()\n            cm[i] = rng.multinomial(self.n, probs)\n        return cm\n\n    def metrics(self, cm):\n        nc = cm.shape[0]\n        results = {}\n        for i, expr in enumerate(self.EXPRESSIONS):\n            tp = cm[i, i]\n            fp = cm[:, i].sum() - tp\n            fn = cm[i, :].sum() - tp\n            prec = tp / max(tp + fp, 1)\n            rec = tp / max(tp + fn, 1)\n            f1 = (2 * prec * rec\n                  / max(prec + rec, 1e-8))\n            results[expr] = {\n                \"precision\": prec,\n                \"recall\": rec, \"f1\": f1}\n        return results\n\n    def run(self):\n        cm = self.generate_confusion()\n\n        # Print confusion matrix\n        header = \"     \" + \" \".join(\n            f\"{e[:4]:>5}\" for e in\n            self.EXPRESSIONS)\n        print(header)\n        for i, expr in enumerate(\n                self.EXPRESSIONS):\n            row = f\"{expr[:4]:>5} \" + \" \".join(\n                f\"{cm[i, j]:>5}\"\n                for j in range(len(\n                    self.EXPRESSIONS)))\n            print(row)\n\n        m = self.metrics(cm)\n        print(f\"\\n{'Class':<12} {'Prec':>6} \"\n              f\"{'Rec':>6} {'F1':>6}\")\n        print(\"-\" * 32)\n        for expr in self.EXPRESSIONS:\n            d = m[expr]\n            print(f\"{expr:<12} \"\n                  f\"{d['precision']:>6.3f} \"\n                  f\"{d['recall']:>6.3f} \"\n                  f\"{d['f1']:>6.3f}\")\n\n        # Most confused pairs\n        nc = len(self.EXPRESSIONS)\n        pairs = []\n        for i in range(nc):\n            for j in range(nc):\n                if i != j:\n                    pairs.append(\n                        (cm[i, j], self.EXPRESSIONS[i],\n                         self.EXPRESSIONS[j]))\n        pairs.sort(reverse=True)\n        print(f\"\\nTop confused pairs:\")\n        for cnt, a, b in pairs[:4]:\n            print(f\"  {a} -> {b}: {cnt}\")\n\n        ranked = sorted(m.items(),\n                        key=lambda x: x[1][\"f1\"])\n        print(f\"\\nDifficulty ranking (worst F1 \"\n              f\"first):\")\n        for expr, d in ranked:\n            print(f\"  {expr}: F1={d['f1']:.3f}\")\n\n        # Boost worst two\n        worst = [ranked[0][0], ranked[1][0]]\n        print(f\"\\nBoosting {worst} by +0.05:\")\n        cm2 = self.generate_confusion(\n            boost={w: 0.05 for w in worst})\n        m2 = self.metrics(cm2)\n        for w in worst:\n            print(f\"  {w}: F1 {m[w]['f1']:.3f}\"\n                  f\" -> {m2[w]['f1']:.3f}\")\n\n\nanalyzer = ExpressionAnalyzer()\nanalyzer.run()\n```\n\nThe angry/disgusted and fearful/surprised pairs dominate the off-diagonal entries -- exactly as configured, and consistent with real expression recognition research. These pairs are genuinely hard because the underlying facial muscle movements overlap: anger and disgust both involve brow lowering and nose wrinkling, while fear and surprise both involve wide eyes and raised eyebrows. Happy achieves the highest F1 because smiling is the most visually distinctive expression (unique cheek raise + lip corner pull), while disgusted and fearful rank lowest due to their high mutual confusion rates. Adding 0.05 to the correct-class probability for the two worst performers improves their F1 scores modestly, demonstrating the diminishing returns of more data when the fundamental visual similarity between confusable classes remains.\n\n### On to today's episode\n\nHere we go! We've spent the past thirteen episodes building a thorough understanding of computer vision: from basic image processing (#77) through object detection (#78-79), segmentation (#80), pose estimation (#81), OCR (#82), video (#83), diffusion models (#84-85), image editing (#86), 3D reconstruction (#87), and face analysis (#88). That entire arc dealt with **natural images** -- photographs of everyday scenes, objects, people, and places.\n\nBut some of the highest-impact applications of computer vision don't involve natural photos at all. They involve **medical images**: chest X-rays, retinal scans, pathology slides, CT volumes. And beyond medicine, there's a whole universe of **scientific imaging**: satellite photos, microscopy, spectrograms, astronomical observations. These domains share a common property -- the images look nothing like ImageNet, the datasets are small and expensive to label, and getting the answer wrong can have serious consequences.\n\nThis episode covers how to adapt everything we've learned to domains where the stakes are literally life-and-death, and where the standard \"download a big dataset, train a deep model, deploy\" pipeline falls apart completely ;-)\n\n## Why medical imaging is fundamentally different\n\nIf you've been following along since episode #13 (evaluation) and #14 (data preparation), you already know that data quality matters more than model architecture. Medical imaging takes every data challenge from those episodes and turns the dial to 11.\n\n**Small datasets**: a hospital might have 500 labeled chest X-rays showing a rare condition. Compare that to ImageNet's 14 million images. You can't just train a deeper model and hope it generalizes -- you'll overfit before the first epoch finishes.\n\n**Annotation cost**: labeling a single medical image often requires a board-certified specialist spending 10-30 minutes. Pixel-level segmentation masks (delineating tumor boundaries on a pathology slide) can take _hours_ per image. Some images need consensus from multiple experts because even specialists disagree on borderline cases. You can't crowdsource this on Mechanical Turk like you can with \"is there a cat in this picture?\"\n\n**Class imbalance**: in screening applications, the positive rate might be 1-5%. A model that always predicts \"healthy\" achieves 95%+ accuracy while being completely useless. Remember the precision-recall tradeoff from episode #13? This is where it becomes a matter of life and death.\n\n**High stakes**: a false negative (missed cancer) can kill. A false positive (unnecessary biopsy) causes harm, stress, and expense. The error _profile_ matters far more than the aggregate accuracy number. Telling a doctor \"my model is 97% accurate\" is meaningless without specifying sensitivity and specificity separately.\n\n**Domain shift**: a model trained on X-rays from Hospital A's GE scanner may fail on Hospital B's Siemens scanner. Different imaging protocols, sensor characteristics, patient demographics, and even the brand of contrast dye can shift the data distribution enough to break a model that worked perfectly in development. This is the same distribution shift problem from episode #14, but with much higher consequences.\n\n```python\nimport numpy as np\n\n\nclass MedicalDatasetProfiler:\n    \"\"\"Profile a medical imaging dataset for\n    common challenges: imbalance, size, and\n    annotation cost estimation.\"\"\"\n\n    def __init__(self, n_total, n_positive,\n                 annotation_minutes=15):\n        self.n_total = n_total\n        self.n_pos = n_positive\n        self.n_neg = n_total - n_positive\n        self.ann_min = annotation_minutes\n\n    def profile(self):\n        ratio = self.n_pos / self.n_total\n        imbalance = self.n_neg / max(\n            self.n_pos, 1)\n\n        # Estimate annotation budget\n        total_hours = (\n            self.n_total * self.ann_min / 60)\n        cost_per_hour = 150  # specialist rate\n        total_cost = total_hours * cost_per_hour\n\n        # Effective training size accounting\n        # for imbalance\n        effective = min(\n            self.n_pos, self.n_neg) * 2\n\n        print(f\"Total samples:     {self.n_total:,}\")\n        print(f\"Positive:          {self.n_pos:,} \"\n              f\"({ratio * 100:.1f}%)\")\n        print(f\"Negative:          {self.n_neg:,} \"\n              f\"({(1 - ratio) * 100:.1f}%)\")\n        print(f\"Imbalance ratio:   \"\n              f\"{imbalance:.1f}:1\")\n        print(f\"Effective samples: \"\n              f\"{effective:,}\")\n        print(f\"Annotation time:   \"\n              f\"{total_hours:,.0f} hours\")\n        print(f\"Annotation cost:   \"\n              f\"${total_cost:,.0f}\")\n\n        if ratio < 0.05:\n            print(\"WARNING: Severe imbalance -- \"\n                  \"focal loss recommended\")\n        if self.n_total < 1000:\n            print(\"WARNING: Small dataset -- \"\n                  \"transfer learning essential\")\n        if effective < 200:\n            print(\"WARNING: Very few effective \"\n                  \"samples -- consider few-shot\")\n\n\n# Typical medical datasets\nprint(\"=== Rare disease screening ===\")\nMedicalDatasetProfiler(500, 15).profile()\nprint()\nprint(\"=== Chest X-ray (pneumonia) ===\")\nMedicalDatasetProfiler(5000, 250).profile()\nprint()\nprint(\"=== Retinal scan (diabetic) ===\")\nMedicalDatasetProfiler(10000, 1500).profile()\n```\n\nThe numbers are sobering. A rare disease screening dataset with 500 images and 15 positives gives you an effective training set of just 30 samples after accounting for imbalance. And annotating those 500 images cost someone over $18,000 in specialist time. This is the reality of medical ML -- you don't get to complain about \"only\" having 50,000 training images like you might in a Kaggle competition.\n\n## Transfer learning: standing on ImageNet's shoulders\n\nDespite the visual differences between ImageNet photos (dogs, cars, landscapes) and chest X-rays (greyscale, abstract anatomical structures), transfer learning works surprisingly well for medical images. The early layers of an ImageNet-pretrained CNN detect edges, textures, gradients, and simple shapes -- these are universal visual features that appear in every type of image. Only the later layers specialize for the target domain:\n\n```python\nimport torch\nimport torch.nn as nn\nimport torchvision.models as models\n\n\nclass MedicalClassifier(nn.Module):\n    \"\"\"Medical image classifier built on top\n    of an ImageNet-pretrained backbone.\"\"\"\n\n    def __init__(self, num_classes=2,\n                 pretrained=True):\n        super().__init__()\n        self.backbone = models.resnet50(\n            weights='DEFAULT' if pretrained\n            else None)\n        num_features = self.backbone.fc.in_features\n        self.backbone.fc = nn.Sequential(\n            nn.Dropout(0.5),\n            nn.Linear(num_features, num_classes))\n\n    def forward(self, x):\n        return self.backbone(x)\n\n\ndef setup_gradual_unfreezing(model,\n                              lr_backbone=1e-5,\n                              lr_head=1e-3):\n    \"\"\"Different learning rates for pretrained\n    backbone vs new classification head.\n    Slow updates preserve learned features,\n    fast updates adapt the new head.\"\"\"\n    backbone_params = []\n    head_params = []\n    for name, param in model.named_parameters():\n        if 'fc' in name:\n            head_params.append(param)\n        else:\n            backbone_params.append(param)\n\n    return torch.optim.Adam([\n        {'params': backbone_params,\n         'lr': lr_backbone},\n        {'params': head_params,\n         'lr': lr_head}])\n\n\nmodel = MedicalClassifier(num_classes=3)\noptimizer = setup_gradual_unfreezing(model)\nprint(f\"Backbone params: \"\n      f\"{sum(p.numel() for p in model.backbone.parameters()):,}\")\nprint(f\"Head params: \"\n      f\"{sum(p.numel() for p in model.backbone.fc.parameters()):,}\")\n```\n\nThe strategy is: freeze the backbone initially, train only the classification head for a few epochs until it converges, then gradually unfreeze backbone layers from top to bottom with a much smaller learning rate. This prevents the pretrained features from being destroyed by agressive updates on a small medical dataset. The differential learning rate (100x difference between head and backbone) is critical -- the head needs to learn fast because it starts from random weights, while the backbone needs to adapt slowly because it already has useful features.\n\n**Medical foundation models** like MedCLIP and BiomedCLIP are now available -- pretrained on large collections of medical images and clinical reports. They serve as better starting points than ImageNet for medical tasks, similar to how domain-specific language models outperform general ones (as we discussed in episode #69). Having said that, ImageNet pretraining remains a surprisingly strong baseline even for medical domains, and many published results showing \"medical foundation model beats ImageNet transfer\" disappear when you control carefully for training procedure and hyperparameters.\n\n## Data augmentation: what works and what breaks things\n\nStandard augmentation needs careful adaptation for medical images. You can't just copy-paste augmentation pipelines from a dog-vs-cat classifier and expect sensible results:\n\n```python\nimport torchvision.transforms as T\nimport numpy as np\nfrom PIL import Image\nfrom scipy.ndimage import gaussian_filter\nfrom scipy.ndimage import map_coordinates\n\n\nclass ElasticDeformation:\n    \"\"\"Elastic deformation: simulates tissue\n    variation in medical images. The single\n    most effective augmentation for soft-tissue\n    segmentation tasks.\"\"\"\n\n    def __init__(self, alpha=50, sigma=5):\n        self.alpha = alpha\n        self.sigma = sigma\n\n    def __call__(self, image):\n        img = np.array(image)\n        shape = img.shape[:2]\n        dx = gaussian_filter(\n            np.random.randn(*shape),\n            self.sigma) * self.alpha\n        dy = gaussian_filter(\n            np.random.randn(*shape),\n            self.sigma) * self.alpha\n        y, x = np.meshgrid(\n            np.arange(shape[0]),\n            np.arange(shape[1]),\n            indexing='ij')\n        indices = [\n            np.clip(y + dy, 0, shape[0] - 1),\n            np.clip(x + dx, 0, shape[1] - 1)]\n        if img.ndim == 3:\n            result = np.stack([\n                map_coordinates(img[:, :, c],\n                                indices, order=1)\n                for c in range(img.shape[2])],\n                axis=-1)\n        else:\n            result = map_coordinates(\n                img, indices, order=1)\n        return Image.fromarray(\n            result.astype(np.uint8))\n\n\n# Safe augmentations for medical images\nmedical_train_transforms = T.Compose([\n    T.RandomRotation(degrees=15),\n    T.RandomAffine(\n        degrees=0,\n        translate=(0.05, 0.05),\n        scale=(0.95, 1.05)),\n    T.RandomResizedCrop(\n        224, scale=(0.85, 1.0)),\n    T.RandomAdjustSharpness(\n        sharpness_factor=2, p=0.3),\n    T.GaussianBlur(\n        kernel_size=3, sigma=(0.1, 1.0)),\n    T.ColorJitter(\n        brightness=0.2, contrast=0.2),\n    T.ToTensor(),\n    T.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]),\n])\n\n# Things you MUST NOT do to medical images:\nprint(\"SAFE augmentations:\")\nprint(\"  Small rotations (< 15 degrees)\")\nprint(\"  Small translations\")\nprint(\"  Elastic deformation\")\nprint(\"  Brightness/contrast jitter\")\nprint(\"  Gaussian blur/noise\")\nprint()\nprint(\"DANGEROUS augmentations:\")\nprint(\"  Horizontal flip (heart is on the LEFT)\")\nprint(\"  Vertical flip (anatomy has a top)\")\nprint(\"  Heavy color jitter (diagnostic info)\")\nprint(\"  Random erasing (may mask pathology)\")\n```\n\nThe horizontal flip warning is not theoretical. The human heart is on the left side of the chest. A horizontally flipped chest X-ray represents **situs inversus** -- a rare anatomical condition where all organs are mirrored. If you augment with horizontal flips, your model sees \"normal\" anatomy in a mirrored configuration and learns that the heart can be on either side. This is a subtle but real failure mode that has tripped up published research papers.\n\nElastic deformation, on the other hand, is a goldmine for medical images. It simulates the natural biological variation in soft tissues -- muscles stretch and compress differently across patients, organs shift slightly with breathing, and pathological changes deform surrounding tissue. It's the single most effective domain-specific augmentation for segmentation tasks like tumor boundary delineation.\n\n## Handling class imbalance\n\nWhen only 2% of your training images show the condition you're trying to detect, standard training produces a model that never predicts positive. Three complementary strategies:\n\n```python\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import WeightedRandomSampler\nfrom torch.utils.data import DataLoader\n\n\n# Strategy 1: Weighted cross-entropy loss\nclass_counts = [9800, 200]  # healthy, diseased\nweights = torch.tensor(\n    [1.0 / c for c in class_counts])\nweights = weights / weights.sum()\ncriterion = nn.CrossEntropyLoss(weight=weights)\nprint(f\"Class weights: {weights.tolist()}\")\n\n\n# Strategy 2: Focal loss (Lin et al., 2017)\nclass FocalLoss(nn.Module):\n    \"\"\"Down-weight easy examples, focus on\n    hard ones. Elegant solution to class\n    imbalance without manual weight tuning.\"\"\"\n\n    def __init__(self, alpha=0.25, gamma=2.0):\n        super().__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n\n    def forward(self, logits, targets):\n        ce = nn.functional.cross_entropy(\n            logits, targets, reduction='none')\n        p = torch.exp(-ce)\n        focal_weight = (\n            self.alpha * (1 - p) ** self.gamma)\n        return (focal_weight * ce).mean()\n\n\n# Strategy 3: Oversampling with\n# WeightedRandomSampler\ndef make_balanced_loader(dataset, labels,\n                         batch_size=32):\n    \"\"\"Each batch gets roughly equal\n    representation of both classes.\"\"\"\n    class_counts_per = np.bincount(labels)\n    sample_weights = [\n        1.0 / class_counts_per[l]\n        for l in labels]\n    sampler = WeightedRandomSampler(\n        sample_weights,\n        num_samples=len(dataset))\n    return DataLoader(\n        dataset, batch_size=batch_size,\n        sampler=sampler)\n\n\n# Compare focal loss behavior\nfocal = FocalLoss(alpha=0.25, gamma=2.0)\nlogits = torch.tensor([[2.0, -2.0],\n                        [0.5, 0.5],\n                        [-1.0, 1.0]])\ntargets = torch.tensor([0, 0, 1])\nloss = focal(logits, targets)\nprint(f\"Focal loss: {loss.item():.4f}\")\n```\n\nFocal loss is particularly elegant. When the model is already confident about an easy normal case (high `p`), the `(1-p)^gamma` term makes the gradient nearly zero -- the model doesn't waste time getting more confident about cases it already handles. When it's uncertain about a difficult positive case (low `p`), the gradient is large, so the model focuses its learning capacity where it matters. No manual tuning of class weights required -- the `gamma` parameter controls how aggressively easy examples are down-weighted, and gamma=2.0 works well across a wide range of imbalance ratios.\n\n## Evaluation: the metrics that actually matter\n\nStandard accuracy is misleading for imbalanced medical data. The metrics clinicians care about:\n\n```python\nfrom sklearn.metrics import roc_auc_score\nfrom sklearn.metrics import confusion_matrix\nimport numpy as np\n\n\ndef medical_evaluation(y_true, y_prob,\n                       threshold=0.5):\n    \"\"\"Comprehensive evaluation for medical\n    binary classification. Prints the metrics\n    that actually matter in clinical settings.\"\"\"\n    y_pred = (y_prob >= threshold).astype(int)\n    cm = confusion_matrix(y_true, y_pred)\n    tn, fp, fn, tp = cm.ravel()\n\n    sensitivity = tp / (tp + fn + 1e-10)\n    specificity = tn / (tn + fp + 1e-10)\n    ppv = tp / (tp + fp + 1e-10)\n    npv = tn / (tn + fn + 1e-10)\n    auc = roc_auc_score(y_true, y_prob)\n\n    print(f\"AUC: {auc:.4f}\")\n    print(f\"Sensitivity: {sensitivity:.4f} \"\n          f\"(missed {fn} of {tp + fn} cases)\")\n    print(f\"Specificity: {specificity:.4f} \"\n          f\"(false alarms: {fp})\")\n    print(f\"PPV: {ppv:.4f}, NPV: {npv:.4f}\")\n    print(f\"Confusion matrix:\")\n    print(f\"  TN={tn}, FP={fp}\")\n    print(f\"  FN={fn}, TP={tp}\")\n\n    # Find threshold for 95% sensitivity\n    thresholds = np.linspace(0, 1, 1000)\n    for t in thresholds:\n        preds = (y_prob >= t).astype(int)\n        sens = preds[y_true == 1].sum() / max(\n            (y_true == 1).sum(), 1)\n        if sens >= 0.95:\n            spec = (\n                1 - preds[y_true == 0]).sum() / max(\n                (y_true == 0).sum(), 1)\n            print(f\"\\nAt 95% sensitivity: \"\n                  f\"thresh={t:.3f}, \"\n                  f\"specificity={spec:.4f}\")\n            break\n\n\n# Simulated screening results\nrng = np.random.RandomState(42)\ny_true = np.array([0] * 950 + [1] * 50)\ny_prob = np.where(\n    y_true == 1,\n    rng.beta(8, 3, len(y_true)),\n    rng.beta(2, 8, len(y_true)))\nmedical_evaluation(y_true, y_prob)\n```\n\nThe threshold choice is a **clinical decision**, not a technical one. For screening (catching every possible case -- think mammography), you want high sensitivity even at the cost of more false positives. For confirmatory diagnosis (being sure about a positive result before scheduling surgery), you want high specificity. The model doesn't change -- only the operating point on the ROC curve changes. This is why AUC is the standard metric for comparing models: it measures performance across _all_ possible thresholds, independent of the clinical use case.\n\n## Explainability: showing the model's reasoning\n\nIn healthcare, a black-box prediction is often unacceptable. Clinicians need to understand _why_ the model flagged an image. **Grad-CAM** (Gradient-weighted Class Activation Mapping) produces heatmaps showing which regions of the image contributed most to the prediction:\n\n```python\nimport torch\nimport torch.nn.functional as F\n\n\nclass GradCAM:\n    \"\"\"Grad-CAM: visual explanations for CNN\n    predictions. Highlights which image regions\n    drove the classification decision.\"\"\"\n\n    def __init__(self, model, target_layer):\n        self.model = model\n        self.gradients = None\n        self.activations = None\n        target_layer.register_forward_hook(\n            self._save_activation)\n        target_layer.register_full_backward_hook(\n            self._save_gradient)\n\n    def _save_activation(self, module,\n                         input, output):\n        self.activations = output.detach()\n\n    def _save_gradient(self, module,\n                       grad_in, grad_out):\n        self.gradients = grad_out[0].detach()\n\n    def generate(self, input_image,\n                 target_class):\n        self.model.eval()\n        output = self.model(input_image)\n        self.model.zero_grad()\n        output[0, target_class].backward()\n\n        # Weight each channel by its\n        # average gradient\n        weights = self.gradients.mean(\n            dim=[2, 3], keepdim=True)\n        cam = (weights * self.activations).sum(\n            dim=1, keepdim=True)\n        cam = F.relu(cam)\n        cam = F.interpolate(\n            cam, size=input_image.shape[2:],\n            mode='bilinear',\n            align_corners=False)\n        cam = cam / (cam.max() + 1e-8)\n        return cam.squeeze()\n\n\n# Demo with a ResNet50\nbackbone = models.resnet50(weights='DEFAULT')\nbackbone.eval()\ntarget = backbone.layer4[-1]  # last conv layer\ngradcam = GradCAM(backbone, target)\n\nfake_input = torch.randn(1, 3, 224, 224)\nheatmap = gradcam.generate(fake_input,\n                            target_class=0)\nprint(f\"Heatmap shape: {heatmap.shape}\")\nprint(f\"Range: [{heatmap.min():.3f}, \"\n      f\"{heatmap.max():.3f}]\")\n```\n\nThe resulting heatmap highlights the region the model focused on when making its prediction. If the model correctly predicts pneumonia and the heatmap highlights the lower right lung where the consolidation is visible, that builds clinical trust. If the heatmap highlights the patient's name label in the corner of the X-ray, that reveals a **data leakage** problem -- the model learned to associate certain patients (who appear in both training and test sets) with certain diagnoses, rather than learning actual radiological features.\n\nThis data leakage scenario is not hypothetical. A 2018 study found that a pneumonia detection model achieved suspiciously high accuracy because it learned to recognize which hospital the X-ray came from (via scanner-specific markings and formatting), and different hospitals had different patient populations with different disease prevalence. The model was predicting hospital identity, not pathology. Grad-CAM caught it ;-)\n\n## Beyond medicine: scientific imaging at large\n\nMedical imaging gets the most attention (and funding), but the same techniques apply across scientific disciplines. The common thread: specialized imaging modalities, expensive expert annotations, small datasets, and high-stakes decisions.\n\n**Satellite and remote sensing**: images have more channels than RGB (infrared, radar, multispectral bands with 10-200+ channels), much larger spatial extents (a single Sentinel-2 tile is 10,980 x 10,980 pixels at 10m resolution), and the objects of interest are often tiny relative to the image (a single building, a few hectares of deforestation, individual vehicles). The preprocessing pipeline alone -- atmospheric correction, cloud masking, radiometric calibration, georeferencing -- is a field of its own.\n\n**Microscopy**: fluorescence microscopy produces multichannel images where each channel corresponds to a different stain or fluorescent marker. The images are often very noisy (photon shot noise at low light levels), may be 3D (confocal z-stacks), and the relevant structures (cell nuclei, mitochondria, protein aggregates) range from a few pixels to thousands of pixels in size. Cell segmentation -- finding individual cell boundaries in densely packed tissue -- is one of the enduring computer vision challenges that gets harder as cells overlap and deform.\n\n**Astronomy**: images from telescopes deal with extreme dynamic range (a star might be millions of times brighter than the background), noise that follows Poisson statistics rather than Gaussian, and objects so faint they're barely distinguishable from noise artifacts. Galaxy morphology classification (spiral, elliptical, irregular) is one of the classic ML astronomy tasks, and the Galaxy Zoo citizen science project produced one of the first large-scale crowd-annotated astronomical datasets.\n\n```python\nimport numpy as np\n\n\nclass MultiChannelNormalizer:\n    \"\"\"Normalize scientific images with\n    arbitrary channel counts (satellite,\n    microscopy, spectral). Each channel\n    gets independent normalization.\"\"\"\n\n    def __init__(self):\n        self.means = None\n        self.stds = None\n\n    def fit(self, images):\n        \"\"\"Compute per-channel statistics\n        from a batch of images.\n        images: (N, C, H, W)\"\"\"\n        self.means = images.mean(\n            axis=(0, 2, 3))\n        self.stds = images.std(\n            axis=(0, 2, 3))\n        self.stds[self.stds < 1e-8] = 1.0\n        return self\n\n    def transform(self, image):\n        \"\"\"Normalize a single image.\n        image: (C, H, W)\"\"\"\n        normalized = np.zeros_like(\n            image, dtype=np.float32)\n        for c in range(image.shape[0]):\n            normalized[c] = (\n                (image[c] - self.means[c])\n                / self.stds[c])\n        return normalized\n\n    def percentile_clip(self, image,\n                        low=1, high=99):\n        \"\"\"Clip each channel to percentile\n        range -- handles extreme values in\n        satellite and astronomical data.\"\"\"\n        clipped = np.zeros_like(\n            image, dtype=np.float32)\n        for c in range(image.shape[0]):\n            lo = np.percentile(image[c], low)\n            hi = np.percentile(image[c], high)\n            clipped[c] = np.clip(\n                image[c], lo, hi)\n            rng = hi - lo\n            if rng > 1e-8:\n                clipped[c] = (\n                    (clipped[c] - lo) / rng)\n        return clipped\n\n\n# Simulate a 13-band satellite image\nrng = np.random.RandomState(42)\n# Different channels have very different\n# value ranges (realistic for Sentinel-2)\nsat_image = np.zeros((13, 256, 256))\nfor c in range(13):\n    base = rng.uniform(100, 5000)\n    scale = rng.uniform(50, 2000)\n    sat_image[c] = rng.normal(\n        base, scale, (256, 256))\n\nnorm = MultiChannelNormalizer()\nbatch = sat_image[np.newaxis]\nnorm.fit(batch)\nresult = norm.transform(sat_image)\nclipped = norm.percentile_clip(sat_image)\n\nprint(f\"Channels: {sat_image.shape[0]}\")\nprint(f\"\\n{'Ch':>3} {'RawMean':>10} \"\n      f\"{'RawStd':>10} {'NormMean':>10} \"\n      f\"{'ClipRange':>12}\")\nprint(\"-\" * 48)\nfor c in range(sat_image.shape[0]):\n    print(f\"{c:>3} \"\n          f\"{sat_image[c].mean():>10.1f} \"\n          f\"{sat_image[c].std():>10.1f} \"\n          f\"{result[c].mean():>10.4f} \"\n          f\"[{clipped[c].min():.3f}, \"\n          f\"{clipped[c].max():.3f}]\")\n```\n\nThe key insight for all scientific imaging: you can't use ImageNet normalization statistics (mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) when your images have 13 bands with value ranges spanning three orders of magnitude. Each domain needs its own preprocessing pipeline, its own normalization strategy, and often its own evaluation metrics. The model architectures we've been building throughout this series (CNNs, transformers, U-Nets) transfer well across domains -- it's the data pipeline that needs the most domain-specific engineering.\n\n## The regulatory landscape\n\nMedical AI devices require regulatory approval before clinical deployment. This isn't optional -- it's the law, and it fundamentally shapes how medical AI is developed:\n\n**FDA (US)**: Software as a Medical Device (SaMD) classification. AI diagnostics typically fall under Class II (510(k) pathway, showing \"substantial equivalence\" to an existing cleared device) or Class III (premarket approval, for higher-risk applications). The FDA has cleared hundreds of AI medical devices as of 2025, mostly in radiology and cardiology.\n\n**CE marking (EU)**: Under the Medical Device Regulation (MDR), AI systems must demonstrate safety and performance through clinical evaluation. The EU's AI Act adds additional requirements for \"high-risk\" AI systems, which includes most medical diagnostic applications.\n\nBoth require: documented training data provenance (where did every image come from, with what consent?), validated performance on representative populations (does it work for all demographics?), post-market surveillance plans (how will you catch failures after deployment?), and clear intended use statements (this model is for X, not for Y). You can't just train a model and put it in a hospital.\n\nThe regulatory process also means that medical AI development follows a fundamentally different timesline than typical ML projects. From initial model development to regulatory clearance takes 1-3 years and costs hundreds of thousands to millions of dollars. This is why most medical AI research stays in academic papers rather than reaching patients -- the gap between \"works on our test set\" and \"approved for clinical use\" is enormous.\n\n## Samengevat\n\n- Medical imaging has unique constraints that fundamentally alter the ML pipeline: small datasets (hundreds, not millions), expensive expert annotations ($150+/hour specialists), severe class imbalance (1-5% positive rates), and high-stakes errors where false negatives can be fatal;\n- **transfer learning from ImageNet works** for medical images despite massive visual differences; the early CNN layers learn universal edge/texture features that transfer across domains; medical foundation models (MedCLIP, BiomedCLIP) are emerging as better starting points but ImageNet remains a strong baseline;\n- augmentation must be **domain-aware**: elastic deformation is highly effective for simulating biological tissue variation, but horizontal flipping breaks anatomical assumptions (the heart is on the left); every augmentation choice must be validated against domain knowledge;\n- **focal loss** automatically focuses training on hard examples without manual class weight tuning; the `(1-p)^gamma` modulation makes the gradient near-zero for easy cases and large for difficult ones;\n- evaluation requires **sensitivity, specificity, and AUC** rather than accuracy; threshold selection is a clinical decision that trades off missed cases against false alarms, and different clinical use cases (screening vs confirmation) demand different operating points;\n- **Grad-CAM** provides visual explanations essential for clinical trust and debugging data leakage -- verifying that models focus on pathology rather than scanner artifacts or patient labels is non-negotiable;\n- **scientific imaging** beyond medicine (satellite, microscopy, astronomy) shares the same challenges of specialized modalities, small datasets, and domain-specific preprocessing but with additional complications like multi-channel inputs, extreme dynamic ranges, and non-Gaussian noise models;\n- regulatory approval (FDA, CE marking) is mandatory before clinical deployment and shapes the entire development process -- from data provenance documentation to post-market surveillance plans.\n\nWe've now covered how computer vision applies to specialized domains where getting it right really matters. The vision arc of this series has been extensive -- from raw pixels all the way to generating images, reconstructing 3D scenes, analyzing faces, and now domain-specific scientific applications. There's one more foundational concept in vision that we haven't explored yet: how machines can learn powerful visual representations _without_ any human labels at all, using only the structure of the data itself.\n\n### Exercises\n\n**Exercise 1:** Build a **medical dataset augmentation validator**. Create a class `AugmentationValidator` that: (a) generates a synthetic 64x64 \"chest X-ray\" as a numpy array with a circle in the left half (simulating the heart) and a smaller circle in the right half (simulating a nodule), both with different intensities against a noisy background, (b) implements `check_anatomical_consistency(original, augmented)` that verifies the heart-like circle is still in the left half after augmentation -- compute the column-wise intensity sum for each half and check that the brighter half hasn't switched sides, (c) applies 5 different augmentations to the synthetic image: (1) small rotation 10 degrees, (2) horizontal flip, (3) elastic deformation with alpha=30 sigma=4, (4) brightness shift +20%, (5) Gaussian noise sigma=0.05, (d) for each augmentation, prints whether anatomical consistency is preserved, (e) computes a \"signal preservation score\" for each augmentation: the correlation coefficient between the original and augmented images (values near 1.0 mean the content is well preserved, values near 0.0 mean significant distortion). Verify that horizontal flip fails the anatomical consistency check while all other augmentations pass, and that elastic deformation preserves anatomy while having lower correlation than simple brightness changes.\n\n**Exercise 2:** Build a **class imbalance strategy comparator**. Create a class `ImbalanceComparator` that: (a) generates a synthetic binary classification dataset with 1000 samples where only 30 are positive, with features drawn from overlapping Gaussians (positive: mean=1.0, std=1.5; negative: mean=0.0, std=1.0) in 10 dimensions, (b) trains 3 logistic regression classifiers (using sklearn): one with default settings, one with `class_weight='balanced'`, and one using SMOTE oversampling on the training set (use imblearn.over_sampling.SMOTE or implement a simple random oversampler if imblearn is not available), (c) evaluates each on a held-out test set (20% split) using sensitivity, specificity, AUC, and F1, (d) prints a comparison table showing all 4 metrics for all 3 strategies, (e) for each strategy, finds the threshold that achieves >= 90% sensitivity and reports the corresponding specificity. Verify that the default (unweighted) classifier has high specificity but poor sensitivity, while balanced weighting and oversampling improve sensitivity at the cost of some specificity.\n\n**Exercise 3:** Build a **multi-channel scientific image normalizer and analyzer**. Create a class `ScientificImageAnalyzer` that: (a) generates a synthetic 8-channel \"satellite image\" of size 128x128 where: channels 0-2 are visible RGB (values 0-255), channels 3-4 are near-infrared (values 500-3000), channel 5 is thermal (values 250-320, representing Kelvin), channels 6-7 are radar backscatter (values in dB, range -25 to 5), (b) implements `per_channel_normalize(image)` that z-score normalizes each channel independently, (c) implements `percentile_clip(image, low_pct=2, high_pct=98)` that clips each channel to its 2nd and 98th percentiles then rescales to [0, 1], (d) implements `compute_ndvi(image)` that computes the Normalized Difference Vegetation Index from the NIR and Red channels: NDVI = (NIR - Red) / (NIR + Red + 1e-8), (e) prints per-channel statistics (mean, std, min, max) before and after normalization, (f) computes and prints NDVI statistics (mean, std, fraction of pixels with NDVI > 0.3 indicating vegetation), (g) compares the correlation between channel pairs before and after normalization to verify that normalization preserves inter-channel relationships. Verify that per-channel normalization brings all channels to mean~0, std~1 regardless of their original value ranges, and that NDVI values are invariant to the normalization method used.\n\n### Thanks for reading!\n\n@scipio\n",
      "json_metadata": "{\"tags\": [\"stem\", \"stemsocial\", \"steemstem\", \"python\", \"programming\"], \"app\": \"hiveblog/0.1\", \"format\": \"markdown\", \"image\": [\"https://images.hive.blog/DQmP6whkhWUYdkmpCQ1kNbtQDbkZS4gB7HojM4NRt86wph1/variant-c-12-green.png\"]}",
      "parent_author": "",
      "parent_permlink": "hive-196387",
      "permlink": "learn-ai-series-89-medical-and-scientific-imaging",
      "title": "Learn AI Series (#89) - Medical and Scientific Imaging"
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:05:09",
  "trx_id": "bd048f75341b420b2bee01a07362384eac2af931",
  "trx_in_block": 0,
  "virtual_op": false
}
2026/06/06 07:05:06
idreblog
json["reblog", {"account": "scipio", "author": "scipio", "permlink": "learn-ai-series-89-medical-and-scientific-imaging"}]
required auths[]
required posting auths["scipio"]
Transaction InfoBlock #107028725/Trx 94739b9dd78749bb9ce9f1358f79de07024af767
View Raw JSON Data
{
  "block": 107028725,
  "op": [
    "custom_json",
    {
      "id": "reblog",
      "json": "[\"reblog\", {\"account\": \"scipio\", \"author\": \"scipio\", \"permlink\": \"learn-ai-series-89-medical-and-scientific-imaging\"}]",
      "required_auths": [],
      "required_posting_auths": [
        "scipio"
      ]
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T07:05:06",
  "trx_id": "94739b9dd78749bb9ce9f1358f79de07024af767",
  "trx_in_block": 16,
  "virtual_op": false
}
scipioreceived 0.006 HP curation reward for @nanixxx / welcome-to-my-new-addiction
2026/06/06 06:36:27
authornanixxx
curatorscipio
payout must be claimedtrue
permlinkwelcome-to-my-new-addiction
reward9.744267 VESTS
Transaction InfoBlock #107028155/Virtual Operation 4294967295:148
View Raw JSON Data
{
  "block": 107028155,
  "op": [
    "curation_reward",
    {
      "author": "nanixxx",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "welcome-to-my-new-addiction",
      "reward": "9.744267 VESTS"
    }
  ],
  "op_in_trx": 148,
  "timestamp": "2026-06-06T06:36:27",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
scipioreceived 0.004 HP curation reward for @kellyane / a-glimpse-of-the-new-art-museum-in-ormoc-city
2026/06/06 06:32:57
authorkellyane
curatorscipio
payout must be claimedtrue
permlinka-glimpse-of-the-new-art-museum-in-ormoc-city
reward6.496179 VESTS
Transaction InfoBlock #107028086/Virtual Operation 4294967295:260
View Raw JSON Data
{
  "block": 107028086,
  "op": [
    "curation_reward",
    {
      "author": "kellyane",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "a-glimpse-of-the-new-art-museum-in-ormoc-city",
      "reward": "6.496179 VESTS"
    }
  ],
  "op_in_trx": 260,
  "timestamp": "2026-06-06T06:32:57",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
scipioeffective vote applied for @meno / 20260606t061606348z
2026/06/06 06:26:27
authormeno
pending payout1.078 HBD
permlink20260606t061606348z
rshares200952377608
total vote weight14395696961464
voterscipio
weight200952377608
Transaction InfoBlock #107027956/Trx 4ee92a3ff64a54c6a74ccecb8bca3ad2b4184d55
View Raw JSON Data
{
  "block": 107027956,
  "op": [
    "effective_comment_vote",
    {
      "author": "meno",
      "pending_payout": "1.078 HBD",
      "permlink": "20260606t061606348z",
      "rshares": 200952377608,
      "total_vote_weight": 14395696961464,
      "voter": "scipio",
      "weight": 200952377608
    }
  ],
  "op_in_trx": 1,
  "timestamp": "2026-06-06T06:26:27",
  "trx_id": "4ee92a3ff64a54c6a74ccecb8bca3ad2b4184d55",
  "trx_in_block": 35,
  "virtual_op": true
}
scipioupvoted (100.00%) @meno / 20260606t061606348z
2026/06/06 06:26:27
authormeno
permlink20260606t061606348z
voterscipio
weight10000 (100.00%)
Transaction InfoBlock #107027956/Trx 4ee92a3ff64a54c6a74ccecb8bca3ad2b4184d55
View Raw JSON Data
{
  "block": 107027956,
  "op": [
    "vote",
    {
      "author": "meno",
      "permlink": "20260606t061606348z",
      "voter": "scipio",
      "weight": 10000
    }
  ],
  "op_in_trx": 0,
  "timestamp": "2026-06-06T06:26:27",
  "trx_id": "4ee92a3ff64a54c6a74ccecb8bca3ad2b4184d55",
  "trx_in_block": 35,
  "virtual_op": false
}
2026/06/06 06:24:45
authorscipio
permlinklearn-ai-series-82-optical-character-recognition
Transaction InfoBlock #107027923/Virtual Operation 4294967295:19
View Raw JSON Data
{
  "block": 107027923,
  "op": [
    "comment_payout_update",
    {
      "author": "scipio",
      "permlink": "learn-ai-series-82-optical-character-recognition"
    }
  ],
  "op_in_trx": 19,
  "timestamp": "2026-06-06T06:24:45",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
scipioreceived 1.155 HBD reward share for learn-ai-series-82-optical-character-recognition
2026/06/06 06:24:45
authorscipio
author rewards10140
beneficiary payout value0.000 HBD
curator payout value0.577 HBD
payout1.155 HBD
permlinklearn-ai-series-82-optical-character-recognition
total payout value0.577 HBD
Transaction InfoBlock #107027923/Virtual Operation 4294967295:18
View Raw JSON Data
{
  "block": 107027923,
  "op": [
    "comment_reward",
    {
      "author": "scipio",
      "author_rewards": 10140,
      "beneficiary_payout_value": "0.000 HBD",
      "curator_payout_value": "0.577 HBD",
      "payout": "1.155 HBD",
      "permlink": "learn-ai-series-82-optical-character-recognition",
      "total_payout_value": "0.577 HBD"
    }
  ],
  "op_in_trx": 18,
  "timestamp": "2026-06-06T06:24:45",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
scipioreceived 5.070 HIVE, 5.072 HP author reward for @scipio / learn-ai-series-82-optical-character-recognition
2026/06/06 06:24:45
authorscipio
curators vesting payout16453.206520 VESTS
hbd payout0.000 HBD
hive payout5.070 HIVE
payout must be claimedtrue
permlinklearn-ai-series-82-optical-character-recognition
vesting payout8233.911465 VESTS
Transaction InfoBlock #107027923/Virtual Operation 4294967295:17
View Raw JSON Data
{
  "block": 107027923,
  "op": [
    "author_reward",
    {
      "author": "scipio",
      "curators_vesting_payout": "16453.206520 VESTS",
      "hbd_payout": "0.000 HBD",
      "hive_payout": "5.070 HIVE",
      "payout_must_be_claimed": true,
      "permlink": "learn-ai-series-82-optical-character-recognition",
      "vesting_payout": "8233.911465 VESTS"
    }
  ],
  "op_in_trx": 17,
  "timestamp": "2026-06-06T06:24:45",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}
scipioreceived 0.129 HP curation reward for @scipio / learn-ai-series-82-optical-character-recognition
2026/06/06 06:24:45
authorscipio
curatorscipio
payout must be claimedtrue
permlinklearn-ai-series-82-optical-character-recognition
reward209.501889 VESTS
Transaction InfoBlock #107027923/Virtual Operation 4294967295:8
View Raw JSON Data
{
  "block": 107027923,
  "op": [
    "curation_reward",
    {
      "author": "scipio",
      "curator": "scipio",
      "payout_must_be_claimed": true,
      "permlink": "learn-ai-series-82-optical-character-recognition",
      "reward": "209.501889 VESTS"
    }
  ],
  "op_in_trx": 8,
  "timestamp": "2026-06-06T06:24:45",
  "trx_id": "0000000000000000000000000000000000000000",
  "trx_in_block": 4294967295,
  "virtual_op": true
}

Account Metadata

POSTING JSON METADATA
profile{"name":"scipio","about":"Does it matter who's right, or who's left?","cover_image":"https://static.pexels.com/photos/290386/pexels-photo-290386.jpeg","profile_image":"https://cdn.steemitimages.com/DQmSHCeBbjgGEsnigAh9qJTdcFuKvm8vaxRYACdkvPi8WBg/scipio.jpg"}
JSON METADATA
profile{"name":"scipio","about":"Does it matter who's right, or who's left?","cover_image":"https://static.pexels.com/photos/290386/pexels-photo-290386.jpeg","profile_image":"https://cdn.steemitimages.com/DQmSHCeBbjgGEsnigAh9qJTdcFuKvm8vaxRYACdkvPi8WBg/scipio.jpg"}
{
  "posting_json_metadata": {
    "profile": {
      "name": "scipio",
      "about": "Does it matter who's right, or who's left?",
      "cover_image": "https://static.pexels.com/photos/290386/pexels-photo-290386.jpeg",
      "profile_image": "https://cdn.steemitimages.com/DQmSHCeBbjgGEsnigAh9qJTdcFuKvm8vaxRYACdkvPi8WBg/scipio.jpg"
    }
  },
  "json_metadata": {
    "profile": {
      "name": "scipio",
      "about": "Does it matter who's right, or who's left?",
      "cover_image": "https://static.pexels.com/photos/290386/pexels-photo-290386.jpeg",
      "profile_image": "https://cdn.steemitimages.com/DQmSHCeBbjgGEsnigAh9qJTdcFuKvm8vaxRYACdkvPi8WBg/scipio.jpg"
    }
  }
}

Auth Keys

Owner
Single Signature
Public Keys
STM54sX7GE6LRFVn9b3rRyZkkgrAJeXAHsaupuxTCfrjqAuHgf6Ej1/1
Active
Single Signature
Public Keys
STM8RLCJUVQyKPiGzkZQpXLmoy2w5bMTv8shWEAfBPPaP4FpUX7To1/1
Posting
Single Signature
Public Keys
STM8eHBTT5K4piEfvYAk99g3CxxuAGFQdNqA98BPtRp1SucFxgBqD1/1
App Permissions
Memo
STM8ZMNP7R48LekiaQu9Kz4UNVYcnVwFsY1fRhgY7MGDx2hxxukvS
{
  "owner": {
    "account_auths": [],
    "key_auths": [
      [
        "STM54sX7GE6LRFVn9b3rRyZkkgrAJeXAHsaupuxTCfrjqAuHgf6Ej",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "active": {
    "account_auths": [],
    "key_auths": [
      [
        "STM8RLCJUVQyKPiGzkZQpXLmoy2w5bMTv8shWEAfBPPaP4FpUX7To",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "posting": {
    "account_auths": [
      [
        "utopian.app",
        1
      ],
      [
        "utopianpay",
        1
      ]
    ],
    "key_auths": [
      [
        "STM8eHBTT5K4piEfvYAk99g3CxxuAGFQdNqA98BPtRp1SucFxgBqD",
        1
      ]
    ],
    "weight_threshold": 1
  },
  "memo": "STM8ZMNP7R48LekiaQu9Kz4UNVYcnVwFsY1fRhgY7MGDx2hxxukvS"
}

Witness Votes

21 / 30
[
  "arcange",
  "ausbitbank",
  "blocktrades",
  "blue-witness",
  "dalz",
  "deathwing",
  "drakos",
  "good-karma",
  "gtg",
  "guiltyparties",
  "mahdiyari",
  "neoxian",
  "nuthman",
  "pharesim",
  "rishi556",
  "roelandp",
  "someguy123",
  "steempeak",
  "stoodkev",
  "threespeak",
  "ura-soul"
]