2026年2月15日 星期日

將 取得或設定 DICOM 檔案中的標籤值

將 取得或設定 DICOM 檔案中的標籤值

在我前面的兩篇文章 列出 DICOM 檔案中的所有標籤 與 將 DICOM 轉換成為一個 Image 中,已經介紹了如何使用 fo-dicom 套件來處理 DICOM 影像,並且展示了如何列出 DICOM 檔案中的所有標籤以及將 DICOM 影像轉換成為一個 Image。接下來,在這篇文章中,我們將進一步探討如何取得或設定 DICOM 檔案中的標籤值。

建立 主控台應用程式 專案

  • 開啟 Visual Studio 2026
  • 選擇「建立新專案」
  • 在 [建立新專案] 視窗中,在右方清單內,找到並選擇 [主控台應用程式] 項目
  • 然後點擊右下方「下一步」按鈕
  • 此時將會看到 [設定新的專案] 對話窗
  • 在該對話窗的 [專案名稱] 欄位中,輸入專案名稱,例如 "csDicomGetSetTagValue"
  • 然後點擊右下方「下一步」按鈕
  • 接著會看到 [其他資訊] 對話窗
  • 在這個對話窗內,確認使用底下的選項
    • 架構:.NET 10.0 (或更新版本)
    • 勾選 不要使用最上層陳述式 (這是我的個人習慣)
  • 然後點擊右下方「建立」按鈕
  • 現在,已經完成了這個 主控台應用程式 專案的建立

安裝 fo-dicom 套件

套件 fo-dicom 是一個用於處理 DICOM 影像的套件,它提供了豐富的功能來處理 DICOM 影像,並且支援多種圖像格式的轉換。這裡是該套件提供功能的主要清單:

  • DICOM 影像處理:提供了豐富的功能來處理 DICOM 影像,包括讀取、寫入、修改和轉換等功能。

  • 支援多種圖像格式:該套件支援多種圖像格式的轉換,包括 JPEG、PNG、BMP 等,方便用戶將 DICOM 影像轉換為其他格式進行使用。

  • 在 Visual Studio 的「方案總管」視窗中,右鍵點擊專案名稱

  • 從右鍵選單中,選擇「管理 NuGet 套件」

  • 在 NuGet 套件管理器視窗中,切換到「瀏覽」標籤頁

  • 在搜尋框中,輸入 "fo-dicom" 並按下 Enter 鍵

  • 從搜尋結果中,找到 "fo-dicom" 套件 並點擊它

  • 在這裡的範例中,使用該套件的版本為 5.12.1

  • 在右側的詳細資訊面板中,點擊「安裝」按鈕

撰寫程式碼

  • 打開 Program.cs 檔案,並將其內容替換為以下程式碼:
using FellowOakDicom;

namespace csDicomGetSetTagValue;

internal class Program
{
    static void Main(string[] args)
    {
        string dicomFile = "image-00000.dcm";
        string dicomUpdateFile = "image-00000Update.dcm";
        string rootPath = Directory.GetCurrentDirectory();
        string dicomFilePath = Path.Combine(rootPath, dicomFile);
        string dicomUpdateFilePath = Path.Combine(rootPath,  dicomUpdateFile);
        GetOrSetDicomTag(dicomFilePath, dicomUpdateFilePath);
    }

    private static void GetOrSetDicomTag(string dicomPath, string outputDicomPath)
    {
        try
        {
            var dicomFile = DicomFile.Open(dicomPath);
            DicomTag patientIdTag = QueryTag(dicomFile);
            SetTag(dicomFile, DicomTag.PatientID);
            DicomTag patientIdTagAgain = QueryTag(dicomFile);
            dicomFile.Save(dicomPath);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"轉換失敗: {ex.Message}");
            throw;
        }
    }

    private static void SetTag(DicomFile dicomFile, DicomTag patientIdTag)
    {
        dicomFile.Dataset.AddOrUpdate(patientIdTag, Guid.NewGuid().ToString());
    }

    private static DicomTag QueryTag(DicomFile dicomFile)
    {
        var patientIdTag = DicomTag.PatientID; // (0010,0020)
        if (dicomFile.Dataset.Contains(patientIdTag))
        {
            var patientId = dicomFile.Dataset.GetString(patientIdTag);
            Console.WriteLine($"原始 Patient ID: {patientId}");
        }
        else
        {
            Console.WriteLine("Patient ID 標籤不存在。");
        }
        return patientIdTag;
    }
}

在程式一開始,宣告了五個字串 [dicomFile] 代表了要處理的 DICOM 檔案名稱, [dicomUpdateFile] 代表了修改後的 DICOM 檔案名稱, [rootPath] 代表了目前專案的根目錄路徑, [dicomFilePath] 代表了要處理的 DICOM 檔案的完整路徑, [dicomUpdateFilePath] 代表了修改後的 DICOM 檔案的完整路徑。

