[Java] Thumbnail Image 만드는 코드

코드 정리하다가 블로그에 정리되지 않은 코드가 있어 정리해 봅니다. 일반적인 이미지를 작은 이미지, 즉 썸네일 이미지로 만들어 주는 코드입니다. 이미지의 종횡비 크기를 유지하며 썸네일 이미지를 만들어 줍니다. 썸네일 이미지 만드는 코드를 실제 프로젝트에 붙이기 위해 만들다 보니, 부가적인 내용이 덧붙여 있으니 참고하시기 바랍니다.

사용은 아래처럼 하면 됩니다. 즉, d:/a.jpg 파일을 썸네일 이미지를 만드는데, 가로 또는 세로 크기 중 하나를 256을 만듭니다.

reateThumbnail("d:/a.jpg", 256);

아래는 d:/a.jpg의 실제 이미지입니다. 이미지 크기는 920×600입니다.

위의 이미지가 위의 코드를 통해 아래와 같이 d:/a.thumbnail.jpg 파일로 만들어지고 크기는 256×166이 됩니다. 원본 이미지의 가로 크기가 세로보다 길다보니 가로를 256으로 만들어지게 되는 것입니다.

createThumbnail 함수는 아래와 같습니다.

private static boolean createThumbnail(String fileName, int maxSize) {
    try {
        int thumbnail_width = maxSize;
        int thumbnail_height = maxSize;

        File origin_file_name = new File(fileName);
		    
        String ext = getFileExt(fileName);
        String newFileName = fileName.replace("." + ext, ".thumbnail." + ext);
		    
        BufferedImage buffer_original_image = ImageIO.read(origin_file_name);
		    
        double imgWidth = buffer_original_image.getWidth();
        double imgHeight = buffer_original_image.getHeight();
		    
        if(imgWidth < imgHeight) {
            thumbnail_width = (int)((imgWidth / imgHeight) * maxSize);
        } else {
            thumbnail_height = (int)((imgHeight / imgWidth) * maxSize);
        }
		    
        int imgType = (buffer_original_image.getTransparency() == Transparency.OPAQUE) ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
        BufferedImage buffer_thumbnail_image = new BufferedImage(thumbnail_width, thumbnail_height, imgType);
        Graphics2D graphic = buffer_thumbnail_image.createGraphics();
		    
        graphic.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        graphic.drawImage(buffer_original_image, 0, 0, thumbnail_width, thumbnail_height, null);
		    
        if(ext.equalsIgnoreCase("jpg")) {
            writeJpeg(buffer_thumbnail_image, newFileName, 1.0f);
        } else {
            File thumb_file_name = new File(newFileName);
            ImageIO.write(buffer_thumbnail_image, ext.toLowerCase(), thumb_file_name);
        }
		    
        graphic.dispose();
    } catch (Exception e) {
        e.printStackTrace(System.err);
        return false;
    }
		
    return true;
}

위의 함수는 2개의 내부 함수를 호출하고 있는데, 해당되는 함수들은 아래와 같습니다.

private static String getFileExt(String fileName) { // "abc.txt" -> "txt", not ".txt"
    int i = fileName.lastIndexOf('.');
    int p = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));

    if (i > p) {
        return fileName.substring(i+1);
    }

    return null;
}
	 
private static void writeJpeg(BufferedImage image, String destFile, float quality) throws IOException {
    ImageWriter writer = null;
    FileImageOutputStream output = null;
		  
    try {
        writer = ImageIO.getImageWritersByFormatName("jpeg").next();
	
        ImageWriteParam param = writer.getDefaultWriteParam();
		    
        param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        param.setCompressionQuality(quality);
		    
        output = new FileImageOutputStream(new File(destFile));
        writer.setOutput(output);
		    
        IIOImage iioImage = new IIOImage(image, null, null);
        writer.write(null, iioImage, param);
    } catch (IOException ex) {
        throw ex;
    } finally {
        if (writer != null) {
            writer.dispose();
        }
		    
        if (output != null) {
            output.close();
        }
    }
}

단위 테스트로 만들어 놓은 코드인지라 최적화가 되어 있지 않으니, 살펴보시고 최적화 해 사용하시기 바랍니다.

실시간 미세먼지 측정

아두이노와 미세먼지 센서를 활용하여 사무실에서 측정한 실제 구동 화면입니다. 캡쳐 받은 이미지 아닙니다~ ^^ 측정 결과를 인터넷을 통해 서버에 전송하여 위처럼 웹에서 살펴볼 수 있도록 하였습니다. 사무실의 미세먼지가 제법 많군요. 오늘 실외 미세먼지가 나쁘지 않은 날인데도, 이렇다면 문제인데.. 공기청정기를 설치해야겠습니다.

옛날에 짬짬이 만들어 본 것을 사무실에 가져와 설치를 한 것인데요. 흔히 말하는 IoT입니다. 1um 크기인 극초미세먼지까지 측정해 줍니다. 흔히 극초미세전지가 건강에 훨씬 위험하지만 우리나라에서는 이 극초미세먼지의 측정값을 제공하지 않습니다.

아래는 실제 장비에 대한 사진입니다.

WebSocket과 Web에서의 Javascript를 통한 바이너리 데이터 통신

크아! 제목 한번 요상합니다. JavaScript가 클라이언트 뿐만 아니라 Java나 서버단(Node.js 등)에서도 활용되다 보니… 여튼, WebSoket과 웹에서의 Javascript를 통한 통신의 예로 Binary 데이터 타입을 주고 받는 코드를 정리합니다. 이 글은 텍스트 타입의 데이터를 주고 받는 [jetty를 이용한 WebSocket 서버 구현하기]라는 글을 기반으로 작성된 글입니다.

