DICOMHERO supplies Java bindings, which allows it to run also on Android devices.

A simple project which uses a pre-compiled AAR containing the DICOMHERO library is available on github.

Clone the sample repository

You can clone the sample repository with:

git@github.com:binarno/DICOMHERO-Simple-Dicom-Viewer.git

Building the project

Open the cloned project with Android Studio and build it.

The following snip in the app gradle.build file takes care of downloading the DICOMHERO AAR from this website and adding it to the project:

task('downloadDicomhero', type: Copy) {
    mkdir project.file("libs")
    def f = project.file('libs/dicomhero6-release-6.0.0.aar')
    if (!f.exists()) {
        new URL('https://dicomhero.com/wp-content/uploads/dicomhero_6_0_0_0/dicomhero6-release-6.0.0.aar').withInputStream { i -> f.withOutputStream { it << i } }
    }
}

build.dependsOn downloadDicomhero

The downloaded AAR is added to the project by declaring all the .aar file as a dependency:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.aar'])
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
}

How does it work?

The MainActivity class contains:

  • an ImageView (which will display the loaded DICOM image)
  • a button, which when clicked displays a file selector

Before calling any method in the DICOMHERO AAR library we first have to load it into memory with System.loadLibrary, so we modify the MainActivity onCreate method like this:

@Override
protected void onCreate(Bundle savedInstanceState) {

    // First thing: load the Imebra library
    System.loadLibrary("dicomhero6");

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // We will use the ImageView widget to display the DICOM image
    mImageView = findViewById(R.id.imageView);
    mTextView = findViewById(R.id.textView);

}

The MainActivity also contains the method called when the button is clicked.
The method displays the Android File Dialog so the user can select the DICOM file to load:

public void loadDicomFileClicked(View view) {

    // Let's use the Android File dialog. It will return an answer in the future, which we
    // get via onActivityResult()
    Intent intent = new Intent()
            .setType("*/*")
            .setAction(Intent.ACTION_GET_CONTENT);

    startActivityForResult(Intent.createChooser(intent, "Select a DICOM file"), 123);

}

After the user select the DICOM file, the method onActivityResult is called, with the selected file and request code (123) properly set.

After checking for the proper request code then we proceed with loading the DICOM file.

Instead of using the DICOMHERO file stream, we use a DICOMHERO Pipe, which is an object that allows our app to pass data from any Android SDK stream. The usage of a DICOMHERO Pipe allows us to load files also from files which are not directly accessible by the DICOMHERO API, like files residing on Google Drive or other Android specific storages:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if(requestCode == 123 && resultCode == RESULT_OK) {
        try {

            CodecFactory.setMaximumImageSize(8000, 8000);

            // Get the selected URI, then open an input stream
            Uri selectedfile = data.getData();
            if(selectedfile == null) {
                return;
            }
            InputStream stream = getContentResolver().openInputStream(selectedfile);

            // The usage of the Pipe allows to use also files on Google Drive or other providers
            PipeStream dicomheroPipe = new PipeStream(32000);

            // Launch a separate thread that read from the InputStream and pushes the data
            // to the Pipe.
            Thread pushThread = new Thread(new PushToDicomheroPipe(dicomheroPipe, stream));
            pushThread.start();

            // The CodecFactory will read from the Pipe which is feed by the thread launched
            // before. We could just pass a file name to it but this would limit what we
            // can read to only local files
            DataSet loadDataSet = CodecFactory.load(new StreamReader(dicomheroPipe.getStreamInput()));


            // Get the first frame from the dataset (after the proper modality transforms
            // have been applied).
            Image dicomImage = loadDataSet.getImageApplyModalityTransform(0);

            // Use a DrawBitmap to build a stream of bytes that can be handled by the
            // Android Bitmap class.
            TransformsChain chain = new TransformsChain();

            if(ColorTransformsFactory.isMonochrome(dicomImage.getColorSpace()))
            {
                VOILUT voilut = new VOILUT(VOILUT.getOptimalVOI(dicomImage, 0, 0, dicomImage.getWidth(), dicomImage.getHeight()));
                chain.addTransform(voilut);
            }
            DrawBitmap drawBitmap = new DrawBitmap(chain);
            Memory memory = drawBitmap.getBitmap(dicomImage, drawBitmapType_t.drawBitmapRGBA, 4);

            // Build the Android Bitmap from the raw bytes returned by DrawBitmap.
            Bitmap renderBitmap = Bitmap.createBitmap((int)dicomImage.getWidth(), (int)dicomImage.getHeight(), Bitmap.Config.ARGB_8888);
            byte[] memoryByte = new byte[(int)memory.size()];
            memory.data(memoryByte);
            ByteBuffer byteBuffer = ByteBuffer.wrap(memoryByte);
            renderBitmap.copyPixelsFromBuffer(byteBuffer);

            // Update the image
            mImageView.setImageBitmap(renderBitmap);
            mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER);

            // Update the text with the patient name
            mTextView.setText(loadDataSet.getPatientName(new TagId(0x10,0x10), 0, new PatientName("Undefined", "", "")).getAlphabeticRepresentation());
        }
        catch(IOException e) {
            AlertDialog.Builder dlgAlert  = new AlertDialog.Builder(this);
            dlgAlert.setMessage(e.getMessage());
            dlgAlert.setTitle("Error");
            dlgAlert.setPositiveButton("OK", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    //dismiss the dialog
                } } );
            dlgAlert.setCancelable(true);
            dlgAlert.create().show();
            String test = "Test";
        }
    }
}

The previous snippet launches a second thread, which is used to feed the Pipe with data loaded from the user’s selected stream:

public class PushToDicomheroPipe implements Runnable {

    private PipeStream mImebraPipe;    // The Pipe into which we push the data
    private InputStream mStream; // The InputStream from which we read the data

    public PushToDicomheroPipe(com.dicomhero.api.PipeStream pipe, InputStream stream) {
        mImebraPipe = pipe;
        mStream = stream;
    }

    @Override
    public void run() {
        StreamWriter pipeWriter = new StreamWriter(mImebraPipe.getStreamOutput());
        try {

            // Buffer used to read from the stream
            byte[] buffer = new byte[128000];
            MutableMemory memory = new MutableMemory();

            // Read until we reach the end
            for (int readBytes = mStream.read(buffer); readBytes >= 0; readBytes = mStream.read(buffer)) {

                // Push the data to the Pipe
                if(readBytes > 0) {
                    memory.assign(buffer);
                    memory.resize(readBytes);
                    pipeWriter.write(memory);
                }
            }
        }
        catch(IOException e) {
        }
        finally {
            pipeWriter.delete();
            mImebraPipe.close(50000);
        }
    }
}

You are now ready to launch the simple Viewer and test it!

Android screenshot

Comments are closed