然後就會呼叫 [GetOrSetDicomTag] 方法,在這個方法內,首先會使用 [DicomFile.Open] 方法來開啟 DICOM 檔案,在這裡將會得到一個 [DicomFile] 物件,接著會呼叫 [QueryTag] 方法來取得 DICOM 檔案中的 Patient ID 標籤值,在這個 [QueryTag] 方法內,透過 [DicomTag.PatientID] 來指定要查詢的標籤(病人病歷號),儲存在 [patientIdTag] 變數中,然後使用 [Contains] 方法來檢查該標籤是否存在於 DICOM 檔案中,如果存在,就使用 [GetString] 方法來取得該標籤的值,並且將其輸出到主控台上。

繼續回到 [GetOrSetDicomTag] 方法內,接著會呼叫 [SetTag] 方法來設定 DICOM 檔案中的 Patient ID 標籤值,在這個 [SetTag] 方法內,使用 [AddOrUpdate] 方法來新增或更新 Patient ID 標籤的值,這裡將其設定為一個新的 GUID 字串。最後,剛剛更新的 DICOM 檔案會被儲存回原本的路徑。 




將語音檔案進行批次轉錄成為文字

將語音檔案進行批次轉錄成為文字

因為工作的關係,經常會與客戶進行開會,討論軟體或者系統該如何開發或者修正的需求,又或者在內部進行開會,因此,無論是會議錄音、訪談內容、醫療紀錄,或是客服通話內容,只要能夠轉換為可搜尋、可分析的文字資料,如此,就可以將這些錄音檔案轉換出來文字稿,使用 ChatGPT 這個 LLM 大語言模型,整理出來一份會議紀錄,可以把該會議的內容分類歸納出來,做為日後執行與追蹤之用。這時候,Azure Speech to Text 便成為一項極具價值的雲端服務。它隸屬於 Microsoft Azure,透過深度學習模型將語音轉換為高品質文字,支援即時串流與離線批次轉錄。

當我們手上有錄音筆或手機錄製的語音檔案,例如 MP3、WAV 或 M4A 檔案,其實整個轉換流程並不複雜。第一步通常是在 Azure 入口網站中建立 Speech 資源。建立完成後,系統會提供金鑰(Key)與服務端點(Endpoint),這兩個資訊就是後續呼叫 API 的核心憑證。

接著,使用者可以選擇透過 SDK 或 REST API 進行語音轉換。如果是單一檔案測試,開發者可以透過簡單的程式碼將音訊檔上傳至服務端點,系統會回傳轉換後的文字內容。這種方式適合小型專案或即時處理需求,例如在 Web 應用程式中即時顯示逐字稿。

在技術實作層面上,開發流程大致包含三個核心動作。首先是音訊格式的確認與優化,例如建議使用單聲道、16kHz 或 44.1kHz 的標準音訊格式,以提升辨識準確率。接著是透過 SDK 初始化 SpeechConfig,設定語言(例如 zh-TW 或 en-US)與金鑰資訊。最後將音訊串流傳入 SpeechRecognizer 物件,等待服務回傳辨識結果。整個過程可以在幾十行程式碼內完成。

從架構角度來看,實務上常見的做法是建立一條完整的語音處理流程。音訊由前端裝置上傳至雲端儲存空間,再由後端服務呼叫 Speech API 進行轉錄,轉換完成後將結果寫入資料庫,最後透過搜尋或 AI 分析系統進行後續應用。這樣的設計能讓語音資料真正成為企業可再利用的數位資產,而不是一次性的錄音檔案。

整體而言,Azure Speech to Text 的導入並不困難,但其價值並不只在於「轉成文字」本身,而是在於它打開了語音資料數位化、結構化與智慧化應用的大門。當語音可以被搜尋、索引、分析與整合時,它就不再只是聲音,而是企業知識的一部分。

建立 主控台應用程式 專案

  • 開啟 Visual Studio 2026
  • 選擇「建立新專案」
  • 在 [建立新專案] 視窗中,在右方清單內,找到並選擇 [主控台應用程式] 項目
  • 然後點擊右下方「下一步」按鈕
  • 此時將會看到 [設定新的專案] 對話窗
  • 在該對話窗的 [專案名稱] 欄位中,輸入專案名稱,例如 "csSpeechToTextBatch"
  • 然後點擊右下方「下一步」按鈕
  • 接著會看到 [其他資訊] 對話窗
  • 在這個對話窗內,確認使用底下的選項
    • 架構:.NET 10.0 (或更新版本)
    • 勾選 不要使用最上層陳述式 (這是我的個人習慣)
  • 然後點擊右下方「建立」按鈕
  • 現在,已經完成了這個 主控台應用程式 專案的建立

