Search code examples
pythonimagepython-imaging-libraryrdkit

RDKit image MolToImage scales Bond widths and Element Labels inconsistently for different image sizes?


I've noticed that when I create an image from a molecule in RDKit, the size argument leads to inconsistent scaling of the bond width and element labels. The bigger the size, the thinner the lines and the smaller the element labels.

I've run a test by generating an image for the same molecule using MolToImage at progressively bigger sizes. I rescaled those images to size=(600,600) and then concatenated them into a GIF. This is the result.

Resulting GIF

Here's my code

from glob import glob
from rdkit import Chem
from rdkit.Chem import Draw
from PIL import Image,ImageDraw,ImageFont


def make_frames_from_smi(smi):

    for i in range(10):
        s = (i+3)*100
        mol = Chem.MolFromSmiles(smi)
        img = Draw.MolToImage(mol,size=(s,s))
        img = img.resize((600,600))
        draw = ImageDraw.Draw(img)

        text = '%d: Initial Size: (%d,%d)'%(i+1,s,s)

        font_size = 40
        font = ImageFont.truetype("arial.ttf", font_size)  # Use your desired font


        # Calculate text position
        image_width, image_height = img.size
        text_x = (image_width - (bbox[2] - bbox[0])) // 2
        text_y = 20  # Adjust the vertical position as needed

        draw.text((text_x, text_y), text, font=font, fill='black')
        img.save('%03dtest.png'%i)


def make_gif_from_frames(paths):

    frames_paths = glob(paths)
    frames = [Image.open(imgp) for imgp in frames_paths]

    frames[0].save("mols.gif", format="GIF", append_images=frames, save_all=True, duration=500, loop=False)



# make RDKit mol obj.
smi = 'CN(C)CC1CCCCC1(C2=CC(=CC=C2)OC)O'
make_frames_from_smi(smi)
make_gif_from_frames('*.png')

Is this expected behaviour? Is the bond width held constant for a certain absolute value of pixels? How can I generate these images with consistent proportions regardless of width/height of pixels?


Solution

  • OK found , I hope , two solutions ???

    First one, using Draw.rdMolDraw2D.MolDraw2DCairo:

    from glob import glob
    from rdkit import Chem
    from rdkit.Chem import Draw
    from PIL import Image
    
    
    from io import BytesIO
    
    def make_frames_from_smi(smi):
    
        mol = Chem.MolFromSmiles(smi)
    
        for i in range(10):
            s = (i+3)*100
            mol = Chem.MolFromSmiles(smi)
            
            d = Draw.rdMolDraw2D.MolDraw2DCairo(s,s)
            
            dopts = d.drawOptions()
            
            # dopts.maxFontSize=40
            # dopts.minFontSize=40
            
            dopts.maxFontSize=int(0.13*s)
            dopts.minFontSize=int(0.13*s)
            
            
            print('dopts.bondLineWidth : ', dopts.bondLineWidth) #default is 2.0
            
            dopts.bondLineWidth=(0.007*s)
            
            print('dopts.bondLineWidth : ', dopts.bondLineWidth) #default is 2.0
        
        
            d.DrawMolecule(mol, legend= '%d: Initial Size: (%d,%d)'%(i+1,s,s))
            
            d.FinishDrawing()
    
            
            img = d.GetDrawingText()
            
            img_b = BytesIO()
        
            img_b.write(img)
            
            
            pil_img = Image.open(img_b).resize((600,600))
            
            pil_img.save('%03dtest.png'%i)
            
            
    
    
    def make_gif_from_frames(paths):
    
        frames_paths = glob(paths)
        
        frames_paths.sort()
        
        print(frames_paths)
        
            
        frames = [Image.open(imgp) for imgp in frames_paths]
        
        frames[0].save("mols.gif", format="GIF", append_images=frames, save_all=True, duration=500, loop=False)
    
    
    smi = 'CN(C)CC1CCCCC1(C2=CC(=CC=C2)OC)O'
    make_frames_from_smi(smi)
    make_gif_from_frames('*.png')
    

    result:

    enter image description here

    or using SVG, Draw.rdMolDraw2D.MolDraw2DSVG, that I like more as a pic format :

    import rdkit
    
    print('\n-------------------------------')
    print('\n rdkit Version : ', rdkit.__version__)
    print('\n-------------------------------')
    
    from glob import glob
    from rdkit import Chem
    from rdkit.Chem import Draw
    from PIL import Image,ImageDraw,ImageFont
    
    from io import BytesIO
    from cairosvg import svg2png
    
    from moviepy.editor import ImageClip, concatenate_videoclips
    
    def make_frames_from_smi(smi):
    
        mol = Chem.MolFromSmiles(smi)
    
        drawer= Draw.rdMolDraw2D.MolDraw2DSVG(600,600)
        
        dopts = drawer.drawOptions()
        
        for i in dir(dopts) :
            
            print(i)
            
        dopts.minFontSize = -1
        dopts.maxFontSize = -1
            
        # dopts.minFontSize = 80
        # dopts.maxFontSize = 80
        
        print('drawer.FontSize : ', drawer.FontSize())
              
        # dopts.annotationFontScale = 0.5
        
        dopts.addAtomIndices = True
            
        drawer.DrawMolecule(mol)
    
            
        drawer.FinishDrawing()
    
        svg_data = drawer.GetDrawingText()
    
    
            
        with open('dtest.svg' , 'w') as handler:
                
                handler.write(svg_data)
        
        for i in range(10):
            s = (i+3)*100
            
            png = svg2png(bytestring=svg_data)
    
            img = Image.open(BytesIO(png)).convert('RGBA').resize((s,s))
    
            img.save('%03dtest.png'%i)
    
    
    def make_gif_from_frames(paths):
        
        input_png_list = glob(paths)
        input_png_list.sort()
        
        
        clips = [ImageClip(i).set_duration(1)
                      for i in input_png_list]
        concat_clip = concatenate_videoclips(clips, method="compose")
        concat_clip.write_gif("test.gif", fps=2)    
        
    
    smi = 'CN(C)CC1CCCCC1(C2=CC(=CC=C2)OC)O'
    make_frames_from_smi(smi)
    make_gif_from_frames('*.png')
    
    

    result:

    enter image description here