commons-app / apps-android-commons

The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons
https://commons-app.github.io/
Apache License 2.0
1.02k stars 1.23k forks source link

Take latitude/longitude from picture taken seconds after #954

Closed nicolas-raoul closed 2 years ago

nicolas-raoul commented 6 years ago

When I go photohunting and finally reach that historical remain after walking for kilometers, I usually take several pictures with slightly different lighting/angle. I usually take about 5 pictures of the thing, and when I am back home I judge which one is the best.

Problem: The first few pictures usually do not have GPS information. And quite often the first picture is the best. Actually, when uploading from Android I usually have the gallery app showing each picture fullscreen, and I swipe in chronological order, so the one which I upload to Commons is usually the first I took, often without GPS info.

Solution: When the user tries to upload a picture with GPS info, try to find a picture taken within 20 seconds after which has GPS info. Then show a screen showing the two picture and asking "Did you shoot these two pictures at the same place? Do you want to use the latitude/longitude of the picture on the right? And use it. Do not use GPS orientation as orientation can totally change within a second. The worst error would probably be someone taking serial shoots from a running high-speed train, that's why the confirmation step is required.

Technically that could be done by reading the chosen picture's file creation date and folder, then listing the files in that folder by creation date, and reading the EXIF of the ones after until either you find GPS coordinates or the EXIF has more than 10 seconds. A "phase 2" enhancement could be getting the EXIF's GPSDOP parameter (measurement precision) to use the one with the best precision, as GPS sometimes takes a bit of time to get precise.

misaochan commented 6 years ago

Solution: When the user tries to upload a picture with GPS info, try to find a picture taken within 10 seconds after which has GPS info.

Interesting, I didn't know this was possible! Very novel idea IMO. :)

harisankerPradeep commented 6 years ago

I'd like to work on this :)

nicolas-raoul commented 6 years ago

@harisankerPradeep It is yours :-)

harisankerPradeep commented 6 years ago

@nicolas-raoul all photos taken with the camera are stored in /DCIM/Camera, so knowing the file's creation date and the folder doesn't help. In that case won't it be necessary to go through the EXIF data of all the photos and find the nearest ones? Is there a better way to do this or am I getting this wrong somewhere?

Thanks in advance!

nicolas-raoul commented 6 years ago

@harisankerPradeep The folder might be different, for instance my pictures are stored in /DCIM/OpenCamera. Many gallery apps found in Google Play have a "Recent pictures" tab that shows the latest pictures you have taken, and that works even when I switch between camera apps that use different folders. So, you should probably use the same algorithm as these apps, in order to get all recently taken pictures. Android stock gallery app source code: http://androidxref.com/4.2_r1/xref/packages/apps/Gallery/ LeafPic source code: https://github.com/HoraApps/LeafPic

harisankerPradeep commented 6 years ago

@nicolas-raoul sure man, I'll look into it.

nicolas-raoul commented 6 years ago

@harisankerPradeep Thanks for the code! It seems that you removed your comment though?

harisankerPradeep commented 6 years ago

@nicolas-raoul This what I have done so far: In ShareActiviy.java the function getFileMetadata() creates a GPSExtractor object to extract the GPS coordinates from the selected image. If the GPSExtractor returns null or if it returns the current the GPS location of the user, findOtherImages() is called.

if (imageObj != null) 
{
  decimalCoords = imageObj.getCoords(gpsEnabled);
  if (decimalCoords == null || !imageObj.imageCoordsExists)
  {
   findOtherImages(gpsEnabled);
  }
  useImageCoords();
}

findOtherImages() find the folder in which the selected image is stored. Files which were created within 20 seconds of the creation of the selected image are passed to a GPSExtractor object to find

private void findOtherImages(boolean gpsEnabled) {
        String filePath = getPathOfMediaOrCopy();
        long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
        File folder = new File(filePath.substring(0,filePath.lastIndexOf('/')));//Folder path of image
        File[] files = folder.listFiles();
        for(File file : files){
            if(file.lastModified()-timeOfCreation<=(20*1000) && file.lastModified()-timeOfCreation>=-(20*1000)){
                //Make sure the photos were taken within 20seconds

                GPSExtractor tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
                ParcelFileDescriptor descriptor
                        = null;
                try {
                    descriptor = getContentResolver().openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r");
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    if (descriptor != null) {
                        tempImageObj = new GPSExtractor(descriptor.getFileDescriptor());
                    }
                } else {
                    if (filePath != null) {
                        tempImageObj = new GPSExtractor(file.getAbsolutePath());
                    }
                }

                if(tempImageObj!=null){
                    if(tempImageObj.getCoords(gpsEnabled)!=null && tempImageObj.imageCoordsExists){
//                       Current image has gps coordinates and it's not current gps location
                    }

                }

            }
        }
    }

So far the code works fine. I'm able to find images which were taken within 20 seconds before or after the creation of the original image and find the ones with GPS data.

Is there something wrong with my approach? If not, what should I do next? Also, sorry for the extremely late post. Thanks in advance.

nicolas-raoul commented 6 years ago

Thanks! Could you please try your algorithm with many pictures? For instance, how much time does it take when there are 1000 pictures? Cheers!

harisankerPradeep commented 6 years ago

@nicolas-raoul are there any tools that can help me do that. So far I've been testing on my phone and photos I took, which are less than 150 in number.

harisankerPradeep commented 6 years ago

@nicolas-raoul While testing with about 250 images, the maximum time it took was 115 milliseconds and the minimum was 87 millis. What do you suggest?

