Android平台下图片EXIF信息的读取与可视化

Sep 9,2018   7991 words   29 min

Tags: Android

如果你在很久以前用个Google Earth,应该会对地图上密密麻麻的图片小图标有印象。 前阵子在外面旅游的时候忽然想到,要是能把旅游的照片也像这样显示在地图上,也是一件挺好玩的事情。 而且去了哪些地方也一目了然。所以便想着实现这个想法了。 整个想法的实现也比较简单,因为利用手机拍摄的照片一般都会随相片保存有地理位置信息(这项功能没关闭且定位正常的情况下)。 所以可以通过读取相片中的地理位置信息并结合地图SDK(如百度地图),便可以将图片显示在地图上,从而可以实现类似刚刚说到的Google Earth上的功能。 所以实现主要分两个步骤,一是读取相片EXIF中的位置信息,二是将位置显示在地图上。 当然会有很多细节问题,这在下面会提到。

1.获取照片EXIF信息

在Android中获取EXIF信息非常简单,因为系统SDK中已经集成了相关API,我们要做的就是调用就好。读取EXIF信息的部分代码如下。

    public double cvtLocTag(String locTag, String locRef) {
        // 直接获取到的位置信息需要解析一下才能使用
        // 33/1,26/1,465/10000
        // 度分秒以逗号隔开,且以除号表示,这样做的好处是以整形存储了浮点型数据
        String loc_deg = locTag.split(",")[0];
        String loc_min = locTag.split(",")[1];
        String loc_sec = locTag.split(",")[2];
        double loc_Deg = Double.parseDouble(loc_deg.split("/")[0]) / 1.0;
        double loc_Min = Double.parseDouble(loc_min.split("/")[0]) / 1.0;
        double loc_Sec = Double.parseDouble(loc_sec.split("/")[0]) / 10000.0;
        double loc = loc_Deg + loc_Min / 60.0 + loc_Sec / 3600.0;
        // 在计算中南纬和西经都为负数,所以增加符号
        if (locRef.contains("S") || locRef.contains("W")) {
            loc = -1 * loc;
        }
        return loc;
    }

    public LatLng getLocInfo(String img_path) {
        try {
            // Step1 新建ExifInterface对象用于获取EXIF信息,传入的参数是图片路径
            ExifInterface exifInterface = new ExifInterface(img_path);
            // Step2 调用获取属性函数获取属性值
            // TAG_GPS_LATITUDE:纬度  TAG_GPS_LONGITUDE:经度
            // TAG_GPS_LATITUDE_REF:南或北半球(S or N) TAG_GPS_LONGITUDE_REF:东或西半球(E or W)
            String latitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
            String longitude = exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
            String latitudeRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
            String longitudeRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
            // 有些照片没有位置信息,因此需要做个判断
            if (latitude == null || longitude == null) {
                return null;
            } else {
                // 对于有位置信息的照片,调用函数解析经纬度并返回
                double lat = cvtLocTag(latitude, latitudeRef);
                double lon = cvtLocTag(longitude, longitudeRef);
                LatLng point = new LatLng(lat, lon);
                return point;
            }
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

EXIF信息有自己的格式规范,所以照着格式读取即可。 这里LatLng是百度地图SDK里的坐标点类,存放的就是经纬度信息。如果只是读取信息不与百度地图交互的话,可以不用这个类。 除此之外,也利用Python实现了EXIF的读取,代码如下。

# coding=utf-8
from PIL import Image
from PIL.ExifTags import TAGS
import os


def findAllFiles(root_dir, filter):
    """
    遍历搜索文件
    
    :param root_dir:搜索目录 
    :param filter: 搜索文件类型
    :return: 路径、文件名、路径+文件名
    """
    print("Finding files ends with \'" + filter + "\' ...")
    separator = os.path.sep
    paths = []
    names = []
    files = []
    # 遍历
    for parent, dirname, filenames in os.walk(root_dir):
        for filename in filenames:
            if filename.endswith(filter):
                paths.append(parent + separator)
                names.append(filename)
    for i in range(paths.__len__()):
        files.append(paths[i] + names[i])
    print (names.__len__().__str__() + " files have been found.")
    paths.sort()
    names.sort()
    files.sort()
    return paths, names, files


def get_exif_data(fname):
    """
    获取EXIF信息
    
    :param fname: 影像文件路径
    :return: 字典类型的EXIF信息
    """
    ret = {}
    try:
        img = Image.open(fname)
        if hasattr(img, '_getexif'):
            exifinfo = img._getexif()
            if exifinfo != None:
                for tag, value in exifinfo.items():
                    decoded = TAGS.get(tag, tag)
                    ret[decoded] = value
    except IOError:
        print 'IOERROR ' + fname
    return ret


def decodeGPS(gps_info):
    """
    从结构数据中解析经纬度信息
    
    :param gps_info: 结构化数据
    :return: 经纬度数据
    """
    latFlag = gps_info[0][1]
    lat = gps_info[1][1]
    lat_deg = lat[0][0]
    lat_min = lat[1][0]
    lat_sec = lat[2][0] / 10000.0
    lonFlag = gps_info[2][1]
    lon = gps_info[3][1]
    lon_deg = lon[0][0]
    lon_min = lon[1][0]
    lon_sec = lon[2][0] / 10000.0
    lat_decimal = lat_deg + lat_min / 60.0 + lat_sec / 3600.0
    lon_decimal = lon_deg + lon_min / 60.0 + lon_sec / 2600.0
    lat_info = (latFlag, lat_decimal)
    lon_info = (lonFlag, lon_decimal)
    return lat_info, lon_info


def decode2Str(loc_info):
    """
    将位置信息转成字符串
    
    :param loc_info: 结构化位置信息
    :return: 字符串位置信息,如32.12345N,117.12345E
    """
    lat = loc_info[0][1].__str__() + loc_info[0][0]
    lon = loc_info[1][1].__str__() + loc_info[1][0]
    str = lat + "," + lon
    return str


def getGPSInfo(filename):
    """
    获取EXIF中的地理位置信息
    
    :param filename: 影像路径
    :return: 字符串类型的地理位置信息
    """

    exif = get_exif_data(filename)
    if exif.has_key('GPSInfo'):
        info = exif.get('GPSInfo')
        gps_info = info.items()
        if len(gps_info) < 4:
            return "No GPSInfo."
        else:
            loc = decodeGPS(gps_info)
            return decode2Str(loc)
    else:
        return "No GPSInfo."


def getGPSInfoBatch(filenames):
    """
    批量获取文件的位置信息
    
    :param filenames: list类型的文件列表
    :return: list类型的位置列表
    """
    GPSInfos = []
    for filename in filenames:
        res = getGPSInfo(filename)
        if res.__contains__('No GPSInfo'):
            continue
        else:
            GPSInfos.append(res)
    return GPSInfos


paths, names, files = findAllFiles('E:\\Camera', 'jpg')
GPS = getGPSInfoBatch(files)
for item in GPS:
    print item

其实最开始我是先写的Python版本,因为写起来比较快,来试试可行性的。 在PIL库中也自带了获取EXIF信息的接口,所以写起来很方便。 后来发现有可行性,感觉还不错,才写了JAVA版的函数,继续进行了下去。

2.调用百度地图SDK

在获取到了影像的位置信息后,下面就是利用百度地图SDK在地图上进行显示了。 关于如何使用百度地图SDK,在前几篇博客中已经说了很多了,如果不会的话可以看看。 主要涉及到的就是Marker的显示。获取到各个影像的位置后,利用for循环即可。 限于篇幅,添加并绘制Marker的部分代码如下。完整项目代码见文末。

    private Marker addMarker(BaiduMap baiduMap, LatLng loc, @DrawableRes int ID) {
        BitmapDescriptor bd = BitmapDescriptorFactory.fromResource(ID);
        MarkerOptions markerOptions = new MarkerOptions().position(loc).icon(bd).zIndex(9);
        Marker marker = (Marker) baiduMap.addOverlay(markerOptions);
        return marker;
    }
	
	@Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        String filepath = data.getData().getPath();

        if (filepath.contains("external_files")) {
            filepath = Environment.getExternalStorageDirectory().toString() + filepath.substring(15);
        }
        
        // 由于这里我们需要的并不是某一个文件,而是文件夹
        // 所以可以让用户选择某个文件,我们识别出当前文件夹,并遍历图像
        String dirPath = filepath.substring(0, filepath.lastIndexOf('/'));
        File file = new File(dirPath);
        File[] subFile = file.listFiles();
        for (File temp : subFile) {
            LatLng point = getLocInfo(temp.getPath());
            if (point != null) {
                img_paths.add(temp.getPath());
                img_locs.add(point);
            } else {
                Log.e("error", "no location info");
            }
        }
        for (LatLng item : img_locs) {
            markerList.add(addMarker(baiduMap, item, R.drawable.marker_01));
        }

    }

这样,整个APP的大体功能就被实现了。演示效果如下。

总体而言效果还是不错的,但有时因为GPS本身定位不准等等问题,可以看到Marker显示的位置并不一定精确。 但这就不是我们这个博客所讨论的问题了。

3.其它细节问题

(1)应用权限问题

在调试过程中发现,可能会出现无法打开图片,打开图片就闪退的情况。遇到这种情况请先检查是否给了APP读写存储的权限。 如果没有,在给了权限以后应该就好了。高版本Android和以前不一样了,虽然在清单中申明了权限,但有时可能也不太好使。 新版本Android引入了动态权限管理机制,在APP需要权限时如果没有,则会弹出对话框让用户选择是否给予权限。 如果有兴趣可以搜索相关资料。这里为了代码的简洁就没有使用过多复杂的东西。

(2)Marker的InfoWindow

在之前介绍Marker的这篇博客中,没有说如何使用InfoWindow,但也比较简单。 直接新建一个InfoWindow对象,并在地图中show出来即可,示例代码如下。

// 新建InfoWindow对象
private InfoWindow infoWindow;

baiduMap.setOnMarkerClickListener(new BaiduMap.OnMarkerClickListener() {
            @Override
            public boolean onMarkerClick(final Marker marker) {
                // 创建InfoWindow的内容
				Button button = new Button(getApplicationContext());
                button.setBackgroundResource(R.drawable.popup);
                // 新建InfoWindow点击事件监听器
                InfoWindow.OnInfoWindowClickListener listener = null;

                // 具体指定点击InfoWindow后触发的操作
                listener = new InfoWindow.OnInfoWindowClickListener() {
                    @Override
                    public void onInfoWindowClick() {
                        passIMG2Sys(img_paths.get(index));
                        baiduMap.hideInfoWindow();
                    }
                };

                // 配置InfoWindow
                infoWindow = new InfoWindow(BitmapDescriptorFactory.fromView(button), ll, -47, listener);
                // 显示InfoWindow
                baiduMap.showInfoWindow(infoWindow);
                return false;
            }
        });

InfoWindow的点击事件不是必须的,如果你只是用它来显示一些信息的话。 最后别忘了showInfoWindow()这句,有时候你点Marker就是不显示InfoWindow,又没报错,很有可能就是漏了这句。

(3)Android遍历获取文件

Android遍历获取文件完全可以采用JAVA的办法,只是需要注意一些权限的问题。在JAVA中遍历获取文件的代码如下。

List<String> files = new ArrayList<>();
String dirPath = "dir";
File file = new File(dirPath);
File[] subFile = file.listFiles();
for (File temp : subFile) {
	files.add(temp.getPath());

File对象不仅仅可以针对具体文件,也可以针对一个目录,这点需要注意一下。

(4)传递文件路径问题

在高版本的Android系统中,对于URI的控制更为严格了。在以前直接Uri.fromFile(String filepath)即可。现在则直接会报错。 具体解决办法可以参考这篇博客,说的挺详细的。 唯一需要补充一点的就是provider标签要放在application标签内部,否则无法识别。 对于高版本的Android系统,传出图片文件路径的代码如下。

    public void passIMG2Sys(String filepath) {
        File img = new File(filepath);
        Intent intent = new Intent(Intent.ACTION_VIEW);
        //判断是否是AndroidN以及更高的版本
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            Uri contentUri = FileProvider.getUriForFile(
                    getApplicationContext(),
                    BuildConfig.APPLICATION_ID + ".fileProvider", img);
            intent.setDataAndType(contentUri, "image/*");
        } else {
            intent.setDataAndType(Uri.fromFile(img), "image/*");
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        }
        startActivity(intent);
    }

上述代码实现了传出一个图片文件路径,并开启一个Intent,用户可以选择打开这个图片文件的不同方式。

(5)标题栏按钮

在标题栏右边添加按钮,可以参考这个网页,写的简单易懂。

4.更多功能

由于时间有限,只是把想法做了最基本的实现。如果有时间还可以继续完善。 如可以将读取的照片以ListView方式展示,用户点击某个Item,地图就可以自动跳转到相应地方。 以及增加不同Marker间的距离统计功能,统计出这一路走来走过的距离等等很多有趣好玩的功能。 而且这些功能实现起来并不困难,基本都有对应的接口和函数。

最后,将项目的所有代码以及apk文件上传到了Github,点击查看。

本文作者原创,未经许可不得转载,谢谢配合

返回顶部