심도 있는 훈련 을 통해 극적 으로 육체 변화 의 시간축 을 보기 쉽다

개시하다


'셀카(육체)'는 대다수의 근육 단련을 좋아하는 사람들이 습관화한 것이다.훈련 후 쥐나는 육체를 찍기 위해 나중에 보는 것이 가장 행복한 순간이다.그리고 찍은 이미지를 타임라인 같은 애니메이션으로 표현하면 근육의 성장은 더욱 쉽게 파악할 수 있겠지!
이 기사는 심도 있는 훈련을 통해 육체를 쉽게 볼 수 있는 시간선을 극적으로 묘사했다.

일단 결과부터.



2017/12~2020/3의 신체변화
※ 데이터 크기 때문에 이미지를 재단 및 압축하고 있습니다.

카탈로그


  • 1. 수동 보정
  • 1-1.직접 표시

  • 1-2.고정 위치
  • 1-2-1.젖꼭지 배꼽 좌표 부여 도구
  • 1-2-2.영상 제작
  • 2. 딥러닝 자동 수정 사용
  • 2-1.초대 데이터 만들기
  • 2-2.배우다
  • 2-3.알 수 없는 이미지에 적용

  • 2-4.뒷처리
  • 2-4-1.각 픽셀의 출력 값이 한도값 이하에서 삭제됨
  • 2-4-2.객체 분할
  • 2-4-3.만약 집단이 네 개 이상이라면, 면적이 큰 순서대로 세 개를 선택하고 나머지 부분을 버려라
  • 2-4-4.각 묶음의 중심을 확정하다
  • 2-4-5.각 묶음의 중심의 x 좌표는 작은 순서로 다시 배열한다(오른쪽 젖꼭지→배꼽→왼쪽 젖꼭지)
  • 2-5.결실
  • 총결산
  • 개요


    촬영한 이미지에서 타임라인을 만들었다.그러나 의도상 간 편차가 있어 수작업으로 수정해 매끄러운 타임라인을 만들었다.또 수작업으로 인한 번거로움을 줄이기 위해 딥러닝을 이용해 자동으로 수정했다.

    1. 수동 보정


    1-1.직접 표시


    한 마디로 하면 먼저 원래의 이미지를 연속적으로 전환할 수 있는 시간선을 만들어야 한다.
    타임라인 제작 코드(일부)
    
    # opencvでもで動画は作れますが、
    # google colabの環境で、discord上で再生できるmp4ファイルを作るためには、
    # skvideoを使うやり方が楽ちんでした。
    import skvideo.io
    
    def create_video(imgs, out_video_path, size_wh):
      video = []
      vid_out = skvideo.io.FFmpegWriter(out_video_path,
          inputdict={
              "-r": "10"
          },
          outputdict={
              "-r": "10"
          })
      
      for img in imgs:
        img = cv2.resize(img, size_wh)
        vid_out.writeFrame(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
    
      vid_out.close()
    
    imgs = load_images("images_dir")
    create_video(imgs,  "video.mp4", (w,h))
    
    결과는 다음과 같다.
    ezgif.com-crop.gif
    편차를 의식하면 자신의 아이(육체)에 집중할 수 없다.

    1-2.고정 위치


    나는 이 편차를 가볍게 없앨 방법을 강구하고 싶다.몸의 어딘가에 기준점을 두고 고정시킨다면 0.1초 정도면'젖꼭지'와'배꼽'을 풀 수 있다.
    다음은 젖꼭지와 배꼽을 어떻게 고정시키는지 설명한다.

    1-2-1.젖꼭지 배꼽 좌표 부여 도구


    우선 젖꼭지와 배꼽에 UV 좌표를 부여하는 도구를 만든다.cvat 등을 사용해도 가능하지만 그 전의 시간과 도구를 사용해 직접 만든 시간에 따라 추산하면 이번에는 스스로 만드는 것이 빠르다는 결론을 내렸다.
    도구의 규격은 폴더를 지정하면 그림이 연속적으로 표시되며, 각 그림에 젖꼭지와 배꼽 3점을 누르면 클릭한 좌표를 csv 파일로 출력합니다.GUI는 트위터를 사용했다.
    ※ 후술에서 사용한 딥러닝용 아날로그 데이터의 경우 이미지와 아날로그 데이터가 1:1이면 잘 처리됩니다.하지만 이번에는 간단하게 끝내기 위해 만들지 않았다.

    1-2-2.영상 제작


    젖꼭지와 배꼽의 위치는 첫 번째 이미지에 따라 모방하여 고정시킨다.
    수정판 타임라인 제작 코드(일부)
    def p3affine_img(img, src_p, dst_p):
        h, w, ch = img.shape
        pts1 = np.float32([src_p[0],src_p[1],src_p[2]])
        pts2 = np.float32([dst_p[0],dst_p[1],dst_p[2]])
        M = cv2.getAffineTransform(pts1,pts2)
        dst = cv2.warpAffine(img,M,(h, w))
        return dst
    
    
    df = read_annotationd() # 省略
    
    imgs = []
    src_p = None
    for index, row in df.iterrows():
        img = cv2.imread(row.file)
        dst_p = [ [row.p1x, row.p1y], # 左乳首
                  [row.p2x, row.p2y], # 右乳首
                  [row.p3x, row.p3y]] # おへそ
        if src_p is None:
          src_p = dst_p
        else:
          img = p3affine_img(img, dst_p, src_p)
        
        imgs.append(img)
    
    write_video(imgs) # 省略
    
    
    결과는 다음과 같다.
    ezgif.com-optimize.gif
    기대했던 것과 같은 타임라인을 만들어 기쁘다.아니에요!
    이번에 좌표에 부여된 장수는 120장(기간 2019/9~2020/3)이다.하지만 수중에는 2017/12부터 촬영된 좌표가 추가된 이미지가 281장 남아 있다.수십 년이 지나면 근육 단련을 해야 한다는 것은 수십 년 안에 좌표를 계속 부여해야 한다는 것이다.상상만 해도 코르티솔이 분비돼 아이보리색에 빠진다.이 문제를 해결하기 위해서 나는 당분을 보충했다.
    맞다, 헬스장 가자.

    2. 딥러닝 자동 수정 사용


    '젖꼭지'와'배꼽'의 위치를 추측하는 모형을 만들다.만약 이것이 실현된다면 나머지는 아까와 마찬가지로 모방 변환만 할 것이다.젖꼭지와 배꼽을 검출하려면 분리 임무로 접근해야 한다.자세 추측 같은 포인트 체크는 합리적이지만 개인이 임무를 나눠본 경험이 많아 선택했다.
    데이터 세트는 다음과 같습니다.2019/9~2010/3에 좌표가 추가되어 이미지를 훈련하고 검증하는 데 사용되며 남은 기간 동안 자동으로 좌표를 계산합니다.

    2-1.초대 데이터 만들기


    '우유두','좌유두','배꼽','배경'등 네 가지 유형으로 나눠 문제를 풀 수도 있는데, 이번에는'우유두, 좌유두, 배꼽','배경'두 가지 유형으로 나뉜다.3점만 검출되면 규칙에 따라 분류하기 쉽다고 생각했기 때문이다.
    그럼 바로 마스크 이미지를 만들겠습니다.방금 생성한 좌표 데이터에 따라 좌표점을 확대하여 1로 채웁니다.이외에 배경이기 때문에 0으로 설정합니다.
    for index, row in df.iterrows():
      file = row.file
      mask = np.zeros((img_h, img_w), dtype=np.uint8)
      mask = cv2.circle(mask,(row.p1x, row.p1y,), 15, (1), -1)
      mask = cv2.circle(mask,(row.p2x, row.p2y,), 15, (1), -1)
      mask = cv2.circle(mask,(row.p3x, row.p3y,), 15, (1), -1)
      save_img(mask, row.file) # 省略
    
    시각적 데이터(1은 흰색, 0은 검은색)는 다음과 같다.

    이것들을 육체 이미지와 한 쌍으로 만들어라.

    2-2.배우다


    학습에서는 DeepLabv3(torchvision)을 사용합니다.120장의 이미지를 훈련하고 검증하기 위해 8:2로 나뉘었다.비록 수량은 매우 적지만, 아래의 이유에 따라 데이터 확장을 진행하지 않았다.
  • 육체 이미지는 같은 카메라로 촬영
  • 카메라 자세와 조명 환경이 이미지 사이에서 어느 정도 일치
  • 하지만 나는 본래 데이터의 확장이 비교적 좋다고 생각한다.
    데이터 세트 클래스, 학습 관련 함수
    class MaskDataset(Dataset):
      def __init__(self, imgs_dir, masks_dir, scale=1, transforms=None):
        self.imgs_dir = imgs_dir
        self.masks_dir = masks_dir
    
        self.imgs = list(sorted(glob.glob(os.path.join(imgs_dir, "*.jpg"))))
        self.msks = list(sorted(glob.glob(os.path.join(masks_dir, "*.png"))))
        self.transforms = transforms
        self.scale = scale
    
      def __len__(self):
          return len(self.imgs_dir)
    
      @classmethod
      def preprocess(cls, pil_img, scale):
    
        # グレースケールにしても良さそうだけど、めんどうだからしない
        # pil_img = pil_img.convert("L") 
    
        w, h = pil_img.size
        newW, newH = int(scale * w), int(scale * h)
        pil_img = pil_img.resize((newW, newH))
    
        img_nd = np.array(pil_img)
    
        if len(img_nd.shape) == 2:
          img_nd = np.expand_dims(img_nd, axis=2)
    
        # HWC to CHW
        img_trans = img_nd.transpose((2, 0, 1))
        if img_trans.max() > 1:
            img_trans = img_trans / 255
    
        return img_trans
    
      def __getitem__(self, i):
          
        mask_file = self.msks[i]
        img_file = self.imgs[i]
    
        mask = Image.open(mask_file)
        img = Image.open(img_file)
    
        img = self.preprocess(img, self.scale)
        mask = self.preprocess(mask, self.scale)
    
        item = {"image": torch.from_numpy(img), "mask": torch.from_numpy(mask)}
        if self.transforms:
          item = self.transforms(item)
        return item
    
    from torchvision.models.segmentation.deeplabv3 import DeepLabHead
    
    def create_deeplabv3(num_classes):
      model = models.segmentation.deeplabv3_resnet101(pretrained=True, progress=True)
      model.classifier = DeepLabHead(2048, num_classes)
    
      # グレースケールにしても良さそうだけど、めんどうだからしない
      #model.backbone.conv1 = nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3, bias=False)
    
      return model
    
    def train_model(model, criterion, optimizer, dataloaders, device, num_epochs=25, print_freq=1):
      since = time.time()
    
      best_model_wts = copy.deepcopy(model.state_dict())
      best_loss = 1e15
    
      for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch+1, num_epochs))
        print('-' * 10)
    
        loss_history = {"train": [], "val": []}
        
        for phase in ["train", "val"]:
            
          if phase == "train":
            model.train()
          else:
            model.eval()
    
          for sample in tqdm(iter(dataloaders[phase])):
            imgs = sample["image"].to(device, dtype=torch.float)
            msks = sample["mask"].to(device, dtype=torch.float)
    
            optimizer.zero_grad()
    
            with torch.set_grad_enabled(phase == "train"):
              outputs = model(imgs)
              loss = criterion(outputs["out"], msks)
    
              if phase == "train":
                loss.backward()
                optimizer.step()
    
          epoch_loss = np.float(loss.data)
          if (epoch + 1) % print_freq == 0:
            print("Epoch: [%d/%d], Loss: %.4f" %(epoch+1, num_epochs, epoch_loss))
            loss_history[phase].append(epoch_loss)
    
          # deep copy the model
          if phase == "val" and epoch_loss < best_loss:
            best_loss = epoch_loss
            best_model_wts = copy.deepcopy(model.state_dict())
    
      time_elapsed = time.time() - since
      print("Training complete in {:.0f}m {:.0f}s".format(time_elapsed // 60, time_elapsed % 60))
      print("Best val Acc: {:4f}".format(best_loss))
    
      model.load_state_dict(best_model_wts)
      
      return model, loss_history
    
    학습을 실행하다
    
    dataset = MaskDataset("images_dir", "masks_dir", 0.5, transforms=None)
    
    # 訓練用と検証用に分ける
    val_percent= 0.2
    batch_size=4
    n_val = int(len(dataset) * val_percent)
    n_train = len(dataset) - n_val
    train, val = random_split(dataset, [n_train, n_val])
    train_loader = DataLoader(train, batch_size=batch_size, shuffle=True, num_workers=8, pin_memory=True, drop_last=True )
    val_loader = DataLoader(val, batch_size=batch_size, shuffle=False, num_workers=8, pin_memory=True, drop_last=True )
    
    dataloaders = {"train": train_loader, "val": val_loader}
    
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    
    # BCEWithLogitsLossを使う際に2値分類だと1と指定
    num_classes = 1 
    
    model = create_deeplabv3(num_classes)
    
    # pre trained用
    #model.load_state_dict(torch.load("model.pth"))
    
    model.to(device)
    
    # 背景が圧倒的に多いのでpos_weightで調整する
    criterion = nn.BCEWithLogitsLoss(pos_weight=torch.tensor(10000.0).to(device))
    
    params = [p for p in model.parameters() if p.requires_grad]
    
    optimizer = optim.Adam(params)
    
    total_epoch = 50
    
    model, loss_dict = train_model(model, criterion, optimizer, dataloaders, device, total_epoch)
    
    이번에 50분 정도 돌았더니 공부가 좀 뜸해졌다.

    2-3.알 수 없는 이미지에 적용


    결과는 대체로 양호하고 3점은 좋은 반응을 보였지만 간혹 다음과 같은 결과(열도 표현)도 있었다.

    물론 왼쪽 젖꼭지가 두 개 없기 때문에 가장 오른쪽 위쪽의 작은 점은 False Positive입니다.
    그나저나 False Negative는 없습니다.

    2-4.뒷처리


    방금의 추론 결과에 근거하여 후처리 중에 다음과 같은 처리를 진행한다.
  • 각 픽셀의 출력 값이 임계값 이하인 픽셀을 잘라낸다
  • 분할 대상
  • 묶음이 4개 이상인 경우 면적이 큰 순서로 3개를 선택하고 나머지 부분
  • 을 버린다.
  • 클러스터당 무게 중심 결정
  • 각 묶음의 중심의 x 좌표를 작은 순서로 다시 배열(오른쪽 젖꼭지→배꼽→왼쪽 젖꼭지)
  • 2-4-1.각 픽셀의 출력 값이 한도값 이하에서 삭제됨


    다음 처리를 위해 명확한 정밀도를 가진 픽셀을 제외한 다른 픽셀은 모두 차단할 수 있다.이번 한도값은 경험상 0.995이다.

    2-4-2.객체 분할


    대상 분할 (그룹으로 나뉘어 있음) cv2.connected Components를 사용합니다.자세한 내용은 OpenCV-cannectedComponents를 통해 연결 성분을 표시하는 방법-pynote를 참조하십시오.

    2-4-3.만약 집단이 네 개 이상이라면, 면적이 큰 순서대로 세 개를 선택하고 나머지 부분을 버려라


    사례를 보면 페이스 포지션은 젖꼭지와 배꼽을 제외한 면적이 작다.따라서 면적이 큰 3개를 선택한다.사실 나는 이런 처리가 그다지 견고하지 않다고 생각하지만, 이번에는 잘 처리되어 채용되었다.

    2-4-4.각 묶음의 중심을 확정하다


    각 집단의 중심을 구하는 것은 cv2다.moments를 사용합니다.자세한 내용은 Python+OpenCV를 통해 중심 구하기 - CV 이미지 해석 입문를 참조하십시오.

    2-4-5.각 묶음의 중심의 x 좌표는 작은 순서로 다시 배열한다(오른쪽 젖꼭지→배꼽→왼쪽 젖꼭지)


    모방 변환 시 대응점이 필요하기 때문에 이미지 간에 젖꼭지와 배꼽의 좌표 순서를 통일해야 한다.이번 영상은 모두 직립으로 촬영된 것으로 가로축 방향에서 젖꼭지→배꼽→젖꼭지가 틀림없이 나타날 것이기 때문에 단순히 x좌표로 다시 배열했다.
    추론시
    
    #マスクから3点検出
    def triangle_pt(heatmask, thresh=0.995):
      mask = heatmask.copy()
    
      # 2-4-1.各ピクセルの出力値が閾値以下のものは切り捨てる
      mask[mask>thresh] = 255
      mask[mask<=thresh] = 0
      mask = mask.astype(np.uint8)
      # 2-4-2.オブジェクト分割する
      nlabels, labels = cv2.connectedComponents(mask)
    
      pt = []
      if nlabels != 4:
    
        # 少ない場合は、何もしない
        # 本当は閾値さげてやりたいけど、めんどいので
        if nlabels < 4:
          return None
        
        # 2-4-3.クラスタが4つ以上の場合は、面積の大きい順に3つ選択し、残りを破棄する
        elif nlabels > 4:
          sum_px = []
          for i in range(1, nlabels):
            sum_px.append((labels==i).sum())
          # 背景分+1する
          indices = [ x+1 for x in np.argsort(-np.array(sum_px))[:3]]
    
      else:
        indices = [x for x in range(1, nlabels)]
    
      # 2-4-4.各クラスタの重心を求める
      for i in indices:
        base = np.zeros_like(mask, dtype=np.uint8)
        base[labels==i] = 255
        mu = cv2.moments(base, False)
        x,y= int(mu["m10"]/mu["m00"]) , int(mu["m01"]/mu["m00"])
        pt.append([x,y])
    
      # 2-4-5.各クラスタの重心のx座標が小さい順に並べ替える(右乳首→おへそ→左乳首)
      sort_key = lambda v: v[0]
      pt.sort(key=sort_key)
      return np.array(pt)
    
    
    def correct_img(model, device, in_dir, out_dir, 
                    draw_heatmap=True, draw_triangle=True, correct=True):
    
      imgs = []
    
      base_3p = None
      model.eval()
      with torch.no_grad():
        imglist = sorted(glob.glob(os.path.join(in_dir, "*.jpg")))
        
        for idx, img_path in enumerate(imglist):
    
          # めんどいのでバッチサイズ1
          full_img = Image.open(img_path)
          img = torch.from_numpy(BasicDataset.preprocess(full_img, 0.5))
          img = img.unsqueeze(0)
          img = img.to(device=device, dtype=torch.float32)
    
          output = model(img)["out"]
          probs = torch.sigmoid(output)
          probs = probs.squeeze(0)
    
          tf = transforms.Compose(
                    [
                        transforms.ToPILImage(),
                        transforms.Resize(full_img.size[0]),
                        transforms.ToTensor()
                    ]
                )
          
          probs = tf(probs.cpu())
          full_mask = probs.squeeze().cpu().numpy()
    
          full_img = np.asarray(full_img).astype(np.uint8)
          full_img = cv2.cvtColor(full_img, cv2.COLOR_RGB2BGR)
    
          # 三角形
          triangle = triangle_pt(full_mask)
          if draw_triangle and triangle is not None:
            cv2.drawContours(full_img, [triangle], 0, (0, 0, 255), 5)
    
          # ヒートマップ
          if draw_heatmap:
            full_mask = (full_mask*255).astype(np.uint8)
            jet = cv2.applyColorMap(full_mask, cv2.COLORMAP_JET)
    
            alpha = 0.7
            full_img = cv2.addWeighted(full_img, alpha, jet, 1 - alpha, 0)
    
          # アフィン変換
          if correct:
            if base_3p is None and triangle is not None:
              base_3p = triangle
            elif triangle is not None:
              full_img = p3affine_img(full_img, triangle, base_3p)
    
          if out_dir is not None:
            cv2.imwrite(os.path.join(out_dir, os.path.basename(img_path)), full_img)
    
          imgs.append(full_img)
    
      return imgs
    
    imgs = correct_img(model, device,
                       "images_dir", None,
                        draw_heatmap=False, draw_triangle=False, correct=True)
    
    

    2-5.결실


    수정 전 타임라인은 다음과 같습니다.

    수정된 타임라인은 다음과 같습니다.
    ezgif.com-optimize (2).gif

    총결산


    깊이 있는 훈련을 통해 젖꼭지와 배꼽을 검측하고 자동으로 이미지 수정을 하여 극적으로 시간축을 쉽게 볼 수 있다.이렇게 되면 훈련에 대한 열정이 더욱 높아진다.
    물론, ** "이런 비심도 CV는 못하죠?"그렇게 생각하는 사람도 있지만 규칙을 생각할 시간이 있으면 바벨을 들 것 같아서 힘으로 해결했다.
    좌표 추가 도구를 제외한 모든 개발은 구글 colab에서 진행되었습니다. 3150 응!
    과제로 삼다
  • 남의 육체로 잘 될 수 있을지 (뭐, 공부를 시키면)
  • 전체가 커질 때 대응하지 않음(젖꼭지, 배꼽 이외의 기준점 필요)
  • 그림자 제거
  • 앱 발표(이거 하고 싶어요!!)
  • 등, 하지만 피질알코올이 분비되기 때문에 딱딱한 일에 너무 신경 쓰지 마세요!
    그럼, 즐거운 근육 단련 생활!

    좋은 웹페이지 즐겨찾기