WebSocket을 위한 서버는 jetty를 활용하였습니다. 먼저 클라이언트가 서버 단에 바이너리 데이터가를 전달하는 코드는 다음과 같습니다.

var buffer = new ArrayBuffer(8);
var dataview = new DataView(buffer);

dataview.setInt32(0, 9438);
dataview.setFloat32(4, 3224.3224);
            
webSocket.send(buffer);

8 Bytes 크기의 데이터 덩어리를 만들고, 이 덩어리 안에 Int와 Float 타입에 대한 값으로 각각 9438과 3224.3224를 넣어서 웹소켓으로 전송하는 코드입니다. 이렇게 전송하면 서버가 받아야 하는데, 아래의 코드는 서버 측의 코드입니다.

@Override
public void onWebSocketBinary(byte[] payload, int offset, int len)
{
    if ((outbound != null) && (outbound.isOpen()))
    {
        InputStream is = new ByteArrayInputStream(payload);
        DataInputStream dis = new DataInputStream(is);
    		
        try {
            int v1 = dis.readInt();
            float v2 = dis.readFloat();
            double result = (double)v1 * (double)v2;
    			
            ByteBuffer bb = ByteBuffer.allocate(8);
            bb.putDouble(0, result);
            outbound.getRemote().sendBytes(bb);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

10번과 11번에서 클라이언트가 보낸 int와 float 값을 읽고, 엄청나게 복잡한 연산인 곱의 결과 값을 16번 코드에서 클라이언트로 전달하고 있는 코드입니다. 이제 아래는 다시 클라이언트가 서버가 계산한 소중한 결과를 받는 코드입니다.

webSocket.onmessage = function (message) {
    var blob = message.data;
    var fileReader = new FileReader();

    fileReader.onload = function (event) {
        var arrayBuffer = event.target.result;
        var dataview = new DataView(arrayBuffer);
        var answer = dataview.getFloat64(0);

        alert("Server> : " + answer);
    };

    fileReader.readAsArrayBuffer(blob);
};

서버로부터 받는 바이너리 데이터는 blob 형식으로 받습니다. 이 데이터는 FileReader를 통해 읽어 해석할 수 있습니다. 이상으로 웹소켓에서 바이너리 데이터를 주고 받는 코드에 대한 글을 정리한 글이였습니다.

울릉도 지역의 지적도와 건물통합데이터의 좌표계 문제

우리가 GIS를 활용해 무언가를 할 때 공간 데이터들 간의 좌표를 맞춰 레이어 단위로 중첩하여 하나의 지도를 구성한 뒤, 시각적 비교와 다양한 분석을 처리하게 됩니다. 이번에 경상북도 지역 중 내륙과 상당히 멀리 떨어져 있는 울릉도 지역에 대한 좌표계에 상당한 문제가 있다는 것을 파악했습니다. 독도는 더 심각!! -_-; 경상북도의 울릉도를 제외한 다른 지역은 그 좌표계가 매우 정확히 일치하는 것을 볼 때 울릉도 지역에 대한 좌표 불일치는 데이터 구축단계에서 원점 처리에 있어 상당한 문제가 있고, 근본적인 해결을 국가공간정보포털의 운영 주체인 정부 차원에서 조속히 해결해줘야 할 부분으로 생각합니다. 아래는 GRS80 UTM-K 좌표계로 변환하여 지적도와 행정경계를 중첩한 지도입니다.

앞서 언급하였듯이, 을릉도 이외의 다른 경상북도 지역은 GRS80 UTM-K 좌표계로 변환하여 중첩했을 경우 정확히 일치합니다. 제대로 구축된 데이터를 구할 수 있다면 좋았겠지만, 그럴 수 없는 상황에서 이처럼 구축 단계에서 근본적으로 잘못 되어진 공간 데이터의 좌표계를 일치시키는 방법으로 Georeferencing 기능을 활용하는 방법밖에 없었고, Georeferencing 기능을 수행할 수 있는 GDAL의 ogr2ogr.exe 콘솔 프로그램을 사용하고자 하였습니다. 그런데 이 프로그램은 콘솔인지라, 시각적으로 GCP를 잡을 수 없다는 문제가 있습니다. QGIS을 활용해 보려고 하였으나, 관련된 기능에 오류가 발생합니다. QGIS 역시 GDAL의 기능을 그대로 활용하면서 단지 시각적으로 GCP를 잡는 기능을 제공하는 것으로 예상됩니다. 여튼.. ogr2ogr을 통한 Georeferencing 기능을 위해 시각적으로 GCP를 효과적을 취득할 수 있는 툴을 별도로 개발하였고 아래의 화면과 같습니다.

위의 프로그램을 통해 19개의 GCP 좌표를 입력하여 Georeferencing 기능을 수행하여 얻은 결과는 아래와 같습니다.

그러나 이러한 수치지도에 대한 Georeferencing 방식을 활용하여 좌표계 일치하는 방법은 최선의 방법이 아닙니다. Georeferencing은 원래 목적은 항공영상과 같은 이미지를 수치지도에 맵핑시키기 위한 방법입니다. 결과적으로 수치지도를 또 다른 수치지도와 맵핑시키는 Georeferencing 방식은 그 결과면에서 정확도가 매우 떨어집니다. 근본적인 방법은 공간 데이터 구축을 처음부터 올바르게 해서 제공해야 한다는 것입니다. 조속히 관련 기관에서 해결해 주기를 바랄 뿐입니다.