nicolas-raoul commented 6 years ago

Sounds good! :-)

harisankerPradeep commented 6 years ago

@nicolas-raoul what's next? What should the UI of the suggestion look like? Do we have some design guidelines that I should follow?

nicolas-raoul commented 6 years ago

I don't have much more insight than what I wrote in the third paragraph of the issue description, so feel free to propose a UI based on this. Thanks!

harisankerPradeep commented 6 years ago

Sure, I'll look into it. Thanks!

harisankerPradeep commented 6 years ago

@nicolas-raoul suggestions? device-2018-02-14-133619

nicolas-raoul commented 6 years ago

Perfect! :-)

harisankerPradeep commented 6 years ago

So here are the changes made to the code: In ShareActivity.java the function findOtherImages() has been modified:

                if(tempImageObj!=null){
                    Timber.d("not null fild EXIF"+tempImageObj.imageCoordsExists +" coords"+tempImageObj.getCoords(gpsEnabled));
                    if(tempImageObj.getCoords(gpsEnabled)!=null && tempImageObj.imageCoordsExists){
//                       Current image has gps coordinates and it's not current gps locaiton
                        Timber.d("This fild has image coords:"+ file.getAbsolutePath());
//                       Create a dialog fragment for the suggestion
                        FragmentManager fragmentManager = getSupportFragmentManager();
                        SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
                        Bundle args = new Bundle();
                        args.putString("originalImagePath",filePath);
                        args.putString("possibleImagePath",file.getAbsolutePath());
                        newFragment.setArguments(args);
                        newFragment.show(fragmentManager, "dialog");
                        break;
                    }

                }

The suggestion dialog box is DialogFragment, SimilarImageDialogFragment.java:

public class SimilarImageDialogFragment extends DialogFragment {
    SimpleDraweeView originalImage;
    SimpleDraweeView possibleImage;
    Button positiveButton;
    Button negativeButton;
    onResponse mOnResponse;//Implemented interface from shareActivity
    public SimilarImageDialogFragment() {
    }
    public interface onResponse{
        public void onPostiveResponse();
        public void onNegativeResponse();
    }
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view =  inflater.inflate(R.layout.fragment_similar_image_dialog, container, false);
        Set<RequestListener> requestListeners = new HashSet<>();
        requestListeners.add(new RequestLoggingListener());

        originalImage =(SimpleDraweeView) view.findViewById(R.id.orginalImage);
        possibleImage =(SimpleDraweeView) view.findViewById(R.id.possibleImage);
        positiveButton = (Button) view.findViewById(R.id.postive_button);
        negativeButton = (Button) view.findViewById(R.id.negative_button);

        originalImage.setHierarchy(GenericDraweeHierarchyBuilder
                .newInstance(getResources())
                .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
                        R.drawable.ic_image_black_24dp,getContext().getTheme()))
                .setFailureImage(VectorDrawableCompat.create(getResources(),
                        R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
                .build());
        possibleImage.setHierarchy(GenericDraweeHierarchyBuilder
                .newInstance(getResources())
                .setPlaceholderImage(VectorDrawableCompat.create(getResources(),
                        R.drawable.ic_image_black_24dp,getContext().getTheme()))
                .setFailureImage(VectorDrawableCompat.create(getResources(),
                        R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
                .build());

        originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath"))));
        possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath"))));

        negativeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mOnResponse.onNegativeResponse();
                dismiss();
            }
        });
        positiveButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mOnResponse.onPostiveResponse();
                dismiss();
            }
        });
        return view;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mOnResponse = (onResponse) getActivity();//Interface Implementation
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Dialog dialog = super.onCreateDialog(savedInstanceState);
        dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        return dialog;
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        mOnResponse.onNegativeResponse();
        super.onDismiss(dialog);
    }
}

The code works fine my on phone. If code looks fine shall I open a PR?

misaochan commented 6 years ago

@harisankerPradeep Yes, please feel free to open a PR - generally it is easier for us to do code review in a PR than with code pasted in an issue comment. :)

harisankerPradeep commented 6 years ago

@misaochan sure! I thought posting my thought train here would be useful for documentation purposes. Thanks

misaochan commented 6 years ago

@harisankerPradeep That's totally fine, and thanks for submitting the PR! We will test it and get back to you shortly.

nicolas-raoul commented 6 years ago

@neslihanturan Here is a ZIP file (60MB) with pictures I took yesterday for possible upload to Commons, so it is a very real-life sample. I had GPS activated all the time, but unfortunately (as it often happens) only some of the pictures actually managed to get GPS information (I marked them with the "-gps" prefix): https://drive.google.com/file/d/1zrJ3382V7cgVK_J-Zh0rJs0J2BkM-a-S/view?usp=sharing

An interesting thing that I noticed is that GPS signal tend to disappear as randomly as it came. In the sample above, you can see that pattern in a very short timeframe: GPS info absent -> GPS info present -> GPS info absent So, I guess we should not only check pictures taken seconds after, but also pictures seconds before (in that order). @harisankerPradeep would it be difficult to modify your code to do this? Thanks a lot!

harisankerPradeep commented 6 years ago

@nicolas-raoul It's already doing that

if(file.lastModified()-timeOfCreation<=(20*1000) && file.lastModified()-timeOfCreation>=-(20*1000)){
      //Check for EXIF data
nicolas-raoul commented 2 years ago

This has been working fine for the last few years, thanks harisankerPradeep and all! :-)