安裝 Newtonsoft.Json 套件

套件 [Newtonsoft.Json] 是一個流行的 JSON 處理庫,提供了方便的序列化和反序列化功能。透過這個套件,我們可以輕鬆地在 C# 程式碼中處理 JSON 資料,例如將物件轉換為 JSON 字串,或將 JSON 字串轉換為物件。這裡將會列出這個套件提供的功能清單:

  • 將物件序列化為 JSON 字串

  • 將 JSON 字串反序列化為物件

  • 支援自訂序列化設定

  • 支援 LINQ to JSON 操作

  • 在 Visual Studio 的「方案總管」視窗中,右鍵點擊專案名稱

  • 從右鍵選單中,選擇「管理 NuGet 套件」

  • 在 NuGet 套件管理器視窗中,切換到「瀏覽」標籤頁

  • 在搜尋框中,輸入 "Newtonsoft.Json" 並按下 Enter 鍵

  • 從搜尋結果中,找到 "Newtonsoft.Json" 套件 並點擊它

  • 在這裡的範例中,使用該套件的版本為 13.0.3

  • 在右側的詳細資訊面板中,點擊「安裝」按鈕

撰寫程式碼

  • 打開 Program.cs 檔案,並將其內容替換為以下程式碼:
using Newtonsoft.Json;
using System.Net.Http.Headers;

namespace csSpeechToTextBatch;

class TranscriptionJson
{
    [JsonProperty("combinedRecognizedPhrases")]
    public CombinedPhrase[] CombinedRecognizedPhrases { get; set; }
}
class CombinedPhrase
{
    [JsonProperty("display")]
    public string Display { get; set; }
}
internal class Program
{
    static async Task Main(string[] args)
    {
        string SubscriptionKey = Environment.GetEnvironmentVariable("AzureSpeechServiceSubscriptionKey");
        string ServiceRegion = Environment.GetEnvironmentVariable("AzureSpeechServiceRegion");
        string AudioFileSasUri = "https://blogstoragekh.blob.core.windows.net/audio-files/250501_0814.mp3?sv=2025-05-05&se=2025-05-01T10%3A53%3A53Z&sr=b&sp=r&sig=HVG%2Bs3hxD5cmv%2FrVOs5HZbekqmIBJujOGJWnsRLTjUQ%3D";

        var client = new HttpClient();
        client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", SubscriptionKey);
        var createUrl = $"https://{ServiceRegion}.api.cognitive.microsoft.com/speechtotext/v3.2/transcriptions";
        var createBody = new
        {
            contentUrls = new[] { AudioFileSasUri },
            locale = "zh-TW",
            displayName = "My Batch Transcription",
            properties = new
            {
                diarizationEnabled = false,
                wordLevelTimestampsEnabled = true,
                punctuationMode = "DictatedAndAutomatic"
            }
        };

        var jsonContent = new StringContent(JsonConvert.SerializeObject(createBody));
        jsonContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        var createResponse = await client.PostAsync(createUrl, jsonContent);
        createResponse.EnsureSuccessStatusCode();

        var createResult = await createResponse.Content.ReadAsStringAsync();
        Console.WriteLine("已建立批次轉錄工作:");
        Console.WriteLine(createResult);

        dynamic createJson = JsonConvert.DeserializeObject(createResult);
        string transcriptionUrl = createJson.self;

        Console.WriteLine("開始輪詢轉錄狀態…");
        TimeSpan elapsedTime;
        DateTime startTime = DateTime.Now;
        while (true)
        {
            elapsedTime = DateTime.Now - startTime;
            Console.WriteLine($"已經花費時間:{elapsedTime.Hours:D2}:{elapsedTime.Minutes:D2}:{elapsedTime.Seconds:D2}");

            var statusResponse = await client.GetAsync(transcriptionUrl);
            statusResponse.EnsureSuccessStatusCode();

            var statusJson = await statusResponse.Content.ReadAsStringAsync();
            dynamic statusObj = JsonConvert.DeserializeObject(statusJson);
            string status = statusObj.status;
            Console.WriteLine($"目前狀態:{status}");

            if (status == "Succeeded")
            {
                string filesUrl = statusObj.links.files;
                var filesResponse = await client.GetAsync(filesUrl);
                filesResponse.EnsureSuccessStatusCode();

                var filesJson = await filesResponse.Content.ReadAsStringAsync();
                dynamic filesObj = JsonConvert.DeserializeObject(filesJson);

                foreach (var file in filesObj.values)
                {
                    if ((string)file.kind == "Transcription")
                    {
                        var fileUrl = (string)file.links.contentUrl;
                        var transcriptionResult = await client.GetStringAsync(fileUrl);
                        var resultObj = JsonConvert.DeserializeObject<TranscriptionJson>(transcriptionResult);

                        string fullText = string.Join(" ",
                            resultObj.CombinedRecognizedPhrases
                                     .Select(p => p.Display?.Trim())
                                     .Where(s => !string.IsNullOrEmpty(s))
                        );
                        Console.WriteLine("---- 完整轉錄文字 ----");
                        Console.WriteLine(fullText);
                    }
                }
                break;
            }
            else if (status == "Failed")
            {
                Console.WriteLine("轉錄失敗");
                break;
            }

            await Task.Delay(TimeSpan.FromSeconds(5));
        }
    }
}

