omz:forum

    • Register
    • Login
    • Search
    • Recent
    • Popular

    Welcome!

    This is the community forum for my apps Pythonista and Editorial.

    For individual support questions, you can also send an email. If you have a very short question or just want to say hello β€” I'm @olemoritz on Twitter.


    Pythonista crashes trying to save merged photo

    Pythonista
    4
    18
    7188
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • jm2466
      jm2466 last edited by

      I wrote a program (as an extension) to take selected photos from the camera roll, combine them vertically and save the combined photo to the camera roll. This would be used for combining multiple screen shots of say a web page where you capture parts of the page while scrolling through it. I know you can buy an app(s) that do this but this seems like a perfect thing for Pythonista (plus an opportunity to learn).

      I have the code merging the photos (at least a few) but Pythonista crashes when more than 3 or so pics are selected and when trying to save to the camera roll. I added the startup code to log crash info but it is always empty. Looking for any help anyone may have.

      import appex
      from PIL import Image
      from PIL.ExifTags import TAGS
      import photos
      
      # standard get exif code - needed for image size
      def get_exif(fn):
      	ret = {}
      	i = Image.open(fn)
      	info = i._getexif()
      	for tag, value in info.items():
      		decoded = TAGS.get(tag, tag)
      		ret[decoded] = value
      	return ret
      
      def main():
      	if not appex.is_running_extension():
      		print('This script is intended to be run from the sharing extension.')
      		return
      	images = appex.get_images()
      	print(f'len(images) = {len(images)})')
      	
      	# get the widths/heights of the images
      	widths, heights = zip(*(i.size for i in images))
      
      	# this is stacking vertically so the width is the max width of all the pics
      	max_width = max(widths)
      	# since stacking vertically the height is the total heights of the pics
      	total_height = sum(heights)
      	print(f'total height = {total_height}')
      	print(f'max width = {max_width}')
      	
      	# create a new blank image in the required size
      	new_im = Image.new('RGBA', (max_width, total_height))
      	print(f'new_im.size = {new_im.size}')
      
      	# now assemble the merged pic
      	
      	# y_offset controls the pixel location of where each image is added
      	# to the new image - to add each end-to-end
      	# starting at the top which is y = 0
      	y_offset = 0
      	# loop through all passed images
      	for im in images:
      		# paste in the image at the left edge (x=0) and current offset for y
      		new_im.paste(im, (0,y_offset))
      		print(f'y_offset = {y_offset}')
      		# increase the offset by the height of current pic
      		y_offset += im.size[1]
      
      	# save the new image to the camera roll
      	photos.save_image(new_im)
      
      if __name__ == '__main__':
      	main()
      
      cvp 1 Reply Last reply Reply Quote 0
      • JonB
        JonB last edited by

        Does it crash both in the extension and in full app?

        Can you check what the crash handler says? This should be a text file in the main app folder.

        cvp 1 Reply Last reply Reply Quote 0
        • cvp
          cvp last edited by cvp

          The line

              new_im = Image.new('RGBA', (max_width, total_height))
          

          crashs if I use 3 photos of 3000x4000, try to create an image of 140MB...(3x3000x4000x4bytes)

          No crash with smaller photos
          and

              new_im.show()
              #photos.save_image(new_im)
          
          1 Reply Last reply Reply Quote 0
          • ccc
            ccc last edited by

            Is there a way to del photo when you are done with them. Pythonista has been known to crash when you load photo after photo without del-ing previous ones.

            1 Reply Last reply Reply Quote 0
            • cvp
              cvp @jm2466 last edited by cvp

              @jm2466 the problem comes (I guess) from 'too much memory' in appex mode.
              Try this script in normal mode and see also how we have to save a new photo now (no more photos.save_image, at least in the help of the beta)
              Obviously, like @ccc adviced, you could delete each image as soon it has been pasted.

              import appex
              import console
              from PIL import Image
              from PIL.ExifTags import TAGS
              import photos
              import os
              import ui
              
              # standard get exif code - needed for image size
              def get_exif(fn):
                  ret = {}
                  i = Image.open(fn)
                  info = i._getexif()
                  for tag, value in info.items():
                      decoded = TAGS.get(tag, tag)
                      ret[decoded] = value
                  return ret
              
              class MyView(ui.View):
                def __init__(self):
                  self.frame = (0,0,500,500)
                  if not appex.is_running_extension():
                      assets = photos.get_assets()
                      assets = photos.pick_asset(assets=assets,multi=True)
                      images = []
                      for asset in assets:
                          images.append(asset.get_image())
                  else:
                      images = appex.get_images(image_type='pil')
                  print(f'len(images) = {len(images)})')
                  #console.hud_alert(str(len(images)))
                  
                  # get the widths/heights of the images
                  widths, heights = zip(*(i.size for i in images))
              
                  # this is stacking vertically so the width is the max width of all the pics
                  max_width = max(widths)
                  # since stacking vertically the height is the total heights of the pics
                  total_height = sum(heights)
                  print(f'total height = {total_height}')
                  #console.hud_alert(str(total_height))
                  print(f'max width = {max_width}')
                  #console.hud_alert(str(max_width))
                  
                  # create a new blank image in the required size
                  console.hud_alert(str(4*max_width*total_height))
                  #return
                  new_im = Image.new('RGBA', (max_width, total_height))
                  print(f'new_im.size = {new_im.size}')
                  console.hud_alert(str(new_im.size))
              
                  # now assemble the merged pic
                  
                  # y_offset controls the pixel location of where each image is added
                  # to the new image - to add each end-to-end
                  # starting at the top which is y = 0
                  y_offset = 0
                  # loop through all passed images
                  for im in images:
                      # paste in the image at the left edge (x=0) and current offset for y
                      new_im.paste(im, (0,y_offset))
                      print(f'y_offset = {y_offset}')
                      console.hud_alert(str(y_offset))
                      # increase the offset by the height of current pic
                      y_offset += im.size[1]
              
                  # save the new image to the camera roll
                  new_im.show()
                  #photos.save_image(new_im)
                  path = 'temp.jpg'
                  new_im.save(path , quality=95)
                  photos.create_image_asset(path)
                  os.remove(path)
              
              def main():
                v = MyView()
                v.present('sheet')
              
              if __name__ == '__main__':
                  main()
              
              1 Reply Last reply Reply Quote 0
              • cvp
                cvp @JonB last edited by cvp

                @JonB unfortunately there is no log generated 😒 , like often for crash due to memory problems

                1 Reply Last reply Reply Quote 0
                • jm2466
                  jm2466 last edited by

                  Sorry for the delay in replying and thanks for the help.

                  So from the above suggestions it appears deleting the photos once merged may be the best solution but I am not quite sure how to do that correctly. This is an extension so will receive the selected photos that I believe are just references/names of the photos and not the actual image - is that correct? My code then loops through the list and actually gets the photos and adds to a list. First it sounds like I should not get all of them before processing but rather merge each one as it is retrieved. Once that is done I need to β€œdelete” it in the program, or really just delete/remove it from program memory what is the best way to do this without deleting the actual photo?

                  Thanks

                  1 Reply Last reply Reply Quote 0
                  • ccc
                    ccc last edited by

                    for asset in assets:
                        image = asset.get_image()
                        images.append(image)
                        del image
                    
                    1 Reply Last reply Reply Quote 0
                    • jm2466
                      jm2466 last edited by

                      Thanks!!

                      1 Reply Last reply Reply Quote 0
                      • jm2466
                        jm2466 last edited by

                        I added the del image after it is pasted into the merged image and that part appears to work. The program is now having a problem saving the merged image to the camera roll. If I instead save it to the Pythonista file system it works and is a workaround until the camera roll save is figured out. Any other thoughts on how to fix the camera roll save is greatly appreciated.

                        1 Reply Last reply Reply Quote 0
                        • ccc
                          ccc last edited by

                          Do you have a GitHub repo? It is more difficult to debug English prose that it is to debug Python code.

                          1 Reply Last reply Reply Quote 0
                          • jm2466
                            jm2466 last edited by

                            Unfortunately I do not.

                            But if you look at the code in my original post I added one line in the for loop to delete the image (below is the loop from that post with the del at the end)

                                for im in images:
                                    # paste in the image at the left edge (x=0) and current offset for y
                                    new_im.paste(im, (0,y_offset))
                                    print(f'y_offset = {y_offset}')
                                    # increase the offset by the height of current pic
                                    y_offset += im.size[1]
                                    del im 
                            

                            As mentioned that seemed to fix the issue pasting more than a few images into the new one.

                            This line crashes Pythonista:

                                photos.save_image(new_im)
                            

                            But I can save it to the Pythonista file system with the following:

                                new_im.save('combined.jpg')
                            
                            1 Reply Last reply Reply Quote 0
                            • ccc
                              ccc last edited by

                              This is long shot but:

                              del new_img
                              photos.save(Image.open('combined.jpg'))
                              
                              1 Reply Last reply Reply Quote 0
                              • jm2466
                                jm2466 last edited by

                                Thanks. I tried that but it crashes trying to save the image to the camera roll. I can open the jpg in Pythonista and manually add it to the camera roll. Here are the lines at the end that I added:

                                	new_im.save('combined.jpg')
                                	del new_im
                                	photos.save_image(Image.open('combined.jpg'))
                                
                                
                                cvp 1 Reply Last reply Reply Quote 0
                                • cvp
                                  cvp @jm2466 last edited by cvp

                                  @jm2466 I'm in holiday, thus perhaps not able to correctly understand all (🍷🍷) but why did you not use

                                  photos.create_image_asset(path)
                                  

                                  instead of

                                  photos.save_image(Image.open(path))
                                  
                                  1 Reply Last reply Reply Quote 0
                                  • jm2466
                                    jm2466 last edited by jm2466

                                    Um... because you never told me to until now??? :) kidding of course. But honestly have no real answer to that question. Possibly because I wanted to save an in-memory image and not have to save the file locally first (at least that was the initial plan but is not where I am at with it now). Not sure if create image asset supports that (I tried passing the variable and got an error that a str is required).

                                    I just tried your suggestion and it works - I mean it saves the locally saved combined.jpg to the camera roll. Thanks!!!

                                    Any thoughts on how to get this to work without saving combined.jpg first???

                                    cvp 2 Replies Last reply Reply Quote 0
                                    • cvp
                                      cvp @jm2466 last edited by

                                      @jm2466 You could always convert your PIL Image into an ui.Image and use this kind of code to save it in camera roll

                                      # only to have an ui.Image
                                      import ui
                                      img = ui.Image.named('test:Bridge')
                                      
                                      # Create a PHAsset from an ui.Image (not from a PIL Image)
                                      from objc_util import *
                                      import threading
                                      
                                      NSBundle.bundleWithPath_('/System/Library/Frameworks/Photos.framework').load()
                                      PHPhotoLibrary = ObjCClass('PHPhotoLibrary')
                                      PHAssetChangeRequest = ObjCClass('PHAssetChangeRequest')
                                      
                                      lib = PHPhotoLibrary.sharedPhotoLibrary()
                                      def change_block():
                                          req = PHAssetChangeRequest.creationRequestForAssetFromImage_(img)
                                      def perform_changes():
                                          lib.performChangesAndWait_error_(change_block, None)
                                      
                                      t = threading.Thread(target=perform_changes)
                                      t.start()
                                      t.join()
                                      
                                      1 Reply Last reply Reply Quote 1
                                      • cvp
                                        cvp @jm2466 last edited by cvp

                                        @jm2466 this

                                            # 1) convert PIL Image to ui.Image
                                            console.hud_alert('convert PIL Image into ui.Image')
                                            with io.BytesIO() as bIO:
                                                new_im.save(bIO, 'PNG')
                                                ui_image = ui.Image.from_data(bIO.getvalue())
                                            del bIO
                                            # 2) Create a PHAsset from an ui.Image (not from a PIL Image)
                                            console.hud_alert('Create a PHAsset from an ui.Image')
                                            lib = PHPhotoLibrary.sharedPhotoLibrary()
                                            def change_block():
                                                req = PHAssetChangeRequest.creationRequestForAssetFromImage_(ui_image)
                                            def perform_changes():
                                                lib.performChangesAndWait_error_(change_block, None)
                                            t = threading.Thread(target=perform_changes)
                                            t.start()
                                            t.join()
                                        

                                        is about (πŸ˜€) 1000 x slower than

                                            path = 'temp.jpg'
                                            new_im.save(path , quality=95)
                                            photos.create_image_asset(path)
                                            os.remove(path)
                                        
                                        1 Reply Last reply Reply Quote 0
                                        • First post
                                          Last post
                                        Powered by NodeBB Forums | Contributors