在這個檔案內,宣告了 [TranscriptionJson] 與 [CombinedPhrase] 兩個類別,這些類別用來對應從 Azure Speech to Text API 回傳的 JSON 結構。

在程式進入點,宣告了三個字串 [SubscriptionKey]、[ServiceRegion] 與 [AudioFileSasUri],分別用來存放 Azure Speech 服務的金鑰、區域與要轉錄的音訊檔案的 SAS URI。其中,關於 [SAS URI] 部分,想要取得這個服務端點,可以參考 如何使用與存取 Azure Blob Storage Service 這篇文章內的說明。

接著,程式碼中使用了 [HttpClient] 類別來與 Azure Speech API 進行 HTTP 通訊。首先,建立了一個新的 HttpClient 實例,並在其標頭中加入了 [Ocp-Apim-Subscription-Key] 的訂閱金鑰。宣告了 [createUrl] 這個服務端點字串,作為呼叫 Speech To Text 的 API 端點,其中,[ServiceRegion] 是前面宣告的區域字串,這個值會根據你在 Azure 入口網站中建立 Speech 資源時所選擇的區域而有所不同。接著要產生一個 [createBody] 的物件,這個物件包含了轉錄工作所需的參數,例如使用 [contentUrls] 做為要轉錄的音訊檔案 URL、[locale] 設定語言,這裡指定了 [zh-tw] 也就是要產生出繁體中文的文稿、[displayName] 是這個轉錄工作的顯示名稱,還有一些屬性設定,例如 [diarizationEnabled] 是否啟用說話人分離、[wordLevelTimestampsEnabled] 是否啟用逐字時間戳記、[punctuationMode] 標點符號的處理方式等等。然後,將這個物件序列化為 JSON 字串,並包裝成一個 [StringContent] 物件,最後呼叫 [PostAsync] 方法來發送 HTTP POST 請求以建立轉錄工作。

一旦這次的 API 呼叫成功,便可以透過 [Content.ReadAsStringAsync()] 方法來取得 API 回傳的結果,這個結果會包含一些關於轉錄工作的資訊,例如轉錄工作的 ID、狀態、以及一個 [self] URL,這個 URL 就是用來輪詢轉錄狀態的端點。透過 [Json.NET] 將取得的回應 JSON 內容,反序列化成為 [createJson] 這個型別為 [dynamic] 的物件,然後從中取出 [self] URL,並將它存放在 [transcriptionUrl] 這個字串變數中。這個 [transcriptionUrl] 就是後續用來輪詢轉錄狀態的 API 端點,透過不斷的呼叫這個 API,確認此次將語音文字轉換成為文字稿的過程是否已經完成。

這裡同樣的是使用 [HttpClient] 來發送 HTTP GET 請求,並在每次呼叫後,解析回傳的 JSON 內容,經過反序列化之後,取得這個 JSON 物件 [statusObj],透過該物件從中取出 [status] 欄位的值,來確認目前轉錄工作的狀態。如果狀態是 "Succeeded",就代表轉錄已經完成,可以進一步取得轉錄結果;如果狀態是 "Failed",則代表轉錄過程中發生了錯誤,需要進行錯誤處理。為了避免過於頻繁地呼叫 API,這裡使用了 [Task.Delay] 方法來讓程式在每次輪詢之間暫停幾秒鐘。

一旦 [status] 的值變成 "Succeeded",就表示轉換過程已經完成了,接著就可以透過 [filesUrl] 這個 API 端點來取得轉錄結果的相關資訊。這裡同樣是使用 [HttpClient] 發送 HTTP GET 請求,並解析回傳的 JSON 內容,從中找到轉錄結果的 URL,然後再發送一次 HTTP GET 請求來下載轉錄結果的 JSON 內容。最後,將這個轉錄結果的 JSON 字串反序列化成為 [TranscriptionJson] 這個物件,從中取出所有的 [display] 欄位內容,並將它們串接起來形成完整的轉錄文字,最後印出來。